@statewalker/webrun-streams 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +197 -13
  2. package/dist/collect.d.ts +7 -0
  3. package/dist/collect.d.ts.map +1 -0
  4. package/dist/duplex.d.ts +52 -0
  5. package/dist/duplex.d.ts.map +1 -0
  6. package/dist/emulate-mux.d.ts +39 -0
  7. package/dist/emulate-mux.d.ts.map +1 -0
  8. package/dist/errors.d.ts +8 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/flow-control.d.ts +71 -0
  11. package/dist/flow-control.d.ts.map +1 -0
  12. package/dist/index.d.ts +16 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/{index.mjs → index.js} +251 -56
  15. package/dist/jsonl.d.ts +5 -0
  16. package/dist/jsonl.d.ts.map +1 -0
  17. package/dist/lines.d.ts +5 -0
  18. package/dist/lines.d.ts.map +1 -0
  19. package/dist/map.d.ts +3 -0
  20. package/dist/map.d.ts.map +1 -0
  21. package/dist/new-async-generator.d.ts +70 -0
  22. package/dist/new-async-generator.d.ts.map +1 -0
  23. package/dist/normalize.d.ts +8 -0
  24. package/dist/normalize.d.ts.map +1 -0
  25. package/dist/readable-streams.d.ts +13 -0
  26. package/dist/readable-streams.d.ts.map +1 -0
  27. package/dist/recieve-iterator.d.ts +14 -0
  28. package/dist/recieve-iterator.d.ts.map +1 -0
  29. package/dist/send-iterator.d.ts +15 -0
  30. package/dist/send-iterator.d.ts.map +1 -0
  31. package/dist/text.d.ts +5 -0
  32. package/dist/text.d.ts.map +1 -0
  33. package/dist/to-chunks.d.ts +11 -0
  34. package/dist/to-chunks.d.ts.map +1 -0
  35. package/dist/uint32.d.ts +25 -0
  36. package/dist/uint32.d.ts.map +1 -0
  37. package/package.json +13 -8
  38. package/src/duplex.ts +59 -0
  39. package/src/emulate-mux.ts +188 -74
  40. package/src/flow-control.ts +146 -0
  41. package/src/index.ts +2 -0
  42. package/src/readable-streams.ts +49 -13
  43. package/src/uint32.ts +34 -0
  44. package/LICENSE +0 -21
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:
@@ -15,6 +17,16 @@ Every higher-level package in the `webrun-*` family (and its consumers — scann
15
17
 
16
18
  The MessagePack codec that previously rode along here is split out to [`@statewalker/webrun-msgpack`](../webrun-msgpack) so consumers that don't need framing don't pull in `@ygoe/msgpack`.
17
19
 
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install @statewalker/webrun-streams
24
+ ```
25
+
26
+ Zero runtime dependencies, zero peer dependencies. ESM only
27
+ (`"type": "module"`); runs in browsers, Node, Deno, Bun and Workers. This is the
28
+ foundation package — everything else in the workspace depends on it.
29
+
18
30
  ## How to use
19
31
 
20
32
  ```sh
@@ -35,6 +47,9 @@ npm install @statewalker/webrun-streams
35
47
  | `recieveIterator(installer)` | Inverse of `sendIterator`: wire an installer's chunk callback into a new `AsyncGenerator<T>`. |
36
48
  | `toReadableStream(it)` | Wrap an `AsyncIterator<Uint8Array>` in a `ReadableStream<Uint8Array>`. |
37
49
  | `fromReadableStream(stream)` | Iterate a `ReadableStream<Uint8Array>` as `AsyncGenerator<Uint8Array>`. |
50
+ | `emulateMux(channel, opts?)` | Multiplex many concurrent `Duplex` calls over one `ByteChannel`; returns `{ call, serve, close }`. |
51
+ | `normalizeToUint8Array(value)` | Coerce a `ByteLike` (string, `ArrayBuffer`, typed array, `Blob`) to `Uint8Array`. Synchronous except for a `Blob`, which returns a `Promise`. |
52
+ | `toChunks(size?)` | Curried: returns a transform that re-chunks an `AsyncIterable<Uint8Array>` into pieces of at most `size` bytes. `size` defaults to 16384. |
38
53
  | `serializeError(error)` | Turn an `Error` (or anything) into a plain `{message, stack, …}` object preserving subclass fields. |
39
54
  | `deserializeError(obj \| string)` | Reconstruct an `Error` from a serialised form, restoring extra fields. |
40
55
 
@@ -75,11 +90,18 @@ async function* chunks() {
75
90
  yield new Uint8Array([0x22, 0x3a, 0x31, 0x7d, 0x0a]);
76
91
  }
77
92
 
78
- const values = decodeJsonl<{ a: number }>(splitLines(decodeText(chunks())));
93
+ // `decodeJsonl` splits lines itself — do not wrap it in `splitLines`, or a
94
+ // stream carrying more than one value arrives as one concatenated line and
95
+ // `JSON.parse` throws.
96
+ const values = decodeJsonl<{ a: number }>(decodeText(chunks()));
79
97
  for await (const v of values) console.log(v); // { a: 1 }
80
98
 
81
- // inverse
82
- const jsonl = encodeText(joinLines(encodeJsonl([{ a: 1 }, { a: 2 }])));
99
+ // inverse. `encodeJsonl` already terminates each value with "\n", so
100
+ // `joinLines` here would emit a blank line between every record.
101
+ const jsonl = encodeText(encodeJsonl([{ a: 1 }, { a: 2 }]));
102
+
103
+ // `splitLines` / `joinLines` are for plain string streams, with no JSON involved:
104
+ for await (const line of splitLines(decodeText(byteStream))) console.log(line);
83
105
  ```
84
106
 
85
107
  ### Callback → AsyncGenerator bridge
@@ -107,19 +129,27 @@ for await (const n of tickEverySecond()) console.log(n); // 0 … 4
107
129
  ### Iterator chunk protocol
108
130
 
109
131
  ```ts
110
- import { sendIterator, recieveIterator } from "@statewalker/webrun-streams";
132
+ import { collect, recieveIterator, sendIterator } from "@statewalker/webrun-streams";
111
133
 
112
134
  // Drain an iterable across any transport.
113
135
  async function transport<T>(chunk: { done: boolean; value?: T; error?: unknown }) {
114
- // …send `chunk` over your channel.
136
+ await myChannel.send(chunk); // …however your channel sends
115
137
  }
116
- await sendIterator(transport, [1, 2, 3]);
117
138
 
118
139
  // On the other side, rebuild the original iterator.
119
140
  const iter = recieveIterator<number>((deliver) => {
120
141
  myChannel.onMessage = (chunk) => deliver(chunk);
121
142
  });
122
- for await (const v of iter) console.log(v); // 1, 2, 3
143
+
144
+ // Start consuming *before* (or concurrently with) draining the source.
145
+ // `deliver` resolves only once the consumer has dequeued the chunk — that is
146
+ // the backpressure — so awaiting `sendIterator` with nobody iterating `iter`
147
+ // deadlocks both sides.
148
+ const [, received] = await Promise.all([
149
+ sendIterator(transport, [1, 2, 3]),
150
+ collect(iter),
151
+ ]);
152
+ console.log(received); // [1, 2, 3]
123
153
  ```
124
154
 
125
155
  ### WHATWG streams ↔ async iterators
@@ -160,6 +190,158 @@ console.log(restored instanceof Error); // true
160
190
  console.log(restored.status); // 404
161
191
  ```
162
192
 
193
+ ## The `Duplex` seam
194
+
195
+ Everything in the `webrun-streams-*` family speaks one shape:
196
+
197
+ ```ts
198
+ type Duplex = (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array>;
199
+ ```
200
+
201
+ One `Duplex` invocation carries **one logical call**: the caller emits bytes,
202
+ the peer yields bytes back. Because both sides have the same shape, an
203
+ in-process test can wire `const caller = handler` and run with no transport at
204
+ all.
205
+
206
+ Iterator semantics carry every signal, which is why no separate close/abort API
207
+ exists:
208
+
209
+ | Signal | Mechanism |
210
+ | --- | --- |
211
+ | Consumer is done early | `.return()` on the output → producer's `finally` runs |
212
+ | Producer failed | `throw` → consumer's `for await` throws |
213
+ | Either side finished normally | Normal exhaustion → matching end on the other side |
214
+
215
+ `Connect<P>` and `Serve<P>` are the adapter-side factories that stand up a
216
+ transport and produce or register a `Duplex`.
217
+
218
+ > **Caller obligation.** Either drain the returned generator or `.return()` it.
219
+ > Dropping the reference without doing either emits no observable signal: the
220
+ > abandoned consumer never acknowledges inbound data, so the peer's outbound
221
+ > pump blocks awaiting that acknowledgement, no end-of-stream is exchanged, and
222
+ > both peers hold the stream open. An unreferenced generator is not observable,
223
+ > so no transport can detect this for you.
224
+
225
+ ## `emulateMux`
226
+
227
+ Turns one `ByteChannel` — anything with `send` / `recv` / `closed` / `close` —
228
+ into many concurrent `Duplex` calls. Used by the MessagePort, WebSocket,
229
+ LiveKit, PeerJS and signaling adapters; transports with native multiplexing
230
+ (libp2p) don't need it.
231
+
232
+ ```ts
233
+ import { emulateMux } from "@statewalker/webrun-streams";
234
+
235
+ const { call, serve, close } = emulateMux(channel, { side: "initiator" });
236
+
237
+ // caller side
238
+ const response = call([new TextEncoder().encode("ping")]);
239
+ for await (const chunk of response) { /* … */ }
240
+
241
+ // responder side
242
+ const stop = serve(async function* handler(input) {
243
+ for await (const chunk of input) yield chunk; // echo
244
+ });
245
+ ```
246
+
247
+ | Option | Default | Purpose |
248
+ | --- | --- | --- |
249
+ | `side` | `"initiator"` | Id allocation: initiator uses even ids, responder odd. Pick one per peer so they cannot collide. |
250
+ | `maxStreams` | `256` | Concurrent streams before new calls are refused. |
251
+ | `mtu` | `65536` | Largest payload per DATA frame; bigger chunks are split. |
252
+ | `maxStreamBuffer` | `8388608` | The credit this side advertises to the peer, in bytes, and the hard cap on inbound bytes one stream may hold undrained. A peer that honours credit never reaches the cap; one that ignores it has that stream torn down. |
253
+
254
+ ### Flow control
255
+
256
+ Receiver-advertised credit. Each side puts its `maxStreamBuffer` in the frame it
257
+ opens with — `OPEN` for the caller, the `ACK` answering it for the responder —
258
+ and the sender may only send what it has been granted. Both sides start at zero,
259
+ so a caller pays one round trip per stream before its first DATA frame and none
260
+ thereafter. The receiver grants more once its consumer has actually drained,
261
+ batched at half the window and flushed as soon as its queue empties. A sender
262
+ therefore cannot overrun the receiver's buffer, and `maxStreamBuffer` bounds only
263
+ peers that ignore the protocol.
264
+
265
+ Backpressure is **per-stream**, so a stalled stream does not block the others,
266
+ and it applies symmetrically in both directions.
267
+
268
+ There is deliberately **no stall timeout**: a peer that never acknowledges
269
+ blocks that producer indefinitely, exactly as a TCP receiver that never reads
270
+ blocks its sender. `maxStreams` and `maxStreamBuffer` bound what that can cost.
271
+ The bound is per stream and there is no mux-wide budget, so the worst case is
272
+ `maxStreams × maxStreamBuffer` — 2 GiB at the defaults, against 16 MiB under the
273
+ one-frame-in-flight rule this replaced. Lower `maxStreamBuffer` if that matters
274
+ more than throughput.
275
+
276
+ ### Behaviour on hostile input
277
+
278
+ A `ByteChannel` is message-oriented, so frames are discrete and a corrupt one
279
+ cannot desync the next. A frame that cannot be parsed is therefore **dropped**
280
+ rather than failing the connection — otherwise one malformed frame would tear
281
+ down every stream sharing the mux. A stream that exceeds `maxStreamBuffer` is
282
+ torn down on its own, with an error frame sent to the peer.
283
+
284
+ ## Exports
285
+
286
+ Everything is exported from the package root.
287
+
288
+ ### Seam types
289
+
290
+ | Export | Kind | Purpose |
291
+ | --- | --- | --- |
292
+ | `Duplex` | type | `(input) => AsyncGenerator<Uint8Array>` — one logical call. |
293
+ | `Connect<P>` | type | `(params) => Promise<{ call: Duplex; close() }>`. |
294
+ | `Serve<P>` | type | `(params, handler) => Promise<() => Promise<void>>`. |
295
+ | `ByteChannel` | type | `{ send, recv, closed, close }` — the minimum a transport must expose. |
296
+ | `ByteLike` | type | Accepted byte inputs before normalisation. |
297
+ | `emulateMux(channel, opts?)` | function | Multi-stream over a single `ByteChannel`. |
298
+ | `EmulateMuxOptions` | type | `maxStreams` (256), `mtu` (64 KiB), `maxStreamBuffer` (8 MiB), `side`. |
299
+ | `TransportClosedError` | class | Thrown when the transport closes with calls in flight. Catch via `instanceof` or `error.name`. |
300
+
301
+ The port multiplexing layer — `multiplexPort`, `PortMux`, `structuredCodec`,
302
+ `PortEnvelope` and the `MessageTarget` family of types — lives in
303
+ [`@statewalker/webrun-rpc`](../webrun-rpc#port-multiplexing), not here.
304
+
305
+ ### Credit
306
+
307
+ | Export | Kind | Purpose |
308
+ | --- | --- | --- |
309
+ | `newCreditLedger(initial?)` | function | Sender-side credit: `reserve(upTo)` waits for any credit and returns how much it got (`upTo` must be >= 1; less rejects with a `RangeError`), `grant(units)` releases waiters in order, `fail(err)` unwinds them. Starts at zero unless told otherwise. |
310
+ | `CreditLedger` | type | `{ available, reserve, grant, fail }`. |
311
+ | `newCreditGrantor(window, threshold?)` | function | Receiver-side: `consumed(units, queueEmpty)` returns the credit to hand back, batched at `threshold` (default half the window) and flushed once the queue empties. |
312
+ | `CreditGrantor` | type | `{ consumed(units, queueEmpty): number }`. |
313
+
314
+ The unit is whatever the caller counts. `emulateMux` counts bytes; a value-
315
+ oriented caller would count values. Nothing in this module interprets it.
316
+
317
+ ### Collectors and codecs
318
+
319
+ | Export | Purpose |
320
+ | --- | --- |
321
+ | `collect` / `collectBytes` / `collectString` | Drain an async iterable to an array / `Uint8Array` / `string`. |
322
+ | `encodeText` / `decodeText` | UTF-8 `string` ↔ `Uint8Array` streams. |
323
+ | `splitLines` / `joinLines` | Cross-chunk-safe line splitting and rejoining. |
324
+ | `encodeJsonl` / `decodeJsonl` | JSON values ↔ `\n`-delimited string stream. |
325
+ | `map` | Stream-map over an `AsyncIterable<T>`. |
326
+ | `toChunks` / `normalizeToUint8Array` | Coerce assorted byte-ish inputs into `Uint8Array` chunks. |
327
+
328
+ ### Iterator plumbing
329
+
330
+ | Export | Purpose |
331
+ | --- | --- |
332
+ | `newAsyncGenerator` | Backpressure-aware queue turning `next`/`done` callbacks into an async generator. |
333
+ | `sendIterator` / `recieveIterator` | Ship an async iterator across any transport. |
334
+ | `IteratorChunk` | type — the `{ done, value, error }` chunk envelope they exchange. |
335
+ | `ChunkSender` / `ChunkReceiver` / `ReceiverInstaller` | types — the transport-side callbacks those two are wired to. |
336
+ | `toReadableStream` / `fromReadableStream` | `AsyncIterator<Uint8Array>` ↔ WHATWG `ReadableStream<Uint8Array>`. |
337
+
338
+ ### Errors
339
+
340
+ | Export | Purpose |
341
+ | --- | --- |
342
+ | `serializeError` / `deserializeError` | Preserve `message`, `stack` and custom fields across JSON / structured-clone boundaries. |
343
+ | `SerializedError` | type — the wire shape those two produce and consume. |
344
+
163
345
  ## Internals
164
346
 
165
347
  ### `newAsyncGenerator` — backpressure queue
@@ -210,9 +392,11 @@ strict one-way converters: no queuing strategy tricks, no transform.
210
392
  - **British/American spelling kept.** `recieveIterator` uses the
211
393
  historical misspelling to stay wire-compatible with `webrun-ports`
212
394
  consumers.
213
- - **No tight coupling to any transport.** Nothing here mentions
214
- `MessagePort`, `fetch`, `Worker`, etc. Those belong to the consuming
215
- packages.
395
+ - **No tight coupling to any transport.** `ByteChannel` is the only
396
+ transport-facing type, and it is an interface — nothing here mentions
397
+ `MessagePort`, `WebSocket`, `fetch`, `Worker`, etc. Those belong to the
398
+ `webrun-streams-*` adapters, each of which supplies a `ByteChannel` and lets
399
+ `emulateMux` do the rest.
216
400
 
217
401
  ### Constraints
218
402
 
@@ -227,17 +411,17 @@ strict one-way converters: no queuing strategy tricks, no transform.
227
411
 
228
412
  **Zero runtime dependencies.**
229
413
 
230
- Dev: TypeScript, vitest, tsdown, rimraf, `@types/node`
414
+ Dev: TypeScript, vitest, rolldown, rimraf, `@types/node`
231
415
  (catalog versions from the monorepo root).
232
416
 
233
417
  ## Scripts
234
418
 
235
419
  ```sh
236
420
  pnpm test # vitest run
237
- pnpm run build # tsdown (publishes src + compiled dist)
421
+ pnpm run build # rolldown + tsc --emitDeclarationOnly (ships src + dist)
238
422
  pnpm lint # biome check
239
423
  ```
240
424
 
241
425
  ## License
242
426
 
243
- MIT © statewalker
427
+ MIT © statewalker — see [LICENSE](../../LICENSE).
@@ -0,0 +1,7 @@
1
+ /** Collect all items from an async iterable into an array. */
2
+ export declare function collect<T>(input: AsyncIterable<T>): Promise<T[]>;
3
+ /** Concatenate all Uint8Array chunks into a single Uint8Array. */
4
+ export declare function collectBytes(input: AsyncIterable<Uint8Array>): Promise<Uint8Array>;
5
+ /** Concatenate all string chunks into a single string. */
6
+ export declare function collectString(input: AsyncIterable<string>): Promise<string>;
7
+ //# sourceMappingURL=collect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,wBAAsB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAItE;AAED,kEAAkE;AAClE,wBAAsB,YAAY,CAAC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAexF;AAED,0DAA0D;AAC1D,wBAAsB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAIjF"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The transport seam every `webrun-streams-*` adapter and the conformance suite
3
+ * are written against.
4
+ *
5
+ * These types live here rather than beside an implementation on purpose: the
6
+ * emulated multiplexer that once declared them is scheduled for deletion, and a
7
+ * seam that disappears with its first implementation is not a seam.
8
+ */
9
+ /**
10
+ * Canonical seam for the webrun-streams transport family. A `Duplex` carries
11
+ * one logical call: caller emits an iterable of bytes as input, peer yields an
12
+ * async generator of bytes as output. Same shape on both sides — an in-process
13
+ * test can wire `const caller = handler` and run without any transport.
14
+ *
15
+ * Iterator semantics carry every signal:
16
+ * - Consumer `.return()` on the output → producer's `finally` runs.
17
+ * - Producer `throw` → consumer's `for await` throws.
18
+ * - Normal exhaustion on either side → matching end on the other side.
19
+ */
20
+ export type Duplex = (input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => AsyncGenerator<Uint8Array>;
21
+ /**
22
+ * Adapter-side factory that stands up a transport connection and yields a
23
+ * caller `Duplex`. One `Connect` invocation owns one transport; each call
24
+ * to the resolved `call` opens a new sub-stream on it.
25
+ *
26
+ * A caller must either drain the returned generator or `.return()` it.
27
+ * Dropping the reference without doing either emits no observable signal:
28
+ * the abandoned consumer never acknowledges inbound data, so the peer's
29
+ * outbound pump blocks awaiting that acknowledgement, no end-of-stream is
30
+ * ever exchanged, and both peers hold the stream's slot open. There is no
31
+ * way for the transport to detect this — an unreferenced generator is not
32
+ * observable — so it is the consumer's obligation.
33
+ */
34
+ export type Connect<P> = (params: P) => Promise<{
35
+ call: Duplex;
36
+ close: () => Promise<void>;
37
+ }>;
38
+ /**
39
+ * Adapter-side factory that registers a handler `Duplex` against a transport.
40
+ * Returns an idempotent teardown.
41
+ */
42
+ export type Serve<P> = (params: P, handler: Duplex) => Promise<() => Promise<void>>;
43
+ /**
44
+ * Thrown by `emulateMux` and adapters when the underlying transport closes
45
+ * while one or more `Duplex` calls are in flight. Consumers can catch by
46
+ * `instanceof TransportClosedError` or by checking `error.name`.
47
+ */
48
+ export declare class TransportClosedError extends Error {
49
+ readonly name = "TransportClosedError";
50
+ constructor(message?: string);
51
+ }
52
+ //# sourceMappingURL=duplex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplex.d.ts","sourceRoot":"","sources":["../src/duplex.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,MAAM,GAAG,CACnB,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,KACpD,cAAc,CAAC,UAAU,CAAC,CAAC;AAEhC;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAEpF;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,SAAkB,IAAI,0BAA0B;IAChD,YAAY,OAAO,SAAqB,EAEvC;CACF"}
@@ -0,0 +1,39 @@
1
+ import { type Duplex } from "./duplex.js";
2
+ /**
3
+ * The minimum a transport must expose to be wrapped by `emulateMux`. Inbound
4
+ * bytes are surfaced as an async iterable; outbound is imperative `send`. All
5
+ * five message-oriented transports (WebSocket, LiveKit data channel, PeerJS
6
+ * DataConnection, MessagePort, in-process pipe) match this shape after a thin
7
+ * wrapper.
8
+ */
9
+ export type ByteChannel = {
10
+ send(bytes: Uint8Array): void;
11
+ recv: AsyncIterable<Uint8Array>;
12
+ closed: Promise<void>;
13
+ close(): void;
14
+ };
15
+ export interface EmulateMuxOptions {
16
+ maxStreams?: number;
17
+ mtu?: number;
18
+ /**
19
+ * Cap on inbound bytes one stream may hold for a consumer that has not
20
+ * drained them. Exceeding it tears down that stream alone.
21
+ *
22
+ * It is also the credit this side advertises to the peer, so it must be at
23
+ * least 1 — a window of 0 authorises nothing and hangs the peer forever —
24
+ * and it travels in a uint32, so a value above `MAX_UINT32` (4 GiB - 1) is
25
+ * advertised as `MAX_UINT32`.
26
+ */
27
+ maxStreamBuffer?: number;
28
+ /**
29
+ * Stream-id allocation side. Initiator uses even ids (2, 4, …); responder
30
+ * uses odd ids (1, 3, …). Pick one per peer so allocations don't collide.
31
+ */
32
+ side?: "initiator" | "responder";
33
+ }
34
+ export declare function emulateMux(channel: ByteChannel, opts?: EmulateMuxOptions): {
35
+ call: Duplex;
36
+ serve: (handler: Duplex) => () => Promise<void>;
37
+ close: () => Promise<void>;
38
+ };
39
+ //# sourceMappingURL=emulate-mux.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emulate-mux.d.ts","sourceRoot":"","sources":["../src/emulate-mux.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAwB,MAAM,aAAa,CAAC;AAUhE;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AA4CF,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;CAClC;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,WAAW,EACpB,IAAI,GAAE,iBAAsB,GAC3B;IACD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAuVA"}
@@ -0,0 +1,8 @@
1
+ export interface SerializedError {
2
+ message: string;
3
+ stack?: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export declare function serializeError(error: unknown): SerializedError;
7
+ export declare function deserializeError(error: SerializedError | string): Error;
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAY9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,GAAG,KAAK,CAGvE"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Credit-based flow control, as a pair of pure state machines with no I/O.
3
+ *
4
+ * The sender holds a {@link CreditLedger}: it reserves credit before it puts
5
+ * anything on the wire, and stalls at zero. The receiver holds a
6
+ * {@link CreditGrantor}: it counts what its consumer has actually drained and
7
+ * says when to hand the sender more.
8
+ *
9
+ * A sender therefore cannot overrun a receiver's buffer, because it was never
10
+ * granted permission to. That is the property a sender-side window cannot
11
+ * offer: the receiver's capacity is not knowable to the sender unless the
12
+ * receiver states it.
13
+ *
14
+ * **The unit is opaque.** This module never interprets the numbers it counts.
15
+ * `emulateMux` passes byte counts and advertises `maxStreamBuffer`; the RPC
16
+ * tier passes 1 per value and advertises a maximum in-flight value count.
17
+ * Nothing here depends on which.
18
+ */
19
+ export interface CreditLedger {
20
+ /** Units authorised by the peer and not yet reserved. */
21
+ readonly available: number;
22
+ /**
23
+ * Reserve up to `upTo` units, resolving with how many were actually
24
+ * granted — at least 1, never more than `upTo`. The caller sends exactly
25
+ * that much and calls `reserve` again for the rest.
26
+ *
27
+ * `upTo` must itself be at least 1; anything less rejects with a
28
+ * `RangeError` rather than resolving with 0, which would be a silent no-op
29
+ * that also consumed a waiter slot.
30
+ *
31
+ * Returning a partial amount rather than waiting for the full request is
32
+ * what makes the ledger deadlock-free: a peer that advertises less than
33
+ * one `upTo` still makes progress, one short piece at a time.
34
+ *
35
+ * Rejects if {@link fail} is called.
36
+ */
37
+ reserve(upTo: number): Promise<number>;
38
+ /** The peer authorised `units` more. */
39
+ grant(units: number): void;
40
+ /** Reject every pending and future reservation — transport or stream is gone. */
41
+ fail(err: Error): void;
42
+ }
43
+ export declare function newCreditLedger(initial?: number): CreditLedger;
44
+ export interface CreditGrantor {
45
+ /**
46
+ * Record that the consumer drained `units`, and whether the receive queue
47
+ * is now empty. Returns the credit to hand back to the peer, or `0` to stay
48
+ * silent and keep accumulating.
49
+ */
50
+ consumed(units: number, queueEmpty: boolean): number;
51
+ }
52
+ /**
53
+ * Grants are batched: replenishing on every chunk while the receiver is
54
+ * behind would reinvent the per-frame ACK this change exists to remove.
55
+ * `threshold` is the fraction of the window that must drain before a grant is
56
+ * emitted.
57
+ *
58
+ * The batch is flushed unconditionally once the receive queue is empty, even
59
+ * below the threshold, so the receiver never sits on credit it owes. Note what
60
+ * this is and is not: paired with a {@link CreditLedger}, a sender blocks only
61
+ * at *exactly* zero credit, and at that point the receiver holds the entire
62
+ * window as `pending` — above any threshold at or below the whole window — so
63
+ * the threshold alone cannot deadlock that pairing. The flush is what keeps
64
+ * that from being an argument about a global accounting identity: it is
65
+ * locally decidable from one boolean, and it returns owed credit now rather
66
+ * than at the next threshold crossing. It costs an extra frame only when the
67
+ * consumer is keeping pace, which is exactly when the sender is not blocked
68
+ * and the frame is cheap.
69
+ */
70
+ export declare function newCreditGrantor(window: number, threshold?: number): CreditGrantor;
71
+ //# sourceMappingURL=flow-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flow-control.d.ts","sourceRoot":"","sources":["../src/flow-control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,wCAAwC;IACxC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iFAAiF;IACjF,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,CAAC;CACxB;AAQD,wBAAgB,eAAe,CAAC,OAAO,SAAI,GAAG,YAAY,CAqDzD;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,aAAa,CAa/E"}
@@ -0,0 +1,16 @@
1
+ export * from "./collect.js";
2
+ export * from "./duplex.js";
3
+ export * from "./emulate-mux.js";
4
+ export * from "./errors.js";
5
+ export * from "./flow-control.js";
6
+ export * from "./jsonl.js";
7
+ export * from "./lines.js";
8
+ export * from "./map.js";
9
+ export * from "./new-async-generator.js";
10
+ export * from "./normalize.js";
11
+ export * from "./readable-streams.js";
12
+ export * from "./recieve-iterator.js";
13
+ export * from "./send-iterator.js";
14
+ export * from "./text.js";
15
+ export * from "./to-chunks.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC"}