Fix: Event session cold-start bug - presentations/presenters now load on first render

CRITICAL BUG FIX: Session view required 1-2 manual refreshes to display
presentations and presenters when IndexedDB was empty.

Root Cause:
- Nested loaders passed try_cache: false, preventing IDB writes
- Missing microtask yields caused race conditions between IDB writes
  and liveQuery subscriptions

Changes:
- ae_events__event_session.ts: Preserve try_cache in nested loads
- ae_events__event_presentation.ts: Block on presenter loads with await
  Promise.all() + preserve try_cache
- ae_events__event_presenter.ts: Add microtask yield after IDB write

Result: Presentations AND presenters now render correctly on first
navigation without requiring manual page refresh.
This commit is contained in:
Scott Idem
2026-02-26 13:40:36 -05:00
parent 9da3e5326b
commit 3849118fec
3 changed files with 49 additions and 28 deletions

View File

@@ -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) {}

View File

@@ -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)

View File

@@ -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<T extends Record<string, any>>({ 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';