@statewalker/webrun-streams-webrtc 0.1.6 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-streams-webrtc",
3
- "version": "0.1.6",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "WebRTC DataChannel-backed Connect/Serve adapter (native multi-stream) in the webrun-streams-* family",
@@ -30,19 +30,21 @@
30
30
  "src"
31
31
  ],
32
32
  "dependencies": {
33
- "@statewalker/webrun-streams": "^0.2.3"
33
+ "@statewalker/webrun-streams": "^0.4.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@biomejs/biome": "^2.5.15",
37
37
  "@roamhq/wrtc": "^0.10.0",
38
38
  "@types/node": "^26.6.4",
39
- "@vitest/browser-playwright": "^5.0.3",
39
+ "@vitest/browser-playwright": "^5.0.5",
40
40
  "playwright": "^1.63.0",
41
41
  "rimraf": "^6.1.3",
42
42
  "rolldown": "^1.2.12",
43
43
  "typescript": "^7.0.2",
44
- "vitest": "^5.0.3",
45
- "@statewalker/webrun-streams-conformance": "^0.3.3"
44
+ "vitest": "^5.0.5",
45
+ "@statewalker/webrun-msgpack": "^0.11.0",
46
+ "@statewalker/webrun-rpc": "^0.12.0",
47
+ "@statewalker/webrun-streams-conformance": "^0.5.0"
46
48
  },
47
49
  "sideEffects": false,
48
50
  "publishConfig": {
@@ -53,6 +55,8 @@
53
55
  "build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
54
56
  "test": "vitest run",
55
57
  "test:browser": "vitest run --config vitest.browser.config.ts",
58
+ "typecheck": "tsc --noEmit",
59
+ "typecheck:tests": "tsc -p tsconfig.tests.json",
56
60
  "lint": "biome check src tests"
57
61
  }
58
62
  }
@@ -1,5 +1,11 @@
1
1
  import type { Connect, Duplex, Serve } from "@statewalker/webrun-streams";
2
- import { duplexOverDataChannel } from "./duplex-over-data-channel.js";
2
+ import {
3
+ duplexOverDataChannel,
4
+ holdEvents,
5
+ markAnswering,
6
+ runStream,
7
+ } from "./duplex-over-data-channel.js";
8
+ import { PORT_LABEL_PREFIX } from "./port-mux.js";
3
9
 
