@statewalker/webrun-streams 0.2.3 → 0.3.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.
package/README.md CHANGED
@@ -7,9 +7,10 @@ Async-iterator and `ReadableStream` primitives, plus the transport seam the
7
7
  (`collect`, `collectBytes`, `collectString`), text, line and JSONL codecs, a
8
8
  backpressure-aware callback-to-generator bridge, a chunk protocol for moving an
9
9
  iterator across any message channel, conversions between async iterators and
10
- WHATWG `ReadableStream<Uint8Array>`, serialisable errors, the `Duplex` /
11
- `Connect` / `Serve` types, and `emulateMux`, which multiplexes many concurrent
12
- `Duplex` calls over one message-oriented byte channel.
10
+ WHATWG `ReadableStream<Uint8Array>`, serialisable errors, and the `Duplex` /
11
+ `Connect` / `Serve` types with `TransportClosedError`. It implements no
12
+ transport and no multiplexer: those live in the adapters and in
13
+ `@statewalker/webrun-rpc`.
13
14
 
14
15
  ## Why it exists
15
16
 
@@ -31,9 +32,12 @@ the same small set of building blocks in every place:
31
32
  split/join/encode/decode.
32
33
  7. **One seam for transports.** Every adapter exposes the same `Duplex` shape,
33
34
  so code above the transport does not change when the transport does.
34
- Message-oriented transports have no streams of their own; `emulateMux`
35
- supplies them once, with credit-based flow control, instead of each adapter
36
- writing its own.
35
+ Message-oriented transports have no streams of their own; they get them
36
+ from `@statewalker/webrun-rpc`'s port stack (`overPipe` with
37
+ `msgpackCodec` from `@statewalker/webrun-msgpack`, then `webrun-rpc`'s
38
+ `connect` / `serve`), which resolves to these same types. This package
39
+ holds only the types, so the seam does not depend on any one
40
+ implementation of it.
37
41
 
38
42
  ## How to use
39
43
 
@@ -63,12 +67,11 @@ runs in browsers, workers and Node.
63
67
  | `normalizeToUint8Array(value)` | Coerce a `ByteLike` (`Uint8Array`, `ArrayBuffer`, `ArrayBufferView`, `Blob`, `string`) to `Uint8Array`. Synchronous, except a `Blob`, which returns a `Promise<Uint8Array>`. |
64
68
  | `toChunks(size?)` | Curried. Returns a transform that splits `Uint8Array` chunks into pieces of at most `size` bytes (default 16384). |
65
69
  | `serializeError(error)` / `deserializeError(obj \| string)` | `Error` (or anything) to a plain `SerializedError` and back, keeping own enumerable fields. |
66
- | `emulateMux(channel, opts?)` | Many concurrent `Duplex` calls over one `ByteChannel`. Returns `{ call, serve, close }`. |
67
- | `newCreditLedger(initial?)` / `newCreditGrantor(window, threshold?)` | The sender and receiver halves of credit-based flow control, as pure state machines. |
70
+ | `newCreditLedger(initial?)` / `newCreditGrantor(window, threshold?)` | **Dormant.** The sender and receiver halves of credit-based flow control, as pure state machines. Nothing in this repository uses them since `emulateMux` was removed (Plan C3); they stay exported and tested for per-stream windowing (spec D13). |
68
71
  | `TransportClosedError` | Error class raised when a transport closes with calls in flight. |
69
72
 
70
- Types: `Duplex`, `Connect<P>`, `Serve<P>`, `ByteChannel`, `ByteLike`,
71
- `EmulateMuxOptions`, `CreditLedger`, `CreditGrantor`, `IteratorChunk<T>`,
73
+ Types: `Duplex`, `Connect<P>`, `Serve<P>`, `ByteLike`, `CreditLedger`,
74
+ `CreditGrantor`, `IteratorChunk<T>`,
72
75
  `ChunkSender<T>`, `ChunkReceiver<T>`, `ReceiverInstaller<T>`,
73
76
  `SerializedError`.
74
77
 
@@ -98,7 +101,7 @@ API:
98
101
 
99
102
  **Caller obligation.** Either drain the returned generator or `.return()` it.
100
103
  Dropping the reference does neither: the consumer never acknowledges inbound
