@statewalker/webrun-streams 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -9
- package/dist/collect.d.ts +7 -0
- package/dist/collect.d.ts.map +1 -0
- package/dist/duplex.d.ts +52 -0
- package/dist/duplex.d.ts.map +1 -0
- package/dist/emulate-mux.d.ts +39 -0
- package/dist/emulate-mux.d.ts.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/flow-control.d.ts +71 -0
- package/dist/flow-control.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/{index.mjs → index.js} +200 -52
- package/dist/jsonl.d.ts +5 -0
- package/dist/jsonl.d.ts.map +1 -0
- package/dist/lines.d.ts +5 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/map.d.ts +3 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/new-async-generator.d.ts +70 -0
- package/dist/new-async-generator.d.ts.map +1 -0
- package/dist/normalize.d.ts +8 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/readable-streams.d.ts +13 -0
- package/dist/readable-streams.d.ts.map +1 -0
- package/dist/recieve-iterator.d.ts +14 -0
- package/dist/recieve-iterator.d.ts.map +1 -0
- package/dist/send-iterator.d.ts +15 -0
- package/dist/send-iterator.d.ts.map +1 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/to-chunks.d.ts +11 -0
- package/dist/to-chunks.d.ts.map +1 -0
- package/dist/uint32.d.ts +25 -0
- package/dist/uint32.d.ts.map +1 -0
- package/package.json +14 -7
- package/src/duplex.ts +59 -0
- package/src/emulate-mux.ts +100 -85
- package/src/flow-control.ts +146 -0
- package/src/index.ts +2 -0
- package/src/readable-streams.ts +49 -13
- package/src/uint32.ts +34 -0
- package/dist/index.d.mts +0 -244
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -29,6 +29,19 @@ async function collectString(input) {
|
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
|
+
//#region src/duplex.ts
|
|
33
|
+
/**
|
|
34
|
+
* Thrown by `emulateMux` and adapters when the underlying transport closes
|
|
35
|
+
* while one or more `Duplex` calls are in flight. Consumers can catch by
|
|
36
|
+
* `instanceof TransportClosedError` or by checking `error.name`.
|
|
37
|
+
*/
|
|
38
|
+
var TransportClosedError = class extends Error {
|
|
39
|
+
name = "TransportClosedError";
|
|
40
|
+
constructor(message = "transport closed") {
|
|
41
|
+
super(message);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
32
45
|
//#region src/errors.ts
|
|
33
46
|
function serializeError(error) {
|
|
34
47
|
if (error instanceof Error) {
|
|
@@ -54,18 +67,118 @@ function deserializeError(error) {
|
|
|
54
67
|
return Object.assign(new Error(payload.message), payload);
|
|
55
68
|
}
|
|
56
69
|
//#endregion
|
|
57
|
-
//#region src/
|
|
70
|
+
//#region src/flow-control.ts
|
|
71
|
+
function newCreditLedger(initial = 0) {
|
|
72
|
+
let available = initial;
|
|
73
|
+
let failure;
|
|
74
|
+
const waiters = [];
|
|
75
|
+
const pump = () => {
|
|
76
|
+
while (waiters.length > 0 && available > 0) {
|
|
77
|
+
const next = waiters[0];
|
|
78
|
+
if (!next) return;
|
|
79
|
+
waiters.shift();
|
|
80
|
+
const granted = Math.min(next.upTo, available);
|
|
81
|
+
available -= granted;
|
|
82
|
+
next.resolve(granted);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
get available() {
|
|
87
|
+
return available;
|
|
88
|
+
},
|
|
89
|
+
reserve(upTo) {
|
|
90
|
+
if (failure) return Promise.reject(failure);
|
|
91
|
+
if (!(upTo >= 1)) return Promise.reject(/* @__PURE__ */ new RangeError(`newCreditLedger: reserve(${upTo}) — upTo must be at least 1`));
|
|
92
|
+
if (waiters.length === 0 && available > 0) {
|
|
93
|
+
const granted = Math.min(upTo, available);
|
|
94
|
+
available -= granted;
|
|
95
|
+
return Promise.resolve(granted);
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
waiters.push({
|
|
99
|
+
upTo,
|
|
100
|
+
resolve,
|
|
101
|
+
reject
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
grant(units) {
|
|
106
|
+
if (failure) return;
|
|
107
|
+
available += units;
|
|
108
|
+
pump();
|
|
109
|
+
},
|
|
110
|
+
fail(err) {
|
|
111
|
+
failure ??= err;
|
|
112
|
+
while (waiters.length > 0) waiters.shift()?.reject(err);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
58
116
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
117
|
+
* Grants are batched: replenishing on every chunk while the receiver is
|
|
118
|
+
* behind would reinvent the per-frame ACK this change exists to remove.
|
|
119
|
+
* `threshold` is the fraction of the window that must drain before a grant is
|
|
120
|
+
* emitted.
|
|
121
|
+
*
|
|
122
|
+
* The batch is flushed unconditionally once the receive queue is empty, even
|
|
123
|
+
* below the threshold, so the receiver never sits on credit it owes. Note what
|
|
124
|
+
* this is and is not: paired with a {@link CreditLedger}, a sender blocks only
|
|
125
|
+
* at *exactly* zero credit, and at that point the receiver holds the entire
|
|
126
|
+
* window as `pending` — above any threshold at or below the whole window — so
|
|
127
|
+
* the threshold alone cannot deadlock that pairing. The flush is what keeps
|
|
128
|
+
* that from being an argument about a global accounting identity: it is
|
|
129
|
+
* locally decidable from one boolean, and it returns owed credit now rather
|
|
130
|
+
* than at the next threshold crossing. It costs an extra frame only when the
|
|
131
|
+
* consumer is keeping pace, which is exactly when the sender is not blocked
|
|
132
|
+
* and the frame is cheap.
|
|
62
133
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
134
|
+
function newCreditGrantor(window, threshold = .5) {
|
|
135
|
+
const trigger = Math.max(1, Math.floor(window * threshold));
|
|
136
|
+
let pending = 0;
|
|
137
|
+
return { consumed(units, queueEmpty) {
|
|
138
|
+
pending += units;
|
|
139
|
+
if (pending === 0) return 0;
|
|
140
|
+
if (pending < trigger && !queueEmpty) return 0;
|
|
141
|
+
const grant = pending;
|
|
142
|
+
pending = 0;
|
|
143
|
+
return grant;
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
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
|
|
69
182
|
const TYPE_OPEN = 1;
|
|
70
183
|
const TYPE_DATA = 2;
|
|
71
184
|
const TYPE_ACK = 3;
|
|
@@ -81,6 +194,7 @@ function emulateMux(channel, opts = {}) {
|
|
|
81
194
|
const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
|
|
82
195
|
const mtu = opts.mtu ?? DEFAULT_MTU;
|
|
83
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}`);
|
|
84
198
|
const side = opts.side ?? "initiator";
|
|
85
199
|
const streams = /* @__PURE__ */ new Map();
|
|
86
200
|
let nextLocalId = side === "initiator" ? 2 : 1;
|
|
@@ -101,13 +215,9 @@ function emulateMux(channel, opts = {}) {
|
|
|
101
215
|
const teardownStream = (s, err) => {
|
|
102
216
|
if (s.closed) return;
|
|
103
217
|
s.closed = true;
|
|
104
|
-
|
|
105
|
-
const reject = s.rejectAck;
|
|
106
|
-
s.resolveAck = null;
|
|
107
|
-
s.rejectAck = null;
|
|
108
|
-
if (reject && err) reject(err);
|
|
109
|
-
else resolve?.();
|
|
218
|
+
s.outboundCredit.fail(err ?? new TransportClosedError("emulateMux: stream closed"));
|
|
110
219
|
s.doneIn(err);
|
|
220
|
+
s.cancelInput?.();
|
|
111
221
|
streams.delete(s.id);
|
|
112
222
|
};
|
|
113
223
|
/**
|
|
@@ -141,8 +251,8 @@ function emulateMux(channel, opts = {}) {
|
|
|
141
251
|
}),
|
|
142
252
|
pushIn: queue.push,
|
|
143
253
|
doneIn: queue.done,
|
|
144
|
-
|
|
145
|
-
|
|
254
|
+
outboundCredit: newCreditLedger(0),
|
|
255
|
+
grantor: newCreditGrantor(maxStreamBuffer),
|
|
146
256
|
closed: false,
|
|
147
257
|
inDone: false,
|
|
148
258
|
outDone: false,
|
|
@@ -151,20 +261,24 @@ function emulateMux(channel, opts = {}) {
|
|
|
151
261
|
return state;
|
|
152
262
|
};
|
|
153
263
|
const pumpOutbound = async (s, input) => {
|
|
264
|
+
const iterator = input[Symbol.asyncIterator]?.() ?? input[Symbol.iterator]();
|
|
265
|
+
s.cancelInput = () => {
|
|
266
|
+
iterator.return?.(void 0);
|
|
267
|
+
};
|
|
154
268
|
try {
|
|
155
|
-
for
|
|
269
|
+
for (;;) {
|
|
270
|
+
const next = await iterator.next();
|
|
271
|
+
if (next.done === true) break;
|
|
272
|
+
const chunk = next.value;
|
|
156
273
|
if (s.closed || muxClosed) return;
|
|
157
274
|
if (chunk.byteLength === 0) continue;
|
|
158
275
|
let off = 0;
|
|
159
276
|
while (off < chunk.byteLength) {
|
|
160
277
|
if (s.closed || muxClosed) return;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
s.resolveAck = resolve;
|
|
166
|
-
s.rejectAck = reject;
|
|
167
|
-
});
|
|
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));
|
|
168
282
|
off = end;
|
|
169
283
|
}
|
|
170
284
|
}
|
|
@@ -222,6 +336,9 @@ function emulateMux(channel, opts = {}) {
|
|
|
222
336
|
}
|
|
223
337
|
const s = createStream(id);
|
|
224
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));
|
|
225
342
|
runHandler(s, handler);
|
|
226
343
|
return;
|
|
227
344
|
}
|
|
@@ -239,15 +356,15 @@ function emulateMux(channel, opts = {}) {
|
|
|
239
356
|
const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
|
|
240
357
|
s.pushIn(copy).then((handled) => {
|
|
241
358
|
s.queuedBytes -= copy.byteLength;
|
|
242
|
-
if (handled
|
|
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));
|
|
243
362
|
});
|
|
244
363
|
return;
|
|
245
364
|
}
|
|
246
365
|
case TYPE_ACK: {
|
|
247
|
-
const
|
|
248
|
-
s.
|
|
249
|
-
s.rejectAck = null;
|
|
250
|
-
r?.();
|
|
366
|
+
const granted = decodeUint32(payload);
|
|
367
|
+
if (granted !== void 0) s.outboundCredit.grant(granted);
|
|
251
368
|
return;
|
|
252
369
|
}
|
|
253
370
|
case TYPE_END:
|
|
@@ -284,7 +401,7 @@ function emulateMux(channel, opts = {}) {
|
|
|
284
401
|
nextLocalId += 2;
|
|
285
402
|
const s = createStream(id);
|
|
286
403
|
streams.set(id, s);
|
|
287
|
-
sendFrame(id, TYPE_OPEN);
|
|
404
|
+
sendFrame(id, TYPE_OPEN, encodeUint32(maxStreamBuffer));
|
|
288
405
|
pumpOutbound(s, input);
|
|
289
406
|
return s.inbound;
|
|
290
407
|
};
|
|
@@ -641,30 +758,63 @@ function describe(value) {
|
|
|
641
758
|
}
|
|
642
759
|
//#endregion
|
|
643
760
|
//#region src/readable-streams.ts
|
|
761
|
+
/**
|
|
762
|
+
* The iterator ↔ `ReadableStream` boundary.
|
|
763
|
+
*
|
|
764
|
+
* Both adapters must carry CANCELLATION, not just data: a response body leaves
|
|
765
|
+
* a handler as a `ReadableStream`, crosses a transport as an iterator, and
|
|
766
|
+
* becomes a `ReadableStream` again at the caller — so when the caller walks
|
|
767
|
+
* away, the only path back to the handler's producer runs through both of
|
|
768
|
+
* these functions. Teardown that stops at an adapter leaves a producer running
|
|
769
|
+
* for ever.
|
|
770
|
+
*/
|
|
644
771
|
function toReadableStream(it) {
|
|
645
|
-
return new ReadableStream({
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
772
|
+
return new ReadableStream({
|
|
773
|
+
/**
|
|
774
|
+
* One chunk per pull. An earlier version drained the whole iterator inside
|
|
775
|
+
* a single `pull`, which defeated the stream's own backpressure (every
|
|
776
|
+
* chunk was enqueued as fast as the producer could make them, however slow
|
|
777
|
+
* the reader was) and left no point between chunks at which a cancellation
|
|
778
|
+
* could take effect.
|
|
779
|
+
*/
|
|
780
|
+
async pull(controller) {
|
|
781
|
+
try {
|
|
649
782
|
const slot = await it.next();
|
|
650
|
-
if (!slot || slot.done)
|
|
651
|
-
|
|
652
|
-
|
|
783
|
+
if (!slot || slot.done) {
|
|
784
|
+
controller.close();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
controller.enqueue(await slot.value);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
controller.error(error);
|
|
653
790
|
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
791
|
+
},
|
|
792
|
+
/**
|
|
793
|
+
* Release the source. NOT awaited: `.return()` on an async generator that
|
|
794
|
+
* is parked awaiting its own source is queued behind that pending
|
|
795
|
+
* `next()`, so awaiting it here would hang `reader.cancel()` on exactly
|
|
796
|
+
* the producers that most need cancelling.
|
|
797
|
+
*/
|
|
798
|
+
cancel(reason) {
|
|
799
|
+
Promise.resolve(it.return?.(reason)).catch(() => {});
|
|
659
800
|
}
|
|
660
|
-
}
|
|
801
|
+
});
|
|
661
802
|
}
|
|
662
803
|
async function* fromReadableStream(stream) {
|
|
663
804
|
const reader = stream.getReader();
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
805
|
+
let drained = false;
|
|
806
|
+
try {
|
|
807
|
+
while (true) {
|
|
808
|
+
const { done, value } = await reader.read();
|
|
809
|
+
if (done) {
|
|
810
|
+
drained = true;
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
if (value !== void 0) yield value;
|
|
814
|
+
}
|
|
815
|
+
} finally {
|
|
816
|
+
if (drained) reader.releaseLock();
|
|
817
|
+
else await reader.cancel().catch(() => {});
|
|
668
818
|
}
|
|
669
819
|
}
|
|
670
820
|
//#endregion
|
|
@@ -764,6 +914,4 @@ function toChunks(size = DEFAULT_CHUNK_SIZE) {
|
|
|
764
914
|
};
|
|
765
915
|
}
|
|
766
916
|
//#endregion
|
|
767
|
-
export { TransportClosedError, collect, collectBytes, collectString, decodeJsonl, decodeText, deserializeError, emulateMux, encodeJsonl, encodeText, fromReadableStream, joinLines, map, newAsyncGenerator, normalizeToUint8Array, recieveIterator, sendIterator, serializeError, splitLines, toChunks, toReadableStream };
|
|
768
|
-
|
|
769
|
-
//# sourceMappingURL=index.mjs.map
|
|
917
|
+
export { TransportClosedError, collect, collectBytes, collectString, decodeJsonl, decodeText, deserializeError, emulateMux, encodeJsonl, encodeText, fromReadableStream, joinLines, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, normalizeToUint8Array, recieveIterator, sendIterator, serializeError, splitLines, toChunks, toReadableStream };
|
package/dist/jsonl.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Encode each value as a JSON line (terminated by \n). */
|
|
2
|
+
export declare function encodeJsonl<T>(input: AsyncIterable<T>): AsyncGenerator<string>;
|
|
3
|
+
/** Decode each line as a JSON value. Skips empty lines. */
|
|
4
|
+
export declare function decodeJsonl<T>(input: AsyncIterable<string>): AsyncGenerator<T>;
|
|
5
|
+
//# sourceMappingURL=jsonl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonl.d.ts","sourceRoot":"","sources":["../src/jsonl.ts"],"names":[],"mappings":"AAGA,2DAA2D;AAC3D,wBAAuB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAErF;AAED,2DAA2D;AAC3D,wBAAuB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAKrF"}
|
package/dist/lines.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Split a stream of string chunks into individual lines (delimited by \n). */
|
|
2
|
+
export declare function splitLines(input: AsyncIterable<string>): AsyncGenerator<string>;
|
|
3
|
+
/** Append \n to each string in the stream. */
|
|
4
|
+
export declare function joinLines(input: AsyncIterable<string>): AsyncGenerator<string>;
|
|
5
|
+
//# sourceMappingURL=lines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lines.d.ts","sourceRoot":"","sources":["../src/lines.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAuB,UAAU,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAYtF;AAED,8CAA8C;AAC9C,wBAAuB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAIrF"}
|
package/dist/map.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../src/map.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,wBAAuB,GAAG,CAAC,CAAC,EAAE,CAAC,EAC7B,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC9B,cAAc,CAAC,CAAC,CAAC,CAInB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The newAsyncGenerator function creates async generators from callback-based initialization
|
|
3
|
+
* functions, providing a bridge between imperative event handling and declarative async
|
|
4
|
+
* iteration patterns.
|
|
5
|
+
*
|
|
6
|
+
* The initialization function receives two callback functions that control the generator's
|
|
7
|
+
* behavior. The `next` function yields values to consumers, while the `done` function
|
|
8
|
+
* signals completion or error conditions. The initialization function can return a cleanup
|
|
9
|
+
* function that will be called when the generator is closed or an error occurs.
|
|
10
|
+
*
|
|
11
|
+
* The generator manages an internal queue of values and completion signals, ensuring that
|
|
12
|
+
* producers can yield values without overwhelming consumers. Proper backpressure is implemented
|
|
13
|
+
* by having the `next` and `done` functions return promises that resolve only after the
|
|
14
|
+
* consumer has processed the values. This prevents memory leaks and ensures that producers
|
|
15
|
+
* are aware of whether their values were successfully handled.
|
|
16
|
+
* The generator also handles cleanup by draining any remaining items in the queue and notifying
|
|
17
|
+
* producers that their values were not processed if the generator is closed early. This ensures
|
|
18
|
+
* that resources are properly managed and that no memory leaks occur.
|
|
19
|
+
*
|
|
20
|
+
* Example usage:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const asyncGen = newAsyncGenerator<number>((next, done) => {
|
|
23
|
+
* let count = 0;
|
|
24
|
+
* const interval = setInterval(() => {
|
|
25
|
+
* if (count < 5) {
|
|
26
|
+
* next(count++);
|
|
27
|
+
* } else {
|
|
28
|
+
* done();
|
|
29
|
+
* clearInterval(interval);
|
|
30
|
+
* }
|
|
31
|
+
* }, 1000);
|
|
32
|
+
* return () => clearInterval(interval); // Cleanup function
|
|
33
|
+
* });
|
|
34
|
+
* (async () => {
|
|
35
|
+
* for await (const num of asyncGen) {
|
|
36
|
+
* console.log(num); // Logs numbers 0 to 4 at 1 second intervals
|
|
37
|
+
* }
|
|
38
|
+
* console.log("Completed");
|
|
39
|
+
* })();
|
|
40
|
+
* ```
|
|
41
|
+
* @template T The type of values yielded by the generator
|
|
42
|
+
* @template E The type of errors that can be thrown; defaults to Error
|
|
43
|
+
* @param init Initialization function that sets up the generator behavior with next/done callbacks;
|
|
44
|
+
* The first parameter - `next` function - is used to yield values to consumers;
|
|
45
|
+
* It returns a Promise<boolean> indicating whether the value was successfully handled;
|
|
46
|
+
* The second parameter - `done` function - is used to signal completion or error;
|
|
47
|
+
* It returns a Promise<boolean> indicating whether the completion was successfully handled;
|
|
48
|
+
* The initialization function can optionally return a cleanup function that will be called
|
|
49
|
+
* when the generator is closed or an error occurs;
|
|
50
|
+
* @param skipValues If true, only the most recent value is kept in the queue,
|
|
51
|
+
* skipping intermediate values (not consumed values are considered skipped);
|
|
52
|
+
* This is useful for scenarios where only the latest value matters, such as UI updates.
|
|
53
|
+
* Defaults to false, meaning all values are queued and processed in order.
|
|
54
|
+
* @returns AsyncGenerator that properly manages backpressure and resource cleanup
|
|
55
|
+
*/
|
|
56
|
+
export declare function newAsyncGenerator<T, E = Error>(
|
|
57
|
+
/**
|
|
58
|
+
* Initialization function that sets up the async generator behavior.
|
|
59
|
+
* @param next - Function to yield values to consumers. Returns a Promise<boolean>
|
|
60
|
+
* indicating whether the value was successfully handled.
|
|
61
|
+
* @param done - Function to signal completion or error. Optional error parameter
|
|
62
|
+
* will cause the generator to throw that error.
|
|
63
|
+
* @returns Optional cleanup function that will be called when the generator terminates.
|
|
64
|
+
*/
|
|
65
|
+
init: (next: (value: T) => Promise<boolean>, done: (err?: E) => Promise<boolean>) => void | (() => void | Promise<void>),
|
|
66
|
+
/**
|
|
67
|
+
* Skipping queue implementation that maintains only the most recent value.
|
|
68
|
+
*/
|
|
69
|
+
skipValues?: boolean): AsyncGenerator<T>;
|
|
70
|
+
//# sourceMappingURL=new-async-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"new-async-generator.d.ts","sourceRoot":"","sources":["../src/new-async-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAuB,iBAAiB,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK;AACnD;;;;;;;GAOG;AACH,IAAI,EAAE,CACJ,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,EACpC,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,KAChC,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAExC;;GAEG;AACH,UAAU,UAAQ,GACjB,cAAc,CAAC,CAAC,CAAC,CA6InB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ByteLike = Uint8Array | ArrayBuffer | ArrayBufferView | Blob | string;
|
|
2
|
+
/**
|
|
3
|
+
* Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
|
|
4
|
+
* Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.
|
|
5
|
+
* Throws `TypeError` for anything else.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeToUint8Array(data: ByteLike): Uint8Array | Promise<Uint8Array>;
|
|
8
|
+
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAElF;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAatF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The iterator ↔ `ReadableStream` boundary.
|
|
3
|
+
*
|
|
4
|
+
* Both adapters must carry CANCELLATION, not just data: a response body leaves
|
|
5
|
+
* a handler as a `ReadableStream`, crosses a transport as an iterator, and
|
|
6
|
+
* becomes a `ReadableStream` again at the caller — so when the caller walks
|
|
7
|
+
* away, the only path back to the handler's producer runs through both of
|
|
8
|
+
* these functions. Teardown that stops at an adapter leaves a producer running
|
|
9
|
+
* for ever.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toReadableStream(it: AsyncIterator<Uint8Array>): ReadableStream<Uint8Array>;
|
|
12
|
+
export declare function fromReadableStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array, void, unknown>;
|
|
13
|
+
//# sourceMappingURL=readable-streams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readable-streams.d.ts","sourceRoot":"","sources":["../src/readable-streams.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CA+B1F;AAED,wBAAuB,kBAAkB,CACvC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAoB3C"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IteratorChunk } from "./send-iterator.js";
|
|
2
|
+
export type ChunkReceiver<T> = (chunk?: IteratorChunk<T>) => Promise<boolean>;
|
|
3
|
+
export type ReceiverInstaller<T> = (deliver: ChunkReceiver<T>) => (() => void | Promise<void>) | undefined | void;
|
|
4
|
+
/**
|
|
5
|
+
* Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
|
|
6
|
+
* chunks (delivered to the supplied callback by `installer`) into an async
|
|
7
|
+
* generator.
|
|
8
|
+
*
|
|
9
|
+
* The `installer` is given a `deliver` function. It should call `deliver`
|
|
10
|
+
* once for each incoming chunk and may return a cleanup callback that
|
|
11
|
+
* runs when the consumer stops iterating.
|
|
12
|
+
*/
|
|
13
|
+
export declare function recieveIterator<T>(installer: ReceiverInstaller<T>): AsyncGenerator<T>;
|
|
14
|
+
//# sourceMappingURL=recieve-iterator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recieve-iterator.d.ts","sourceRoot":"","sources":["../src/recieve-iterator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAC9E,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,CACjC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,KACtB,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAerF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IteratorChunk<T> {
|
|
2
|
+
done: boolean;
|
|
3
|
+
value?: T;
|
|
4
|
+
error?: unknown;
|
|
5
|
+
}
|
|
6
|
+
export type ChunkSender<T> = (chunk: IteratorChunk<T>) => void | Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Drain an async iterator into a sink that consumes one chunk at a time.
|
|
9
|
+
*
|
|
10
|
+
* Each yielded value becomes `{ done: false, value }`. Completion emits
|
|
11
|
+
* `{ done: true }`. If the iterator throws, the error is caught and the
|
|
12
|
+
* final `done` chunk carries it so the peer can rethrow on its side.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sendIterator<T>(send: ChunkSender<T>, it: AsyncIterable<T> | Iterable<T>): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=send-iterator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-iterator.d.ts","sourceRoot":"","sources":["../src/send-iterator.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,CAAC,CAAC;IACV,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE/E;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EACpB,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAWf"}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Decode Uint8Array chunks to string chunks via TextDecoder, handling split multi-byte characters. */
|
|
2
|
+
export declare function decodeText(input: AsyncIterable<Uint8Array>): AsyncGenerator<string>;
|
|
3
|
+
/** Encode string chunks to Uint8Array chunks via TextEncoder. */
|
|
4
|
+
export declare function encodeText(input: AsyncIterable<string>): AsyncGenerator<Uint8Array>;
|
|
5
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,wBAAuB,UAAU,CAAC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CAQ1F;AAED,iEAAiE;AACjE,wBAAuB,UAAU,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC,UAAU,CAAC,CAK1F"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
|
|
3
|
+
* chunks no larger than `size` bytes. Empty input chunks are skipped; small
|
|
4
|
+
* input chunks pass through unchanged (zero-copy `subarray` views are used
|
|
5
|
+
* when splitting, so no allocation per chunk).
|
|
6
|
+
*
|
|
7
|
+
* Use to respect transport MTUs before reaching `channel.send`. Reassembly on
|
|
8
|
+
* the receive side is not needed — consumers iterate a continuous byte stream.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toChunks(size?: number): (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array>;
|
|
11
|
+
//# sourceMappingURL=to-chunks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-chunks.d.ts","sourceRoot":"","sources":["../src/to-chunks.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,IAAI,GAAE,MAA2B,GAChC,CAAC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,cAAc,CAAC,UAAU,CAAC,CAmBzF"}
|
package/dist/uint32.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-streams",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Async-iterator / ReadableStream primitives: collect, text/jsonl codecs, lines, backpressure generators, serialisable errors.",
|
|
@@ -16,31 +16,38 @@
|
|
|
16
16
|
"directory": "packages/webrun-streams"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
|
-
".":
|
|
19
|
+
".": {
|
|
20
|
+
"source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist",
|
|
23
27
|
"src"
|
|
24
28
|
],
|
|
25
29
|
"devDependencies": {
|
|
26
|
-
"@
|
|
30
|
+
"@biomejs/biome": "^2.5.15",
|
|
31
|
+
"@types/node": "^26.6.4",
|
|
27
32
|
"rimraf": "^6.1.3",
|
|
28
|
-
"
|
|
33
|
+
"rolldown": "^1.2.12",
|
|
29
34
|
"typescript": "^7.0.2",
|
|
30
|
-
"vitest": "^
|
|
35
|
+
"vitest": "^5.0.3"
|
|
31
36
|
},
|
|
32
37
|
"sideEffects": false,
|
|
33
38
|
"publishConfig": {
|
|
34
39
|
"access": "public"
|
|
35
40
|
},
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
36
42
|
"scripts": {
|
|
37
|
-
"build": "
|
|
43
|
+
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
38
44
|
"dev": "tsdown --watch",
|
|
39
45
|
"test": "vitest run",
|
|
40
46
|
"test:watch": "vitest",
|
|
41
47
|
"typecheck": "tsc --noEmit",
|
|
48
|
+
"typecheck:tests": "tsc -p tsconfig.tests.json",
|
|
42
49
|
"clean": "rimraf dist",
|
|
43
|
-
"lint": "biome check
|
|
50
|
+
"lint": "biome check src tests",
|
|
44
51
|
"format": "biome format --write ."
|
|
45
52
|
}
|
|
46
53
|
}
|