@statewalker/webrun-streams 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Async-iterator and `ReadableStream` primitives: `collect` / `collectBytes` / `collectString`, text and JSONL codecs, line splitting/joining, a backpressure-aware queue-based generator, a chunk protocol for pushing iterators across transports, conversions between async iterators and WHATWG `ReadableStream<Uint8Array>`, and serialisable `Error` objects.
4
4
 
5
+ It also defines the **`Duplex` seam** the whole `webrun-streams-*` transport family implements, and `emulateMux` — a stream multiplexer that turns any message-oriented byte channel into many concurrent `Duplex` calls.
6
+
5
7
  ## Why it exists
6
8
 
7
9
  Every higher-level package in the `webrun-*` family (and its consumers — scanners, indexers, chat pipelines) needs the same small set of building blocks:
@@ -35,6 +37,9 @@ npm install @statewalker/webrun-streams
35
37
  | `recieveIterator(installer)` | Inverse of `sendIterator`: wire an installer's chunk callback into a new `AsyncGenerator<T>`. |
36
38
  | `toReadableStream(it)` | Wrap an `AsyncIterator<Uint8Array>` in a `ReadableStream<Uint8Array>`. |
37
39
  | `fromReadableStream(stream)` | Iterate a `ReadableStream<Uint8Array>` as `AsyncGenerator<Uint8Array>`. |
40
+ | `emulateMux(channel, opts?)` | Multiplex many concurrent `Duplex` calls over one `ByteChannel`; returns `{ call, serve, close }`. |
41
+ | `normalizeToUint8Array(value)` | Coerce a `ByteLike` (string, `ArrayBuffer`, typed array, `Blob`) to `Uint8Array`. Synchronous except for a `Blob`, which returns a `Promise`. |
42
+ | `toChunks(size?)` | Curried: returns a transform that re-chunks an `AsyncIterable<Uint8Array>` into pieces of at most `size` bytes. `size` defaults to 16384. |
38
43
  | `serializeError(error)` | Turn an `Error` (or anything) into a plain `{message, stack, …}` object preserving subclass fields. |
39
44
  | `deserializeError(obj \| string)` | Reconstruct an `Error` from a serialised form, restoring extra fields. |
40
45
 
@@ -75,11 +80,18 @@ async function* chunks() {
75
80
  yield new Uint8Array([0x22, 0x3a, 0x31, 0x7d, 0x0a]);
76
81
  }
77
82
 
78
- const values = decodeJsonl<{ a: number }>(splitLines(decodeText(chunks())));
83
+ // `decodeJsonl` splits lines itself — do not wrap it in `splitLines`, or a
84
+ // stream carrying more than one value arrives as one concatenated line and
85
+ // `JSON.parse` throws.
86
+ const values = decodeJsonl<{ a: number }>(decodeText(chunks()));
79
87
  for await (const v of values) console.log(v); // { a: 1 }
80
88
 
81
- // inverse
82
- const jsonl = encodeText(joinLines(encodeJsonl([{ a: 1 }, { a: 2 }])));
89
+ // inverse. `encodeJsonl` already terminates each value with "\n", so
90
+ // `joinLines` here would emit a blank line between every record.
91
+ const jsonl = encodeText(encodeJsonl([{ a: 1 }, { a: 2 }]));
92
+
93
+ // `splitLines` / `joinLines` are for plain string streams, with no JSON involved:
94
+ for await (const line of splitLines(decodeText(byteStream))) console.log(line);
83
95
  ```
84
96
 
85
97
  ### Callback → AsyncGenerator bridge
@@ -107,19 +119,27 @@ for await (const n of tickEverySecond()) console.log(n); // 0 … 4
107
119
  ### Iterator chunk protocol
108
120
 
109
121
  ```ts
110
- import { sendIterator, recieveIterator } from "@statewalker/webrun-streams";
122
+ import { collect, recieveIterator, sendIterator } from "@statewalker/webrun-streams";
111
123
 
112
124
  // Drain an iterable across any transport.
113
125
  async function transport<T>(chunk: { done: boolean; value?: T; error?: unknown }) {
114
- // …send `chunk` over your channel.
126
+ await myChannel.send(chunk); // …however your channel sends
115
127
  }
116
- await sendIterator(transport, [1, 2, 3]);
117
128
 
118
129
  // On the other side, rebuild the original iterator.
119
130
  const iter = recieveIterator<number>((deliver) => {
120
131
  myChannel.onMessage = (chunk) => deliver(chunk);
121
132
  });
122
- for await (const v of iter) console.log(v); // 1, 2, 3
133
+
134
+ // Start consuming *before* (or concurrently with) draining the source.
135
+ // `deliver` resolves only once the consumer has dequeued the chunk — that is
136
+ // the backpressure — so awaiting `sendIterator` with nobody iterating `iter`
137
+ // deadlocks both sides.
138
+ const [, received] = await Promise.all([
139
+ sendIterator(transport, [1, 2, 3]),
140
+ collect(iter),
141
+ ]);
142
+ console.log(received); // [1, 2, 3]
123
143
  ```
124
144
 
125
145
  ### WHATWG streams ↔ async iterators
@@ -160,6 +180,89 @@ console.log(restored instanceof Error); // true
160
180
  console.log(restored.status); // 404
161
181
  ```
162
182
 
183
+ ## The `Duplex` seam
184
+
185
+ Everything in the `webrun-streams-*` family speaks one shape:
186
+
187
+ ```ts
188
+ type Duplex = (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array>;
189
+ ```
190
+
191
+ One `Duplex` invocation carries **one logical call**: the caller emits bytes,
192
+ the peer yields bytes back. Because both sides have the same shape, an
193
+ in-process test can wire `const caller = handler` and run with no transport at
194
+ all.
195
+
196
+ Iterator semantics carry every signal, which is why no separate close/abort API
197
+ exists:
198
+
199
+ | Signal | Mechanism |
200
+ | --- | --- |
201
+ | Consumer is done early | `.return()` on the output → producer's `finally` runs |
202
+ | Producer failed | `throw` → consumer's `for await` throws |
203
+ | Either side finished normally | Normal exhaustion → matching end on the other side |
204
+
205
+ `Connect<P>` and `Serve<P>` are the adapter-side factories that stand up a
206
+ transport and produce or register a `Duplex`.
207
+
208
+ > **Caller obligation.** Either drain the returned generator or `.return()` it.
209
+ > Dropping the reference without doing either emits no observable signal: the
210
+ > abandoned consumer never acknowledges inbound data, so the peer's outbound
211
+ > pump blocks awaiting that acknowledgement, no end-of-stream is exchanged, and
212
+ > both peers hold the stream open. An unreferenced generator is not observable,
213
+ > so no transport can detect this for you.
214
+
215
+ ## `emulateMux`
216
+
217
+ Turns one `ByteChannel` — anything with `send` / `recv` / `closed` / `close` —
218
+ into many concurrent `Duplex` calls. Used by the MessagePort, WebSocket,
219
+ LiveKit, PeerJS and signaling adapters; transports with native multiplexing
220
+ (libp2p) don't need it.
221
+
222
+ ```ts
223
+ import { emulateMux } from "@statewalker/webrun-streams";
224
+
225
+ const { call, serve, close } = emulateMux(channel, { side: "initiator" });
226
+
227
+ // caller side
228
+ const response = call([new TextEncoder().encode("ping")]);
229
+ for await (const chunk of response) { /* … */ }
230
+
231
+ // responder side
232
+ const stop = serve(async function* handler(input) {
233
+ for await (const chunk of input) yield chunk; // echo
234
+ });
235
+ ```
236
+
237
+ | Option | Default | Purpose |
238
+ | --- | --- | --- |
239
+ | `side` | `"initiator"` | Id allocation: initiator uses even ids, responder odd. Pick one per peer so they cannot collide. |
240
+ | `maxStreams` | `256` | Concurrent streams before new calls are refused. |
241
+ | `mtu` | `65536` | Largest payload per DATA frame; bigger chunks are split. |
242
+ | `maxStreamBuffer` | `8388608` | Inbound bytes one stream may hold for a consumer that has not drained them. Exceeding it tears down that stream alone. |
243
+
244
+ ### Flow control
245
+
246
+ One in-flight DATA frame per stream. The sender waits for an `ACK`, and the ACK
247
+ is sent only once the consumer has pulled *past* that chunk — so a fast producer
248
+ cannot run ahead of a slow consumer, and outbound is bounded to one `mtu` per
249
+ stream even against a peer that never acknowledges.
250
+
251
+ Backpressure is **per-stream**, so a stalled stream does not block the others,
252
+ and it applies symmetrically in both directions.
253
+
254
+ There is deliberately **no stall timeout**: a peer that never acknowledges
255
+ blocks that producer indefinitely, exactly as a TCP receiver that never reads
256
+ blocks its sender. `maxStreams` and `maxStreamBuffer` bound what that can cost.
257
+
258
+ ### Behaviour on hostile input
259
+
260
+ A `ByteChannel` is message-oriented, so frames are discrete and a corrupt one
261
+ cannot desync the next. A frame that cannot be parsed is therefore **dropped**
262
+ rather than failing the connection — otherwise one malformed frame would tear
263
+ down every stream sharing the mux. A stream that exceeds `maxStreamBuffer` is
264
+ torn down on its own, with an error frame sent to the peer.
265
+
163
266
  ## Internals
164
267
 
165
268
  ### `newAsyncGenerator` — backpressure queue
@@ -210,9 +313,11 @@ strict one-way converters: no queuing strategy tricks, no transform.
210
313
  - **British/American spelling kept.** `recieveIterator` uses the
211
314
  historical misspelling to stay wire-compatible with `webrun-ports`
212
315
  consumers.
