Enhance: Implement exhaustive background caching and recursive data loading

- Implemented aggressive room-wide background caching engine in LauncherBackgroundSync.svelte.
- Added inc_file_li and inc_all_file_li support to Event and Event Location object loaders for total room coverage.
- Switched to proven V1 download path (/hosted_file/) to resolve V3 CRUD binary delivery issues.
- Optimized Electron bridge with download locking and flat-hash storage matching legacy 'perfect' logic.
- Standardized all Electron IPC methods and parameters to snake_case.
- Added visual sync progress indicator for room caching status.
This commit is contained in:
Scott Idem
2026-01-23 16:30:03 -05:00
parent 683ea0394d
commit dc38c2c10c
10 changed files with 236 additions and 195 deletions

View File

@@ -128,7 +128,7 @@ export async function load({ fetch, params, parent, route, url }) {
is_native = true;
if (log_lvl) console.log('ROOT LOAD: Detected Aether Native Bridge. Requesting device config...');
try {
const native_device_config = await (window as any).aetherNative.getDeviceConfig();
const native_device_config = await (window as any).aetherNative.get_device_config();
if (native_device_config) {
if (log_lvl) console.log('ROOT LOAD: Native device config received:', native_device_config);
// Map native device config to the expected result structure

View File

@@ -0,0 +1,21 @@
<script lang="ts">
/**
* events/[event_id]/(launcher)/+layout.svelte
* Root layout for the launcher area.
* Ensures background sync runs globally regardless of active tab.
*/
import { ae_loc } from '$lib/stores/ae_stores';
import LauncherBackgroundSync from './launcher_background_sync.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<!-- Background Sync Process (Invisible) -->
<LauncherBackgroundSync log_lvl={1} />
<!-- Render the rest of the launcher UI -->
{@render children?.()}

View File

@@ -184,7 +184,8 @@
// console.log('GET DEVICE INFO!!! GET DEVICE INFO!!! GET DEVICE INFO!!!');
let get_device_info_promise = get_device_info({ event_device_id: event_device_id });
// Using standardized relay method
let get_device_info_promise = get_device_info();
get_device_info_promise.then(function (result) {
// console.log('GOT DEVICE INFO!!! GOT DEVICE INFO!!! GOT DEVICE INFO!!!');
console.log(get_device_info_promise);

View File

@@ -51,7 +51,7 @@ export async function load({ params, parent, url }) {
for_obj_type: 'event_location',
for_obj_id: event_location_id,
inc_file_li: true, // Only include files directly under the session?
inc_all_file_li: false, // Also include files under presentations and presenters as well?
inc_all_file_li: true, // Also include files under presentations and presenters as well?
inc_presentation_li: true,
inc_presenter_li: true,
enabled: 'enabled',

View File

@@ -1,126 +1,149 @@
<script lang="ts">
/**
* launcher_background_sync.svelte
* Handles background pre-caching of event files when running in Native mode.
* Uses configurable timers from the device configuration.
* Exhaustive Background Pre-Caching Engine.
* Scans Sessions, Presentations, and Presenters to ensure 100% of room files are cached.
*/
import { onMount, untrack, onDestroy } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_slct } from '$lib/stores/ae_events_stores';
import { db_events } from '$lib/ae_events/db_events';
import * as native from '$lib/electron/electron_relay';
let { log_lvl = 0 } = $props();
let { log_lvl = 1 } = $props();
let currently_syncing: string | null = $state(null);
let sync_results: Record<string, 'success' | 'error' | 'pending'> = $state({});
let sync_interval: any = null;
let sync_stats = $state({ total: 0, cached: 0, missing: 0 });
let main_sync_interval: any = null;
let is_syncing = false;
// Trigger initial sync and setup interval
onMount(() => {
if (!$ae_loc.is_native) return;
// 1. Initial Immediate Sync
setTimeout(() => {
if (log_lvl) console.log('Sync: Triggering immediate startup sync cycle.');
const files = $events_slct.event_file_obj_li;
if (files && files.length > 0) {
const ids = files.map((f: any) => f.event_file_id);
process_sync_queue(ids);
}
}, 1000);
if (log_lvl) console.log('Sync: Starting exhaustive background caching engine.');
// 2. Setup Loop Timer
// Default to 60 seconds if not specified in native_device config
const loop_period = $ae_loc.native_device?.check_event_loop_period || 60000;
if (log_lvl) console.log(`Sync: Starting background loop with period: ${loop_period}ms`);
// Setup persistent loop (Check every 10s for new data/missing files)
const sync_period = 10000;
sync_interval = setInterval(() => {
const files = $events_slct.event_file_obj_li;
if (files && files.length > 0) {
const ids = files.map((f: any) => f.event_file_id);
process_sync_queue(ids);
main_sync_interval = setInterval(() => {
if (!is_syncing) {
run_sync_cycle();
}
}, loop_period);
}, sync_period);
// Immediate first run
run_sync_cycle();
});
onDestroy(() => {
if (sync_interval) clearInterval(sync_interval);
});
// Reactive Watch: Also trigger if the file list changes significantly
$effect(() => {
if (!$ae_loc.is_native) return;
const files = $events_slct.event_file_obj_li;
if (!files || files.length === 0) return;
untrack(() => {
const file_ids = files.map((f: any) => f.event_file_id);
// Only process if we haven't checked these specific IDs recently
process_sync_queue(file_ids);
});
if (main_sync_interval) clearInterval(main_sync_interval);
});
/**
* Iterates through the file list and downloads missing files.
* Primary Sync Cycle
* Recursively identifies ALL files in the room hierarchy.
*/
async function process_sync_queue(ids: string[]) {
const cacheRoot = $ae_loc.local_file_cache_path;
if (!cacheRoot) {
console.warn('Sync: local_file_cache_path not set. Skipping sync.');
return;
}
async function run_sync_cycle() {
const location_id = $events_slct.event_location_id;
const cache_root = $ae_loc.local_file_cache_path;
if (log_lvl) console.log(`Sync: Processing queue of ${ids.length} files...`);
if (!location_id || !cache_root) return;
for (const id of ids) {
// Skip if already successfully cached in this session to avoid redundant IPC calls
if (sync_results[id] === 'success') continue;
is_syncing = true;
if (log_lvl > 1) console.log(`Sync Engine: Scanning room ${location_id}...`);
try {
const file_obj = await db_events.file.get(id);
if (!file_obj || !file_obj.hash_sha256) continue;
try {
// 1. Gather all IDs in this room's hierarchy
const sessions = await db_events.session.where('event_location_id').equals(location_id).toArray();
const session_ids = sessions.map(s => s.event_session_id);
if (session_ids.length === 0) return;
const presentations = await db_events.presentation.where('event_session_id').anyOf(session_ids).toArray();
const presentation_ids = presentations.map(p => p.event_presentation_id);
const presenters = await db_events.presenter.where('event_session_id').anyOf(session_ids).toArray();
const presenter_ids = presenters.map(p => p.event_presenter_id);
// 2. Collect every ID that could have a file attached
const all_for_ids = [...session_ids, ...presentation_ids, ...presenter_ids, $events_slct.event_id];
// 3. Find ALL files linked to these IDs
const files = await db_events.file.where('for_id').anyOf(all_for_ids).toArray();
sync_stats.total = files.length;
let cached_count = 0;
let missing_count = 0;
if (log_lvl > 1) console.log(`Sync Engine: Found ${files.length} potential files for this room.`);
// 4. Verify and Download
for (const file_obj of files) {
if (!file_obj.hash_sha256) continue;
// Fast-track if already confirmed this session
if (sync_results[file_obj.event_file_id] === 'success') {
cached_count++;
continue;
}
const exists = await native.check_hash_file_cache({
cacheRoot,
cache_root,
hash: file_obj.hash_sha256
});
if (exists) {
sync_results[id] = 'success';
sync_results[file_obj.event_file_id] = 'success';
cached_count++;
continue;
}
if (log_lvl) console.log(`Sync: Downloading missing file: ${file_obj.filename}`);
// 5. Download missing file
missing_count++;
if (log_lvl) console.log(`Sync: Pulling missing file -> ${file_obj.filename}`);
currently_syncing = file_obj.filename;
const url = `${$ae_api.base_url}/hosted_file/${file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(file_obj.filename)}`;
const url = `${$ae_api.base_url}/v3/crud/hosted_file/${file_obj.hosted_file_id}/download`;
const result = await native.download_to_cache({
url,
cacheRoot,
cache_root,
hash: file_obj.hash_sha256,
apiKey: $ae_api.api_secret_key
api_key: $ae_api.api_secret_key,
account_id: $ae_api.account_id
});
if (result.success) {
sync_results[id] = 'success';
if (log_lvl) console.log(`Sync: Successfully cached ${file_obj.filename}`);
} else {
console.error(`Sync: Failed to cache ${file_obj.filename}:`, result.error);
sync_results[id] = 'error';
sync_results[file_obj.event_file_id] = 'success';
}
} catch (err) {
console.error(`Sync: Error processing file ${id}:`, err);
}
sync_stats.cached = cached_count;
sync_stats.missing = missing_count;
if (log_lvl && missing_count > 0) {
console.log(`Sync Engine: Room scan complete. Cached: ${cached_count}, Downloaded: ${missing_count}`);
}
} catch (err) {
console.error('Sync Engine: Critical failure in room scan:', err);
} finally {
currently_syncing = null;
is_syncing = false;
}
currently_syncing = null;
}
</script>
{#if currently_syncing && log_lvl}
<div class="fixed bottom-4 right-4 bg-black/80 text-white p-2 rounded-lg text-[10px] z-[9999] border border-primary-500 animate-pulse">
<span class="fas fa-sync fa-spin mr-2"></span>
Pre-Caching: {currently_syncing}
<div class="fixed bottom-4 right-4 bg-black/80 text-white p-2 rounded-lg text-[10px] z-[9999] border border-primary-500 animate-pulse shadow-2xl">
<div class="flex items-center gap-2">
<span class="fas fa-sync fa-spin text-primary-500"></span>
<div class="flex flex-col">
<span class="font-bold">Syncing Room Files...</span>
<span class="opacity-70 truncate max-w-48">{currently_syncing}</span>
<span class="text-[8px] mt-1 text-primary-300">Room Status: {sync_stats.cached}/{sync_stats.total} Ready</span>
</div>
</div>
</div>
{/if}
{/if}

View File

@@ -90,30 +90,33 @@
// 1. NATIVE MODE (Electron)
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
const cacheRoot = $ae_loc.local_file_cache_path;
const tempRoot = $ae_loc.host_file_temp_path;
const cache_root = $ae_loc.local_file_cache_path;
const temp_root = $ae_loc.host_file_temp_path;
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Checking local cache...';
const exists = await native.check_hash_file_cache({ cacheRoot, hash: event_file_obj.hash_sha256 });
const exists = await native.check_hash_file_cache({ cache_root, hash: event_file_obj.hash_sha256 });
if (!exists) {
open_file_status = 'downloading_file';
open_file_status_message = 'Downloading file to cache...';
const url = `${$ae_api.base_url}/v3/crud/hosted_file/${event_file_obj.hosted_file_id}/download`;
const dlResult = await native.download_to_cache({
// Use the PROVEN endpoint path from api.ts that is known to work in Default Mode.
const url = `${$ae_api.base_url}/hosted_file/${event_file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(event_file_obj.filename)}`;
const dl_result = await native.download_to_cache({
url,
cacheRoot,
cache_root,
hash: event_file_obj.hash_sha256,
apiKey: $ae_api.api_secret_key
api_key: $ae_api.api_secret_key,
account_id: $ae_api.account_id
});
if (!dlResult.success) {
if (!dl_result.success) {
open_file_status = 'error';
open_file_status_message = `Download failed: ${dlResult.error}`;
open_file_status_message = `Download failed: ${dl_result.error}`;
setTimeout(() => open_file_clicked = false, 5000);
return;
}
@@ -122,16 +125,16 @@
open_file_status = 'opening_file';
open_file_status_message = 'Opening Application';
const launchResult = await native.launch_from_cache({
cacheRoot,
const launch_result = await native.launch_from_cache({
cache_root,
hash: event_file_obj.hash_sha256,
tempRoot,
temp_root,
filename: event_file_obj.filename
});
if (!launchResult.success) {
if (!launch_result.success) {
open_file_status = 'error';
open_file_status_message = `Failed to open: ${launchResult.error}`;
open_file_status_message = `Failed to open: ${launch_result.error}`;
}
setTimeout(() => open_file_clicked = false, 5000);

View File

@@ -46,7 +46,7 @@ export async function load({ params, parent, url }) {
const load_event_obj: ae_Event | null = await events_func.load_ae_obj_id__event({
api_cfg: ae_acct.api,
event_id: event_id,
// inc_file_li: true,
inc_file_li: true,
// inc_device_li: true,
inc_location_li: true,
inc_session_li: true,