返回 DeepSeek-Reasonix
eventStream.ts
根目录 / desktop / electron / src / shared / eventStream.ts
1 import type { EventFrame, ServiceState } from "./ipc.js";
2
3 export interface EventRecovery {
4 generation: string;
5 reason: "generation" | "gap" | "subscription";
6 expectedSeq: number;
7 actualSeq: number;
8 }
9
10 // Dynamic remote-tab event names must not accumulate for the renderer's
11 // lifetime. Once the cap is reached, retain no names and conservatively
12 // repair every new subscription until the next service generation.
13 export class MissedEventSubscriptions {
14 private readonly names = new Set<string>();
15 private overflow = false;
16 get size(): number { return this.names.size; }
17 add(name: string): void {
18 if (this.overflow || this.names.has(name)) return;
19 if (this.names.size === 64) { this.names.clear(); this.overflow = true; }
20 else this.names.add(name);
21 }
22 consume(name: string): boolean { return this.names.delete(name) || this.overflow; }
23 clear(): void { this.names.clear(); this.overflow = false; }
24 }
25
26 export function eventFrame(value: unknown, allowShellEvent = false): EventFrame | null {
27 if (typeof value !== "object" || value === null) return null;
28 const frame = value as Partial<EventFrame>;
29 if (!Number.isSafeInteger(frame.seq) || (frame.seq ?? -1) < 0 || typeof frame.generation !== "string"
30 || typeof frame.name !== "string" || !Array.isArray(frame.args)) return null;
31 if (frame.seq === 0 && (!allowShellEvent || !frame.name.startsWith("app:"))) return null;
32 return frame as EventFrame;
33 }
34
35 // The transport cursor is independent of individual event subscriptions. A
36 // gap requests authoritative read-side repair; it never replays an invocation.
37 export class DesktopEventStream {
38 private generation = "";
39 private seq = 0;
40 private lastRecovery: EventRecovery | null = null;
41
42 constructor(private readonly deliver: (frame: EventFrame) => void, private readonly recover: (event: EventRecovery) => void) {}
43
44 get recovery(): EventRecovery | null { return this.lastRecovery; }
45
46 observeState(state: ServiceState): void {
47 const generation = state.phase === "ready" ? state.generation : "";
48 if (generation === this.generation) return;
49 this.generation = generation;
50 this.seq = 0;
51 this.lastRecovery = null;
52 if (generation !== "") this.requestRecovery("generation");
53 }
54
55 requestRecovery(reason: EventRecovery["reason"], actualSeq = this.seq): void {
56 if (this.generation === "") return;
57 const event: EventRecovery = { generation: this.generation, reason, expectedSeq: this.seq + 1, actualSeq };
58 this.lastRecovery = event;
59 this.recover(event);
60 }
61
62 accept(value: unknown): boolean {
63 const frame = eventFrame(value, true);
64 if (!frame || this.generation === "" || frame.generation !== this.generation) return false;
65 if (frame.seq !== 0) {
66 if (frame.seq <= this.seq) return false;
67 if (frame.seq > this.seq + 1) this.requestRecovery("gap", frame.seq);
68 this.seq = frame.seq;
69 }
70 this.deliver(frame);
71 return true;
72 }
73 }
74
74 lines TYPESCRIPT