返回 DeepSeek-Reasonix
tabMetaRefresh.ts
根目录 / desktop / frontend / src / lib / tabMetaRefresh.ts
1 import type { TabMeta } from "./types";
2
3 export const TAB_META_VISIBLE_FALLBACK_MS = 15_000;
4 export const TAB_META_HIDDEN_FALLBACK_MS = 60_000;
5 export const TAB_META_MAX_IN_FLIGHT = 2;
6
7 export type BoundedRefreshResult<T> = {
8 value: T;
9 latest: boolean;
10 coalesced: boolean;
11 };
12
13 export type BoundedRefreshOptions = {
14 /**
15 * Mutation-sensitive refresh: never treat a pre-mutation in-flight request
16 * as authoritative. When saturated, queue a trailing load that starts after
17 * a slot frees instead of applying the joined pre-mutation snapshot.
18 */
19 invalidate?: boolean;
20 };
21
22 export function createBoundedRefreshCoordinator<T>(maxInFlight: number) {
23 if (!Number.isInteger(maxInFlight) || maxInFlight < 1) {
24 throw new Error("maxInFlight must be a positive integer");
25 }
26 let sequence = 0;
27 let generation = 0;
28 const inFlight: Array<{ sequence: number; generation: number; promise: Promise<T> }> = [];
29 let trailing: {
30 load: () => Promise<T>;
31 waiters: Array<{
32 resolve: (result: BoundedRefreshResult<T>) => void;
33 reject: (reason?: unknown) => void;
34 }>;
35 } | null = null;
36
37 const settleEntry = (entry: { sequence: number; generation: number; promise: Promise<T> }) => {
38 const index = inFlight.indexOf(entry);
39 if (index >= 0) inFlight.splice(index, 1);
40 pumpTrailing();
41 };
42
43 const startEntry = (load: () => Promise<T>) => {
44 const entry = {
45 sequence: ++sequence,
46 generation,
47 promise: Promise.resolve().then(load),
48 };
49 inFlight.push(entry);
50 void entry.promise.then(
51 () => settleEntry(entry),
52 () => settleEntry(entry),
53 );
54 return entry;
55 };
56
57 const resultFor = (
58 entry: { sequence: number; generation: number },
59 value: T,
60 coalesced: boolean,
61 ): BoundedRefreshResult<T> => ({
62 value,
63 latest: entry.sequence === sequence && entry.generation === generation,
64 coalesced,
65 });
66
67 const queueTrailing = (load: () => Promise<T>): Promise<BoundedRefreshResult<T>> => {
68 if (!trailing) {
69 trailing = { load, waiters: [] };
70 } else {
71 trailing.load = load;
72 }
73 return new Promise<BoundedRefreshResult<T>>((resolve, reject) => {
74 trailing!.waiters.push({ resolve, reject });
75 pumpTrailing();
76 });
77 };
78
79 function pumpTrailing() {
80 if (!trailing || inFlight.length >= maxInFlight) return;
81 const job = trailing;
82 trailing = null;
83 const entry = startEntry(job.load);
84 void entry.promise.then(
85 (value) => {
86 const result = resultFor(entry, value, false);
87 for (const waiter of job.waiters) waiter.resolve(result);
88 },
89 (reason) => {
90 for (const waiter of job.waiters) waiter.reject(reason);
91 },
92 );
93 }
94
95 return {
96 run(load: () => Promise<T>, options?: BoundedRefreshOptions): Promise<BoundedRefreshResult<T>> {
97 if (options?.invalidate) {
98 generation += 1;
99 // Mutation-sensitive callers must not join a pre-mutation request.
100 if (inFlight.length >= maxInFlight) {
101 return queueTrailing(load);
102 }
103 const entry = startEntry(load);
104 return entry.promise.then((value) => resultFor(entry, value, false));
105 }
106
107 let entry = inFlight.length >= maxInFlight ? inFlight[inFlight.length - 1] : undefined;
108 const coalesced = entry !== undefined;
109 if (!entry) {
110 entry = startEntry(load);
111 }
112 const selected = entry;
113 return selected.promise.then((value) => resultFor(selected, value, coalesced));
114 },
115 };
116 }
117
118 const TAB_META_EVENT_KINDS = new Set([
119 "turn_started",
120 "turn_done",
121 "retrying",
122 "approval_request",
123 "ask_request",
124 ]);
125
126 export function tabMetaFallbackDelay(visibility: DocumentVisibilityState): number {
127 return visibility === "hidden" ? TAB_META_HIDDEN_FALLBACK_MS : TAB_META_VISIBLE_FALLBACK_MS;
128 }
129
130 export function shouldRefreshTabMetaForEvent(kind: string): boolean {
131 return TAB_META_EVENT_KINDS.has(kind);
132 }
133
134 export function sameTabMetaLists(current: readonly TabMeta[], next: readonly TabMeta[]): boolean {
135 if (current === next) return true;
136 if (current.length !== next.length) return false;
137 for (let index = 0; index < current.length; index += 1) {
138 if (JSON.stringify(current[index]) !== JSON.stringify(next[index])) return false;
139 }
140 return true;
141 }
142
142 lines TYPESCRIPT