@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.
Files changed (44) hide show
  1. package/README.md +197 -13
  2. package/dist/collect.d.ts +7 -0
  3. package/dist/collect.d.ts.map +1 -0
  4. package/dist/duplex.d.ts +52 -0
  5. package/dist/duplex.d.ts.map +1 -0
  6. package/dist/emulate-mux.d.ts +39 -0
  7. package/dist/emulate-mux.d.ts.map +1 -0
  8. package/dist/errors.d.ts +8 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/flow-control.d.ts +71 -0
  11. package/dist/flow-control.d.ts.map +1 -0
  12. package/dist/index.d.ts +16 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/{index.mjs → index.js} +251 -56
  15. package/dist/jsonl.d.ts +5 -0
  16. package/dist/jsonl.d.ts.map +1 -0
  17. package/dist/lines.d.ts +5 -0
  18. package/dist/lines.d.ts.map +1 -0
  19. package/dist/map.d.ts +3 -0
  20. package/dist/map.d.ts.map +1 -0
  21. package/dist/new-async-generator.d.ts +70 -0
  22. package/dist/new-async-generator.d.ts.map +1 -0
  23. package/dist/normalize.d.ts +8 -0
  24. package/dist/normalize.d.ts.map +1 -0
  25. package/dist/readable-streams.d.ts +13 -0
  26. package/dist/readable-streams.d.ts.map +1 -0
  27. package/dist/recieve-iterator.d.ts +14 -0
  28. package/dist/recieve-iterator.d.ts.map +1 -0
  29. package/dist/send-iterator.d.ts +15 -0
  30. package/dist/send-iterator.d.ts.map +1 -0
  31. package/dist/text.d.ts +5 -0
  32. package/dist/text.d.ts.map +1 -0
  33. package/dist/to-chunks.d.ts +11 -0
  34. package/dist/to-chunks.d.ts.map +1 -0
  35. package/dist/uint32.d.ts +25 -0
  36. package/dist/uint32.d.ts.map +1 -0
  37. package/package.json +13 -8
  38. package/src/duplex.ts +59 -0
  39. package/src/emulate-mux.ts +188 -74
  40. package/src/flow-control.ts +146 -0
  41. package/src/index.ts +2 -0
  42. package/src/readable-streams.ts +49 -13
  43. package/src/uint32.ts +34 -0
  44. 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/emulate-mux.ts
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
- * Thrown by `emulateMux` and adapters when the underlying transport closes
60
- * while one or more `Duplex` calls are in flight. Consumers can catch by
61
- * `instanceof TransportClosedError` or by checking `error.name`.
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
- var TransportClosedError = class extends Error {
64
- name = "TransportClosedError";
65
- constructor(message = "transport closed") {
66
- super(message);
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 = 64 * 1024;
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
- const resolve = s.resolveAck;
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
- resolveAck: null,
128
- rejectAck: null,
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 await (const chunk of input) {
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 end = Math.min(off + mtu, chunk.byteLength);
142
- const piece = chunk.subarray(off, end);
143
- sendFrame(s.id, TYPE_DATA, piece);
144
- await new Promise((resolve, reject) => {
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) sendFrame(s.id, TYPE_END);
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
- const { value: id, offset } = decodeVarint(frame, 0);
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
- if (handled && !s.closed && !muxClosed) sendFrame(id, TYPE_ACK);
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 r = s.resolveAck;
205
- s.resolveAck = null;
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({ async pull(controller) {
601
- let handled = false;
602
- try {
603
- while (true) {
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) break;
606
- const value = await slot.value;
607
- controller.enqueue(value);
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
- } catch (error) {
610
- handled = true;
611
- controller.error(error);
612
- } finally {
613
- if (!handled) controller.close();
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
- while (true) {
620
- const { done, value } = await reader.read();
621
- if (done) break;
622
- if (value !== void 0) yield value;
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 = 16 * 1024;
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 };
@@ -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"}
@@ -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,3 @@
1
+ /** Apply a sync or async function to each item in a stream. */
2
+ export declare function map<I, O>(input: AsyncIterable<I>, fn: (item: I) => O | Promise<O>): AsyncGenerator<O>;
3
+ //# sourceMappingURL=map.d.ts.map
@@ -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"}
@@ -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