From 23ecb21fe14db13bd220b1434c975f083b29c2b0 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 27 Jan 2026 16:50:18 -0500 Subject: [PATCH] fix(journals): eliminate infinite search loop with criteria-based guard - Implemented a 'Search Guard' pattern in '+page.svelte' that snapshots search criteria and bails out of redundant executions. - Stabilized reactivity by removing immediate list clearing in Remote First mode, ensuring a consistent data stream. - Isolated all search-driven state updates with 'untrack' to prevent circular dependency triggers. - Hardened the 'lq__journal_entry_obj_li' observable to ensure stable result emission. --- src/routes/journals/[journal_id]/+page.svelte | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/routes/journals/[journal_id]/+page.svelte b/src/routes/journals/[journal_id]/+page.svelte index ae3f388f..51bb3aa4 100644 --- a/src/routes/journals/[journal_id]/+page.svelte +++ b/src/routes/journals/[journal_id]/+page.svelte @@ -50,6 +50,7 @@ let search_id_li: Array = $state([]); let search_debounce_timer: any = null; let last_search_id = 0; + let last_executed_key = ''; // Search Guard Key function handle_import_complete() { // Trigger a refresh of the journal entry list @@ -62,27 +63,20 @@ let lq__journal_obj = $derived( liveQuery(async () => { - let results = await db_journals.journal.get($journals_slct?.journal_id ?? ''); - if ($journals_slct.journal_obj && results) { - if (JSON.stringify($journals_slct.journal_obj) !== JSON.stringify(results)) { - $journals_slct.journal_obj = { ...results }; - } - } - return results; + return await db_journals.journal.get($journals_slct?.journal_id ?? ''); }) ); // Stable LiveQuery Pattern (Aether UI V3) - // Shared across ID view and List Wrapper - let lq__journal_entry_obj_li = $derived.by(() => { - // 1. Capture dependencies for Svelte tracking - const ids = search_id_li; - const journal_id = $lq__journal_obj?.journal_id; - const search_text = $journals_loc.entry.qry__search_text; - const cat_code = $journals_loc.entry.qry__category_code; + // Re-wrapped in $derived to ensure the observable instance remains stable + // unless the underlying dependencies (ids, search context) change. + let lq__journal_entry_obj_li = $derived( + liveQuery(async () => { + const ids = search_id_li; + const journal_id = $lq__journal_obj?.journal_id; + const search_text = $journals_loc.entry.qry__search_text; + const cat_code = $journals_loc.entry.qry__category_code; - // 2. Return the observable - return liveQuery(async () => { // SCENARIO 1: Specific IDs provided (Search Results) if (Array.isArray(ids) && ids.length > 0) { if (log_lvl) console.log(`Journal Page LQ: bulkGet ${ids.length} IDs`); @@ -91,7 +85,6 @@ } // SCENARIO 2: Fallback to broad search (Default view) - // Only if search is empty and we have a journal context if (journal_id && !search_text && !cat_code) { if (log_lvl) console.log(`Journal Page LQ: Fallback search for journal: ${journal_id}`); return await db_journals.journal_entry @@ -102,8 +95,8 @@ } return []; - }); - }); + }) + ); // Standardized Reactive Search Pattern (Aether UI V3) // 1. Isolate dependencies into a stable derived object @@ -121,7 +114,7 @@ // 2. Controlled effect for triggering searches $effect(() => { - // Track specifically the isolated search params + // Establishes reactive dependency on search_params const params = search_params; if (search_debounce_timer) clearTimeout(search_debounce_timer); @@ -138,30 +131,35 @@ }); async function handle_search_refresh(params: any) { + // 1. Guard: Check if criteria actually changed + const qry_key = JSON.stringify(params); + if (qry_key === last_executed_key) return; + last_executed_key = qry_key; + const current_search_id = ++last_search_id; const journal_id = params.journal_id; const remote_first = params.remote_first; if (log_lvl) console.log(`[Journal Search #${current_search_id}] Refreshing entries (remote=${remote_first}, journal=${journal_id})...`); - $journals_sess.entry.qry__status = 'loading'; - - if (remote_first) { - search_id_li = []; - } + // 2. Setup State + untrack(() => { + $journals_sess.entry.qry__status = 'loading'; + }); const qry_str = params.str; const cat_code = params.cat; let local_ids: string[] = []; - // 1. FAST PATH: Local IDB Search + // 3. FAST PATH: Local IDB Search (SWR) + // We skip this ONLY if remote_first is checked AND we have search text if (!remote_first) { try { if (journal_id) { - let query = db_journals.journal_entry.where('journal_id').equals(journal_id); - - let local_results = await query + let local_results = await db_journals.journal_entry + .where('journal_id') + .equals(journal_id) .filter(entry => { if (cat_code && entry.category_code !== cat_code) return false; if (qry_str) { @@ -183,8 +181,10 @@ if (current_search_id === last_search_id) { if (log_lvl) console.log(`[Journal Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`); - search_id_li = local_ids; - if (local_ids.length > 0) $journals_sess.entry.qry__status = 'done'; + untrack(() => { + search_id_li = local_ids; + if (local_ids.length > 0) $journals_sess.entry.qry__status = 'done'; + }); } } } catch (e) { @@ -192,7 +192,7 @@ } } - // 2. REVALIDATE: API Request + // 4. REVALIDATE: API Request try { const results = await journals_func.qry__journal_entry({ api_cfg: $ae_api, @@ -210,23 +210,30 @@ const api_results = results || []; const api_ids = api_results.map((e: any) => e.id || e.journal_entry_id_random).filter(Boolean); + // Protect UI cache if API returns empty during revalidation if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !qry_str) { - $journals_sess.entry.qry__status = 'done'; + untrack(() => { + $journals_sess.entry.qry__status = 'done'; + }); return; } - $journals_sess.entry_li = api_results; - search_id_li = api_ids; - $journals_sess.entry.qry__status = 'done'; + untrack(() => { + $journals_sess.entry_li = api_results; + search_id_li = api_ids; + $journals_sess.entry.qry__status = 'done'; + }); if (log_lvl) console.log(`[Journal Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`); } } catch (error) { if (current_search_id === last_search_id) { console.error('Journal revalidation failed:', error); - $journals_sess.entry.qry__status = 'error'; - if (search_id_li.length === 0 && local_ids.length > 0) { - search_id_li = local_ids; - } + untrack(() => { + $journals_sess.entry.qry__status = 'error'; + if (search_id_li.length === 0 && local_ids.length > 0) { + search_id_li = local_ids; + } + }); } } } @@ -245,8 +252,8 @@ {#if $lq__journal_obj === undefined} -
- +
+

Loading Journal...

{:else if $ae_loc.person_id == $lq__journal_obj?.person_id}