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