@statewalker/webrun-streams 0.1.0 → 0.2.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 +197 -13
- 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} +251 -56
- 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 +13 -8
- package/src/duplex.ts +59 -0
- package/src/emulate-mux.ts +188 -74
- 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/LICENSE +0 -21
|
@@ -13,7 +13,7 @@ async function collectBytes(input) {
|
|
|
13
13
|
chunks.push(chunk);
|
|
14
14
|
total += chunk.length;
|
|
15
15
|
}
|
|
16
|
-
if (chunks.length === 1) return chunks[0] ?? new Uint8Array(0);
|
|
16
|
+
if (chunks.length === 1) return chunks[0] ?? /* @__PURE__ */ new Uint8Array(0);
|
|
17
17
|
const result = new Uint8Array(total);
|
|
18
18
|
let offset = 0;
|
|
19
19
|
for (const chunk of chunks) {
|
|
@@ -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;
|
|
@@ -73,12 +186,15 @@ const TYPE_END = 4;
|
|
|
73
186
|
const TYPE_ERROR = 5;
|
|
74
187
|
const TYPE_CLOSE = 6;
|
|
75
188
|
const DEFAULT_MAX_STREAMS = 256;
|
|
76
|
-
const DEFAULT_MTU =
|
|
189
|
+
const DEFAULT_MTU = 65536;
|
|
190
|
+
const DEFAULT_MAX_STREAM_BUFFER = 8388608;
|
|
77
191
|
const textEncoder = new TextEncoder();
|
|
78
192
|
const textDecoder = new TextDecoder();
|
|
79
193
|
function emulateMux(channel, opts = {}) {
|
|
80
194
|
const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
|
|
81
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}`);
|
|
82
198
|
const side = opts.side ?? "initiator";
|
|
83
199
|
const streams = /* @__PURE__ */ new Map();
|
|
84
200
|
let nextLocalId = side === "initiator" ? 2 : 1;
|
|
@@ -99,13 +215,24 @@ function emulateMux(channel, opts = {}) {
|
|
|
99
215
|
const teardownStream = (s, err) => {
|
|
100
216
|
if (s.closed) return;
|
|
101
217
|
s.closed = true;
|
|
102
|
-
|
|
103
|
-
const reject = s.rejectAck;
|
|
104
|
-
s.resolveAck = null;
|
|
105
|
-
s.rejectAck = null;
|
|
106
|
-
if (reject && err) reject(err);
|
|
107
|
-
else resolve?.();
|
|
218
|
+
s.outboundCredit.fail(err ?? new TransportClosedError("emulateMux: stream closed"));
|
|
108
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;
|
|
109
236
|
streams.delete(s.id);
|
|
110
237
|
};
|
|
111
238
|
const failAll = (err) => {
|
|
@@ -124,31 +251,42 @@ function emulateMux(channel, opts = {}) {
|
|
|
124
251
|
}),
|
|
125
252
|
pushIn: queue.push,
|
|
126
253
|
doneIn: queue.done,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
closed: false
|
|
254
|
+
outboundCredit: newCreditLedger(0),
|
|
255
|
+
grantor: newCreditGrantor(maxStreamBuffer),
|
|
256
|
+
closed: false,
|
|
257
|
+
inDone: false,
|
|
258
|
+
outDone: false,
|
|
259
|
+
queuedBytes: 0
|
|
130
260
|
};
|
|
131
261
|
return state;
|
|
132
262
|
};
|
|
133
263
|
const pumpOutbound = async (s, input) => {
|
|
264
|
+
const iterator = input[Symbol.asyncIterator]?.() ?? input[Symbol.iterator]();
|
|
265
|
+
s.cancelInput = () => {
|
|
266
|
+
iterator.return?.(void 0);
|
|
267
|
+
};
|
|
134
268
|
try {
|
|
135
|
-
for
|
|
269
|
+
for (;;) {
|
|
270
|
+
const next = await iterator.next();
|
|
271
|
+
if (next.done === true) break;
|
|
272
|
+
const chunk = next.value;
|
|
136
273
|
if (s.closed || muxClosed) return;
|
|
137
274
|
if (chunk.byteLength === 0) continue;
|
|
138
275
|
let off = 0;
|
|
139
276
|
while (off < chunk.byteLength) {
|
|
140
277
|
if (s.closed || muxClosed) return;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
s.resolveAck = resolve;
|
|
146
|
-
s.rejectAck = reject;
|
|
147
|
-
});
|
|
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));
|
|
148
282
|
off = end;
|
|
149
283
|
}
|
|
150
284
|
}
|
|
151
|
-
if (!s.closed && !muxClosed)
|
|
285
|
+
if (!s.closed && !muxClosed) {
|
|
286
|
+
sendFrame(s.id, TYPE_END);
|
|
287
|
+
s.outDone = true;
|
|
288
|
+
releaseIfComplete(s);
|
|
289
|
+
}
|
|
152
290
|
} catch (err) {
|
|
153
291
|
if (!s.closed && !muxClosed) {
|
|
154
292
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -168,11 +306,22 @@ function emulateMux(channel, opts = {}) {
|
|
|
168
306
|
return;
|
|
169
307
|
}
|
|
170
308
|
await pumpOutbound(s, outbound);
|
|
309
|
+
if (!s.inDone && !s.closed && !muxClosed) {
|
|
310
|
+
sendFrame(s.id, TYPE_CLOSE);
|
|
311
|
+
teardownStream(s);
|
|
312
|
+
}
|
|
171
313
|
};
|
|
172
314
|
const handleFrame = (frame) => {
|
|
173
315
|
if (muxClosed) return;
|
|
174
316
|
if (frame.byteLength < 2) return;
|
|
175
|
-
|
|
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;
|
|
176
325
|
const type = frame[offset];
|
|
177
326
|
const payload = frame.subarray(offset + 1);
|
|
178
327
|
if (type === TYPE_OPEN) {
|
|
@@ -187,6 +336,9 @@ function emulateMux(channel, opts = {}) {
|
|
|
187
336
|
}
|
|
188
337
|
const s = createStream(id);
|
|
189
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));
|
|
190
342
|
runHandler(s, handler);
|
|
191
343
|
return;
|
|
192
344
|
}
|
|
@@ -194,21 +346,31 @@ function emulateMux(channel, opts = {}) {
|
|
|
194
346
|
if (!s) return;
|
|
195
347
|
switch (type) {
|
|
196
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
|
+
}
|
|
197
356
|
const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
|
|
198
357
|
s.pushIn(copy).then((handled) => {
|
|
199
|
-
|
|
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));
|
|
200
362
|
});
|
|
201
363
|
return;
|
|
202
364
|
}
|
|
203
365
|
case TYPE_ACK: {
|
|
204
|
-
const
|
|
205
|
-
s.
|
|
206
|
-
s.rejectAck = null;
|
|
207
|
-
r?.();
|
|
366
|
+
const granted = decodeUint32(payload);
|
|
367
|
+
if (granted !== void 0) s.outboundCredit.grant(granted);
|
|
208
368
|
return;
|
|
209
369
|
}
|
|
210
370
|
case TYPE_END:
|
|
371
|
+
s.inDone = true;
|
|
211
372
|
s.doneIn();
|
|
373
|
+
releaseIfComplete(s);
|
|
212
374
|
return;
|
|
213
375
|
case TYPE_ERROR:
|
|
214
376
|
teardownStream(s, decodeError(payload));
|
|
@@ -239,7 +401,7 @@ function emulateMux(channel, opts = {}) {
|
|
|
239
401
|
nextLocalId += 2;
|
|
240
402
|
const s = createStream(id);
|
|
241
403
|
streams.set(id, s);
|
|
242
|
-
sendFrame(id, TYPE_OPEN);
|
|
404
|
+
sendFrame(id, TYPE_OPEN, encodeUint32(maxStreamBuffer));
|
|
243
405
|
pumpOutbound(s, input);
|
|
244
406
|
return s.inbound;
|
|
245
407
|
};
|
|
@@ -596,30 +758,63 @@ function describe(value) {
|
|
|
596
758
|
}
|
|
597
759
|
//#endregion
|
|
598
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
|
+
*/
|
|
599
771
|
function toReadableStream(it) {
|
|
600
|
-
return new ReadableStream({
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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 {
|
|
604
782
|
const slot = await it.next();
|
|
605
|
-
if (!slot || slot.done)
|
|
606
|
-
|
|
607
|
-
|
|
783
|
+
if (!slot || slot.done) {
|
|
784
|
+
controller.close();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
controller.enqueue(await slot.value);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
controller.error(error);
|
|
608
790
|
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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(() => {});
|
|
614
800
|
}
|
|
615
|
-
}
|
|
801
|
+
});
|
|
616
802
|
}
|
|
617
803
|
async function* fromReadableStream(stream) {
|
|
618
804
|
const reader = stream.getReader();
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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(() => {});
|
|
623
818
|
}
|
|
624
819
|
}
|
|
625
820
|
//#endregion
|
|
@@ -690,7 +885,7 @@ async function* encodeText(input) {
|
|
|
690
885
|
}
|
|
691
886
|
//#endregion
|
|
692
887
|
//#region src/to-chunks.ts
|
|
693
|
-
const DEFAULT_CHUNK_SIZE =
|
|
888
|
+
const DEFAULT_CHUNK_SIZE = 16384;
|
|
694
889
|
/**
|
|
695
890
|
* Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
|
|
696
891
|
* chunks no larger than `size` bytes. Empty input chunks are skipped; small
|
|
@@ -719,4 +914,4 @@ function toChunks(size = DEFAULT_CHUNK_SIZE) {
|
|
|
719
914
|
};
|
|
720
915
|
}
|
|
721
916
|
//#endregion
|
|
722
|
-
export { TransportClosedError, collect, collectBytes, collectString, decodeJsonl, decodeText, deserializeError, emulateMux, encodeJsonl, encodeText, fromReadableStream, joinLines, map, newAsyncGenerator, normalizeToUint8Array, recieveIterator, sendIterator, serializeError, splitLines, toChunks, toReadableStream };
|
|
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
|