@statewalker/webrun-streams-livekit 0.1.1 → 0.2.2

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
@@ -1,16 +1,149 @@
1
1
  # @statewalker/webrun-streams-livekit
2
2
 
3
- LiveKit-backed `Connect` / `Serve` adapter. Wraps a connected `Room` plus a participant identity into a byte channel; `emulateMux` provides multi-stream. `RELIABLE` publish mode is forced (ordered, retransmitted).
3
+ LiveKit-backed `Connect` / `Serve` adapter in the `webrun-streams-*` family.
4
+
5
+ ## What this is
6
+
7
+ A binding between a connected LiveKit [`Room`](https://docs.livekit.io/client-sdk-js/classes/Room.html)
8
+ and the [`webrun-streams`](../webrun-streams) `Duplex` seam. A room plus a
9
+ remote participant identity becomes one `ByteChannel`; `emulateMux` layers many
10
+ concurrent logical calls on top.
11
+
12
+ Your handler is an ordinary `Duplex` —
13
+ `(input: AsyncIterable<Uint8Array>) => AsyncGenerator<Uint8Array>` — the same
14
+ function you would run over a WebSocket or WebRTC.
15
+
16
+ ## Why it exists
17
+
18
+ Peer-to-peer links are excellent until they aren't: symmetric NATs, corporate
19
+ firewalls, mobile networks and multi-party sessions all argue for a managed
20
+ SFU. LiveKit provides one, with authentication, room membership and presence
21
+ already solved.
22
+
23
+ What LiveKit's data channel does *not* provide is request semantics. This
24
+ adapter adds them, so an application written against the `Duplex` seam can move
25
+ from a direct WebRTC link to a LiveKit room by changing one import — the
26
+ handlers, the HTTP layer above them, and the tests all stay put.
27
+
28
+ Publishes are forced to LiveKit's **`RELIABLE`** mode (ordered, retransmitted),
29
+ because the framing above assumes byte-stream ordering.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ npm install @statewalker/webrun-streams-livekit livekit-client
35
+ ```
36
+
37
+ `livekit-client` is a **peer dependency** (`^2.18.3`) — you own the `Room`, its
38
+ token, and its lifecycle.
39
+
40
+ ## Getting started
41
+
42
+ Both sides take the same parameters: a connected `Room` and the identity of the
43
+ participant at the other end.
44
+
45
+ Responder:
46
+
47
+ ```ts
48
+ import { Room } from "livekit-client";
49
+ import { serve } from "@statewalker/webrun-streams-livekit";
50
+
51
+ const room = new Room();
52
+ await room.connect(url, token); // identity: "agent-7"
53
+
54
+ const stop = await serve({ room, peerIdentity: "client-3" }, async function* echo(input) {
55
+ for await (const chunk of input) yield chunk;
56
+ });
57
+ ```
58
+
59
+ Caller:
60
+
61
+ ```ts
62
+ import { connect } from "@statewalker/webrun-streams-livekit";
63
+
64
+ const { call, close } = await connect({ room, peerIdentity: "agent-7" });
65
+
66
+ for await (const chunk of call([new TextEncoder().encode("ping")])) {
67
+ console.log(new TextDecoder().decode(chunk)); // "ping"
68
+ }
69
+
70
+ await close();
71
+ ```
72
+
73
+ ### Carrying HTTP over it
4
74
 
5
75
  ```ts
6
- import { connect, serve } from "@statewalker/webrun-streams-livekit";
76
+ import { fetchOverDuplex } from "@statewalker/webrun-http-streams";
77
+
78
+ const response = await fetchOverDuplex(call, new Request("http://peer/api/events"));
79
+ ```
80
+
81
+ [`apps/livekit-demo`](../../apps/livekit-demo) runs this end to end — a dev
82
+ LiveKit server, a token service, and two browser pages exchanging HTTP and SSE
83
+ through a room.
84
+
85
+ ## API
86
+
87
+ ### `connect(params): Promise<{ call, close }>`
88
+
89
+ Type: `Connect<LiveKitParams>`. Each `call(input)` opens a new logical stream
90
+ addressed to `peerIdentity`; `close()` tears them down. It does not disconnect
91
+ the `Room`, which you own.
92
+
93
+ ### `serve(params, handler): Promise<() => Promise<void>>`
94
+
95
+ Type: `Serve<LiveKitParams>`. Runs `handler` for inbound streams from
96
+ `peerIdentity`. Returns an idempotent teardown.
97
+
98
+ ### `LiveKitParams`
7
99
 
8
- const { call } = await connect({ room, peerIdentity: "agent-7" });
9
- const stop = await serve({ room, peerIdentity: "client-3" }, handler);
100
+ | Field | Type | Meaning |
101
+ | --- | --- | --- |
102
+ | `room` | `Room` | An already-connected LiveKit room. |
103
+ | `peerIdentity` | `string` | Identity of the remote participant this side talks to. |
104
+ | `mux` | `EmulateMuxOptions` | Flow-control tuning (`mtu`, `maxStreamBuffer`) forwarded to `emulateMux`. `side` is set by which function you call (`"initiator"` for `connect`, `"responder"` for `serve`) regardless of `mux.side`. **`mtu` defaults to 12 KiB here**, not `emulateMux`'s 64 KiB — see below. |
105
+
106
+ ### `byteChannelFromLiveKit(room, peerIdentity): ByteChannel`
107
+
108
+ Wraps a room + peer identity as a `ByteChannel` (`send` / `recv` / `closed` /
109
+ `close`) for driving `emulateMux` yourself.
110
+
111
+
112
+ ### Why the MTU default is lower here
113
+
114
+ A LiveKit reliable data packet is capped at roughly 15 KiB, and a payload over
115
+ that is dropped rather than fragmented. `emulateMux`'s own 64 KiB default
116
+ therefore does not survive this transport, and it fails silently: a 1 MiB body
117
+ arrives as zero bytes and a 10 MiB body never completes, with no error on
118
+ either side. So `connect` and `serve` default `mtu` to 12 KiB — under the cap
119
+ with room for the frame header (`[varint streamId][1-byte type]`).
120
+
121
+ An explicit `mux.mtu` still wins, so a deployment that permits larger packets
122
+ can raise it.
123
+
124
+ ## Conformance
125
+
126
+ Conformance is **browser-gated** — it needs a real LiveKit server rather than a
127
+ Node shim:
128
+
129
+ ```sh
130
+ pnpm --filter @statewalker/webrun-streams-livekit test:browser
10
131
  ```
11
132
 
12
- Conformance is browser-gated; run with `pnpm test:browser` plus `WEBRUN_STREAMS_LIVEKIT_*` env vars set against a running LiveKit server.
133
+ with the `WEBRUN_STREAMS_LIVEKIT_*` environment variables pointing at a running
134
+ server. The package's dev dependencies include `testcontainers` and
135
+ `livekit-server-sdk` for standing one up.
136
+
137
+ ## Dependencies
138
+
139
+ | Dependency | Kind | Why |
140
+ | --- | --- | --- |
141
+ | [`@statewalker/webrun-streams`](../webrun-streams) | runtime | The `Duplex` / `ByteChannel` seam and `emulateMux`. |
142
+ | `livekit-client` | **peer** (`^2.18.3`) | You supply and own the `Room`. |
143
+ | `livekit-server-sdk`, `testcontainers` | dev | Token minting and a containerised server for conformance. |
144
+
145
+ ESM only (`"type": "module"`).
13
146
 
14
147
  ## License
15
148
 
16
- MIT
149
+ MIT © statewalker — see [LICENSE](../../LICENSE).
@@ -1,10 +1,18 @@
1
- import { type Connect, type Serve } from "@statewalker/webrun-streams";
1
+ import { type Connect, type EmulateMuxOptions, type Serve } from "@statewalker/webrun-streams";
2
2
  import type { Room } from "livekit-client";
3
3
  export interface LiveKitParams {
4
4
  /** Already-connected LiveKit `Room`. */
5
5
  room: Room;
6
6
  /** Identity of the remote participant the call addresses. */
7
7
  peerIdentity: string;
8
+ /**
9
+ * Flow-control tuning forwarded to `emulateMux` — `mtu` and
10
+ * `maxStreamBuffer`, which is the credit this side advertises. `side` here
11
+ * wins over `mux.side`. Defaults are `emulateMux`'s own; the conformance
12
+ * suite's L6 uses this to run at a window small enough that a sender
13
+ * genuinely stalls.
14
+ */
15
+ mux?: EmulateMuxOptions;
8
16
  }
9
17
  export declare const connect: Connect<LiveKitParams>;
10
18
  export declare const serve: Serve<LiveKitParams>;
@@ -1 +1 @@
1
- {"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAA2B,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAChG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAG3C,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,IAAI,EAAE,IAAI,CAAC;IACX,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,aAAa,CAS1C,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,aAAa,CAYtC,CAAC"}
1
+ {"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAEZ,KAAK,iBAAiB,EAEtB,KAAK,KAAK,EACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAkB3C,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,IAAI,EAAE,IAAI,CAAC;IACX,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAED,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,aAAa,CAa1C,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,aAAa,CAmBtC,CAAC"}
package/dist/index.js CHANGED
@@ -87,9 +87,27 @@ function byteChannelFromLiveKit(room, peerIdentity) {
87
87
  }
88
88
  //#endregion
89
89
  //#region src/connect-serve.ts
90
- const connect = async ({ room, peerIdentity }) => {
90
+ /**
91
+ * Default `mtu` for this transport.
92
+ *
93
+ * `emulateMux` defaults to 64 KiB, which LiveKit cannot carry: a reliable data
94
+ * packet is capped around 15 KiB, and anything larger is dropped rather than
95
+ * fragmented. The symptom is not an error — a 1 MiB body simply arrives as
96
+ * zero bytes and a 10 MiB body hangs — so the ceiling has to be applied here,
97
+ * where the transport is known, rather than left to every caller.
98
+ *
99
+ * 12 KiB leaves room for the mux frame header (`[varint streamId][1-byte
100
+ * type]`) inside that budget. An explicit `mux.mtu` still wins, so a caller who
101
+ * knows their deployment allows more can raise it.
102
+ */
103
+ const LIVEKIT_SAFE_MTU = 12288;
104
+ const connect = async ({ room, peerIdentity, mux: muxOpts }) => {
91
105
  const channel = byteChannelFromLiveKit(room, peerIdentity);
92
- const mux = emulateMux(channel, { side: "initiator" });
106
+ const mux = emulateMux(channel, {
107
+ mtu: LIVEKIT_SAFE_MTU,
108
+ ...muxOpts,
109
+ side: "initiator"
110
+ });
93
111
  return {
94
112
  call: mux.call,
95
113
  async close() {
@@ -97,9 +115,13 @@ const connect = async ({ room, peerIdentity }) => {
97
115
  }
98
116
  };
99
117
  };
100
- const serve = async ({ room, peerIdentity }, handler) => {
118
+ const serve = async ({ room, peerIdentity, mux: muxOpts }, handler) => {
101
119
  const channel = byteChannelFromLiveKit(room, peerIdentity);
102
- const mux = emulateMux(channel, { side: "responder" });
120
+ const mux = emulateMux(channel, {
121
+ mtu: LIVEKIT_SAFE_MTU,
122
+ ...muxOpts,
123
+ side: "responder"
124
+ });
103
125
  const off = mux.serve(handler);
104
126
  channel.closed.then(() => mux.close());
105
127
  let torn = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-streams-livekit",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "LiveKit-backed Connect/Serve adapter in the webrun-streams-* family",
@@ -15,33 +15,41 @@
15
15
  "url": "git@github.com:statewalker/webrun-wire.git"
16
16
  },
17
17
  "exports": {
18
- ".": "./src/index.ts"
18
+ ".": {
19
+ "source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
19
23
  },
20
24
  "files": [
21
25
  "dist",
22
26
  "src"
23
27
  ],
24
28
  "dependencies": {
25
- "@statewalker/webrun-streams": "0.1.1"
29
+ "@statewalker/webrun-streams": "^0.2.1"
26
30
  },
27
31
  "peerDependencies": {
28
32
  "livekit-client": "^2.18.3"
29
33
  },
30
34
  "devDependencies": {
31
- "@types/node": "^26.2.0",
32
- "livekit-client": "^2.18.3",
33
- "livekit-server-sdk": "^2.10.0",
35
+ "@biomejs/biome": "^2.5.15",
36
+ "@types/node": "^26.6.4",
37
+ "@vitest/browser-playwright": "^5.0.3",
38
+ "livekit-client": "^2.22.3",
39
+ "livekit-server-sdk": "^2.19.1",
40
+ "playwright": "^1.63.0",
34
41
  "rimraf": "^6.1.3",
35
- "rolldown": "^1.2.4",
36
- "testcontainers": "^11.0.0",
42
+ "rolldown": "^1.2.12",
43
+ "testcontainers": "^12.2.0",
37
44
  "typescript": "^7.0.2",
38
- "vitest": "^4.1.10",
39
- "@statewalker/webrun-streams-conformance": "0.1.1"
45
+ "vitest": "^5.0.3",
46
+ "@statewalker/webrun-streams-conformance": "^0.3.1"
40
47
  },
41
48
  "sideEffects": false,
42
49
  "publishConfig": {
43
50
  "access": "public"
44
51
  },
52
+ "types": "./dist/index.d.ts",
45
53
  "scripts": {
46
54
  "build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
47
55
  "test": "vitest run",
@@ -1,17 +1,50 @@
1
- import { type Connect, type Duplex, emulateMux, type Serve } from "@statewalker/webrun-streams";
1
+ import {
2
+ type Connect,
3
+ type Duplex,
4
+ type EmulateMuxOptions,
5
+ emulateMux,
6
+ type Serve,
7
+ } from "@statewalker/webrun-streams";
2
8
  import type { Room } from "livekit-client";
3
9
  import { byteChannelFromLiveKit } from "./byte-channel.js";
4
10
 
11
+ /**
12
+ * Default `mtu` for this transport.
13
+ *
14
+ * `emulateMux` defaults to 64 KiB, which LiveKit cannot carry: a reliable data
15
+ * packet is capped around 15 KiB, and anything larger is dropped rather than
16
+ * fragmented. The symptom is not an error — a 1 MiB body simply arrives as
17
+ * zero bytes and a 10 MiB body hangs — so the ceiling has to be applied here,
18
+ * where the transport is known, rather than left to every caller.
19
+ *
20
+ * 12 KiB leaves room for the mux frame header (`[varint streamId][1-byte
21
+ * type]`) inside that budget. An explicit `mux.mtu` still wins, so a caller who
22
+ * knows their deployment allows more can raise it.
23
+ */
24
+ const LIVEKIT_SAFE_MTU = 12 * 1024;
25
+
5
26
  export interface LiveKitParams {
6
27
  /** Already-connected LiveKit `Room`. */
7
28
  room: Room;
8
29
  /** Identity of the remote participant the call addresses. */
9
30
  peerIdentity: string;
31
+ /**
32
+ * Flow-control tuning forwarded to `emulateMux` — `mtu` and
33
+ * `maxStreamBuffer`, which is the credit this side advertises. `side` here
34
+ * wins over `mux.side`. Defaults are `emulateMux`'s own; the conformance
35
+ * suite's L6 uses this to run at a window small enough that a sender
36
+ * genuinely stalls.
37
+ */
38
+ mux?: EmulateMuxOptions;
10
39
  }
11
40
 
12
- export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity }) => {
41
+ export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity, mux: muxOpts }) => {
13
42
  const channel = byteChannelFromLiveKit(room, peerIdentity);
14
- const mux = emulateMux(channel, { side: "initiator" });
43
+ const mux = emulateMux(channel, {
44
+ mtu: LIVEKIT_SAFE_MTU,
45
+ ...muxOpts,
46
+ side: "initiator",
47
+ });
15
48
  return {
16
49
  call: mux.call,
17
50
  async close() {
@@ -20,9 +53,16 @@ export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity }) =>
20
53
  };
21
54
  };
22
55
 
23
- export const serve: Serve<LiveKitParams> = async ({ room, peerIdentity }, handler: Duplex) => {
56
+ export const serve: Serve<LiveKitParams> = async (
57
+ { room, peerIdentity, mux: muxOpts },
58
+ handler: Duplex,
59
+ ) => {
24
60
  const channel = byteChannelFromLiveKit(room, peerIdentity);
25
- const mux = emulateMux(channel, { side: "responder" });
61
+ const mux = emulateMux(channel, {
62
+ mtu: LIVEKIT_SAFE_MTU,
63
+ ...muxOpts,
64
+ side: "responder",
65
+ });
26
66
  const off = mux.serve(handler);
27
67
  void channel.closed.then(() => mux.close());
28
68
  let torn = false;