@statewalker/webrun-streams 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -152
- package/dist/duplex.d.ts +1 -1
- package/dist/flow-control.d.ts +7 -4
- package/dist/flow-control.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -401
- package/package.json +2 -2
- package/src/duplex.ts +1 -1
- package/src/flow-control.ts +7 -4
- package/src/index.ts +0 -1
- package/src/normalize.ts +1 -1
- package/dist/emulate-mux.d.ts +0 -39
- package/dist/emulate-mux.d.ts.map +0 -1
- package/dist/uint32.d.ts +0 -25
- package/dist/uint32.d.ts.map +0 -1
- package/src/emulate-mux.ts +0 -557
- package/src/uint32.ts +0 -34
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ async function collectString(input) {
|
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/duplex.ts
|
|
33
33
|
/**
|
|
34
|
-
* Thrown by
|
|
34
|
+
* Thrown by adapters when the underlying transport closes
|
|
35
35
|
* while one or more `Duplex` calls are in flight. Consumers can catch by
|
|
36
36
|
* `instanceof TransportClosedError` or by checking `error.name`.
|
|
37
37
|
*/
|
|
@@ -144,404 +144,6 @@ function newCreditGrantor(window, threshold = .5) {
|
|
|
144
144
|
} };
|
|
145
145
|
}
|
|
146
146
|
//#endregion
|
|
147
|
-
//#region src/uint32.ts
|
|
148
|
-
/**
|
|
149
|
-
* Credit payloads are a single big-endian uint32, matching the frame header's
|
|
150
|
-
* byte order.
|
|
151
|
-
*
|
|
152
|
-
* Deliberately **not** re-exported from `index.ts`: this is an internal codec
|
|
153
|
-
* for the `emulateMux` wire format, not a compatibility commitment. Tests
|
|
154
|
-
* import it by path.
|
|
155
|
-
*/
|
|
156
|
-
/** The largest credit a single frame can advertise or grant: 2^32 - 1. */
|
|
157
|
-
const MAX_UINT32 = 4294967295;
|
|
158
|
-
/**
|
|
159
|
-
* Clamps rather than wraps. `n >>> 0` is the obvious spelling and it is wrong
|
|
160
|
-
* here: 2^32 becomes **0**, so a 4 GiB window advertises *zero credit* and the
|
|
161
|
-
* peer stalls forever with no error — the exact silent hang credit exists to
|
|
162
|
-
* remove. 2^32 + 5 becomes 5, which looks like a working window and is worse.
|
|
163
|
-
* Anything above the ceiling is advertised as the ceiling.
|
|
164
|
-
*/
|
|
165
|
-
function encodeUint32(n) {
|
|
166
|
-
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
167
|
-
const clamped = Number.isFinite(n) ? Math.min(MAX_UINT32, Math.max(0, Math.floor(n))) : 0;
|
|
168
|
-
new DataView(bytes.buffer).setUint32(0, clamped, false);
|
|
169
|
-
return bytes;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Returns `undefined` rather than a garbage number when the payload is too
|
|
173
|
-
* short, so a truncated frame — or one from a peer predating credit — is
|
|
174
|
-
* detectable at the call site instead of silently granting nonsense.
|
|
175
|
-
*/
|
|
176
|
-
function decodeUint32(bytes) {
|
|
177
|
-
if (bytes.byteLength < 4) return void 0;
|
|
178
|
-
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, false);
|
|
179
|
-
}
|
|
180
|
-
//#endregion
|
|
181
|
-
//#region src/emulate-mux.ts
|
|
182
|
-
const TYPE_OPEN = 1;
|
|
183
|
-
const TYPE_DATA = 2;
|
|
184
|
-
const TYPE_ACK = 3;
|
|
185
|
-
const TYPE_END = 4;
|
|
186
|
-
const TYPE_ERROR = 5;
|
|
187
|
-
const TYPE_CLOSE = 6;
|
|
188
|
-
const DEFAULT_MAX_STREAMS = 256;
|
|
189
|
-
const DEFAULT_MTU = 65536;
|
|
190
|
-
const DEFAULT_MAX_STREAM_BUFFER = 8388608;
|
|
191
|
-
const textEncoder = new TextEncoder();
|
|
192
|
-
const textDecoder = new TextDecoder();
|
|
193
|
-
function emulateMux(channel, opts = {}) {
|
|
194
|
-
const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
|
|
195
|
-
const mtu = opts.mtu ?? DEFAULT_MTU;
|
|
196
|
-
const maxStreamBuffer = opts.maxStreamBuffer ?? DEFAULT_MAX_STREAM_BUFFER;
|
|
197
|
-
if (!(maxStreamBuffer >= 1)) throw new RangeError(`emulateMux: maxStreamBuffer must be at least 1, got ${maxStreamBuffer}`);
|
|
198
|
-
const side = opts.side ?? "initiator";
|
|
199
|
-
const streams = /* @__PURE__ */ new Map();
|
|
200
|
-
let nextLocalId = side === "initiator" ? 2 : 1;
|
|
201
|
-
let handler = null;
|
|
202
|
-
let muxClosed = false;
|
|
203
|
-
const sendFrame = (id, type, payload) => {
|
|
204
|
-
if (muxClosed) return;
|
|
205
|
-
const idEnc = encodeVarint(id);
|
|
206
|
-
const total = idEnc.length + 1 + (payload?.byteLength ?? 0);
|
|
207
|
-
const frame = new Uint8Array(total);
|
|
208
|
-
frame.set(idEnc, 0);
|
|
209
|
-
frame[idEnc.length] = type;
|
|
210
|
-
if (payload && payload.byteLength > 0) frame.set(payload, idEnc.length + 1);
|
|
211
|
-
try {
|
|
212
|
-
channel.send(frame);
|
|
213
|
-
} catch {}
|
|
214
|
-
};
|
|
215
|
-
const teardownStream = (s, err) => {
|
|
216
|
-
if (s.closed) return;
|
|
217
|
-
s.closed = true;
|
|
218
|
-
s.outboundCredit.fail(err ?? new TransportClosedError("emulateMux: stream closed"));
|
|
219
|
-
s.doneIn(err);
|
|
220
|
-
s.cancelInput?.();
|
|
221
|
-
streams.delete(s.id);
|
|
222
|
-
};
|
|
223
|
-
/**
|
|
224
|
-
* Graceful counterpart to `teardownStream`. A stream occupies a slot in
|
|
225
|
-
* `streams` until BOTH directions finish; releasing on inbound END alone
|
|
226
|
-
* would set `closed` while our own `pumpOutbound` is still sending, and it
|
|
227
|
-
* checks that flag to decide whether to keep going.
|
|
228
|
-
*
|
|
229
|
-
* Without this, only abnormal endings (cancel, ERROR, CLOSE, transport
|
|
230
|
-
* failure) ever freed a slot, so every normally-completed call leaked one
|
|
231
|
-
* until `maxStreams` began rejecting new calls.
|
|
232
|
-
*/
|
|
233
|
-
const releaseIfComplete = (s) => {
|
|
234
|
-
if (s.closed || !s.inDone || !s.outDone) return;
|
|
235
|
-
s.closed = true;
|
|
236
|
-
streams.delete(s.id);
|
|
237
|
-
};
|
|
238
|
-
const failAll = (err) => {
|
|
239
|
-
if (muxClosed) return;
|
|
240
|
-
muxClosed = true;
|
|
241
|
-
for (const s of [...streams.values()]) teardownStream(s, err);
|
|
242
|
-
streams.clear();
|
|
243
|
-
};
|
|
244
|
-
const createStream = (id) => {
|
|
245
|
-
const queue = makeInboundQueue();
|
|
246
|
-
const state = {
|
|
247
|
-
id,
|
|
248
|
-
inbound: queue.generator(() => {
|
|
249
|
-
if (!state.closed && !muxClosed) sendFrame(state.id, TYPE_CLOSE);
|
|
250
|
-
teardownStream(state);
|
|
251
|
-
}),
|
|
252
|
-
pushIn: queue.push,
|
|
253
|
-
doneIn: queue.done,
|
|
254
|
-
outboundCredit: newCreditLedger(0),
|
|
255
|
-
grantor: newCreditGrantor(maxStreamBuffer),
|
|
256
|
-
closed: false,
|
|
257
|
-
inDone: false,
|
|
258
|
-
outDone: false,
|
|
259
|
-
queuedBytes: 0
|
|
260
|
-
};
|
|
261
|
-
return state;
|
|
262
|
-
};
|
|
263
|
-
const pumpOutbound = async (s, input) => {
|
|
264
|
-
const iterator = input[Symbol.asyncIterator]?.() ?? input[Symbol.iterator]();
|
|
265
|
-
s.cancelInput = () => {
|
|
266
|
-
iterator.return?.(void 0);
|
|
267
|
-
};
|
|
268
|
-
try {
|
|
269
|
-
for (;;) {
|
|
270
|
-
const next = await iterator.next();
|
|
271
|
-
if (next.done === true) break;
|
|
272
|
-
const chunk = next.value;
|
|
273
|
-
if (s.closed || muxClosed) return;
|
|
274
|
-
if (chunk.byteLength === 0) continue;
|
|
275
|
-
let off = 0;
|
|
276
|
-
while (off < chunk.byteLength) {
|
|
277
|
-
if (s.closed || muxClosed) return;
|
|
278
|
-
const take = await s.outboundCredit.reserve(Math.min(mtu, chunk.byteLength - off));
|
|
279
|
-
if (s.closed || muxClosed) return;
|
|
280
|
-
const end = off + take;
|
|
281
|
-
sendFrame(s.id, TYPE_DATA, chunk.subarray(off, end));
|
|
282
|
-
off = end;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (!s.closed && !muxClosed) {
|
|
286
|
-
sendFrame(s.id, TYPE_END);
|
|
287
|
-
s.outDone = true;
|
|
288
|
-
releaseIfComplete(s);
|
|
289
|
-
}
|
|
290
|
-
} catch (err) {
|
|
291
|
-
if (!s.closed && !muxClosed) {
|
|
292
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
293
|
-
sendFrame(s.id, TYPE_ERROR, encodeError(e));
|
|
294
|
-
teardownStream(s, e);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
const runHandler = async (s, h) => {
|
|
299
|
-
let outbound;
|
|
300
|
-
try {
|
|
301
|
-
outbound = h(s.inbound);
|
|
302
|
-
} catch (err) {
|
|
303
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
304
|
-
sendFrame(s.id, TYPE_ERROR, encodeError(e));
|
|
305
|
-
teardownStream(s, e);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
await pumpOutbound(s, outbound);
|
|
309
|
-
if (!s.inDone && !s.closed && !muxClosed) {
|
|
310
|
-
sendFrame(s.id, TYPE_CLOSE);
|
|
311
|
-
teardownStream(s);
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
const handleFrame = (frame) => {
|
|
315
|
-
if (muxClosed) return;
|
|
316
|
-
if (frame.byteLength < 2) return;
|
|
317
|
-
let id;
|
|
318
|
-
let offset;
|
|
319
|
-
try {
|
|
320
|
-
({value: id, offset} = decodeVarint(frame, 0));
|
|
321
|
-
} catch {
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (offset >= frame.byteLength) return;
|
|
325
|
-
const type = frame[offset];
|
|
326
|
-
const payload = frame.subarray(offset + 1);
|
|
327
|
-
if (type === TYPE_OPEN) {
|
|
328
|
-
if (streams.has(id)) return;
|
|
329
|
-
if (streams.size >= maxStreams) {
|
|
330
|
-
sendFrame(id, TYPE_ERROR, encodeError(/* @__PURE__ */ new RangeError(`emulateMux: maxStreams=${maxStreams} exceeded`)));
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (!handler) {
|
|
334
|
-
sendFrame(id, TYPE_ERROR, encodeError(/* @__PURE__ */ new Error("emulateMux: no handler registered")));
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
const s = createStream(id);
|
|
338
|
-
streams.set(id, s);
|
|
339
|
-
const advertised = decodeUint32(payload);
|
|
340
|
-
if (advertised !== void 0) s.outboundCredit.grant(advertised);
|
|
341
|
-
sendFrame(id, TYPE_ACK, encodeUint32(maxStreamBuffer));
|
|
342
|
-
runHandler(s, handler);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const s = streams.get(id);
|
|
346
|
-
if (!s) return;
|
|
347
|
-
switch (type) {
|
|
348
|
-
case TYPE_DATA: {
|
|
349
|
-
s.queuedBytes += payload.byteLength;
|
|
350
|
-
if (s.queuedBytes > maxStreamBuffer) {
|
|
351
|
-
const err = /* @__PURE__ */ new RangeError(`emulateMux: stream ${id} buffered ${s.queuedBytes} bytes past maxStreamBuffer=${maxStreamBuffer} without being drained`);
|
|
352
|
-
sendFrame(id, TYPE_ERROR, encodeError(err));
|
|
353
|
-
teardownStream(s, err);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
|
|
357
|
-
s.pushIn(copy).then((handled) => {
|
|
358
|
-
s.queuedBytes -= copy.byteLength;
|
|
359
|
-
if (!handled || s.closed || muxClosed) return;
|
|
360
|
-
const grant = s.grantor.consumed(copy.byteLength, s.queuedBytes === 0);
|
|
361
|
-
if (grant > 0) sendFrame(id, TYPE_ACK, encodeUint32(grant));
|
|
362
|
-
});
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
case TYPE_ACK: {
|
|
366
|
-
const granted = decodeUint32(payload);
|
|
367
|
-
if (granted !== void 0) s.outboundCredit.grant(granted);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
case TYPE_END:
|
|
371
|
-
s.inDone = true;
|
|
372
|
-
s.doneIn();
|
|
373
|
-
releaseIfComplete(s);
|
|
374
|
-
return;
|
|
375
|
-
case TYPE_ERROR:
|
|
376
|
-
teardownStream(s, decodeError(payload));
|
|
377
|
-
return;
|
|
378
|
-
case TYPE_CLOSE:
|
|
379
|
-
teardownStream(s);
|
|
380
|
-
return;
|
|
381
|
-
default: return;
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
(async () => {
|
|
385
|
-
try {
|
|
386
|
-
for await (const frame of channel.recv) {
|
|
387
|
-
if (muxClosed) break;
|
|
388
|
-
handleFrame(frame);
|
|
389
|
-
}
|
|
390
|
-
} catch (err) {
|
|
391
|
-
failAll(err instanceof Error ? err : new Error(String(err)));
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
failAll(new TransportClosedError());
|
|
395
|
-
})();
|
|
396
|
-
channel.closed.then(() => failAll(new TransportClosedError())).catch(() => {});
|
|
397
|
-
const call = (input) => {
|
|
398
|
-
if (muxClosed) return failedGenerator(new TransportClosedError());
|
|
399
|
-
if (streams.size >= maxStreams) return failedGenerator(/* @__PURE__ */ new RangeError(`emulateMux: maxStreams=${maxStreams} exceeded`));
|
|
400
|
-
const id = nextLocalId;
|
|
401
|
-
nextLocalId += 2;
|
|
402
|
-
const s = createStream(id);
|
|
403
|
-
streams.set(id, s);
|
|
404
|
-
sendFrame(id, TYPE_OPEN, encodeUint32(maxStreamBuffer));
|
|
405
|
-
pumpOutbound(s, input);
|
|
406
|
-
return s.inbound;
|
|
407
|
-
};
|
|
408
|
-
const serve = (h) => {
|
|
409
|
-
handler = h;
|
|
410
|
-
let torn = false;
|
|
411
|
-
return async () => {
|
|
412
|
-
if (torn) return;
|
|
413
|
-
torn = true;
|
|
414
|
-
if (handler === h) handler = null;
|
|
415
|
-
};
|
|
416
|
-
};
|
|
417
|
-
const close = async () => {
|
|
418
|
-
if (muxClosed) return;
|
|
419
|
-
failAll(new TransportClosedError());
|
|
420
|
-
try {
|
|
421
|
-
channel.close();
|
|
422
|
-
} catch {}
|
|
423
|
-
};
|
|
424
|
-
return {
|
|
425
|
-
call,
|
|
426
|
-
serve,
|
|
427
|
-
close
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
function failedGenerator(err) {
|
|
431
|
-
return (async function* () {
|
|
432
|
-
throw err;
|
|
433
|
-
})();
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Push/pull queue with eager push/done handles. Unlike `newAsyncGenerator`, the
|
|
437
|
-
* push and done functions are usable *before* the consumer begins iterating —
|
|
438
|
-
* `emulateMux` needs to enqueue frames from inbound traffic regardless of when
|
|
439
|
-
* (or whether) the consumer pulls them.
|
|
440
|
-
*
|
|
441
|
-
* `onCancel` fires only when the consumer terminates the generator before
|
|
442
|
-
* `done()` was called — i.e., a unilateral cancellation. If `done()` is
|
|
443
|
-
* called first (peer END/ERROR/CLOSE), the generator ends naturally and
|
|
444
|
-
* `onCancel` does not fire.
|
|
445
|
-
*/
|
|
446
|
-
function makeInboundQueue() {
|
|
447
|
-
const slots = [];
|
|
448
|
-
let wake = null;
|
|
449
|
-
let queueClosed = false;
|
|
450
|
-
let doneCalled = false;
|
|
451
|
-
const push = (chunk) => {
|
|
452
|
-
if (queueClosed) return Promise.resolve(false);
|
|
453
|
-
return new Promise((resolve) => {
|
|
454
|
-
slots.push({
|
|
455
|
-
type: "value",
|
|
456
|
-
value: chunk,
|
|
457
|
-
resolve
|
|
458
|
-
});
|
|
459
|
-
wake?.();
|
|
460
|
-
});
|
|
461
|
-
};
|
|
462
|
-
const done = (err) => {
|
|
463
|
-
if (queueClosed || doneCalled) return Promise.resolve(false);
|
|
464
|
-
doneCalled = true;
|
|
465
|
-
return new Promise((resolve) => {
|
|
466
|
-
slots.push({
|
|
467
|
-
type: "done",
|
|
468
|
-
err,
|
|
469
|
-
resolve
|
|
470
|
-
});
|
|
471
|
-
wake?.();
|
|
472
|
-
});
|
|
473
|
-
};
|
|
474
|
-
async function* generator(onCancel) {
|
|
475
|
-
try {
|
|
476
|
-
while (true) {
|
|
477
|
-
if (slots.length === 0) {
|
|
478
|
-
await new Promise((r) => {
|
|
479
|
-
wake = r;
|
|
480
|
-
});
|
|
481
|
-
wake = null;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
const slot = slots.shift();
|
|
485
|
-
if (slot.type === "done") {
|
|
486
|
-
slot.resolve(true);
|
|
487
|
-
if (slot.err) throw slot.err;
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
yield slot.value;
|
|
491
|
-
slot.resolve(true);
|
|
492
|
-
}
|
|
493
|
-
} finally {
|
|
494
|
-
queueClosed = true;
|
|
495
|
-
for (const s of slots) s.resolve(false);
|
|
496
|
-
slots.length = 0;
|
|
497
|
-
if (!doneCalled) onCancel();
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return {
|
|
501
|
-
generator,
|
|
502
|
-
push,
|
|
503
|
-
done
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function encodeVarint(value) {
|
|
507
|
-
if (!Number.isInteger(value) || value < 0) throw new RangeError(`encodeVarint: ${value} is not a non-negative integer`);
|
|
508
|
-
const out = [];
|
|
509
|
-
let v = value;
|
|
510
|
-
while (v >= 128) {
|
|
511
|
-
out.push(v & 127 | 128);
|
|
512
|
-
v >>>= 7;
|
|
513
|
-
}
|
|
514
|
-
out.push(v & 127);
|
|
515
|
-
return new Uint8Array(out);
|
|
516
|
-
}
|
|
517
|
-
function decodeVarint(buf, start) {
|
|
518
|
-
let value = 0;
|
|
519
|
-
let shift = 0;
|
|
520
|
-
let i = start;
|
|
521
|
-
while (i < buf.length) {
|
|
522
|
-
const b = buf[i++];
|
|
523
|
-
value |= (b & 127) << shift;
|
|
524
|
-
if ((b & 128) === 0) return {
|
|
525
|
-
value: value >>> 0,
|
|
526
|
-
offset: i
|
|
527
|
-
};
|
|
528
|
-
shift += 7;
|
|
529
|
-
if (shift > 28) throw new Error("decodeVarint: too long");
|
|
530
|
-
}
|
|
531
|
-
throw new Error("decodeVarint: truncated");
|
|
532
|
-
}
|
|
533
|
-
function encodeError(err) {
|
|
534
|
-
return textEncoder.encode(JSON.stringify(serializeError(err)));
|
|
535
|
-
}
|
|
536
|
-
function decodeError(buf) {
|
|
537
|
-
if (buf.byteLength === 0) return /* @__PURE__ */ new Error("unknown stream error");
|
|
538
|
-
try {
|
|
539
|
-
return deserializeError(JSON.parse(textDecoder.decode(buf)));
|
|
540
|
-
} catch {
|
|
541
|
-
return new Error(textDecoder.decode(buf));
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
//#endregion
|
|
545
147
|
//#region src/lines.ts
|
|
546
148
|
/** Split a stream of string chunks into individual lines (delimited by \n). */
|
|
547
149
|
async function* splitLines(input) {
|
|
@@ -736,7 +338,7 @@ async function* newAsyncGenerator(init, skipValues = false) {
|
|
|
736
338
|
}
|
|
737
339
|
//#endregion
|
|
738
340
|
//#region src/normalize.ts
|
|
739
|
-
const utf8 = new TextEncoder();
|
|
341
|
+
const utf8 = /* @__PURE__ */ new TextEncoder();
|
|
740
342
|
/**
|
|
741
343
|
* Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
|
|
742
344
|
* Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.
|
|
@@ -914,4 +516,4 @@ function toChunks(size = DEFAULT_CHUNK_SIZE) {
|
|
|
914
516
|
};
|
|
915
517
|
}
|
|
916
518
|
//#endregion
|
|
917
|
-
export { TransportClosedError, collect, collectBytes, collectString, decodeJsonl, decodeText, deserializeError,
|
|
519
|
+
export { TransportClosedError, collect, collectBytes, collectString, decodeJsonl, decodeText, deserializeError, encodeJsonl, encodeText, fromReadableStream, joinLines, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, normalizeToUint8Array, recieveIterator, sendIterator, serializeError, splitLines, toChunks, toReadableStream };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-streams",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Async-iterator / ReadableStream primitives: collect, text/jsonl codecs, lines, backpressure generators, serialisable errors.",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"rimraf": "^6.1.3",
|
|
36
36
|
"rolldown": "^1.2.12",
|
|
37
37
|
"typescript": "^7.0.2",
|
|
38
|
-
"vitest": "^5.0.
|
|
38
|
+
"vitest": "^5.0.5"
|
|
39
39
|
},
|
|
40
40
|
"sideEffects": false,
|
|
41
41
|
"publishConfig": {
|
package/src/duplex.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type Connect<P> = (params: P) => Promise<{
|
|
|
47
47
|
export type Serve<P> = (params: P, handler: Duplex) => Promise<() => Promise<void>>;
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Thrown by
|
|
50
|
+
* Thrown by adapters when the underlying transport closes
|
|
51
51
|
* while one or more `Duplex` calls are in flight. Consumers can catch by
|
|
52
52
|
* `instanceof TransportClosedError` or by checking `error.name`.
|
|
53
53
|
*/
|
package/src/flow-control.ts
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* offer: the receiver's capacity is not knowable to the sender unless the
|
|
12
12
|
* receiver states it.
|
|
13
13
|
*
|
|
14
|
-
* **The unit is opaque.** This module never interprets the numbers it counts
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* **The unit is opaque.** This module never interprets the numbers it counts:
|
|
15
|
+
* bytes or values, it does not depend on which.
|
|
16
|
+
*
|
|
17
|
+
* **Dormant.** Its consumer, `emulateMux`, was removed in Plan C3, and
|
|
18
|
+
* `webrun-rpc`'s port stack keeps its own window. It stays exported and
|
|
19
|
+
* tested as the mechanism for per-stream windowing (spec D13) rather than be
|
|
20
|
+
* rewritten later.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
export interface CreditLedger {
|
package/src/index.ts
CHANGED
package/src/normalize.ts
CHANGED
package/dist/emulate-mux.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type Duplex } from "./duplex.js";
|
|
2
|
-
/**
|
|
3
|
-
* The minimum a transport must expose to be wrapped by `emulateMux`. Inbound
|
|
4
|
-
* bytes are surfaced as an async iterable; outbound is imperative `send`. All
|
|
5
|
-
* five message-oriented transports (WebSocket, LiveKit data channel, PeerJS
|
|
6
|
-
* DataConnection, MessagePort, in-process pipe) match this shape after a thin
|
|
7
|
-
* wrapper.
|
|
8
|
-
*/
|
|
9
|
-
export type ByteChannel = {
|
|
10
|
-
send(bytes: Uint8Array): void;
|
|
11
|
-
recv: AsyncIterable<Uint8Array>;
|
|
12
|
-
closed: Promise<void>;
|
|
13
|
-
close(): void;
|
|
14
|
-
};
|
|
15
|
-
export interface EmulateMuxOptions {
|
|
16
|
-
maxStreams?: number;
|
|
17
|
-
mtu?: number;
|
|
18
|
-
/**
|
|
19
|
-
* Cap on inbound bytes one stream may hold for a consumer that has not
|
|
20
|
-
* drained them. Exceeding it tears down that stream alone.
|
|
21
|
-
*
|
|
22
|
-
* It is also the credit this side advertises to the peer, so it must be at
|
|
23
|
-
* least 1 — a window of 0 authorises nothing and hangs the peer forever —
|
|
24
|
-
* and it travels in a uint32, so a value above `MAX_UINT32` (4 GiB - 1) is
|
|
25
|
-
* advertised as `MAX_UINT32`.
|
|
26
|
-
*/
|
|
27
|
-
maxStreamBuffer?: number;
|
|
28
|
-
/**
|
|
29
|
-
* Stream-id allocation side. Initiator uses even ids (2, 4, …); responder
|
|
30
|
-
* uses odd ids (1, 3, …). Pick one per peer so allocations don't collide.
|
|
31
|
-
*/
|
|
32
|
-
side?: "initiator" | "responder";
|
|
33
|
-
}
|
|
34
|
-
export declare function emulateMux(channel: ByteChannel, opts?: EmulateMuxOptions): {
|
|
35
|
-
call: Duplex;
|
|
36
|
-
serve: (handler: Duplex) => () => Promise<void>;
|
|
37
|
-
close: () => Promise<void>;
|
|
38
|
-
};
|
|
39
|
-
//# sourceMappingURL=emulate-mux.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"emulate-mux.d.ts","sourceRoot":"","sources":["../src/emulate-mux.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAwB,MAAM,aAAa,CAAC;AAUhE;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AA4CF,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;CAClC;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,WAAW,EACpB,IAAI,GAAE,iBAAsB,GAC3B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAuVA"}
|
package/dist/uint32.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credit payloads are a single big-endian uint32, matching the frame header's
|
|
3
|
-
* byte order.
|
|
4
|
-
*
|
|
5
|
-
* Deliberately **not** re-exported from `index.ts`: this is an internal codec
|
|
6
|
-
* for the `emulateMux` wire format, not a compatibility commitment. Tests
|
|
7
|
-
* import it by path.
|
|
8
|
-
*/
|
|
9
|
-
/** The largest credit a single frame can advertise or grant: 2^32 - 1. */
|
|
10
|
-
export declare const MAX_UINT32 = 4294967295;
|
|
11
|
-
/**
|
|
12
|
-
* Clamps rather than wraps. `n >>> 0` is the obvious spelling and it is wrong
|
|
13
|
-
* here: 2^32 becomes **0**, so a 4 GiB window advertises *zero credit* and the
|
|
14
|
-
* peer stalls forever with no error — the exact silent hang credit exists to
|
|
15
|
-
* remove. 2^32 + 5 becomes 5, which looks like a working window and is worse.
|
|
16
|
-
* Anything above the ceiling is advertised as the ceiling.
|
|
17
|
-
*/
|
|
18
|
-
export declare function encodeUint32(n: number): Uint8Array;
|
|
19
|
-
/**
|
|
20
|
-
* Returns `undefined` rather than a garbage number when the payload is too
|
|
21
|
-
* short, so a truncated frame — or one from a peer predating credit — is
|
|
22
|
-
* detectable at the call site instead of silently granting nonsense.
|
|
23
|
-
*/
|
|
24
|
-
export declare function decodeUint32(bytes: Uint8Array): number | undefined;
|
|
25
|
-
//# sourceMappingURL=uint32.d.ts.map
|
package/dist/uint32.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"uint32.d.ts","sourceRoot":"","sources":["../src/uint32.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,0EAA0E;AAC1E,eAAO,MAAM,UAAU,aAAa,CAAC;AAErC;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAKlD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGlE"}
|