fix(launcher): use $derived.by for session liveQueries to fix stale presentation/presenter data

When switching sessions within the same location, presentations and presenters
were not updating. The root cause: plain $derived(liveQuery(...)) never recreates
the Observable when slct__event_session_id changes, because liveQuery's async
callback runs in Dexie's zone where Svelte tracking is off. Dexie's range-level
change detection then ignores new session data (it arrives under a different
event_session_id index value, outside the originally observed range).

Replaced all four liveQuery declarations with $derived.by(() => { const id = ...;
return liveQuery(...id...); }) — the same pattern already used in +layout.svelte
for location-dependent queries. Svelte tracks the id read in the outer closure
and recreates the Observable on every session change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-26 19:55:17 -04:00
parent a90572bcb8
commit 6282fb167f

View File

@@ -50,10 +50,17 @@ import {
Users Users
} from '@lucide/svelte'; } from '@lucide/svelte';
// Event Session (Main View Trigger) // Event Session (Main View Trigger)
// WHY: We use a simple derived observable. The template handles the $ prefix. // WHY: $derived.by captures the id in the outer closure so Svelte tracks it as a
let lq__event_session_obj = $derived( // reactive dependency and recreates the Observable when slct__event_session_id changes.
liveQuery(() => db_events.session.get(slct__event_session_id)) // Plain $derived(liveQuery(...)) does NOT work here: liveQuery's async callback runs in
); // Dexie's zone where Svelte tracking is off, so $derived never sees the id read and never
// recreates the Observable when the session changes. Dexie's range-level change tracking
// then keeps the stale Observable alive — it only re-fires when data in the originally
// observed key range changes, not when a different session's data arrives.
let lq__event_session_obj = $derived.by(() => {
const id = slct__event_session_id;
return liveQuery(() => db_events.session.get(id));
});
// WHY: type_code drives poster vs. oral UI branching throughout this component. // WHY: type_code drives poster vs. oral UI branching throughout this component.
// It was previously a prop that was never passed by the parent, so all poster // It was previously a prop that was never passed by the parent, so all poster
@@ -62,65 +69,63 @@ let lq__event_session_obj = $derived(
let type_code = $derived($lq__event_session_obj?.type_code ?? ''); let type_code = $derived($lq__event_session_obj?.type_code ?? '');
// Event File (for a Session) // Event File (for a Session)
// WHY: Pure data retrieval. Side effects (updating global stores) are removed // WHY: $derived.by — same reason as lq__event_session_obj above. Without recreating
// to prevent circular reactivity loops during rapid navigation. // the Observable when slct__event_session_id changes, Dexie never re-fires for the new
let lq__event_file_obj_li = $derived( // session's files (they land in a different for_id range than what was originally observed).
liveQuery(async () => { let lq__event_file_obj_li = $derived.by(() => {
if (!slct__event_session_id) return []; const id = slct__event_session_id;
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1) { if (log_lvl > 1) {
console.log( console.log(`[LQ] Fetching files for session: ${id}`);
`[LQ] Fetching files for session: ${slct__event_session_id}`
);
} }
return await db_events.file return await db_events.file
.where('for_id') .where('for_id')
.equals(slct__event_session_id) .equals(id)
.reverse() .reverse()
.sortBy('created_on'); .sortBy('created_on');
}) });
); });
// Event Presentation // Event Presentation
let lq__event_presentation_obj_li = $derived( // WHY: $derived.by — same reason as above. Captures both id and sort_by in the outer
liveQuery(async () => { // closure so a new Observable is created whenever the session or its type changes.
if (!slct__event_session_id) return []; let lq__event_presentation_obj_li = $derived.by(() => {
const id = slct__event_session_id;
const sort_by = type_code == 'poster' ? 'name' : 'start_datetime';
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1) { if (log_lvl > 1) {
console.log( console.log(`[LQ] Fetching presentations for session: ${id}`);
`[LQ] Fetching presentations for session: ${slct__event_session_id}`
);
} }
let sort_by = 'start_datetime';
if (type_code == 'poster') {
sort_by = 'name';
}
return await db_events.presentation return await db_events.presentation
.where('event_session_id') .where('event_session_id')
.equals(slct__event_session_id) .equals(id)
.sortBy(sort_by); .sortBy(sort_by);
}) });
); });
// Event Presenter // Event Presenter
let lq__event_presenter_obj_li = $derived( // WHY: $derived.by — same reason as above.
liveQuery(async () => { let lq__event_presenter_obj_li = $derived.by(() => {
if (!slct__event_session_id) return []; const id = slct__event_session_id;
return liveQuery(async () => {
if (!id) return [];
if (log_lvl > 1) { if (log_lvl > 1) {
console.log( console.log(`[LQ] Fetching presenters for session: ${id}`);
`[LQ] Fetching presenters for session: ${slct__event_session_id}`
);
} }
return await db_events.presenter return await db_events.presenter
.where('event_session_id') .where('event_session_id')
.equals(slct__event_session_id) .equals(id)
.sortBy('full_name'); .sortBy('full_name');
}) });
); });
// let show_modal_upload_files: boolean = false; // let show_modal_upload_files: boolean = false;
// let link_to_type: null|string = null; // let link_to_type: null|string = null;