diff --git a/src/lib/ae_events/ae_events__event_presentation.ts b/src/lib/ae_events/ae_events__event_presentation.ts index 79bacdcc..d6ea6bfa 100644 --- a/src/lib/ae_events/ae_events__event_presentation.ts +++ b/src/lib/ae_events/ae_events__event_presentation.ts @@ -151,7 +151,7 @@ export async function load_ae_obj_li__event_presentation({ if (cached_li && cached_li.length > 0) { // Background refresh (non-blocking) _refresh_presentation_li_background({ api_cfg, for_obj_type, for_obj_id, inc_file_li, inc_presenter_li, enabled, hidden, view, limit, offset, order_by_li, try_cache, log_lvl: 0 }); - + // Warm cache for nested loads in the background (FIRE AND FORGET) // DEPRECATED Optimization: Don't fire child loads for every item in a list here. // Let the specific Presentation component handle its own children to stagger requests. @@ -176,7 +176,7 @@ async function _refresh_presentation_li_background({ api_cfg, for_obj_type, for_ const result_li = await api.get_ae_obj_li_v3({ api_cfg, obj_type: 'event_presentation', for_obj_type, for_obj_id, enabled, hidden, view, limit, offset, order_by_li, log_lvl }); if (result_li) { const processed = await process_ae_obj__event_presentation_props({ obj_li: result_li, log_lvl }); - + // Ensure the linking ID is set correctly for indexing if (for_obj_type === 'event_session') { processed.forEach(p => p.event_session_id = for_obj_id); @@ -184,11 +184,20 @@ async function _refresh_presentation_li_background({ api_cfg, for_obj_type, for_ if (try_cache) { await db_save_ae_obj_li__ae_obj({ db_instance: db_events, table_name: 'presentation', obj_li: processed, properties_to_save, log_lvl }); + // CRITICAL FIX (2026-02-26): Yield to microtask queue so Dexie liveQuery observers + // fire before we return. Without this, component-mounted liveQueries may subscribe + // to IDB *before* the write completes, causing empty results on cold-start. + await Promise.resolve(); + } + // CRITICAL FIX (2026-02-26): Block on nested loads when explicitly requested. + // Previously fire-and-forget (forEach without await), which meant the function returned + // before presenter data was loaded, causing "refresh twice" bug on cold-start. + // Now we await all nested loads AND preserve try_cache so presenters are written to IDB. + if (inc_file_li || inc_presenter_li) { + await Promise.all(processed.map(p => + _handle_nested_loads(p, { api_cfg, inc_file_li, inc_presenter_li, enabled, hidden, limit, offset, try_cache, log_lvl: 0 }) + )); } - // Background nested loads for refreshed items (FIRE AND FORGET) - processed.forEach(p => { - _handle_nested_loads(p, { api_cfg, inc_file_li, inc_presenter_li, enabled, hidden, limit, offset, try_cache: false, log_lvl: 0 }); - }); return processed; } } catch (e) {} diff --git a/src/lib/ae_events/ae_events__event_presenter.ts b/src/lib/ae_events/ae_events__event_presenter.ts index 789071a1..eaa1d002 100644 --- a/src/lib/ae_events/ae_events__event_presenter.ts +++ b/src/lib/ae_events/ae_events__event_presenter.ts @@ -134,7 +134,7 @@ async function _refresh_presenter_li_background({ api_cfg, for_obj_type, for_obj const result_li = await api.get_ae_obj_li_v3({ api_cfg, obj_type: 'event_presenter', for_obj_type, for_obj_id, enabled, hidden, view, limit, offset, order_by_li, log_lvl }); if (result_li) { const processed = await process_ae_obj__event_presenter_props({ obj_li: result_li, log_lvl }); - + // String-Only ID Vision: Ensure linking ID is set for indexing processed.forEach((p) => { if (for_obj_type === 'event_presentation') { @@ -153,6 +153,10 @@ async function _refresh_presenter_li_background({ api_cfg, for_obj_type, for_obj if (try_cache) { await db_save_ae_obj_li__ae_obj({ db_instance: db_events, table_name: 'presenter', obj_li: processed, properties_to_save, log_lvl }); + // CRITICAL FIX (2026-02-26): Yield to microtask queue so Dexie liveQuery observers + // fire before we return. Without this, component-mounted liveQueries may subscribe + // to IDB *before* the write completes, causing empty results on cold-start. + await Promise.resolve(); } // Background nested loads for refreshed items (FIRE AND FORGET) diff --git a/src/lib/ae_events/ae_events__event_session.ts b/src/lib/ae_events/ae_events__event_session.ts index bc493270..3f3c82d0 100644 --- a/src/lib/ae_events/ae_events__event_session.ts +++ b/src/lib/ae_events/ae_events__event_session.ts @@ -55,17 +55,17 @@ export async function load_ae_obj_id__event_session({ if (cached) { const elapsed = (performance.now() - start_time).toFixed(2); if (log_lvl) console.log(`✅ [Trace] load_ae_obj_id: CACHE HIT at ${elapsed}ms. Returning stale shell for id=${event_session_id}`); - + // Background tasks: refresh parent and warm child caches (non-blocking) - _refresh_session_id_background({ - api_cfg, event_session_id, view, try_cache, + _refresh_session_id_background({ + api_cfg, event_session_id, view, try_cache, inc_file_li, inc_all_file_li, inc_presentation_li, inc_presenter_li, - enabled, hidden, limit, offset, log_lvl: log_lvl > 1 ? log_lvl : 0 + enabled, hidden, limit, offset, log_lvl: log_lvl > 1 ? log_lvl : 0 }); - + // In SWR mode, we fire child loads in background to warm IDB for the view's LiveQueries _handle_nested_loads(cached, { api_cfg, inc_file_li, inc_all_file_li, inc_presentation_li, inc_presenter_li, enabled, hidden, limit, offset, try_cache, log_lvl: 0 }); - + return cached; // Return immediately without awaiting nested loads } else if (log_lvl) { console.log(`⏳ [Trace] load_ae_obj_id: CACHE MISS at ${(performance.now() - start_time).toFixed(2)}ms for id=${event_session_id}`); @@ -89,19 +89,27 @@ async function _refresh_session_id_background({ api_cfg, event_session_id, view, try { if (log_lvl) console.log(`📡 [Trace] _refresh_session_id: API Fetching id=${event_session_id}`); const result = await api.get_ae_obj_v3({ api_cfg, obj_type: 'event_session', obj_id: event_session_id, view, log_lvl }); - + if (result) { const processed = await process_ae_obj__event_session_props({ obj_li: [result], log_lvl }); const processed_obj = processed[0]; const elapsed = (performance.now() - start_time).toFixed(2); - + if (log_lvl) console.log(`📦 [Trace] _refresh_session_id: Received from API at ${elapsed}ms (id=${processed_obj.id})`); if (try_cache) { await db_save_ae_obj_li__ae_obj({ db_instance: db_events, table_name: 'session', obj_li: [processed_obj], properties_to_save, log_lvl }); + // CRITICAL FIX (2026-02-26): Yield to microtask queue so Dexie liveQuery observers + // fire before we return. Without this, component-mounted liveQueries may subscribe + // to IDB *before* the write completes, causing empty results on cold-start. + await Promise.resolve(); if (log_lvl) console.log(`💾 [Trace] _refresh_session_id: Saved to IDB cache.`); } - return await _handle_nested_loads(processed_obj, { api_cfg, inc_file_li, inc_all_file_li, inc_presentation_li, inc_presenter_li, enabled, hidden, limit, offset, try_cache: false, log_lvl }); + // CRITICAL FIX (2026-02-26): Preserve parent's try_cache value when loading nested data. + // Previously set to `false`, which meant presentations/presenters were fetched from API + // but NEVER written to IndexedDB, causing "refresh twice" bug on cold-start. + // Now nested loads inherit parent's caching behavior for deterministic first-render. + return await _handle_nested_loads(processed_obj, { api_cfg, inc_file_li, inc_all_file_li, inc_presentation_li, inc_presenter_li, enabled, hidden, limit, offset, try_cache, log_lvl }); } } catch (e) { if (log_lvl) console.error(`❌ [Trace] _refresh_session_id: API error for id=${event_session_id}:`, e); @@ -196,20 +204,20 @@ export async function load_ae_obj_li__event_session({ else query = db_events.session.where('for_id').equals(for_obj_id); const cached_li = await query.toArray(); - + if (cached_li && cached_li.length > 0) { const elapsed = (performance.now() - start_time).toFixed(2); if (log_lvl) console.log(`✅ [Trace] load_ae_obj_li: CACHE HIT at ${elapsed}ms (${cached_li.length} items).`); - + // Background refresh (non-blocking) - _refresh_session_li_background({ - api_cfg, for_obj_type, for_obj_id, view, + _refresh_session_li_background({ + api_cfg, for_obj_type, for_obj_id, view, // Optimization: Disable nested loads for list members to prevent request storms - inc_file_li: false, inc_all_file_li: false, inc_presentation_li: false, inc_presenter_li: false, - enabled, hidden, limit, offset, order_by_li, try_cache, - log_lvl: log_lvl > 1 ? log_lvl : 0 + inc_file_li: false, inc_all_file_li: false, inc_presentation_li: false, inc_presenter_li: false, + enabled, hidden, limit, offset, order_by_li, try_cache, + log_lvl: log_lvl > 1 ? log_lvl : 0 }); - + return cached_li; } else if (log_lvl) { console.log(`⏳ [Trace] load_ae_obj_li: CACHE MISS at ${(performance.now() - start_time).toFixed(2)}ms for type=${for_obj_type} id=${for_obj_id}`); @@ -228,7 +236,7 @@ async function _refresh_session_li_background({ api_cfg, for_obj_type, for_obj_i try { if (log_lvl) console.log(`📡 [Trace] _refresh_session_li: API Fetching for=${for_obj_type}:${for_obj_id} (view=${view})`); const result_li = await api.get_ae_obj_li_v3({ api_cfg, obj_type: 'event_session', for_obj_type, for_obj_id, view, enabled, hidden, limit, offset, order_by_li, log_lvl }); - + if (result_li) { const processed = await process_ae_obj__event_session_props({ obj_li: result_li, log_lvl }); const elapsed = (performance.now() - start_time).toFixed(2); @@ -377,13 +385,13 @@ export async function search__event_session({ else if (enabled === 'not_enabled') search_query.and.push({ field: 'enable', op: 'eq', value: 0 }); if (hidden === 'hidden') search_query.and.push({ field: 'hide', op: 'eq', value: 1 }); else if (hidden === 'not_hidden') search_query.and.push({ field: 'hide', op: 'eq', value: 0 }); - + if (location_name) { search_query.and.push({ field: 'event_location_name', op: 'eq', value: location_name }); } const result_li = await api.search_ae_obj_v3({ api_cfg, obj_type: 'event_session', search_query, order_by_li, view, limit, offset, log_lvl }); - + // Handle V3 API envelope let valid_result_li: ae_EventSession[] = []; if (Array.isArray(result_li)) { @@ -433,7 +441,7 @@ async function _process_generic_props>({ obj_li, o (processed_obj as any)[baseIdKey] = processed_obj[randomIdKey]; } else if (processed_obj[baseIdKey]) (processed_obj as any).id = processed_obj[baseIdKey]; - + const group = processed_obj.group ?? '0'; const priority = processed_obj.priority ? 1 : 0; const sort = processed_obj.sort ?? '0';