返回 DeepSeek-Reasonix
rpc.ts
根目录 / desktop / electron / src / main / rpc.ts
1 export const MAX_FRAME_BYTES = 64 * 1024 * 1024;
2
3 export class OversizeFrameError extends Error {
4 constructor(bytes: number, limit: number) {
5 super(`protocol frame of ${bytes} bytes exceeds the ${limit} byte limit`);
6 this.name = "OversizeFrameError";
7 }
8 }
9
10 export class LineDecoder {
11 private chunks: Buffer[] = [];
12 private buffered = 0;
13
14 constructor(private readonly limit = MAX_FRAME_BYTES) {}
15
16 push(chunk: Buffer): string[] {
17 const lines: string[] = [];
18 let start = 0;
19 for (let i = 0; i < chunk.length; i++) {
20 if (chunk[i] !== 0x0a) continue;
21 const tail = chunk.subarray(start, i);
22 const total = this.buffered + tail.length;
23 if (total > this.limit) {
24 this.reset();
25 throw new OversizeFrameError(total, this.limit);
26 }
27 const line = this.chunks.length ? Buffer.concat([...this.chunks, tail]).toString("utf8") : tail.toString("utf8");
28 this.reset();
29 lines.push(line.endsWith("\r") ? line.slice(0, -1) : line);
30 start = i + 1;
31 }
32 if (start < chunk.length) {
33 const rest = chunk.subarray(start);
34 this.buffered += rest.length;
35 if (this.buffered > this.limit) {
36 const bytes = this.buffered;
37 this.reset();
38 throw new OversizeFrameError(bytes, this.limit);
39 }
40 this.chunks.push(Buffer.from(rest));
41 }
42 return lines;
43 }
44
45 private reset(): void {
46 this.chunks = [];
47 this.buffered = 0;
48 }
49 }
50
51 export class RpcError extends Error {
52 constructor(readonly code: number, message: string, readonly data?: unknown) {
53 super(message);
54 this.name = "RpcError";
55 }
56 }
57
58 export interface RpcTransport {
59 write(line: string): void;
60 }
61
62 export interface RpcHandlers {
63 onRequest(method: string, params: unknown): Promise<unknown>;
64 onNotification(method: string, params: unknown): void;
65 onProtocolError?(kind: "non-json" | "invalid" | "orphan-response", line: string): void;
66 }
67
68 interface Pending {
69 resolve(value: unknown): void;
70 reject(error: Error): void;
71 timer: NodeJS.Timeout | null;
72 }
73
74 function isRecord(value: unknown): value is Record<string, unknown> {
75 return typeof value === "object" && value !== null && !Array.isArray(value);
76 }
77
78 export class RpcClient {
79 private nextId = 1;
80 private readonly pending = new Map<number, Pending>();
81 private readonly decoder: LineDecoder;
82 private closedWith: Error | null = null;
83 readonly stats = { ignoredLines: 0, orphanResponses: 0 };
84
85 constructor(private readonly transport: RpcTransport, private readonly handlers: RpcHandlers, limit = MAX_FRAME_BYTES) {
86 this.decoder = new LineDecoder(limit);
87 }
88
89 get closed(): boolean {
90 return this.closedWith !== null;
91 }
92
93 get pendingCount(): number {
94 return this.pending.size;
95 }
96
97 // Feeds raw stdout bytes; throws OversizeFrameError when the stream cannot
98 // be resynchronised, after rejecting everything in flight.
99 feed(chunk: Buffer): void {
100 let lines: string[];
101 try {
102 lines = this.decoder.push(chunk);
103 } catch (error) {
104 this.close(error instanceof Error ? error : new Error(String(error)));
105 throw error;
106 }
107 for (const line of lines) this.dispatch(line);
108 }
109
110 request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {
111 if (this.closedWith) return Promise.reject(this.closedWith);
112 const id = this.nextId++;
113 return new Promise((resolve, reject) => {
114 const timer = timeoutMs && timeoutMs > 0
115 ? setTimeout(() => {
116 this.pending.delete(id);
117 reject(new RpcError(-32000, `${method} timed out after ${timeoutMs} ms`));
118 }, timeoutMs)
119 : null;
120 this.pending.set(id, { resolve, reject, timer });
121 try {
122 this.transport.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
123 } catch (error) {
124 this.pending.delete(id);
125 if (timer) clearTimeout(timer);
126 reject(error instanceof Error ? error : new Error(String(error)));
127 }
128 });
129 }
130
131 notify(method: string, params: unknown): void {
132 if (this.closedWith) return;
133 this.transport.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
134 }
135
136 close(error: Error): void {
137 if (this.closedWith) return;
138 this.closedWith = error;
139 for (const [id, entry] of this.pending) {
140 this.pending.delete(id);
141 if (entry.timer) clearTimeout(entry.timer);
142 entry.reject(error);
143 }
144 }
145
146 private dispatch(line: string): void {
147 if (line.trim() === "") return;
148 let frame: unknown;
149 try {
150 frame = JSON.parse(line);
151 } catch {
152 this.stats.ignoredLines++;
153 this.handlers.onProtocolError?.("non-json", line);
154 return;
155 }
156 if (!isRecord(frame) || frame.jsonrpc !== "2.0") {
157 this.stats.ignoredLines++;
158 this.handlers.onProtocolError?.("invalid", line);
159 return;
160 }
161 if (typeof frame.method === "string") {
162 if (frame.id === undefined || frame.id === null) {
163 this.handlers.onNotification(frame.method, frame.params);
164 return;
165 }
166 this.serve(frame.id as number | string, frame.method, frame.params);
167 return;
168 }
169 if (typeof frame.id !== "number") {
170 this.stats.ignoredLines++;
171 this.handlers.onProtocolError?.("invalid", line);
172 return;
173 }
174 const entry = this.pending.get(frame.id);
175 if (!entry) {
176 this.stats.orphanResponses++;
177 this.handlers.onProtocolError?.("orphan-response", line);
178 return;
179 }
180 this.pending.delete(frame.id);
181 if (entry.timer) clearTimeout(entry.timer);
182 if (isRecord(frame.error)) {
183 const code = typeof frame.error.code === "number" ? frame.error.code : -32000;
184 const message = typeof frame.error.message === "string" ? frame.error.message : "unknown error";
185 entry.reject(new RpcError(code, message, frame.error.data));
186 return;
187 }
188 entry.resolve(frame.result);
189 }
190
191 private serve(id: number | string, method: string, params: unknown): void {
192 this.handlers.onRequest(method, params).then(
193 (result) => this.reply({ jsonrpc: "2.0", id, result: result === undefined ? null : result }),
194 (error: unknown) => {
195 const code = error instanceof RpcError ? error.code : -32000;
196 const message = error instanceof Error ? error.message : String(error);
197 this.reply({ jsonrpc: "2.0", id, error: { code, message } });
198 },
199 );
200 }
201
202 private reply(frame: Record<string, unknown>): void {
203 if (this.closedWith) return;
204 try {
205 this.transport.write(JSON.stringify(frame) + "\n");
206 } catch {
207 // The transport owner observes the broken pipe through the process exit.
208 }
209 }
210 }
211
211 lines TYPESCRIPT