fix(sidebar): cancel stale table search work across modes
This commit is contained in:
1 parent
e671b3da7d
commit
71136377e5
8 files changed
+763
-24
No files matched your search
@@ -0,0 +1,103 @@
|
||||
import type { TableInfo } from "@/types/database";
|
||||
|
||||
/**
|
||||
* In-flight full-index builds, keyed by sidebar scope (parent node id).
|
||||
*
|
||||
* A missing persisted index (read returns null) means the scope was never
|
||||
* indexed, so only the currently loaded first page of children would be
|
||||
* searchable. That silently misses alphabetically-late tables — e.g.
|
||||
* "T_Erp_Nc_SuPlan_List" for the fuzzy query "erpncs" — until an explicit
|
||||
* index refresh happens (the refresh button). Building the index on the first
|
||||
* search makes the complete table set searchable from the start, matching the
|
||||
* behavior of an explicit refresh.
|
||||
*
|
||||
* Building pages through every table of a scope, so rapid consecutive first
|
||||
* searches must never start concurrent full builds: while one build for a
|
||||
* scope is in flight, further requests reuse the same promise instead of
|
||||
* spawning a duplicate scan. Entries are removed once the build settles
|
||||
* (success or failure) so a later search can retry.
|
||||
*/
|
||||
const inFlightSidebarTableSearchBuilds = new Map<string, Promise<TableInfo[]>>();
|
||||
|
||||
function dedupeInFlightBuild(scopeKey: string, build: () => Promise<TableInfo[]>): Promise<TableInfo[]> {
|
||||
const existing = inFlightSidebarTableSearchBuilds.get(scopeKey);
|
||||
if (existing) return existing;
|
||||
const pending = (async () => {
|
||||
try {
|
||||
return await build();
|
||||
} finally {
|
||||
inFlightSidebarTableSearchBuilds.delete(scopeKey);
|
||||
}
|
||||
})();
|
||||
inFlightSidebarTableSearchBuilds.set(scopeKey, pending);
|
||||
return pending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the local table search index for a sidebar scope, building it on first
|
||||
* use — exactly once per scope, even under rapid consecutive input.
|
||||
*
|
||||
* - An empty (or cleared) query filters nothing, so no read or build happens:
|
||||
* clearing the input must not scan every page of the table set for nothing.
|
||||
* - An explicit refresh (`refresh = true`) always rebuilds, but still through
|
||||
* the in-flight lock so double-clicks cannot start duplicate full builds.
|
||||
* - A persisted index is reused without rebuilding; a missing one is built
|
||||
* through the lock, and concurrent callers share the same build promise.
|
||||
*/
|
||||
export async function loadOrBuildSidebarTableSearchIndex(scopeKey: string, query: string, read: () => Promise<TableInfo[] | null>, build: () => Promise<TableInfo[]>, refresh = false): Promise<TableInfo[] | null> {
|
||||
if (!refresh && !query.trim()) return null;
|
||||
if (refresh) return dedupeInFlightBuild(scopeKey, build);
|
||||
const entries = await read();
|
||||
if (entries) return entries;
|
||||
return dedupeInFlightBuild(scopeKey, build);
|
||||
}
|
||||
|
||||
export interface SidebarTableSearchDebouncer {
|
||||
/** Debounce a per-key load; consecutive calls for the same key within the window coalesce. */
|
||||
schedule(key: string, run: () => void): void;
|
||||
/** Cancel a pending (scheduled, not yet run) load for a key. */
|
||||
cancel(key: string): void;
|
||||
/** Cancel every pending load. */
|
||||
cancelAll(): void;
|
||||
/** Number of pending (scheduled, not yet run) loads. */
|
||||
pendingCount(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-key debouncer used to coalesce rapid table-search input into a single
|
||||
* load/build. Each schedule() for the same key resets the window, so fast
|
||||
* typing issues one load instead of one full index build per keystroke.
|
||||
*/
|
||||
export function createSidebarTableSearchDebouncer(delayMs = 250): SidebarTableSearchDebouncer {
|
||||
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
return {
|
||||
schedule(key, run) {
|
||||
const existing = timers.get(key);
|
||||
if (existing !== undefined) clearTimeout(existing);
|
||||
const timer = setTimeout(() => {
|
||||
timers.delete(key);
|
||||
run();
|
||||
}, delayMs);
|
||||
timers.set(key, timer);
|
||||
},
|
||||
cancel(key) {
|
||||
const existing = timers.get(key);
|
||||
if (existing !== undefined) {
|
||||
clearTimeout(existing);
|
||||
timers.delete(key);
|
||||
}
|
||||
},
|
||||
cancelAll() {
|
||||
for (const timer of timers.values()) clearTimeout(timer);
|
||||
timers.clear();
|
||||
},
|
||||
pendingCount() {
|
||||
return timers.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function scheduleExclusiveSidebarTableSearchDebounce(key: string, active: SidebarTableSearchDebouncer, inactive: SidebarTableSearchDebouncer, run: () => void): void {
|
||||
inactive.cancel(key);
|
||||
active.schedule(key, run);
|
||||
}
|
||||
Reference in new issue
Block a user