返回 DeepSeek-Reasonix
controllerEventRecoveryWorker.ts
根目录 / desktop / frontend / src / lib / controllerEventRecoveryWorker.ts
1 import { asArray } from "./array";
2 import { app } from "./bridge";
3 import { startDesktopEventRecovery } from "./desktopEventRecovery";
4 import { recordFrontendDiagnostic } from "./frontendDiagnosticBridge";
5 import type { Meta, TabMeta } from "./types";
6 import { sameSessionIdentity } from "./sessionIdentity";
7
8 export interface ControllerRecoveryPorts {
9 navigation(): number;
10 bindings(): Map<string, string>;
11 meta(tabId: string): Meta | undefined;
12 now(): number;
13 flush(): void;
14 prepare(tab: TabMeta): void;
15 runtime(tab: TabMeta, snapshotAt: number): void;
16 resynchronize?(tab: TabMeta): Promise<void>;
17 reset(tabId: string): void;
18 hydrate(tab: TabMeta, isCurrent: () => boolean): Promise<void>;
19 }
20
21 export function startControllerEventRecovery(ports: ControllerRecoveryPorts, subscribe: (callback: () => void) => () => void): () => void {
22 let version = 0;
23 const hydrating = new Set<string>();
24 const failed = (error: unknown) => recordFrontendDiagnostic("runtime", "desktop-event-recovery-failed", {
25 error: error instanceof Error ? error.message : String(error),
26 });
27 const off = startDesktopEventRecovery({
28 subscribe: callback => subscribe(() => { version++; callback(); }),
29 capture: () => ({ version, navigation: ports.navigation(), snapshotAt: ports.now(), bindings: ports.bindings() }),
30 isCurrent: scope => {
31 const bindings = ports.bindings();
32 return scope.navigation === ports.navigation() && Array.from(scope.bindings).every(([id, value]) => bindings.get(id) === value);
33 },
34 read: async () => asArray(await app.ListTabs()),
35 apply: (tabs, scope) => {
36 ports.flush();
37 for (const tab of tabs) {
38 if (!scope.bindings.has(tab.id)) continue;
39 const meta = ports.meta(tab.id);
40 const changedSession = !sameSessionIdentity(meta, tab);
41 ports.prepare(tab);
42 if (changedSession || hydrating.has(tab.id)) {
43 hydrating.add(tab.id);
44 ports.reset(tab.id);
45 const isCurrent = () => scope.version === version && scope.navigation === ports.navigation();
46 const hydration = ports.hydrate(tab, isCurrent);
47 // hydrate advances sessionLoadSeq synchronously before its first
48 // await. Capture that new binding, not the pre-hydration snapshot:
49 // a background session change can supersede it without navigation.
50 const binding = ports.bindings().get(tab.id);
51 void hydration.then(() => {
52 if (isCurrent() && binding !== undefined && ports.bindings().get(tab.id) === binding) {
53 hydrating.delete(tab.id); ports.runtime(tab, scope.snapshotAt);
54 }
55 }).catch(failed);
56 } else {
57 // The existing runtime projection requests missing durable turn
58 // events and pending prompt presentation, never another invocation.
59 ports.runtime(tab, scope.snapshotAt);
60 if (ports.resynchronize) void ports.resynchronize(tab).catch(failed);
61 }
62 }
63 },
64 failed,
65 timer: (callback, delay) => window.setTimeout(callback, delay),
66 clearTimer: timer => { if (timer !== undefined) window.clearTimeout(timer as number); },
67 });
68 return () => { version++; off(); };
69 }
70
70 lines TYPESCRIPT