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.
- package/dist/{hooks-Dpbe39oa.d.ts → hooks-DO-efRM_.d.ts} +2 -2
- package/dist/{hooks-Dpbe39oa.d.ts.map → hooks-DO-efRM_.d.ts.map} +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/plugins/debug-logger.d.ts +2 -2
- package/dist/plugins/tree-introspection.d.ts +2 -2
- package/dist/{process-CoEKoY5f.d.ts → process-eCAv2tIS.d.ts} +2 -2
- package/dist/{process-CoEKoY5f.d.ts.map → process-eCAv2tIS.d.ts.map} +1 -1
- package/dist/remote/gateway-cli.js +1157 -50
- package/dist/remote/gateway-cli.js.map +1 -1
- package/dist/remote/index.d.ts +4 -232
- package/dist/remote/index.d.ts.map +1 -1
- package/dist/remote/index.js +2 -2
- package/dist/remote/index.js.map +1 -1
- package/dist/remote/node.d.ts +29 -54
- package/dist/remote/node.d.ts.map +1 -1
- package/dist/remote/node.js +128 -128
- package/dist/remote/node.js.map +1 -1
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-CuabnerI.d.ts → types-U3NvKgpg.d.ts} +10 -1
- package/dist/{types-CuabnerI.d.ts.map → types-U3NvKgpg.d.ts.map} +1 -1
- package/dist/{version-B6eNJRZD.js → version-yonmO5bG.js} +2 -2
- package/dist/{version-B6eNJRZD.js.map → version-yonmO5bG.js.map} +1 -1
- package/dist/websocket-DT8zKD34.d.ts +836 -0
- package/dist/websocket-DT8zKD34.d.ts.map +1 -0
- package/dist/{websocket-DecgMfb3.js → websocket-DkDAHMFm.js} +1102 -85
- package/dist/websocket-DkDAHMFm.js.map +1 -0
- package/dist/xfetch.d.ts +2 -2
- package/package.json +1 -1
- package/dist/websocket-DecgMfb3.js.map +0 -1
- package/dist/websocket-sGQcEDbm.d.ts +0 -310
- 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
|
-
|
|
7
|
-
|
|
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/
|
|
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
|
-
*
|
|
992
|
+
* The wire as a sequence of lines, where a line may announce raw octets to follow it.
|
|
30
993
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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(
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
1211
|
+
this.reader = null;
|
|
167
1212
|
}
|
|
168
1213
|
if (this.writeFd) {
|
|
169
|
-
this.ws = this.writeFd.createWriteStream({
|
|
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.
|
|
258
|
-
this.
|
|
259
|
-
this.
|
|
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
|
-
/**
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
const
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
1533
|
+
payload.onSegment?.((head, bytes) => {
|
|
1534
|
+
client.sendSegment?.(head, bytes);
|
|
428
1535
|
});
|
|
429
|
-
|
|
1536
|
+
client.onClose(() => {
|
|
430
1537
|
child?.kill();
|
|
431
1538
|
});
|
|
432
1539
|
const code = await exited;
|