213
- - **No tight coupling to any transport.** Nothing here mentions
214
- `MessagePort`, `fetch`, `Worker`, etc. Those belong to the consuming
215
- packages.
316
+ - **No tight coupling to any transport.** `ByteChannel` is the only
317
+ transport-facing type, and it is an interface — nothing here mentions
318
+ `MessagePort`, `WebSocket`, `fetch`, `Worker`, etc. Those belong to the
319
+ `webrun-streams-*` adapters, each of which supplies a `ByteChannel` and lets
320
+ `emulateMux` do the rest.
216
321
 
217
322
  ### Constraints
218
323
 
@@ -0,0 +1,244 @@
1
+ //#region src/collect.d.ts
2
+ /** Collect all items from an async iterable into an array. */
3
+ declare function collect<T>(input: AsyncIterable<T>): Promise<T[]>;
4
+ /** Concatenate all Uint8Array chunks into a single Uint8Array. */
5
+ declare function collectBytes(input: AsyncIterable<Uint8Array>): Promise<Uint8Array>;
6
+ /** Concatenate all string chunks into a single string. */
7
+ declare function collectString(input: AsyncIterable<string>): Promise<string>;
8
+ //#endregion
9
+ //#region src/emulate-mux.d.ts
10
+ /**
11
+ * Canonical seam for the webrun-streams transport family. A `Duplex` carries
12
+ * one logical call: caller emits an iterable of bytes as input, peer yields an
13
+ * async generator of bytes as output. Same shape on both sides — an in-process
14
+ * test can wire `const caller = handler` and run without any transport.
15
+ *
16
+ * Iterator semantics carry every signal:
17
+ * - Consumer `.return()` on the output → producer's `finally` runs.
18
+ * - Producer `throw` → consumer's `for await` throws.
19
+ * - Normal exhaustion on either side → matching end on the other side.
20
+ */
21
+ type Duplex = (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array>;
22
+ /**
23
+ * Adapter-side factory that stands up a transport connection and yields a
24
+ * caller `Duplex`. One `Connect` invocation owns one transport; each call
25
+ * to the resolved `call` opens a new sub-stream on it.
26
+ *
27
+ * A caller must either drain the returned generator or `.return()` it.
28
+ * Dropping the reference without doing either emits no observable signal:
29
+ * the abandoned consumer never acknowledges inbound data, so the peer's
30
+ * outbound pump blocks awaiting that acknowledgement, no end-of-stream is
31
+ * ever exchanged, and both peers hold the stream's slot open. There is no
32
+ * way for the transport to detect this — an unreferenced generator is not
33
+ * observable — so it is the consumer's obligation.
34
+ */
35
+ type Connect<P> = (params: P) => Promise<{
36
+ call: Duplex;
37
+ close: () => Promise<void>;
38
+ }>;
39
+ /**
40
+ * Adapter-side factory that registers a handler `Duplex` against a transport.
41
+ * Returns an idempotent teardown.
42
+ */
43
+ type Serve<P> = (params: P, handler: Duplex) => Promise<() => Promise<void>>;
44
+ /**
45
+ * The minimum a transport must expose to be wrapped by `emulateMux`. Inbound
46
+ * bytes are surfaced as an async iterable; outbound is imperative `send`. All
47
+ * five message-oriented transports (WebSocket, LiveKit data channel, PeerJS
48
+ * DataConnection, MessagePort, in-process pipe) match this shape after a thin
49
+ * wrapper.
50
+ */
51
+ type ByteChannel = {
52
+ send(bytes: Uint8Array): void;
53
+ recv: AsyncIterable<Uint8Array>;
54
+ closed: Promise<void>;
55
+ close(): void;
56
+ };
57
+ /**
58
+ * Thrown by `emulateMux` and adapters when the underlying transport closes
59
+ * while one or more `Duplex` calls are in flight. Consumers can catch by
60
+ * `instanceof TransportClosedError` or by checking `error.name`.
61
+ */
62
+ declare class TransportClosedError extends Error {
63
+ readonly name = "TransportClosedError";
64
+ constructor(message?: string);
65
+ }
66
+ interface EmulateMuxOptions {
67
+ maxStreams?: number;
68
+ mtu?: number;
69
+ /**
70
+ * Cap on inbound bytes one stream may hold for a consumer that has not
71
+ * drained them. Exceeding it tears down that stream alone.
72
+ */
73
+ maxStreamBuffer?: number;
74
+ /**
75
+ * Stream-id allocation side. Initiator uses even ids (2, 4, …); responder
76
+ * uses odd ids (1, 3, …). Pick one per peer so allocations don't collide.
77
+ */
78
+ side?: "initiator" | "responder";
79
+ }
80
+ declare function emulateMux(channel: ByteChannel, opts?: EmulateMuxOptions): {
81
+ call: Duplex;
82
+ serve: (handler: Duplex) => () => Promise<void>;
83
+ close: () => Promise<void>;
84
+ };
85
+ //#endregion
86
+ //#region src/errors.d.ts
87
+ interface SerializedError {
88
+ message: string;
89
+ stack?: string;
90
+ [key: string]: unknown;
91
+ }
92
+ declare function serializeError(error: unknown): SerializedError;
93
+ declare function deserializeError(error: SerializedError | string): Error;
94
+ //#endregion
95
+ //#region src/jsonl.d.ts
96
+ /** Encode each value as a JSON line (terminated by \n). */
97
+ declare function encodeJsonl<T>(input: AsyncIterable<T>): AsyncGenerator<string>;
98
+ /** Decode each line as a JSON value. Skips empty lines. */
99
+ declare function decodeJsonl<T>(input: AsyncIterable<string>): AsyncGenerator<T>;
100
+ //#endregion
101
+ //#region src/lines.d.ts
102
+ /** Split a stream of string chunks into individual lines (delimited by \n). */
103
+ declare function splitLines(input: AsyncIterable<string>): AsyncGenerator<string>;
104
+ /** Append \n to each string in the stream. */
105
+ declare function joinLines(input: AsyncIterable<string>): AsyncGenerator<string>;
106
+ //#endregion
107
+ //#region src/map.d.ts
108
+ /** Apply a sync or async function to each item in a stream. */
109
+ declare function map<I, O>(input: AsyncIterable<I>, fn: (item: I) => O | Promise<O>): AsyncGenerator<O>;
110
+ //#endregion
111
+ //#region src/new-async-generator.d.ts
112
+ /**
113
+ * The newAsyncGenerator function creates async generators from callback-based initialization
114
+ * functions, providing a bridge between imperative event handling and declarative async
115
+ * iteration patterns.
116
+ *
117
+ * The initialization function receives two callback functions that control the generator's
118
+ * behavior. The `next` function yields values to consumers, while the `done` function
119
+ * signals completion or error conditions. The initialization function can return a cleanup
120
+ * function that will be called when the generator is closed or an error occurs.
121
+ *
122
+ * The generator manages an internal queue of values and completion signals, ensuring that
123
+ * producers can yield values without overwhelming consumers. Proper backpressure is implemented
124
+ * by having the `next` and `done` functions return promises that resolve only after the
125
+ * consumer has processed the values. This prevents memory leaks and ensures that producers
126
+ * are aware of whether their values were successfully handled.
127
+ * The generator also handles cleanup by draining any remaining items in the queue and notifying
128
+ * producers that their values were not processed if the generator is closed early. This ensures
129
+ * that resources are properly managed and that no memory leaks occur.
130
+ *
131
+ * Example usage:
132
+ * ```typescript
133
+ * const asyncGen = newAsyncGenerator<number>((next, done) => {
134
+ * let count = 0;
135
+ * const interval = setInterval(() => {
136
+ * if (count < 5) {
137
+ * next(count++);
138
+ * } else {
139
+ * done();
140
+ * clearInterval(interval);
141
+ * }
142
+ * }, 1000);
143
+ * return () => clearInterval(interval); // Cleanup function
144
+ * });
145
+ * (async () => {
146
+ * for await (const num of asyncGen) {
147
+ * console.log(num); // Logs numbers 0 to 4 at 1 second intervals
148
+ * }
149
+ * console.log("Completed");
150
+ * })();
151
+ * ```
152
+ * @template T The type of values yielded by the generator
153
+ * @template E The type of errors that can be thrown; defaults to Error
154
+ * @param init Initialization function that sets up the generator behavior with next/done callbacks;
155
+ * The first parameter - `next` function - is used to yield values to consumers;
156
+ * It returns a Promise<boolean> indicating whether the value was successfully handled;
157
+ * The second parameter - `done` function - is used to signal completion or error;
158
+ * It returns a Promise<boolean> indicating whether the completion was successfully handled;
159
+ * The initialization function can optionally return a cleanup function that will be called
160
+ * when the generator is closed or an error occurs;
161
+ * @param skipValues If true, only the most recent value is kept in the queue,
162
+ * skipping intermediate values (not consumed values are considered skipped);
163
+ * This is useful for scenarios where only the latest value matters, such as UI updates.
164
+ * Defaults to false, meaning all values are queued and processed in order.
165
+ * @returns AsyncGenerator that properly manages backpressure and resource cleanup
166
+ */
167
+ declare function newAsyncGenerator<T, E = Error>(
168
+ /**
169
+ * Initialization function that sets up the async generator behavior.
170
+ * @param next - Function to yield values to consumers. Returns a Promise<boolean>
171
+ * indicating whether the value was successfully handled.
172
+ * @param done - Function to signal completion or error. Optional error parameter
173
+ * will cause the generator to throw that error.
174
+ * @returns Optional cleanup function that will be called when the generator terminates.
175
+ */
176
+ init: (next: (value: T) => Promise<boolean>, done: (err?: E) => Promise<boolean>) => void | (() => void | Promise<void>),
177
+ /**
178
+ * Skipping queue implementation that maintains only the most recent value.
179
+ */
180
+ skipValues?: boolean): AsyncGenerator<T>;
181
+ //#endregion
182
+ //#region src/normalize.d.ts
183
+ type ByteLike = Uint8Array | ArrayBuffer | ArrayBufferView | Blob | string;
184
+ /**
185
+ * Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
186
+ * Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.
187
+ * Throws `TypeError` for anything else.
188
+ */
189
+ declare function normalizeToUint8Array(data: ByteLike): Uint8Array | Promise<Uint8Array>;
190
+ //#endregion
191
+ //#region src/readable-streams.d.ts
192
+ declare function toReadableStream(it: AsyncIterator<Uint8Array>): ReadableStream<Uint8Array>;
193
+ declare function fromReadableStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array, void, unknown>;
194
+ //#endregion
195
+ //#region src/send-iterator.d.ts
196
+ interface IteratorChunk<T> {
197
+ done: boolean;
198
+ value?: T;
199
+ error?: unknown;
200
+ }
201
+ type ChunkSender<T> = (chunk: IteratorChunk<T>) => void | Promise<void>;
202
+ /**
203
+ * Drain an async iterator into a sink that consumes one chunk at a time.
204
+ *
205
+ * Each yielded value becomes `{ done: false, value }`. Completion emits
206
+ * `{ done: true }`. If the iterator throws, the error is caught and the
207
+ * final `done` chunk carries it so the peer can rethrow on its side.
208
+ */
209
+ declare function sendIterator<T>(send: ChunkSender<T>, it: AsyncIterable<T> | Iterable<T>): Promise<void>;
210
+ //#endregion
211
+ //#region src/recieve-iterator.d.ts
212
+ type ChunkReceiver<T> = (chunk?: IteratorChunk<T>) => Promise<boolean>;
213
+ type ReceiverInstaller<T> = (deliver: ChunkReceiver<T>) => (() => void | Promise<void>) | undefined | void;
214
+ /**
215
+ * Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
216
+ * chunks (delivered to the supplied callback by `installer`) into an async
217
+ * generator.
218
+ *
219
+ * The `installer` is given a `deliver` function. It should call `deliver`
220
+ * once for each incoming chunk and may return a cleanup callback that
221
+ * runs when the consumer stops iterating.
222
+ */
223
+ declare function recieveIterator<T>(installer: ReceiverInstaller<T>): AsyncGenerator<T>;
224
+ //#endregion
225
+ //#region src/text.d.ts
226
+ /** Decode Uint8Array chunks to string chunks via TextDecoder, handling split multi-byte characters. */
227
+ declare function decodeText(input: AsyncIterable<Uint8Array>): AsyncGenerator<string>;
228
+ /** Encode string chunks to Uint8Array chunks via TextEncoder. */
229
+ declare function encodeText(input: AsyncIterable<string>): AsyncGenerator<Uint8Array>;
230
+ //#endregion
231
+ //#region src/to-chunks.d.ts
232
+ /**
233
+ * Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
234
+ * chunks no larger than `size` bytes. Empty input chunks are skipped; small
235
+ * input chunks pass through unchanged (zero-copy `subarray` views are used
236
+ * when splitting, so no allocation per chunk).
237
+ *
238
+ * Use to respect transport MTUs before reaching `channel.send`. Reassembly on
239
+ * the receive side is not needed — consumers iterate a continuous byte stream.
240
+ */
241
+ declare function toChunks(size?: number): (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array>;
242
+ //#endregion
243
+ export { ByteChannel, ByteLike, ChunkReceiver, ChunkSender, Connect, Duplex, EmulateMuxOptions, IteratorChunk, ReceiverInstaller, SerializedError, Serve, TransportClosedError, collect, collectBytes, collectString, decodeJsonl, decodeText, deserializeError, emulateMux, encodeJsonl, encodeText, fromReadableStream, joinLines, map, newAsyncGenerator, normalizeToUint8Array, recieveIterator, sendIterator, serializeError, splitLines, toChunks, toReadableStream };
244
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/collect.ts","../src/emulate-mux.ts","../src/errors.ts","../src/jsonl.ts","../src/lines.ts","../src/map.ts","../src/new-async-generator.ts","../src/normalize.ts","../src/readable-streams.ts","../src/send-iterator.ts","../src/recieve-iterator.ts","../src/text.ts","../src/to-chunks.ts"],"mappings":";;iBACsB,QAAQ,GAAG,OAAO,cAAc,KAAK,QAAQ;;iBAO7C,aAAa,OAAO,cAAc,cAAc,QAAQ;;iBAkBxD,cAAc,OAAO,wBAAwB;;;;;;;;;;;;;;KCbvD,UACV,OAAO,cAAc,cAAc,SAAS,gBACzC,eAAe;;;;;;;;;;;;;;KAeR,QAAQ,MAAM,QAAQ,MAAM;EACtC,MAAM;EACN,aAAa;;;;;;KAOH,MAAM,MAAM,QAAQ,GAAG,SAAS,WAAW,cAAc;;;;;;;;KASzD;EACV,KAAK,OAAO;EACZ,MAAM,cAAc;EACpB,QAAQ;EACR;;;;;;;cAQW,6BAA6B;WACtB;EAClB,YAAY;;UAmCG;EACf;EACA;;;;;EAKA;;;;;EAKA;;iBAGc,WACd,SAAS,aACT,OAAM;EAEN,MAAM;EACN,QAAQ,SAAS,iBAAiB;EAClC,aAAa;;;;UCtHE;EACf;EACA;GACC;;iBAGa,eAAe,iBAAiB;iBAchC,iBAAiB,OAAO,2BAA2B;;;;iBChB5C,YAAY,GAAG,OAAO,cAAc,KAAK;;iBAKzC,YAAY,GAAG,OAAO,wBAAwB,eAAe;;;;iBCR7D,WAAW,OAAO,wBAAwB;;iBAe1C,UAAU,OAAO,wBAAwB;;;;iBCfzC,IAAI,GAAG,GAC5B,OAAO,cAAc,IACrB,KAAK,MAAM,MAAM,IAAI,QAAQ,KAC5B,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCmDK,kBAAkB,GAAG,IAAI,OAS9C;;;;;;;;;AAAA,OACE,OAAO,OAAO,MAAM,kBACpB,OAAO,MAAM,MAAM,0CACK,gBAK1B;;;;AAAA,uBACC,eAAe;;;KCvEN,WAAW,aAAa,cAAc,kBAAkB;;;;;;iBAOpD,sBAAsB,MAAM,WAAW,aAAa,QAAQ;;;iBCT5D,iBAAiB,IAAI,cAAc,cAAc,eAAe;iBAqBzD,mBACrB,QAAQ,eAAe,cACtB,eAAe;;;UCvBD,cAAc;EAC7B;EACA,QAAQ;EACR;;KAGU,YAAY,MAAM,OAAO,cAAc,cAAc;;;;;;;;iBAS3C,aAAa,GACjC,MAAM,YAAY,IAClB,IAAI,cAAc,KAAK,SAAS,KAC/B;;;KCfS,cAAc,MAAM,QAAQ,cAAc,OAAO;KACjD,kBAAkB,MAC5B,SAAS,cAAc,qBACN;;;;;;;;;;iBAWH,gBAAgB,GAAG,WAAW,kBAAkB,KAAK,eAAe;;;;iBChB7D,WAAW,OAAO,cAAc,cAAc;;iBAW9C,WAAW,OAAO,wBAAwB,eAAe;;;;;;;;;;;;iBCDhE,SACd,iBACE,OAAO,cAAc,cAAc,SAAS,gBAAgB,eAAe"}
package/dist/index.mjs CHANGED
@@ -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) {
@@ -73,12 +73,14 @@ const TYPE_END = 4;
73
73
  const TYPE_ERROR = 5;
74
74
  const TYPE_CLOSE = 6;
75
75
  const DEFAULT_MAX_STREAMS = 256;
76
- const DEFAULT_MTU = 64 * 1024;
76
+ const DEFAULT_MTU = 65536;
77
+ const DEFAULT_MAX_STREAM_BUFFER = 8388608;
77
78
  const textEncoder = new TextEncoder();
78
79
  const textDecoder = new TextDecoder();
79
80
  function emulateMux(channel, opts = {}) {
80
81
  const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
81
82
  const mtu = opts.mtu ?? DEFAULT_MTU;
83
+ const maxStreamBuffer = opts.maxStreamBuffer ?? DEFAULT_MAX_STREAM_BUFFER;
82
84
  const side = opts.side ?? "initiator";
83
85
  const streams = /* @__PURE__ */ new Map();
84
86
  let nextLocalId = side === "initiator" ? 2 : 1;
@@ -108,6 +110,21 @@ function emulateMux(channel, opts = {}) {
108
110
  s.doneIn(err);
109
111
  streams.delete(s.id);
110
112
  };
113
+ /**
114
+ * Graceful counterpart to `teardownStream`. A stream occupies a slot in
115
+ * `streams` until BOTH directions finish; releasing on inbound END alone
116
+ * would set `closed` while our own `pumpOutbound` is still sending, and it
117
+ * checks that flag to decide whether to keep going.
118
+ *
119
+ * Without this, only abnormal endings (cancel, ERROR, CLOSE, transport
120
+ * failure) ever freed a slot, so every normally-completed call leaked one
121
+ * until `maxStreams` began rejecting new calls.
122
+ */
123
+ const releaseIfComplete = (s) => {
124
+ if (s.closed || !s.inDone || !s.outDone) return;
125
+ s.closed = true;
126
+ streams.delete(s.id);
127
+ };
111
128
  const failAll = (err) => {
112
129
  if (muxClosed) return;
113
130
  muxClosed = true;
@@ -126,7 +143,10 @@ function emulateMux(channel, opts = {}) {
126
143
  doneIn: queue.done,
127
144
  resolveAck: null,
128
145
  rejectAck: null,
129
- closed: false
146
+ closed: false,
147
+ inDone: false,
148
+ outDone: false,
149
+ queuedBytes: 0
130
150
  };
131
151
  return state;
132
152
  };
@@ -148,7 +168,11 @@ function emulateMux(channel, opts = {}) {
148
168
  off = end;
149
169
  }
150
170
  }
151
- if (!s.closed && !muxClosed) sendFrame(s.id, TYPE_END);
171
+ if (!s.closed && !muxClosed) {
172
+ sendFrame(s.id, TYPE_END);
173
+ s.outDone = true;
174
+ releaseIfComplete(s);
175
+ }
152
176
  } catch (err) {
153
177
  if (!s.closed && !muxClosed) {
154
178
  const e = err instanceof Error ? err : new Error(String(err));
@@ -168,11 +192,22 @@ function emulateMux(channel, opts = {}) {
168
192
  return;
169
193
  }
170
194
  await pumpOutbound(s, outbound);
195
+ if (!s.inDone && !s.closed && !muxClosed) {
196
+ sendFrame(s.id, TYPE_CLOSE);
197
+ teardownStream(s);
198
+ }
171
199
  };
172
200
  const handleFrame = (frame) => {
173
201
  if (muxClosed) return;
174
202
  if (frame.byteLength < 2) return;
175
- const { value: id, offset } = decodeVarint(frame, 0);
203
+ let id;
204
+ let offset;
205
+ try {
206
+ ({value: id, offset} = decodeVarint(frame, 0));
207
+ } catch {
208
+ return;
209
+ }
210
+ if (offset >= frame.byteLength) return;
176
211
  const type = frame[offset];
177
212
  const payload = frame.subarray(offset + 1);
178
213
  if (type === TYPE_OPEN) {
@@ -194,8 +229,16 @@ function emulateMux(channel, opts = {}) {
194
229
  if (!s) return;
195
230
  switch (type) {
196
231
  case TYPE_DATA: {
232
+ s.queuedBytes += payload.byteLength;
233
+ if (s.queuedBytes > maxStreamBuffer) {
234
+ const err = /* @__PURE__ */ new RangeError(`emulateMux: stream ${id} buffered ${s.queuedBytes} bytes past maxStreamBuffer=${maxStreamBuffer} without being drained`);
235
+ sendFrame(id, TYPE_ERROR, encodeError(err));
236
+ teardownStream(s, err);
237
+ return;
238
+ }
197
239
  const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
198
240
  s.pushIn(copy).then((handled) => {
241
+ s.queuedBytes -= copy.byteLength;
199
242
  if (handled && !s.closed && !muxClosed) sendFrame(id, TYPE_ACK);
200
243
  });
201
244
  return;
@@ -208,7 +251,9 @@ function emulateMux(channel, opts = {}) {
208
251
  return;
209
252
  }
210
253
  case TYPE_END:
254
+ s.inDone = true;
211
255
  s.doneIn();
256
+ releaseIfComplete(s);
212
257
  return;
213
258
  case TYPE_ERROR:
214
259
  teardownStream(s, decodeError(payload));
@@ -690,7 +735,7 @@ async function* encodeText(input) {
690
735
  }
691
736
  //#endregion
692
737
  //#region src/to-chunks.ts
693
- const DEFAULT_CHUNK_SIZE = 16 * 1024;
738
+ const DEFAULT_CHUNK_SIZE = 16384;
694
739
  /**
695
740
  * Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
696
741
  * chunks no larger than `size` bytes. Empty input chunks are skipped; small
@@ -720,3 +765,5 @@ function toChunks(size = DEFAULT_CHUNK_SIZE) {
720
765
  }
721
766
  //#endregion
722
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/collect.ts","../src/errors.ts","../src/emulate-mux.ts","../src/lines.ts","../src/map.ts","../src/jsonl.ts","../src/new-async-generator.ts","../src/normalize.ts","../src/readable-streams.ts","../src/recieve-iterator.ts","../src/send-iterator.ts","../src/text.ts","../src/to-chunks.ts"],"sourcesContent":["/** Collect all items from an async iterable into an array. */\nexport async function collect<T>(input: AsyncIterable<T>): Promise<T[]> {\n const items: T[] = [];\n for await (const item of input) items.push(item);\n return items;\n}\n\n/** Concatenate all Uint8Array chunks into a single Uint8Array. */\nexport async function collectBytes(input: AsyncIterable<Uint8Array>): Promise<Uint8Array> {\n const chunks: Uint8Array[] = [];\n let total = 0;\n for await (const chunk of input) {\n chunks.push(chunk);\n total += chunk.length;\n }\n if (chunks.length === 1) return chunks[0] ?? new Uint8Array(0);\n const result = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n return result;\n}\n\n/** Concatenate all string chunks into a single string. */\nexport async function collectString(input: AsyncIterable<string>): Promise<string> {\n let result = \"\";\n for await (const chunk of input) result += chunk;\n return result;\n}\n","export interface SerializedError {\n message: string;\n stack?: string;\n [key: string]: unknown;\n}\n\nexport function serializeError(error: unknown): SerializedError {\n if (error instanceof Error) {\n const out: SerializedError = { message: error.message, stack: error.stack };\n const bag = error as unknown as Record<string, unknown>;\n for (const key of Object.keys(bag)) out[key] = bag[key];\n return out;\n }\n if (typeof error === \"object\" && error !== null) {\n const bag = error as Record<string, unknown>;\n return { message: String(bag.message ?? error), ...bag };\n }\n return { message: String(error) };\n}\n\nexport function deserializeError(error: SerializedError | string): Error {\n const payload = typeof error === \"string\" ? { message: error } : error;\n return Object.assign(new Error(payload.message), payload);\n}\n","import { deserializeError, serializeError } from \"./errors.js\";\n\n/**\n * Canonical seam for the webrun-streams transport family. A `Duplex` carries\n * one logical call: caller emits an iterable of bytes as input, peer yields an\n * async generator of bytes as output. Same shape on both sides — an in-process\n * test can wire `const caller = handler` and run without any transport.\n *\n * Iterator semantics carry every signal:\n * - Consumer `.return()` on the output → producer's `finally` runs.\n * - Producer `throw` → consumer's `for await` throws.\n * - Normal exhaustion on either side → matching end on the other side.\n */\nexport type Duplex = (\n input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,\n) => AsyncGenerator<Uint8Array>;\n\n/**\n * Adapter-side factory that stands up a transport connection and yields a\n * caller `Duplex`. One `Connect` invocation owns one transport; each call\n * to the resolved `call` opens a new sub-stream on it.\n *\n * A caller must either drain the returned generator or `.return()` it.\n * Dropping the reference without doing either emits no observable signal:\n * the abandoned consumer never acknowledges inbound data, so the peer's\n * outbound pump blocks awaiting that acknowledgement, no end-of-stream is\n * ever exchanged, and both peers hold the stream's slot open. There is no\n * way for the transport to detect this — an unreferenced generator is not\n * observable — so it is the consumer's obligation.\n */\nexport type Connect<P> = (params: P) => Promise<{\n call: Duplex;\n close: () => Promise<void>;\n}>;\n\n/**\n * Adapter-side factory that registers a handler `Duplex` against a transport.\n * Returns an idempotent teardown.\n */\nexport type Serve<P> = (params: P, handler: Duplex) => Promise<() => Promise<void>>;\n\n/**\n * The minimum a transport must expose to be wrapped by `emulateMux`. Inbound\n * bytes are surfaced as an async iterable; outbound is imperative `send`. All\n * five message-oriented transports (WebSocket, LiveKit data channel, PeerJS\n * DataConnection, MessagePort, in-process pipe) match this shape after a thin\n * wrapper.\n */\nexport type ByteChannel = {\n send(bytes: Uint8Array): void;\n recv: AsyncIterable<Uint8Array>;\n closed: Promise<void>;\n close(): void;\n};\n\n/**\n * Thrown by `emulateMux` and adapters when the underlying transport closes\n * while one or more `Duplex` calls are in flight. Consumers can catch by\n * `instanceof TransportClosedError` or by checking `error.name`.\n */\nexport class TransportClosedError extends Error {\n override readonly name = \"TransportClosedError\";\n constructor(message = \"transport closed\") {\n super(message);\n }\n}\n\nconst TYPE_OPEN = 0x01;\nconst TYPE_DATA = 0x02;\nconst TYPE_ACK = 0x03;\nconst TYPE_END = 0x04;\nconst TYPE_ERROR = 0x05;\nconst TYPE_CLOSE = 0x06;\n\nconst DEFAULT_MAX_STREAMS = 256;\nconst DEFAULT_MTU = 64 * 1024;\nconst DEFAULT_MAX_STREAM_BUFFER = 8 * 1024 * 1024;\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\ninterface Stream {\n id: number;\n inbound: AsyncGenerator<Uint8Array>;\n pushIn: (chunk: Uint8Array) => Promise<boolean>;\n doneIn: (err?: Error) => Promise<boolean>;\n resolveAck: (() => void) | null;\n rejectAck: ((err: Error) => void) | null;\n closed: boolean;\n /** Peer sent END (or the stream was torn down): nothing more arrives. */\n inDone: boolean;\n /** Our outbound pump sent END: nothing more will be sent. */\n outDone: boolean;\n /** Inbound bytes pushed but not yet taken by the consumer. */\n queuedBytes: number;\n}\n\nexport interface EmulateMuxOptions {\n maxStreams?: number;\n mtu?: number;\n /**\n * Cap on inbound bytes one stream may hold for a consumer that has not\n * drained them. Exceeding it tears down that stream alone.\n */\n maxStreamBuffer?: number;\n /**\n * Stream-id allocation side. Initiator uses even ids (2, 4, …); responder\n * uses odd ids (1, 3, …). Pick one per peer so allocations don't collide.\n */\n side?: \"initiator\" | \"responder\";\n}\n\nexport function emulateMux(\n channel: ByteChannel,\n opts: EmulateMuxOptions = {},\n): {\n call: Duplex;\n serve: (handler: Duplex) => () => Promise<void>;\n close: () => Promise<void>;\n} {\n const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;\n const mtu = opts.mtu ?? DEFAULT_MTU;\n const maxStreamBuffer = opts.maxStreamBuffer ?? DEFAULT_MAX_STREAM_BUFFER;\n const side = opts.side ?? \"initiator\";\n\n const streams = new Map<number, Stream>();\n let nextLocalId = side === \"initiator\" ? 2 : 1;\n let handler: Duplex | null = null;\n let muxClosed = false;\n\n const sendFrame = (id: number, type: number, payload?: Uint8Array): void => {\n if (muxClosed) return;\n const idEnc = encodeVarint(id);\n const total = idEnc.length + 1 + (payload?.byteLength ?? 0);\n const frame = new Uint8Array(total);\n frame.set(idEnc, 0);\n frame[idEnc.length] = type;\n if (payload && payload.byteLength > 0) frame.set(payload, idEnc.length + 1);\n try {\n channel.send(frame);\n } catch {\n /* underlying transport closed; inbound loop will detect */\n }\n };\n\n const teardownStream = (s: Stream, err?: Error): void => {\n if (s.closed) return;\n s.closed = true;\n const resolve = s.resolveAck;\n const reject = s.rejectAck;\n s.resolveAck = null;\n s.rejectAck = null;\n if (reject && err) reject(err);\n else resolve?.();\n void s.doneIn(err);\n streams.delete(s.id);\n };\n\n /**\n * Graceful counterpart to `teardownStream`. A stream occupies a slot in\n * `streams` until BOTH directions finish; releasing on inbound END alone\n * would set `closed` while our own `pumpOutbound` is still sending, and it\n * checks that flag to decide whether to keep going.\n *\n * Without this, only abnormal endings (cancel, ERROR, CLOSE, transport\n * failure) ever freed a slot, so every normally-completed call leaked one\n * until `maxStreams` began rejecting new calls.\n */\n const releaseIfComplete = (s: Stream): void => {\n if (s.closed || !s.inDone || !s.outDone) return;\n s.closed = true;\n streams.delete(s.id);\n };\n\n const failAll = (err: Error): void => {\n if (muxClosed) return;\n muxClosed = true;\n for (const s of [...streams.values()]) teardownStream(s, err);\n streams.clear();\n };\n\n const createStream = (id: number): Stream => {\n const queue = makeInboundQueue();\n const state: Stream = {\n id,\n inbound: queue.generator(() => {\n if (!state.closed && !muxClosed) sendFrame(state.id, TYPE_CLOSE);\n teardownStream(state);\n }),\n pushIn: queue.push,\n doneIn: queue.done,\n resolveAck: null,\n rejectAck: null,\n closed: false,\n inDone: false,\n outDone: false,\n queuedBytes: 0,\n };\n return state;\n };\n\n const pumpOutbound = async (\n s: Stream,\n input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,\n ): Promise<void> => {\n try {\n for await (const chunk of input) {\n if (s.closed || muxClosed) return;\n if (chunk.byteLength === 0) continue;\n let off = 0;\n while (off < chunk.byteLength) {\n if (s.closed || muxClosed) return;\n const end = Math.min(off + mtu, chunk.byteLength);\n const piece = chunk.subarray(off, end);\n sendFrame(s.id, TYPE_DATA, piece);\n await new Promise<void>((resolve, reject) => {\n s.resolveAck = resolve;\n s.rejectAck = reject;\n });\n off = end;\n }\n }\n if (!s.closed && !muxClosed) {\n sendFrame(s.id, TYPE_END);\n s.outDone = true;\n releaseIfComplete(s);\n }\n } catch (err) {\n if (!s.closed && !muxClosed) {\n const e = err instanceof Error ? err : new Error(String(err));\n sendFrame(s.id, TYPE_ERROR, encodeError(e));\n teardownStream(s, e);\n }\n }\n };\n\n const runHandler = async (s: Stream, h: Duplex): Promise<void> => {\n let outbound: AsyncGenerator<Uint8Array>;\n try {\n outbound = h(s.inbound);\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n sendFrame(s.id, TYPE_ERROR, encodeError(e));\n teardownStream(s, e);\n return;\n }\n await pumpOutbound(s, outbound);\n // The response is complete. If the handler never consumed the request\n // body, nothing will ever drain the inbound queue, so no ACK is sent, the\n // caller's pump parks forever awaiting one, and neither peer frees the\n // slot — while the caller's `for await` completes normally, so nothing\n // looks wrong from user code. Tell the peer we no longer want the body.\n //\n // This is why a handler must consume `input` within its generator's\n // lifetime: draining it from a detached task is indistinguishable, from\n // here, from not draining it at all.\n if (!s.inDone && !s.closed && !muxClosed) {\n sendFrame(s.id, TYPE_CLOSE);\n teardownStream(s);\n }\n };\n\n const handleFrame = (frame: Uint8Array): void => {\n if (muxClosed) return;\n if (frame.byteLength < 2) return;\n\n // The id is the one field parsed from untrusted bytes before we know which\n // stream a frame belongs to, and decodeVarint throws on a truncated or\n // over-long encoding. Left unguarded that exception escapes handleFrame,\n // reaches the inbound loop's catch, and calls failAll — so a single\n // malformed frame tears down every stream on the connection.\n //\n // A ByteChannel is message-oriented, so frames are discrete and a corrupt\n // one cannot desync the next. Dropping it is therefore safe, and is what\n // keeps one bad frame from becoming a denial of service against every\n // healthy stream sharing the mux.\n let id: number;\n let offset: number;\n try {\n ({ value: id, offset } = decodeVarint(frame, 0));\n } catch {\n return;\n }\n if (offset >= frame.byteLength) return; // id consumed the whole frame: no type byte\n const type = frame[offset];\n const payload = frame.subarray(offset + 1);\n\n if (type === TYPE_OPEN) {\n // A duplicate OPEN for a LIVE stream is ignored. A replay of one that\n // already finished is deliberately not guarded: every transport here is\n // ordered and reliable, so a replay cannot occur by accident, and a\n // hostile peer gains nothing by it — opening a fresh id invokes the same\n // handler just as well. An earlier monotonic high-water mark did guard\n // it, at the cost of letting one OPEN with a high id permanently refuse\n // every later stream, including that peer's own legitimate traffic.\n if (streams.has(id)) return;\n if (streams.size >= maxStreams) {\n sendFrame(\n id,\n TYPE_ERROR,\n encodeError(new RangeError(`emulateMux: maxStreams=${maxStreams} exceeded`)),\n );\n return;\n }\n if (!handler) {\n sendFrame(id, TYPE_ERROR, encodeError(new Error(\"emulateMux: no handler registered\")));\n return;\n }\n const s = createStream(id);\n streams.set(id, s);\n void runHandler(s, handler);\n return;\n }\n\n const s = streams.get(id);\n if (!s) return;\n\n switch (type) {\n case TYPE_DATA: {\n // Flow control is the peer holding one in-flight DATA per stream and\n // waiting for its ACK — voluntary, and a hostile peer simply does not.\n // Pushes are fire-and-forget by necessity (see the comment below), so\n // nothing else bounds this queue: a peer that floods DATA at a handler\n // which has not started draining retains every payload. Refuse past a\n // per-stream cap and tear down THAT stream, never the whole mux.\n s.queuedBytes += payload.byteLength;\n if (s.queuedBytes > maxStreamBuffer) {\n const err = new RangeError(\n `emulateMux: stream ${id} buffered ${s.queuedBytes} bytes past maxStreamBuffer=${maxStreamBuffer} without being drained`,\n );\n sendFrame(id, TYPE_ERROR, encodeError(err));\n teardownStream(s, err);\n return;\n }\n const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);\n // Push fire-and-forget; ACK after the consumer drains. The inbound\n // loop must NOT block on consumer drainage — peer holds one in-flight\n // DATA per stream and waits for ACK, so blocking here causes a\n // cross-direction deadlock where ACK frames can't be processed.\n void s.pushIn(copy).then((handled) => {\n s.queuedBytes -= copy.byteLength;\n if (handled && !s.closed && !muxClosed) sendFrame(id, TYPE_ACK);\n });\n return;\n }\n case TYPE_ACK: {\n const r = s.resolveAck;\n s.resolveAck = null;\n s.rejectAck = null;\n r?.();\n return;\n }\n case TYPE_END: {\n s.inDone = true;\n void s.doneIn();\n releaseIfComplete(s);\n return;\n }\n case TYPE_ERROR: {\n teardownStream(s, decodeError(payload));\n return;\n }\n case TYPE_CLOSE: {\n teardownStream(s);\n return;\n }\n default:\n return;\n }\n };\n\n // Inbound consumer\n void (async () => {\n try {\n for await (const frame of channel.recv) {\n if (muxClosed) break;\n handleFrame(frame);\n }\n } catch (err) {\n failAll(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n failAll(new TransportClosedError());\n })();\n\n void channel.closed.then(() => failAll(new TransportClosedError())).catch(() => {});\n\n const call: Duplex = (input) => {\n if (muxClosed) return failedGenerator(new TransportClosedError());\n if (streams.size >= maxStreams) {\n return failedGenerator(new RangeError(`emulateMux: maxStreams=${maxStreams} exceeded`));\n }\n const id = nextLocalId;\n nextLocalId += 2;\n const s = createStream(id);\n streams.set(id, s);\n sendFrame(id, TYPE_OPEN);\n void pumpOutbound(s, input);\n return s.inbound;\n };\n\n const serve = (h: Duplex): (() => Promise<void>) => {\n handler = h;\n let torn = false;\n return async () => {\n if (torn) return;\n torn = true;\n if (handler === h) handler = null;\n };\n };\n\n const close = async (): Promise<void> => {\n if (muxClosed) return;\n failAll(new TransportClosedError());\n try {\n channel.close();\n } catch {\n /* ignore */\n }\n };\n\n return { call, serve, close };\n}\n\nfunction failedGenerator(err: Error): AsyncGenerator<Uint8Array> {\n return (async function* () {\n if ((0 as number) === 0) throw err;\n yield new Uint8Array(0);\n })();\n}\n\n/**\n * Push/pull queue with eager push/done handles. Unlike `newAsyncGenerator`, the\n * push and done functions are usable *before* the consumer begins iterating —\n * `emulateMux` needs to enqueue frames from inbound traffic regardless of when\n * (or whether) the consumer pulls them.\n *\n * `onCancel` fires only when the consumer terminates the generator before\n * `done()` was called — i.e., a unilateral cancellation. If `done()` is\n * called first (peer END/ERROR/CLOSE), the generator ends naturally and\n * `onCancel` does not fire.\n */\nfunction makeInboundQueue(): {\n generator: (onCancel: () => void) => AsyncGenerator<Uint8Array>;\n push: (chunk: Uint8Array) => Promise<boolean>;\n done: (err?: Error) => Promise<boolean>;\n} {\n type Slot =\n | { type: \"value\"; value: Uint8Array; resolve: (v: boolean) => void }\n | { type: \"done\"; err?: Error; resolve: (v: boolean) => void };\n const slots: Slot[] = [];\n let wake: (() => void) | null = null;\n let queueClosed = false;\n let doneCalled = false;\n\n const push = (chunk: Uint8Array): Promise<boolean> => {\n if (queueClosed) return Promise.resolve(false);\n return new Promise<boolean>((resolve) => {\n slots.push({ type: \"value\", value: chunk, resolve });\n wake?.();\n });\n };\n\n const done = (err?: Error): Promise<boolean> => {\n if (queueClosed || doneCalled) return Promise.resolve(false);\n doneCalled = true;\n return new Promise<boolean>((resolve) => {\n slots.push({ type: \"done\", err, resolve });\n wake?.();\n });\n };\n\n async function* generator(onCancel: () => void): AsyncGenerator<Uint8Array> {\n try {\n while (true) {\n if (slots.length === 0) {\n await new Promise<void>((r) => {\n wake = r;\n });\n wake = null;\n continue;\n }\n const slot = slots.shift() as Slot;\n if (slot.type === \"done\") {\n slot.resolve(true);\n if (slot.err) throw slot.err;\n return;\n }\n yield slot.value;\n slot.resolve(true);\n }\n } finally {\n queueClosed = true;\n for (const s of slots) s.resolve(false);\n slots.length = 0;\n if (!doneCalled) onCancel();\n }\n }\n\n return { generator, push, done };\n}\n\nfunction encodeVarint(value: number): Uint8Array {\n if (!Number.isInteger(value) || value < 0) {\n throw new RangeError(`encodeVarint: ${value} is not a non-negative integer`);\n }\n const out: number[] = [];\n let v = value;\n while (v >= 0x80) {\n out.push((v & 0x7f) | 0x80);\n v >>>= 7;\n }\n out.push(v & 0x7f);\n return new Uint8Array(out);\n}\n\nfunction decodeVarint(buf: Uint8Array, start: number): { value: number; offset: number } {\n let value = 0;\n let shift = 0;\n let i = start;\n while (i < buf.length) {\n const b = buf[i++]!; // i < buf.length, checked by the while condition, so this index exists\n value |= (b & 0x7f) << shift;\n if ((b & 0x80) === 0) return { value: value >>> 0, offset: i };\n shift += 7;\n if (shift > 28) throw new Error(\"decodeVarint: too long\");\n }\n throw new Error(\"decodeVarint: truncated\");\n}\n\nfunction encodeError(err: Error): Uint8Array {\n return textEncoder.encode(JSON.stringify(serializeError(err)));\n}\n\nfunction decodeError(buf: Uint8Array): Error {\n if (buf.byteLength === 0) return new Error(\"unknown stream error\");\n try {\n return deserializeError(JSON.parse(textDecoder.decode(buf)));\n } catch {\n return new Error(textDecoder.decode(buf));\n }\n}\n","/** Split a stream of string chunks into individual lines (delimited by \\n). */\nexport async function* splitLines(input: AsyncIterable<string>): AsyncGenerator<string> {\n let buffer = \"\";\n for await (const chunk of input) {\n buffer += chunk;\n let idx = buffer.indexOf(\"\\n\");\n while (idx !== -1) {\n yield buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n idx = buffer.indexOf(\"\\n\");\n }\n }\n if (buffer) yield buffer;\n}\n\n/** Append \\n to each string in the stream. */\nexport async function* joinLines(input: AsyncIterable<string>): AsyncGenerator<string> {\n for await (const line of input) {\n yield `${line}\\n`;\n }\n}\n","/** Apply a sync or async function to each item in a stream. */\nexport async function* map<I, O>(\n input: AsyncIterable<I>,\n fn: (item: I) => O | Promise<O>,\n): AsyncGenerator<O> {\n for await (const item of input) {\n yield await fn(item);\n }\n}\n","import { splitLines } from \"./lines.js\";\nimport { map } from \"./map.js\";\n\n/** Encode each value as a JSON line (terminated by \\n). */\nexport async function* encodeJsonl<T>(input: AsyncIterable<T>): AsyncGenerator<string> {\n yield* map(input, (item) => `${JSON.stringify(item)}\\n`);\n}\n\n/** Decode each line as a JSON value. Skips empty lines. */\nexport async function* decodeJsonl<T>(input: AsyncIterable<string>): AsyncGenerator<T> {\n for await (const line of splitLines(input)) {\n const trimmed = line.trim();\n if (trimmed) yield JSON.parse(trimmed) as T;\n }\n}\n","/**\n * The newAsyncGenerator function creates async generators from callback-based initialization\n * functions, providing a bridge between imperative event handling and declarative async\n * iteration patterns.\n *\n * The initialization function receives two callback functions that control the generator's\n * behavior. The `next` function yields values to consumers, while the `done` function\n * signals completion or error conditions. The initialization function can return a cleanup\n * function that will be called when the generator is closed or an error occurs.\n *\n * The generator manages an internal queue of values and completion signals, ensuring that\n * producers can yield values without overwhelming consumers. Proper backpressure is implemented\n * by having the `next` and `done` functions return promises that resolve only after the\n * consumer has processed the values. This prevents memory leaks and ensures that producers\n * are aware of whether their values were successfully handled.\n * The generator also handles cleanup by draining any remaining items in the queue and notifying\n * producers that their values were not processed if the generator is closed early. This ensures\n * that resources are properly managed and that no memory leaks occur.\n *\n * Example usage:\n * ```typescript\n * const asyncGen = newAsyncGenerator<number>((next, done) => {\n * let count = 0;\n * const interval = setInterval(() => {\n * if (count < 5) {\n * next(count++);\n * } else {\n * done();\n * clearInterval(interval);\n * }\n * }, 1000);\n * return () => clearInterval(interval); // Cleanup function\n * });\n * (async () => {\n * for await (const num of asyncGen) {\n * console.log(num); // Logs numbers 0 to 4 at 1 second intervals\n * }\n * console.log(\"Completed\");\n * })();\n * ```\n * @template T The type of values yielded by the generator\n * @template E The type of errors that can be thrown; defaults to Error\n * @param init Initialization function that sets up the generator behavior with next/done callbacks;\n * The first parameter - `next` function - is used to yield values to consumers;\n * It returns a Promise<boolean> indicating whether the value was successfully handled;\n * The second parameter - `done` function - is used to signal completion or error;\n * It returns a Promise<boolean> indicating whether the completion was successfully handled;\n * The initialization function can optionally return a cleanup function that will be called\n * when the generator is closed or an error occurs;\n * @param skipValues If true, only the most recent value is kept in the queue,\n * skipping intermediate values (not consumed values are considered skipped);\n * This is useful for scenarios where only the latest value matters, such as UI updates.\n * Defaults to false, meaning all values are queued and processed in order.\n * @returns AsyncGenerator that properly manages backpressure and resource cleanup\n */\nexport async function* newAsyncGenerator<T, E = Error>(\n /**\n * Initialization function that sets up the async generator behavior.\n * @param next - Function to yield values to consumers. Returns a Promise<boolean>\n * indicating whether the value was successfully handled.\n * @param done - Function to signal completion or error. Optional error parameter\n * will cause the generator to throw that error.\n * @returns Optional cleanup function that will be called when the generator terminates.\n */\n init: (\n next: (value: T) => Promise<boolean>,\n done: (err?: E) => Promise<boolean>,\n ) => void | (() => void | Promise<void>),\n\n /**\n * Skipping queue implementation that maintains only the most recent value.\n */\n skipValues = false,\n): AsyncGenerator<T> {\n /**\n * Internal queue slot type that wraps values and completion signals with resolution callbacks.\n * This enables the async generator to communicate back to producers whether their values\n * were successfully processed, enabling backpressure management.\n */\n type IterationSlot<T, E> =\n | { done: false; value: T } // Regular value slot\n | { done: true; error?: E }; // Completion/error slot\n type QueueSlot<T, E> = IterationSlot<T, E> & {\n next: QueueSlot<T, E> | undefined; // Pointer to the next slot in the queue\n resolve: (handled: boolean) => void; // Callback to signal if the value was handled\n };\n\n let head: QueueSlot<T, E> | undefined; // Head of the queue\n let tail: QueueSlot<T, E> | undefined; // Tail of the queue\n\n /** Flag to prevent new values from being queued after generator closes */\n let closed = false;\n\n /** Wake-up function to notify the generator loop of new items */\n let wakeUp: undefined | (() => void);\n\n /**\n * Drains the internal queue, notifying all pending producers that their values\n * were not processed due to generator closure. This prevents memory leaks and\n * ensures proper backpressure signaling.\n */\n const drainQueue = () => {\n for (; head; head = head.next) {\n closed = closed || head.done;\n head.resolve(false); // Notify producer that value was not handled\n }\n tail = undefined;\n };\n\n /**\n * Enqueues a value or completion signal with a promise that resolves when the item\n * is processed. This enables backpressure by allowing producers to know when their\n * values have been consumed.\n */\n const enqueue = (params: IterationSlot<T, E>): Promise<boolean> => {\n if (skipValues) {\n // Remove any previous value slots\n drainQueue();\n }\n return !closed\n ? new Promise<boolean>((resolve) => {\n // Add the new slot to the queue\n const next = { ...params, next: undefined, resolve };\n if (tail) {\n tail.next = next;\n }\n tail = next;\n if (!head) {\n head = tail;\n }\n // Notify the generator loop that a new item is available\n wakeUp?.();\n })\n : // If the generator is closed, immediately resolve as not handled\n Promise.resolve(false);\n };\n\n /**\n * Producer function to yield a value to consumers. Returns a promise that resolves\n * to true if the value was successfully processed, false if the generator is closed\n * or the value was skipped due to backpressure.\n */\n const next = (value: T): Promise<boolean> => enqueue({ done: false, value });\n\n /**\n * Producer function to signal completion or error. Optional error parameter will\n * cause the generator to throw that error to consumers.\n */\n const done = (error?: E): Promise<boolean> => enqueue({ done: true, error });\n\n // Initialize the producer by calling the init function with our control functions\n const unsubscribe = init(next, done);\n\n try {\n // Main async generator loop - processes queued items and yields values\n while (!closed) {\n // Try to get the next item from the queue\n if (!head) {\n // If no items available, wait for producers to add something\n await new Promise<void>((resolve) => {\n wakeUp = resolve;\n }).then(() => {\n wakeUp = undefined;\n });\n continue;\n }\n\n // Process the next item in the queue\n const slot = head;\n // Move head to the next item\n head = head.next;\n // If we removed the tail, clear it as well\n if (tail === slot) {\n tail = head;\n }\n try {\n // Handle completion/error signals\n if (slot.done) {\n closed = true;\n if (slot.error !== undefined) {\n throw slot.error;\n }\n break;\n }\n // Yield the value to the consumer\n yield slot.value;\n } finally {\n // Signal successful processing\n slot.resolve(true);\n }\n }\n } finally {\n /**\n * Cleanup phase - ensures proper resource management and notification of any\n * remaining producers. This runs whether the generator completes normally,\n * encounters an error, or is closed early by the consumer.\n */\n closed = true;\n\n // Wake up any pending operations to allow them to exit\n wakeUp?.();\n\n // Call the cleanup function returned by the initialization function\n if (typeof unsubscribe === \"function\") {\n await unsubscribe();\n }\n\n /**\n * Drain any remaining items in the queue and notify their producers that\n * the values were not processed. This prevents memory leaks and ensures\n * proper backpressure signaling.\n */\n drainQueue();\n }\n}\n","const utf8 = new TextEncoder();\n\nexport type ByteLike = Uint8Array | ArrayBuffer | ArrayBufferView | Blob | string;\n\n/**\n * Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.\n * Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.\n * Throws `TypeError` for anything else.\n */\nexport function normalizeToUint8Array(data: ByteLike): Uint8Array | Promise<Uint8Array> {\n if (data instanceof Uint8Array) return data;\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (ArrayBuffer.isView(data)) {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n if (typeof Blob !== \"undefined\" && data instanceof Blob) {\n return data.arrayBuffer().then((buf) => new Uint8Array(buf));\n }\n if (typeof data === \"string\") return utf8.encode(data);\n throw new TypeError(\n `normalizeToUint8Array: unsupported input ${describe(data)}; expected Uint8Array, ArrayBuffer, ArrayBufferView, Blob, or string`,\n );\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n const t = typeof value;\n if (t !== \"object\") return t;\n const ctor = (value as { constructor?: { name?: string } }).constructor;\n return ctor?.name ?? \"object\";\n}\n","export function toReadableStream(it: AsyncIterator<Uint8Array>): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n let handled = false;\n try {\n while (true) {\n const slot = await it.next();\n if (!slot || slot.done) break;\n const value = (await slot.value) as Uint8Array;\n controller.enqueue(value);\n }\n } catch (error) {\n handled = true;\n controller.error(error);\n } finally {\n if (!handled) controller.close();\n }\n },\n });\n}\n\nexport async function* fromReadableStream(\n stream: ReadableStream<Uint8Array>,\n): AsyncGenerator<Uint8Array, void, unknown> {\n const reader = stream.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value !== undefined) yield value;\n }\n}\n","import { newAsyncGenerator } from \"./new-async-generator.js\";\nimport type { IteratorChunk } from \"./send-iterator.js\";\n\nexport type ChunkReceiver<T> = (chunk?: IteratorChunk<T>) => Promise<boolean>;\nexport type ReceiverInstaller<T> = (\n deliver: ChunkReceiver<T>,\n) => (() => void | Promise<void>) | undefined | void;\n\n/**\n * Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`\n * chunks (delivered to the supplied callback by `installer`) into an async\n * generator.\n *\n * The `installer` is given a `deliver` function. It should call `deliver`\n * once for each incoming chunk and may return a cleanup callback that\n * runs when the consumer stops iterating.\n */\nexport function recieveIterator<T>(installer: ReceiverInstaller<T>): AsyncGenerator<T> {\n return newAsyncGenerator<T>((next, done) => {\n const cleanup = installer(async (chunk = { done: true }) => {\n const { done: isDone = true, value, error } = chunk;\n if (error) return await done(error as Error);\n if (isDone) return await done();\n return await next(value as T);\n });\n if (cleanup) {\n return async () => {\n await cleanup();\n };\n }\n return undefined;\n });\n}\n","export interface IteratorChunk<T> {\n done: boolean;\n value?: T;\n error?: unknown;\n}\n\nexport type ChunkSender<T> = (chunk: IteratorChunk<T>) => void | Promise<void>;\n\n/**\n * Drain an async iterator into a sink that consumes one chunk at a time.\n *\n * Each yielded value becomes `{ done: false, value }`. Completion emits\n * `{ done: true }`. If the iterator throws, the error is caught and the\n * final `done` chunk carries it so the peer can rethrow on its side.\n */\nexport async function sendIterator<T>(\n send: ChunkSender<T>,\n it: AsyncIterable<T> | Iterable<T>,\n): Promise<void> {\n let error: unknown;\n try {\n for await (const value of it as AsyncIterable<T>) {\n await send({ done: false, value });\n }\n } catch (err) {\n error = err;\n } finally {\n await send({ done: true, error });\n }\n}\n","/** Decode Uint8Array chunks to string chunks via TextDecoder, handling split multi-byte characters. */\nexport async function* decodeText(input: AsyncIterable<Uint8Array>): AsyncGenerator<string> {\n const decoder = new TextDecoder(\"utf-8\", { fatal: false });\n for await (const chunk of input) {\n const text = decoder.decode(chunk, { stream: true });\n if (text) yield text;\n }\n const tail = decoder.decode();\n if (tail) yield tail;\n}\n\n/** Encode string chunks to Uint8Array chunks via TextEncoder. */\nexport async function* encodeText(input: AsyncIterable<string>): AsyncGenerator<Uint8Array> {\n const encoder = new TextEncoder();\n for await (const str of input) {\n yield encoder.encode(str);\n }\n}\n","const DEFAULT_CHUNK_SIZE = 16 * 1024;\n\n/**\n * Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into\n * chunks no larger than `size` bytes. Empty input chunks are skipped; small\n * input chunks pass through unchanged (zero-copy `subarray` views are used\n * when splitting, so no allocation per chunk).\n *\n * Use to respect transport MTUs before reaching `channel.send`. Reassembly on\n * the receive side is not needed — consumers iterate a continuous byte stream.\n */\nexport function toChunks(\n size: number = DEFAULT_CHUNK_SIZE,\n): (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array> {\n if (!(Number.isInteger(size) && size > 0)) {\n throw new RangeError(`toChunks: size must be a positive integer, got ${size}`);\n }\n return async function* (input) {\n for await (const block of input) {\n if (block.byteLength === 0) continue;\n if (block.byteLength <= size) {\n yield block;\n continue;\n }\n let offset = 0;\n while (offset < block.byteLength) {\n const end = Math.min(offset + size, block.byteLength);\n yield block.subarray(offset, end);\n offset = end;\n }\n }\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-streams",
3
- "version": "0.1.0",
3
+ "version": "0.1.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.",
@@ -23,11 +23,11 @@
23
23
  "src"
24
24
  ],
25
25
  "devDependencies": {
26
- "@types/node": "^25.6.0",
26
+ "@types/node": "^26.2.0",
27
27
  "rimraf": "^6.1.3",
28
- "tsdown": "^0.21.9",
29
- "typescript": "^6.0.3",
30
- "vitest": "^4.1.4"
28
+ "tsdown": "^0.22.14",
29
+ "typescript": "^7.0.2",
30
+ "vitest": "^4.1.10"
31
31
  },
32
32
  "sideEffects": false,
33
33
  "publishConfig": {
@@ -19,6 +19,14 @@ export type Duplex = (
19
19
  * Adapter-side factory that stands up a transport connection and yields a
20
20
  * caller `Duplex`. One `Connect` invocation owns one transport; each call
21
21
  * to the resolved `call` opens a new sub-stream on it.
22
+ *
23
+ * A caller must either drain the returned generator or `.return()` it.
24
+ * Dropping the reference without doing either emits no observable signal:
25
+ * the abandoned consumer never acknowledges inbound data, so the peer's
26
+ * outbound pump blocks awaiting that acknowledgement, no end-of-stream is
27
+ * ever exchanged, and both peers hold the stream's slot open. There is no
28
+ * way for the transport to detect this — an unreferenced generator is not
29
+ * observable — so it is the consumer's obligation.
22
30
  */
23
31
  export type Connect<P> = (params: P) => Promise<{
24
32
  call: Duplex;
@@ -66,6 +74,7 @@ const TYPE_CLOSE = 0x06;
66
74
 
67
75
  const DEFAULT_MAX_STREAMS = 256;
68
76
  const DEFAULT_MTU = 64 * 1024;
77
+ const DEFAULT_MAX_STREAM_BUFFER = 8 * 1024 * 1024;
69
78
 
70
79
  const textEncoder = new TextEncoder();
71
80
  const textDecoder = new TextDecoder();
@@ -78,11 +87,22 @@ interface Stream {
78
87
  resolveAck: (() => void) | null;
79
88
  rejectAck: ((err: Error) => void) | null;
80
89
  closed: boolean;
90
+ /** Peer sent END (or the stream was torn down): nothing more arrives. */
91
+ inDone: boolean;
92
+ /** Our outbound pump sent END: nothing more will be sent. */
93
+ outDone: boolean;
94
+ /** Inbound bytes pushed but not yet taken by the consumer. */
95
+ queuedBytes: number;
81
96
  }
82
97
 
83
98
  export interface EmulateMuxOptions {
84
99
  maxStreams?: number;
85
100
  mtu?: number;
101
+ /**
102
+ * Cap on inbound bytes one stream may hold for a consumer that has not
103
+ * drained them. Exceeding it tears down that stream alone.
104
+ */
105
+ maxStreamBuffer?: number;
86
106
  /**
87
107
  * Stream-id allocation side. Initiator uses even ids (2, 4, …); responder
88
108
  * uses odd ids (1, 3, …). Pick one per peer so allocations don't collide.
@@ -100,6 +120,7 @@ export function emulateMux(
100
120
  } {
101
121
  const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
102
122
  const mtu = opts.mtu ?? DEFAULT_MTU;
123
+ const maxStreamBuffer = opts.maxStreamBuffer ?? DEFAULT_MAX_STREAM_BUFFER;
103
124
  const side = opts.side ?? "initiator";
104
125
 
105
126
  const streams = new Map<number, Stream>();
@@ -135,6 +156,22 @@ export function emulateMux(
135
156
  streams.delete(s.id);
136
157
  };
137
158
 
159
+ /**
160
+ * Graceful counterpart to `teardownStream`. A stream occupies a slot in
161
+ * `streams` until BOTH directions finish; releasing on inbound END alone
162
+ * would set `closed` while our own `pumpOutbound` is still sending, and it
163
+ * checks that flag to decide whether to keep going.
164
+ *
165
+ * Without this, only abnormal endings (cancel, ERROR, CLOSE, transport
166
+ * failure) ever freed a slot, so every normally-completed call leaked one
167
+ * until `maxStreams` began rejecting new calls.
168
+ */
169
+ const releaseIfComplete = (s: Stream): void => {
170
+ if (s.closed || !s.inDone || !s.outDone) return;
171
+ s.closed = true;
172
+ streams.delete(s.id);
173
+ };
174
+
138
175
  const failAll = (err: Error): void => {
139
176
  if (muxClosed) return;
140
177
  muxClosed = true;
@@ -155,6 +192,9 @@ export function emulateMux(
155
192
  resolveAck: null,
156
193
  rejectAck: null,
157
194
  closed: false,
195
+ inDone: false,
196
+ outDone: false,
197
+ queuedBytes: 0,
158
198
  };
159
199
  return state;
160
200
  };
@@ -180,7 +220,11 @@ export function emulateMux(
180
220
  off = end;
181
221
  }
182
222
  }
183
- if (!s.closed && !muxClosed) sendFrame(s.id, TYPE_END);
223
+ if (!s.closed && !muxClosed) {
224
+ sendFrame(s.id, TYPE_END);
225
+ s.outDone = true;
226
+ releaseIfComplete(s);
227
+ }
184
228
  } catch (err) {
185
229
  if (!s.closed && !muxClosed) {
186
230
  const e = err instanceof Error ? err : new Error(String(err));
@@ -201,16 +245,54 @@ export function emulateMux(
201
245
  return;
202
246
  }
203
247
  await pumpOutbound(s, outbound);
248
+ // The response is complete. If the handler never consumed the request
249
+ // body, nothing will ever drain the inbound queue, so no ACK is sent, the
250
+ // caller's pump parks forever awaiting one, and neither peer frees the
251
+ // slot — while the caller's `for await` completes normally, so nothing
252
+ // looks wrong from user code. Tell the peer we no longer want the body.
253
+ //
254
+ // This is why a handler must consume `input` within its generator's
255
+ // lifetime: draining it from a detached task is indistinguishable, from
256
+ // here, from not draining it at all.
257
+ if (!s.inDone && !s.closed && !muxClosed) {
258
+ sendFrame(s.id, TYPE_CLOSE);
259
+ teardownStream(s);
260
+ }
204
261
  };
205
262
 
206
263
  const handleFrame = (frame: Uint8Array): void => {
207
264
  if (muxClosed) return;
208
265
  if (frame.byteLength < 2) return;
209
- const { value: id, offset } = decodeVarint(frame, 0);
266
+
267
+ // The id is the one field parsed from untrusted bytes before we know which
268
+ // stream a frame belongs to, and decodeVarint throws on a truncated or
269
+ // over-long encoding. Left unguarded that exception escapes handleFrame,
270
+ // reaches the inbound loop's catch, and calls failAll — so a single
271
+ // malformed frame tears down every stream on the connection.
272
+ //
273
+ // A ByteChannel is message-oriented, so frames are discrete and a corrupt
274
+ // one cannot desync the next. Dropping it is therefore safe, and is what
275
+ // keeps one bad frame from becoming a denial of service against every
276
+ // healthy stream sharing the mux.
277
+ let id: number;
278
+ let offset: number;
279
+ try {
280
+ ({ value: id, offset } = decodeVarint(frame, 0));
281
+ } catch {
282
+ return;
283
+ }
284
+ if (offset >= frame.byteLength) return; // id consumed the whole frame: no type byte
210
285
  const type = frame[offset];
211
286
  const payload = frame.subarray(offset + 1);
212
287
 
213
288
  if (type === TYPE_OPEN) {
289
+ // A duplicate OPEN for a LIVE stream is ignored. A replay of one that
290
+ // already finished is deliberately not guarded: every transport here is
291
+ // ordered and reliable, so a replay cannot occur by accident, and a
292
+ // hostile peer gains nothing by it — opening a fresh id invokes the same
293
+ // handler just as well. An earlier monotonic high-water mark did guard
294
+ // it, at the cost of letting one OPEN with a high id permanently refuse
295
+ // every later stream, including that peer's own legitimate traffic.
214
296
  if (streams.has(id)) return;
215
297
  if (streams.size >= maxStreams) {
216
298
  sendFrame(
@@ -235,12 +317,28 @@ export function emulateMux(
235
317
 
236
318
  switch (type) {
237
319
  case TYPE_DATA: {
320
+ // Flow control is the peer holding one in-flight DATA per stream and
321
+ // waiting for its ACK — voluntary, and a hostile peer simply does not.
322
+ // Pushes are fire-and-forget by necessity (see the comment below), so
323
+ // nothing else bounds this queue: a peer that floods DATA at a handler
324
+ // which has not started draining retains every payload. Refuse past a
325
+ // per-stream cap and tear down THAT stream, never the whole mux.
326
+ s.queuedBytes += payload.byteLength;
327
+ if (s.queuedBytes > maxStreamBuffer) {
328
+ const err = new RangeError(
329
+ `emulateMux: stream ${id} buffered ${s.queuedBytes} bytes past maxStreamBuffer=${maxStreamBuffer} without being drained`,
330
+ );
331
+ sendFrame(id, TYPE_ERROR, encodeError(err));
332
+ teardownStream(s, err);
333
+ return;
334
+ }
238
335
  const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
239
336
  // Push fire-and-forget; ACK after the consumer drains. The inbound
240
337
  // loop must NOT block on consumer drainage — peer holds one in-flight
241
338
  // DATA per stream and waits for ACK, so blocking here causes a
242
339
  // cross-direction deadlock where ACK frames can't be processed.
243
340
  void s.pushIn(copy).then((handled) => {
341
+ s.queuedBytes -= copy.byteLength;
244
342
  if (handled && !s.closed && !muxClosed) sendFrame(id, TYPE_ACK);
245
343
  });
246
344
  return;
@@ -253,7 +351,9 @@ export function emulateMux(
253
351
  return;
254
352
  }
255
353
  case TYPE_END: {
354
+ s.inDone = true;
256
355
  void s.doneIn();
356
+ releaseIfComplete(s);
257
357
  return;
258
358
  }
259
359
  case TYPE_ERROR: {
@@ -419,7 +519,7 @@ function decodeVarint(buf: Uint8Array, start: number): { value: number; offset:
419
519
  let shift = 0;
420
520
  let i = start;
421
521
  while (i < buf.length) {
422
- const b = buf[i++];
522
+ const b = buf[i++]!; // i < buf.length, checked by the while condition, so this index exists
423
523
  value |= (b & 0x7f) << shift;
424
524
  if ((b & 0x80) === 0) return { value: value >>> 0, offset: i };
425
525
  shift += 7;