I am done. Just saving things for the night. Not a good day.
This commit is contained in:
@@ -1,3 +1,112 @@
|
||||
# Stability Patterns for liveQuery + Svelte 5
|
||||
|
||||
Dexie's `liveQuery` works well with Svelte 5 runes, but the combination requires a few stable patterns so queries don't get recreated unintentionally and components render correctly on a "cold start" (empty IndexedDB).
|
||||
- Keep the observable instance stable: wrap `liveQuery` in a stable `$derived` so the observable isn't recreated on every render. Recreate the `liveQuery` only when explicit dependencies change (IDs, filters, or search keys).
|
||||
|
||||
```typescript
|
||||
// stable derived wrapper — only recreated when `id` changes
|
||||
let lq__obj = $derived(
|
||||
(() => {
|
||||
// capture the dependency(s) in a single stable closure
|
||||
const id = url_id;
|
||||
return liveQuery(async () => {
|
||||
if (!id) return null;
|
||||
console.log('[LQ] running for id=', id);
|
||||
return await db.table.get(id);
|
||||
});
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
- Use `$derived.by(() => ...)` where available in your runes shim to build queries from a computed set of inputs (IDs list, search params). This preserves a stable observable instance while still reacting to explicit dependency changes.
|
||||
|
||||
- Avoid capturing mutable objects or inline expressions in the `liveQuery` closure. If the closure captures a changing reference, the query may be recreated unexpectedly or miss the first write.
|
||||
## Common Gotchas and Fixes (Why things sometimes need multiple refreshes)
|
||||
|
||||
- Cold start (IDB empty) + non-blocking API writes: If you mount a component before data is written to IDB, `liveQuery` may run against an empty DB. The API write will populate IDB later, but sometimes a chain of dependent queries (e.g., presentations -> presenters) won't all rerun in the order you expect. The symptoms you described — session shows after one refresh, presenters only after a second — are consistent with either (a) queries recreated in the wrong order or (b) dependent store values being set only after some subscriptions are already created.
|
||||
|
||||
Fixes:
|
||||
- Prefer the "Blocking Loader" when you can: `await` the API call in `+page.ts` so IDB is populated before Svelte mounts.
|
||||
- If you cannot block, return an `initial_*` object from `+page.ts` and use it as an immediate fallback in your component so the UI renders from that payload while `liveQuery` takes over for subsequent updates. Example from Aether:
|
||||
|
||||
```svelte
|
||||
<Comp_event_presentation_obj_li
|
||||
lq__event_presentation_obj_li={$lq__event_presentation_obj_li ?? data.initial_session_obj?.event_presentation_li ?? []}
|
||||
{log_lvl}
|
||||
/>
|
||||
```
|
||||
|
||||
- Ensure store IDs are set before subscribers that depend on them are created. Use `untrack()` (or an equivalent non-reactive assignment) to set IDs in stores during initialization so components subscribe to the correct IDs immediately:
|
||||
|
||||
```typescript
|
||||
$effect(() => {
|
||||
if (!ae_acct) return;
|
||||
untrack(() => {
|
||||
$events_slct.event_id = url_event_id;
|
||||
$events_slct.event_session_id = url_session_id;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- When you have chains (presentations depend on session; presenters depend on presentation.person_id), make the dependent liveQuery explicitly wait for the upstream ID and log inside each query to verify the order — adding a small `await Promise.resolve()` or `await 0` inside the `liveQuery` is sometimes useful during debugging to ensure the JS microtask queue has a chance to settle after DB writes.
|
||||
|
||||
## Practical Patterns from Aether (Journals & Events)
|
||||
- Journals: The journaling pages use SWR-style background refreshes but reliably render because either (a) the page `+page.ts` blocks to populate DB for critical views, or (b) components accept `data.initial_*` fallback values until `liveQuery` emits. This hybrid approach avoids the "refresh twice" problem while keeping navigation snappy.
|
||||
|
||||
- Sessions / Presentations: The session page demonstrates several best practices:
|
||||
- Use `url_*` constants (derived from `data.params`) so the `liveQuery` closure captures a stable value instead of the reactive store directly.
|
||||
- Provide `initial_session_obj` from `+page.ts` as a first-draw fallback to child components.
|
||||
- Use `$derived.by(() => liveQuery(...))` for presentation lists so the observable instance is stable across renders and recreated only when `event_session_id` or `search` changes.
|
||||
|
||||
Example (presentation list pattern):
|
||||
```typescript
|
||||
let lq__event_presentation_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!url_session_id) return [];
|
||||
console.log('[LQ] Querying Presentations for Session:', url_session_id);
|
||||
return await db_events.presentation.where('event_session_id').equals(url_session_id).sortBy('name');
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
- Add a small `console.log` inside each `liveQuery` closure to confirm when it runs and what `id` it sees.
|
||||
- Verify that `+page.ts` either `await`s critical loads or returns `initial_*` payloads for first-render hydration.
|
||||
- Confirm that dependent store values (selected IDs) are assigned before components subscribe — use `untrack` to prevent extra reactive cycles.
|
||||
- Ensure your `liveQuery` closures return quickly and do not throw; any exception inside the query can stop updates.
|
||||
- If a dependent query appears stale, temporarily add `await 0` in the upstream query or an explicit `Promise.resolve()` after the IDB write to force the microtask queue to flush during debugging.
|
||||
|
||||
## Summary Recommendations
|
||||
|
||||
- Prefer blocking loads for primary views when first-render correctness matters.
|
||||
- Use `initial_*` fallback data when non-blocking loads are required.
|
||||
- Wrap `liveQuery` in stable `$derived` instances and only recreate when explicit inputs change.
|
||||
- Use `untrack` to set selection IDs during initialization to avoid subscribe-order bugs.
|
||||
- Add targeted logs inside `liveQuery` closures to diagnose ordering and subscription behavior.
|
||||
|
||||
These patterns are deliberately conservative — they trade minimal blocking or small explicit fallbacks for predictable first-render behaviour. The Aether app's Journals and Event session pages are working examples of these techniques in practice.
|
||||
|
||||
## Examples in this repository
|
||||
|
||||
The following files demonstrate stable `liveQuery` usage, `initial_*` fallbacks, and stable `$derived` wrappers used across the Aether app. Inspect these for copy/paste patterns and concrete implementations.
|
||||
|
||||
- Journals page (stable LQ + search patterns): [src/routes/journals/[journal_id]/+page.svelte](src/routes/journals/[journal_id]/+page.svelte#L51)
|
||||
- Journals layout (blocking background loader): [src/routes/journals/[journal_id]/+layout.ts](src/routes/journals/[journal_id]/+layout.ts#L1)
|
||||
- Session page with URL capture + initial fallback: [src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte#L41)
|
||||
- Presentation management overview (stable derived + search): [src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte#L70)
|
||||
- Event settings example (simple observable): [src/routes/events/[event_id]/settings/+page.svelte](src/routes/events/[event_id]/settings/+page.svelte#L51)
|
||||
- Badge/detail pages (examples of nested LQ): [src/routes/events/[event_id]/(badges)/badges/+page.svelte](src/routes/events/[event_id]/(badges)/badges/+page.svelte#L66)
|
||||
|
||||
Refer to these files when you need concrete code examples to adopt the patterns described above.
|
||||
|
||||
Known broken example (do NOT copy):
|
||||
|
||||
- Presentation Management — Session view: [src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte](src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte#L1)
|
||||
- Status: PARTIALLY BROKEN. This page currently fails when the IndexedDB does not already contain the linked Presentations, Hosted Files, and Presenter records. Users must refresh manually (sometimes twice) to see all linked data.
|
||||
- Root cause: dependent `liveQuery` subscriptions run before required related records are written to IDB; the chain of dependent queries does not reliably rerun on the first cold-start write.
|
||||
- Short-term mitigation: avoid copying this non-blocking pattern for views that require nested related records — use a blocking loader or return `initial_*` payloads from `+page.ts` until the LQ subscriptions are proven stable.
|
||||
- TODO: refactor this page to explicitly block for critical related records or to hydrate all related objects before mounting; consider writing a small integration test that simulates a cold start to validate the fix.
|
||||
# Svelte and Dexie.js Integration Guide
|
||||
|
||||
This document provides a guide to integrating Svelte (with a focus on Runes) and Dexie.js for building reactive web applications. It covers key concepts and best practices for managing reactivity between Svelte components and the Dexie.js database.
|
||||
@@ -112,9 +221,9 @@ Ensure the data is in IndexedDB **before** the component mounts.
|
||||
```typescript
|
||||
export async function load({ params }) {
|
||||
// Blocking await ensures IDB is populated
|
||||
await journals_func.load_ae_obj_id__journal({
|
||||
journal_id: params.journal_id,
|
||||
try_cache: true
|
||||
await journals_func.load_ae_obj_id__journal({
|
||||
journal_id: params.journal_id,
|
||||
try_cache: true
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user