tywrap 0.8.0 → 0.9.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/README.md +22 -7
- package/SECURITY.md +39 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +2 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +6 -3
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/generator.d.ts +23 -4
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +243 -170
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +3 -2
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +5 -5
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +3 -7
- package/dist/runtime/base-bridge.d.ts.map +1 -1
- package/dist/runtime/base-bridge.js +4 -17
- package/dist/runtime/base-bridge.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +3 -22
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +13 -53
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +1 -1
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +78 -55
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/errors.d.ts +16 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +17 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-transport.d.ts +1 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +1 -1
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -36
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +2 -80
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +120 -59
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +345 -78
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +1 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +2 -3
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +7 -36
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +20 -102
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +30 -69
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +179 -236
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
- package/dist/runtime/transport.d.ts +9 -19
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +49 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +152 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -39
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +3 -2
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +140 -13
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +3 -16
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/codec.d.ts +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +13 -13
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +2 -1
- package/dist/utils/ir-cache.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +0 -29
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +16 -107
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/runtime/frame_codec.py +7 -1
- package/runtime/python_bridge.py +85 -125
- package/runtime/tywrap_bridge_core.py +133 -150
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +7 -4
- package/src/core/generator.ts +315 -205
- package/src/core/mapper.ts +8 -8
- package/src/index.ts +7 -4
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +94 -65
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +6 -0
- package/src/runtime/node.ts +9 -120
- package/src/runtime/pooled-transport.ts +424 -90
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +7 -3
- package/src/runtime/rpc-client.ts +37 -139
- package/src/runtime/subprocess-transport.ts +210 -285
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +15 -23
- package/src/runtime/validators.ts +204 -0
- package/src/types/index.ts +21 -60
- package/src/tywrap.ts +157 -22
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +21 -13
- package/src/utils/ir-cache.ts +1 -1
- package/src/utils/runtime.ts +17 -128
- package/src/version.ts +1 -1
- package/dist/core/discovery.d.ts +0 -103
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -380
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/validation.d.ts +0 -102
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js +0 -490
- package/dist/core/validation.js.map +0 -1
- package/dist/runtime/base.d.ts +0 -22
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js +0 -23
- package/dist/runtime/base.js.map +0 -1
- package/dist/runtime/transport-pool.d.ts +0 -196
- package/dist/runtime/transport-pool.d.ts.map +0 -1
- package/dist/runtime/transport-pool.js +0 -418
- package/dist/runtime/transport-pool.js.map +0 -1
- package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/safe_codec.py +0 -352
- package/src/core/discovery.ts +0 -477
- package/src/core/validation.ts +0 -729
- package/src/runtime/base.ts +0 -24
- package/src/runtime/transport-pool.ts +0 -538
|
@@ -17,7 +17,6 @@ import { DisposableBase } from './bounded-context.js';
|
|
|
17
17
|
import { BridgeDisposedError, BridgeProtocolError, BridgeTimeoutError } from './errors.js';
|
|
18
18
|
import { Reassembler, encodeFrames, utf8ByteLength } from './frame-codec.js';
|
|
19
19
|
import { TimedOutRequestTracker } from './timed-out-request-tracker.js';
|
|
20
|
-
import { FRAME_PROTOCOL_ID, PROTOCOL_ID, } from './transport.js';
|
|
21
20
|
// =============================================================================
|
|
22
21
|
// CONSTANTS
|
|
23
22
|
// =============================================================================
|
|
@@ -47,14 +46,6 @@ const FRAME_ENVELOPE_HEADROOM = 1024;
|
|
|
47
46
|
* ceiling sound without over-allocating buffer headroom.
|
|
48
47
|
*/
|
|
49
48
|
const FRAME_DATA_ESCAPE_FACTOR = 2;
|
|
50
|
-
/** Negotiation env var: `1` enables `tywrap-frame/1` chunked transport. */
|
|
51
|
-
const ENV_CHUNKING = 'TYWRAP_TRANSPORT_CHUNKING';
|
|
52
|
-
/** Negotiation env var: the framing protocol id the bridge must implement. */
|
|
53
|
-
const ENV_FRAME_PROTOCOL = 'TYWRAP_TRANSPORT_FRAME_PROTOCOL';
|
|
54
|
-
/** Negotiation env var: per-frame UTF-8 byte ceiling for the data slice. */
|
|
55
|
-
const ENV_MAX_FRAME_BYTES = 'TYWRAP_TRANSPORT_MAX_FRAME_BYTES';
|
|
56
|
-
/** Timeout (ms) for the in-init meta negotiation probe. */
|
|
57
|
-
const NEGOTIATION_PROBE_TIMEOUT_MS = 30_000;
|
|
58
49
|
/**
|
|
59
50
|
* Default cap (UTF-8 bytes) on a single chunked response reassembled in memory.
|
|
60
51
|
* Mirrors the codec's `DEFAULT_MAX_PAYLOAD_BYTES` (10 MiB) so chunking never
|
|
@@ -154,25 +145,15 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
154
145
|
maxReassemblyBytes;
|
|
155
146
|
restartAfterRequests;
|
|
156
147
|
writeQueueTimeoutMs;
|
|
157
|
-
/** Whether `tywrap-frame/1` negotiation was requested by the caller. */
|
|
158
|
-
enableChunking;
|
|
159
148
|
/**
|
|
160
|
-
*
|
|
161
|
-
* `
|
|
162
|
-
*
|
|
149
|
+
* Per-frame UTF-8 data ceiling. The packaged Python peer receives this
|
|
150
|
+
* transport's `maxLineLength` when it is spawned, so request and response
|
|
151
|
+
* framing use the same fixed ceiling without runtime negotiation.
|
|
163
152
|
*/
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* The per-frame UTF-8 byte ceiling actually agreed with the bridge:
|
|
167
|
-
* `min(maxLineLength, the bridge's advertised maxFrameBytes)`. Defaults to
|
|
168
|
-
* `maxLineLength` and is narrowed during negotiation, so REQUEST frames are
|
|
169
|
-
* never sized larger than the bridge said it will accept (the reference bridge
|
|
170
|
-
* echoes the requested value, but a custom bridge may advertise a smaller one).
|
|
171
|
-
*/
|
|
172
|
-
negotiatedFrameBytes;
|
|
153
|
+
frameBytes;
|
|
173
154
|
/**
|
|
174
155
|
* Reassembles `tywrap-frame/1` response frames into single logical response
|
|
175
|
-
* lines.
|
|
156
|
+
* lines. Per-id discard tracks
|
|
176
157
|
* timed-out/aborted streams so late frames cannot desync stdout.
|
|
177
158
|
*/
|
|
178
159
|
responseReassembler = null;
|
|
@@ -182,6 +163,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
182
163
|
processError = null;
|
|
183
164
|
// Stream buffers
|
|
184
165
|
stdoutBuffer = '';
|
|
166
|
+
stdoutScanOffset = 0;
|
|
185
167
|
stderrBuffer = '';
|
|
186
168
|
// Request tracking
|
|
187
169
|
pending = new Map();
|
|
@@ -190,8 +172,13 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
190
172
|
});
|
|
191
173
|
requestCount = 0;
|
|
192
174
|
needsRestart = false;
|
|
175
|
+
/** Serializes restart decisions with dispatch reservations. */
|
|
176
|
+
dispatchMutex = Promise.resolve();
|
|
177
|
+
activeDispatches = 0;
|
|
178
|
+
dispatchDrainWaiters = [];
|
|
193
179
|
// Write queue for backpressure
|
|
194
180
|
writeQueue = [];
|
|
181
|
+
writeQueueHead = 0;
|
|
195
182
|
draining = false;
|
|
196
183
|
/**
|
|
197
184
|
* Per-logical-request write mutex (W5). When a request is chunked into
|
|
@@ -217,9 +204,12 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
217
204
|
this.maxLineLength = options.maxLineLength ?? DEFAULT_MAX_LINE_LENGTH;
|
|
218
205
|
this.restartAfterRequests = options.restartAfterRequests ?? 0;
|
|
219
206
|
this.writeQueueTimeoutMs = options.writeQueueTimeoutMs ?? DEFAULT_WRITE_QUEUE_TIMEOUT_MS;
|
|
220
|
-
this.enableChunking = options.enableChunking ?? false;
|
|
221
207
|
this.maxReassemblyBytes = options.maxReassemblyBytes ?? DEFAULT_MAX_REASSEMBLY_BYTES;
|
|
222
|
-
this.
|
|
208
|
+
this.frameBytes = this.maxLineLength;
|
|
209
|
+
this.responseReassembler = new Reassembler({
|
|
210
|
+
maxReassemblyBytes: this.maxReassemblyBytes,
|
|
211
|
+
expectedStream: 'response',
|
|
212
|
+
});
|
|
223
213
|
}
|
|
224
214
|
// ===========================================================================
|
|
225
215
|
// TRANSPORT INTERFACE
|
|
@@ -232,7 +222,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
232
222
|
* @param signal - Optional AbortSignal for cancellation
|
|
233
223
|
* @returns The raw JSON response string
|
|
234
224
|
*/
|
|
235
|
-
async send(message, timeoutMs, signal) {
|
|
225
|
+
async send(message, timeoutMs, signal, requestId) {
|
|
236
226
|
// Check disposed state
|
|
237
227
|
if (this.isDisposed) {
|
|
238
228
|
throw new BridgeDisposedError('Transport has been disposed');
|
|
@@ -253,17 +243,20 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
253
243
|
throw new BridgeTimeoutError('Operation aborted');
|
|
254
244
|
}
|
|
255
245
|
// Extract message ID for response correlation
|
|
256
|
-
const messageId = extractMessageId(message);
|
|
246
|
+
const messageId = requestId ?? extractMessageId(message);
|
|
257
247
|
if (messageId === null) {
|
|
258
248
|
throw new BridgeProtocolError('Message must contain an "id" field');
|
|
259
249
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
250
|
+
let ownsDispatchReservation = false;
|
|
251
|
+
let reservationReleased = false;
|
|
252
|
+
const releaseOwnedReservation = () => {
|
|
253
|
+
if (ownsDispatchReservation && !reservationReleased) {
|
|
254
|
+
reservationReleased = true;
|
|
255
|
+
this.releaseDispatch();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
265
258
|
// Create promise for response
|
|
266
|
-
|
|
259
|
+
const response = new Promise((resolve, reject) => {
|
|
267
260
|
// Set up timeout if specified
|
|
268
261
|
let timer;
|
|
269
262
|
// Defined before the timer so the timeout path can also detach it.
|
|
@@ -322,13 +315,28 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
322
315
|
timer,
|
|
323
316
|
};
|
|
324
317
|
this.pending.set(messageId, pendingEntry);
|
|
325
|
-
// Write message to stdin. When
|
|
326
|
-
//
|
|
318
|
+
// Write message to stdin. When the encoded request exceeds the per-frame
|
|
319
|
+
// ceiling, it is fragmented into
|
|
327
320
|
// `tywrap-frame/1` request frames written contiguously under the write
|
|
328
321
|
// mutex (W5); otherwise it goes out as a single JSONL line. Both paths
|
|
329
322
|
// serialize through the same mutex so a small request can never slip
|
|
330
323
|
// between another request's frames.
|
|
331
|
-
this.
|
|
324
|
+
this.reserveDispatch(() => this.pending.get(messageId) === pendingEntry && !signal?.aborted)
|
|
325
|
+
.then(reserved => {
|
|
326
|
+
if (!reserved) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
ownsDispatchReservation = true;
|
|
330
|
+
// Timeout/abort may have settled while this reservation waited behind
|
|
331
|
+
// a generation restart. Do not write the abandoned request, and release
|
|
332
|
+
// only the reservation this send actually acquired.
|
|
333
|
+
if (this.pending.get(messageId) !== pendingEntry) {
|
|
334
|
+
releaseOwnedReservation();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
return this.writeRequest(message, messageId, signal, pendingEntry);
|
|
338
|
+
})
|
|
339
|
+
.catch(err => {
|
|
332
340
|
this.pending.delete(messageId);
|
|
333
341
|
if (timer) {
|
|
334
342
|
clearTimeout(timer);
|
|
@@ -336,31 +344,86 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
336
344
|
signal?.removeEventListener('abort', abortHandler);
|
|
337
345
|
reject(this.classifyError(err));
|
|
338
346
|
});
|
|
347
|
+
});
|
|
348
|
+
return response.finally(releaseOwnedReservation);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Atomically reserve the current process generation for one request. A
|
|
352
|
+
* threshold restart waits for every request reserved on the old generation
|
|
353
|
+
* to settle before killing it; later callers remain queued behind this lock.
|
|
354
|
+
*/
|
|
355
|
+
async withDispatchMutex(operation) {
|
|
356
|
+
let unlock;
|
|
357
|
+
const previous = this.dispatchMutex;
|
|
358
|
+
this.dispatchMutex = new Promise(resolve => {
|
|
359
|
+
unlock = resolve;
|
|
360
|
+
});
|
|
361
|
+
await previous;
|
|
362
|
+
try {
|
|
363
|
+
return await operation();
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
unlock();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async reserveDispatch(isLive) {
|
|
370
|
+
return this.withDispatchMutex(async () => {
|
|
371
|
+
// Cancellation may win while this request waits behind an earlier
|
|
372
|
+
// generation. It must consume neither a request slot nor a restart.
|
|
373
|
+
if (!isLive()) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (this.isLifecycleEnding()) {
|
|
377
|
+
throw new BridgeDisposedError('Transport has been disposed');
|
|
378
|
+
}
|
|
379
|
+
if (this.needsRestart ||
|
|
380
|
+
(this.restartAfterRequests > 0 && this.requestCount >= this.restartAfterRequests)) {
|
|
381
|
+
if (this.activeDispatches > 0) {
|
|
382
|
+
await new Promise(resolve => this.dispatchDrainWaiters.push(resolve));
|
|
383
|
+
}
|
|
384
|
+
// The owner or transport can settle while waiting for the old
|
|
385
|
+
// generation to drain. Do not restart for work that no longer exists.
|
|
386
|
+
if (!isLive()) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (this.isLifecycleEnding()) {
|
|
390
|
+
throw new BridgeDisposedError('Transport has been disposed');
|
|
391
|
+
}
|
|
392
|
+
await this.restartProcess();
|
|
393
|
+
}
|
|
394
|
+
this.activeDispatches++;
|
|
339
395
|
this.requestCount++;
|
|
396
|
+
return true;
|
|
340
397
|
});
|
|
341
398
|
}
|
|
399
|
+
releaseDispatch() {
|
|
400
|
+
this.activeDispatches = Math.max(0, this.activeDispatches - 1);
|
|
401
|
+
if (this.activeDispatches === 0) {
|
|
402
|
+
for (const resolve of this.dispatchDrainWaiters.splice(0)) {
|
|
403
|
+
resolve();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
isLifecycleEnding() {
|
|
408
|
+
return this.state === 'disposing' || this.isDisposed;
|
|
409
|
+
}
|
|
342
410
|
/**
|
|
343
411
|
* Static capability descriptor for the subprocess backend.
|
|
344
412
|
*
|
|
345
413
|
* Per the {@link Transport.capabilities} contract this is lifecycle-independent
|
|
346
414
|
* (safe before `init()` / after `dispose()`) and never makes a Python round
|
|
347
415
|
* trip. Subprocess carries Arrow IPC and arbitrary binary (bytes envelopes)
|
|
348
|
-
* over the JSONL stream. `supportsChunking`
|
|
349
|
-
*
|
|
350
|
-
* `
|
|
351
|
-
*
|
|
352
|
-
* actually advertised framing is the negotiated fact, surfaced separately on
|
|
353
|
-
* `BridgeInfo.transport.supportsChunking`; "will chunking actually happen"
|
|
354
|
-
* needs both `true`. `supportsStreaming` stays `false` (0.8.0). `maxFrameBytes`
|
|
355
|
-
* is the configured JSONL line-length limit — the largest single (unchunked)
|
|
356
|
-
* response line this transport accepts.
|
|
416
|
+
* over the JSONL stream. `supportsChunking` is always `true` because the npm
|
|
417
|
+
* package ships both `tywrap-frame/1` peers. `supportsStreaming` stays `false`.
|
|
418
|
+
* `maxFrameBytes` is the fixed per-frame data ceiling supplied to the Python
|
|
419
|
+
* bridge at spawn time.
|
|
357
420
|
*/
|
|
358
421
|
capabilities() {
|
|
359
422
|
return {
|
|
360
423
|
backend: 'subprocess',
|
|
361
424
|
supportsArrow: true,
|
|
362
425
|
supportsBinary: true,
|
|
363
|
-
supportsChunking:
|
|
426
|
+
supportsChunking: true,
|
|
364
427
|
supportsStreaming: false,
|
|
365
428
|
maxFrameBytes: this.maxLineLength,
|
|
366
429
|
};
|
|
@@ -369,130 +432,11 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
369
432
|
// BOUNDED CONTEXT LIFECYCLE
|
|
370
433
|
// ===========================================================================
|
|
371
434
|
/**
|
|
372
|
-
* Initialize the transport by spawning the Python process
|
|
373
|
-
*
|
|
435
|
+
* Initialize the transport by spawning the Python process. The packaged
|
|
436
|
+
* bridge always speaks `tywrap-frame/1`.
|
|
374
437
|
*/
|
|
375
438
|
async doInit() {
|
|
376
439
|
await this.spawnProcess();
|
|
377
|
-
if (this.enableChunking) {
|
|
378
|
-
await this.negotiateChunking();
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Probe the freshly-spawned bridge for `tywrap-frame/1` support.
|
|
383
|
-
*
|
|
384
|
-
* Sends a small unchunked `meta` request directly over stdin (NOT via the
|
|
385
|
-
* public {@link send}, which would re-enter init while we are mid-init) and
|
|
386
|
-
* reads the single-line response. If the bridge reports
|
|
387
|
-
* `transport.supportsChunking: true`, response reassembly is enabled. If the
|
|
388
|
-
* bridge does not advertise chunking (old bridge, or it disabled framing), the
|
|
389
|
-
* transport stays single-frame and an oversize response still fails loud — no
|
|
390
|
-
* silent fallback. A probe failure leaves chunking disabled but does not fail
|
|
391
|
-
* init (small calls must keep working); the loud failure is deferred to the
|
|
392
|
-
* first oversize response.
|
|
393
|
-
*/
|
|
394
|
-
async negotiateChunking() {
|
|
395
|
-
const probeId = -1;
|
|
396
|
-
const probeMessage = JSON.stringify({
|
|
397
|
-
id: probeId,
|
|
398
|
-
protocol: PROTOCOL_ID,
|
|
399
|
-
method: 'meta',
|
|
400
|
-
params: {},
|
|
401
|
-
});
|
|
402
|
-
let responseLine;
|
|
403
|
-
try {
|
|
404
|
-
responseLine = await this.sendProbe(probeId, probeMessage, NEGOTIATION_PROBE_TIMEOUT_MS);
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
// Probe failed (slow/old bridge, transient): leave chunking off. Oversize
|
|
408
|
-
// responses will fail loud at the line ceiling; small calls keep working.
|
|
409
|
-
this.negotiatedChunking = false;
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
let parsed;
|
|
413
|
-
try {
|
|
414
|
-
parsed = JSON.parse(responseLine);
|
|
415
|
-
}
|
|
416
|
-
catch {
|
|
417
|
-
this.negotiatedChunking = false;
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const supports = this.bridgeAdvertisesChunking(parsed);
|
|
421
|
-
this.negotiatedChunking = supports;
|
|
422
|
-
if (supports) {
|
|
423
|
-
// Honor the bridge's advertised ceiling so request frames are never sized
|
|
424
|
-
// larger than it will accept. bridgeAdvertisesChunking already validated
|
|
425
|
-
// maxFrameBytes is a positive integer, so this read is safe.
|
|
426
|
-
const advertised = parsed.result
|
|
427
|
-
?.transport?.maxFrameBytes;
|
|
428
|
-
if (typeof advertised === 'number') {
|
|
429
|
-
this.negotiatedFrameBytes = Math.min(this.maxLineLength, advertised);
|
|
430
|
-
}
|
|
431
|
-
this.responseReassembler = new Reassembler({
|
|
432
|
-
maxReassemblyBytes: this.maxReassemblyBytes,
|
|
433
|
-
expectedStream: 'response',
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Whether a parsed `meta` response advertises `tywrap-frame/1` chunking with a
|
|
439
|
-
* matching frame protocol. Defensive: any missing/mismatched field => `false`.
|
|
440
|
-
*/
|
|
441
|
-
bridgeAdvertisesChunking(parsed) {
|
|
442
|
-
if (parsed === null || typeof parsed !== 'object') {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
const result = parsed.result;
|
|
446
|
-
if (result === null || typeof result !== 'object') {
|
|
447
|
-
return false;
|
|
448
|
-
}
|
|
449
|
-
const transport = result.transport;
|
|
450
|
-
if (transport === null || typeof transport !== 'object') {
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
const t = transport;
|
|
454
|
-
// Match the BridgeInfo validator (rpc-client.ts): a valid framing block needs
|
|
455
|
-
// the matching protocol AND a positive-integer maxFrameBytes. A bridge that
|
|
456
|
-
// advertises chunking with a bogus frame ceiling is not a contract we honor.
|
|
457
|
-
return (t.supportsChunking === true &&
|
|
458
|
-
t.frameProtocol === FRAME_PROTOCOL_ID &&
|
|
459
|
-
typeof t.maxFrameBytes === 'number' &&
|
|
460
|
-
Number.isInteger(t.maxFrameBytes) &&
|
|
461
|
-
t.maxFrameBytes > 0);
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Send a single in-init probe message over stdin and resolve with its raw
|
|
465
|
-
* response line. Registers a pending entry keyed by `probeId` exactly like the
|
|
466
|
-
* normal send path so {@link handleResponseLine} resolves it, but bypasses the
|
|
467
|
-
* `isReady`/auto-init guard (we are deliberately running during `doInit`).
|
|
468
|
-
*/
|
|
469
|
-
sendProbe(probeId, message, timeoutMs) {
|
|
470
|
-
return new Promise((resolve, reject) => {
|
|
471
|
-
const timer = setTimeout(() => {
|
|
472
|
-
this.pending.delete(probeId);
|
|
473
|
-
this.timedOutRequests.mark(probeId);
|
|
474
|
-
reject(new BridgeTimeoutError(`Negotiation probe timed out after ${timeoutMs}ms`));
|
|
475
|
-
}, timeoutMs);
|
|
476
|
-
if (typeof timer.unref === 'function') {
|
|
477
|
-
timer.unref();
|
|
478
|
-
}
|
|
479
|
-
this.pending.set(probeId, {
|
|
480
|
-
resolve: (value) => {
|
|
481
|
-
clearTimeout(timer);
|
|
482
|
-
resolve(value);
|
|
483
|
-
},
|
|
484
|
-
reject: (error) => {
|
|
485
|
-
clearTimeout(timer);
|
|
486
|
-
reject(error);
|
|
487
|
-
},
|
|
488
|
-
timer,
|
|
489
|
-
});
|
|
490
|
-
this.writeToStdin(`${message}\n`).catch(err => {
|
|
491
|
-
this.pending.delete(probeId);
|
|
492
|
-
clearTimeout(timer);
|
|
493
|
-
reject(this.classifyError(err));
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
440
|
}
|
|
497
441
|
/**
|
|
498
442
|
* Dispose the transport by killing the Python process.
|
|
@@ -510,20 +454,21 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
510
454
|
}
|
|
511
455
|
this.pending.clear();
|
|
512
456
|
// Clear write queue
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this.
|
|
518
|
-
// Kill process
|
|
519
|
-
await this.killProcess();
|
|
457
|
+
this.rejectAllQueuedWrites(error);
|
|
458
|
+
// Fence queued restart work before killing the process. A restart already
|
|
459
|
+
// holding the mutex observes the disposing state before it can spawn; if it
|
|
460
|
+
// spawned just before disposal began, this barrier kills that child too.
|
|
461
|
+
await this.withDispatchMutex(() => this.killProcess());
|
|
520
462
|
// Clear buffers
|
|
521
463
|
this.stdoutBuffer = '';
|
|
464
|
+
this.stdoutScanOffset = 0;
|
|
522
465
|
this.stderrBuffer = '';
|
|
523
466
|
this.timedOutRequests.clear();
|
|
524
467
|
this.requestCount = 0;
|
|
525
|
-
this.responseReassembler =
|
|
526
|
-
|
|
468
|
+
this.responseReassembler = new Reassembler({
|
|
469
|
+
maxReassemblyBytes: this.maxReassemblyBytes,
|
|
470
|
+
expectedStream: 'response',
|
|
471
|
+
});
|
|
527
472
|
// Reset the per-request write mutex so a disposed transport starts from a
|
|
528
473
|
// clean (resolved) tail if reused.
|
|
529
474
|
this.writeMutex = Promise.resolve();
|
|
@@ -546,34 +491,16 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
546
491
|
// on top of it. Spread (rather than dynamic index assignment) keeps the
|
|
547
492
|
// computed keys off ESLint's object-injection sink. See
|
|
548
493
|
// docs/transport-framing.md.
|
|
549
|
-
const chunkingEnv = this.enableChunking
|
|
550
|
-
? {
|
|
551
|
-
[ENV_CHUNKING]: '1',
|
|
552
|
-
[ENV_FRAME_PROTOCOL]: FRAME_PROTOCOL_ID,
|
|
553
|
-
[ENV_MAX_FRAME_BYTES]: String(this.maxLineLength),
|
|
554
|
-
}
|
|
555
|
-
: {};
|
|
556
494
|
const env = {
|
|
557
495
|
...baseEnv,
|
|
558
|
-
...chunkingEnv,
|
|
559
496
|
// Ensure Python uses UTF-8
|
|
560
497
|
PYTHONUTF8: '1',
|
|
561
498
|
PYTHONIOENCODING: 'UTF-8',
|
|
562
499
|
// Disable Python buffering
|
|
563
500
|
PYTHONUNBUFFERED: '1',
|
|
564
501
|
};
|
|
565
|
-
// This transport's enableChunking is authoritative: when chunking is OFF,
|
|
566
|
-
// strip any inherited TYWRAP_TRANSPORT_* from the parent environment so a
|
|
567
|
-
// host-level value can't make the bridge negotiate framing the TS side won't
|
|
568
|
-
// reassemble (or a different frame ceiling). When ON, chunkingEnv above
|
|
569
|
-
// already set all three to this transport's values.
|
|
570
|
-
if (!this.enableChunking) {
|
|
571
|
-
delete env[ENV_CHUNKING];
|
|
572
|
-
delete env[ENV_FRAME_PROTOCOL];
|
|
573
|
-
delete env[ENV_MAX_FRAME_BYTES];
|
|
574
|
-
}
|
|
575
502
|
// Spawn process
|
|
576
|
-
this.process = spawn(this.pythonPath, [this.bridgeScript], {
|
|
503
|
+
this.process = spawn(this.pythonPath, [this.bridgeScript, '--tywrap-max-frame-bytes', String(this.frameBytes)], {
|
|
577
504
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
578
505
|
env,
|
|
579
506
|
cwd: this.cwd,
|
|
@@ -649,26 +576,34 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
649
576
|
* Restart the Python process.
|
|
650
577
|
*/
|
|
651
578
|
async restartProcess() {
|
|
579
|
+
if (this.isLifecycleEnding()) {
|
|
580
|
+
throw new BridgeDisposedError('Transport has been disposed');
|
|
581
|
+
}
|
|
652
582
|
// Kill existing process
|
|
653
583
|
await this.killProcess();
|
|
654
584
|
// Clear buffers and restart flags
|
|
655
585
|
this.stdoutBuffer = '';
|
|
586
|
+
this.stdoutScanOffset = 0;
|
|
656
587
|
this.stderrBuffer = '';
|
|
657
588
|
this.requestCount = 0;
|
|
658
589
|
this.needsRestart = false;
|
|
659
590
|
// Drop any partial reassembly + discard tracking: the new process owns a
|
|
660
591
|
// fresh stdout stream, so stale per-id state from the dead process must not
|
|
661
592
|
// leak across the restart boundary.
|
|
662
|
-
this.responseReassembler =
|
|
663
|
-
|
|
593
|
+
this.responseReassembler = new Reassembler({
|
|
594
|
+
maxReassemblyBytes: this.maxReassemblyBytes,
|
|
595
|
+
expectedStream: 'response',
|
|
596
|
+
});
|
|
664
597
|
// The new process owns a fresh stdin stream; reset the write mutex so a
|
|
665
598
|
// pending frame burst against the dead process cannot serialize behind it.
|
|
666
599
|
this.writeMutex = Promise.resolve();
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
if (this.
|
|
670
|
-
|
|
600
|
+
// Disposal may begin while the old child is being killed. Never publish a
|
|
601
|
+
// replacement after lifecycle teardown has started.
|
|
602
|
+
if (this.isLifecycleEnding()) {
|
|
603
|
+
throw new BridgeDisposedError('Transport has been disposed');
|
|
671
604
|
}
|
|
605
|
+
// Spawn a fresh process with the packaged always-on framing protocol.
|
|
606
|
+
await this.spawnProcess();
|
|
672
607
|
}
|
|
673
608
|
/**
|
|
674
609
|
* Mark the process for restart on the next send.
|
|
@@ -684,17 +619,13 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
684
619
|
/**
|
|
685
620
|
* Effective stdout line ceiling.
|
|
686
621
|
*
|
|
687
|
-
*
|
|
688
|
-
* chunking negotiated, a single wire line is a `tywrap-frame/1` envelope whose
|
|
622
|
+
* A single wire line may be a `tywrap-frame/1` envelope whose
|
|
689
623
|
* `data` slice is capped at `maxLineLength` UTF-8 bytes by the bridge, but the
|
|
690
624
|
* JSON envelope adds escaping (`"`/`\`) plus fixed keys; the ceiling is widened
|
|
691
625
|
* to bound that overhead so a legitimate frame line is never rejected while a
|
|
692
626
|
* runaway/garbage line still is.
|
|
693
627
|
*/
|
|
694
628
|
effectiveLineCeiling() {
|
|
695
|
-
if (!this.negotiatedChunking) {
|
|
696
|
-
return this.maxLineLength;
|
|
697
|
-
}
|
|
698
629
|
return this.maxLineLength * FRAME_DATA_ESCAPE_FACTOR + FRAME_ENVELOPE_HEADROOM;
|
|
699
630
|
}
|
|
700
631
|
/**
|
|
@@ -703,20 +634,23 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
703
634
|
handleStdoutData(chunk) {
|
|
704
635
|
this.stdoutBuffer += chunk.toString();
|
|
705
636
|
const ceiling = this.effectiveLineCeiling();
|
|
706
|
-
//
|
|
707
|
-
|
|
637
|
+
// Scan only bytes newly appended since the last incomplete line.
|
|
638
|
+
let newlineIndex = this.stdoutBuffer.indexOf('\n', this.stdoutScanOffset);
|
|
639
|
+
if (this.stdoutBuffer.length > ceiling && newlineIndex === -1) {
|
|
708
640
|
const snippet = this.stdoutBuffer.slice(0, 500);
|
|
709
641
|
this.stdoutBuffer = '';
|
|
642
|
+
this.stdoutScanOffset = 0;
|
|
710
643
|
this.handleProtocolError(`Response line exceeded ${ceiling} bytes`, snippet);
|
|
711
644
|
return;
|
|
712
645
|
}
|
|
713
646
|
// Process complete lines
|
|
714
|
-
|
|
715
|
-
while ((newlineIndex = this.stdoutBuffer.indexOf('\n')) !== -1) {
|
|
647
|
+
while (newlineIndex !== -1) {
|
|
716
648
|
const line = this.stdoutBuffer.slice(0, newlineIndex);
|
|
717
649
|
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
650
|
+
this.stdoutScanOffset = 0;
|
|
718
651
|
// Skip empty lines
|
|
719
652
|
if (!line.trim()) {
|
|
653
|
+
newlineIndex = this.stdoutBuffer.indexOf('\n', this.stdoutScanOffset);
|
|
720
654
|
continue;
|
|
721
655
|
}
|
|
722
656
|
// Check line length
|
|
@@ -726,18 +660,20 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
726
660
|
return;
|
|
727
661
|
}
|
|
728
662
|
this.handleResponseLine(line);
|
|
663
|
+
newlineIndex = this.stdoutBuffer.indexOf('\n', this.stdoutScanOffset);
|
|
729
664
|
}
|
|
665
|
+
this.stdoutScanOffset = this.stdoutBuffer.length;
|
|
730
666
|
}
|
|
731
667
|
/**
|
|
732
668
|
* Handle a complete response line from stdout.
|
|
733
669
|
*
|
|
734
|
-
*
|
|
670
|
+
* A line may be a `tywrap-frame/1` envelope: it is
|
|
735
671
|
* routed into the per-id {@link Reassembler}, which returns the reassembled
|
|
736
672
|
* logical response only once the stream is complete and valid. Single-line
|
|
737
673
|
* (non-frame) responses keep the original fast path unchanged.
|
|
738
674
|
*/
|
|
739
675
|
handleResponseLine(line) {
|
|
740
|
-
if (this.
|
|
676
|
+
if (this.responseReassembler) {
|
|
741
677
|
const probe = probeFrameLine(line);
|
|
742
678
|
if (probe.kind === 'frame') {
|
|
743
679
|
this.handleResponseFrame(probe.value, line);
|
|
@@ -786,7 +722,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
786
722
|
handleResponseFrame(rawFrame, line) {
|
|
787
723
|
const reassembler = this.responseReassembler;
|
|
788
724
|
if (!reassembler) {
|
|
789
|
-
// Unreachable:
|
|
725
|
+
// Unreachable: the always-on framing path constructs the reassembler.
|
|
790
726
|
this.handleProtocolError('Received frame with no reassembler', line);
|
|
791
727
|
return;
|
|
792
728
|
}
|
|
@@ -896,11 +832,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
896
832
|
// EPIPE means process died
|
|
897
833
|
const error = new BridgeProtocolError(this.withStderrTail(`stdin error: ${err.message}`));
|
|
898
834
|
// Reject all pending writes
|
|
899
|
-
|
|
900
|
-
this.clearQueuedWriteTimeout(queued);
|
|
901
|
-
queued.reject(error);
|
|
902
|
-
}
|
|
903
|
-
this.writeQueue.length = 0;
|
|
835
|
+
this.rejectAllQueuedWrites(error);
|
|
904
836
|
// Reject all pending requests
|
|
905
837
|
this.rejectAllPending(error);
|
|
906
838
|
// Mark for restart on next send
|
|
@@ -934,13 +866,11 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
934
866
|
*/
|
|
935
867
|
createQueuedWrite(data, resolve, reject, isLive) {
|
|
936
868
|
const queuedAt = Date.now();
|
|
937
|
-
const entry = { data, resolve, reject, queuedAt, isLive };
|
|
869
|
+
const entry = { data, resolve, reject, queuedAt, isLive, isQueued: true };
|
|
938
870
|
// Set up timeout timer that fires if drain never happens
|
|
939
871
|
entry.timeoutHandle = setTimeout(() => {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (index !== -1) {
|
|
943
|
-
this.writeQueue.splice(index, 1);
|
|
872
|
+
if (entry.isQueued) {
|
|
873
|
+
entry.isQueued = false;
|
|
944
874
|
reject(new BridgeTimeoutError(`Write queue timeout: entry waited ${this.writeQueueTimeoutMs}ms without drain`));
|
|
945
875
|
}
|
|
946
876
|
}, this.writeQueueTimeoutMs);
|
|
@@ -968,7 +898,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
968
898
|
reject(new BridgeProtocolError(this.withStderrTail('Process stdin not available')));
|
|
969
899
|
return;
|
|
970
900
|
}
|
|
971
|
-
if (this.draining || this.writeQueue.length >
|
|
901
|
+
if (this.draining || this.writeQueue.length > this.writeQueueHead) {
|
|
972
902
|
// Queue the write with timestamp, timeout timer, and liveness predicate
|
|
973
903
|
// (checked again at flush — see processQueuedWrite).
|
|
974
904
|
this.writeQueue.push(this.createQueuedWrite(data, resolve, reject, isLive));
|
|
@@ -1003,7 +933,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1003
933
|
}
|
|
1004
934
|
/**
|
|
1005
935
|
* Write one logical request to stdin, fragmenting it into `tywrap-frame/1`
|
|
1006
|
-
* request frames when
|
|
936
|
+
* request frames when the encoded request exceeds
|
|
1007
937
|
* the per-frame ceiling (W5 — the mirror of W4's response chunking).
|
|
1008
938
|
*
|
|
1009
939
|
* Both the chunked and single-line paths run under {@link writeMutex} so a
|
|
@@ -1039,10 +969,9 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1039
969
|
if (!isLive()) {
|
|
1040
970
|
return Promise.resolve();
|
|
1041
971
|
}
|
|
1042
|
-
//
|
|
1043
|
-
// the
|
|
1044
|
-
|
|
1045
|
-
if (this.negotiatedChunking && utf8ByteLength(message) > this.negotiatedFrameBytes) {
|
|
972
|
+
// Chunk when the encoded request exceeds the fixed per-frame ceiling
|
|
973
|
+
// supplied to the packaged Python peer. Otherwise use one JSONL line.
|
|
974
|
+
if (utf8ByteLength(message) > this.frameBytes) {
|
|
1046
975
|
return this.writeChunkedRequest(message, messageId, signal, isLive);
|
|
1047
976
|
}
|
|
1048
977
|
return this.writeToStdin(`${message}\n`, isLive);
|
|
@@ -1069,7 +998,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1069
998
|
const frames = encodeFrames(message, {
|
|
1070
999
|
id: messageId,
|
|
1071
1000
|
stream: 'request',
|
|
1072
|
-
maxFrameBytes: this.
|
|
1001
|
+
maxFrameBytes: this.frameBytes,
|
|
1073
1002
|
});
|
|
1074
1003
|
for (const frame of frames) {
|
|
1075
1004
|
// Stop the burst if the caller aborted, the request was abandoned (timed
|
|
@@ -1092,11 +1021,16 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1092
1021
|
* Clears each entry's timeout before rejecting so no late timer fires.
|
|
1093
1022
|
*/
|
|
1094
1023
|
rejectAllQueuedWrites(error) {
|
|
1095
|
-
for (
|
|
1096
|
-
this.
|
|
1097
|
-
|
|
1024
|
+
for (let index = this.writeQueueHead; index < this.writeQueue.length; index += 1) {
|
|
1025
|
+
const queued = this.writeQueue[index];
|
|
1026
|
+
if (queued?.isQueued) {
|
|
1027
|
+
queued.isQueued = false;
|
|
1028
|
+
this.clearQueuedWriteTimeout(queued);
|
|
1029
|
+
queued.reject(error);
|
|
1030
|
+
}
|
|
1098
1031
|
}
|
|
1099
1032
|
this.writeQueue.length = 0;
|
|
1033
|
+
this.writeQueueHead = 0;
|
|
1100
1034
|
}
|
|
1101
1035
|
/**
|
|
1102
1036
|
* Process a single dequeued write entry: enforce the fallback queue timeout,
|
|
@@ -1148,7 +1082,7 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1148
1082
|
*/
|
|
1149
1083
|
flushWriteQueue() {
|
|
1150
1084
|
const now = Date.now();
|
|
1151
|
-
while (this.writeQueue.length
|
|
1085
|
+
while (this.writeQueueHead < this.writeQueue.length && !this.draining) {
|
|
1152
1086
|
const stdin = this.process?.stdin;
|
|
1153
1087
|
if (!stdin || this.processExited) {
|
|
1154
1088
|
// Process died - reject all queued writes
|
|
@@ -1156,10 +1090,15 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1156
1090
|
this.markForRestart();
|
|
1157
1091
|
return;
|
|
1158
1092
|
}
|
|
1159
|
-
const queued = this.writeQueue.
|
|
1093
|
+
const queued = this.writeQueue[this.writeQueueHead];
|
|
1094
|
+
this.writeQueueHead += 1;
|
|
1160
1095
|
if (!queued) {
|
|
1161
1096
|
return;
|
|
1162
1097
|
}
|
|
1098
|
+
if (!queued.isQueued) {
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
queued.isQueued = false;
|
|
1163
1102
|
// Clear the timeout since we're processing this entry now
|
|
1164
1103
|
this.clearQueuedWriteTimeout(queued);
|
|
1165
1104
|
const status = this.processQueuedWrite(queued, stdin, now);
|
|
@@ -1167,6 +1106,10 @@ export class SubprocessTransport extends DisposableBase {
|
|
|
1167
1106
|
return;
|
|
1168
1107
|
}
|
|
1169
1108
|
}
|
|
1109
|
+
if (this.writeQueueHead === this.writeQueue.length) {
|
|
1110
|
+
this.writeQueue.length = 0;
|
|
1111
|
+
this.writeQueueHead = 0;
|
|
1112
|
+
}
|
|
1170
1113
|
}
|
|
1171
1114
|
// ===========================================================================
|
|
1172
1115
|
// HELPERS
|