返回 DeepSeek-Reasonix
queryCoalesce.ts
根目录 / desktop / frontend / src / lib / queryCoalesce.ts
1 /**
2 * Coalesces identical read-only backend queries that a single UI transition
3 * fires more than once.
4 *
5 * Switching a session fans out ~20-28 bridge calls, and measurement showed a
6 * third of them are duplicates within the same transition: independent hooks
7 * each ask for the context panel, the project tree, the tab meta. Deduping at
8 * the call sites would mean threading a cache through a dozen hooks; deduping
9 * at the bridge is one place and cannot be forgotten by the next hook.
10 *
11 * Only queries on the allowlist are coalesced, and only for the few
12 * milliseconds a transition takes: a stale answer is worse than a slow one, so
13 * nothing here outlives the burst it exists to collapse.
14 */
15
16 /** How long an in-flight or just-settled answer may be shared. */
17 const WINDOW_MS = 200;
18
19 /**
20 * COALESCED lists read-only queries whose answer cannot change within one UI
21 * transition. Anything that mutates, starts work, or reads a value the user is
22 * actively editing stays off this list.
23 */
24 const COALESCED = new Set([
25 "ListProjectTree",
26 "ListTabs",
27 "ContextPanel",
28 "MetaForTab",
29 "EffortForTab",
30 "JobsForTab",
31 "BackgroundRuntimes",
32 "BalanceForTab",
33 ]);
34
35 // Any command that can change backend-visible state starts a new query epoch.
36 // Prefixes match the generated binding names while leaving ordinary reads
37 // (List/Get/Meta/History/Context/Jobs/Checkpoints) available for coalescing.
38 const MUTATION_PREFIX = /^(Activate|Add|Answer|Apply|Approve|Cancel|Clear|Close|Connect|Create|Delete|Ensure|Fetch|Install|New|Open|Refresh|Remove|Rename|Reorder|Replay|Resolve|Resume|Run|Save|Send|Set|Start|Steer|Stop|Switch|Trash|Try|Update|Upgrade)/;
39
40 type Entry = { promise: Promise<unknown>; at: number };
41
42 const inflight = new Map<string, Entry>();
43
44 function now(): number {
45 return typeof performance !== "undefined" ? performance.now() : Date.now();
46 }
47
48 export function coalescesQuery(method: string): boolean {
49 return COALESCED.has(method);
50 }
51
52 export function invalidatesQueryCoalescing(method: string): boolean {
53 return MUTATION_PREFIX.test(method);
54 }
55
56 /**
57 * maybeShare is the bridge's single entry point: it decides whether a call is
58 * shareable and shares it, so the proxy never has to know the allowlist.
59 */
60 export function maybeShare(method: string, args: unknown[], run: () => unknown): unknown {
61 if (!coalescesQuery(method)) {
62 if (invalidatesQueryCoalescing(method)) inflight.clear();
63 return run();
64 }
65 return shareQuery(method, args, async () => run());
66 }
67
68 /**
69 * shareQuery returns the in-flight answer for an identical call made inside the
70 * window, or runs it. A rejection is never shared beyond its own settlement:
71 * the next caller retries rather than inheriting a stale failure. Callers that
72 * cross an external state boundary may explicitly invalidate that query, while
73 * bridge mutations invalidate the whole burst automatically.
74 */
75 export function shareQuery<T>(method: string, args: unknown[], run: () => Promise<T>): Promise<T> {
76 const key = `${method}|${safeKey(args)}`;
77 const at = now();
78 const hit = inflight.get(key);
79 if (hit && at - hit.at < WINDOW_MS) return hit.promise as Promise<T>;
80 const promise = run();
81 inflight.set(key, { promise, at });
82 void promise.then(
83 () => {
84 if (method === "ListTabs") {
85 forget(key, promise);
86 return;
87 }
88 setTimeout(() => forget(key, promise), WINDOW_MS);
89 },
90 () => forget(key, promise),
91 );
92 return promise;
93 }
94
95 function forget(key: string, promise: Promise<unknown>): void {
96 if (inflight.get(key)?.promise === promise) inflight.delete(key);
97 }
98
99 /** Arguments are identifiers and flags; anything unserialisable disables the share. */
100 function safeKey(args: unknown[]): string {
101 try {
102 return JSON.stringify(args);
103 } catch {
104 return `\u0000${Math.random()}`;
105 }
106 }
107
108 /** Drop a settled burst answer at an external state boundary (for example turn_done). */
109 export function invalidateSharedQuery(method: string, args: unknown[]): void {
110 inflight.delete(`${method}|${safeKey(args)}`);
111 }
112
113 /** Test seam: forget everything remembered so far. */
114 export function resetQueryCoalescing(): void {
115 inflight.clear();
116 }
117
117 lines TYPESCRIPT