@harapter/transport-jsonrpc-stdio 0.1.1
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/LICENSE +201 -0
- package/README.md +163 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +829 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { setImmediate as scheduleImmediate } from 'node:timers';
|
|
2
|
+
import { inspect } from 'node:util';
|
|
3
|
+
const defaultMaxMessageBytes = 1024 * 1024;
|
|
4
|
+
const defaultMaxBufferedMessages = 128;
|
|
5
|
+
const defaultMaxPendingRequests = 128;
|
|
6
|
+
const defaultMaxPendingInboundRequests = 128;
|
|
7
|
+
const defaultMaxPendingWrites = 128;
|
|
8
|
+
const defaultRequestTimeoutMs = 30_000;
|
|
9
|
+
const maximumTimerMilliseconds = 2_147_483_647;
|
|
10
|
+
/** Safe transport failure that never includes a frame or stream error body. */
|
|
11
|
+
export class JsonRpcTransportError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'JsonRpcTransportError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Safe remote failure with raw fields behind an explicit extraction method. */
|
|
20
|
+
export class JsonRpcRemoteError extends Error {
|
|
21
|
+
#remoteError;
|
|
22
|
+
constructor(error) {
|
|
23
|
+
super('Remote JSON-RPC request failed.');
|
|
24
|
+
Object.defineProperty(this, 'name', { value: 'JsonRpcRemoteError' });
|
|
25
|
+
this.#remoteError = Object.prototype.hasOwnProperty.call(error, 'data')
|
|
26
|
+
? { code: error.code, data: error.data, message: error.message }
|
|
27
|
+
: { code: error.code, message: error.message };
|
|
28
|
+
}
|
|
29
|
+
/** Explicitly extract untrusted fields for Provider validation and redaction. */
|
|
30
|
+
getRemoteError() {
|
|
31
|
+
return this.#remoteError;
|
|
32
|
+
}
|
|
33
|
+
/** Keep generic JSON error logging bounded and content-free. */
|
|
34
|
+
toJSON() {
|
|
35
|
+
return { message: this.message, name: this.name };
|
|
36
|
+
}
|
|
37
|
+
/** Keep Node inspection bounded and content-free. */
|
|
38
|
+
[inspect.custom]() {
|
|
39
|
+
return `${this.name}: ${this.message}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
class InboundQueue {
|
|
43
|
+
capacity;
|
|
44
|
+
values = [];
|
|
45
|
+
waiter;
|
|
46
|
+
closed = false;
|
|
47
|
+
failure;
|
|
48
|
+
constructor(capacity) {
|
|
49
|
+
this.capacity = capacity;
|
|
50
|
+
}
|
|
51
|
+
push(value) {
|
|
52
|
+
if (this.closed)
|
|
53
|
+
return false;
|
|
54
|
+
if (this.waiter) {
|
|
55
|
+
const waiter = this.waiter;
|
|
56
|
+
this.waiter = undefined;
|
|
57
|
+
waiter.resolve({ done: false, value });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (this.values.length >= this.capacity)
|
|
61
|
+
return false;
|
|
62
|
+
this.values.push(value);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
next() {
|
|
66
|
+
const value = this.values.shift();
|
|
67
|
+
if (value)
|
|
68
|
+
return Promise.resolve({ done: false, value });
|
|
69
|
+
if (this.failure)
|
|
70
|
+
return Promise.reject(this.failure);
|
|
71
|
+
if (this.closed)
|
|
72
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
73
|
+
if (this.waiter) {
|
|
74
|
+
return Promise.reject(transportError('consumer_conflict', 'Only one inbound read may be pending at a time.'));
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
this.waiter = { resolve, reject };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
close(failure) {
|
|
81
|
+
if (this.closed)
|
|
82
|
+
return;
|
|
83
|
+
this.closed = true;
|
|
84
|
+
this.failure = failure;
|
|
85
|
+
this.values.length = 0;
|
|
86
|
+
if (!this.waiter)
|
|
87
|
+
return;
|
|
88
|
+
const waiter = this.waiter;
|
|
89
|
+
this.waiter = undefined;
|
|
90
|
+
if (failure)
|
|
91
|
+
waiter.reject(failure);
|
|
92
|
+
else
|
|
93
|
+
waiter.resolve({ done: true, value: undefined });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Bounded bidirectional JSONL RPC transport over caller-owned Node streams.
|
|
98
|
+
* It correlates responses but leaves Provider methods and lifecycle semantics
|
|
99
|
+
* to the consuming Adapter.
|
|
100
|
+
*/
|
|
101
|
+
export class JsonRpcStdioTransport {
|
|
102
|
+
readable;
|
|
103
|
+
writable;
|
|
104
|
+
cleanup;
|
|
105
|
+
emitJsonRpcVersion;
|
|
106
|
+
requireJsonRpcVersion;
|
|
107
|
+
requireIntegerNumericIds;
|
|
108
|
+
maxMessageBytes;
|
|
109
|
+
maxPendingRequests;
|
|
110
|
+
maxPendingInboundRequests;
|
|
111
|
+
maxPendingWrites;
|
|
112
|
+
requestTimeoutMs;
|
|
113
|
+
onDiagnostic;
|
|
114
|
+
inboundQueue;
|
|
115
|
+
pendingRequests = new Map();
|
|
116
|
+
pendingInboundRequestIds = new Set();
|
|
117
|
+
respondingInboundRequestIds = new Set();
|
|
118
|
+
abandoningInboundRequestIds = new Set();
|
|
119
|
+
terminalGuardStreams = new Set();
|
|
120
|
+
activeWriteRejectors = new Set();
|
|
121
|
+
lineChunks = [];
|
|
122
|
+
lineBytes = 0;
|
|
123
|
+
nextRequestId = 1;
|
|
124
|
+
pendingWrites = 0;
|
|
125
|
+
incomingClaimed = false;
|
|
126
|
+
inboundAcknowledgedSequence = 0;
|
|
127
|
+
inboundSequence = 0;
|
|
128
|
+
inboundBarrierWaiters = new Set();
|
|
129
|
+
open = true;
|
|
130
|
+
terminalError;
|
|
131
|
+
cleanupFailure;
|
|
132
|
+
cleanupPromise;
|
|
133
|
+
writeTail = Promise.resolve();
|
|
134
|
+
writableCallbackFailed = false;
|
|
135
|
+
handleReadableData = (chunk) => {
|
|
136
|
+
if (!this.open)
|
|
137
|
+
return;
|
|
138
|
+
if (typeof chunk !== 'string' &&
|
|
139
|
+
!Buffer.isBuffer(chunk) &&
|
|
140
|
+
!(chunk instanceof Uint8Array)) {
|
|
141
|
+
this.fail(transportError('stream_failed', 'The readable stream emitted an unsupported chunk.'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.consumeChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
145
|
+
};
|
|
146
|
+
handleReadableEnd = () => {
|
|
147
|
+
if (!this.open)
|
|
148
|
+
return;
|
|
149
|
+
this.fail(this.lineBytes > 0
|
|
150
|
+
? transportError('truncated_message', 'The readable stream ended with an incomplete JSONL frame.')
|
|
151
|
+
: transportError('stream_ended', 'The readable stream ended before the transport was closed.'));
|
|
152
|
+
};
|
|
153
|
+
handleStreamError = (_error) => {
|
|
154
|
+
this.fail(transportError('stream_failed', 'A transport stream reported a failure.'));
|
|
155
|
+
};
|
|
156
|
+
handleWritableError = (_error) => {
|
|
157
|
+
this.fail(this.writableCallbackFailed
|
|
158
|
+
? transportError('write_failed', 'The transport could not write a JSON-RPC frame.')
|
|
159
|
+
: transportError('stream_failed', 'A transport stream reported a failure.'));
|
|
160
|
+
};
|
|
161
|
+
handleStreamClose = () => {
|
|
162
|
+
if (!this.open)
|
|
163
|
+
return;
|
|
164
|
+
this.fail(transportError('stream_ended', 'A transport stream closed before the transport was closed.'));
|
|
165
|
+
};
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.maxMessageBytes = positiveInteger(options.maxMessageBytes ?? defaultMaxMessageBytes);
|
|
168
|
+
const maxBufferedMessages = positiveInteger(options.maxBufferedMessages ?? defaultMaxBufferedMessages);
|
|
169
|
+
this.maxPendingRequests = positiveInteger(options.maxPendingRequests ?? defaultMaxPendingRequests);
|
|
170
|
+
this.maxPendingInboundRequests = positiveInteger(options.maxPendingInboundRequests ?? defaultMaxPendingInboundRequests);
|
|
171
|
+
this.maxPendingWrites = positiveInteger(options.maxPendingWrites ?? defaultMaxPendingWrites);
|
|
172
|
+
this.requestTimeoutMs = timerMilliseconds(options.requestTimeoutMs ?? defaultRequestTimeoutMs);
|
|
173
|
+
this.readable = options.readable;
|
|
174
|
+
this.writable = options.writable;
|
|
175
|
+
this.cleanup = options.cleanup;
|
|
176
|
+
this.emitJsonRpcVersion = options.emitJsonRpcVersion ?? false;
|
|
177
|
+
this.requireJsonRpcVersion = options.requireJsonRpcVersion ?? false;
|
|
178
|
+
this.requireIntegerNumericIds = options.requireIntegerNumericIds ?? false;
|
|
179
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
180
|
+
this.inboundQueue = new InboundQueue(maxBufferedMessages);
|
|
181
|
+
this.readable.on('data', this.handleReadableData);
|
|
182
|
+
this.readable.once('end', this.handleReadableEnd);
|
|
183
|
+
this.readable.once('error', this.handleStreamError);
|
|
184
|
+
this.readable.once('close', this.handleStreamClose);
|
|
185
|
+
this.writable.once('error', this.handleWritableError);
|
|
186
|
+
this.writable.once('close', this.handleStreamClose);
|
|
187
|
+
}
|
|
188
|
+
/** Send a request and resolve it exactly once from its correlated response. */
|
|
189
|
+
request(method, params, options = {}) {
|
|
190
|
+
return this.requestInternal(method, params, options, false);
|
|
191
|
+
}
|
|
192
|
+
/** Resolve a request only after earlier inbound messages were consumed. */
|
|
193
|
+
requestAfterInbound(method, params, options = {}) {
|
|
194
|
+
return this.requestInternal(method, params, options, true);
|
|
195
|
+
}
|
|
196
|
+
requestInternal(method, params, options, waitForInbound) {
|
|
197
|
+
try {
|
|
198
|
+
this.assertOpen();
|
|
199
|
+
assertMethod(method);
|
|
200
|
+
if (options.signal?.aborted) {
|
|
201
|
+
throw transportError('request_aborted', 'The local request wait was aborted before the request was sent.');
|
|
202
|
+
}
|
|
203
|
+
if (this.pendingRequests.size >= this.maxPendingRequests) {
|
|
204
|
+
throw transportError('capacity_exceeded', 'The pending outbound request limit was reached.');
|
|
205
|
+
}
|
|
206
|
+
const timeoutMs = timerMilliseconds(options.timeoutMs ?? this.requestTimeoutMs);
|
|
207
|
+
const id = this.allocateRequestId();
|
|
208
|
+
const envelope = this.outboundEnvelope({ id, method, params });
|
|
209
|
+
const frame = this.encode(envelope);
|
|
210
|
+
this.assertOpen();
|
|
211
|
+
const response = new Promise((resolve, reject) => {
|
|
212
|
+
const abortListener = options.signal
|
|
213
|
+
? () => {
|
|
214
|
+
this.settlePending(id, (pending) => {
|
|
215
|
+
pending.reject(transportError('request_aborted', 'The local request wait was aborted.'));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
: undefined;
|
|
219
|
+
const timer = setTimeout(() => {
|
|
220
|
+
this.settlePending(id, (pending) => {
|
|
221
|
+
pending.reject(transportError('request_timeout', 'The JSON-RPC request timed out.'));
|
|
222
|
+
});
|
|
223
|
+
}, timeoutMs);
|
|
224
|
+
timer.unref();
|
|
225
|
+
this.pendingRequests.set(id, {
|
|
226
|
+
resolve,
|
|
227
|
+
reject,
|
|
228
|
+
timer,
|
|
229
|
+
signal: options.signal,
|
|
230
|
+
abortListener,
|
|
231
|
+
waitForInbound,
|
|
232
|
+
responseReceived: false,
|
|
233
|
+
writeStarted: false,
|
|
234
|
+
});
|
|
235
|
+
if (abortListener) {
|
|
236
|
+
options.signal?.addEventListener('abort', abortListener, {
|
|
237
|
+
once: true,
|
|
238
|
+
});
|
|
239
|
+
if (options.signal?.aborted)
|
|
240
|
+
abortListener();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
void this.enqueueWrite(frame, () => this.startPendingWrite(id)).catch((error) => {
|
|
244
|
+
this.settlePending(id, (pending) => {
|
|
245
|
+
pending.reject(asTransportError(error));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return response;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
return Promise.reject(asTransportError(error));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Send a notification and wait until its complete frame is flushed. */
|
|
255
|
+
notify(method, params) {
|
|
256
|
+
try {
|
|
257
|
+
this.assertOpen();
|
|
258
|
+
assertMethod(method);
|
|
259
|
+
return this.enqueueWrite(this.encode(this.outboundEnvelope({ method, params })));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return Promise.reject(asTransportError(error));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Iterate remote requests and notifications in wire order. */
|
|
266
|
+
incoming() {
|
|
267
|
+
if (this.incomingClaimed) {
|
|
268
|
+
throw transportError('consumer_conflict', 'The inbound message stream already has a consumer.');
|
|
269
|
+
}
|
|
270
|
+
this.incomingClaimed = true;
|
|
271
|
+
const source = this.iterateIncoming();
|
|
272
|
+
return {
|
|
273
|
+
[Symbol.asyncIterator]() {
|
|
274
|
+
return this;
|
|
275
|
+
},
|
|
276
|
+
next: () => source.next(),
|
|
277
|
+
return: async () => {
|
|
278
|
+
await this.close();
|
|
279
|
+
return source.return(undefined);
|
|
280
|
+
},
|
|
281
|
+
throw: async (error) => {
|
|
282
|
+
await this.close();
|
|
283
|
+
return source.throw(error);
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Respond successfully to one outstanding remote request. */
|
|
288
|
+
respond(id, result) {
|
|
289
|
+
if (result === undefined) {
|
|
290
|
+
return Promise.reject(transportError('invalid_outbound_message', 'A JSON-RPC response result cannot be undefined.'));
|
|
291
|
+
}
|
|
292
|
+
return this.respondWithEnvelope(id, { id, result });
|
|
293
|
+
}
|
|
294
|
+
/** Respond with a JSON-RPC error to one outstanding remote request. */
|
|
295
|
+
respondError(id, error) {
|
|
296
|
+
return this.respondWithEnvelope(id, { error, id }, 'error');
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Release a remote request that the Provider authoritatively resolved without
|
|
300
|
+
* a client response. No wire message is emitted.
|
|
301
|
+
*/
|
|
302
|
+
abandonInboundRequest(id) {
|
|
303
|
+
if (!isJsonRpcId(id, this.requireIntegerNumericIds))
|
|
304
|
+
return false;
|
|
305
|
+
const key = requestIdKey(id);
|
|
306
|
+
if (!this.pendingInboundRequestIds.has(key))
|
|
307
|
+
return false;
|
|
308
|
+
if (this.respondingInboundRequestIds.has(key)) {
|
|
309
|
+
this.abandoningInboundRequestIds.add(key);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
this.pendingInboundRequestIds.delete(key);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
/** Whether the logical transport can still accept operations. */
|
|
316
|
+
isOpen() {
|
|
317
|
+
return this.open;
|
|
318
|
+
}
|
|
319
|
+
/** Close the logical transport and run caller-provided cleanup once. */
|
|
320
|
+
async close() {
|
|
321
|
+
if (this.open) {
|
|
322
|
+
this.terminate(undefined, transportError('transport_closed', 'The transport was closed.'));
|
|
323
|
+
}
|
|
324
|
+
await this.cleanupPromise;
|
|
325
|
+
if (this.cleanupFailure)
|
|
326
|
+
throw this.cleanupFailure;
|
|
327
|
+
}
|
|
328
|
+
async *iterateIncoming() {
|
|
329
|
+
try {
|
|
330
|
+
for (;;) {
|
|
331
|
+
const next = await this.inboundQueue.next();
|
|
332
|
+
if (next.done)
|
|
333
|
+
return;
|
|
334
|
+
yield next.value.message;
|
|
335
|
+
this.acknowledgeInbound(next.value.sequence);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
if (this.open)
|
|
340
|
+
await this.close();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
respondWithEnvelope(id, envelope, responseKind = 'result') {
|
|
344
|
+
let reservedKey;
|
|
345
|
+
try {
|
|
346
|
+
this.assertOpen();
|
|
347
|
+
if (!isJsonRpcId(id, this.requireIntegerNumericIds)) {
|
|
348
|
+
throw transportError('invalid_outbound_message', 'A JSON-RPC response identifier is invalid.');
|
|
349
|
+
}
|
|
350
|
+
const key = requestIdKey(id);
|
|
351
|
+
if (!this.pendingInboundRequestIds.has(key) ||
|
|
352
|
+
this.respondingInboundRequestIds.has(key)) {
|
|
353
|
+
throw transportError('response_not_pending', 'No inbound request is awaiting that response.');
|
|
354
|
+
}
|
|
355
|
+
this.respondingInboundRequestIds.add(key);
|
|
356
|
+
reservedKey = key;
|
|
357
|
+
const frame = this.encode(this.outboundEnvelope(envelope));
|
|
358
|
+
assertSerializedResponse(frame, key, responseKind, this.requireIntegerNumericIds);
|
|
359
|
+
return this.enqueueWrite(frame).then(() => {
|
|
360
|
+
this.respondingInboundRequestIds.delete(key);
|
|
361
|
+
this.abandoningInboundRequestIds.delete(key);
|
|
362
|
+
this.pendingInboundRequestIds.delete(key);
|
|
363
|
+
}, (error) => {
|
|
364
|
+
this.respondingInboundRequestIds.delete(key);
|
|
365
|
+
if (this.abandoningInboundRequestIds.delete(key)) {
|
|
366
|
+
this.pendingInboundRequestIds.delete(key);
|
|
367
|
+
}
|
|
368
|
+
throw asTransportError(error);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (reservedKey) {
|
|
373
|
+
this.respondingInboundRequestIds.delete(reservedKey);
|
|
374
|
+
if (this.abandoningInboundRequestIds.delete(reservedKey)) {
|
|
375
|
+
this.pendingInboundRequestIds.delete(reservedKey);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return Promise.reject(asTransportError(error));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
outboundEnvelope(envelope) {
|
|
382
|
+
const output = this.emitJsonRpcVersion
|
|
383
|
+
? { jsonrpc: '2.0', ...envelope }
|
|
384
|
+
: { ...envelope };
|
|
385
|
+
if (output['params'] === undefined)
|
|
386
|
+
delete output['params'];
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
389
|
+
encode(envelope) {
|
|
390
|
+
let encoded;
|
|
391
|
+
try {
|
|
392
|
+
encoded = JSON.stringify(envelope);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
throw transportError('invalid_outbound_message', 'The outbound JSON-RPC message is not JSON serializable.');
|
|
396
|
+
}
|
|
397
|
+
if (!encoded) {
|
|
398
|
+
throw transportError('invalid_outbound_message', 'The outbound JSON-RPC message is invalid.');
|
|
399
|
+
}
|
|
400
|
+
if (Buffer.byteLength(encoded) > this.maxMessageBytes) {
|
|
401
|
+
throw transportError('message_too_large', 'The outbound JSON-RPC message exceeds the configured limit.');
|
|
402
|
+
}
|
|
403
|
+
return `${encoded}\n`;
|
|
404
|
+
}
|
|
405
|
+
enqueueWrite(frame, shouldWrite) {
|
|
406
|
+
if (this.pendingWrites >= this.maxPendingWrites) {
|
|
407
|
+
return Promise.reject(transportError('capacity_exceeded', 'The pending transport write limit was reached.'));
|
|
408
|
+
}
|
|
409
|
+
this.pendingWrites += 1;
|
|
410
|
+
const operation = this.writeTail.then(async () => {
|
|
411
|
+
this.assertOpen();
|
|
412
|
+
if (shouldWrite && !shouldWrite())
|
|
413
|
+
return;
|
|
414
|
+
await this.writeFrame(frame);
|
|
415
|
+
});
|
|
416
|
+
const tracked = operation.finally(() => {
|
|
417
|
+
this.pendingWrites -= 1;
|
|
418
|
+
});
|
|
419
|
+
this.writeTail = tracked.catch(() => undefined);
|
|
420
|
+
return tracked;
|
|
421
|
+
}
|
|
422
|
+
writeFrame(frame) {
|
|
423
|
+
return new Promise((resolve, reject) => {
|
|
424
|
+
let settled = false;
|
|
425
|
+
const rejectWrite = (error) => {
|
|
426
|
+
if (settled)
|
|
427
|
+
return;
|
|
428
|
+
settled = true;
|
|
429
|
+
this.activeWriteRejectors.delete(rejectWrite);
|
|
430
|
+
reject(error);
|
|
431
|
+
};
|
|
432
|
+
const resolveWrite = () => {
|
|
433
|
+
if (settled)
|
|
434
|
+
return;
|
|
435
|
+
settled = true;
|
|
436
|
+
this.activeWriteRejectors.delete(rejectWrite);
|
|
437
|
+
resolve();
|
|
438
|
+
};
|
|
439
|
+
this.activeWriteRejectors.add(rejectWrite);
|
|
440
|
+
try {
|
|
441
|
+
this.writable.write(frame, (error) => {
|
|
442
|
+
if (error) {
|
|
443
|
+
// Node emits the matching `error` event after this callback. Let
|
|
444
|
+
// that listener terminate the transport so the event cannot become
|
|
445
|
+
// unhandled when termination detaches stream listeners.
|
|
446
|
+
this.writableCallbackFailed = true;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
resolveWrite();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
this.fail(transportError('write_failed', 'The transport could not write a JSON-RPC frame.'));
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
consumeChunk(chunk) {
|
|
459
|
+
let segmentStart = 0;
|
|
460
|
+
for (let index = 0; index < chunk.length && this.open; index += 1) {
|
|
461
|
+
if (chunk[index] !== 0x0a)
|
|
462
|
+
continue;
|
|
463
|
+
if (!this.appendLineSegment(chunk.subarray(segmentStart, index)))
|
|
464
|
+
return;
|
|
465
|
+
this.consumeLine();
|
|
466
|
+
segmentStart = index + 1;
|
|
467
|
+
}
|
|
468
|
+
if (this.open && segmentStart < chunk.length) {
|
|
469
|
+
this.appendLineSegment(chunk.subarray(segmentStart));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
appendLineSegment(segment) {
|
|
473
|
+
if (segment.length === 0)
|
|
474
|
+
return true;
|
|
475
|
+
if (this.lineBytes + segment.length > this.maxMessageBytes) {
|
|
476
|
+
this.fail(transportError('message_too_large', 'An inbound JSONL frame exceeds the configured limit.'));
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
this.lineChunks.push(Buffer.from(segment));
|
|
480
|
+
this.lineBytes += segment.length;
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
consumeLine() {
|
|
484
|
+
let line = Buffer.concat(this.lineChunks, this.lineBytes);
|
|
485
|
+
this.lineChunks = [];
|
|
486
|
+
this.lineBytes = 0;
|
|
487
|
+
if (line.at(-1) === 0x0d)
|
|
488
|
+
line = line.subarray(0, -1);
|
|
489
|
+
let decoded;
|
|
490
|
+
try {
|
|
491
|
+
decoded = new TextDecoder('utf-8', { fatal: true }).decode(line);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
this.fail(transportError('malformed_message', 'An inbound frame is not UTF-8.'));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
let value;
|
|
498
|
+
try {
|
|
499
|
+
value = JSON.parse(decoded);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
this.fail(transportError('malformed_message', 'An inbound frame is not valid JSON.'));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.consumeEnvelope(value);
|
|
506
|
+
}
|
|
507
|
+
consumeEnvelope(value) {
|
|
508
|
+
if (!isRecord(value) ||
|
|
509
|
+
!validJsonRpcVersion(value, this.requireJsonRpcVersion)) {
|
|
510
|
+
this.fail(malformedEnvelope());
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (hasOwn(value, 'method')) {
|
|
514
|
+
this.consumeMethodEnvelope(value);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this.consumeResponseEnvelope(value);
|
|
518
|
+
}
|
|
519
|
+
consumeMethodEnvelope(envelope) {
|
|
520
|
+
if (typeof envelope['method'] !== 'string' ||
|
|
521
|
+
envelope['method'].length === 0 ||
|
|
522
|
+
hasOwn(envelope, 'result') ||
|
|
523
|
+
hasOwn(envelope, 'error')) {
|
|
524
|
+
this.fail(malformedEnvelope());
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const params = hasOwn(envelope, 'params')
|
|
528
|
+
? { params: envelope['params'] }
|
|
529
|
+
: {};
|
|
530
|
+
if (!hasOwn(envelope, 'id')) {
|
|
531
|
+
this.enqueueInbound({
|
|
532
|
+
kind: 'notification',
|
|
533
|
+
method: envelope['method'],
|
|
534
|
+
...params,
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const id = envelope['id'];
|
|
539
|
+
if (!isJsonRpcId(id, this.requireIntegerNumericIds)) {
|
|
540
|
+
this.fail(malformedEnvelope());
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const key = requestIdKey(id);
|
|
544
|
+
if (this.pendingInboundRequestIds.has(key) ||
|
|
545
|
+
this.pendingInboundRequestIds.size >= this.maxPendingInboundRequests) {
|
|
546
|
+
this.fail(this.pendingInboundRequestIds.has(key)
|
|
547
|
+
? malformedEnvelope()
|
|
548
|
+
: transportError('capacity_exceeded', 'The pending inbound request limit was reached.'));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
this.pendingInboundRequestIds.add(key);
|
|
552
|
+
this.enqueueInbound({
|
|
553
|
+
id,
|
|
554
|
+
kind: 'request',
|
|
555
|
+
method: envelope['method'],
|
|
556
|
+
...params,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
consumeResponseEnvelope(envelope) {
|
|
560
|
+
if (!hasOwn(envelope, 'id')) {
|
|
561
|
+
this.fail(malformedEnvelope());
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const hasResult = hasOwn(envelope, 'result');
|
|
565
|
+
const hasError = hasOwn(envelope, 'error');
|
|
566
|
+
if (hasResult === hasError) {
|
|
567
|
+
this.fail(malformedEnvelope());
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (hasError && !isJsonRpcErrorObject(envelope['error'])) {
|
|
571
|
+
this.fail(malformedEnvelope());
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const id = envelope['id'];
|
|
575
|
+
if (id === null) {
|
|
576
|
+
this.emitDiagnostic({ code: 'unmatched_response' });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (!isJsonRpcId(id, this.requireIntegerNumericIds)) {
|
|
580
|
+
this.fail(malformedEnvelope());
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const pending = this.pendingRequests.get(id);
|
|
584
|
+
if (!pending?.writeStarted || pending.responseReceived) {
|
|
585
|
+
this.emitDiagnostic({ code: 'unmatched_response' });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
pending.responseReceived = true;
|
|
589
|
+
const inboundTarget = this.inboundSequence;
|
|
590
|
+
const settleResponse = () => {
|
|
591
|
+
this.settlePending(id, (pending) => {
|
|
592
|
+
if (hasError) {
|
|
593
|
+
pending.reject(new JsonRpcRemoteError(envelope['error']));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
pending.resolve(envelope['result']);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
if (!pending.waitForInbound) {
|
|
601
|
+
settleResponse();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
void this.waitForInbound(inboundTarget, pending).then(settleResponse, (error) => {
|
|
605
|
+
this.settlePending(id, (pending) => {
|
|
606
|
+
pending.reject(asTransportError(error));
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
enqueueInbound(message) {
|
|
611
|
+
const sequence = ++this.inboundSequence;
|
|
612
|
+
if (this.inboundQueue.push({ message, sequence }))
|
|
613
|
+
return;
|
|
614
|
+
this.fail(transportError('capacity_exceeded', 'The buffered inbound message limit was reached.'));
|
|
615
|
+
}
|
|
616
|
+
acknowledgeInbound(sequence) {
|
|
617
|
+
this.inboundAcknowledgedSequence = sequence;
|
|
618
|
+
for (const waiter of [...this.inboundBarrierWaiters]) {
|
|
619
|
+
if (waiter.target > sequence)
|
|
620
|
+
continue;
|
|
621
|
+
this.inboundBarrierWaiters.delete(waiter);
|
|
622
|
+
waiter.resolve();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
waitForInbound(target, pending) {
|
|
626
|
+
if (target <= this.inboundAcknowledgedSequence)
|
|
627
|
+
return Promise.resolve();
|
|
628
|
+
return new Promise((resolve, reject) => {
|
|
629
|
+
const waiter = { target, resolve, reject };
|
|
630
|
+
pending.inboundWaiter = waiter;
|
|
631
|
+
this.inboundBarrierWaiters.add(waiter);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
settlePending(id, settle) {
|
|
635
|
+
const pending = this.pendingRequests.get(id);
|
|
636
|
+
if (!pending)
|
|
637
|
+
return false;
|
|
638
|
+
this.pendingRequests.delete(id);
|
|
639
|
+
if (pending.inboundWaiter) {
|
|
640
|
+
this.inboundBarrierWaiters.delete(pending.inboundWaiter);
|
|
641
|
+
pending.inboundWaiter.resolve();
|
|
642
|
+
delete pending.inboundWaiter;
|
|
643
|
+
}
|
|
644
|
+
clearTimeout(pending.timer);
|
|
645
|
+
if (pending.abortListener) {
|
|
646
|
+
pending.signal?.removeEventListener('abort', pending.abortListener);
|
|
647
|
+
}
|
|
648
|
+
settle(pending);
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
startPendingWrite(id) {
|
|
652
|
+
const pending = this.pendingRequests.get(id);
|
|
653
|
+
if (!pending)
|
|
654
|
+
return false;
|
|
655
|
+
pending.writeStarted = true;
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
allocateRequestId() {
|
|
659
|
+
const start = this.nextRequestId;
|
|
660
|
+
do {
|
|
661
|
+
const candidate = this.nextRequestId;
|
|
662
|
+
this.nextRequestId =
|
|
663
|
+
candidate === Number.MAX_SAFE_INTEGER ? 1 : candidate + 1;
|
|
664
|
+
if (!this.pendingRequests.has(candidate))
|
|
665
|
+
return candidate;
|
|
666
|
+
} while (this.nextRequestId !== start);
|
|
667
|
+
throw transportError('capacity_exceeded', 'No JSON-RPC request identifier is available.');
|
|
668
|
+
}
|
|
669
|
+
emitDiagnostic(diagnostic) {
|
|
670
|
+
try {
|
|
671
|
+
this.onDiagnostic?.(diagnostic);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
// Diagnostic callbacks cannot affect transport lifecycle.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
assertOpen() {
|
|
678
|
+
if (!this.open) {
|
|
679
|
+
throw (this.terminalError ??
|
|
680
|
+
transportError('transport_closed', 'The transport is closed.'));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
fail(error) {
|
|
684
|
+
if (!this.open)
|
|
685
|
+
return;
|
|
686
|
+
this.terminate(error, error);
|
|
687
|
+
}
|
|
688
|
+
terminate(inboundFailure, operationFailure) {
|
|
689
|
+
if (!this.open)
|
|
690
|
+
return;
|
|
691
|
+
this.open = false;
|
|
692
|
+
this.terminalError = operationFailure;
|
|
693
|
+
this.armTerminalErrorGuards();
|
|
694
|
+
this.detachStreams();
|
|
695
|
+
this.lineChunks = [];
|
|
696
|
+
this.lineBytes = 0;
|
|
697
|
+
this.pendingInboundRequestIds.clear();
|
|
698
|
+
this.respondingInboundRequestIds.clear();
|
|
699
|
+
this.abandoningInboundRequestIds.clear();
|
|
700
|
+
for (const waiter of this.inboundBarrierWaiters) {
|
|
701
|
+
waiter.reject(operationFailure);
|
|
702
|
+
}
|
|
703
|
+
this.inboundBarrierWaiters.clear();
|
|
704
|
+
for (const id of [...this.pendingRequests.keys()]) {
|
|
705
|
+
this.settlePending(id, (pending) => {
|
|
706
|
+
pending.reject(operationFailure);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
for (const rejectWrite of [...this.activeWriteRejectors]) {
|
|
710
|
+
rejectWrite(operationFailure);
|
|
711
|
+
}
|
|
712
|
+
this.inboundQueue.close(inboundFailure);
|
|
713
|
+
this.startCleanup();
|
|
714
|
+
}
|
|
715
|
+
detachStreams() {
|
|
716
|
+
this.readable.off('data', this.handleReadableData);
|
|
717
|
+
this.readable.off('end', this.handleReadableEnd);
|
|
718
|
+
this.readable.off('error', this.handleStreamError);
|
|
719
|
+
this.readable.off('close', this.handleStreamClose);
|
|
720
|
+
this.writable.off('error', this.handleWritableError);
|
|
721
|
+
this.writable.off('close', this.handleStreamClose);
|
|
722
|
+
}
|
|
723
|
+
startCleanup() {
|
|
724
|
+
this.cleanupPromise ??= Promise.resolve()
|
|
725
|
+
.then(() => this.cleanup?.())
|
|
726
|
+
.then(() => undefined)
|
|
727
|
+
.catch(() => {
|
|
728
|
+
this.cleanupFailure = transportError('cleanup_failed', 'Transport cleanup failed.');
|
|
729
|
+
})
|
|
730
|
+
.then(() => new Promise((resolve) => {
|
|
731
|
+
scheduleImmediate(resolve);
|
|
732
|
+
}))
|
|
733
|
+
.then(() => {
|
|
734
|
+
this.releaseTerminalErrorGuards();
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
armTerminalErrorGuards() {
|
|
738
|
+
this.terminalGuardStreams.add(this.readable);
|
|
739
|
+
this.terminalGuardStreams.add(this.writable);
|
|
740
|
+
for (const stream of this.terminalGuardStreams) {
|
|
741
|
+
stream.on('error', ignoreTerminalStreamError);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
releaseTerminalErrorGuards() {
|
|
745
|
+
for (const stream of this.terminalGuardStreams) {
|
|
746
|
+
stream.off('error', ignoreTerminalStreamError);
|
|
747
|
+
}
|
|
748
|
+
this.terminalGuardStreams.clear();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function transportError(code, message) {
|
|
752
|
+
return new JsonRpcTransportError(code, message);
|
|
753
|
+
}
|
|
754
|
+
function asTransportError(error) {
|
|
755
|
+
return error instanceof JsonRpcTransportError
|
|
756
|
+
? error
|
|
757
|
+
: transportError('invalid_outbound_message', 'The outbound JSON-RPC operation is invalid.');
|
|
758
|
+
}
|
|
759
|
+
function positiveInteger(value) {
|
|
760
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
761
|
+
throw transportError('invalid_configuration', 'Transport limits and timeouts must be positive safe integers.');
|
|
762
|
+
}
|
|
763
|
+
return value;
|
|
764
|
+
}
|
|
765
|
+
function timerMilliseconds(value) {
|
|
766
|
+
const timeout = positiveInteger(value);
|
|
767
|
+
if (timeout > maximumTimerMilliseconds) {
|
|
768
|
+
throw transportError('invalid_configuration', `Transport timeouts cannot exceed ${String(maximumTimerMilliseconds)} milliseconds.`);
|
|
769
|
+
}
|
|
770
|
+
return timeout;
|
|
771
|
+
}
|
|
772
|
+
function assertMethod(method) {
|
|
773
|
+
if (typeof method !== 'string' || method.length === 0) {
|
|
774
|
+
throw transportError('invalid_outbound_message', 'A JSON-RPC method must be a non-empty string.');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function isRecord(value) {
|
|
778
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
779
|
+
}
|
|
780
|
+
function hasOwn(value, key) {
|
|
781
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
782
|
+
}
|
|
783
|
+
function validJsonRpcVersion(value, requireJsonRpcVersion) {
|
|
784
|
+
return requireJsonRpcVersion
|
|
785
|
+
? value['jsonrpc'] === '2.0'
|
|
786
|
+
: !hasOwn(value, 'jsonrpc') || value['jsonrpc'] === '2.0';
|
|
787
|
+
}
|
|
788
|
+
function isJsonRpcId(value, requireIntegerNumericIds = false) {
|
|
789
|
+
return (value === null ||
|
|
790
|
+
typeof value === 'string' ||
|
|
791
|
+
(typeof value === 'number' &&
|
|
792
|
+
Number.isFinite(value) &&
|
|
793
|
+
(!requireIntegerNumericIds || Number.isInteger(value))));
|
|
794
|
+
}
|
|
795
|
+
function requestIdKey(id) {
|
|
796
|
+
return `${typeof id}:${String(id)}`;
|
|
797
|
+
}
|
|
798
|
+
function isJsonRpcErrorObject(value) {
|
|
799
|
+
return (isRecord(value) &&
|
|
800
|
+
typeof value['code'] === 'number' &&
|
|
801
|
+
Number.isSafeInteger(value['code']) &&
|
|
802
|
+
typeof value['message'] === 'string');
|
|
803
|
+
}
|
|
804
|
+
function malformedEnvelope() {
|
|
805
|
+
return transportError('malformed_message', 'An inbound JSON-RPC envelope is malformed.');
|
|
806
|
+
}
|
|
807
|
+
function assertSerializedResponse(frame, expectedIdKey, responseKind, requireIntegerNumericIds) {
|
|
808
|
+
const value = JSON.parse(frame);
|
|
809
|
+
if (!isRecord(value) || !isJsonRpcId(value['id'], requireIntegerNumericIds)) {
|
|
810
|
+
throw invalidSerializedResponse();
|
|
811
|
+
}
|
|
812
|
+
const hasError = hasOwn(value, 'error');
|
|
813
|
+
const hasResult = hasOwn(value, 'result');
|
|
814
|
+
if (requestIdKey(value['id']) !== expectedIdKey ||
|
|
815
|
+
hasOwn(value, 'method') ||
|
|
816
|
+
(responseKind === 'result'
|
|
817
|
+
? !hasResult || hasError
|
|
818
|
+
: hasResult || !hasError || !isJsonRpcErrorObject(value['error']))) {
|
|
819
|
+
throw invalidSerializedResponse();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function invalidSerializedResponse() {
|
|
823
|
+
return transportError('invalid_outbound_message', 'The serialized JSON-RPC response is invalid.');
|
|
824
|
+
}
|
|
825
|
+
function ignoreTerminalStreamError(_error) {
|
|
826
|
+
// The first terminal error is already recorded; suppress only errors that
|
|
827
|
+
// race with that terminal path or its awaited cleanup operation.
|
|
828
|
+
}
|
|
829
|
+
//# sourceMappingURL=index.js.map
|