diff --git a/documentation/BOOTSTRAP__AI_Agent_Quickstart.md b/documentation/BOOTSTRAP__AI_Agent_Quickstart.md index 7dc44132..e0536527 100644 --- a/documentation/BOOTSTRAP__AI_Agent_Quickstart.md +++ b/documentation/BOOTSTRAP__AI_Agent_Quickstart.md @@ -149,6 +149,12 @@ subscribes to the **entire store**. This means unrelated writes to `$ae_loc` what you read from these stores inside `$effect` blocks. See `PROJECT__Stores_Svelte5_Migration.md` for the long-term fix plan. +For search pages specifically, this usually means: +- keep true user preferences in persisted local state +- keep transient triggers, loading flags, and last-executed search keys in session state when possible +- let the page effect schedule the search, but put the duplicate-execution guard inside the search executor so page-load auto-search still runs after hydration +- if the search text or filters are mirrored from localStorage on mount, expect that mount-time writes can re-trigger the effect unless the executor has its own guard + ### `{#await}` blocks ```svelte {#await somePromise} diff --git a/documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md b/documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md index a81feb82..d6d4a3f8 100644 --- a/documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md +++ b/documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md @@ -89,7 +89,7 @@ $effect(() => { - 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) +## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings) - 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. - Journals broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows. @@ -98,6 +98,12 @@ $effect(() => { - 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. +- Search pages with persisted filters or saved query text should keep the auto-search trigger in a page-level `$effect`, but the duplicate guard should live inside the actual search executor. That preserves the first page-load search while blocking repeated identical reruns from localStorage-backed rerenders. In practice: + - derive a single `qry_key` from the search inputs + - debounce in the `$effect` + - compare `qry_key` against a `last_executed_key` inside `handle_search_refresh()` + - keep transient loading flags and trigger counters in session state when the value is only used to force a refresh, not as a persisted preference + Example (presentation list pattern): ```typescript let lq__event_presentation_obj_li = $derived( @@ -114,6 +120,7 @@ let lq__event_presentation_obj_li = $derived( - 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. +- If a search page stops auto-loading after a localStorage change, check whether the duplicate guard was placed in the `$effect` instead of the executor. Guarding too early can suppress the initial search; guard at execution time instead. - If a broad Dexie-backed list shows fewer rows than a narrower filter, look for a limit or revalidation step overwriting the local IDB result set. Broad views should stay unbounded unless the user is actually narrowing by text. - 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. diff --git a/src/lib/stores/ae_idaa_stores.ts b/src/lib/stores/ae_idaa_stores.ts index f09b9e6c..b6d3f21c 100644 --- a/src/lib/stores/ae_idaa_stores.ts +++ b/src/lib/stores/ae_idaa_stores.ts @@ -1,16 +1,15 @@ +import { AE_IDAA_LOC_VERSION } from '$lib/stores/store_versions'; import { persisted } from 'svelte-persisted-store'; import { writable } from 'svelte/store'; import type { Writable } from 'svelte/store'; import type { key_val } from '$lib/stores/ae_stores'; -const ver = '2024-08-21_1646'; - /* *** BEGIN *** Initialize idaa_local_data_struct */ // Persisted to localStorage. Retains Novi identity, auth state, and IDAA // query preferences across sessions. Wiped on schema change via store_versions.ts. const idaa_local_data_struct: key_val = { - ver: ver, + __version: AE_IDAA_LOC_VERSION, name: 'Aether - IDAA', title: `OSIT's Æ IDAA`, @@ -111,7 +110,6 @@ export const idaa_loc: Writable = persisted( /* *** BEGIN *** Initialize idaa_session_data_struct */ // In-memory only (not persisted). Resets on page load. const idaa_session_data_struct: key_val = { - ver: ver, log_lvl: 1, archives: { @@ -137,6 +135,7 @@ const idaa_session_data_struct: key_val = { recovery_meetings: { qry__status: null, qry__fulltext_str: null, + search_version: 0, edit__event_obj: null, @@ -185,7 +184,7 @@ const idaa_trig_template: key_val = { event_id: false, post_id: false }; -export const idaa_trig: any = writable(idaa_trig_template); +export const idaa_trig: Writable = writable(idaa_trig_template); // Promise map — keyed by object type; used to track in-flight async operations. const idaa_prom_template: key_val = { @@ -194,4 +193,4 @@ const idaa_prom_template: key_val = { event_id: false, post_id: false }; -export const idaa_prom: any = writable(idaa_prom_template); +export const idaa_prom: Writable = writable(idaa_prom_template); diff --git a/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte b/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte index 55a9a0b5..e14f0aa0 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte @@ -31,16 +31,15 @@ if (browser) { $idaa_slct.event_id = null; window.parent.postMessage({ event_id: null }, '*'); - // Use versioning instead of boolean to avoid loops - if ($idaa_loc.recovery_meetings.search_version === undefined) { - $idaa_loc.recovery_meetings.search_version = 0; - } - $idaa_loc.recovery_meetings.search_version++; + // Use a session-scoped trigger so the persisted IDAA profile is not rewritten + // on every page mount. Recovery Meetings only needs this to kick the initial search. + $idaa_sess.recovery_meetings.search_version++; } let event_id_li: Array = $state([]); let search_debounce_timer: any = null; let last_search_id = 0; +let last_executed_key = ''; // Standardized Reactive Search Pattern (Aether UI V3) // This effect manages the orchestration between UI state and data fetching. @@ -56,7 +55,7 @@ $effect(() => { // Track filters and the search version (trigger) const qry_params = { - v: $idaa_loc.recovery_meetings.search_version, + v: $idaa_sess.recovery_meetings.search_version, str: $idaa_loc.recovery_meetings.qry__fulltext_str, phys: $idaa_loc.recovery_meetings.qry__physical, virt: $idaa_loc.recovery_meetings.qry__virtual, @@ -65,13 +64,14 @@ $effect(() => { order: $idaa_loc.recovery_meetings.qry__order_by, remote: $idaa_loc.recovery_meetings.qry__remote_first }; + const qry_key = JSON.stringify(qry_params); // 2. Debounce Logic if (search_debounce_timer) clearTimeout(search_debounce_timer); search_debounce_timer = setTimeout(() => { // 3. Execution (Untracked to prevent loops) untrack(() => { - handle_search_refresh(); + handle_search_refresh(qry_key); }); }, 250); @@ -85,7 +85,10 @@ $effect(() => { * * GOAL: Render matching meetings in < 50ms, then update with perfect server data. */ -async function handle_search_refresh() { +async function handle_search_refresh(qry_key: string) { + if (qry_key === last_executed_key) return; + last_executed_key = qry_key; + const current_search_id = ++last_search_id; const account_id = $ae_loc.account_id; const remote_first = $idaa_loc.recovery_meetings.qry__remote_first; diff --git a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte index 29053432..a3ba2fd4 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte @@ -69,10 +69,7 @@ if ( * debounced search cycle automatically. */ function handle_search_trigger() { - if ($idaa_loc.recovery_meetings.search_version === undefined) { - $idaa_loc.recovery_meetings.search_version = 0; - } - $idaa_loc.recovery_meetings.search_version++; + $idaa_sess.recovery_meetings.search_version++; } function prevent_default(fn: (event: T) => void) {