@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 +40 -152
- package/dist/duplex.d.ts +1 -1
- package/dist/flow-control.d.ts +7 -4
- package/dist/flow-control.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -400
- package/package.json +2 -2
- package/src/duplex.ts +1 -1
- package/src/flow-control.ts +7 -4
- package/src/index.ts +0 -1
- package/dist/emulate-mux.d.ts +0 -39
- package/dist/emulate-mux.d.ts.map +0 -1
- package/dist/uint32.d.ts +0 -25
- package/dist/uint32.d.ts.map +0 -1
- package/src/emulate-mux.ts +0 -557
- package/src/uint32.ts +0 -34
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
|
|
12
|
-
|
|
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;
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
| `
|
|
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>`, `
|
|
71
|
-
`
|
|
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
|
|
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
|
-
###
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
340
|
+
### Who implements the seam
|
|
453
341
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
`@statewalker/webrun-streams-
|
|
457
|
-
and `@statewalker/webrun-
|
|
458
|
-
(`@statewalker/webrun-streams-webrtc`,
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
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
|
*/
|
package/dist/flow-control.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|