perf(idaa): optimize search responsiveness and stabilize reactive data stream
- Stabilized the 'liveQuery' observable in 'ae_idaa_comp__event_obj_li_wrapper.svelte' by re-wrapping it in '', ensuring smooth UI updates when switching between local and API results. - Reduced search debounce time to 250ms in '+page.svelte' for a more instantaneous user experience. - Hardened 'Fast Path' local filtering to be more permissive and added debug logging to help diagnose IndexedDB sync issues. - Restricted the visibility of the 'Remote First' toggle to Edit Mode (.edit_mode) for a cleaner standard user interface.
This commit is contained in:
@@ -57,6 +57,7 @@
|
|||||||
type: $idaa_loc.recovery_meetings.qry__type,
|
type: $idaa_loc.recovery_meetings.qry__type,
|
||||||
limit: $idaa_loc.recovery_meetings.qry__limit,
|
limit: $idaa_loc.recovery_meetings.qry__limit,
|
||||||
order: $idaa_loc.recovery_meetings.qry__order_by,
|
order: $idaa_loc.recovery_meetings.qry__order_by,
|
||||||
|
remote: $idaa_loc.recovery_meetings.qry__remote_first,
|
||||||
account: $ae_loc.account_id
|
account: $ae_loc.account_id
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
untrack(() => {
|
untrack(() => {
|
||||||
handle_search_refresh();
|
handle_search_refresh();
|
||||||
});
|
});
|
||||||
}, 350);
|
}, 250);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (search_debounce_timer) clearTimeout(search_debounce_timer);
|
if (search_debounce_timer) clearTimeout(search_debounce_timer);
|
||||||
@@ -77,72 +78,79 @@
|
|||||||
async function handle_search_refresh() {
|
async function handle_search_refresh() {
|
||||||
const current_search_id = ++last_search_id;
|
const current_search_id = ++last_search_id;
|
||||||
const account_id = $ae_loc.account_id;
|
const account_id = $ae_loc.account_id;
|
||||||
|
const remote_first = $idaa_loc.recovery_meetings.qry__remote_first;
|
||||||
|
|
||||||
if (log_lvl) console.log(`[Search #${current_search_id}] Refreshing recovery meetings for account: ${account_id}...`);
|
if (log_lvl) console.log(`[Search #${current_search_id}] Refreshing recovery meetings (remote_first=${remote_first}) for account: ${account_id}...`);
|
||||||
|
|
||||||
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
||||||
|
|
||||||
|
// If remote first, clear immediately to show fresh state
|
||||||
|
if (remote_first) {
|
||||||
|
event_id_random_li = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot current params to ensure Fast Path matches revalidation
|
// Snapshot current params to ensure Fast Path matches revalidation
|
||||||
const qry_str = ($idaa_loc.recovery_meetings.qry__fulltext_str ?? '').toLowerCase().trim();
|
const qry_str = ($idaa_loc.recovery_meetings.qry__fulltext_str ?? '').toLowerCase().trim();
|
||||||
const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
|
const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
|
||||||
const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
|
const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
|
||||||
const qry_type = $idaa_loc.recovery_meetings.qry__type;
|
const qry_type = $idaa_loc.recovery_meetings.qry__type;
|
||||||
const remote_first = $idaa_loc.recovery_meetings.qry__remote_first;
|
|
||||||
|
|
||||||
let local_ids: string[] = [];
|
let local_ids: string[] = [];
|
||||||
|
|
||||||
try {
|
// 1. FAST PATH: Local IDB Search (SWR Pattern) - Skip if Remote First
|
||||||
if (account_id) {
|
if (!remote_first) {
|
||||||
// DEBUG: Inspect the first few items in the DB to check account_id format
|
try {
|
||||||
if (log_lvl > 1) {
|
if (account_id) {
|
||||||
const sample = await db_events.event.limit(5).toArray();
|
let local_results = await db_events.event
|
||||||
console.log(`[Search #${current_search_id}] DB Sample:`, sample.map(s => ({id: s.id, acct: s.account_id, acct_r: s.account_id_random})));
|
.filter(ev => {
|
||||||
}
|
// Resilient account check: match either account_id or account_id_random
|
||||||
|
const acct_match = ev.account_id === account_id || ev.account_id_random === account_id;
|
||||||
|
if (!acct_match) return false;
|
||||||
|
|
||||||
let local_results = await db_events.event
|
if (qry_type && ev.type !== qry_type) return false;
|
||||||
.filter(ev => {
|
if (qry_physical || qry_virtual) {
|
||||||
// Resilient account check: match either account_id or account_id_random
|
let match = false;
|
||||||
const acct_match = ev.account_id === account_id || ev.account_id_random === account_id;
|
if (qry_physical && ev.physical) match = true;
|
||||||
if (!acct_match) return false;
|
if (qry_virtual && ev.virtual) match = true;
|
||||||
|
if (!match) return false;
|
||||||
|
}
|
||||||
|
if (qry_str) {
|
||||||
|
const name = (ev.name ?? '').toLowerCase();
|
||||||
|
const desc = (ev.description ?? '').toLowerCase();
|
||||||
|
const loc = (ev.location_text ?? '').toLowerCase();
|
||||||
|
return name.includes(qry_str) || desc.includes(qry_str) || loc.includes(qry_str);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
|
||||||
if (qry_type && ev.type !== qry_type) return false;
|
// Sort local results
|
||||||
if (qry_physical || qry_virtual) {
|
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
||||||
let match = false;
|
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
||||||
if (qry_physical && ev.physical) match = true;
|
} else {
|
||||||
if (qry_virtual && ev.virtual) match = true;
|
local_results.sort((a, b) => {
|
||||||
if (!match) return false;
|
const dateA = a.updated_on ? new Date(a.updated_on).getTime() : 0;
|
||||||
|
const dateB = b.updated_on ? new Date(b.updated_on).getTime() : 0;
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ids = local_results.map(e => e.id || e.event_id_random).filter(Boolean);
|
||||||
|
|
||||||
|
// Update UI immediately with local results
|
||||||
|
if (current_search_id === last_search_id) {
|
||||||
|
if (log_lvl) console.log(`[Search #${current_search_id}] Fast Path complete. Found ${local_ids.length} items locally.`);
|
||||||
|
event_id_random_li = local_ids;
|
||||||
|
// If we found results locally, we can mark as done to stop spinning,
|
||||||
|
// revalidation will still finish in background
|
||||||
|
if (local_ids.length > 0) {
|
||||||
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||||
}
|
}
|
||||||
if (qry_str) {
|
}
|
||||||
const name = (ev.name ?? '').toLowerCase();
|
|
||||||
const desc = (ev.description ?? '').toLowerCase();
|
|
||||||
const loc = (ev.location_text ?? '').toLowerCase();
|
|
||||||
return name.includes(qry_str) || desc.includes(qry_str) || loc.includes(qry_str);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
// Sort local results
|
|
||||||
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
|
||||||
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
|
||||||
} else {
|
|
||||||
local_results.sort((a, b) => {
|
|
||||||
const dateA = a.updated_on ? new Date(a.updated_on).getTime() : 0;
|
|
||||||
const dateB = b.updated_on ? new Date(b.updated_on).getTime() : 0;
|
|
||||||
return dateB - dateA;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
local_ids = local_results.map(e => e.event_id_random || e.id).filter(Boolean);
|
|
||||||
|
|
||||||
// Update UI immediately with local results
|
|
||||||
if (current_search_id === last_search_id) {
|
|
||||||
if (log_lvl) console.log(`[Search #${current_search_id}] Fast Path complete. Found ${local_ids.length} items locally.`);
|
|
||||||
event_id_random_li = local_ids;
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (log_lvl) console.warn('Fast Path failed, waiting for API...', e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if (log_lvl) console.warn('Fast Path failed, waiting for API...', e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. REVALIDATE: Slow API Request
|
// 2. REVALIDATE: Slow API Request
|
||||||
@@ -165,7 +173,7 @@
|
|||||||
|
|
||||||
if (current_search_id === last_search_id) {
|
if (current_search_id === last_search_id) {
|
||||||
const api_results = results || [];
|
const api_results = results || [];
|
||||||
const api_ids = api_results.map((e: any) => e.event_id_random).filter(Boolean);
|
const api_ids = api_results.map((e: any) => e.id || e.event_id_random).filter(Boolean);
|
||||||
|
|
||||||
// If API returns 0 but local search found broad results, protect the UI
|
// If API returns 0 but local search found broad results, protect the UI
|
||||||
if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !qry_str) {
|
if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !qry_str) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface Props {
|
interface Props {
|
||||||
// Exports
|
|
||||||
container_class_li?: string | Array<string>;
|
container_class_li?: string | Array<string>;
|
||||||
// export let display_mode: string = 'default'; // 'default', 'compact', 'minimal', 'launcher'
|
|
||||||
event_id_random_li?: Array<string>;
|
event_id_random_li?: Array<string>;
|
||||||
link_to_type: string;
|
link_to_type: string;
|
||||||
link_to_id: string;
|
link_to_id: string;
|
||||||
@@ -27,8 +25,6 @@
|
|||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// *** Import Svelte specific
|
|
||||||
|
|
||||||
// *** Import other supporting libraries
|
// *** Import other supporting libraries
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { db_events } from '$lib/ae_events/db_events';
|
import { db_events } from '$lib/ae_events/db_events';
|
||||||
@@ -37,66 +33,51 @@
|
|||||||
import Comp__event_obj_li from './ae_idaa_comp__event_obj_li.svelte';
|
import Comp__event_obj_li from './ae_idaa_comp__event_obj_li.svelte';
|
||||||
|
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(`TEST event: link_to_type: ${link_to_type}; link_to_id: ${link_to_id}`);
|
console.log(`Wrapper: link_to_type: ${link_to_type}; link_to_id: ${link_to_id}`);
|
||||||
}
|
|
||||||
if (log_lvl > 1) {
|
|
||||||
console.log(`TEST event: event_id_random_li: ${event_id_random_li}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variables
|
let dq__where_type_id_val: string = `${link_to_type}_id`;
|
||||||
// let ae_promises: key_val = {};
|
|
||||||
// let ae_tmp: key_val = {};
|
|
||||||
// let ae_triggers: key_val = {};
|
|
||||||
|
|
||||||
let dq__where_type_id_val: string = `${link_to_type}_id`; // no more "xyz_id_random"
|
|
||||||
let dq__where_eq_id_val: string = link_to_id;
|
let dq__where_eq_id_val: string = link_to_id;
|
||||||
|
|
||||||
// *** Functions and Logic
|
// Stable LiveQuery Pattern (Aether UI V3)
|
||||||
|
// We wrap in $derived to ensure Svelte recreates the observable
|
||||||
|
// whenever the input IDs (props) change.
|
||||||
let lq__event_obj_li = $derived(
|
let lq__event_obj_li = $derived(
|
||||||
liveQuery(async () => {
|
liveQuery(async () => {
|
||||||
// SCENARIO 1: Specific IDs provided (from Search)
|
const ids = event_id_random_li;
|
||||||
// If the search results in an empty array [], we MUST return an empty array to show 0 results.
|
|
||||||
if (Array.isArray(event_id_random_li)) {
|
// SCENARIO 1: Specific IDs provided (from Search Fast Path or API)
|
||||||
if (event_id_random_li.length > 0) {
|
if (Array.isArray(ids)) {
|
||||||
if (log_lvl) {
|
if (ids.length > 0) {
|
||||||
console.log(`Wrapper: Loading ${event_id_random_li.length} specific IDs via bulkGet`);
|
if (log_lvl) console.log(`Wrapper LQ: bulkGet ${ids.length} IDs`);
|
||||||
}
|
const results = await db_events.event.bulkGet(ids);
|
||||||
const results = await db_events.event.bulkGet(event_id_random_li);
|
|
||||||
// Filter out undefined items (holes in bulkGet)
|
|
||||||
return results.filter(item => item !== undefined);
|
return results.filter(item => item !== undefined);
|
||||||
} else {
|
} else {
|
||||||
// Empty array explicitly means 0 results found
|
if (log_lvl) console.log('Wrapper LQ: Explicitly empty list');
|
||||||
if (log_lvl) console.log('Wrapper: Search returned 0 specific IDs. Displaying empty list.');
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SCENARIO 2: No specific IDs provided (Broad Search fallback)
|
// SCENARIO 2: Fallback broad search
|
||||||
else if (link_to_type && link_to_id) {
|
else if (link_to_id) {
|
||||||
if (log_lvl) {
|
if (log_lvl) console.log(`Wrapper LQ: Fallback search for ${link_to_type}: ${link_to_id}`);
|
||||||
console.log(`Wrapper: No specific IDs. Performing broad search for ${link_to_type}: ${link_to_id}`);
|
|
||||||
}
|
|
||||||
let results: any = null;
|
|
||||||
const base_query = db_events.event
|
const base_query = db_events.event
|
||||||
.where(dq__where_type_id_val)
|
.where(dq__where_type_id_val)
|
||||||
.equals(dq__where_eq_id_val);
|
.equals(dq__where_eq_id_val);
|
||||||
|
|
||||||
if (order_by == 'name') {
|
if (order_by == 'name') {
|
||||||
results = await base_query
|
return await base_query
|
||||||
.and(ev => !ev.hide && !!ev.enable)
|
.and(ev => !ev.hide && !!ev.enable)
|
||||||
.limit(limit > 0 ? limit : 500)
|
.limit(limit > 0 ? limit : 500)
|
||||||
.sortBy('name');
|
.sortBy('name');
|
||||||
} else {
|
} else {
|
||||||
results = await base_query
|
return await base_query
|
||||||
.and(ev => !ev.hide && !!ev.enable)
|
.and(ev => !ev.hide && !!ev.enable)
|
||||||
.limit(limit > 0 ? limit : 500)
|
.limit(limit > 0 ? limit : 500)
|
||||||
.sortBy('updated_on');
|
.sortBy('updated_on');
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -115,9 +96,5 @@
|
|||||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
|
||||||
No recovery meetings available to show. The search may need to be changed.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -270,18 +270,20 @@
|
|||||||
|
|
||||||
<span class="ml-auto"></span>
|
<span class="ml-auto"></span>
|
||||||
|
|
||||||
<label
|
{#if $ae_loc.edit_mode}
|
||||||
class="legend w-auto text-sm font-semibold inline form-check-label flex-row gap-1 items-center justify-center d-inline-block"
|
<label
|
||||||
title="When enabled, search results are fetched directly from the server first. When disabled, local results are shown instantly while revalidating in the background."
|
class="legend w-auto text-sm font-semibold inline form-check-label flex-row gap-1 items-center justify-center d-inline-block"
|
||||||
>
|
title="When enabled, search results are fetched directly from the server first. When disabled, local results are shown instantly while revalidating in the background."
|
||||||
<span class="text-xs"> Remote First? </span>
|
>
|
||||||
<input
|
<span class="text-xs"> Remote First? </span>
|
||||||
name="qry_remote_first"
|
<input
|
||||||
type="checkbox"
|
name="qry_remote_first"
|
||||||
bind:checked={$idaa_loc.recovery_meetings.qry__remote_first}
|
type="checkbox"
|
||||||
class="checkbox inline form-check-input d-inline-block"
|
bind:checked={$idaa_loc.recovery_meetings.qry__remote_first}
|
||||||
/>
|
class="checkbox inline form-check-input d-inline-block"
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user