posipaki 0.44.0 → 0.45.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.
Files changed (35) hide show
  1. package/dist/{hooks-Dpbe39oa.d.ts → hooks-DO-efRM_.d.ts} +2 -2
  2. package/dist/{hooks-Dpbe39oa.d.ts.map → hooks-DO-efRM_.d.ts.map} +1 -1
  3. package/dist/hooks.d.ts +1 -1
  4. package/dist/index.d.ts +4 -4
  5. package/dist/index.js +1 -1
  6. package/dist/plugins/debug-logger.d.ts +2 -2
  7. package/dist/plugins/tree-introspection.d.ts +2 -2
  8. package/dist/{process-CoEKoY5f.d.ts → process-eCAv2tIS.d.ts} +2 -2
  9. package/dist/{process-CoEKoY5f.d.ts.map → process-eCAv2tIS.d.ts.map} +1 -1
  10. package/dist/remote/gateway-cli.js +1157 -50
  11. package/dist/remote/gateway-cli.js.map +1 -1
  12. package/dist/remote/index.d.ts +4 -232
  13. package/dist/remote/index.d.ts.map +1 -1
  14. package/dist/remote/index.js +2 -2
  15. package/dist/remote/index.js.map +1 -1
  16. package/dist/remote/node.d.ts +29 -54
  17. package/dist/remote/node.d.ts.map +1 -1
  18. package/dist/remote/node.js +128 -128
  19. package/dist/remote/node.js.map +1 -1
  20. package/dist/testing/index.d.ts +4 -4
  21. package/dist/testing/index.d.ts.map +1 -1
  22. package/dist/testing/index.js.map +1 -1
  23. package/dist/{types-CuabnerI.d.ts → types-U3NvKgpg.d.ts} +10 -1
  24. package/dist/{types-CuabnerI.d.ts.map → types-U3NvKgpg.d.ts.map} +1 -1
  25. package/dist/{version-B6eNJRZD.js → version-yonmO5bG.js} +2 -2
  26. package/dist/{version-B6eNJRZD.js.map → version-yonmO5bG.js.map} +1 -1
  27. package/dist/websocket-DT8zKD34.d.ts +836 -0
  28. package/dist/websocket-DT8zKD34.d.ts.map +1 -0
  29. package/dist/{websocket-DecgMfb3.js → websocket-DkDAHMFm.js} +1102 -85
  30. package/dist/websocket-DkDAHMFm.js.map +1 -0
  31. package/dist/xfetch.d.ts +2 -2
  32. package/package.json +1 -1
  33. package/dist/websocket-DecgMfb3.js.map +0 -1
  34. package/dist/websocket-sGQcEDbm.d.ts +0 -310
  35. package/dist/websocket-sGQcEDbm.d.ts.map +0 -1
