@statewalker/webrun-streams-webrtc 0.1.5 → 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
@@ -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 does not use `emulateMux`. What an
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 0x00 body bytes, at most 16 KiB - 1 per message
125
- END 0x01 this side has finished sending (half-close)
126
- ERROR 0x02 JSON of serializeError(err): message, stack, custom fields
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
- - **No flow control in the adapter.** Outbound messages go to `dc.send` as
184
- fast as `input` yields; `bufferedAmount` is not checked, and the inbound
185
- queue is not bounded. Very large or fast bodies rely on the browser's send
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's small window does not apply here (there is
190
- no `emulateMux`), so L6 is an integrity check.
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;AAG1E,MAAM,WAAW,YAAY;IAC3B,wGAAwG;IACxG,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,YAAY,CAyCzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,YAAY,CA4BrC,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
- * and `ERROR` (0x02, followed by a serialised `Error`).
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":"AAiBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,GAC/B,cAAc,CAAC,UAAU,CAAC,CAE5B"}
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
@@ -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"}