101
- data, the peer's outbound pump blocks waiting for credit, no end-of-stream is
104
+ data, the peer's outbound pump blocks waiting for that acknowledgement, no end-of-stream is
102
105
  exchanged, and both peers hold the stream open. An unreferenced generator is
103
106
  not observable, so no transport can detect this for you.
104
107
 
@@ -251,141 +254,26 @@ restored instanceof Error; // true (a plain Error, not NotFoundError)
251
254
  restored.status; // 404
252
255
  ```
253
256
 
254
- ### `emulateMux`
255
-
256
- ```ts
257
- import { type ByteChannel, collectBytes, emulateMux } from "@statewalker/webrun-streams";
258
-
259
- declare const clientChannel: ByteChannel; // e.g. one end of a WebSocket
260
- declare const serverChannel: ByteChannel; // the other end
261
-
262
- const client = emulateMux(clientChannel, { side: "initiator" });
263
- const server = emulateMux(serverChannel, { side: "responder" });
264
-
265
- const stop = server.serve(async function* echo(input) {
266
- for await (const chunk of input) yield chunk;
267
- });
268
-
269
- const response = client.call([new TextEncoder().encode("ping")]);
270
- new TextDecoder().decode(await collectBytes(response)); // "ping"
271
-
272
- await stop();
273
- await client.close();
274
- await server.close();
275
- ```
276
-
277
- | Option | Default | Purpose |
278
- | --- | --- | --- |
279
- | `side` | `"initiator"` | Stream-id allocation: initiator uses even ids (2, 4, …), responder odd ids (1, 3, …). Give the two peers different sides so ids cannot collide. |
280
- | `maxStreams` | `256` | Concurrent streams. Further calls, and further OPENs from the peer, are refused. |
281
- | `mtu` | `65536` | Largest payload per DATA frame. Larger chunks are split. |
282
- | `maxStreamBuffer` | `8388608` (8 MiB) | The credit this side advertises per stream, in bytes, and the hard cap on inbound bytes one stream may hold undrained. Must be at least 1. |
283
-
284
257
  ## Internals
285
258
 
286
- ### `emulateMux` frames
287
-
288
- A `ByteChannel` is `{ send(bytes), recv: AsyncIterable<Uint8Array>, closed: Promise<void>, close() }`
289
- and is message-oriented: each `send` arrives as one `recv` item. `emulateMux`
290
- puts one frame in each message:
291
-
292
- ```
293
- +----------------+--------+------------------+
294
- | stream id | type | payload |
295
- | varint, LEB128 | 1 byte | rest of message |
296
- +----------------+--------+------------------+
297
-
298
- OPEN 0x01 payload: uint32 BE credit the opener grants
299
- DATA 0x02 payload: bytes (at most mtu, at most the credit held)
300
- ACK 0x03 payload: uint32 BE credit granted (answer to OPEN, or more later)
301
- END 0x04 sender finished this direction
302
- ERROR 0x05 payload: JSON of serializeError(err); tears the stream down
303
- CLOSE 0x06 receiver no longer wants this stream; tears it down
304
- ```
305
-
306
- ```
307
- caller responder
308
- | OPEN id=2, credit=8 MiB ------------> | handler(input) starts
309
- | <---------------- ACK id=2, credit=8 MiB
310
- | DATA id=2 ... (spends credit) -------> |
311
- | <------------------- ACK id=2, credit=n (after the consumer drained)
312
- | END id=2 ---------------------------> |
313
- | <----------------------------- DATA / END
314
- ```
315
-
316
- ### Why credit flows from the receiver
317
-
318
- The sender cannot know how much the receiver can buffer unless the receiver
319
- says so. Each side therefore advertises its `maxStreamBuffer` in the frame it
320
- opens with (OPEN for the caller, the ACK that answers it for the responder),
321
- and a sender may only put on the wire what it has been granted. Both ledgers
322
- start at zero, so a caller pays one round trip per stream before its first DATA
323
- frame and none after that. A uint32 cannot carry more than `2^32 - 1`, so a
324
- larger window is advertised as `2^32 - 1` rather than wrapping (a wrapped
325
- `2^32` would advertise zero credit and hang the peer).
326
-
327
- The receiver grants more only for bytes its consumer actually took. Grants are
328
- batched until half the window has drained (`newCreditGrantor`'s default
329
- `threshold` of `0.5`) and flushed as soon as the receive queue is empty, so the
330
- receiver never sits on credit it owes. `reserve(upTo)` on the sender side
331
- resolves with whatever is available, at least 1 and at most `upTo`, and waiters
332
- are released strictly in order; a peer whose window is smaller than one `mtu`
333
- still makes progress, one short piece at a time.
334
-
335
- Backpressure is per stream: a stalled stream does not block the others, and it
336
- applies in both directions. Inbound frames are pushed to the stream's queue
337
- without waiting for the consumer, because the inbound loop must keep processing
338
- ACKs for this side's own senders; blocking it on a slow consumer would deadlock
339
- the two directions against each other.
340
-
341
- The unit is opaque to `newCreditLedger` and `newCreditGrantor`. `emulateMux`
342
- counts bytes; a value-oriented caller can count values.
343
-
344
- ### Why there is no stall timeout
345
-
346
- A peer that never drains blocks that stream's producer indefinitely, as a TCP
347
- receiver that never reads blocks its sender. `maxStreams` and `maxStreamBuffer`
348
- bound the cost. The bound is per stream and there is no mux-wide budget, so the
349
- worst case is `maxStreams × maxStreamBuffer`: 2 GiB at the defaults. Lower
350
- `maxStreamBuffer` if memory matters more than throughput.
351
-
352
- ### What a hostile or broken peer can and cannot do
353
-
354
- - A frame that cannot be parsed (truncated or over-long varint id, no type
355
- byte) is **dropped**. Frames are discrete messages, so a corrupt one cannot
356
- desync the next, and failing the connection would let one bad frame tear down
357
- every stream sharing it.
358
- - A peer that ignores credit and floods DATA has that one stream torn down when
359
- it holds more than `maxStreamBuffer` undrained bytes. The peer receives an
360
- ERROR frame: `emulateMux: stream <id> buffered <n> bytes past maxStreamBuffer=<max> without being drained`.
361
- - An OPEN for a stream id that is already live is ignored. An OPEN past the
362
- limit is answered with ERROR `emulateMux: maxStreams=<n> exceeded`; an OPEN
363
- with no handler registered, with ERROR `emulateMux: no handler registered`.
364
- - An ACK without a 4-byte credit payload is ignored rather than granting an
365
- arbitrary amount.
366
-
367
- ### How streams end
368
-
369
- A stream holds a slot until both directions finish (END sent and END
370
- received), so a normally completed call releases its slot. Cancellation
371
- releases it at once:
372
-
373
- - The caller calling `.return()` on the response sends CLOSE and returns the
374
- caller's input iterator, so the input's `finally` runs. The return is not
375
- awaited: `.return()` on a generator parked in its own `next()` is queued
376
- behind that `next()`, and awaiting it would hang teardown.
377
- - When a handler's output finishes before it has read all of its input, the
378
- mux sends CLOSE for the request body. A handler must therefore consume
379
- `input` within its generator's lifetime; reading it from a detached task
380
- looks the same as not reading it.
381
- - When the channel's `recv` ends or `closed` resolves, every live stream fails
382
- with `TransportClosedError` (message `transport closed`). `call` after
383
- `close()` returns a generator that throws the same error. `call` past
384
- `maxStreams` returns a generator that throws
385
- `RangeError: emulateMux: maxStreams=<n> exceeded`.
386
- - `emulateMux` throws `RangeError: emulateMux: maxStreamBuffer must be at least 1, got <n>`
387
- at construction, because a zero window authorises nothing and would stall the
388
- first call forever.
259
+ ### The credit pair is dormant
260
+
261
+ `newCreditLedger` and `newCreditGrantor` are credit-based flow control with no
262
+ I/O: the receiver states what it can buffer, and the sender may put on the wire
263
+ only what it has been granted. Their consumer, `emulateMux` (a byte-channel
264
+ multiplexer that lived in this package), was removed in Plan C3; the port stack
265
+ in `@statewalker/webrun-rpc` that replaced it keeps its own window. They stay
266
+ exported and tested as the mechanism for per-stream windowing (spec D13) rather
267
+ than be rewritten later.
268
+
269
+ Their behaviour, for when they are used: `reserve(upTo)` on the ledger resolves
270
+ with whatever is available, at least 1 and at most `upTo`, and waiters are
271
+ released strictly in order, so a peer whose window is smaller than one request
272
+ still makes progress, one short piece at a time. The grantor hands credit back
273
+ only for units the consumer actually took, batched until `threshold` (default
274
+ `0.5`) of the window has drained, and flushed as soon as the receive queue is
275
+ empty, so the receiver never sits on credit it owes. The unit is opaque: count
276
+ bytes or values.
389
277
 
390
278
  ### `newAsyncGenerator` is a backpressure queue
391
279
 
@@ -449,15 +337,15 @@ Both assume `Uint8Array` chunks.
449
337
  `RangeError: newCreditLedger: reserve(<upTo>) — upTo must be at least 1`;
450
338
  after `fail(err)`, every pending and future `reserve` rejects with `err`.
451
339
 
452
- ### Who uses `emulateMux`
340
+ ### Who implements the seam
453
341
 
454
- The message-oriented adapters supply a `ByteChannel` and let `emulateMux` do
455
- the rest: `@statewalker/webrun-streams-ws`, `@statewalker/webrun-streams-peerjs`,
456
- `@statewalker/webrun-streams-livekit`, `@statewalker/webrun-streams-signaling`
457
- and `@statewalker/webrun-http-streams`. Transports with native multiplexing
458
- (`@statewalker/webrun-streams-webrtc`, `@statewalker/webrun-streams-libp2p`) do
459
- not use it. Nothing in this package names a concrete transport; `ByteChannel`
460
- is the only transport-facing type.
342
+ `@statewalker/webrun-rpc`'s `connect` / `serve` implement `Connect` / `Serve`
343
+ over ports. Message-oriented transports supply only a pipe of bytes for its
344
+ port stack: `@statewalker/webrun-streams-ws`, `@statewalker/webrun-streams-peerjs`,
345
+ `@statewalker/webrun-streams-livekit` and `@statewalker/webrun-streams-signaling`.
346
+ Transports with native multiplexing (`@statewalker/webrun-streams-webrtc`,
347
+ `@statewalker/webrun-streams-libp2p`) implement the seam directly, one call per
348
+ native stream. Nothing in this package names a concrete transport.
461
349
 
462
350
  ### Dependencies
463
351
 
package/dist/duplex.d.ts CHANGED
@@ -41,7 +41,7 @@ export type Connect<P> = (params: P) => Promise<{
41
41
  */
42
42
  export type Serve<P> = (params: P, handler: Duplex) => Promise<() => Promise<void>>;
43
43
  /**
44
- * Thrown by `emulateMux` and adapters when the underlying transport closes
44
+ * Thrown by adapters when the underlying transport closes
45
45
  * while one or more `Duplex` calls are in flight. Consumers can catch by
46
46
  * `instanceof TransportClosedError` or by checking `error.name`.
47
47
  */
@@ -11,10 +11,13 @@
11
11
  * offer: the receiver's capacity is not knowable to the sender unless the
12
12
  * receiver states it.
13
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.
14
+ * **The unit is opaque.** This module never interprets the numbers it counts:
15
+ * bytes or values, it does not depend on which.
16
+ *
17
+ * **Dormant.** Its consumer, `emulateMux`, was removed in Plan C3, and
18
+ * `webrun-rpc`'s port stack keeps its own window. It stays exported and
19
+ * tested as the mechanism for per-stream windowing (spec D13) rather than be
20
+ * rewritten later.
18
21
  */
19
22
  export interface CreditLedger {
20
23
  /** Units authorised by the peer and not yet reserved. */
@@ -1 +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"}
1
+ {"version":3,"file":"flow-control.d.ts","sourceRoot":"","sources":["../src/flow-control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from "./collect.js";
2
2
  export * from "./duplex.js";
3
- export * from "./emulate-mux.js";
4
3
  export * from "./errors.js";
5
4
  export * from "./flow-control.js";
6
5
  export * from "./jsonl.js";
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,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"}