@statewalker/webrun-streams-livekit 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 statewalker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @statewalker/webrun-streams-livekit
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).
4
+
5
+ ```ts
6
+ import { connect, serve } from "@statewalker/webrun-streams-livekit";
7
+
8
+ const { call } = await connect({ room, peerIdentity: "agent-7" });
9
+ const stop = await serve({ room, peerIdentity: "client-3" }, handler);
10
+ ```
11
+
12
+ Conformance is browser-gated; run with `pnpm test:browser` plus `WEBRUN_STREAMS_LIVEKIT_*` env vars set against a running LiveKit server.
13
+
14
+ ## License
15
+
16
+ MIT
@@ -0,0 +1,9 @@
1
+ import type { ByteChannel } from "@statewalker/webrun-streams";
2
+ import type { Room } from "livekit-client";
3
+ /**
4
+ * Wrap a connected LiveKit `Room` plus a remote participant identity into a
5
+ * `ByteChannel`. Outbound bytes are addressed only to the named participant;
6
+ * inbound is filtered by sender identity.
7
+ */
8
+ export declare function byteChannelFromLiveKit(room: Room, peerIdentity: string): ByteChannel;
9
+ //# sourceMappingURL=byte-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byte-channel.d.ts","sourceRoot":"","sources":["../src/byte-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAqB,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAa9D;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,GAAG,WAAW,CAsGpF"}
@@ -0,0 +1,11 @@
1
+ import { type Connect, type Serve } from "@statewalker/webrun-streams";
2
+ import type { Room } from "livekit-client";
3
+ export interface LiveKitParams {
4
+ /** Already-connected LiveKit `Room`. */
5
+ room: Room;
6
+ /** Identity of the remote participant the call addresses. */
7
+ peerIdentity: string;
8
+ }
9
+ export declare const connect: Connect<LiveKitParams>;
10
+ export declare const serve: Serve<LiveKitParams>;
11
+ //# sourceMappingURL=connect-serve.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,3 @@
1
+ export { byteChannelFromLiveKit } from "./byte-channel.js";
2
+ export { connect, type LiveKitParams, serve } from "./connect-serve.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ import { emulateMux } from "@statewalker/webrun-streams";
2
+ //#region src/byte-channel.ts
3
+ /**
4
+ * Wrap a connected LiveKit `Room` plus a remote participant identity into a
5
+ * `ByteChannel`. Outbound bytes are addressed only to the named participant;
6
+ * inbound is filtered by sender identity.
7
+ */
8
+ function byteChannelFromLiveKit(room, peerIdentity) {
9
+ let closedResolve;
10
+ const closed = new Promise((r) => {
11
+ closedResolve = r;
12
+ });
13
+ let isClosed = false;
14
+ const queue = [];
15
+ let pending = null;
16
+ const deliver = (bytes) => {
17
+ if (isClosed) return;
18
+ if (pending) {
19
+ const r = pending;
20
+ pending = null;
21
+ r({
22
+ value: bytes,
23
+ done: false
24
+ });
25
+ } else queue.push(bytes);
26
+ };
27
+ const onData = (payload, participant) => {
28
+ if (!participant) return;
29
+ if (participant.identity !== peerIdentity) return;
30
+ deliver(new Uint8Array(payload));
31
+ };
32
+ const onDisconnected = () => {
33
+ fireClose();
34
+ };
35
+ const onParticipantDisconnected = (participant) => {
36
+ if (participant.identity !== peerIdentity) return;
37
+ fireClose();
38
+ };
39
+ const fireClose = () => {
40
+ if (isClosed) return;
41
+ isClosed = true;
42
+ roomApi.off("dataReceived", onData);
43
+ roomApi.off("disconnected", onDisconnected);
44
+ roomApi.off("participantDisconnected", onParticipantDisconnected);
45
+ if (pending) {
46
+ const r = pending;
47
+ pending = null;
48
+ r({
49
+ value: void 0,
50
+ done: true
51
+ });
52
+ }
53
+ closedResolve();
54
+ };
55
+ const roomApi = room;
56
+ roomApi.on("dataReceived", onData);
57
+ roomApi.on("disconnected", onDisconnected);
58
+ roomApi.on("participantDisconnected", onParticipantDisconnected);
59
+ return {
60
+ send(bytes) {
61
+ if (isClosed) return;
62
+ room.localParticipant.publishData(bytes, {
63
+ reliable: true,
64
+ destinationIdentities: [peerIdentity]
65
+ }).catch(() => {});
66
+ },
67
+ recv: { [Symbol.asyncIterator]() {
68
+ return { next() {
69
+ if (queue.length > 0) return Promise.resolve({
70
+ value: queue.shift(),
71
+ done: false
72
+ });
73
+ if (isClosed) return Promise.resolve({
74
+ value: void 0,
75
+ done: true
76
+ });
77
+ return new Promise((resolve) => {
78
+ pending = resolve;
79
+ });
80
+ } };
81
+ } },
82
+ closed,
83
+ close() {
84
+ fireClose();
85
+ }
86
+ };
87
+ }
88
+ //#endregion
89
+ //#region src/connect-serve.ts
90
+ const connect = async ({ room, peerIdentity }) => {
91
+ const channel = byteChannelFromLiveKit(room, peerIdentity);
92
+ const mux = emulateMux(channel, { side: "initiator" });
93
+ return {
94
+ call: mux.call,
95
+ async close() {
96
+ await mux.close();
97
+ }
98
+ };
99
+ };
100
+ const serve = async ({ room, peerIdentity }, handler) => {
101
+ const channel = byteChannelFromLiveKit(room, peerIdentity);
102
+ const mux = emulateMux(channel, { side: "responder" });
103
+ const off = mux.serve(handler);
104
+ channel.closed.then(() => mux.close());
105
+ let torn = false;
106
+ return async () => {
107
+ if (torn) return;
108
+ torn = true;
109
+ await off();
110
+ await mux.close();
111
+ };
112
+ };
113
+ //#endregion
114
+ export { byteChannelFromLiveKit, connect, serve };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@statewalker/webrun-streams-livekit",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "LiveKit-backed Connect/Serve adapter in the webrun-streams-* family",
7
+ "homepage": "https://github.com/statewalker/webrun-wire",
8
+ "author": {
9
+ "name": "Mikhail Kotelnikov",
10
+ "email": "mikhail.kotelnikov@gmail.com"
11
+ },
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git@github.com:statewalker/webrun-wire.git"
16
+ },
17
+ "exports": {
18
+ ".": "./src/index.ts"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "dependencies": {
25
+ "@statewalker/webrun-streams": "0.1.1"
26
+ },
27
+ "peerDependencies": {
28
+ "livekit-client": "^2.18.3"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^26.2.0",
32
+ "livekit-client": "^2.18.3",
33
+ "livekit-server-sdk": "^2.10.0",
34
+ "rimraf": "^6.1.3",
35
+ "rolldown": "^1.2.4",
36
+ "testcontainers": "^11.0.0",
37
+ "typescript": "^7.0.2",
38
+ "vitest": "^4.1.10",
39
+ "@statewalker/webrun-streams-conformance": "0.1.1"
40
+ },
41
+ "sideEffects": false,
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
47
+ "test": "vitest run",
48
+ "test:browser": "vitest run --config vitest.browser.config.ts",
49
+ "lint": "biome check src tests"
50
+ }
51
+ }
@@ -0,0 +1,122 @@
1
+ import type { ByteChannel } from "@statewalker/webrun-streams";
2
+ import type { RemoteParticipant, Room } from "livekit-client";
3
+
4
+ /**
5
+ * livekit-client v2 `publishData` options shape. Reliable mode is ordered +
6
+ * retransmitted; the bridged-byte contract requires this. `destinationIdentities`
7
+ * restricts the publish to the named recipients so peer-to-peer pairings on the
8
+ * same room don't fan out to every participant.
9
+ */
10
+ type DataPublishOptions = {
11
+ reliable?: boolean;
12
+ destinationIdentities?: string[];
13
+ };
14
+
15
+ /**
16
+ * Wrap a connected LiveKit `Room` plus a remote participant identity into a
17
+ * `ByteChannel`. Outbound bytes are addressed only to the named participant;
18
+ * inbound is filtered by sender identity.
19
+ */
20
+ export function byteChannelFromLiveKit(room: Room, peerIdentity: string): ByteChannel {
21
+ let closedResolve!: () => void;
22
+ const closed = new Promise<void>((r) => {
23
+ closedResolve = r;
24
+ });
25
+ let isClosed = false;
26
+
27
+ const queue: Uint8Array[] = [];
28
+ let pending: ((value: IteratorResult<Uint8Array>) => void) | null = null;
29
+
30
+ const deliver = (bytes: Uint8Array): void => {
31
+ if (isClosed) return;
32
+ if (pending) {
33
+ const r = pending;
34
+ pending = null;
35
+ r({ value: bytes, done: false });
36
+ } else {
37
+ queue.push(bytes);
38
+ }
39
+ };
40
+
41
+ const onData = (payload: Uint8Array, participant?: RemoteParticipant): void => {
42
+ if (!participant) return;
43
+ if (participant.identity !== peerIdentity) return;
44
+ deliver(new Uint8Array(payload));
45
+ };
46
+
47
+ const onDisconnected = (): void => {
48
+ fireClose();
49
+ };
50
+
51
+ const onParticipantDisconnected = (participant: RemoteParticipant): void => {
52
+ if (participant.identity !== peerIdentity) return;
53
+ fireClose();
54
+ };
55
+
56
+ const fireClose = (): void => {
57
+ if (isClosed) return;
58
+ isClosed = true;
59
+ roomApi.off("dataReceived", onData as (...args: unknown[]) => void);
60
+ roomApi.off("disconnected", onDisconnected);
61
+ roomApi.off(
62
+ "participantDisconnected",
63
+ onParticipantDisconnected as (...args: unknown[]) => void,
64
+ );
65
+ if (pending) {
66
+ const r = pending;
67
+ pending = null;
68
+ r({ value: undefined, done: true } as IteratorResult<Uint8Array>);
69
+ }
70
+ closedResolve();
71
+ };
72
+
73
+ // livekit-client surfaces events via a typed emitter; coerce to the
74
+ // structural on/off shape we need.
75
+ const roomApi = room as unknown as {
76
+ on(ev: string, fn: (...args: unknown[]) => void): void;
77
+ off(ev: string, fn: (...args: unknown[]) => void): void;
78
+ };
79
+ roomApi.on("dataReceived", onData as (...args: unknown[]) => void);
80
+ roomApi.on("disconnected", onDisconnected);
81
+ roomApi.on("participantDisconnected", onParticipantDisconnected as (...args: unknown[]) => void);
82
+
83
+ const recv: AsyncIterable<Uint8Array> = {
84
+ [Symbol.asyncIterator]() {
85
+ return {
86
+ next(): Promise<IteratorResult<Uint8Array>> {
87
+ if (queue.length > 0) {
88
+ return Promise.resolve({ value: queue.shift() as Uint8Array, done: false });
89
+ }
90
+ if (isClosed) {
91
+ return Promise.resolve({
92
+ value: undefined,
93
+ done: true,
94
+ } as IteratorResult<Uint8Array>);
95
+ }
96
+ return new Promise<IteratorResult<Uint8Array>>((resolve) => {
97
+ pending = resolve;
98
+ });
99
+ },
100
+ };
101
+ },
102
+ };
103
+
104
+ return {
105
+ send(bytes) {
106
+ if (isClosed) return;
107
+ const publish = room.localParticipant as unknown as {
108
+ publishData: (data: Uint8Array, options?: DataPublishOptions) => Promise<void>;
109
+ };
110
+ publish
111
+ .publishData(bytes, { reliable: true, destinationIdentities: [peerIdentity] })
112
+ .catch(() => {
113
+ /* publish failure surfaces via disconnect events */
114
+ });
115
+ },
116
+ recv,
117
+ closed,
118
+ close() {
119
+ fireClose();
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,35 @@
1
+ import { type Connect, type Duplex, emulateMux, type Serve } from "@statewalker/webrun-streams";
2
+ import type { Room } from "livekit-client";
3
+ import { byteChannelFromLiveKit } from "./byte-channel.js";
4
+
5
+ export interface LiveKitParams {
6
+ /** Already-connected LiveKit `Room`. */
7
+ room: Room;
8
+ /** Identity of the remote participant the call addresses. */
9
+ peerIdentity: string;
10
+ }
11
+
12
+ export const connect: Connect<LiveKitParams> = async ({ room, peerIdentity }) => {
13
+ const channel = byteChannelFromLiveKit(room, peerIdentity);
14
+ const mux = emulateMux(channel, { side: "initiator" });
15
+ return {
16
+ call: mux.call,
17
+ async close() {
18
+ await mux.close();
19
+ },
20
+ };
21
+ };
22
+
23
+ export const serve: Serve<LiveKitParams> = async ({ room, peerIdentity }, handler: Duplex) => {
24
+ const channel = byteChannelFromLiveKit(room, peerIdentity);
25
+ const mux = emulateMux(channel, { side: "responder" });
26
+ const off = mux.serve(handler);
27
+ void channel.closed.then(() => mux.close());
28
+ let torn = false;
29
+ return async () => {
30
+ if (torn) return;
31
+ torn = true;
32
+ await off();
33
+ await mux.close();
34
+ };
35
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { byteChannelFromLiveKit } from "./byte-channel.js";
2
+ export { connect, type LiveKitParams, serve } from "./connect-serve.js";