@@ -3,38 +3,1075 @@ import { mkdtemp, open, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { Writable } from "node:stream";
7
- import * as readline from "node:readline";
6
+ //#region src/remote/process-ref.ts
7
+ /**
8
+ * The id a side gives its own root: the first id of its parity. The two ends of a
9
+ * connection therefore number their roots differently, and an id says whose root it is
10
+ * as it says who holds every other process. A side hands out its next id from two
11
+ * steps further on, the one in between belonging to the root of the side it talks to.
12
+ */
13
+ function rootIdFor(parity) {
14
+ return parity === "odd" ? 1 : 0;
15
+ }
16
+ /**
17
+ * The ids one side of a connection hands out: its own parity, from the far side of the low band.
18
+ *
19
+ * Everything numbered on the connection draws from one space — a process reference and a stream alike —
20
+ * so a number names one thing, and the parity says which side made it. Each side builds its own and
21
+ * hands the same one to whatever allocates.
22
+ */
23
+ var IdSpace = class {
24
+ constructor(parity, first = parity === "odd" ? 9 : 10) {
25
+ this.parity = parity;
26
+ this.pvtNext = first;
27
+ this.pvtBand = first <= 8;
28
+ }
29
+ /** This side's own end of the connection: the first id of its parity, never handed out. */
30
+ root() {
31
+ return rootIdFor(this.parity);
32
+ }
33
+ /** The next id of this side's parity, and never the same one twice. */
34
+ next() {
35
+ if (this.pvtBand && this.pvtNext > 8) throw new Error("IdSpace: the low band is spent");
36
+ const id = this.pvtNext;
37
+ this.pvtNext += 2;
38
+ return id;
39
+ }
40
+ };
41
+ /**
42
+ * A space inside the low band, for a party that speaks on a connection it otherwise relays: its ids
43
+ * come from a range no end allocates in, so announcing something of its own cannot take a number
44
+ * either end might mint.
45
+ */
46
+ function lowBand(parity) {
47
+ return new IdSpace(parity, parity === "odd" ? 3 : 2);
48
+ }
49
+ //#endregion
8
50
  //#region src/remote/channel.ts
51
+ /** Is this something JSON would carry as an object, rather than an instance with behaviour? */
52
+ function isPlainObject(value) {
53
+ if (typeof value !== "object" || value === null) return false;
54
+ const prototype = Object.getPrototypeOf(value);
55
+ return prototype === Object.prototype || prototype === null;
56
+ }
57
+ /**
58
+ * Is this a stream — a value whose bytes come later, carried by reference?
59
+ *
60
+ * The question every walk that copies containers has to ask: copying a stream keeps the object and
61
+ * loses the behaviour, because its iterator is a symbol-keyed property and `Object.entries` does not
62
+ * see those. What such a value is belongs to `stream.ts`; that it must be left alone belongs to
63
+ * every layer that walks a message.
64
+ */
65
+ function isStreamLike(value) {
66
+ if (typeof value !== "object" || value === null) return false;
67
+ return typeof value[Symbol.asyncIterator] === "function";
68
+ }
69
+ /**
70
+ * A channel that is `base`, with every incoming frame offered to `onFrame` first — and not handed
71
+ * on when it is claimed.
72
+ *
73
+ * Every wrapper above a protocol wants this one thing, and none of them may lose the octets: a
74
+ * connection that carries segments keeps carrying them through whatever wraps it, or a stream
75
+ * dies one layer above the wire it was written for.
76
+ */
77
+ function channelThrough(base, onFrame) {
78
+ const wrapped = {
79
+ send: (frame) => base.send(frame),
80
+ onMessage: (handler) => base.onMessage((frame) => {
81
+ if (onFrame(frame)) return;
82
+ handler(frame);
83
+ }),
84
+ onClose: (handler) => base.onClose(handler),
85
+ close: () => base.close()
86
+ };
87
+ if (base.sendSegment) wrapped.sendSegment = (frame, bytes) => base.sendSegment?.(frame, bytes);
88
+ if (base.onSegment) wrapped.onSegment = (handler) => base.onSegment?.(handler) ?? (() => {});
89
+ return wrapped;
90
+ }
91
+ const BAD_KEY = "$bad";
92
+ /** The frame that says a frame could not be read: why, how long it was, and the head of it. */
93
+ function badFrame(reason, raw) {
94
+ const prefix = raw.length > 32 ? `${raw.slice(0, 32)}…` : raw;
95
+ return { [BAD_KEY]: {
96
+ reason,
97
+ length: raw.length,
98
+ prefix
99
+ } };
100
+ }
9
101
  const ERROR_KEY = "$error";
10
102
  /** The frame that says the wire is over: the reason, and the connection ends with it. */
11
103
  function errorFrame$1(reason) {
12
104
  return { [ERROR_KEY]: reason };
13
105
  }
14
- /** A frame of the far end's own output: `data` on the stream `fd`. */
15
- function outputFrame(fd, data) {
16
- return {
17
- $fd: fd,
18
- data
19
- };
20
- }
21
106
  //#endregion
22
107
  //#region src/remote/protocols/json1.ts
23
108
  function encode(frame) {
24
109
  return JSON.stringify(frame);
25
110
  }
111
+ /** The frame a line carries, throwing on a line that is not JSON. `readFrame` is how the
112
+ * seam reads a line; this is the primitive underneath it. */
113
+ function decode(str) {
114
+ return JSON.parse(str);
115
+ }
116
+ /** A line read as a frame: the frame itself, or the frame that says this one could not be
117
+ * read. Never throws — see the note at the top of this file. */
118
+ function readFrame(str) {
119
+ let parsed;
120
+ try {
121
+ parsed = decode(str);
122
+ } catch (err) {
123
+ return badFrame(err instanceof Error ? err.message : String(err), str);
124
+ }
125
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return badFrame(`a frame is one JSON object, and this was ${kindOf(parsed)}`, str);
126
+ return parsed;
127
+ }
128
+ function kindOf(value) {
129
+ if (value === null) return "null";
130
+ if (Array.isArray(value)) return "an array";
131
+ return `a ${typeof value}`;
132
+ }
133
+ /**
134
+ * The octets a frame announces after it, or null when it announces none.
135
+ *
136
+ * One shape only: a chunk head carries the number of raw octets that follow, and that number
137
+ * is what keeps the reader's framing. A head without one is not a segment — it goes on as a
138
+ * frame, and whoever reads it refuses it by name.
139
+ */
140
+ function announcedOctets(frame) {
141
+ const head = frame.$chunk;
142
+ if (typeof head !== "object" || head === null) return null;
143
+ const bytes = head.bytes;
144
+ return typeof bytes === "number" && Number.isInteger(bytes) && bytes >= 0 ? bytes : null;
145
+ }
146
+ function json1Channel(transport) {
147
+ const frameReaders = /* @__PURE__ */ new Set();
148
+ const segmentReaders = /* @__PURE__ */ new Set();
149
+ /** The head whose octets are on their way: set between the line and the run it announced. */
150
+ let expecting = null;
151
+ const deliver = (frame) => {
152
+ for (const reader of [...frameReaders]) reader(frame);
153
+ };
154
+ transport.onMessage((str) => {
155
+ const frame = readFrame(str);
156
+ const octets = announcedOctets(frame);
157
+ if (octets === null) {
158
+ deliver(frame);
159
+ return;
160
+ }
161
+ expecting = frame;
162
+ return octets;
163
+ });
164
+ transport.onBytes((bytes) => {
165
+ const frame = expecting;
166
+ expecting = null;
167
+ if (frame === null) {
168
+ deliver(badFrame(`${bytes.length} octets arrived that no frame announced`, ""));
169
+ return;
170
+ }
171
+ for (const reader of [...segmentReaders]) reader(frame, bytes);
172
+ });
173
+ return {
174
+ send(frame) {
175
+ return transport.send(encode(frame));
176
+ },
177
+ onMessage(handler) {
178
+ frameReaders.add(handler);
179
+ return () => {
180
+ frameReaders.delete(handler);
181
+ };
182
+ },
183
+ async sendSegment(frame, bytes) {
184
+ await transport.send(encode(frame));
185
+ await transport.sendBytes(bytes);
186
+ },
187
+ onSegment(handler) {
188
+ segmentReaders.add(handler);
189
+ return () => {
190
+ segmentReaders.delete(handler);
191
+ };
192
+ },
193
+ onClose(handler) {
194
+ transport.onClose(handler);
195
+ },
196
+ close() {
197
+ return transport.close();
198
+ }
199
+ };
200
+ }
201
+ function isStreamRef(value) {
202
+ const body = value?.$s;
203
+ return typeof body?.id === "number" || typeof body?.name === "string";
204
+ }
205
+ function isBytesRef(value) {
206
+ const body = value?.$bytes;
207
+ return typeof body?.id === "number" && typeof body?.size === "number";
208
+ }
209
+ function isBlobRef(value) {
210
+ const body = value?.$blob;
211
+ return typeof body?.id === "number" && typeof body?.size === "number";
212
+ }
213
+ function isBlob(value) {
214
+ if (typeof value !== "object" || value === null) return false;
215
+ const candidate = value;
216
+ if (typeof candidate.size !== "number") return false;
217
+ if (typeof candidate.arrayBuffer !== "function") return false;
218
+ return typeof candidate.slice === "function" || typeof candidate.stream === "function";
219
+ }
220
+ /** Every octet of a stream, in one array. */
221
+ async function bytesOf(stream) {
222
+ const parts = [];
223
+ let total = 0;
224
+ for await (const part of stream) {
225
+ parts.push(part);
226
+ total += part.length;
227
+ }
228
+ const all = new Uint8Array(total);
229
+ let at = 0;
230
+ for (const part of parts) {
231
+ all.set(part, at);
232
+ at += part.length;
233
+ }
234
+ return all;
235
+ }
236
+ let pvtCrc = null;
237
+ function crcOf() {
238
+ if (pvtCrc !== null) return pvtCrc;
239
+ const bun = globalThis.Bun;
240
+ if (typeof bun?.hash?.crc32 === "function") {
241
+ const native = bun.hash.crc32;
242
+ pvtCrc = (bytes) => native(bytes) >>> 0;
243
+ return pvtCrc;
244
+ }
245
+ const builtin = globalThis.process?.getBuiltinModule?.("node:zlib");
246
+ if (typeof builtin?.crc32 === "function") {
247
+ const native = builtin.crc32;
248
+ pvtCrc = (bytes) => native(bytes) >>> 0;
249
+ return pvtCrc;
250
+ }
251
+ pvtCrc = tableCrc;
252
+ return pvtCrc;
253
+ }
254
+ /** CRC-32 by hand, for a runtime that has none. Correct, and slow where nothing is a JIT. */
255
+ function tableCrc(bytes) {
256
+ let crc = 4294967295;
257
+ for (const byte of bytes) crc = crc >>> 8 ^ pvtTable[(crc ^ byte) & 255];
258
+ return (crc ^ 4294967295) >>> 0;
259
+ }
260
+ const pvtTable = (() => {
261
+ const table = [];
262
+ for (let i = 0; i < 256; i += 1) {
263
+ let value = i;
264
+ for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
265
+ table.push(value >>> 0);
266
+ }
267
+ return table;
268
+ })();
269
+ /** The checksum a chunk's tail carries. */
270
+ function crc32(bytes) {
271
+ return crcOf()(bytes);
272
+ }
273
+ /**
274
+ * Bytes the far side offers, read at the consumer's pace.
275
+ *
276
+ * An async iterable of octets, and nothing more: turning it into an array, a `Blob` or a digest
277
+ * is the platform's job — `new Response(stream).arrayBuffer()` where there is a `Response`,
278
+ * `for await` where there is not.
279
+ */
280
+ var RemoteStream = class {
281
+ constructor(hooks) {
282
+ this.hooks = hooks;
283
+ this.queue = [];
284
+ this.waiting = [];
285
+ this.ended = false;
286
+ this.failure = null;
287
+ this.stopped = false;
288
+ this.platform = null;
289
+ }
290
+ /** The far side sent more of it. */
291
+ push(bytes) {
292
+ if (this.ended || this.failure || this.stopped) return;
293
+ const waiting = this.waiting.shift();
294
+ if (!waiting) {
295
+ this.queue.push(bytes);
296
+ return;
297
+ }
298
+ this.hooks.consumed(bytes.length);
299
+ waiting.resolve({
300
+ value: bytes,
301
+ done: false
302
+ });
303
+ }
304
+ /** The far side has sent all of it. */
305
+ end() {
306
+ if (this.ended || this.failure) return;
307
+ this.ended = true;
308
+ for (const waiting of this.waiting.splice(0)) waiting.resolve({
309
+ value: void 0,
310
+ done: true
311
+ });
312
+ }
313
+ /** The far side gave up on it, or it arrived corrupt. */
314
+ fail(reason) {
315
+ if (this.ended || this.failure) return;
316
+ this.failure = new Error(reason);
317
+ for (const waiting of this.waiting.splice(0)) waiting.reject(this.failure);
318
+ }
319
+ /** Take no more of it. Said to the far side, so it stops reading its source. */
320
+ cancel(reason = "the reader stopped") {
321
+ if (this.stopped || this.ended) return;
322
+ this.stopped = true;
323
+ this.queue.length = 0;
324
+ for (const waiting of this.waiting.splice(0)) waiting.resolve({
325
+ value: void 0,
326
+ done: true
327
+ });
328
+ this.hooks.cancelled(reason);
329
+ }
330
+ async next() {
331
+ const queued = this.queue.shift();
332
+ if (queued) {
333
+ this.hooks.consumed(queued.length);
334
+ return {
335
+ value: queued,
336
+ done: false
337
+ };
338
+ }
339
+ if (this.failure) throw this.failure;
340
+ if (this.ended || this.stopped) return {
341
+ value: void 0,
342
+ done: true
343
+ };
344
+ this.hooks.wanted();
345
+ return new Promise((resolve, reject) => {
346
+ this.waiting.push({
347
+ resolve,
348
+ reject
349
+ });
350
+ });
351
+ }
352
+ async return() {
353
+ this.cancel();
354
+ return {
355
+ value: void 0,
356
+ done: true
357
+ };
358
+ }
359
+ /**
360
+ * The runtime's own stream, built over this one the first time something asks for it. What a
361
+ * sender usually hands over is a `ReadableStream`, so what arrives offers the same interface —
362
+ * `getReader()`, `pipeTo()`, `tee()` — rather than a narrower shape of its own. A runtime
363
+ * without `ReadableStream` keeps the iterable, which is the library's own interface.
364
+ */
365
+ asReadableStream() {
366
+ const ctor = globalThis.ReadableStream;
367
+ if (typeof ctor !== "function") throw new Error("this runtime has no ReadableStream: read the stream with `for await`");
368
+ this.platform ??= new ctor({
369
+ pull: async (controller) => {
370
+ const next = await this.next();
371
+ if (next.done) controller.close();
372
+ else controller.enqueue(next.value);
373
+ },
374
+ cancel: (reason) => {
375
+ this.cancel(typeof reason === "string" ? reason : "the reader stopped");
376
+ }
377
+ });
378
+ return this.platform;
379
+ }
380
+ getReader() {
381
+ return this.asReadableStream().getReader();
382
+ }
383
+ pipeTo(destination, options) {
384
+ return this.asReadableStream().pipeTo(destination, options);
385
+ }
386
+ tee() {
387
+ return this.asReadableStream().tee();
388
+ }
389
+ /** One consumer, whichever door it comes through: where the runtime has a platform stream this
390
+ * path goes through it, so the lock and the queue are the platform's and not two of ours. */
391
+ async *[Symbol.asyncIterator]() {
392
+ if (typeof globalThis.ReadableStream !== "function") for (;;) {
393
+ const next = await this.next();
394
+ if (next.done) return;
395
+ yield next.value;
396
+ }
397
+ const reader = this.asReadableStream().getReader();
398
+ let finished = false;
399
+ try {
400
+ for (;;) {
401
+ const next = await reader.read();
402
+ if (next.done) {
403
+ finished = true;
404
+ return;
405
+ }
406
+ yield next.value;
407
+ }
408
+ } finally {
409
+ reader.releaseLock();
410
+ if (!finished) this.cancel();
411
+ }
412
+ }
413
+ };
414
+ /**
415
+ * A blob that came over a connection: the same value the far side offered, with its bytes still
416
+ * over there. Reading it is what asks for them — and only at the pace the reader takes them, so a
417
+ * file nobody opens costs nothing on the wire.
418
+ */
419
+ var RemoteBlob = class {
420
+ constructor(body, facts) {
421
+ this.facts = facts;
422
+ this.body = body;
423
+ }
424
+ get size() {
425
+ return this.facts.size;
426
+ }
427
+ get type() {
428
+ return this.facts.type;
429
+ }
430
+ get name() {
431
+ return this.facts.name;
432
+ }
433
+ stream() {
434
+ return this.body;
435
+ }
436
+ async arrayBuffer() {
437
+ return (await bytesOf(this.body)).buffer;
438
+ }
439
+ };
440
+ /**
441
+ * Bytes that crossed a connection: the sender's array, taken apart into a stream and put back
442
+ * together here as the chunks arrive. Not lazy — the sender was asked the moment the reference
443
+ * arrived — so awaiting this is waiting for the last chunk, not for a read to start.
444
+ */
445
+ var RemoteBytes = class {
446
+ constructor(body, size) {
447
+ this.filling = null;
448
+ this.body = body;
449
+ this.size = size;
450
+ }
451
+ /** Start re-assembling. Called by the store once its record for this stream is in place. */
452
+ start() {
453
+ this.bytes().catch(() => {});
454
+ }
455
+ /** The octets, once they are all here. */
456
+ bytes() {
457
+ this.filling ??= this.pvtFill();
458
+ return this.filling;
459
+ }
460
+ /** The same octets as an array's buffer, for a caller that wants the platform's shape. */
461
+ async arrayBuffer() {
462
+ return (await this.bytes()).buffer;
463
+ }
464
+ then(onfulfilled, onrejected) {
465
+ return this.bytes().then(onfulfilled, onrejected);
466
+ }
467
+ async pvtFill() {
468
+ const all = new Uint8Array(this.size);
469
+ let at = 0;
470
+ for await (const chunk of this.body) {
471
+ if (at + chunk.length > all.length) throw new Error(`the array is longer than the ${this.size} bytes it stated`);
472
+ all.set(chunk, at);
473
+ at += chunk.length;
474
+ }
475
+ if (at !== all.length) throw new Error(`the array is short: ${at} of ${this.size} octets arrived`);
476
+ return all;
477
+ }
478
+ };
479
+ /**
480
+ * One connection's streams, as this side sees them.
481
+ *
482
+ * Both directions are in one table, because both are the same numbering: what this side offers
483
+ * (and answers asks for) and what it may read from the far side. Ids follow the process table's
484
+ * parity, so an id names one stream on one connection and nothing anywhere else.
485
+ */
486
+ var StreamStore = class {
487
+ constructor(channel, parity, options = {}) {
488
+ this.pvtOffers = /* @__PURE__ */ new Map();
489
+ this.pvtIncoming = /* @__PURE__ */ new Map();
490
+ this.pvtRefused = /* @__PURE__ */ new Map();
491
+ this.pvtNamesOut = /* @__PURE__ */ new Map();
492
+ this.pvtNamesIn = /* @__PURE__ */ new Map();
493
+ this.pvtNamedListeners = /* @__PURE__ */ new Set();
494
+ this.pvtSegmentOff = null;
495
+ this.pvtClosed = false;
496
+ this.channel = channel;
497
+ this.chunkBytes = options.chunkBytes ?? 65536;
498
+ this.windowBytes = options.windowBytes ?? 262144;
499
+ this.maxBytes = options.maxBytes ?? 67108864;
500
+ this.ids = options.ids ?? new IdSpace(parity);
501
+ const attached = options.attach !== false;
502
+ this.pvtOff = attached ? channel.onMessage((frame) => {
503
+ this.onFrame(frame);
504
+ }) : () => {};
505
+ this.pvtSegmentOff = attached ? channel.onSegment?.((head, bytes) => this.onSegment(head, bytes)) ?? null : null;
506
+ }
507
+ /** Whether this connection can carry a stream at all: it must carry octets beside its lines. */
508
+ get canCarryStreams() {
509
+ return this.pvtSegmentOff !== null && this.channel.sendSegment !== void 0;
510
+ }
511
+ /**
512
+ * Offer a stream to the connection: the announcement goes out now, and the bytes when the far
513
+ * side asks for them. What comes back is what a message carries in their place.
514
+ *
515
+ * A `name` makes the stream findable: the far side can reference it by that name instead of the
516
+ * id, and ask for it with `named()` without waiting for a message to carry a reference.
517
+ */
518
+ offer(stream, name) {
519
+ return { $s: { id: this.pvtAnnounce(stream[Symbol.asyncIterator](), name) } };
520
+ }
521
+ /**
522
+ * Offer a blob — a whole file, read on demand. It crosses as a stream over its bytes and arrives
523
+ * on the far side as a `RemoteBlob`, so nothing of it is read until somebody wants the bytes.
524
+ */
525
+ offerBlob(blob) {
526
+ const { size, type, name } = blob;
527
+ return { $blob: {
528
+ id: this.pvtAnnounce(this.pvtBlobChunks(blob)),
529
+ size,
530
+ ...type === void 0 ? {} : { type },
531
+ ...name === void 0 ? {} : { name }
532
+ } };
533
+ }
534
+ /**
535
+ * Offer bytes this side already holds. The reference states the length, so the far side can
536
+ * check it against its bound and size the array before the first chunk arrives — a typed array
537
+ * is one piece, so the wire carries it as a stream of one.
538
+ */
539
+ offerBytes(bytes) {
540
+ return { $bytes: {
541
+ id: this.pvtAnnounce(this.pvtOnePiece(bytes)),
542
+ size: bytes.length
543
+ } };
544
+ }
545
+ pvtAnnounce(source, name) {
546
+ const id = this.ids.next();
547
+ this.pvtOffers.set(id, {
548
+ id,
549
+ source,
550
+ pending: null,
551
+ credit: 0,
552
+ seq: 0,
553
+ pumping: false,
554
+ dropped: false
555
+ });
556
+ if (name !== void 0) this.pvtNamesOut.set(name, id);
557
+ this.channel.send(name === void 0 ? { $stream: { id } } : { $stream: {
558
+ id,
559
+ name
560
+ } });
561
+ return id;
562
+ }
563
+ async *pvtOnePiece(bytes) {
564
+ if (bytes.length > 0) yield bytes;
565
+ }
566
+ /** A blob's octets, a window at a time: sliced where the value can, streamed where it cannot. */
567
+ async *pvtBlobChunks(blob) {
568
+ const slice = blob.slice;
569
+ if (slice) {
570
+ for (let at = 0; at < blob.size; at += this.chunkBytes) {
571
+ const end = Math.min(at + this.chunkBytes, blob.size);
572
+ yield new Uint8Array(await slice.call(blob, at, end).arrayBuffer());
573
+ }
574
+ return;
575
+ }
576
+ const stream = blob.stream;
577
+ if (!stream) throw new TypeError("a blob must offer a slice or a stream");
578
+ yield* stream.call(blob);
579
+ }
580
+ /** A value on its way out: every stream in it becomes an announcement and a reference. */
581
+ encode(value) {
582
+ return this.pvtEncode(value, /* @__PURE__ */ new Set());
583
+ }
584
+ /** A value just read: every reference in it becomes something this side can read. */
585
+ decode(value) {
586
+ return this.pvtDecode(value, /* @__PURE__ */ new Set());
587
+ }
588
+ /** Drop every stream of this connection, in both directions. Called when it ends. */
589
+ close() {
590
+ if (this.pvtClosed) return;
591
+ this.pvtClosed = true;
592
+ this.pvtOff();
593
+ this.pvtSegmentOff?.();
594
+ for (const offered of this.pvtOffers.values()) {
595
+ offered.dropped = true;
596
+ offered.source.return?.();
597
+ }
598
+ this.pvtOffers.clear();
599
+ for (const incoming of this.pvtIncoming.values()) incoming.reader.fail("the connection ended");
600
+ this.pvtIncoming.clear();
601
+ }
602
+ /** The ids this side is holding, in the order they were offered. */
603
+ held() {
604
+ return [...this.pvtOffers.keys()];
605
+ }
606
+ /**
607
+ * Take a frame if it is one this store answers for, and say whether it did. A party that carries a
608
+ * connection it does not speak — a first stage, relaying a client to a payload — has to know which
609
+ * inbound frames are its own and which belong to the far side, and this is the question it asks.
610
+ * Only frames about a stream this side offered are claimed, so a stream the far side offers passes
611
+ * through untouched.
612
+ */
613
+ handle(frame) {
614
+ if (this.pvtClosed) return false;
615
+ const id = frameId(frame);
616
+ if (id === null || !this.pvtOffers.has(id)) return false;
617
+ this.onFrame(frame);
618
+ return true;
619
+ }
620
+ /**
621
+ * The stream the far side announced under `name`, if it is still live — the way a caller finds a
622
+ * stream nobody sent it a reference to. A name that was never announced answers `undefined`, and a
623
+ * name whose stream is over does too: the sentence lives on the value the reference would give.
624
+ */
625
+ named(name) {
626
+ const id = this.pvtNamesIn.get(name);
627
+ if (id === void 0) return void 0;
628
+ return this.pvtValueFor(id, "stream");
629
+ }
630
+ /**
631
+ * Every named stream the far side announces, now and later. The listener is called for the names
632
+ * already live when it is installed and for each new one as it arrives, so a caller that wires a
633
+ * sink to `stdout` does not have to know whether the far side has printed yet.
634
+ */
635
+ onNamed(listener) {
636
+ this.pvtNamedListeners.add(listener);
637
+ for (const [name, id] of this.pvtNamesIn) if (this.pvtIncoming.has(id)) listener(name, this.pvtValueFor(id, "stream"));
638
+ return () => {
639
+ this.pvtNamedListeners.delete(listener);
640
+ };
641
+ }
642
+ pvtSend(frame) {
643
+ return Promise.resolve(this.channel.send(frame));
644
+ }
645
+ pvtEncode(value, seen) {
646
+ if (typeof value !== "object" || value === null) return value;
647
+ if (isBlob(value)) return this.offerBlob(value);
648
+ if (isStreamLike(value)) return this.offer(value);
649
+ if (seen.has(value)) return value;
650
+ seen.add(value);
651
+ if (Array.isArray(value)) return value.map((item) => this.pvtEncode(item, seen));
652
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return this.offerBytes(asOctets(value));
653
+ if (!isPlainObject(value)) return value;
654
+ const copy = {};
655
+ for (const [key, item] of Object.entries(value)) copy[key] = this.pvtEncode(item, seen);
656
+ return copy;
657
+ }
658
+ pvtTake(offered) {
659
+ if (offered.pending === null) return null;
660
+ const size = Math.min(offered.pending.length, this.chunkBytes, offered.credit);
661
+ const piece = offered.pending.subarray(0, size);
662
+ offered.pending = size === offered.pending.length ? null : offered.pending.subarray(size);
663
+ return piece;
664
+ }
665
+ async pvtPull(offered) {
666
+ const next = await offered.source.next();
667
+ if (next.done) return false;
668
+ const value = next.value;
669
+ const bytes = value instanceof Uint8Array ? value : ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : value instanceof ArrayBuffer ? new Uint8Array(value) : null;
670
+ if (bytes === null) throw new TypeError("a stream may only yield octets");
671
+ if (bytes.length === 0) return true;
672
+ offered.pending = bytes;
673
+ return true;
674
+ }
675
+ async pvtPump(offered) {
676
+ if (offered.pumping || offered.dropped || this.pvtClosed) return;
677
+ offered.pumping = true;
678
+ try {
679
+ while (!offered.dropped && offered.credit > 0) {
680
+ if (offered.pending === null && !await this.pvtPull(offered)) {
681
+ await this.channel.send({ $streamDone: { id: offered.id } });
682
+ this.pvtDrop(offered);
683
+ return;
684
+ }
685
+ const piece = this.pvtTake(offered);
686
+ if (piece === null) continue;
687
+ const seq = offered.seq;
688
+ offered.seq += 1;
689
+ offered.credit -= piece.length;
690
+ await this.channel.sendSegment?.({ $chunk: {
691
+ id: offered.id,
692
+ seq,
693
+ bytes: piece.length
694
+ } }, piece);
695
+ await this.channel.send({ $chunkEnd: {
696
+ id: offered.id,
697
+ seq,
698
+ crc32: crc32(piece)
699
+ } });
700
+ }
701
+ } catch (error) {
702
+ await this.pvtSend({ $streamFail: {
703
+ id: offered.id,
704
+ reason: reasonOf(error)
705
+ } }).catch(() => {});
706
+ this.pvtDrop(offered);
707
+ } finally {
708
+ offered.pumping = false;
709
+ }
710
+ }
711
+ pvtDrop(offered) {
712
+ if (offered.dropped) return;
713
+ offered.dropped = true;
714
+ this.pvtOffers.delete(offered.id);
715
+ this.pvtRelease(offered.id);
716
+ }
717
+ /**
718
+ * Take a stream out of the live table and remember why it ended, for a reference that arrives
719
+ * after the fact: "no stream was announced" is the wrong sentence for a stream that was refused.
720
+ */
721
+ pvtRetire(id, reason, form = "stream") {
722
+ this.pvtIncoming.delete(id);
723
+ this.pvtRelease(id);
724
+ this.pvtRefused.set(id, {
725
+ reason,
726
+ form
727
+ });
728
+ if (this.pvtRefused.size > 64) {
729
+ const oldest = this.pvtRefused.keys().next();
730
+ if (!oldest.done) this.pvtRefused.delete(oldest.value);
731
+ }
732
+ }
733
+ /** A stream that is over gives its name back, so the next announcement of it is not a duplicate. */
734
+ pvtRelease(id) {
735
+ for (const [name, bound] of this.pvtNamesIn) if (bound === id) this.pvtNamesIn.delete(name);
736
+ for (const [name, bound] of this.pvtNamesOut) if (bound === id) this.pvtNamesOut.delete(name);
737
+ }
738
+ pvtDecode(value, seen) {
739
+ if (typeof value !== "object" || value === null) return value;
740
+ if (isBytesRef(value)) return this.pvtValueFor(value.$bytes.id, "bytes", { size: value.$bytes.size });
741
+ if (isBlobRef(value)) {
742
+ const { id, size, type, name } = value.$blob;
743
+ return this.pvtValueFor(id, "blob", {
744
+ size,
745
+ type,
746
+ name
747
+ });
748
+ }
749
+ if (isStreamRef(value)) {
750
+ const ref = value.$s;
751
+ return "id" in ref ? this.pvtValueFor(ref.id, "stream") : this.pvtValueForNamed(ref.name);
752
+ }
753
+ if (seen.has(value)) return value;
754
+ seen.add(value);
755
+ if (Array.isArray(value)) return value.map((item) => this.pvtDecode(item, seen));
756
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
757
+ if (!isPlainObject(value)) return value;
758
+ const copy = {};
759
+ for (const [key, item] of Object.entries(value)) copy[key] = this.pvtDecode(item, seen);
760
+ return copy;
761
+ }
762
+ pvtValueFor(id, form, facts) {
763
+ const incoming = this.pvtIncoming.get(id);
764
+ if (!incoming) {
765
+ const ended = this.pvtRefused.get(id);
766
+ const failed = new RemoteStream({
767
+ consumed() {},
768
+ cancelled() {},
769
+ wanted() {}
770
+ });
771
+ failed.fail(ended?.reason ?? `no stream was announced with id ${id}`);
772
+ return this.pvtBuild(failed, ended?.form ?? form, facts);
773
+ }
774
+ if (incoming.value) return incoming.value;
775
+ incoming.form = form;
776
+ const value = incoming.value = this.pvtBuild(incoming.reader, form, facts);
777
+ if (facts !== void 0 && facts.size > this.maxBytes) {
778
+ const reason = `stream ${id} is ${facts.size} bytes, over this side's bound of ${this.maxBytes}`;
779
+ incoming.done = true;
780
+ incoming.reader.fail(reason);
781
+ this.pvtRetire(id, reason, form);
782
+ this.channel.send({ $streamFail: {
783
+ id,
784
+ reason: "over the reader's bound"
785
+ } });
786
+ }
787
+ if (value instanceof RemoteBytes) value.start();
788
+ return value;
789
+ }
790
+ /** The value a name the far side announced stands for — or one that says it was never announced. */
791
+ pvtValueForNamed(name) {
792
+ const id = this.pvtNamesIn.get(name);
793
+ if (id !== void 0 && this.pvtIncoming.has(id)) return this.pvtValueFor(id, "stream");
794
+ const failed = new RemoteStream({
795
+ consumed() {},
796
+ cancelled() {},
797
+ wanted() {}
798
+ });
799
+ failed.fail(`no stream was announced with the name ${name}`);
800
+ return failed;
801
+ }
802
+ pvtBuild(reader, form, facts) {
803
+ if (form === "blob" && facts !== void 0) return new RemoteBlob(reader, facts);
804
+ if (form === "bytes" && facts !== void 0) return new RemoteBytes(reader, facts.size);
805
+ return reader;
806
+ }
807
+ pvtAsk(incoming) {
808
+ if (incoming.done || this.pvtClosed) return;
809
+ const outstanding = incoming.asked - incoming.consumed;
810
+ if (outstanding >= this.windowBytes / 2) return;
811
+ const credit = this.windowBytes - outstanding;
812
+ incoming.asked += credit;
813
+ this.channel.send({ $streamAsk: {
814
+ id: incoming.id,
815
+ credit
816
+ } });
817
+ }
818
+ pvtAnnounced(id, name) {
819
+ this.pvtIncoming.get(id)?.reader.fail(`a stream was announced twice with id ${id}`);
820
+ if (name !== void 0) {
821
+ const taken = this.pvtNamesIn.get(name);
822
+ if (taken !== void 0) {
823
+ const reason = `a stream named ${name} is already live (id ${taken})`;
824
+ this.pvtRetire(id, reason, "stream");
825
+ this.channel.send({ $streamFail: {
826
+ id,
827
+ reason
828
+ } });
829
+ return;
830
+ }
831
+ this.pvtNamesIn.set(name, id);
832
+ }
833
+ let incoming;
834
+ incoming = {
835
+ id,
836
+ reader: new RemoteStream({
837
+ consumed: (bytes) => {
838
+ incoming.consumed += bytes;
839
+ this.pvtAsk(incoming);
840
+ },
841
+ wanted: () => {
842
+ this.pvtAsk(incoming);
843
+ },
844
+ cancelled: (reason) => {
845
+ incoming.done = true;
846
+ this.pvtIncoming.delete(id);
847
+ this.pvtRelease(id);
848
+ this.channel.send({ $streamCancel: {
849
+ id,
850
+ reason
851
+ } });
852
+ }
853
+ }),
854
+ form: "stream",
855
+ asked: 0,
856
+ consumed: 0,
857
+ received: 0,
858
+ chunk: null,
859
+ done: false
860
+ };
861
+ this.pvtIncoming.set(id, incoming);
862
+ if (name !== void 0) {
863
+ const stream = this.pvtValueFor(id, "stream");
864
+ for (const listener of this.pvtNamedListeners) listener(name, stream);
865
+ }
866
+ }
867
+ onSegment(head, bytes) {
868
+ const chunk = head.$chunk;
869
+ if (typeof chunk?.id !== "number" || typeof chunk.seq !== "number") return;
870
+ const incoming = this.pvtIncoming.get(chunk.id);
871
+ if (!incoming || incoming.done) return;
872
+ incoming.received += bytes.length;
873
+ if (incoming.received > this.maxBytes) {
874
+ const reason = `stream ${incoming.id} passed this side's bound of ${this.maxBytes} bytes`;
875
+ incoming.done = true;
876
+ incoming.reader.fail(reason);
877
+ this.pvtRetire(incoming.id, reason, incoming.form);
878
+ this.channel.send({ $streamFail: {
879
+ id: incoming.id,
880
+ reason: "over the reader's bound"
881
+ } });
882
+ return;
883
+ }
884
+ incoming.chunk = {
885
+ seq: chunk.seq,
886
+ bytes
887
+ };
888
+ }
889
+ onFrame(frame) {
890
+ if (this.pvtClosed) return;
891
+ if ("$stream" in frame) {
892
+ const body = frame.$stream;
893
+ if (typeof body?.id !== "number") return;
894
+ this.pvtAnnounced(body.id, typeof body.name === "string" ? body.name : void 0);
895
+ return;
896
+ }
897
+ if ("$streamAsk" in frame) {
898
+ const body = frame.$streamAsk;
899
+ const offered = typeof body?.id === "number" ? this.pvtOffers.get(body.id) : void 0;
900
+ if (!offered || typeof body?.credit !== "number" || body.credit <= 0) return;
901
+ offered.credit += body.credit;
902
+ this.pvtPump(offered);
903
+ return;
904
+ }
905
+ if ("$chunkEnd" in frame) {
906
+ const body = frame.$chunkEnd;
907
+ const incoming = typeof body?.id === "number" ? this.pvtIncoming.get(body.id) : void 0;
908
+ if (!incoming) return;
909
+ const chunk = incoming.chunk;
910
+ incoming.chunk = null;
911
+ if (chunk === null || chunk.seq !== body?.seq) {
912
+ incoming.done = true;
913
+ incoming.reader.fail(`stream ${incoming.id} lost its framing: a chunk ended that never began`);
914
+ this.pvtRetire(incoming.id, `stream ${incoming.id} lost its framing: a chunk ended that never began`, incoming.form);
915
+ this.channel.send({ $streamFail: {
916
+ id: incoming.id,
917
+ reason: "lost the framing"
918
+ } });
919
+ return;
920
+ }
921
+ if (typeof body?.crc32 === "number" && crc32(chunk.bytes) !== body.crc32) {
922
+ incoming.done = true;
923
+ incoming.reader.fail(`stream ${incoming.id} arrived corrupt: chunk ${chunk.seq} failed its checksum`);
924
+ this.pvtRetire(incoming.id, `stream ${incoming.id} arrived corrupt: chunk ${chunk.seq} failed its checksum`, incoming.form);
925
+ this.channel.send({ $streamFail: {
926
+ id: incoming.id,
927
+ reason: "a chunk failed its checksum"
928
+ } });
929
+ return;
930
+ }
931
+ incoming.reader.push(chunk.bytes);
932
+ return;
933
+ }
934
+ if ("$streamDone" in frame) {
935
+ const body = frame.$streamDone;
936
+ const incoming = typeof body?.id === "number" ? this.pvtIncoming.get(body.id) : void 0;
937
+ if (!incoming) return;
938
+ incoming.done = true;
939
+ this.pvtIncoming.delete(incoming.id);
940
+ this.pvtRelease(incoming.id);
941
+ incoming.reader.end();
942
+ return;
943
+ }
944
+ if ("$streamFail" in frame) {
945
+ const body = frame.$streamFail;
946
+ const incoming = typeof body?.id === "number" ? this.pvtIncoming.get(body.id) : void 0;
947
+ if (!incoming) return;
948
+ const reason = typeof body?.reason === "string" ? body.reason : `stream ${incoming.id} failed`;
949
+ incoming.done = true;
950
+ incoming.reader.fail(reason);
951
+ this.pvtRetire(incoming.id, reason, incoming.form);
952
+ return;
953
+ }
954
+ if ("$streamCancel" in frame) {
955
+ const body = frame.$streamCancel;
956
+ const offered = typeof body?.id === "number" ? this.pvtOffers.get(body.id) : void 0;
957
+ if (!offered) return;
958
+ this.pvtDrop(offered);
959
+ offered.source.return?.();
960
+ }
961
+ }
962
+ };
963
+ /** A typed array or an `ArrayBuffer` as the octets the wire carries. */
964
+ function asOctets(value) {
965
+ if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
966
+ return new Uint8Array(value);
967
+ }
968
+ function reasonOf(error) {
969
+ return error instanceof Error ? error.message : String(error);
970
+ }
971
+ /** The stream id a control frame names, or null when it names none of them. */
972
+ function frameId(frame) {
973
+ for (const key of [
974
+ "$streamAsk",
975
+ "$streamCancel",
976
+ "$streamDone",
977
+ "$streamFail"
978
+ ]) {
979
+ const body = frame[key];
980
+ if (typeof body?.id === "number") return body.id;
981
+ }
982
+ return null;
983
+ }
26
984
  //#endregion
27
- //#region src/remote/streams.ts
985
+ //#region src/remote/lines.ts
986
+ const NL = 10;
987
+ const CR = 13;
988
+ function withoutCr(line) {
989
+ return line.length > 0 && line[line.length - 1] === CR ? line.subarray(0, line.length - 1) : line;
990
+ }
28
991
  /**
29
- * A writable that writes what is written to it as output frames of one fd: one chunk is one frame.
992
+ * The wire as a sequence of lines, where a line may announce raw octets to follow it.
30
993
  *
31
- * The shape of an output frame lives here rather than at each caller — the gateway relays a payload's
32
- * own output and holds no channel — and a send that fails is the stream's error, not a lost line.
994
+ * `onLine` gets a line and answers how many octets come next — none, for every frame but
995
+ * one that carries them. Those octets reach `onBytes` whole, however many reads they
996
+ * arrived in, and the reader goes back to looking for a newline.
33
997
  */
34
- function fdWriter(send, fd) {
35
- return new Writable({ write(chunk, _encoding, done) {
36
- Promise.resolve(send(outputFrame(fd, String(chunk)))).then(() => done(), (error) => done(error));
37
- } });
998
+ var PartReader = class {
999
+ constructor(onLine, onBytes) {
1000
+ this.onLine = onLine;
1001
+ this.onBytes = onBytes;
1002
+ this.acc = Buffer.alloc(0);
1003
+ this.want = 0;
1004
+ this.done = false;
1005
+ }
1006
+ feed(chunk) {
1007
+ if (this.done) return;
1008
+ this.acc = this.acc.length === 0 ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) : Buffer.concat([this.acc, chunk]);
1009
+ this.pump();
1010
+ }
1011
+ /** The stream is over: whatever is left of a line is still a line, as `readline` said. */
1012
+ end() {
1013
+ if (this.done) return;
1014
+ const rest = this.acc;
1015
+ this.acc = Buffer.alloc(0);
1016
+ this.done = true;
1017
+ if (rest.length > 0 && this.want === 0) this.onLine(withoutCr(rest).toString("utf8"));
1018
+ }
1019
+ pump() {
1020
+ for (;;) {
1021
+ if (this.want > 0) {
1022
+ if (this.acc.length < this.want) return;
1023
+ const bytes = this.acc.subarray(0, this.want);
1024
+ this.acc = this.acc.subarray(this.want);
1025
+ this.want = 0;
1026
+ this.onBytes(bytes);
1027
+ continue;
1028
+ }
1029
+ const at = this.acc.indexOf(NL);
1030
+ if (at < 0) return;
1031
+ const line = this.acc.subarray(0, at);
1032
+ this.acc = this.acc.subarray(at + 1);
1033
+ const asked = this.onLine(withoutCr(line).toString("utf8"));
1034
+ if (typeof asked === "number") {
1035
+ if (asked > 0) this.want = asked;
1036
+ else this.onBytes(/* @__PURE__ */ new Uint8Array(0));
1037
+ }
1038
+ }
1039
+ }
1040
+ };
1041
+ /**
1042
+ * Read `stream` as lines, with the octets a line announces.
1043
+ *
1044
+ * `onEnd` is called once when the stream is over, after the last line: a stream that
1045
+ * ends without its final newline still gives up its last line, because that is what the
1046
+ * peer meant to say.
1047
+ */
1048
+ function readParts(stream, onLine, onBytes, onEnd) {
1049
+ const reader = new PartReader(onLine, onBytes);
1050
+ return readChunks(stream, (chunk) => reader.feed(chunk), () => {
1051
+ reader.end();
1052
+ onEnd?.();
1053
+ });
1054
+ }
1055
+ /** The bytes a stream carries, before anything is made of them. */
1056
+ function readChunks(stream, onChunk, onEnd) {
1057
+ let stopped = false;
1058
+ const onData = (chunk) => {
1059
+ if (stopped) return;
1060
+ onChunk(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
1061
+ };
1062
+ const finish = () => {
1063
+ if (stopped) return;
1064
+ stopped = true;
1065
+ stream.off("data", onData);
1066
+ onEnd?.();
1067
+ };
1068
+ stream.on("data", onData);
1069
+ stream.once("end", finish);
1070
+ return { stop() {
1071
+ stopped = true;
1072
+ stream.off("data", onData);
1073
+ stream.off("end", finish);
1074
+ } };
38
1075
  }
39
1076
  //#endregion
40
1077
  //#region src/remote/stdio.ts
@@ -67,15 +1104,14 @@ function errorText$1(err) {
67
1104
  var LineTransport = class {
68
1105
  constructor(streams) {
69
1106
  this.pvtOnMessage = null;
1107
+ this.pvtOnBytes = null;
70
1108
  this.pvtOnClose = null;
71
1109
  this.pvtClosed = false;
72
1110
  this.pvtClosing = null;
73
1111
  this.streams = streams;
74
- const lines = readline.createInterface({ input: streams.read });
75
- lines.on("line", (line) => {
76
- if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);
77
- });
78
- lines.on("close", () => {
1112
+ readParts(streams.read, (line) => this.pvtClosed ? void 0 : this.pvtOnMessage?.(line), (bytes) => {
1113
+ if (!this.pvtClosed) this.pvtOnBytes?.(bytes);
1114
+ }, () => {
79
1115
  this.pvtOnClose?.();
80
1116
  this.close();
81
1117
  });
@@ -97,6 +1133,10 @@ var LineTransport = class {
97
1133
  if (this.pvtOnMessage !== null) throw new StdioWireError("stdio transport: handler already set — call removeHandler()");
98
1134
  this.pvtOnMessage = handler;
99
1135
  }
1136
+ onBytes(handler) {
1137
+ if (this.pvtClosed) throw new StdioWireError("stdio transport: closed");
1138
+ this.pvtOnBytes = handler;
1139
+ }
100
1140
  removeHandler() {
101
1141
  const previous = this.pvtOnMessage;
102
1142
  this.pvtOnMessage = null;
@@ -106,11 +1146,17 @@ var LineTransport = class {
106
1146
  this.pvtOnClose = handler;
107
1147
  }
108
1148
  send(frame) {
109
- if (this.pvtClosed) return Promise.resolve();
110
1149
  const line = frame.endsWith("\n") ? frame : `${frame}\n`;
1150
+ return this.pvtWrite(line);
1151
+ }
1152
+ sendBytes(bytes) {
1153
+ return this.pvtWrite(bytes);
1154
+ }
1155
+ pvtWrite(data) {
1156
+ if (this.pvtClosed) return Promise.resolve();
111
1157
  return new Promise((resolve, reject) => {
112
1158
  try {
113
- this.streams.write.write(line, (err) => {
1159
+ this.streams.write.write(data, (err) => {
114
1160
  if (!err) return resolve();
115
1161
  const code = err.code;
116
1162
  if (peerGone(code)) return resolve();
@@ -141,6 +1187,7 @@ var LineTransport = class {
141
1187
  var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
142
1188
  constructor(opts) {
143
1189
  this.pvtOnMessage = null;
1190
+ this.pvtOnBytes = null;
144
1191
  this.pvtOnClose = null;
145
1192
  this.closed = false;
146
1193
  this.closingPromise = null;
@@ -148,12 +1195,10 @@ var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
148
1195
  this.readFd = opts.readFd ?? null;
149
1196
  this.writeFd = opts.writeFd ?? null;
150
1197
  if (this.readFd) {
151
- this.rs = this.readFd.createReadStream({ encoding: "utf-8" });
152
- this.rl = readline.createInterface({ input: this.rs });
153
- this.rl.on("line", (line) => {
154
- if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);
155
- });
156
- this.rl.on("close", () => {
1198
+ this.rs = this.readFd.createReadStream();
1199
+ this.reader = readParts(this.rs, (line) => this.closed ? void 0 : this.pvtOnMessage?.(line), (bytes) => {
1200
+ if (!this.closed) this.pvtOnBytes?.(bytes);
1201
+ }, () => {
157
1202
  this.pvtOnClose?.();
158
1203
  this.close();
159
1204
  });
@@ -163,10 +1208,10 @@ var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
163
1208
  });
164
1209
  } else {
165
1210
  this.rs = null;
166
- this.rl = null;
1211
+ this.reader = null;
167
1212
  }
168
1213
  if (this.writeFd) {
169
- this.ws = this.writeFd.createWriteStream({ encoding: "utf-8" });
1214
+ this.ws = this.writeFd.createWriteStream({});
170
1215
  this.ws.on("error", (err) => {
171
1216
  this.pvtError = err;
172
1217
  this.close();
@@ -221,6 +1266,11 @@ var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
221
1266
  get canSend() {
222
1267
  return this.writeFd !== null && !this.closed;
223
1268
  }
1269
+ onBytes(handler) {
1270
+ if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
1271
+ if (this.readFd === null) throw new Error("FifoUtf8NlineTransport: not a reader");
1272
+ this.pvtOnBytes = handler;
1273
+ }
224
1274
  onMessage(handler) {
225
1275
  if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
226
1276
  if (this.readFd === null) throw new Error("FifoUtf8NlineTransport: not a reader");
@@ -247,6 +1297,11 @@ var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
247
1297
  if (!line.endsWith("\n")) line += "\n";
248
1298
  await this.ws?.write(line);
249
1299
  }
1300
+ async sendBytes(bytes) {
1301
+ if (this.closed) throw new Error("FifoUtf8NlineTransport: closed");
1302
+ if (this.writeFd === null) throw new Error("FifoUtf8NlineTransport: not a writer");
1303
+ await this.ws?.write(bytes);
1304
+ }
250
1305
  async close() {
251
1306
  this.closingPromise = this.closingPromise || this.pvtClose();
252
1307
  await this.closingPromise;
@@ -254,9 +1309,9 @@ var FifoUtf8NlineTransport = class FifoUtf8NlineTransport {
254
1309
  }
255
1310
  async pvtClose() {
256
1311
  await Promise.resolve();
257
- if (this.rl) {
258
- this.rl.close();
259
- this.rl = null;
1312
+ if (this.reader) {
1313
+ this.reader.stop();
1314
+ this.reader = null;
260
1315
  }
261
1316
  if (this.rs) {
262
1317
  this.rs.destroy();
@@ -325,13 +1380,13 @@ async function fail(wire, reason) {
325
1380
  await wire.send(errorFrame(reason));
326
1381
  } catch {}
327
1382
  }
328
- /** Copy everything the payload prints to the client, as output frames of the fd it came in on. */
329
- function forwardOutput(stream, wire, fd) {
330
- stream.setEncoding("utf-8");
331
- const out = fdWriter((frame) => wire.send(encode(frame)), fd);
332
- out.on("error", () => {});
333
- stream.pipe(out, { end: false });
1383
+ /** The payload's own prints, as a stream: the pipe's chunks as they arrive. */
1384
+ async function* chunksOf(stream, prefix = "") {
1385
+ if (prefix.length > 0) yield new TextEncoder().encode(prefix);
1386
+ for await (const chunk of stream) yield typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
334
1387
  }
1388
+ /** Everything a payload printed into its pipes, up to the point they end: what a start that dies
1389
+ * before its channel says about itself. A stream cannot carry it — there is no connection yet. */
335
1390
  /**
336
1391
  * Serve one environment over stdin/stdout. Returns the exit code; the caller
337
1392
  * turns it into `process.exitCode`.
@@ -341,6 +1396,11 @@ async function runGateway(argv = process.argv.slice(2)) {
341
1396
  read: process.stdin,
342
1397
  write: process.stdout
343
1398
  });
1399
+ const client = json1Channel(wire);
1400
+ const output = new StreamStore(client, "even", {
1401
+ ids: lowBand("even"),
1402
+ attach: false
1403
+ });
344
1404
  let booted;
345
1405
  try {
346
1406
  booted = gatewayBoot(argv);
@@ -405,8 +1465,39 @@ async function runGateway(argv = process.argv.slice(2)) {
405
1465
  "pipe"
406
1466
  ] });
407
1467
  child = workerProc;
408
- forwardOutput(workerProc.stdout, wire, 1);
409
- forwardOutput(workerProc.stderr, wire, 2);
1468
+ /** What the payload printed before the client spoke: a start that dies that early has no connection
1469
+ * to announce a stream on, and its words are the only way to say why it went. Kept per stream, so
1470
+ * the output that comes after is still attributed to the right one. */
1471
+ const early = {
1472
+ stdout: "",
1473
+ stderr: ""
1474
+ };
1475
+ const keep = (name) => (chunk) => {
1476
+ early[name] = (early[name] + String(chunk)).slice(-4096);
1477
+ };
1478
+ const keepOut = keep("stdout");
1479
+ const keepErr = keep("stderr");
1480
+ workerProc.stdout?.on("data", keepOut);
1481
+ workerProc.stderr?.on("data", keepErr);
1482
+ /** Announce the payload's own output as the two streams a program's prints belong on, with whatever
1483
+ * it said before the connection could carry it replayed as the first chunk. */
1484
+ const offerOutput = () => {
1485
+ const sides = [{
1486
+ stream: workerProc.stdout,
1487
+ name: "stdout",
1488
+ keep: keepOut
1489
+ }, {
1490
+ stream: workerProc.stderr,
1491
+ name: "stderr",
1492
+ keep: keepErr
1493
+ }];
1494
+ for (const { stream, name, keep: drop } of sides) {
1495
+ if (!stream) continue;
1496
+ stream.off("data", drop);
1497
+ stream.pause();
1498
+ output.offer(chunksOf(stream, early[name]), name);
1499
+ }
1500
+ };
410
1501
  const exited = new Promise((settle) => {
411
1502
  workerProc.once("exit", (code) => settle(code ?? 0));
412
1503
  });
@@ -415,18 +1506,34 @@ async function runGateway(argv = process.argv.slice(2)) {
415
1506
  throw new Error(`the payload exited with code ${code} before it opened its channel`);
416
1507
  })]);
417
1508
  } catch (err) {
418
- await fail(wire, errorText(err));
1509
+ const said = `${early.stdout}${early.stderr}`.trim();
1510
+ await fail(wire, `${errorText(err)}${said ? `: ${said}` : ""}`);
419
1511
  await teardown();
420
1512
  abandon();
421
1513
  }
422
- const relay = fifo;
423
- relay.onMessage((line) => {
424
- wire.send(line).catch(() => {});
1514
+ const payload = json1Channel(fifo);
1515
+ const fromClient = channelThrough(client, (frame) => output.handle(frame));
1516
+ const forward = (send, frame) => {
1517
+ Promise.resolve(send(frame)).catch(() => {});
1518
+ };
1519
+ let offered = false;
1520
+ fromClient.onMessage((frame) => {
1521
+ if (!offered) {
1522
+ offered = true;
1523
+ offerOutput();
1524
+ }
1525
+ forward((next) => payload.send(next), frame);
1526
+ });
1527
+ fromClient.onSegment?.((head, bytes) => {
1528
+ payload.sendSegment?.(head, bytes);
1529
+ });
1530
+ payload.onMessage((frame) => {
1531
+ forward((next) => client.send(next), frame);
425
1532
  });
426
- wire.onMessage((line) => {
427
- relay.send(line).catch(() => {});
1533
+ payload.onSegment?.((head, bytes) => {
1534
+ client.sendSegment?.(head, bytes);
428
1535
  });
429
- wire.onClose(() => {
1536
+ client.onClose(() => {
430
1537
  child?.kill();
431
1538
  });
432
1539
  const code = await exited;