@openlv/transport 0.1.1 → 0.2.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/dist/base.test.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { generateKeyPair } from "@openlv/core/encryption";
2
2
  import { describe, expect, it } from "vitest";
3
- import { createTransportBase, TRANSPORT_STATE, } from "./index.js";
3
+ import { createTransportBase, Status, } from "./index.js";
4
4
  const createFakeTransport = () => {
5
5
  let emitter;
6
6
  const sent = [];
7
7
  const factory = createTransportBase("fake", (parameters) => {
8
8
  emitter = parameters.emitter;
9
9
  return {
10
- setup: () => { },
11
- teardown: () => { },
10
+ setup: async () => { },
11
+ teardown: async () => { },
12
12
  handle: async () => { },
13
13
  send: async (message) => {
14
14
  sent.push(message);
@@ -68,7 +68,7 @@ describe("createTransportBase", () => {
68
68
  });
69
69
  await layer.setup();
70
70
  getEmitter().emit("connected");
71
- await layer.waitFor(TRANSPORT_STATE.CONNECTED);
71
+ expect(layer.status.get()).toBe(Status.CONNECTED);
72
72
  await layer.send({ type: "request", messageId: "1", payload: { a: 1 } });
73
73
  expect(sent).toHaveLength(1);
74
74
  // Ciphertext on the wire, not the plaintext envelope.
@@ -1 +1 @@
1
- {"version":3,"file":"base.test.js","sourceRoot":"","sources":["../src/base.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport {\n createTransportBase,\n TRANSPORT_STATE,\n type TransportLayerBaseEmitter,\n type TransportMessage,\n} from \"./index.js\";\n\nconst createFakeTransport = () => {\n let emitter: TransportLayerBaseEmitter | undefined;\n const sent: string[] = [];\n\n const factory = createTransportBase(\"fake\", (parameters) => {\n emitter = parameters.emitter;\n\n return {\n setup: () => {},\n teardown: () => {},\n handle: async () => {},\n send: async (message) => {\n sent.push(message);\n },\n };\n });\n\n return {\n factory,\n sent,\n getEmitter: (): TransportLayerBaseEmitter => {\n if (!emitter) throw new Error(\"transport not created\");\n\n return emitter;\n },\n };\n};\n\ndescribe(\"createTransportBase\", () => {\n it(\"exposes the wire id on the factory and instance\", () => {\n const { factory } = createFakeTransport();\n\n expect(factory.transportId).toBe(\"fake\");\n });\n\n it(\"relays negotiate events through subsend\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory, getEmitter } = createFakeTransport();\n const relayed: TransportMessage[] = [];\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: () => {},\n subsend: async (message) => {\n relayed.push(message);\n },\n });\n\n expect(layer.type).toBe(\"fake\");\n await layer.setup();\n\n getEmitter().emit(\"negotiate\", { type: \"offer\", payload: \"sdp\" });\n getEmitter().emit(\"negotiate\", { type: \"candidate\", payload: \"cand\" });\n await Promise.resolve();\n\n expect(relayed).toEqual([\n { type: \"offer\", payload: \"sdp\" },\n { type: \"candidate\", payload: \"cand\" },\n ]);\n });\n\n it(\"encrypts outgoing messages and decrypts incoming ones\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory, sent, getEmitter } = createFakeTransport();\n const received: object[] = [];\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: (message) => {\n received.push(message);\n },\n subsend: async () => {},\n });\n\n await layer.setup();\n getEmitter().emit(\"connected\");\n await layer.waitFor(TRANSPORT_STATE.CONNECTED);\n\n await layer.send({ type: \"request\", messageId: \"1\", payload: { a: 1 } });\n expect(sent).toHaveLength(1);\n // Ciphertext on the wire, not the plaintext envelope.\n expect(sent[0]).not.toContain(\"messageId\");\n\n getEmitter().emit(\"message\", sent[0]);\n await new Promise(resolve => setTimeout(resolve, 10));\n expect(received).toEqual([{ type: \"request\", messageId: \"1\", payload: { a: 1 } }]);\n });\n\n it(\"rejects sends before the transport is connected\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory } = createFakeTransport();\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: () => {},\n subsend: async () => {},\n });\n\n await layer.setup();\n await expect(layer.send({ a: 1 })).rejects.toThrow(\"Transport not connected\");\n });\n});\n"]}
1
+ {"version":3,"file":"base.test.js","sourceRoot":"","sources":["../src/base.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport {\n createTransportBase,\n Status,\n type TransportMessage,\n} from \"./index.js\";\nimport type { TransportLayerBaseEmitter } from \"./layer.js\";\n\nconst createFakeTransport = () => {\n let emitter: TransportLayerBaseEmitter | undefined;\n const sent: string[] = [];\n\n const factory = createTransportBase(\"fake\", (parameters) => {\n emitter = parameters.emitter;\n\n return {\n setup: async () => {},\n teardown: async () => {},\n handle: async () => {},\n send: async (message) => {\n sent.push(message);\n },\n };\n });\n\n return {\n factory,\n sent,\n getEmitter: (): TransportLayerBaseEmitter => {\n if (!emitter) throw new Error(\"transport not created\");\n\n return emitter;\n },\n };\n};\n\ndescribe(\"createTransportBase\", () => {\n it(\"exposes the wire id on the factory and instance\", () => {\n const { factory } = createFakeTransport();\n\n expect(factory.transportId).toBe(\"fake\");\n });\n\n it(\"relays negotiate events through subsend\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory, getEmitter } = createFakeTransport();\n const relayed: TransportMessage[] = [];\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: () => {},\n subsend: async (message) => {\n relayed.push(message);\n },\n });\n\n expect(layer.type).toBe(\"fake\");\n await layer.setup();\n\n getEmitter().emit(\"negotiate\", { type: \"offer\", payload: \"sdp\" });\n getEmitter().emit(\"negotiate\", { type: \"candidate\", payload: \"cand\" });\n await Promise.resolve();\n\n expect(relayed).toEqual([\n { type: \"offer\", payload: \"sdp\" },\n { type: \"candidate\", payload: \"cand\" },\n ]);\n });\n\n it(\"encrypts outgoing messages and decrypts incoming ones\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory, sent, getEmitter } = createFakeTransport();\n const received: object[] = [];\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: (message) => {\n received.push(message);\n },\n subsend: async () => {},\n });\n\n await layer.setup();\n getEmitter().emit(\"connected\");\n expect(layer.status.get()).toBe(Status.CONNECTED);\n\n await layer.send({ type: \"request\", messageId: \"1\", payload: { a: 1 } });\n expect(sent).toHaveLength(1);\n // Ciphertext on the wire, not the plaintext envelope.\n expect(sent[0]).not.toContain(\"messageId\");\n\n getEmitter().emit(\"message\", sent[0]);\n await new Promise(resolve => setTimeout(resolve, 10));\n expect(received).toEqual([{ type: \"request\", messageId: \"1\", payload: { a: 1 } }]);\n });\n\n it(\"rejects sends before the transport is connected\", async () => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n const { factory } = createFakeTransport();\n\n const layer = factory.create({\n encrypt: encryptionKey.encrypt,\n decrypt: decryptionKey.decrypt,\n isHost: true,\n onmessage: () => {},\n subsend: async () => {},\n });\n\n await layer.setup();\n await expect(layer.send({ a: 1 })).rejects.toThrow(\"Transport not connected\");\n });\n});\n"]}
package/dist/index.d.ts CHANGED
@@ -4,23 +4,23 @@
4
4
  * A privacy-first, peer-to-peer protocol for connecting dApps with wallets
5
5
  * without relying on centralized infrastructure.
6
6
  */
7
+ import { type Observable } from "@openlv/core";
7
8
  import type { DecryptionKey, EncryptionKey } from "@openlv/core/encryption";
8
9
  import { EventEmitter } from "eventemitter3";
9
- import type { MaybePromise } from "viem";
10
+ import type { TransportLayerImplFunction } from "./layer.js";
10
11
  import type { WebRTCConfig } from "./webrtc/index.js";
11
- export declare const TRANSPORT_STATE: {
12
+ export declare const Status: {
12
13
  readonly STANDBY: "standby";
13
14
  readonly CONNECTING: "connecting";
14
15
  readonly READY: "ready";
15
16
  readonly CONNECTED: "connected";
16
17
  readonly ERROR: "error";
17
18
  };
18
- export type TransportState = (typeof TRANSPORT_STATE)[keyof typeof TRANSPORT_STATE];
19
+ export type Status = (typeof Status)[keyof typeof Status];
19
20
  export type TLayerEventMap = {
20
- state_change: (state: TransportState) => void;
21
21
  error: (reason?: string) => void;
22
22
  };
23
- export type TransportLayerSetupParameters = {
23
+ export type TransportLayerParameters = {
24
24
  isHost: boolean;
25
25
  encrypt: EncryptionKey["encrypt"];
26
26
  decrypt: DecryptionKey["decrypt"];
@@ -42,41 +42,22 @@ export type TransportMessage = {
42
42
  };
43
43
  export type TransportLayer = {
44
44
  type: TransportProtocol;
45
- setup: () => MaybePromise<void>;
46
- teardown: () => MaybePromise<void>;
45
+ setup: () => Promise<void>;
46
+ teardown: () => Promise<void>;
47
47
  send: (message: object) => Promise<void>;
48
48
  handle: (message: TransportMessage) => Promise<void>;
49
- waitFor: (state: TransportState) => Promise<void>;
50
49
  emitter: EventEmitter<TLayerEventMap>;
50
+ status: Observable<Status>;
51
51
  };
52
- /** Wire identifier advertised in the `capabilities` handshake packet. */
53
52
  export type TransportProtocol = "wrtc" | "ws" | (string & {});
54
- export type TransportLayerFn = {
53
+ export type TransportLayerFunction = {
55
54
  transportId: TransportProtocol;
56
- create: (parameters: TransportLayerSetupParameters) => TransportLayer;
55
+ create: (parameters: TransportLayerParameters) => TransportLayer;
57
56
  };
58
- export type Transport = (config?: WebRTCConfig) => TransportLayerFn;
59
- export type TransportLayerImpl = {
60
- setup: () => MaybePromise<void>;
61
- teardown: () => MaybePromise<void>;
62
- handle: (message: TransportMessage) => Promise<void>;
63
- send: (message: string) => Promise<void>;
64
- };
65
- export type TransportLayerBaseEventMap = {
66
- negotiate: (message: TransportMessage) => void;
67
- connected: () => void;
68
- message: (message: string) => void;
69
- error: (reason?: string) => void;
70
- };
71
- export type TransportLayerBaseEmitter = EventEmitter<TransportLayerBaseEventMap>;
72
- export type TransportLayerBaseParameters = {
73
- emitter: TransportLayerBaseEmitter;
74
- isHost: boolean;
75
- };
76
- export type TransportLayerImplFn = (parameters: TransportLayerBaseParameters) => TransportLayerImpl;
57
+ export type Transport = (config?: WebRTCConfig) => TransportLayerFunction;
77
58
  /**
78
59
  * Base Transport Layer implementation
79
60
  *
80
61
  * https://openlv.sh/api/transport
81
62
  */
82
- export declare const createTransportBase: (transportId: TransportProtocol, init: TransportLayerImplFn) => TransportLayerFn;
63
+ export declare const createTransportBase: (transportId: TransportProtocol, init: TransportLayerImplFunction) => TransportLayerFunction;
package/dist/index.js CHANGED
@@ -1,6 +1,13 @@
1
+ /**
2
+ * Open Lavatory Protocol - Decentralized Wallet Connection Library
3
+ *
4
+ * A privacy-first, peer-to-peer protocol for connecting dApps with wallets
5
+ * without relying on centralized infrastructure.
6
+ */
7
+ import { observable } from "@openlv/core";
1
8
  import { EventEmitter } from "eventemitter3";
2
9
  import { log } from "./utils/log.js";
3
- export const TRANSPORT_STATE = {
10
+ export const Status = {
4
11
  STANDBY: "standby",
5
12
  CONNECTING: "connecting",
6
13
  READY: "ready",
@@ -17,24 +24,25 @@ export const createTransportBase = (transportId, init) => ({
17
24
  create: ({ encrypt, decrypt, subsend, isHost, onmessage }) => {
18
25
  const emitter = new EventEmitter();
19
26
  const internalEmitter = new EventEmitter();
20
- let state = TRANSPORT_STATE.STANDBY;
21
- const setState = (newState) => {
22
- state = newState;
23
- emitter.emit("state_change", newState);
24
- };
25
- internalEmitter.on("negotiate", (message) => {
26
- subsend(message).catch(error => log("failed to relay negotiation message", error));
27
+ const [status, setStatus] = observable(Status.STANDBY);
28
+ internalEmitter.on("negotiate", async (message) => {
29
+ try {
30
+ await subsend(message);
31
+ }
32
+ catch (error) {
33
+ log("failed to relay negotiation message", error);
34
+ }
27
35
  });
28
36
  internalEmitter.on("connected", () => {
29
37
  log("onConnected");
30
- setState(TRANSPORT_STATE.CONNECTED);
38
+ setStatus(Status.CONNECTED);
31
39
  });
32
40
  internalEmitter.on("error", (reason) => {
33
41
  log("transport error", reason);
34
42
  // Surface the reason before the state flips so listeners reading
35
43
  // state on state_change already see it.
36
44
  emitter.emit("error", reason);
37
- setState(TRANSPORT_STATE.ERROR);
45
+ setStatus(Status.ERROR);
38
46
  });
39
47
  internalEmitter.on("message", async (message) => {
40
48
  // Peer data is untrusted until decrypted AND parsed; drop anything that
@@ -47,48 +55,32 @@ export const createTransportBase = (transportId, init) => ({
47
55
  log("dropping undecryptable transport message", error);
48
56
  }
49
57
  });
50
- const { setup, teardown, send: sendLayer, handle, } = init({
58
+ const { send: sendLayer, handle, ...channel } = init({
51
59
  emitter: internalEmitter,
52
60
  isHost,
53
61
  });
54
62
  const send = async (message) => {
55
- if (state !== TRANSPORT_STATE.CONNECTED)
63
+ if (status.get() !== Status.CONNECTED)
56
64
  throw new Error("Transport not connected");
57
65
  const payload = await encrypt(JSON.stringify(message));
58
66
  await sendLayer(payload);
59
67
  };
60
- const waitFor = async (targetState) => {
61
- if (state === targetState)
62
- return;
63
- if (state === TRANSPORT_STATE.ERROR) {
64
- throw new Error("Transport is in error state");
65
- }
66
- return new Promise((resolve, reject) => {
67
- const handler = (newState) => {
68
- if (newState === targetState) {
69
- emitter.off("state_change", handler);
70
- resolve();
71
- }
72
- else if (newState === TRANSPORT_STATE.ERROR) {
73
- emitter.off("state_change", handler);
74
- reject(new Error("Transport is in error state"));
75
- }
76
- };
77
- emitter.on("state_change", handler);
78
- });
68
+ const setup = async () => {
69
+ setStatus(Status.CONNECTING);
70
+ await channel.setup();
71
+ setStatus(Status.READY);
72
+ };
73
+ const teardown = async () => {
74
+ await channel.teardown();
79
75
  };
80
76
  return {
81
77
  type: transportId,
82
- async setup() {
83
- setState(TRANSPORT_STATE.CONNECTING);
84
- await setup();
85
- setState(TRANSPORT_STATE.READY);
86
- },
78
+ setup,
87
79
  teardown,
88
80
  handle,
89
81
  send,
90
- waitFor,
91
82
  emitter,
83
+ status,
92
84
  };
93
85
  },
94
86
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Open Lavatory Protocol - Decentralized Wallet Connection Library\n *\n * A privacy-first, peer-to-peer protocol for connecting dApps with wallets\n * without relying on centralized infrastructure.\n */\nimport type { DecryptionKey, EncryptionKey } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport type { MaybePromise } from \"viem\";\n\nimport { log } from \"./utils/log.js\";\nimport type { WebRTCConfig } from \"./webrtc/index.js\";\n\nexport const TRANSPORT_STATE = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n CONNECTED: \"connected\",\n ERROR: \"error\",\n} as const;\nexport type TransportState =\n (typeof TRANSPORT_STATE)[keyof typeof TRANSPORT_STATE];\n\nexport type TLayerEventMap = {\n state_change: (state: TransportState) => void;\n error: (reason?: string) => void;\n};\n\nexport type TransportLayerSetupParameters = {\n isHost: boolean;\n encrypt: EncryptionKey[\"encrypt\"];\n decrypt: DecryptionKey[\"decrypt\"];\n subsend: (message: TransportMessage) => Promise<void>;\n onmessage: (message: { type: string; payload: object; messageId: string; }) => void;\n};\n\n/**\n * A transport negotiation message, relayed over signaling before the direct\n * connection exists. The shape of `type`/`payload` is owned by the selected\n * transport; unknown types must be ignored, not fatal.\n */\nexport type TransportMessage = {\n type: string;\n payload: string;\n};\nexport type TransportLayer = {\n type: TransportProtocol;\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n send: (message: object) => Promise<void>;\n handle: (message: TransportMessage) => Promise<void>;\n waitFor: (state: TransportState) => Promise<void>;\n emitter: EventEmitter<TLayerEventMap>;\n};\n/** Wire identifier advertised in the `capabilities` handshake packet. */\nexport type TransportProtocol = \"wrtc\" | \"ws\" | (string & {});\nexport type TransportLayerFn = {\n transportId: TransportProtocol;\n create: (parameters: TransportLayerSetupParameters) => TransportLayer;\n};\nexport type Transport = (config?: WebRTCConfig) => TransportLayerFn;\n\nexport type TransportLayerImpl = {\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n handle: (message: TransportMessage) => Promise<void>;\n send: (message: string) => Promise<void>;\n};\nexport type TransportLayerBaseEventMap = {\n negotiate: (message: TransportMessage) => void;\n connected: () => void;\n message: (message: string) => void;\n error: (reason?: string) => void;\n};\nexport type TransportLayerBaseEmitter =\n EventEmitter<TransportLayerBaseEventMap>;\nexport type TransportLayerBaseParameters = {\n emitter: TransportLayerBaseEmitter;\n isHost: boolean;\n};\nexport type TransportLayerImplFn = (\n parameters: TransportLayerBaseParameters,\n) => TransportLayerImpl;\n\n/**\n * Base Transport Layer implementation\n *\n * https://openlv.sh/api/transport\n */\nexport const createTransportBase = (\n transportId: TransportProtocol,\n init: TransportLayerImplFn,\n): TransportLayerFn => ({\n transportId,\n create: ({ encrypt, decrypt, subsend, isHost, onmessage }) => {\n const emitter = new EventEmitter<TLayerEventMap>();\n const internalEmitter = new EventEmitter<TransportLayerBaseEventMap>();\n let state: TransportState = TRANSPORT_STATE.STANDBY;\n\n const setState = (newState: TransportState) => {\n state = newState;\n emitter.emit(\"state_change\", newState);\n };\n\n internalEmitter.on(\"negotiate\", (message) => {\n subsend(message).catch(error => log(\"failed to relay negotiation message\", error));\n });\n internalEmitter.on(\"connected\", () => {\n log(\"onConnected\");\n setState(TRANSPORT_STATE.CONNECTED);\n });\n internalEmitter.on(\"error\", (reason) => {\n log(\"transport error\", reason);\n // Surface the reason before the state flips so listeners reading\n // state on state_change already see it.\n emitter.emit(\"error\", reason);\n setState(TRANSPORT_STATE.ERROR);\n });\n internalEmitter.on(\"message\", async (message) => {\n // Peer data is untrusted until decrypted AND parsed; drop anything that\n // fails either step rather than surfacing an unhandled rejection.\n try {\n const data = await decrypt(message);\n\n onmessage(JSON.parse(data) as { type: string; payload: object; messageId: string; });\n }\n catch (error) {\n log(\"dropping undecryptable transport message\", error);\n }\n });\n\n const {\n setup,\n teardown,\n send: sendLayer,\n handle,\n } = init({\n emitter: internalEmitter,\n isHost,\n });\n\n const send = async (message: object) => {\n if (state !== TRANSPORT_STATE.CONNECTED)\n throw new Error(\"Transport not connected\");\n\n const payload = await encrypt(JSON.stringify(message));\n\n await sendLayer(payload);\n };\n\n const waitFor = async (targetState: TransportState) => {\n if (state === targetState) return;\n\n if (state === TRANSPORT_STATE.ERROR) {\n throw new Error(\"Transport is in error state\");\n }\n\n return new Promise<void>((resolve, reject) => {\n const handler = (newState: TransportState) => {\n if (newState === targetState) {\n emitter.off(\"state_change\", handler);\n resolve();\n }\n else if (newState === TRANSPORT_STATE.ERROR) {\n emitter.off(\"state_change\", handler);\n reject(new Error(\"Transport is in error state\"));\n }\n };\n\n emitter.on(\"state_change\", handler);\n });\n };\n\n return {\n type: transportId,\n async setup() {\n setState(TRANSPORT_STATE.CONNECTING);\n await setup();\n setState(TRANSPORT_STATE.READY);\n },\n teardown,\n handle,\n send,\n waitFor,\n emitter,\n };\n },\n});\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Open Lavatory Protocol - Decentralized Wallet Connection Library\n *\n * A privacy-first, peer-to-peer protocol for connecting dApps with wallets\n * without relying on centralized infrastructure.\n */\nimport { type Observable, observable } from \"@openlv/core\";\nimport type { DecryptionKey, EncryptionKey } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\n\nimport type { TransportLayerBaseEventMap, TransportLayerImplFunction } from \"./layer.js\";\nimport { log } from \"./utils/log.js\";\nimport type { WebRTCConfig } from \"./webrtc/index.js\";\n\nexport const Status = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n CONNECTED: \"connected\",\n ERROR: \"error\",\n} as const;\nexport type Status\n = (typeof Status)[keyof typeof Status];\n\nexport type TLayerEventMap = {\n error: (reason?: string) => void;\n};\n\nexport type TransportLayerParameters = {\n isHost: boolean;\n encrypt: EncryptionKey[\"encrypt\"];\n decrypt: DecryptionKey[\"decrypt\"];\n subsend: (message: TransportMessage) => Promise<void>;\n onmessage: (message: { type: string; payload: object; messageId: string; }) => void;\n};\n\n/**\n * A transport negotiation message, relayed over signaling before the direct\n * connection exists. The shape of `type`/`payload` is owned by the selected\n * transport; unknown types must be ignored, not fatal.\n */\nexport type TransportMessage = {\n type: string;\n payload: string;\n};\nexport type TransportLayer = {\n type: TransportProtocol;\n setup: () => Promise<void>;\n teardown: () => Promise<void>;\n send: (message: object) => Promise<void>;\n handle: (message: TransportMessage) => Promise<void>;\n emitter: EventEmitter<TLayerEventMap>;\n status: Observable<Status>;\n};\nexport type TransportProtocol = \"wrtc\" | \"ws\" | (string & {});\nexport type TransportLayerFunction = {\n transportId: TransportProtocol;\n create: (parameters: TransportLayerParameters) => TransportLayer;\n};\nexport type Transport = (config?: WebRTCConfig) => TransportLayerFunction;\n\n/**\n * Base Transport Layer implementation\n *\n * https://openlv.sh/api/transport\n */\nexport const createTransportBase = (\n transportId: TransportProtocol,\n init: TransportLayerImplFunction,\n): TransportLayerFunction => ({\n transportId,\n create: ({ encrypt, decrypt, subsend, isHost, onmessage }) => {\n const emitter = new EventEmitter<TLayerEventMap>();\n const internalEmitter = new EventEmitter<TransportLayerBaseEventMap>();\n\n const [status, setStatus] = observable<Status>(Status.STANDBY);\n\n internalEmitter.on(\"negotiate\", async (message) => {\n try {\n await subsend(message);\n }\n catch (error) {\n log(\"failed to relay negotiation message\", error);\n }\n });\n internalEmitter.on(\"connected\", () => {\n log(\"onConnected\");\n setStatus(Status.CONNECTED);\n });\n internalEmitter.on(\"error\", (reason) => {\n log(\"transport error\", reason);\n // Surface the reason before the state flips so listeners reading\n // state on state_change already see it.\n emitter.emit(\"error\", reason);\n setStatus(Status.ERROR);\n });\n internalEmitter.on(\"message\", async (message) => {\n // Peer data is untrusted until decrypted AND parsed; drop anything that\n // fails either step rather than surfacing an unhandled rejection.\n try {\n const data = await decrypt(message);\n\n onmessage(JSON.parse(data) as { type: string; payload: object; messageId: string; });\n }\n catch (error) {\n log(\"dropping undecryptable transport message\", error);\n }\n });\n\n const {\n send: sendLayer,\n handle,\n ...channel\n } = init({\n emitter: internalEmitter,\n isHost,\n });\n\n const send = async (message: object) => {\n if (status.get() !== Status.CONNECTED)\n throw new Error(\"Transport not connected\");\n\n const payload = await encrypt(JSON.stringify(message));\n\n await sendLayer(payload);\n };\n\n const setup = async () => {\n setStatus(Status.CONNECTING);\n await channel.setup();\n setStatus(Status.READY);\n };\n\n const teardown = async () => {\n await channel.teardown();\n };\n\n return {\n type: transportId,\n setup,\n teardown,\n handle,\n send,\n emitter,\n status,\n };\n },\n});\n"]}
@@ -1,7 +1,7 @@
1
1
  import { generateKeyPair } from "@openlv/core/encryption";
2
2
  import { EventEmitter } from "eventemitter3";
3
3
  import { describe, expect, it } from "vitest";
4
- import { TRANSPORT_STATE } from "./index.js";
4
+ import { Status } from "./index.js";
5
5
  import { webrtc } from "./webrtc/index.js";
6
6
  describe("Transport", () => {
7
7
  it("should be able to create a transport", async () => {
@@ -43,10 +43,14 @@ describe("Transport", () => {
43
43
  console.log("test: setup");
44
44
  await Promise.all([transportA.setup(), transportB.setup()]);
45
45
  console.log("test: waitFor connected");
46
- await Promise.all([
47
- transportA.waitFor(TRANSPORT_STATE.CONNECTED),
48
- transportB.waitFor(TRANSPORT_STATE.CONNECTED),
49
- ]);
46
+ await Promise.all([transportA, transportB].map(transport => new Promise((resolve) => {
47
+ if (transport.status.get() === Status.CONNECTED)
48
+ return resolve();
49
+ transport.status.subscribe((status) => {
50
+ if (status === Status.CONNECTED)
51
+ resolve();
52
+ });
53
+ })));
50
54
  console.log("test: connected");
51
55
  const awaitedMessageAtB = new Promise((resolve) => {
52
56
  signalB.on("message", (m) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { TRANSPORT_STATE } from \"./index.js\";\nimport { webrtc } from \"./webrtc/index.js\";\n\ndescribe(\"Transport\", () => {\n it(\"should be able to create a transport\", async () => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const { encrypt } = publicKey;\n const { decrypt } = decryptionKey;\n const signalA = new EventEmitter<{ signal: string; message: object; }>();\n const signalB = new EventEmitter<{ signal: string; message: object; }>();\n\n const transportA = webrtc().create({\n encrypt,\n decrypt,\n subsend: async (m) => {\n signalA.emit(\"signal\", m);\n },\n isHost: true,\n onmessage: (m) => {\n signalA.emit(\"message\", m);\n },\n });\n const transportB = webrtc().create({\n encrypt,\n decrypt,\n subsend: async (m) => {\n signalB.emit(\"signal\", m);\n },\n isHost: false,\n onmessage: (m) => {\n signalB.emit(\"message\", m);\n },\n });\n\n signalA.on(\"signal\", (m) => {\n console.log(\"messageA\", m);\n transportB.handle(m);\n });\n signalB.on(\"signal\", (m) => {\n console.log(\"messageB\", m);\n transportA.handle(m);\n });\n\n console.log(\"test: setup\");\n await Promise.all([transportA.setup(), transportB.setup()]);\n\n console.log(\"test: waitFor connected\");\n await Promise.all([\n transportA.waitFor(TRANSPORT_STATE.CONNECTED),\n transportB.waitFor(TRANSPORT_STATE.CONNECTED),\n ]);\n\n console.log(\"test: connected\");\n\n const awaitedMessageAtB = new Promise<object>((resolve) => {\n signalB.on(\"message\", (m) => {\n console.log(\"messageB\", m);\n resolve(m);\n });\n });\n\n console.log(\"test: send message to A\");\n await transportA.send({ data: \"test_123\" });\n\n console.log(\"test: await message at B\");\n const messageAtB = await awaitedMessageAtB;\n\n expect(messageAtB).toEqual({ data: \"test_123\" });\n\n console.log(\"test: message at B\", messageAtB);\n const awaitedMessageAtA = new Promise<object>((resolve) => {\n signalA.on(\"message\", (m) => {\n console.log(\"messageA\", m);\n resolve(m);\n });\n });\n\n await transportB.send({ data: \"test_456\" });\n const messageAtA = await awaitedMessageAtA;\n\n expect(messageAtA).toEqual({ data: \"test_456\" });\n\n await Promise.all([transportA.teardown(), transportB.teardown()]);\n\n expect(transportA).toBeDefined();\n expect(transportB).toBeDefined();\n });\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateKeyPair } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { Status } from \"./index.js\";\nimport { webrtc } from \"./webrtc/index.js\";\n\ndescribe(\"Transport\", () => {\n it(\"should be able to create a transport\", async () => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const { encrypt } = publicKey;\n const { decrypt } = decryptionKey;\n const signalA = new EventEmitter<{ signal: string; message: object; }>();\n const signalB = new EventEmitter<{ signal: string; message: object; }>();\n\n const transportA = webrtc().create({\n encrypt,\n decrypt,\n subsend: async (m) => {\n signalA.emit(\"signal\", m);\n },\n isHost: true,\n onmessage: (m) => {\n signalA.emit(\"message\", m);\n },\n });\n const transportB = webrtc().create({\n encrypt,\n decrypt,\n subsend: async (m) => {\n signalB.emit(\"signal\", m);\n },\n isHost: false,\n onmessage: (m) => {\n signalB.emit(\"message\", m);\n },\n });\n\n signalA.on(\"signal\", (m) => {\n console.log(\"messageA\", m);\n transportB.handle(m);\n });\n signalB.on(\"signal\", (m) => {\n console.log(\"messageB\", m);\n transportA.handle(m);\n });\n\n console.log(\"test: setup\");\n await Promise.all([transportA.setup(), transportB.setup()]);\n\n console.log(\"test: waitFor connected\");\n await Promise.all([transportA, transportB].map(transport => new Promise<void>((resolve) => {\n if (transport.status.get() === Status.CONNECTED) return resolve();\n\n transport.status.subscribe((status) => {\n if (status === Status.CONNECTED) resolve();\n });\n })));\n\n console.log(\"test: connected\");\n\n const awaitedMessageAtB = new Promise<object>((resolve) => {\n signalB.on(\"message\", (m) => {\n console.log(\"messageB\", m);\n resolve(m);\n });\n });\n\n console.log(\"test: send message to A\");\n await transportA.send({ data: \"test_123\" });\n\n console.log(\"test: await message at B\");\n const messageAtB = await awaitedMessageAtB;\n\n expect(messageAtB).toEqual({ data: \"test_123\" });\n\n console.log(\"test: message at B\", messageAtB);\n const awaitedMessageAtA = new Promise<object>((resolve) => {\n signalA.on(\"message\", (m) => {\n console.log(\"messageA\", m);\n resolve(m);\n });\n });\n\n await transportB.send({ data: \"test_456\" });\n const messageAtA = await awaitedMessageAtA;\n\n expect(messageAtA).toEqual({ data: \"test_456\" });\n\n await Promise.all([transportA.teardown(), transportB.teardown()]);\n\n expect(transportA).toBeDefined();\n expect(transportB).toBeDefined();\n });\n});\n"]}
@@ -0,0 +1,20 @@
1
+ import type { EventEmitter } from "eventemitter3";
2
+ import type { TransportMessage } from "./index.js";
3
+ export type TransportLayerImpl = {
4
+ setup: () => Promise<void>;
5
+ teardown: () => Promise<void>;
6
+ handle: (message: TransportMessage) => Promise<void>;
7
+ send: (message: string) => Promise<void>;
8
+ };
9
+ export type TransportLayerBaseEventMap = {
10
+ negotiate: (message: TransportMessage) => void;
11
+ connected: () => void;
12
+ message: (message: string) => void;
13
+ error: (reason?: string) => void;
14
+ };
15
+ export type TransportLayerBaseEmitter = EventEmitter<TransportLayerBaseEventMap>;
16
+ export type TransportLayerBaseParameters = {
17
+ emitter: TransportLayerBaseEmitter;
18
+ isHost: boolean;
19
+ };
20
+ export type TransportLayerImplFunction = (parameters: TransportLayerBaseParameters) => TransportLayerImpl;
package/dist/layer.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.js","sourceRoot":"","sources":["../src/layer.ts"],"names":[],"mappings":"","sourcesContent":["import type { EventEmitter } from \"eventemitter3\";\n\nimport type { TransportMessage } from \"./index.js\";\n\nexport type TransportLayerImpl = {\n setup: () => Promise<void>;\n teardown: () => Promise<void>;\n handle: (message: TransportMessage) => Promise<void>;\n send: (message: string) => Promise<void>;\n};\nexport type TransportLayerBaseEventMap = {\n negotiate: (message: TransportMessage) => void;\n connected: () => void;\n message: (message: string) => void;\n error: (reason?: string) => void;\n};\nexport type TransportLayerBaseEmitter\n = EventEmitter<TransportLayerBaseEventMap>;\nexport type TransportLayerBaseParameters = {\n emitter: TransportLayerBaseEmitter;\n isHost: boolean;\n};\nexport type TransportLayerImplFunction = (\n parameters: TransportLayerBaseParameters,\n) => TransportLayerImpl;\n"]}
@@ -1 +1 @@
1
- export declare const log: (...args: Parameters<typeof console.log>) => void;
1
+ export declare const log: (...arguments_: Parameters<typeof console.log>) => void;
@@ -64,14 +64,14 @@ export const webrtc = (config = defaultConfig) => {
64
64
  // Deliberately not debug-gated: without a single local candidate the
65
65
  // connection can never establish, and the cause is environmental
66
66
  // (blocked UDP, no usable interface, unreachable STUN/TURN).
67
- console.warn("[openlv] WebRTC gathered zero local ICE candidates — "
67
+ console.warn("[openlv] WebRTC gathered zero local ICE candidates -- "
68
68
  + "the peer-to-peer connection cannot establish. "
69
69
  + "Check network/UDP access or configure reachable STUN/TURN servers.");
70
70
  emitter.emit("error", "no local ICE candidates");
71
71
  }
72
72
  };
73
- const onDataChannel = (e) => {
74
- channel = e.channel;
73
+ const onDataChannel = (event) => {
74
+ channel = event.channel;
75
75
  hookChannel(channel);
76
76
  log("onDataChannel");
77
77
  };
@@ -79,8 +79,8 @@ export const webrtc = (config = defaultConfig) => {
79
79
  log("onDataChannelOpen");
80
80
  emitter.emit("connected");
81
81
  };
82
- const onDataChannelMessage = (e) => {
83
- emitter.emit("message", e.data);
82
+ const onDataChannelMessage = (event) => {
83
+ emitter.emit("message", event.data);
84
84
  };
85
85
  const onDataChannelClose = () => {
86
86
  emitter.emit("error", "Data channel closed");
@@ -141,36 +141,38 @@ export const webrtc = (config = defaultConfig) => {
141
141
  throw new Error("Channel not found");
142
142
  channel.send(message);
143
143
  };
144
+ const setup = async () => {
145
+ log("webrtc setup");
146
+ connection = new RTCPeerConnection(rtcConfig);
147
+ connection.onconnectionstatechange = onConnectionStateChange;
148
+ connection.onicecandidate = onIceCandidate;
149
+ connection.onicegatheringstatechange = onIceGatheringStateChange;
150
+ connection.ondatachannel = onDataChannel;
151
+ connection.onnegotiationneeded = onNegotiationNeeded;
152
+ if (isHost) {
153
+ channel = connection.createDataChannel("openlv-data");
154
+ hookChannel(channel);
155
+ }
156
+ };
157
+ const teardown = async () => {
158
+ log("webrtc teardown");
159
+ pendingCandidates = [];
160
+ if (channel) {
161
+ // Detach the close listener first: a deliberate teardown must not
162
+ // surface as a transport error.
163
+ channel.removeEventListener("close", onDataChannelClose);
164
+ channel.close();
165
+ channel = undefined;
166
+ }
167
+ if (connection) {
168
+ connection.onconnectionstatechange = null;
169
+ connection.close();
170
+ connection = undefined;
171
+ }
172
+ };
144
173
  return {
145
- async setup() {
146
- log("webrtc setup");
147
- connection = new RTCPeerConnection(rtcConfig);
148
- connection.onconnectionstatechange = onConnectionStateChange;
149
- connection.onicecandidate = onIceCandidate;
150
- connection.onicegatheringstatechange = onIceGatheringStateChange;
151
- connection.ondatachannel = onDataChannel;
152
- connection.onnegotiationneeded = onNegotiationNeeded;
153
- if (isHost) {
154
- channel = connection.createDataChannel("openlv-data");
155
- hookChannel(channel);
156
- }
157
- },
158
- teardown() {
159
- log("webrtc teardown");
160
- pendingCandidates = [];
161
- if (channel) {
162
- // Detach the close listener first: a deliberate teardown must not
163
- // surface as a transport error.
164
- channel.removeEventListener("close", onDataChannelClose);
165
- channel.close();
166
- channel = undefined;
167
- }
168
- if (connection) {
169
- connection.onconnectionstatechange = null;
170
- connection.close();
171
- connection = undefined;
172
- }
173
- },
174
+ setup,
175
+ teardown,
174
176
  handle,
175
177
  send,
176
178
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { match } from \"ts-pattern\";\n\nimport {\n createTransportBase,\n type Transport,\n type TransportMessage,\n} from \"../index.js\";\nimport { log } from \"../utils/log.js\";\n\nexport type WebRTCConfig = {\n iceServers?: RTCConfiguration[\"iceServers\"];\n};\n\n// Deliberately STUN-only: TURN relays all traffic through a third party, so\n// operators should opt in explicitly with their own infrastructure.\n// (The previously bundled openrelay.metered.ca TURN service is discontinued.)\nconst defaultConfig: WebRTCConfig = {\n iceServers: [\n { urls: \"stun:stun.l.google.com:19302\" },\n { urls: \"stun:stun1.l.google.com:19302\" },\n { urls: \"stun:stun.services.mozilla.com:3478\" },\n {\n urls: [\"turn:openrelay.metered.ca:80\", \"turn:openrelay.metered.ca:443\"],\n username: \"openrelayproject\",\n credential: \"openrelayproject\",\n },\n ],\n};\n\nexport const webrtc: Transport = (\n config: WebRTCConfig = defaultConfig,\n) => {\n const { iceServers = defaultConfig.iceServers } = config;\n\n return createTransportBase(\"wrtc\", ({ emitter, isHost }) => {\n const rtcConfig: RTCConfiguration = { iceServers };\n let connection: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n // Candidates can arrive over signaling before the offer/answer has been\n // applied; addIceCandidate would reject, so they are buffered until a\n // remote description exists.\n let pendingCandidates: RTCIceCandidateInit[] = [];\n\n const flushPendingCandidates = async () => {\n const queued = pendingCandidates;\n\n pendingCandidates = [];\n\n for (const candidate of queued) {\n await connection?.addIceCandidate(new RTCIceCandidate(candidate));\n }\n };\n\n const onConnectionStateChange = () => {\n const state = connection?.connectionState;\n\n log(\"onConnectionStateChange\", state);\n\n match(state)\n // \"disconnected\" is transient and may recover on its own; only\n // \"failed\"/\"closed\" are terminal.\n .with(\"failed\", () => emitter.emit(\"error\", \"WebRTC connection failed\"))\n .with(\"closed\", () => emitter.emit(\"error\", \"WebRTC connection closed\"))\n .otherwise(() => {});\n };\n let localCandidates = 0;\n\n const onIceCandidate = (c: RTCPeerConnectionIceEvent) => {\n if (channel?.readyState === \"open\") return;\n\n // End-of-candidates (null) needs no relay; the peer simply stops\n // receiving candidates.\n if (!c.candidate) return;\n\n localCandidates += 1;\n log(\"local ICE candidate\", c.candidate.type, c.candidate.protocol);\n emitter.emit(\"negotiate\", {\n type: \"candidate\",\n payload: JSON.stringify(c.candidate.toJSON()),\n });\n };\n const onIceGatheringStateChange = () => {\n log(\"iceGatheringState\", connection?.iceGatheringState);\n\n if (connection?.iceGatheringState === \"complete\" && localCandidates === 0) {\n // Deliberately not debug-gated: without a single local candidate the\n // connection can never establish, and the cause is environmental\n // (blocked UDP, no usable interface, unreachable STUN/TURN).\n console.warn(\n \"[openlv] WebRTC gathered zero local ICE candidates — \"\n + \"the peer-to-peer connection cannot establish. \"\n + \"Check network/UDP access or configure reachable STUN/TURN servers.\",\n );\n emitter.emit(\"error\", \"no local ICE candidates\");\n }\n };\n const onDataChannel = (e: RTCDataChannelEvent) => {\n channel = e.channel;\n hookChannel(channel);\n log(\"onDataChannel\");\n };\n const onDataChannelOpen = () => {\n log(\"onDataChannelOpen\");\n emitter.emit(\"connected\");\n };\n const onDataChannelMessage = (e: MessageEvent<string>) => {\n emitter.emit(\"message\", e.data);\n };\n const onDataChannelClose = () => {\n emitter.emit(\"error\", \"Data channel closed\");\n };\n const onNegotiationNeeded = async () => {\n log(\"onNegotiationNeeded\");\n\n if (isHost && connection) {\n await connection.setLocalDescription();\n\n if (connection.localDescription) {\n emitter.emit(\"negotiate\", {\n type: \"offer\",\n payload: JSON.stringify(connection.localDescription),\n });\n }\n }\n };\n\n const hookChannel = (channel: RTCDataChannel) => {\n channel.addEventListener(\"open\", onDataChannelOpen);\n channel.addEventListener(\"message\", onDataChannelMessage);\n channel.addEventListener(\"close\", onDataChannelClose);\n };\n\n const handle = async (message: TransportMessage): Promise<void> => {\n log(\"webrtc handle\", message.type);\n\n if (!connection) throw new Error(\"Connection not found\");\n\n return match(message)\n .with({ type: \"offer\" }, async ({ payload }) => {\n const offer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(offer),\n );\n await flushPendingCandidates();\n\n const answer = await connection!.createAnswer();\n\n await connection!.setLocalDescription(answer);\n emitter.emit(\"negotiate\", { type: \"answer\", payload: JSON.stringify(answer) });\n })\n .with({ type: \"answer\" }, async ({ payload }) => {\n const answer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(answer),\n );\n await flushPendingCandidates();\n })\n .with({ type: \"candidate\" }, async ({ payload }) => {\n if (!payload) return;\n\n const candidate = JSON.parse(payload) as RTCIceCandidateInit;\n\n if (!connection!.remoteDescription) {\n log(\"buffering remote ICE candidate until remote description is set\");\n pendingCandidates.push(candidate);\n\n return;\n }\n\n log(\"applying remote ICE candidate\");\n await connection!.addIceCandidate(new RTCIceCandidate(candidate));\n })\n .otherwise(() => {\n log(\"received unknown transport message type\", message);\n });\n };\n\n const send = async (message: string) => {\n if (!channel) throw new Error(\"Channel not found\");\n\n channel.send(message);\n };\n\n return {\n async setup() {\n log(\"webrtc setup\");\n\n connection = new RTCPeerConnection(rtcConfig);\n connection.onconnectionstatechange = onConnectionStateChange;\n connection.onicecandidate = onIceCandidate;\n connection.onicegatheringstatechange = onIceGatheringStateChange;\n connection.ondatachannel = onDataChannel;\n connection.onnegotiationneeded = onNegotiationNeeded;\n\n if (isHost) {\n channel = connection.createDataChannel(\"openlv-data\");\n hookChannel(channel);\n }\n },\n teardown() {\n log(\"webrtc teardown\");\n pendingCandidates = [];\n\n if (channel) {\n // Detach the close listener first: a deliberate teardown must not\n // surface as a transport error.\n channel.removeEventListener(\"close\", onDataChannelClose);\n channel.close();\n channel = undefined;\n }\n\n if (connection) {\n connection.onconnectionstatechange = null;\n connection.close();\n connection = undefined;\n }\n },\n handle,\n send,\n };\n });\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { match } from \"ts-pattern\";\n\nimport {\n createTransportBase,\n type Transport,\n type TransportMessage,\n} from \"../index.js\";\nimport { log } from \"../utils/log.js\";\n\nexport type WebRTCConfig = {\n iceServers?: RTCConfiguration[\"iceServers\"];\n};\n\n// Deliberately STUN-only: TURN relays all traffic through a third party, so\n// operators should opt in explicitly with their own infrastructure.\n// (The previously bundled openrelay.metered.ca TURN service is discontinued.)\nconst defaultConfig: WebRTCConfig = {\n iceServers: [\n { urls: \"stun:stun.l.google.com:19302\" },\n { urls: \"stun:stun1.l.google.com:19302\" },\n { urls: \"stun:stun.services.mozilla.com:3478\" },\n {\n urls: [\"turn:openrelay.metered.ca:80\", \"turn:openrelay.metered.ca:443\"],\n username: \"openrelayproject\",\n credential: \"openrelayproject\",\n },\n ],\n};\n\nexport const webrtc: Transport = (\n config: WebRTCConfig = defaultConfig,\n) => {\n const { iceServers = defaultConfig.iceServers } = config;\n\n return createTransportBase(\"wrtc\", ({ emitter, isHost }) => {\n const rtcConfig: RTCConfiguration = { iceServers };\n let connection: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n // Candidates can arrive over signaling before the offer/answer has been\n // applied; addIceCandidate would reject, so they are buffered until a\n // remote description exists.\n let pendingCandidates: RTCIceCandidateInit[] = [];\n\n const flushPendingCandidates = async () => {\n const queued = pendingCandidates;\n\n pendingCandidates = [];\n\n for (const candidate of queued) {\n await connection?.addIceCandidate(new RTCIceCandidate(candidate));\n }\n };\n\n const onConnectionStateChange = () => {\n const state = connection?.connectionState;\n\n log(\"onConnectionStateChange\", state);\n\n match(state)\n // \"disconnected\" is transient and may recover on its own; only\n // \"failed\"/\"closed\" are terminal.\n .with(\"failed\", () => emitter.emit(\"error\", \"WebRTC connection failed\"))\n .with(\"closed\", () => emitter.emit(\"error\", \"WebRTC connection closed\"))\n .otherwise(() => {});\n };\n let localCandidates = 0;\n\n const onIceCandidate = (c: RTCPeerConnectionIceEvent) => {\n if (channel?.readyState === \"open\") return;\n\n // End-of-candidates (null) needs no relay; the peer simply stops\n // receiving candidates.\n if (!c.candidate) return;\n\n localCandidates += 1;\n log(\"local ICE candidate\", c.candidate.type, c.candidate.protocol);\n emitter.emit(\"negotiate\", {\n type: \"candidate\",\n payload: JSON.stringify(c.candidate.toJSON()),\n });\n };\n const onIceGatheringStateChange = () => {\n log(\"iceGatheringState\", connection?.iceGatheringState);\n\n if (connection?.iceGatheringState === \"complete\" && localCandidates === 0) {\n // Deliberately not debug-gated: without a single local candidate the\n // connection can never establish, and the cause is environmental\n // (blocked UDP, no usable interface, unreachable STUN/TURN).\n console.warn(\n \"[openlv] WebRTC gathered zero local ICE candidates -- \"\n + \"the peer-to-peer connection cannot establish. \"\n + \"Check network/UDP access or configure reachable STUN/TURN servers.\",\n );\n emitter.emit(\"error\", \"no local ICE candidates\");\n }\n };\n const onDataChannel = (event: RTCDataChannelEvent) => {\n channel = event.channel;\n hookChannel(channel);\n log(\"onDataChannel\");\n };\n const onDataChannelOpen = () => {\n log(\"onDataChannelOpen\");\n emitter.emit(\"connected\");\n };\n const onDataChannelMessage = (event: MessageEvent<string>) => {\n emitter.emit(\"message\", event.data);\n };\n const onDataChannelClose = () => {\n emitter.emit(\"error\", \"Data channel closed\");\n };\n const onNegotiationNeeded = async () => {\n log(\"onNegotiationNeeded\");\n\n if (isHost && connection) {\n await connection.setLocalDescription();\n\n if (connection.localDescription) {\n emitter.emit(\"negotiate\", {\n type: \"offer\",\n payload: JSON.stringify(connection.localDescription),\n });\n }\n }\n };\n\n const hookChannel = (channel: RTCDataChannel) => {\n channel.addEventListener(\"open\", onDataChannelOpen);\n channel.addEventListener(\"message\", onDataChannelMessage);\n channel.addEventListener(\"close\", onDataChannelClose);\n };\n\n const handle = async (message: TransportMessage): Promise<void> => {\n log(\"webrtc handle\", message.type);\n\n if (!connection) throw new Error(\"Connection not found\");\n\n return match(message)\n .with({ type: \"offer\" }, async ({ payload }) => {\n const offer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(offer),\n );\n await flushPendingCandidates();\n\n const answer = await connection!.createAnswer();\n\n await connection!.setLocalDescription(answer);\n emitter.emit(\"negotiate\", { type: \"answer\", payload: JSON.stringify(answer) });\n })\n .with({ type: \"answer\" }, async ({ payload }) => {\n const answer = JSON.parse(payload) as RTCSessionDescriptionInit;\n\n await connection!.setRemoteDescription(\n new RTCSessionDescription(answer),\n );\n await flushPendingCandidates();\n })\n .with({ type: \"candidate\" }, async ({ payload }) => {\n if (!payload) return;\n\n const candidate = JSON.parse(payload) as RTCIceCandidateInit;\n\n if (!connection!.remoteDescription) {\n log(\"buffering remote ICE candidate until remote description is set\");\n pendingCandidates.push(candidate);\n\n return;\n }\n\n log(\"applying remote ICE candidate\");\n await connection!.addIceCandidate(new RTCIceCandidate(candidate));\n })\n .otherwise(() => {\n log(\"received unknown transport message type\", message);\n });\n };\n\n const send = async (message: string) => {\n if (!channel) throw new Error(\"Channel not found\");\n\n channel.send(message);\n };\n\n const setup = async () => {\n log(\"webrtc setup\");\n\n connection = new RTCPeerConnection(rtcConfig);\n connection.onconnectionstatechange = onConnectionStateChange;\n connection.onicecandidate = onIceCandidate;\n connection.onicegatheringstatechange = onIceGatheringStateChange;\n connection.ondatachannel = onDataChannel;\n connection.onnegotiationneeded = onNegotiationNeeded;\n\n if (isHost) {\n channel = connection.createDataChannel(\"openlv-data\");\n hookChannel(channel);\n }\n };\n\n const teardown = async () => {\n log(\"webrtc teardown\");\n pendingCandidates = [];\n\n if (channel) {\n // Detach the close listener first: a deliberate teardown must not\n // surface as a transport error.\n channel.removeEventListener(\"close\", onDataChannelClose);\n channel.close();\n channel = undefined;\n }\n\n if (connection) {\n connection.onconnectionstatechange = null;\n connection.close();\n connection = undefined;\n }\n };\n\n return {\n setup,\n teardown,\n handle,\n send,\n };\n });\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlv/transport",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
@@ -42,11 +42,11 @@
42
42
  "eventemitter3": "^5.0.1",
43
43
  "globals": "^16.2.0",
44
44
  "ts-pattern": "^5",
45
- "@openlv/core": "0.1.1"
45
+ "@openlv/core": "0.2.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@vitest/browser-playwright": "^4.0.8",
49
- "playwright": "1.58.1",
49
+ "playwright": "1.60.0",
50
50
  "typescript": "^5",
51
51
  "vitest": "^4"
52
52
  },