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:
@@ -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) {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user