@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.
Files changed (46) hide show
  1. package/README.md +88 -9
  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} +200 -52
  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 +14 -7
  38. package/src/duplex.ts +59 -0
  39. package/src/emulate-mux.ts +100 -85
  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/dist/index.d.mts +0 -244
  45. package/dist/index.d.mts.map +0 -1
  46. 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/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;
@@ -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
- const resolve = s.resolveAck;
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
- resolveAck: null,
145
- rejectAck: null,
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 await (const chunk of input) {
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 end = Math.min(off + mtu, chunk.byteLength);
162
- const piece = chunk.subarray(off, end);
163
- sendFrame(s.id, TYPE_DATA, piece);
164
- await new Promise((resolve, reject) => {
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 && !s.closed && !muxClosed) sendFrame(id, TYPE_ACK);
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 r = s.resolveAck;
248
- s.resolveAck = null;
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({ async pull(controller) {
646
- let handled = false;
647
- try {
648
- 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 {
649
782
  const slot = await it.next();
650
- if (!slot || slot.done) break;
651
- const value = await slot.value;
652
- 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);
653
790
  }
654
- } catch (error) {
655
- handled = true;
656
- controller.error(error);
657
- } finally {
658
- 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(() => {});
659
800
  }
660
- } });
801
+ });
661
802
  }
662
803
  async function* fromReadableStream(stream) {
663
804
  const reader = stream.getReader();
664
- while (true) {
665
- const { done, value } = await reader.read();
666
- if (done) break;
667
- 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(() => {});
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 };
@@ -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
@@ -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.1.1",
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
- ".": "./src/index.ts"
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
- "@types/node": "^26.2.0",
30
+ "@biomejs/biome": "^2.5.15",
31
+ "@types/node": "^26.6.4",
27
32
  "rimraf": "^6.1.3",
28
- "tsdown": "^0.22.14",
33
+ "rolldown": "^1.2.12",
29
34
  "typescript": "^7.0.2",
30
- "vitest": "^4.1.10"
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": "tsdown",
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 --write .",
50
+ "lint": "biome check src tests",
44
51
  "format": "biome format --write ."
45
52
  }
46
53
  }