@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 +115 -10
- package/dist/index.d.mts +244 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +53 -6
- package/dist/index.mjs.map +1 -0
- package/package.json +5 -5
- package/src/emulate-mux.ts +103 -3
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.**
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
package/dist/index.d.mts
ADDED
|
@@ -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 =
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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": "^
|
|
26
|
+
"@types/node": "^26.2.0",
|
|
27
27
|
"rimraf": "^6.1.3",
|
|
28
|
-
"tsdown": "^0.
|
|
29
|
-
"typescript": "^
|
|
30
|
-
"vitest": "^4.1.
|
|
28
|
+
"tsdown": "^0.22.14",
|
|
29
|
+
"typescript": "^7.0.2",
|
|
30
|
+
"vitest": "^4.1.10"
|
|
31
31
|
},
|
|
32
32
|
"sideEffects": false,
|
|
33
33
|
"publishConfig": {
|
package/src/emulate-mux.ts
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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;
|