@statewalker/webrun-streams-webrtc 0.1.6 → 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 +80 -11
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/duplex-over-data-channel.d.ts +22 -1
- package/dist/duplex-over-data-channel.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +371 -36
- package/dist/port-mux.d.ts +74 -0
- package/dist/port-mux.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/connect-serve.ts +40 -11
- package/src/duplex-over-data-channel.ts +141 -9
- package/src/index.ts +11 -0
- package/src/port-mux.ts +288 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ between the two peers with no server in the data path.
|
|
|
12
12
|
## Why it exists
|
|
13
13
|
|
|
14
14
|
WebRTC can open as many data channels as needed, so concurrency comes from the
|
|
15
|
-
transport and this adapter
|
|
15
|
+
transport and this adapter needs no multiplexer over it. What an
|
|
16
16
|
`RTCDataChannel` lacks is what request/response needs:
|
|
17
17
|
|
|
18
18
|
1. **No half-close.** A channel is open or closed. There is no way to say "I
|
|
@@ -44,6 +44,8 @@ below.
|
|
|
44
44
|
| `serve({ pc }, handler)` | `Serve<WebRtcParams>`. Listens for `datachannel` events on `pc` and runs `handler` on each channel. Resolves to an idempotent teardown that stops accepting new channels; calls in flight continue. |
|
|
45
45
|
| `duplexOverDataChannel(dc, input)` | The primitive under both: runs one call over one data channel. Sends `input` framed, and returns an `AsyncGenerator<Uint8Array>` of the peer's bytes. |
|
|
46
46
|
| `WebRtcParams` | `{ pc: RTCPeerConnection }`. |
|
|
47
|
+
| `webrtcPortMux(options)` | Native MessagePorts: a `webrun-rpc` `PortMux` (by shape) whose every port is its own data channel (labelled `webrun-port/…`), for `overPorts`. `options`: `pc`, `codec` (a value codec: `{ encode: serialize, decode: deserialize }` from `webrun-msgpack`), `onPort(port)` (to accept ports), `maxMessageSize`, `maxFrameBytes`. See "MessagePorts over WebRTC" below. |
|
|
48
|
+
| `DEFAULT_PORT_MAX_FRAME_BYTES`, `DEFAULT_PORT_MAX_MESSAGE_SIZE`, `PORT_DRAIN_LOW_WATER`, `PORT_LABEL_PREFIX` | 16 KiB, 16 KiB − 256, 256 KiB, `"webrun-port/"`. |
|
|
47
49
|
|
|
48
50
|
## Examples
|
|
49
51
|
|
|
@@ -121,11 +123,43 @@ for await (const chunk of duplexOverDataChannel(dc, request())) {
|
|
|
121
123
|
| 1 byte | rest of the message |
|
|
122
124
|
+--------+---------------------------+
|
|
123
125
|
|
|
124
|
-
DATA
|
|
125
|
-
END
|
|
126
|
-
ERROR
|
|
126
|
+
DATA 0x00 body bytes, at most 16 KiB - 1 per message
|
|
127
|
+
END 0x01 this side has finished sending (half-close)
|
|
128
|
+
ERROR 0x02 JSON of serializeError(err): message, stack, custom fields
|
|
129
|
+
CREDIT 0x03 big-endian u32: payload bytes the receiver's consumer pulled
|
|
127
130
|
```
|
|
128
131
|
|
|
132
|
+
### Flow control
|
|
133
|
+
|
|
134
|
+
Two limits keep memory bounded whatever the speed of either side:
|
|
135
|
+
|
|
136
|
+
- **Send buffer.** Before each `DATA` message the sender waits while the
|
|
137
|
+
channel holds more than `HIGH_WATER_MARK` (1 MiB) unsent
|
|
138
|
+
(`bufferedAmount`), until `bufferedamountlow` fires at `LOW_WATER_MARK`
|
|
139
|
+
(256 KiB).
|
|
140
|
+
- **Receive window.** A sender may have at most `RECEIVE_WINDOW` (4 MiB) of
|
|
141
|
+
payload outstanding that the receiving consumer has not pulled. The receiver
|
|
142
|
+
returns credit with `CREDIT` frames as its consumer pulls, half a window at a
|
|
143
|
+
time. On the responder, the next chunk is pulled only once the handler took
|
|
144
|
+
the previous one, so a slow handler stalls the caller.
|
|
145
|
+
|
|
146
|
+
Both ends must speak `CREDIT`: a sender facing a peer that never returns
|
|
147
|
+
credit stops after the first window.
|
|
148
|
+
|
|
149
|
+
Each side keeps listening for `CREDIT` until both directions are done, so a
|
|
150
|
+
reply larger than the window, sent after the input ended, still flows. When
|
|
151
|
+
the responder's handler finishes (or throws) without reading all its input,
|
|
152
|
+
the rest of the caller's upload is read and dropped, so the caller's upload
|
|
153
|
+
finishes rather than waiting for credit. A caller that cancels, or gets an
|
|
154
|
+
error, stops its own upload and closes the channel at once.
|
|
155
|
+
|
|
156
|
+
A finished call's channel is closed by the caller, once it has the
|
|
157
|
+
responder's `END` or `ERROR`: a responder that closed right after sending them
|
|
158
|
+
could lose them in flight. Each side also holds the frames that arrive before
|
|
159
|
+
its stream starts listening, so a responder that answers at once (a handler
|
|
160
|
+
that throws, a reply with no input to wait for) is not faster than the
|
|
161
|
+
caller's listener.
|
|
162
|
+
|
|
129
163
|
```
|
|
130
164
|
caller responder
|
|
131
165
|
| createDataChannel("webrun/…") | datachannel event -> handler(input)
|
|
@@ -139,6 +173,42 @@ is at most 16 KiB, a size that every browser's SCTP implementation accepts. The
|
|
|
139
173
|
channel's `binaryType` is set to `"arraybuffer"`; text messages and empty
|
|
140
174
|
messages are ignored. The channel is closed once both directions have ended.
|
|
141
175
|
|
|
176
|
+
### MessagePorts over WebRTC, and the port stack on them
|
|
177
|
+
|
|
178
|
+
`connect` / `serve` above run their own framing and credit window per data
|
|
179
|
+
channel, so a consumer that stops reading lets its producer get up to 4 MiB
|
|
180
|
+
ahead (L6). `webrtcPortMux` gives `webrun-rpc`'s port stack a data channel per
|
|
181
|
+
port instead, and its stream tier keeps a producer within one confirmed chunk:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { deserialize, serialize } from "@statewalker/webrun-msgpack";
|
|
185
|
+
import { connect, overPorts, serve } from "@statewalker/webrun-rpc";
|
|
186
|
+
import { webrtcPortMux } from "@statewalker/webrun-streams-webrtc";
|
|
187
|
+
|
|
188
|
+
const codec = { encode: serialize, decode: deserialize };
|
|
189
|
+
await serve({ mux: overPorts((onPort) => webrtcPortMux({ pc, codec, onPort })) }, handler);
|
|
190
|
+
const { call } = await connect({ mux: overPorts(() => webrtcPortMux({ pc: other, codec })) });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Ports are ordered, reliable channels labelled `webrun-port/…`; a mux ignores
|
|
194
|
+
inbound channels without that label, and this package's `serve()` ignores
|
|
195
|
+
channels with it, so ports share a connection with `connect` / `serve` and with
|
|
196
|
+
anyone else's channels. On one connection, a mux without `onPort` (a caller's)
|
|
197
|
+
leaves inbound ports to the serving mux, so both directions can call; of
|
|
198
|
+
several serving muxes, the first takes each port. Each message is one
|
|
199
|
+
data channel message, so `maxMessageSize` defaults to 16 KiB − 256 (Firefox to
|
|
200
|
+
Chrome cuts off at 16 KiB; the port stack's framing needs the margin), and a
|
|
201
|
+
message past `maxFrameBytes` fails the port with a `RangeError` instead of
|
|
202
|
+
being dropped. Each port carries `closeSignal`, aborted when its channel closes
|
|
203
|
+
or errs, the connection becomes `failed` or `closed`, or the port is closed
|
|
204
|
+
here. A refused port (`onPort` returning `false`) is closed, so the opener's
|
|
205
|
+
call fails. Messages that arrive before a port's first listener are held for
|
|
206
|
+
it, delivered after that tick to every listener added by then. A connection
|
|
207
|
+
that has failed already makes `openPort` reject, as does an aborted `signal`.
|
|
208
|
+
`whenDrained()` waits on `bufferedamountlow` (256 KiB). The suite runs a
|
|
209
|
+
second conformance pair over these ports (`tests/port-mux.test.ts`), with L6 at
|
|
210
|
+
a one-chunk bound.
|
|
211
|
+
|
|
142
212
|
### How cancellation reaches the handler
|
|
143
213
|
|
|
144
214
|
`END` means "I am done sending", and the peer answers it by carrying on, so it
|
|
@@ -180,14 +250,13 @@ is caught, so it does not become an unhandled rejection.
|
|
|
180
250
|
|
|
181
251
|
### Constraints
|
|
182
252
|
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
buffer, which closes the channel when it overflows.
|
|
187
|
-
- Conformance runs in Chromium through Playwright: `pnpm run test:browser`.
|
|
253
|
+
- Conformance and the flow-control tests (100 MiB under the high-water mark;
|
|
254
|
+
a sleeping reader stalls the sender within the window) run in Chromium
|
|
255
|
+
through Playwright: `pnpm run test:browser`, also in CI.
|
|
188
256
|
Under `pnpm test` (Node) the suite is registered as skipped, because Node
|
|
189
|
-
has no `RTCPeerConnection`. L6
|
|
190
|
-
|
|
257
|
+
has no `RTCPeerConnection`. L6 asserts the per-channel window: a consumer
|
|
258
|
+
that stops reading lets the producer get at most `RECEIVE_WINDOW` (4 MiB)
|
|
259
|
+
ahead, measured at exactly that.
|
|
191
260
|
|
|
192
261
|
### Dependencies
|
|
193
262
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAI1E,MAAM,WAAW,YAAY;IAC3B,wGAAwG;IACxG,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,YAAY,CA0CzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,YAAY,CA0CrC,CAAC"}
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send-side backpressure: a sender waits while the DataChannel holds more than this many bytes
|
|
3
|
+
* unsent (`bufferedAmount`), until it drains to {@link LOW_WATER_MARK} (`bufferedamountlow`).
|
|
4
|
+
* Without it a fast producer fills the browser's send queue (Chrome fails `send` past 16 MiB).
|
|
5
|
+
*/
|
|
6
|
+
export declare const HIGH_WATER_MARK: number;
|
|
7
|
+
export declare const LOW_WATER_MARK: number;
|
|
8
|
+
/**
|
|
9
|
+
* Receive-side backpressure: a sender may have at most this many payload bytes outstanding that
|
|
10
|
+
* the receiving consumer has not yet pulled. The receiver returns credit (`CREDIT` frames) as its
|
|
11
|
+
* consumer pulls, half a window at a time. A DataChannel delivers every message as it arrives,
|
|
12
|
+
* whether or not anyone reads, so without credit a slow reader's queue grows without bound. The
|
|
13
|
+
* window is larger than the send buffer's high-water mark so a long round trip does not throttle a
|
|
14
|
+
* fast link: the buffer limit is what bounds the sender's memory.
|
|
15
|
+
*/
|
|
16
|
+
export declare const RECEIVE_WINDOW: number;
|
|
17
|
+
export declare function markAnswering(dc: RTCDataChannel): void;
|
|
18
|
+
/** starts holding a channel's events until {@link duplexOverDataChannel} takes them */
|
|
19
|
+
export declare function holdEvents(dc: RTCDataChannel): void;
|
|
1
20
|
/**
|
|
2
21
|
* Drive one `Duplex` over one `RTCDataChannel`. The DataChannel hosts a tiny
|
|
3
22
|
* in-band framing protocol:
|
|
@@ -6,7 +25,9 @@
|
|
|
6
25
|
*
|
|
7
26
|
* Types are `DATA` (0x00, body bytes), `END` (0x01, signals half-close — the
|
|
8
27
|
* sender is done emitting; the channel stays open for the other direction),
|
|
9
|
-
*
|
|
28
|
+
* `ERROR` (0x02, followed by a serialised `Error`) and `CREDIT` (0x03, a
|
|
29
|
+
* big-endian u32: payload bytes the receiver's consumer has pulled, which the
|
|
30
|
+
* sender may now send again; see {@link RECEIVE_WINDOW}).
|
|
10
31
|
*
|
|
11
32
|
* `RTCDataChannel` has no native half-close, so `END` is emitted by the
|
|
12
33
|
* sender once its input iterator exhausts; the channel is only physically
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duplex-over-data-channel.d.ts","sourceRoot":"","sources":["../src/duplex-over-data-channel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"duplex-over-data-channel.d.ts","sourceRoot":"","sources":["../src/duplex-over-data-channel.ts"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,QAAa,CAAC;AACzC;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,QAAkB,CAAC;AAoB9C,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,CAEtD;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,CAUnD;AAWD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAC/B,cAAc,CAAC,UAAU,CAAC,CAE5B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { connect, serve, type WebRtcParams } from "./connect-serve.js";
|
|
2
2
|
export { duplexOverDataChannel } from "./duplex-over-data-channel.js";
|
|
3
|
+
export { type DataChannelPort, DEFAULT_PORT_MAX_FRAME_BYTES, DEFAULT_PORT_MAX_MESSAGE_SIZE, PORT_DRAIN_LOW_WATER, PORT_LABEL_PREFIX, type ValueCodec, type WebRtcPortMux, type WebRtcPortMuxOptions, webrtcPortMux, } from "./port-mux.js";
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EACL,KAAK,eAAe,EACpB,4BAA4B,EAC5B,6BAA6B,EAC7B,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,aAAa,GACd,MAAM,eAAe,CAAC"}
|