From b1162b9f0858b62fec8dcf4c0fb53ea0da00e5c6 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 25 Feb 2026 20:17:03 -0500 Subject: [PATCH] I am done. Just saving things for the night. Not a good day. --- .../PRES_MGMT_SESSION_REFACTOR_PLAN.md | 117 ++++++++++++++++++ documentation/SVELTE_DEXIE_GUIDE.md | 115 ++++++++++++++++- .../(pres_mgmt)/pres_mgmt/+page.svelte | 4 + .../session/[session_id]/+page.svelte | 25 ++-- src/routes/journals/[journal_id]/+page.svelte | 4 + tests/README.md | 18 ++- 6 files changed, 267 insertions(+), 16 deletions(-) create mode 100644 documentation/PRES_MGMT_SESSION_REFACTOR_PLAN.md diff --git a/documentation/PRES_MGMT_SESSION_REFACTOR_PLAN.md b/documentation/PRES_MGMT_SESSION_REFACTOR_PLAN.md new file mode 100644 index 00000000..6ce207ea --- /dev/null +++ b/documentation/PRES_MGMT_SESSION_REFACTOR_PLAN.md @@ -0,0 +1,117 @@ +PRES-MGMT Session View — Refactor Plan + +Goal + +Make the Presentation Management Session view deterministic on cold-start (empty IndexedDB). The page must render Presentations, Presenters, and Hosted Files without requiring manual refreshes. + +Constraints + +- Svelte 5 runes and Dexie `liveQuery` behavior (observable recreation, subscription timing). +- Minimize user-perceived latency — keep navigation snappy where possible. +- Avoid large architectural changes unless necessary. + +Options (high level) + +A) Blocking Hydration (recommended for correctness) +- Block the route `+page.ts` load until the session and all directly required related objects are fetched from the backend and written into IndexedDB. Return `initial_session_obj` in the load data for immediate rendering. +- Pros: simplest to guarantee first-draw correctness; minimal component changes. +- Cons: adds latency to navigation (can be mitigated with optimistic UI or progress indicator). + +B) Prefetch Related Records + Hydrate Fallback (hybrid) +- Non-blocking load but `+page.ts` returns `initial_session_obj` and small related-objects payloads (presentations, presenter IDs, hosted_file metadata). Components use these fallbacks while `liveQuery` takes over. +- Pros: keeps navigation responsive; often sufficient. +- Cons: requires careful payload shaping and DB write ordering. + +C) Explicit Dependency Chaining in UI (advanced) +- Keep non-blocking loads and use explicit dependency chaining: write session -> await write completion -> then write presentations -> await -> then presenters, ensuring microtask queue flushes between writes. Use targeted `liveQuery` re-creation only when upstream dependency fully resolved. +- Pros: minimal route latency; deterministic ordering. +- Cons: more complex to implement and test. + +Recommendation + +Start with Option A (Blocking Hydration) for the session page to restore deterministic behavior quickly. After correctness is achieved, consider converting to Option B or C for improved perceived performance if needed. + +Detailed Steps (Option A - Blocking Hydration) + +1) Add a small helper in `events_func` (e.g., `load_session_with_relations`) that: + - fetches session by ID from API + - fetches related presentations (limit/filters as needed) + - fetches presenters referenced by those presentations (deduplicate IDs) + - fetches hosted_file metadata for presentation files (if required for the view) + - writes all results to IndexedDB in a controlled order (session -> presentations -> presenters -> hosted_files) + - returns a compact `initial_session_obj` payload containing fields needed for first-draw (session, presentation list, presenter summary) + + Implementation note: Use `await db.transaction('rw', db_events.session, db_events.presentation, db_events.presenter, async () => {...})` if atomicity helps. Alternatively write in sequential awaits and call `await Promise.resolve()` after each write to let the microtask queue settle. + +2) Update route loader: `src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.ts` (create if missing) to call and `await` the helper, then return `initial_session_obj` on `data`. + + Example pseudo-code: + + export async function load({ params, parent }) { + const data = await parent(); + if (browser) { + const init = await events_func.load_session_with_relations({ api_cfg: data[data.account_id].api, session_id: params.session_id, log_lvl: 0 }); + data.initial_session_obj = init; + } + return data; + } + +3) Ensure the page component `+page.svelte` uses the `initial_session_obj` as immediate fallback (it already does in Aether). + +4) Add instrumentation logs inside `liveQuery` closures and the helper to verify ordering during QA. + +5) Add tests (see below) and manual verification steps. + +Alternative (Option B - Hybrid) Implementation Notes + +- If you cannot block the route, return an `initial_session_obj` that includes minimal related object arrays (IDs + small metadata) and have `+page.svelte` write those into IDB before mounting heavy child components. +- Use `untrack()` to set selection IDs so stores are updated without causing premature reactivity loops. + +Explicit Dependency Chaining (Option C) Notes + +- Implement a single `prefetch` function that sequentially performs writes and `await Promise.resolve()` between stages. +- For debugging, add microtask delays (e.g., `await 0`) between writes to observe behaviour. + +Testing and Verification + +1) Integration test (Playwright recommended) + - Clear IndexedDB for the app origin. + - Navigate to `/events//.../session/` and assert that the presentation list and presenters are visible within N ms without manual refresh. + - Repeat on subsequent navigations to ensure no regressions. + +2) Unit tests + - For `events_func.load_session_with_relations`, stub API responses and assert DB writes are made in expected order. + +3) Manual QA + - With a cold profile or after clearing Site storage, navigate to the session page and confirm content is present after the initial navigation and that no manual refreshes are required. + +Migration and Rollout + +- Implement Option A behind a feature flag if you want to control rollout. +- Short-term: apply Option A to the single problematic route to reduce blast radius. +- Long-term: consider a library-level helper to standardize "blocking prefetch for nested related records" across other pages. + +Rollback Plan + +- Because changes are additive and limited to one route and helper, revert the `+page.ts` modification and helper call to restore prior behavior. + +Deliverables for tomorrow + +- `events_func.load_session_with_relations` helper (TS) + unit tests +- Updated `+page.ts` loader for session route to `await` helper and return `initial_session_obj` +- Small test harness / Playwright test that reproduces the cold-start issue and verifies the fix +- Instrumentation logs temporarily enabled for QA + +Estimated effort + +- Blocking hydration implementation + tests: 2-4 hours +- Hybrid or chaining implementations: additional 2-6 hours depending on thoroughness + +Notes about Svelte 5 + Dexie specifics + +- Keep `liveQuery` closures stable; capture primitive IDs rather than reactive objects. +- Use `$derived` and `$derived.by` to keep observable instances stable across renders. +- Use `untrack()` when setting selection values to avoid premature subscriptions. +- After DB writes, allowing the microtask queue to settle (`await Promise.resolve()`) helps ensure observers are notified in the expected order during development and debugging. + +If you want I can implement Option A for the session route tomorrow (create helper, update loader, add test). \ No newline at end of file diff --git a/documentation/SVELTE_DEXIE_GUIDE.md b/documentation/SVELTE_DEXIE_GUIDE.md index 956e3923..fceef535 100644 --- a/documentation/SVELTE_DEXIE_GUIDE.md +++ b/documentation/SVELTE_DEXIE_GUIDE.md @@ -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 + +``` + +- 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 {}; } diff --git a/src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte b/src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte index bca483ea..15dff6d5 100644 --- a/src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte +++ b/src/routes/events/[event_id]/(pres_mgmt)/pres_mgmt/+page.svelte @@ -97,6 +97,10 @@ let last_executed_key = ''; // Search Guard Key // Stable LiveQuery Pattern (Aether UI V3) + // Use `$derived.by(() => ...)` to build a stable observable instance from + // explicit, plain dependencies (`event_session_id_li`, `event_id`). This + // ensures the `liveQuery` is recreated only when those inputs change and + // avoids accidental recreation from surrounding reactive state. let lq__event_session_obj_li = $derived.by(() => { const ids = event_session_id_li; const event_id = $events_slct?.event_id; diff --git a/src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte b/src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte index 2088f0d4..4e3e55b2 100644 --- a/src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte +++ b/src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.svelte @@ -21,10 +21,19 @@ import Session_page_menu from './session_page_menu.svelte'; import Comp_event_presentation_obj_li from '../../../../ae_comp__event_presentation_obj_li.svelte'; - // STABILITY FIX: Use URL params directly for queries. + // STABILITY FIX: Capture URL params as plain constants for the liveQuery + // closures so the observable sees a stable identifier value. Capturing + // the raw `data.params` or a reactive store reference here can lead to + // the liveQuery being recreated or seeing transient values on cold-start. const url_session_id = data.params.session_id; const url_event_id = data.params.event_id; + // KNOWN ISSUE (TODO): This page currently depends on related records + // (presentations, hosted files, presenters) already existing in IndexedDB. + // On a cold start (empty IDB) the dependent LQs may not re-run in the + // expected order and the UI can require manual refreshes. Do NOT copy + // this pattern for critical views until the refactor is implemented. + // Sync stores in the background let ae_acct = $derived(data[data.account_id]); $effect(() => { @@ -89,7 +98,7 @@
- + @@ -105,11 +114,13 @@
- + { const ids = search_id_li; diff --git a/tests/README.md b/tests/README.md index e84ae32c..f49d875c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -57,9 +57,15 @@ Help - If a test fails due to external network calls or platform-specific behavior, try mocking the relevant endpoints and move the test to `tests/disabled` if it cannot be made deterministic. -Development / Testing / Demo environment information -Use snake_case (or Snake_Case or Snake_case or test_NASA_example or test_API_key) -Aether test/demo base URL: 'http://demo.localhost:5173' -Aether development API: 'https://dev-api.oneskyit.com' -Aether test/demo account: '_XY7DXtc9MY' (1) "One Sky IT Demo" -Aether test/demo event: 'pjrcghqwert' (1) "Demo One Sky IT Conference" \ No newline at end of file +## Development / Testing / Demo environment information +* Use snake_case (or Snake_Case or Snake_case or test_NASA_example or test_API_key) +* Aether test/demo base URL: 'http://demo.localhost:5173' +* Aether development API: 'https://dev-api.oneskyit.com' +* Aether test/demo Account: '_XY7DXtc9MY' (1) "One Sky IT Demo" +* Aether test/demo Event: 'pjrcghqwert' (1) "Demo One Sky IT Conference" +* Aether test/demo Event Session: 'DOW3h7v6H42' (703) "How To Do Things" +* Aether test/demo Event Presentation: '7U2eXSjR6H4' (1670) "Build a House" +* Aether test/demo Event Presenter: 'gT-hxnifb-0' (2202) "Bob The Builder" +* Aether test/demo Event File: 'OOsHXtng5mr' (2985) "1 Quick Test for macOS.mp4" +* Aether test/demo Journal: 'BVYE-94-46-29' (42) "Testing Things" +* Aether test/demo Journal Entry: 'xRx-Y4-h3-fU' (233) "Another Journal Entry in the Test Journal"