@triflux/remote 10.28.0 → 10.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hub/server.mjs +44 -1
- package/hub/team/synapse-http.mjs +114 -13
- package/hub/team/synapse-registry.mjs +187 -10
- package/hub/workers/codex-app-server-worker.mjs +4 -1
- package/hub/workers/lib/jsonrpc-core.mjs +299 -0
- package/hub/workers/lib/jsonrpc-stdio.mjs +17 -276
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +479 -0
- package/package.json +1 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// hub/workers/lib/jsonrpc-ws-uds.mjs
|
|
2
|
+
// JSON-RPC 2.0 client over WebSocket-over-Unix-Domain-Socket.
|
|
3
|
+
//
|
|
4
|
+
// codex `app-server --listen unix://PATH` does NOT speak newline-delimited JSON
|
|
5
|
+
// on the socket — it speaks WebSocket (RFC 6455) after a standard HTTP/1.1
|
|
6
|
+
// Upgrade handshake, one JSON-RPC message per text frame. Confirmed live against
|
|
7
|
+
// codex-cli 0.135.0 (see experiments/native-bridge-feasibility/
|
|
8
|
+
// codex-app-server-uds-probe.mjs). This is the UDS sibling of the stdio
|
|
9
|
+
// `JsonRpcStdioClient`: same public surface (request/notify/onNotification/
|
|
10
|
+
// close/isOpen), different framing.
|
|
11
|
+
//
|
|
12
|
+
// Wire framing notes:
|
|
13
|
+
// - client -> server frames MUST be masked (RFC 6455 §5.3).
|
|
14
|
+
// - server -> client frames are unmasked.
|
|
15
|
+
// - one JSON-RPC object per text frame; fragmented frames are reassembled.
|
|
16
|
+
// - outbound JSON-RPC omits the `"jsonrpc":"2.0"` header (OpenAI App Server
|
|
17
|
+
// JSONL variant), matching JsonRpcStdioClient.
|
|
18
|
+
//
|
|
19
|
+
// Zero new dependencies — minimal hand-rolled WS client (no `ws` package).
|
|
20
|
+
|
|
21
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
22
|
+
import net from "node:net";
|
|
23
|
+
|
|
24
|
+
import { JsonRpcDispatchBase } from "./jsonrpc-core.mjs";
|
|
25
|
+
import {
|
|
26
|
+
JsonRpcProtocolError,
|
|
27
|
+
JsonRpcTransportError,
|
|
28
|
+
} from "./jsonrpc-stdio.mjs";
|
|
29
|
+
|
|
30
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
31
|
+
const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024; // 16 MiB
|
|
32
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
33
|
+
// After a graceful close("closing") we wait this long for the peer's close/EOF
|
|
34
|
+
// before force-destroying the socket so a silent peer cannot pin it open.
|
|
35
|
+
const CLOSING_DEADLINE_MS = 2_000;
|
|
36
|
+
const MAX_CONTROL_PAYLOAD = 125; // RFC 6455 §5.5: control frames <=125 bytes
|
|
37
|
+
const CLOSED_MESSAGE = "JsonRpcWsUdsClient closed";
|
|
38
|
+
|
|
39
|
+
// RFC 6455 opcodes
|
|
40
|
+
const OP_CONTINUATION = 0x0;
|
|
41
|
+
const OP_TEXT = 0x1;
|
|
42
|
+
const OP_BINARY = 0x2;
|
|
43
|
+
const OP_CLOSE = 0x8;
|
|
44
|
+
const OP_PING = 0x9;
|
|
45
|
+
const OP_PONG = 0xa;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Encode a single client->server WebSocket frame (always masked).
|
|
49
|
+
* @param {number} opcode
|
|
50
|
+
* @param {Buffer} payload
|
|
51
|
+
* @returns {Buffer}
|
|
52
|
+
*/
|
|
53
|
+
function encodeClientFrame(opcode, payload) {
|
|
54
|
+
const len = payload.length;
|
|
55
|
+
const mask = randomBytes(4);
|
|
56
|
+
let header;
|
|
57
|
+
if (len < 126) {
|
|
58
|
+
header = Buffer.from([0x80 | opcode, 0x80 | len]);
|
|
59
|
+
} else if (len < 65536) {
|
|
60
|
+
header = Buffer.alloc(4);
|
|
61
|
+
header[0] = 0x80 | opcode;
|
|
62
|
+
header[1] = 0x80 | 126;
|
|
63
|
+
header.writeUInt16BE(len, 2);
|
|
64
|
+
} else {
|
|
65
|
+
header = Buffer.alloc(10);
|
|
66
|
+
header[0] = 0x80 | opcode;
|
|
67
|
+
header[1] = 0x80 | 127;
|
|
68
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
69
|
+
}
|
|
70
|
+
const masked = Buffer.allocUnsafe(len);
|
|
71
|
+
for (let i = 0; i < len; i++) masked[i] = payload[i] ^ mask[i % 4];
|
|
72
|
+
return Buffer.concat([header, mask, masked]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* JSON-RPC 2.0 client over WebSocket-over-UDS.
|
|
77
|
+
*/
|
|
78
|
+
export class JsonRpcWsUdsClient extends JsonRpcDispatchBase {
|
|
79
|
+
/**
|
|
80
|
+
* @param {object} options
|
|
81
|
+
* @param {string} options.socketPath Absolute path to the AF_UNIX socket.
|
|
82
|
+
* @param {(err: Error) => void} [options.onError]
|
|
83
|
+
* @param {number} [options.maxFrameSize] Max bytes per inbound frame payload.
|
|
84
|
+
* @param {number} [options.connectTimeoutMs]
|
|
85
|
+
*/
|
|
86
|
+
constructor({
|
|
87
|
+
socketPath,
|
|
88
|
+
onError,
|
|
89
|
+
maxFrameSize = DEFAULT_MAX_FRAME_SIZE,
|
|
90
|
+
connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
|
|
91
|
+
} = {}) {
|
|
92
|
+
if (typeof socketPath !== "string" || !socketPath) {
|
|
93
|
+
throw new TypeError("JsonRpcWsUdsClient requires a socketPath string");
|
|
94
|
+
}
|
|
95
|
+
super({ onError, closedMessage: CLOSED_MESSAGE });
|
|
96
|
+
this._socketPath = socketPath;
|
|
97
|
+
this._maxFrameSize =
|
|
98
|
+
Number.isFinite(maxFrameSize) && maxFrameSize > 0
|
|
99
|
+
? maxFrameSize
|
|
100
|
+
: DEFAULT_MAX_FRAME_SIZE;
|
|
101
|
+
this._connectTimeoutMs = Number.isFinite(connectTimeoutMs)
|
|
102
|
+
? connectTimeoutMs
|
|
103
|
+
: DEFAULT_CONNECT_TIMEOUT_MS;
|
|
104
|
+
|
|
105
|
+
/** @type {'idle'|'running'|'closing'|'closed'} */
|
|
106
|
+
this._state = "idle";
|
|
107
|
+
this._socket = null;
|
|
108
|
+
|
|
109
|
+
// RX framing state
|
|
110
|
+
this._rxBuf = Buffer.alloc(0);
|
|
111
|
+
/** @type {Buffer[]} accumulated payloads of a fragmented message */
|
|
112
|
+
this._fragmentChunks = [];
|
|
113
|
+
/** @type {number|null} opcode of the in-progress fragmented message */
|
|
114
|
+
this._fragmentOpcode = null;
|
|
115
|
+
this._fragmentSize = 0;
|
|
116
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
117
|
+
this._closingTimer = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Open the socket and complete the WebSocket HTTP Upgrade handshake.
|
|
122
|
+
* Resolves once the connection is in the `running` state.
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
connect() {
|
|
126
|
+
if (this._state !== "idle") {
|
|
127
|
+
return Promise.reject(
|
|
128
|
+
new Error(
|
|
129
|
+
`JsonRpcWsUdsClient.connect() invalid in state ${this._state}`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const key = randomBytes(16).toString("base64");
|
|
135
|
+
const expectedAccept = createHash("sha1")
|
|
136
|
+
.update(key + WS_GUID)
|
|
137
|
+
.digest("base64");
|
|
138
|
+
let handshakeBuf = Buffer.alloc(0);
|
|
139
|
+
let settled = false;
|
|
140
|
+
|
|
141
|
+
const socket = net.connect(this._socketPath);
|
|
142
|
+
this._socket = socket;
|
|
143
|
+
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
if (settled) return;
|
|
146
|
+
settled = true;
|
|
147
|
+
try {
|
|
148
|
+
socket.destroy();
|
|
149
|
+
} catch {}
|
|
150
|
+
this._state = "closed";
|
|
151
|
+
reject(
|
|
152
|
+
new JsonRpcTransportError(
|
|
153
|
+
`WebSocket-over-UDS connect timed out after ${this._connectTimeoutMs}ms: ${this._socketPath}`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}, this._connectTimeoutMs);
|
|
157
|
+
|
|
158
|
+
const onConnectError = (err) => {
|
|
159
|
+
if (settled) return;
|
|
160
|
+
settled = true;
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
this._state = "closed";
|
|
163
|
+
reject(
|
|
164
|
+
new JsonRpcTransportError(
|
|
165
|
+
`WebSocket-over-UDS connect error: ${err?.message || err}`,
|
|
166
|
+
{ cause: err },
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
socket.once("error", onConnectError);
|
|
172
|
+
socket.on("connect", () => {
|
|
173
|
+
const req =
|
|
174
|
+
"GET / HTTP/1.1\r\n" +
|
|
175
|
+
"Host: localhost\r\n" +
|
|
176
|
+
"Upgrade: websocket\r\n" +
|
|
177
|
+
"Connection: Upgrade\r\n" +
|
|
178
|
+
`Sec-WebSocket-Key: ${key}\r\n` +
|
|
179
|
+
"Sec-WebSocket-Version: 13\r\n\r\n";
|
|
180
|
+
socket.write(req);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const onHandshakeData = (chunk) => {
|
|
184
|
+
if (settled) return;
|
|
185
|
+
handshakeBuf = Buffer.concat([handshakeBuf, chunk]);
|
|
186
|
+
const sep = handshakeBuf.indexOf("\r\n\r\n");
|
|
187
|
+
if (sep < 0) {
|
|
188
|
+
if (handshakeBuf.length > 64 * 1024) {
|
|
189
|
+
settled = true;
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
socket.removeListener("error", onConnectError);
|
|
192
|
+
socket.removeListener("data", onHandshakeData);
|
|
193
|
+
try {
|
|
194
|
+
socket.destroy();
|
|
195
|
+
} catch {}
|
|
196
|
+
this._state = "closed";
|
|
197
|
+
reject(
|
|
198
|
+
new JsonRpcProtocolError(
|
|
199
|
+
"WebSocket handshake response headers exceeded 64 KiB",
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const rawHeaders = handshakeBuf.subarray(0, sep).toString("utf8");
|
|
206
|
+
const leftover = handshakeBuf.subarray(sep + 4);
|
|
207
|
+
|
|
208
|
+
const lines = rawHeaders.split("\r\n");
|
|
209
|
+
const ok101 = /^HTTP\/1\.\d 101/.test(lines[0] || "");
|
|
210
|
+
const headerMap = new Map();
|
|
211
|
+
for (let i = 1; i < lines.length; i++) {
|
|
212
|
+
const idx = lines[i].indexOf(":");
|
|
213
|
+
if (idx < 0) continue;
|
|
214
|
+
headerMap.set(
|
|
215
|
+
lines[i].slice(0, idx).trim().toLowerCase(),
|
|
216
|
+
lines[i].slice(idx + 1).trim(),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const acceptOk =
|
|
220
|
+
headerMap.get("sec-websocket-accept") === expectedAccept;
|
|
221
|
+
|
|
222
|
+
if (!ok101 || !acceptOk) {
|
|
223
|
+
settled = true;
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
socket.removeListener("error", onConnectError);
|
|
226
|
+
socket.removeListener("data", onHandshakeData);
|
|
227
|
+
try {
|
|
228
|
+
socket.destroy();
|
|
229
|
+
} catch {}
|
|
230
|
+
this._state = "closed";
|
|
231
|
+
reject(
|
|
232
|
+
new JsonRpcProtocolError(
|
|
233
|
+
`WebSocket upgrade rejected (status101=${ok101}, acceptValid=${acceptOk})`,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Handshake complete — switch to running and wire frame handling.
|
|
240
|
+
settled = true;
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
socket.removeListener("error", onConnectError);
|
|
243
|
+
socket.removeListener("data", onHandshakeData);
|
|
244
|
+
this._state = "running";
|
|
245
|
+
|
|
246
|
+
socket.on("data", (buf) => this._onSocketData(buf));
|
|
247
|
+
socket.on("error", (err) => this._handleStreamError("socket", err));
|
|
248
|
+
socket.on("close", () => {
|
|
249
|
+
if (this._state === "running") {
|
|
250
|
+
const err = new JsonRpcTransportError(
|
|
251
|
+
"WebSocket-over-UDS closed unexpectedly (EOF during running)",
|
|
252
|
+
);
|
|
253
|
+
this._emitError(err);
|
|
254
|
+
this._closeWith("closed", err);
|
|
255
|
+
} else if (this._state !== "closed") {
|
|
256
|
+
this._closeWith("closed");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (leftover.length > 0) {
|
|
261
|
+
this._rxBuf = Buffer.concat([this._rxBuf, leftover]);
|
|
262
|
+
this._drainFrames();
|
|
263
|
+
}
|
|
264
|
+
resolve();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
socket.on("data", onHandshakeData);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- internals ---------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
_encodeAndSend(frame) {
|
|
274
|
+
this._sendText(JSON.stringify(frame));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_onEnterClosing() {
|
|
278
|
+
// best-effort WS close frame
|
|
279
|
+
try {
|
|
280
|
+
this._socket?.write(encodeClientFrame(OP_CLOSE, Buffer.alloc(0)));
|
|
281
|
+
} catch {}
|
|
282
|
+
// Arm a deadline so a peer that never replies with close/EOF cannot pin
|
|
283
|
+
// the socket + pending requests open forever.
|
|
284
|
+
this._closingTimer = setTimeout(() => {
|
|
285
|
+
if (this._state === "closing") this._closeWith("closed");
|
|
286
|
+
}, CLOSING_DEADLINE_MS);
|
|
287
|
+
if (typeof this._closingTimer.unref === "function") {
|
|
288
|
+
this._closingTimer.unref();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_beforeRejectPending() {
|
|
293
|
+
if (this._closingTimer) {
|
|
294
|
+
clearTimeout(this._closingTimer);
|
|
295
|
+
this._closingTimer = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_teardownTransport() {
|
|
300
|
+
try {
|
|
301
|
+
this._socket?.destroy();
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_sendText(text) {
|
|
306
|
+
if (this._state === "closed") throw new Error(CLOSED_MESSAGE);
|
|
307
|
+
const frame = encodeClientFrame(OP_TEXT, Buffer.from(text, "utf8"));
|
|
308
|
+
this._socket.write(frame, (err) => {
|
|
309
|
+
if (err) this._handleStreamError("socket-write", err);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_onSocketData(chunk) {
|
|
314
|
+
if (this._state === "closed") return;
|
|
315
|
+
this._rxBuf = Buffer.concat([this._rxBuf, chunk]);
|
|
316
|
+
this._drainFrames();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Parse as many complete WebSocket frames as are buffered. Control frames
|
|
321
|
+
* (ping/pong/close) are handled inline; text/continuation frames are
|
|
322
|
+
* reassembled into a full JSON-RPC message before dispatch.
|
|
323
|
+
*/
|
|
324
|
+
_drainFrames() {
|
|
325
|
+
while (this._rxBuf.length >= 2) {
|
|
326
|
+
const b0 = this._rxBuf[0];
|
|
327
|
+
const b1 = this._rxBuf[1];
|
|
328
|
+
const fin = (b0 & 0x80) !== 0;
|
|
329
|
+
const opcode = b0 & 0x0f;
|
|
330
|
+
const masked = (b1 & 0x80) !== 0;
|
|
331
|
+
let len = b1 & 0x7f;
|
|
332
|
+
let cursor = 2;
|
|
333
|
+
|
|
334
|
+
if (len === 126) {
|
|
335
|
+
if (this._rxBuf.length < cursor + 2) return;
|
|
336
|
+
len = this._rxBuf.readUInt16BE(cursor);
|
|
337
|
+
cursor += 2;
|
|
338
|
+
} else if (len === 127) {
|
|
339
|
+
if (this._rxBuf.length < cursor + 8) return;
|
|
340
|
+
const big = this._rxBuf.readBigUInt64BE(cursor);
|
|
341
|
+
if (big > BigInt(this._maxFrameSize)) {
|
|
342
|
+
const err = new JsonRpcProtocolError(
|
|
343
|
+
`WebSocket frame payload ${big} exceeds max ${this._maxFrameSize}`,
|
|
344
|
+
);
|
|
345
|
+
this._emitError(err);
|
|
346
|
+
this._closeWith("closed", err);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
len = Number(big);
|
|
350
|
+
cursor += 8;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (len > this._maxFrameSize) {
|
|
354
|
+
const err = new JsonRpcProtocolError(
|
|
355
|
+
`WebSocket frame payload ${len} exceeds max ${this._maxFrameSize}`,
|
|
356
|
+
);
|
|
357
|
+
this._emitError(err);
|
|
358
|
+
this._closeWith("closed", err);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// RFC 6455 §5.1: server -> client frames MUST NOT be masked.
|
|
363
|
+
if (masked) {
|
|
364
|
+
this._protocolClose("WebSocket server frame is masked (RFC 6455 §5.1)");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (this._rxBuf.length < cursor + len) return; // wait for full payload
|
|
368
|
+
|
|
369
|
+
// Detach from rxBuf before we slice it off.
|
|
370
|
+
const payload = Buffer.from(this._rxBuf.subarray(cursor, cursor + len));
|
|
371
|
+
this._rxBuf = this._rxBuf.subarray(cursor + len);
|
|
372
|
+
|
|
373
|
+
this._handleFrame(fin, opcode, payload);
|
|
374
|
+
if (this._state === "closed") return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_handleFrame(fin, opcode, payload) {
|
|
379
|
+
// Control frames (>=0x8): must be FIN and <=125 bytes (RFC 6455 §5.5).
|
|
380
|
+
if (opcode >= 0x8) {
|
|
381
|
+
if (!fin || payload.length > MAX_CONTROL_PAYLOAD) {
|
|
382
|
+
this._protocolClose(
|
|
383
|
+
`Invalid control frame (opcode 0x${opcode.toString(16)}, fin=${fin}, len=${payload.length})`,
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (opcode === OP_PING) {
|
|
388
|
+
try {
|
|
389
|
+
this._socket?.write(encodeClientFrame(OP_PONG, payload));
|
|
390
|
+
} catch {}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (opcode === OP_PONG) return;
|
|
394
|
+
if (opcode === OP_CLOSE) {
|
|
395
|
+
if (this._state === "running") {
|
|
396
|
+
const err = new JsonRpcTransportError(
|
|
397
|
+
"WebSocket server sent close frame",
|
|
398
|
+
);
|
|
399
|
+
this._emitError(err);
|
|
400
|
+
this._closeWith("closed", err);
|
|
401
|
+
} else {
|
|
402
|
+
this._closeWith("closed");
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this._protocolClose(`Unknown control opcode 0x${opcode.toString(16)}`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Data frames: text / binary (message start) or continuation.
|
|
411
|
+
if (opcode === OP_TEXT || opcode === OP_BINARY) {
|
|
412
|
+
if (this._fragmentOpcode !== null) {
|
|
413
|
+
this._protocolClose(
|
|
414
|
+
"New data frame received while a fragmented message is active",
|
|
415
|
+
);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (opcode === OP_BINARY) {
|
|
419
|
+
this._protocolClose(
|
|
420
|
+
"Binary frames are not supported (expected text JSON-RPC)",
|
|
421
|
+
);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (fin) {
|
|
425
|
+
this._handleInboundMessage(payload.toString("utf8"));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this._fragmentOpcode = opcode;
|
|
429
|
+
this._fragmentChunks = [payload];
|
|
430
|
+
this._fragmentSize = payload.length;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (opcode === OP_CONTINUATION) {
|
|
435
|
+
if (this._fragmentOpcode === null) {
|
|
436
|
+
this._protocolClose(
|
|
437
|
+
"Continuation frame with no active fragmented message",
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
this._fragmentSize += payload.length;
|
|
442
|
+
if (this._fragmentSize > this._maxFrameSize) {
|
|
443
|
+
this._protocolClose(
|
|
444
|
+
`Fragmented message ${this._fragmentSize} exceeds max ${this._maxFrameSize}`,
|
|
445
|
+
);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this._fragmentChunks.push(payload);
|
|
449
|
+
if (!fin) return;
|
|
450
|
+
const full = Buffer.concat(this._fragmentChunks);
|
|
451
|
+
this._fragmentChunks = [];
|
|
452
|
+
this._fragmentOpcode = null;
|
|
453
|
+
this._fragmentSize = 0;
|
|
454
|
+
this._handleInboundMessage(full.toString("utf8"));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this._protocolClose(`Unknown WebSocket opcode 0x${opcode.toString(16)}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_protocolClose(message) {
|
|
462
|
+
const err = new JsonRpcProtocolError(message);
|
|
463
|
+
this._emitError(err);
|
|
464
|
+
this._closeWith("closed", err);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_handleStreamError(which, err) {
|
|
468
|
+
if (this._state === "closed") return;
|
|
469
|
+
const base = err instanceof Error ? err : new Error(String(err));
|
|
470
|
+
const wrapped = new JsonRpcTransportError(
|
|
471
|
+
`WebSocket-over-UDS stream error on ${which}: ${base.message}`,
|
|
472
|
+
{ cause: base },
|
|
473
|
+
);
|
|
474
|
+
this._emitError(wrapped);
|
|
475
|
+
this._closeWith("closed", wrapped);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export default JsonRpcWsUdsClient;
|