4
10
  export interface WebRtcParams {
5
11
  /** Open `RTCPeerConnection`. Signalling, auth, and connection setup are the caller's responsibility. */
@@ -15,12 +21,10 @@ export const connect: Connect<WebRtcParams> = async ({ pc }) => {
15
21
  let counter = 0;
16
22
  let closed = false;
17
23
  const open = new Set<RTCDataChannel>();
18
- const onConnectionClose = (): void => {
19
- closed = true;
24
+ const onStateChange = (): void => {
25
+ if (pc.connectionState === "closed" || pc.connectionState === "failed") closed = true;
20
26
  };
21
- pc.addEventListener("connectionstatechange", () => {
22
- if (pc.connectionState === "closed" || pc.connectionState === "failed") onConnectionClose();
23
- });
27
+ pc.addEventListener("connectionstatechange", onStateChange);
24
28
 
25
29
  const call: Duplex = (input) => {
26
30
  if (closed) {
@@ -31,6 +35,7 @@ export const connect: Connect<WebRtcParams> = async ({ pc }) => {
31
35
  }
32
36
  const label = `webrun/${Date.now().toString(36)}/${(counter++).toString(36)}`;
33
37
  const dc = pc.createDataChannel(label);
38
+ holdEvents(dc); // the responder may answer before our stream starts listening
34
39
  open.add(dc);
35
40
  dc.addEventListener("close", () => open.delete(dc));
36
41
  return (async function* () {
@@ -43,6 +48,7 @@ export const connect: Connect<WebRtcParams> = async ({ pc }) => {
43
48
  call,
44
49
  async close() {
45
50
  closed = true;
51
+ pc.removeEventListener("connectionstatechange", onStateChange);
46
52
  for (const dc of open) {
47
53
  try {
48
54
  dc.close();
@@ -62,22 +68,41 @@ export const connect: Connect<WebRtcParams> = async ({ pc }) => {
62
68
  export const serve: Serve<WebRtcParams> = async ({ pc }, handler: Duplex) => {
63
69
  const onChannel = (ev: RTCDataChannelEvent): void => {
64
70
  const dc = ev.channel;
71
+ if (dc.label.startsWith(PORT_LABEL_PREFIX)) return; // a port's channel: `webrtcPortMux`'s
72
+ holdEvents(dc);
73
+ markAnswering(dc); // the caller closes the channel once the call is over
65
74
  let inputFromPeer: PeekInput | null = null;
66
75
  void (async () => {
67
76
  await waitForOpen(dc);
68
- inputFromPeer = peekInput(dc);
69
- const out = handler(inputFromPeer.input);
70
- for await (const chunk of duplexOverDataChannel(dc, out)) {
71
- inputFromPeer.deliver(chunk);
77
+ const peek = peekInput(dc);
78
+ inputFromPeer = peek;
79
+ // Once the handler's output ends (or throws) the handler is done with its input: what the
80
+ // peer still sends is dropped, so its upload finishes instead of waiting for credit.
81
+ const handlerOutput = handler(peek.input);
82
+ const out = (async function* () {
83
+ try {
84
+ yield* handlerOutput;
85
+ } finally {
86
+ peek.release();
87
+ }
88
+ })();
89
+ // The next chunk is pulled (and its credit returned) only once the handler took this one.
90
+ // When the inbound side fails (the caller cancelled and closed the channel, or its input
91
+ // threw), the handler's input ends with that error before the stream waits for its outbound
92
+ // pump: the pump may be waiting for the handler's output, the handler for its input.
93
+ const inbound = runStream(dc, out, { onInboundFailed: (err) => peek.done(err) });
94
+ for await (const chunk of inbound) {
95
+ await peek.deliver(chunk);
72
96
  }
73
- inputFromPeer.done();
74
- })().catch(() => {
97
+ peek.done();
98
+ })().catch((err: unknown) => {
75
99
  // A caller that cancels mid-response closes the DataChannel, which
76
100
  // surfaces here as TransportClosedError. That is the cancellation
77
101
  // working, not a fault: this task is fire-and-forget, so without a
78
- // catch it becomes an unhandled rejection that fails the page. Closing
79
- // the handler's input is what runs the handler's own `finally`.
80
- inputFromPeer?.done();
102
+ // catch it becomes an unhandled rejection that fails the page. Ending
103
+ // the handler's input (with the error, so a handler does not take a
104
+ // truncated upload for a complete one) is what runs its own `finally`.
105
+ inputFromPeer?.done(err instanceof Error ? err : new Error(String(err)));
81
106
  });
82
107
  };
83
108
  pc.addEventListener("datachannel", onChannel);
@@ -137,8 +162,12 @@ function toAsyncIterable(
137
162
 
138
163
  interface PeekInput {
139
164
  input: AsyncGenerator<Uint8Array>;
140
- deliver(chunk: Uint8Array): void;
141
- done(): void;
165
+ /** resolves once the handler has taken the chunk (or stopped reading) */
166
+ deliver(chunk: Uint8Array): Promise<void>;
167
+ /** ends the input after the chunks already delivered; with `err`, the handler's read throws it */
168
+ done(err?: Error): void;
169
+ /** the handler is finished: pending and later chunks are dropped */
170
+ release(): void;
142
171
  }
143
172
 
144
173
  /**
@@ -152,9 +181,12 @@ interface PeekInput {
152
181
  * the handler's input queue.
153
182
  */
154
183
  function peekInput(_dc: RTCDataChannel): PeekInput {
155
- const slots: Array<{ type: "value"; value: Uint8Array } | { type: "done" }> = [];
184
+ const slots: Array<
185
+ { type: "value"; value: Uint8Array; taken: () => void } | { type: "done"; err?: Error }
186
+ > = [];
156
187
  let wake: (() => void) | null = null;
157
188
  let closed = false;
189
+ let ended = false;
158
190
  const input = (async function* () {
159
191
  try {
160
192
  while (true) {
@@ -167,24 +199,37 @@ function peekInput(_dc: RTCDataChannel): PeekInput {
167
199
  }
168
200
  const slot = slots.shift();
169
201
  if (!slot) continue;
170
- if (slot.type === "done") return;
202
+ if (slot.type === "done") {
203
+ if (slot.err) throw slot.err;
204
+ return;
205
+ }
206
+ slot.taken();
171
207
  yield slot.value;
172
208
  }
173
209
  } finally {
174
- closed = true;
210
+ release();
175
211
  }
176
212
  })();
213
+ function release(): void {
214
+ closed = true;
215
+ for (const s of slots.splice(0)) if (s.type === "value") s.taken();
216
+ wake?.();
217
+ }
177
218
  return {
178
219
  input,
179
- deliver(chunk: Uint8Array): void {
180
- if (closed) return;
181
- slots.push({ type: "value", value: chunk });
182
- wake?.();
220
+ deliver(chunk: Uint8Array): Promise<void> {
221
+ if (closed) return Promise.resolve();
222
+ return new Promise<void>((taken) => {
223
+ slots.push({ type: "value", value: chunk, taken });
224
+ wake?.();
225
+ });
183
226
  },
184
- done(): void {
185
- if (closed) return;
186
- slots.push({ type: "done" });
227
+ done(err?: Error): void {
228
+ if (closed || ended) return;
229
+ ended = true;
230
+ slots.push({ type: "done", err });
187
231
  wake?.();
188
232
  },
233
+ release,
189
234
  };
190
235
  }
@@ -8,13 +8,74 @@ import {
8
8
  const TYPE_DATA = 0x00;
9
9
  const TYPE_END = 0x01;
10
10
  const TYPE_ERROR = 0x02;
11
+ const TYPE_CREDIT = 0x03;
11
12
 
12
13
  const DC_MTU = 16 * 1024; // conservative across browsers
14
+
15
+ /**
16
+ * Send-side backpressure: a sender waits while the DataChannel holds more than this many bytes
17
+ * unsent (`bufferedAmount`), until it drains to {@link LOW_WATER_MARK} (`bufferedamountlow`).
18
+ * Without it a fast producer fills the browser's send queue (Chrome fails `send` past 16 MiB).
19
+ */
20
+ export const HIGH_WATER_MARK = 1024 * 1024;
21
+ export const LOW_WATER_MARK = 256 * 1024;
22
+ /**
23
+ * Receive-side backpressure: a sender may have at most this many payload bytes outstanding that
24
+ * the receiving consumer has not yet pulled. The receiver returns credit (`CREDIT` frames) as its
25
+ * consumer pulls, half a window at a time. A DataChannel delivers every message as it arrives,
26
+ * whether or not anyone reads, so without credit a slow reader's queue grows without bound. The
27
+ * window is larger than the send buffer's high-water mark so a long round trip does not throttle a
28
+ * fast link: the buffer limit is what bounds the sender's memory.
29
+ *
30
+ * The receiver enforces it: a peer that sends more than the window plus the credit returned fails
31
+ * the call (the queued payload is dropped, the stream throws, the channel is closed).
32
+ */
33
+ export const RECEIVE_WINDOW = 4 * 1024 * 1024;
13
34
  const FRAME_OVERHEAD = 1; // 1-byte type tag
14
35
 
15
36
  const textEncoder = new TextEncoder();
16
37
  const textDecoder = new TextDecoder();
17
38
 
39
+ /**
40
+ * Messages (and a close) that arrive on a channel before its stream starts listening. A generator
41
+ * runs only on its first `next()`, so without this a peer that answers at once (a handler that
42
+ * throws, a reply with no input to wait for) can be faster than our listener: its frames are
43
+ * dropped and the call hangs or reports a bare close.
44
+ */
45
+ const held = new WeakMap<RTCDataChannel, { events: Event[]; listener: (ev: Event) => void }>();
46
+
47
+ /**
48
+ * Channels this side answers (from `datachannel`). A finished call is closed by its caller only,
49
+ * once it has the responder's `END` or `ERROR`: a responder that closed right after sending them
50
+ * could lose them in flight (Chrome delivers the close first), turning an error into a bare close.
51
+ */
52
+ const answering = new WeakSet<RTCDataChannel>();
53
+ export function markAnswering(dc: RTCDataChannel): void {
54
+ answering.add(dc);
55
+ }
56
+
57
+ /** starts holding a channel's events until {@link duplexOverDataChannel} takes them */
58
+ export function holdEvents(dc: RTCDataChannel): void {
59
+ if (held.has(dc)) return;
60
+ dc.binaryType = "arraybuffer";
61
+ const events: Event[] = [];
62
+ const listener = (ev: Event): void => {
63
+ events.push(ev);
64
+ };
65
+ dc.addEventListener("message", listener);
66
+ dc.addEventListener("close", listener);
67
+ held.set(dc, { events, listener });
68
+ }
69
+
70
+ function takeHeld(dc: RTCDataChannel): Event[] {
71
+ const h = held.get(dc);
72
+ if (!h) return [];
73
+ held.delete(dc);
74
+ dc.removeEventListener("message", h.listener);
75
+ dc.removeEventListener("close", h.listener);
76
+ return h.events;
77
+ }
78
+
18
79
  /**
19
80
  * Drive one `Duplex` over one `RTCDataChannel`. The DataChannel hosts a tiny
20
81
  * in-band framing protocol:
@@ -23,7 +84,9 @@ const textDecoder = new TextDecoder();
23
84
  *
24
85
  * Types are `DATA` (0x00, body bytes), `END` (0x01, signals half-close — the
25
86
  * sender is done emitting; the channel stays open for the other direction),
26
- * and `ERROR` (0x02, followed by a serialised `Error`).
87
+ * `ERROR` (0x02, followed by a serialised `Error`) and `CREDIT` (0x03, a
88
+ * big-endian u32: payload bytes the receiver's consumer has pulled, which the
89
+ * sender may now send again; see {@link RECEIVE_WINDOW}).
27
90
  *
28
91
  * `RTCDataChannel` has no native half-close, so `END` is emitted by the
29
92
  * sender once its input iterator exhausts; the channel is only physically
@@ -36,9 +99,21 @@ export function duplexOverDataChannel(
36
99
  return runStream(dc, input);
37
100
  }
38
101
 
39
- async function* runStream(
102
+ export interface RunStreamHooks {
103
+ /**
104
+ * Called once when the inbound side fails (the peer's `ERROR`, the channel closing, a peer
105
+ * past the receive window), before the stream waits for its outbound pump to settle. A
106
+ * responder ends its handler's input here: the pump may be waiting for the handler's next
107
+ * output while the handler waits for its next input.
108
+ */
109
+ onInboundFailed?(err: Error): void;
110
+ }
111
+
112
+ /** {@link duplexOverDataChannel}, with hooks for `serve` */
113
+ export async function* runStream(
40
114
  dc: RTCDataChannel,
41
115
  input: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
116
+ hooks: RunStreamHooks = {},
42
117
  ): AsyncGenerator<Uint8Array> {
43
118
  dc.binaryType = "arraybuffer";
44
119
 
@@ -46,8 +121,56 @@ async function* runStream(
46
121
  let peerEnded = false;
47
122
  let localEnded = false;
48
123
 
124
+ // flow control: what the peer still lets us send, and what our consumer has pulled
125
+ let credit = RECEIVE_WINDOW;
126
+ let pulled = 0;
127
+ /** what the peer may still send us: the window plus the credit we returned, less what came */
128
+ let allowance = RECEIVE_WINDOW;
129
+ let overrun = false;
130
+ let wakeSender: (() => void) | null = null;
131
+ const nudge = (): void => {
132
+ const w = wakeSender;
133
+ wakeSender = null;
134
+ w?.();
135
+ };
136
+ const stalled = (): Promise<void> =>
137
+ new Promise<void>((r) => {
138
+ wakeSender = r;
139
+ });
140
+ dc.bufferedAmountLowThreshold = LOW_WATER_MARK;
141
+ const sendRaw = (bytes: Uint8Array): void => {
142
+ (dc as unknown as { send: (data: Uint8Array) => void }).send(bytes);
143
+ };
144
+ /** set when this call is abandoned (cancelled, or the peer failed): the sender stops waiting */
145
+ let aborted = false;
146
+ /** waits for credit for `n` bytes and room in the send buffer; false once the call is over */
147
+ const room = async (n: number): Promise<boolean> => {
148
+ while (
149
+ !aborted &&
150
+ dc.readyState === "open" &&
151
+ (credit < n || dc.bufferedAmount > HIGH_WATER_MARK)
152
+ )
153
+ await stalled();
154
+ return !aborted && dc.readyState === "open";
155
+ };
156
+ /** returns credit for what our consumer pulled, half a window at a time */
157
+ const consumed = (n: number): void => {
158
+ pulled += n;
159
+ if (pulled < RECEIVE_WINDOW / 2 || dc.readyState !== "open") return;
160
+ const frame = new Uint8Array(5);
161
+ frame[0] = TYPE_CREDIT;
162
+ new DataView(frame.buffer).setUint32(1, pulled);
163
+ try {
164
+ sendRaw(frame);
165
+ allowance += pulled;
166
+ } catch {
167
+ /* closing */
168
+ }
169
+ pulled = 0;
170
+ };
171
+
49
172
  const maybeClose = (): void => {
50
- if (peerEnded && localEnded) {
173
+ if (peerEnded && localEnded && !answering.has(dc)) {
51
174
  try {
52
175
  dc.close();
53
176
  } catch {
@@ -69,11 +192,35 @@ async function* runStream(
69
192
  const type = bytes[0];
70
193
  const payload = bytes.subarray(1);
71
194
  if (type === TYPE_DATA) {
195
+ if (overrun) return;
196
+ allowance -= payload.byteLength;
197
+ if (allowance < 0) {
198
+ // the peer ignores credit: fail the call rather than queue without bound
199
+ overrun = true;
200
+ peerEnded = true;
201
+ incoming.fail(
202
+ new Error(
203
+ `webrun-streams-webrtc: the peer sent past the receive window (${RECEIVE_WINDOW} bytes) without credit`,
204
+ ),
205
+ );
206
+ // and tell the peer now, not once our consumer reaches the error
207
+ aborted = true;
208
+ nudge();
209
+ try {
210
+ dc.close();
211
+ } catch {
212
+ /* already closing */
213
+ }
214
+ return;
215
+ }
72
216
  incoming.push(new Uint8Array(payload));
73
217
  } else if (type === TYPE_END) {
74
218
  peerEnded = true;
75
219
  incoming.done();
76
220
  maybeClose();
221
+ } else if (type === TYPE_CREDIT && payload.byteLength >= 4) {
222
+ credit += new DataView(payload.buffer, payload.byteOffset, 4).getUint32(0);
223
+ nudge();
77
224
  } else if (type === TYPE_ERROR) {
78
225
  let err: Error;
79
226
  try {
@@ -88,6 +235,7 @@ async function* runStream(
88
235
  };
89
236
 
90
237
  const onClose = (): void => {
238
+ nudge();
91
239
  if (!peerEnded) {
92
240
  incoming.done(new TransportClosedError());
93
241
  peerEnded = true;
@@ -96,16 +244,33 @@ async function* runStream(
96
244
 
97
245
  dc.addEventListener("message", onMessage);
98
246
  dc.addEventListener("close", onClose);
247
+ dc.addEventListener("bufferedamountlow", nudge);
248
+ // what arrived before we listened, in order
249
+ for (const ev of takeHeld(dc)) {
250
+ if (ev.type === "message") onMessage(ev as MessageEvent);
251
+ else onClose();
252
+ }
253
+ if (dc.readyState === "closed") onClose();
254
+ // The listeners stay until both halves are done: after the peer's END our own sending may still
255
+ // wait for the peer's CREDIT frames (a reply larger than the window, sent after the input ended).
256
+ let inboundFinished = false;
257
+ const detach = (): void => {
258
+ if (!inboundFinished || !localEnded) return;
259
+ dc.removeEventListener("message", onMessage);
260
+ dc.removeEventListener("close", onClose);
261
+ dc.removeEventListener("bufferedamountlow", nudge);
262
+ };
99
263
 
100
264
  // Outbound pump runs in parallel with inbound iteration.
101
265
  const outbound = (async () => {
102
266
  try {
103
267
  for await (const chunk of toChunks(DC_MTU - FRAME_OVERHEAD)(input)) {
104
- if (dc.readyState !== "open") return;
268
+ if (!(await room(chunk.byteLength))) return;
269
+ credit -= chunk.byteLength;
105
270
  const framed = new Uint8Array(chunk.byteLength + 1);
106
271
  framed[0] = TYPE_DATA;
107
272
  framed.set(chunk, 1);
108
- (dc as unknown as { send: (data: Uint8Array) => void }).send(framed);
273
+ sendRaw(framed);
109
274
  }
110
275
  if (dc.readyState === "open") {
111
276
  const end = new Uint8Array([TYPE_END]);
@@ -127,20 +292,27 @@ async function* runStream(
127
292
  } finally {
128
293
  localEnded = true;
129
294
  maybeClose();
295
+ detach();
130
296
  }
131
297
  })();
132
298
 
133
299
  let inboundDrained = false;
300
+ let inboundError: Error | undefined;
134
301
  try {
135
302
  for await (const chunk of incoming.iterate()) {
136
303
  yield chunk;
304
+ consumed(chunk.byteLength); // pulled: the peer may send that much again
137
305
  }
138
306
  inboundDrained = true;
307
+ } catch (err) {
308
+ inboundError = err instanceof Error ? err : new Error(String(err));
309
+ throw err;
139
310
  } finally {
140
- dc.removeEventListener("message", onMessage);
141
- dc.removeEventListener("close", onClose);
142
- // Wait for outbound to settle so an early exit doesn't truncate in-flight
143
- // sends — but ONLY on an early exit.
311
+ inboundFinished = true;
312
+ detach();
313
+ // On an early exit, stop the outbound pump (it may be waiting for credit
314
+ // the peer will never send now), close the channel, then let the pump
315
+ // settle — but ONLY on an early exit.
144
316
  //
145
317
  // Awaiting it unconditionally deadlocks a responder. `serve` feeds the
146
318
  // handler from the values this generator yields and closes the handler's
@@ -154,7 +326,8 @@ async function* runStream(
154
326
  // there is nothing of theirs left to truncate; outbound keeps running on
155
327
  // its own and `maybeClose` still closes the channel once both halves end.
156
328
  if (!inboundDrained) {
157
- await outbound.catch(() => {});
329
+ aborted = true;
330
+ nudge();
158
331
  // The consumer walked away mid-response (`.return()` on the generator),
159
332
  // or the peer errored. Either way this call is over while the peer may
160
333
  // still be producing, and a DataChannel has no half-close to say so —
@@ -164,11 +337,17 @@ async function* runStream(
164
337
  // `onClose` then ends its inbound queue, which closes the handler's
165
338
  // input and runs the handler's `finally`. Without this, cancellation is
166
339
  // silently local and the handler leaks until the connection drops.
340
+ // The close comes before waiting on the pump: a pump stalled on credit
341
+ // would otherwise hold the cancellation back.
167
342
  try {
168
343
  dc.close();
169
344
  } catch {
170
345
  /* already closing */
171
346
  }
347
+ // A responder's pump may be waiting for the handler's next output while the handler waits
348
+ // for its next input: end that input (with the error) before waiting on the pump.
349
+ if (inboundError) hooks.onInboundFailed?.(inboundError);
350
+ await outbound.catch(() => {});
172
351
  }
173
352
  }
174
353
  }
@@ -176,6 +355,8 @@ async function* runStream(
176
355
  interface InboundQueue {
177
356
  push(chunk: Uint8Array): void;
178
357
  done(err?: Error): void;
358
+ /** drops what is queued and ends with `err` */
359
+ fail(err: Error): void;
179
360
  iterate(): AsyncGenerator<Uint8Array>;
180
361
  }
181
362
 
@@ -196,6 +377,12 @@ function makeInboundQueue(): InboundQueue {
196
377
  slots.push({ type: "done", err });
197
378
  wake?.();
198
379
  },
380
+ fail(err: Error): void {
381
+ if (closed) return;
382
+ slots.length = 0;
383
+ slots.push({ type: "done", err });
384
+ wake?.();
385
+ },
199
386
  iterate(): AsyncGenerator<Uint8Array> {
200
387
  return (async function* () {
201
388
  try {
package/src/index.ts CHANGED
@@ -1,2 +1,13 @@
1
1
  export { connect, serve, type WebRtcParams } from "./connect-serve.js";
2
2
  export { duplexOverDataChannel } from "./duplex-over-data-channel.js";
3
+ export {
4
+ type DataChannelPort,
5
+ DEFAULT_PORT_MAX_FRAME_BYTES,
6
+ DEFAULT_PORT_MAX_MESSAGE_SIZE,
7
+ PORT_DRAIN_LOW_WATER,
8
+ PORT_LABEL_PREFIX,
9
+ type ValueCodec,
10
+ type WebRtcPortMux,
11
+ type WebRtcPortMuxOptions,
12
+ webrtcPortMux,
13
+ } from "./port-mux.js";