@openlv/session 0.1.3 → 0.3.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.
@@ -0,0 +1,2 @@
1
+ export declare const loadSignaling: (protocol: string) => Promise<import("@openlv/signaling").SignalingProtocol>;
2
+ export declare const loadTransport: (protocol: string) => Promise<import("@openlv/transport").Transport>;
@@ -0,0 +1,26 @@
1
+ import { match } from "ts-pattern";
2
+ export const loadSignaling = async (protocol) => match(protocol)
3
+ .with("mqtt", async () => {
4
+ const module_ = await import("@openlv/signaling/mqtt");
5
+ return module_.mqtt;
6
+ })
7
+ .with("ntfy", async () => {
8
+ const module_ = await import("@openlv/signaling/ntfy");
9
+ return module_.ntfy;
10
+ })
11
+ .with("gun", async () => {
12
+ const module_ = await import("@openlv/signaling/gundb");
13
+ return module_.gundb;
14
+ })
15
+ .otherwise(() => {
16
+ throw new Error(`Unknown signaling protocol: ${protocol}`);
17
+ });
18
+ export const loadTransport = async (protocol) => match(protocol)
19
+ .with("webrtc", async () => {
20
+ const module_ = await import("@openlv/transport/webrtc");
21
+ return module_.webrtc;
22
+ })
23
+ .otherwise(() => {
24
+ throw new Error(`Unknown transport protocol: ${protocol}`);
25
+ });
26
+ //# sourceMappingURL=dynamic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic.js","sourceRoot":"","sources":["../src/dynamic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;KACrE,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;IACvB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;IAEvD,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC,CAAC;KACD,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;IACvB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;IAEvD,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC,CAAC;KACD,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;IACtB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;IAExD,OAAO,OAAO,CAAC,KAAK,CAAC;AACvB,CAAC,CAAC;KACD,SAAS,CAAC,GAAG,EAAE;IACd,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;KACrE,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IACzB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAEzD,OAAO,OAAO,CAAC,MAAM,CAAC;AACxB,CAAC,CAAC;KACD,SAAS,CAAC,GAAG,EAAE;IACd,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC","sourcesContent":["import { match } from \"ts-pattern\";\n\nexport const loadSignaling = async (protocol: string) => match(protocol)\n .with(\"mqtt\", async () => {\n const module_ = await import(\"@openlv/signaling/mqtt\");\n\n return module_.mqtt;\n })\n .with(\"ntfy\", async () => {\n const module_ = await import(\"@openlv/signaling/ntfy\");\n\n return module_.ntfy;\n })\n .with(\"gun\", async () => {\n const module_ = await import(\"@openlv/signaling/gundb\");\n\n return module_.gundb;\n })\n .otherwise(() => {\n throw new Error(`Unknown signaling protocol: ${protocol}`);\n });\n\nexport const loadTransport = async (protocol: string) => match(protocol)\n .with(\"webrtc\", async () => {\n const module_ = await import(\"@openlv/transport/webrtc\");\n\n return module_.webrtc;\n })\n .otherwise(() => {\n throw new Error(`Unknown transport protocol: ${protocol}`);\n });\n"]}
package/dist/events.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- import type { SessionStateObject } from "./index.js";
2
1
  export type SessionEvents = {
3
- state_change: (state?: SessionStateObject) => void;
4
2
  /**
5
- * Emitted when a request arrives from the remote peer — before the reply
6
- * is sent. Useful for observability, logging, or UI integration without
7
- * needing to intercept the `onMessage` callback.
8
- */
9
- request: (payload: object | string) => void;
3
+ * Emitted when a request arrives from the remote peer -- before the reply
4
+ * is sent. Useful for observability, logging, or UI integration without
5
+ * needing to intercept the `onMessage` callback.
6
+ */
7
+ request: (payload: unknown) => void;
10
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"","sourcesContent":["import type { SessionStateObject } from \"./index.js\";\n\nexport type SessionEvents = {\n state_change: (state?: SessionStateObject) => void;\n /**\n * Emitted when a request arrives from the remote peer — before the reply\n * is sent. Useful for observability, logging, or UI integration without\n * needing to intercept the `onMessage` callback.\n */\n request: (payload: object | string) => void;\n};\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"","sourcesContent":["export type SessionEvents = {\n /**\n * Emitted when a request arrives from the remote peer -- before the reply\n * is sent. Useful for observability, logging, or UI integration without\n * needing to intercept the `onMessage` callback.\n */\n request: (payload: unknown) => void;\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { type SessionHandshakeParameters, type SessionLinkParameters } from "@openlv/core";
2
- import { type PeerInfo, type SignalingLayer, type SignalingProtocol, type SignalState } from "@openlv/signaling";
3
- import { type TransportLayer, type TransportLayerFn } from "@openlv/transport";
1
+ import { type Observable, type SessionHandshakeParameters, type SessionLinkParameters } from "@openlv/core";
2
+ import { type PeerInfo, type SignalingLayer, Status as SignalStatus } from "@openlv/signaling";
3
+ import { type TransportLayer, type TransportLayerFunction } from "@openlv/transport";
4
4
  import { EventEmitter } from "eventemitter3";
5
5
  import type { SessionEvents } from "./events.js";
6
- export type { PeerCapabilities, PeerInfo } from "@openlv/signaling";
7
- export declare const SESSION_STATE: {
6
+ export { loadSignaling, loadTransport } from "./dynamic.js";
7
+ export { RequestFailedError } from "./messages/correlate.js";
8
+ export type { SessionMessage } from "./messages/index.js";
9
+ export type { PeerInfo } from "@openlv/signaling";
10
+ export declare const SessionStatus: {
8
11
  readonly CREATED: "created";
9
12
  readonly SIGNALING: "signaling";
10
13
  readonly READY: "ready";
@@ -12,19 +15,8 @@ export declare const SESSION_STATE: {
12
15
  readonly CONNECTED: "connected";
13
16
  readonly DISCONNECTED: "disconnected";
14
17
  };
15
- export type SessionState = (typeof SESSION_STATE)[keyof typeof SESSION_STATE];
16
- export type SessionStateObject = {
17
- status: SessionState;
18
- signaling?: {
19
- state: SignalState;
20
- };
21
- /** The peer's self-description, set once capabilities are exchanged. */
22
- peerInfo?: PeerInfo;
23
- /** Human-readable reason for the last disconnect/failure, if any. */
24
- error?: string;
25
- };
18
+ export type SessionStatus = (typeof SessionStatus)[keyof typeof SessionStatus];
26
19
  export type SessionOptions = {
27
- /** Shared with the peer during the handshake and shown in its UI. */
28
20
  info?: PeerInfo;
29
21
  };
30
22
  /**
@@ -33,22 +25,14 @@ export type SessionOptions = {
33
25
  * https://openlv.sh/api/session
34
26
  */
35
27
  export type Session = {
36
- getState(): SessionStateObject;
28
+ status: Observable<SessionStatus>;
29
+ signalStatus: Observable<SignalStatus>;
30
+ error: Observable<string | undefined>;
31
+ peerInfo: Observable<PeerInfo | undefined>;
37
32
  getHandshakeParameters(): SessionHandshakeParameters;
38
33
  connect(): Promise<void>;
39
- waitForLink(): Promise<void>;
40
34
  close(): Promise<void>;
41
- /**
42
- * Send a request to the remote peer and await a correlated response.
43
- *
44
- * Two-phase timeout:
45
- * - `ackTimeout` (default 10 s): the remote peer must send back an ack
46
- * within this window, confirming it received the message.
47
- * - After an ack arrives the wait is extended to `responseTimeout`
48
- * (default 1 hour) — enough for user-interactive flows such as
49
- * `eth_sendTransaction` or `personal_sign`.
50
- */
51
- send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
35
+ send(message: unknown, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
52
36
  emitter: EventEmitter<SessionEvents>;
53
37
  _internal: {
54
38
  signal: SignalingLayer;
@@ -61,8 +45,8 @@ export type Session = {
61
45
  *
62
46
  * https://openlv.sh/api/session
63
47
  */
64
- export declare const createSession: (initParameters: SessionLinkParameters, signalLayer: SignalingProtocol, transportLayers: TransportLayerFn[], onMessage: (message: object) => Promise<object | string>, options?: SessionOptions) => Promise<Session>;
48
+ export declare const createSession: (initParameters: SessionLinkParameters, transportLayers: TransportLayerFunction[], onMessage: (message: unknown) => Promise<unknown>, options?: SessionOptions) => Promise<Session>;
65
49
  /**
66
50
  * Connect to a session from its openlv:// URL
67
51
  */
68
- export declare const connectSession: (connectionUrl: string, onMessage: (message: object) => Promise<object | string>, transports: TransportLayerFn[], options?: SessionOptions) => Promise<Session>;
52
+ export declare const connectSession: (connectionUrl: string, onMessage: (message: unknown) => Promise<unknown>, transports: TransportLayerFunction[], options?: SessionOptions) => Promise<Session>;
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
- import { decodeConnectionURL, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
- import { deriveSymmetricKey, generateHandshakeKey, generateSessionId, initEncryptionKeys, initHash, parseEncryptionKey, } from "@openlv/core/encryption";
3
- import { SIGNAL_STATE, validatePeerInfo, } from "@openlv/signaling";
4
- import { dynamicSignalingLayer } from "@openlv/signaling/dynamic";
5
- import { TRANSPORT_STATE, } from "@openlv/transport";
1
+ import { createScope, decodeConnectionURL, observable, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
+ import { createHandshakeCipher, generateHandshakeKey, generateKeyPair, generateSessionId, hashPublicKey, } from "@openlv/core/encryption";
3
+ import { Status as SignalStatus, validatePeerInfo, } from "@openlv/signaling";
4
+ import { Status as TransportStatus, } from "@openlv/transport";
6
5
  import { EventEmitter } from "eventemitter3";
6
+ import { loadSignaling } from "./dynamic.js";
7
7
  import { awaitCorrelatedResponse } from "./messages/correlate.js";
8
- import { selectTransportId } from "./transports.js";
8
+ import { isSessionMessage } from "./messages/index.js";
9
9
  import { log } from "./utils/log.js";
10
- export const SESSION_STATE = {
10
+ export { loadSignaling, loadTransport } from "./dynamic.js";
11
+ export { RequestFailedError } from "./messages/correlate.js";
12
+ export const SessionStatus = {
11
13
  CREATED: "created",
12
14
  SIGNALING: "signaling",
13
15
  READY: "ready",
@@ -20,13 +22,11 @@ export const SESSION_STATE = {
20
22
  *
21
23
  * https://openlv.sh/api/session
22
24
  */
23
- export const createSession = async (initParameters, signalLayer, transportLayers, onMessage, options) => {
25
+ export const createSession = async (initParameters, transportLayers, onMessage, options) => {
24
26
  if (transportLayers.length === 0) {
25
27
  throw new Error("At least one transport is required");
26
28
  }
27
29
  if (options?.info) {
28
- // A receiver silently drops out-of-bounds info with the whole packet,
29
- // which would surface only as a handshake timeout — fail loudly here.
30
30
  const problem = validatePeerInfo(options.info);
31
31
  if (problem)
32
32
  throw new Error(`Invalid session info: ${problem}`);
@@ -36,101 +36,100 @@ export const createSession = async (initParameters, signalLayer, transportLayers
36
36
  const sessionId = "sessionId" in initParameters
37
37
  ? initParameters.sessionId
38
38
  : generateSessionId();
39
- const { encryptionKey, decryptionKey: { decrypt }, } = await initEncryptionKeys(initParameters);
40
- let relyingPublicKey;
41
- const handshakeKey = "k" in initParameters
42
- ? await deriveSymmetricKey(initParameters.k)
43
- : await generateHandshakeKey();
44
- const { hash, isHost } = await initHash("h" in initParameters ? initParameters.h : undefined, encryptionKey);
45
- let status = SESSION_STATE.CREATED;
46
- let lastError;
39
+ const keyPair = generateKeyPair();
40
+ const k = "k" in initParameters ? initParameters.k : generateHandshakeKey();
41
+ const ownHash = await hashPublicKey(keyPair.publicKey);
42
+ const hash = ("h" in initParameters && initParameters.h) || ownHash;
43
+ const isHost = hash === ownHash;
47
44
  const protocol = initParameters.p;
48
45
  const server = initParameters.s;
49
46
  const capabilities = {
50
47
  transports: transportLayers.map(layer => layer.transportId),
51
48
  ...(options?.info && { info: options.info }),
52
49
  };
53
- let peerCaps;
50
+ const [status, setStatus] = observable(SessionStatus.CREATED);
51
+ const [lastError, setLastError] = observable(undefined);
52
+ const [peerInfo, setPeerInfo] = observable(undefined);
53
+ const setLastErrorIfUnset = (reason) => {
54
+ if (lastError.get() === undefined)
55
+ setLastError(reason);
56
+ };
54
57
  const updateStatus = (newStatus) => {
55
58
  log("updateStatus", newStatus);
56
- status = newStatus;
57
- emitter.emit("state_change", {
58
- status,
59
- signaling: signal.getState(),
60
- peerInfo: peerCaps?.info,
61
- error: lastError,
62
- });
59
+ setStatus(newStatus);
63
60
  };
61
+ status.subscribe((current) => {
62
+ if (current === SessionStatus.DISCONNECTED) {
63
+ messages.emit("ended", lastError.get() ?? "Session closed");
64
+ }
65
+ });
66
+ const signalLayer = await loadSignaling(protocol);
64
67
  const signaling = await signalLayer({
65
68
  topic: sessionId,
66
69
  url: server,
67
70
  });
68
71
  const signal = await signaling({
69
72
  h: hash,
70
- canEncrypt() {
71
- return relyingPublicKey !== undefined;
72
- },
73
- async encrypt(message) {
74
- if (!relyingPublicKey) {
75
- throw new Error("Relying party public key not found");
76
- }
77
- return await relyingPublicKey.encrypt(message);
78
- },
79
- decrypt,
80
- publicKey: encryptionKey,
81
- k: handshakeKey,
82
- async rpDiscovered(rpKey) {
83
- const role = isHost ? "host" : "client";
84
- log(`rpKey discovered by ${role}`, rpKey);
85
- relyingPublicKey = await parseEncryptionKey(rpKey);
86
- },
73
+ handshakeCipher: await createHandshakeCipher(k),
74
+ keyPair,
87
75
  capabilities,
88
- peerCapabilities(received) {
89
- log("peer capabilities received", received);
90
- peerCaps = received;
91
- // Re-emit the current status so observers see peerInfo the moment the
92
- // capabilities exchange completes, not at the next status transition.
93
- updateStatus(status);
94
- },
95
76
  isHost,
96
77
  });
78
+ const scope = createScope();
79
+ scope.add(signal.teardown);
80
+ signal.peer.subscribe((peer) => {
81
+ const role = isHost ? "host" : "client";
82
+ log(`rpKey discovered by ${role}`, peer?.peerKey);
83
+ });
84
+ signal.peerCapabilities.subscribe((received) => {
85
+ log("peer capabilities received", received);
86
+ setPeerInfo(received?.info);
87
+ });
97
88
  let transport;
98
- const createTransport = (layer) => layer.create({
99
- encrypt(message) {
100
- if (!relyingPublicKey) {
101
- throw new Error("Relying party public key not found");
102
- }
103
- return relyingPublicKey?.encrypt(message);
104
- },
105
- decrypt,
89
+ const createTransport = (layer, cipher) => layer.create({
90
+ cipher,
106
91
  isHost,
107
92
  onmessage: async (message) => {
108
93
  log("Session: received message from transport", message);
94
+ if (!isSessionMessage(message)) {
95
+ log("dropping invalid session message", message);
96
+ return;
97
+ }
98
+ if (message["type"] === "close") {
99
+ await teardown();
100
+ return;
101
+ }
109
102
  if (message["type"] === "request") {
110
103
  const messageId = message["messageId"];
111
104
  try {
112
105
  // Immediately acknowledge receipt so the sender's ack-timeout does
113
106
  // not fire while the handler (which may await user interaction) runs.
114
107
  await transport?.send({ type: "ack", messageId });
115
- // Notify observers before processing (e.g. wallet UI can show a
116
- // pending indicator before the handler resolves).
117
- emitter.emit("request", message["payload"]);
118
- const data = await onMessage(message["payload"])
119
- // A throwing handler must still answer, otherwise the peer waits
120
- // out its full response timeout.
121
- .catch(() => ({ error: { code: -32603, message: "Internal error" } }));
122
- await transport?.send({
123
- type: "response",
124
- messageId,
125
- payload: data,
126
- });
108
+ let answer;
109
+ try {
110
+ // Notify observers before processing (e.g. wallet UI can show a
111
+ // pending indicator before the handler resolves).
112
+ emitter.emit("request", message["payload"]);
113
+ answer = {
114
+ type: "response",
115
+ messageId,
116
+ payload: await onMessage(message["payload"]),
117
+ };
118
+ }
119
+ catch (error) {
120
+ // Only the fact of failure goes to the peer; the thrown value
121
+ // may carry wallet-private details and stays in the local log.
122
+ log("request handler threw", error);
123
+ answer = { type: "failed", messageId };
124
+ }
125
+ await transport?.send(answer);
127
126
  }
128
127
  catch (error) {
129
128
  log("failed to respond to peer request", error);
130
129
  }
131
130
  }
132
- if (message["type"] === "response" || message["type"] === "ack") {
133
- // Both acks and responses are forwarded to the send() correlator.
131
+ else {
132
+ // What is left is an ack, a response or a failure: the send() correlator takes all three.
134
133
  messages.emit("message", message);
135
134
  }
136
135
  },
@@ -149,63 +148,66 @@ export const createSession = async (initParameters, signalLayer, transportLayers
149
148
  * keep the first configured transport.
150
149
  */
151
150
  const selectTransportLayer = () => {
151
+ const peerCaps = signal.peerCapabilities.get();
152
152
  if (!peerCaps)
153
153
  return transportLayers[0];
154
- const selected = selectTransportId(isHost, capabilities.transports, peerCaps.transports);
154
+ // The host's preferred transport that the client also supports; both
155
+ // peers compute the same result independently -- no confirmation round trip.
156
+ const hostPreference = isHost ? capabilities.transports : peerCaps.transports;
157
+ const clientSupported = new Set(isHost ? peerCaps.transports : capabilities.transports);
158
+ const selected = hostPreference.find(transportId => clientSupported.has(transportId));
155
159
  return transportLayers.find(layer => layer.transportId === selected);
156
160
  };
157
161
  // Once both peers are present (signaling encrypted) the transport should
158
162
  // connect promptly; if it cannot (e.g. no ICE candidates on a restricted
159
163
  // network) fail loudly instead of sitting in "linking" forever.
160
164
  const TRANSPORT_LINK_TIMEOUT_MS = 45000;
161
- let linkDeadline;
162
- const clearLinkDeadline = () => {
163
- clearTimeout(linkDeadline);
164
- linkDeadline = undefined;
165
- };
166
- const onTransportStateChange = (state) => {
167
- log("transport state change", state);
168
- if (state === TRANSPORT_STATE.CONNECTED) {
169
- clearLinkDeadline();
170
- lastError = undefined;
171
- updateStatus(SESSION_STATE.CONNECTED);
165
+ // From transport start until the session connects or ends
166
+ const linking = scope.child();
167
+ linking.closeWhen(status, value => value === SessionStatus.CONNECTED || value === SessionStatus.DISCONNECTED);
168
+ const onTransportStateChange = (transportStatus) => {
169
+ log("transport state change", transportStatus);
170
+ if (transportStatus === TransportStatus.CONNECTED) {
171
+ setLastError(undefined);
172
+ updateStatus(SessionStatus.CONNECTED);
172
173
  }
173
- if (state === TRANSPORT_STATE.ERROR) {
174
- clearLinkDeadline();
175
- lastError ?? (lastError = "Peer-to-peer transport failed");
176
- updateStatus(SESSION_STATE.DISCONNECTED);
174
+ if (transportStatus === TransportStatus.ERROR) {
175
+ setLastErrorIfUnset("Peer-to-peer transport failed");
176
+ updateStatus(SessionStatus.DISCONNECTED);
177
177
  }
178
178
  };
179
179
  const onTransportError = (reason) => {
180
180
  if (reason)
181
- lastError = reason;
181
+ setLastError(reason);
182
182
  };
183
183
  const startTransport = () => {
184
184
  if (!transport) {
185
185
  const layer = selectTransportLayer();
186
186
  if (!layer) {
187
- log("no common transport with peer", capabilities.transports, peerCaps?.transports);
188
- lastError ?? (lastError = "No common transport with peer");
189
- updateStatus(SESSION_STATE.DISCONNECTED);
187
+ log("no common transport with peer", capabilities.transports, signal.peerCapabilities.get()?.transports);
188
+ setLastErrorIfUnset("No common transport with peer");
189
+ updateStatus(SessionStatus.DISCONNECTED);
190
190
  return;
191
191
  }
192
192
  log("selected transport", layer.transportId);
193
- transport = createTransport(layer);
194
- transport.emitter.on("state_change", onTransportStateChange);
195
- transport.emitter.on("error", onTransportError);
193
+ const peer = signal.peer.get();
194
+ if (!peer) {
195
+ throw new Error("Relying party public key not found");
196
+ }
197
+ transport = createTransport(layer, peer.cipher);
198
+ scope.add(transport.teardown);
199
+ scope.add(transport.status.subscribe(onTransportStateChange));
200
+ scope.listen(transport.emitter, "error", onTransportError);
201
+ linking.timeout(TRANSPORT_LINK_TIMEOUT_MS, () => {
202
+ log("transport failed to connect in time");
203
+ setLastErrorIfUnset("Timed out establishing the peer-to-peer connection");
204
+ updateStatus(SessionStatus.DISCONNECTED);
205
+ });
196
206
  }
197
- linkDeadline ?? (linkDeadline = setTimeout(() => {
198
- if (status === SESSION_STATE.CONNECTED)
199
- return;
200
- log("transport failed to connect in time");
201
- lastError ?? (lastError = "Timed out establishing the peer-to-peer connection");
202
- updateStatus(SESSION_STATE.DISCONNECTED);
203
- }, TRANSPORT_LINK_TIMEOUT_MS));
204
207
  Promise.resolve(transport.setup()).catch((error) => {
205
208
  log("transport setup failed", error);
206
- clearLinkDeadline();
207
- lastError ?? (lastError = error instanceof Error ? error.message : "Transport setup failed");
208
- updateStatus(SESSION_STATE.DISCONNECTED);
209
+ setLastErrorIfUnset(error instanceof Error ? error.message : "Transport setup failed");
210
+ updateStatus(SessionStatus.DISCONNECTED);
209
211
  });
210
212
  };
211
213
  const onSignalMessage = async (message) => {
@@ -214,7 +216,7 @@ export const createSession = async (initParameters, signalLayer, transportLayers
214
216
  if (sessionMessage.type === "response") {
215
217
  messages.emit("message", sessionMessage);
216
218
  }
217
- if (sessionMessage.type === "request") {
219
+ else if (sessionMessage.type === "request") {
218
220
  if (!transport) {
219
221
  log("dropping negotiation message: transport not selected yet");
220
222
  return;
@@ -229,83 +231,60 @@ export const createSession = async (initParameters, signalLayer, transportLayers
229
231
  }
230
232
  }
231
233
  };
232
- const onSignalStateChange = (state) => {
233
- log("signal state change", state);
234
- if (state === SIGNAL_STATE.READY) {
235
- updateStatus(SESSION_STATE.READY);
234
+ const onSignalStateChange = (signalStatus) => {
235
+ log("signal state change", signalStatus);
236
+ if (signalStatus === SignalStatus.READY) {
237
+ updateStatus(SessionStatus.READY);
236
238
  }
237
239
  if ([
238
- SIGNAL_STATE.HANDSHAKE,
239
- SIGNAL_STATE.HANDSHAKE_PARTIAL,
240
- ].includes(state)) {
241
- updateStatus(SESSION_STATE.LINKING);
240
+ SignalStatus.HANDSHAKE,
241
+ SignalStatus.HANDSHAKE_PARTIAL,
242
+ ].includes(signalStatus)) {
243
+ updateStatus(SessionStatus.LINKING);
242
244
  }
243
- if (state === SIGNAL_STATE.ENCRYPTED) {
245
+ if (signalStatus === SignalStatus.ENCRYPTED) {
244
246
  startTransport();
245
247
  }
246
- if (state === SIGNAL_STATE.ERROR) {
247
- log("signaling error — marking session disconnected");
248
- lastError ?? (lastError = "Signaling failed or timed out");
249
- updateStatus(SESSION_STATE.DISCONNECTED);
248
+ if (signalStatus === SignalStatus.ERROR) {
249
+ log("signaling error -- marking session disconnected");
250
+ setLastErrorIfUnset("Signaling failed or timed out");
251
+ updateStatus(SessionStatus.DISCONNECTED);
250
252
  }
251
253
  };
254
+ const teardown = async () => {
255
+ await scope.close();
256
+ updateStatus(SessionStatus.DISCONNECTED);
257
+ };
252
258
  return {
253
259
  connect: async () => {
254
- updateStatus(SESSION_STATE.SIGNALING);
260
+ updateStatus(SessionStatus.SIGNALING);
255
261
  log("connecting to session, isHost:", isHost);
256
- signal.on("message", onSignalMessage);
257
- signal.on("state_change", onSignalStateChange);
262
+ scope.listen(signal, "message", onSignalMessage);
263
+ scope.add(signal.status.subscribe(onSignalStateChange));
258
264
  await signal.setup();
259
265
  },
260
266
  async close() {
261
267
  log("session teardown");
262
- clearLinkDeadline();
263
- signal.off("message", onSignalMessage);
264
- signal.off("state_change", onSignalStateChange);
265
- transport?.emitter.off("state_change", onTransportStateChange);
266
- transport?.emitter.off("error", onTransportError);
267
- await Promise.all([
268
- transport?.teardown(),
269
- signal.teardown(),
270
- ]);
271
- updateStatus(SESSION_STATE.DISCONNECTED);
272
- },
273
- getState() {
274
- return {
275
- status,
276
- signaling: signal.getState(),
277
- peerInfo: peerCaps?.info,
278
- error: lastError,
279
- };
268
+ await transport?.send({ type: "close" })
269
+ .catch(error => log("failed to send close to peer", error));
270
+ await teardown();
280
271
  },
272
+ status,
273
+ signalStatus: signal.status,
274
+ error: lastError,
275
+ peerInfo,
281
276
  getHandshakeParameters() {
282
277
  return {
283
278
  version: OPENLV_PROTOCOL_VERSION,
284
279
  sessionId,
285
280
  h: hash,
286
- k: handshakeKey.toString(),
281
+ k,
287
282
  p: protocol,
288
283
  s: server,
289
284
  };
290
285
  },
291
- waitForLink: async () => new Promise((resolve, reject) => {
292
- const handler = (state) => {
293
- if (state?.status === SESSION_STATE.CONNECTED) {
294
- emitter.off("state_change", handler);
295
- resolve();
296
- }
297
- else if (state?.status === SESSION_STATE.DISCONNECTED) {
298
- emitter.off("state_change", handler);
299
- reject(new Error(lastError ?? "Session failed to connect"));
300
- }
301
- };
302
- // Subscribe before inspecting the current status so a transition
303
- // between the check and the subscription cannot be missed.
304
- emitter.on("state_change", handler);
305
- handler({ status });
306
- }),
307
286
  async send(message, ackTimeout = 10000, responseTimeout = 60 * 60000) {
308
- if (!transport || signal.getState().state !== SIGNAL_STATE.ENCRYPTED) {
287
+ if (!transport || signal.status.get() !== SignalStatus.ENCRYPTED) {
309
288
  throw new Error("Session not ready");
310
289
  }
311
290
  const messageId = crypto.randomUUID();
@@ -331,10 +310,6 @@ export const createSession = async (initParameters, signalLayer, transportLayers
331
310
  */
332
311
  export const connectSession = async (connectionUrl, onMessage, transports, options) => {
333
312
  const initParameters = decodeConnectionURL(connectionUrl);
334
- const signaling = await dynamicSignalingLayer(initParameters.p);
335
- if (!signaling) {
336
- throw new Error(`Invalid signaling protocol: ${initParameters.p}`);
337
- }
338
- return createSession(initParameters, signaling, transports, onMessage, options);
313
+ return createSession(initParameters, transports, onMessage, options);
339
314
  };
340
315
  //# sourceMappingURL=index.js.map
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":["import {\n decodeConnectionURL,\n OPENLV_PROTOCOL_VERSION,\n type SessionHandshakeParameters,\n type SessionLinkParameters,\n} from \"@openlv/core\";\nimport {\n deriveSymmetricKey,\n type EncryptionKey,\n generateHandshakeKey,\n generateSessionId,\n initEncryptionKeys,\n initHash,\n parseEncryptionKey,\n} from \"@openlv/core/encryption\";\nimport {\n type PeerCapabilities,\n type PeerInfo,\n SIGNAL_STATE,\n type SignalingLayer,\n type SignalingProtocol,\n type SignalState,\n validatePeerInfo,\n} from \"@openlv/signaling\";\nimport { dynamicSignalingLayer } from \"@openlv/signaling/dynamic\";\nimport {\n TRANSPORT_STATE,\n type TransportLayer,\n type TransportLayerFn,\n type TransportMessage,\n} from \"@openlv/transport\";\nimport { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionEvents } from \"./events.js\";\nimport { awaitCorrelatedResponse } from \"./messages/correlate.js\";\nimport type { SessionMessage } from \"./messages/index.js\";\nimport { selectTransportId } from \"./transports.js\";\nimport { log } from \"./utils/log.js\";\n\nexport type { PeerCapabilities, PeerInfo } from \"@openlv/signaling\";\n\nexport const SESSION_STATE = {\n CREATED: \"created\",\n SIGNALING: \"signaling\",\n READY: \"ready\",\n LINKING: \"linking\",\n CONNECTED: \"connected\",\n DISCONNECTED: \"disconnected\",\n} as const;\nexport type SessionState = (typeof SESSION_STATE)[keyof typeof SESSION_STATE];\n\nexport type SessionStateObject = {\n status: SessionState;\n signaling?: {\n state: SignalState;\n };\n /** The peer's self-description, set once capabilities are exchanged. */\n peerInfo?: PeerInfo;\n /** Human-readable reason for the last disconnect/failure, if any. */\n error?: string;\n};\n\nexport type SessionOptions = {\n /** Shared with the peer during the handshake and shown in its UI. */\n info?: PeerInfo;\n};\n\n/**\n * an OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport type Session = {\n getState(): SessionStateObject;\n getHandshakeParameters(): SessionHandshakeParameters;\n connect(): Promise<void>;\n waitForLink(): Promise<void>;\n close(): Promise<void>;\n /**\n * Send a request to the remote peer and await a correlated response.\n *\n * Two-phase timeout:\n * - `ackTimeout` (default 10 s): the remote peer must send back an ack\n * within this window, confirming it received the message.\n * - After an ack arrives the wait is extended to `responseTimeout`\n * (default 1 hour) — enough for user-interactive flows such as\n * `eth_sendTransaction` or `personal_sign`.\n */\n send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;\n emitter: EventEmitter<SessionEvents>;\n _internal: {\n signal: SignalingLayer;\n /** Instantiated when signaling reaches ENCRYPTED and a transport is selected. */\n transport: TransportLayer | undefined;\n };\n};\n\n/**\n * OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport const createSession = async (\n initParameters: SessionLinkParameters,\n signalLayer: SignalingProtocol,\n transportLayers: TransportLayerFn[],\n onMessage: (message: object) => Promise<object | string>,\n options?: SessionOptions,\n): Promise<Session> => {\n if (transportLayers.length === 0) {\n throw new Error(\"At least one transport is required\");\n }\n\n if (options?.info) {\n // A receiver silently drops out-of-bounds info with the whole packet,\n // which would surface only as a handshake timeout — fail loudly here.\n const problem = validatePeerInfo(options.info);\n\n if (problem) throw new Error(`Invalid session info: ${problem}`);\n }\n\n const emitter = new EventEmitter<SessionEvents>();\n const messages = new EventEmitter<{ message: SessionMessage; }>();\n const sessionId\n = \"sessionId\" in initParameters\n ? initParameters.sessionId\n : generateSessionId();\n const {\n encryptionKey,\n decryptionKey: { decrypt },\n } = await initEncryptionKeys(initParameters);\n let relyingPublicKey: EncryptionKey | undefined;\n const handshakeKey\n = \"k\" in initParameters\n ? await deriveSymmetricKey(initParameters.k)\n : await generateHandshakeKey();\n const { hash, isHost } = await initHash(\n \"h\" in initParameters ? initParameters.h : undefined,\n encryptionKey,\n );\n let status: SessionState = SESSION_STATE.CREATED;\n let lastError: string | undefined;\n const protocol = initParameters.p;\n const server = initParameters.s;\n const capabilities: PeerCapabilities = {\n transports: transportLayers.map(layer => layer.transportId),\n ...(options?.info && { info: options.info }),\n };\n let peerCaps: PeerCapabilities | undefined;\n\n const updateStatus = (newStatus: SessionState) => {\n log(\"updateStatus\", newStatus);\n status = newStatus;\n emitter.emit(\"state_change\", {\n status,\n signaling: signal.getState(),\n peerInfo: peerCaps?.info,\n error: lastError,\n });\n };\n\n const signaling = await signalLayer({\n topic: sessionId,\n url: server,\n });\n const signal = await signaling({\n h: hash,\n canEncrypt() {\n return relyingPublicKey !== undefined;\n },\n async encrypt(message) {\n if (!relyingPublicKey) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return await relyingPublicKey.encrypt(message);\n },\n decrypt,\n publicKey: encryptionKey,\n k: handshakeKey,\n async rpDiscovered(rpKey) {\n const role = isHost ? \"host\" : \"client\";\n\n log(`rpKey discovered by ${role}`, rpKey);\n\n relyingPublicKey = await parseEncryptionKey(rpKey);\n },\n capabilities,\n peerCapabilities(received) {\n log(\"peer capabilities received\", received);\n peerCaps = received;\n // Re-emit the current status so observers see peerInfo the moment the\n // capabilities exchange completes, not at the next status transition.\n updateStatus(status);\n },\n isHost,\n });\n\n let transport: TransportLayer | undefined;\n\n const createTransport = (layer: TransportLayerFn): TransportLayer => layer.create({\n encrypt(message) {\n if (!relyingPublicKey) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return relyingPublicKey?.encrypt(message);\n },\n decrypt,\n isHost,\n onmessage: async (message: { type: string; payload: object; messageId: string; }) => {\n log(\"Session: received message from transport\", message);\n\n if (message[\"type\"] === \"request\") {\n const messageId = message[\"messageId\"] as string;\n\n try {\n // Immediately acknowledge receipt so the sender's ack-timeout does\n // not fire while the handler (which may await user interaction) runs.\n await transport?.send({ type: \"ack\", messageId } satisfies SessionMessage);\n\n // Notify observers before processing (e.g. wallet UI can show a\n // pending indicator before the handler resolves).\n emitter.emit(\"request\", message[\"payload\"] as object);\n\n const data = await onMessage(message[\"payload\"] as object)\n // A throwing handler must still answer, otherwise the peer waits\n // out its full response timeout.\n .catch(() => ({ error: { code: -32_603, message: \"Internal error\" } }));\n\n await transport?.send({\n type: \"response\",\n messageId,\n payload: data,\n } satisfies SessionMessage);\n }\n catch (error) {\n log(\"failed to respond to peer request\", error);\n }\n }\n\n if (message[\"type\"] === \"response\" || message[\"type\"] === \"ack\") {\n // Both acks and responses are forwarded to the send() correlator.\n messages.emit(\"message\", message as SessionMessage);\n }\n },\n async subsend(message) {\n log(\"Session: sending trans msg to signal\", message);\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId: crypto.randomUUID(),\n payload: message,\n };\n\n await signal.send(sessionMessage);\n },\n });\n\n /**\n * Sessions resumed with a known peer key never exchange capabilities and\n * keep the first configured transport.\n */\n const selectTransportLayer = (): TransportLayerFn | undefined => {\n if (!peerCaps) return transportLayers[0];\n\n const selected = selectTransportId(isHost, capabilities.transports, peerCaps.transports);\n\n return transportLayers.find(layer => layer.transportId === selected);\n };\n\n // Once both peers are present (signaling encrypted) the transport should\n // connect promptly; if it cannot (e.g. no ICE candidates on a restricted\n // network) fail loudly instead of sitting in \"linking\" forever.\n const TRANSPORT_LINK_TIMEOUT_MS = 45_000;\n let linkDeadline: ReturnType<typeof setTimeout> | undefined;\n\n const clearLinkDeadline = () => {\n clearTimeout(linkDeadline);\n linkDeadline = undefined;\n };\n\n const onTransportStateChange = (state: string) => {\n log(\"transport state change\", state);\n\n if (state === TRANSPORT_STATE.CONNECTED) {\n clearLinkDeadline();\n lastError = undefined;\n updateStatus(SESSION_STATE.CONNECTED);\n }\n\n if (state === TRANSPORT_STATE.ERROR) {\n clearLinkDeadline();\n lastError ??= \"Peer-to-peer transport failed\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n }\n };\n\n const onTransportError = (reason?: string) => {\n if (reason) lastError = reason;\n };\n\n const startTransport = () => {\n if (!transport) {\n const layer = selectTransportLayer();\n\n if (!layer) {\n log(\"no common transport with peer\", capabilities.transports, peerCaps?.transports);\n lastError ??= \"No common transport with peer\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n\n return;\n }\n\n log(\"selected transport\", layer.transportId);\n transport = createTransport(layer);\n transport.emitter.on(\"state_change\", onTransportStateChange);\n transport.emitter.on(\"error\", onTransportError);\n }\n\n linkDeadline ??= setTimeout(() => {\n if (status === SESSION_STATE.CONNECTED) return;\n\n log(\"transport failed to connect in time\");\n lastError ??= \"Timed out establishing the peer-to-peer connection\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n }, TRANSPORT_LINK_TIMEOUT_MS);\n\n Promise.resolve(transport.setup()).catch((error) => {\n log(\"transport setup failed\", error);\n clearLinkDeadline();\n lastError ??= error instanceof Error ? error.message : \"Transport setup failed\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n });\n };\n\n const onSignalMessage = async (message: object) => {\n log(\"Session: received message from signaling\", message);\n\n const sessionMessage = message as SessionMessage;\n\n if (sessionMessage.type === \"response\") {\n messages.emit(\"message\", sessionMessage);\n }\n\n if (sessionMessage.type === \"request\") {\n if (!transport) {\n log(\"dropping negotiation message: transport not selected yet\");\n\n return;\n }\n\n try {\n await transport.handle(sessionMessage.payload as TransportMessage);\n }\n catch (error) {\n // Negotiation payloads come from the (untrusted) relay; a malformed\n // one must not become an unhandled rejection.\n log(\"failed to handle negotiation message\", error);\n }\n }\n };\n\n const onSignalStateChange = (state: SignalState) => {\n log(\"signal state change\", state);\n\n if (state === SIGNAL_STATE.READY) {\n updateStatus(SESSION_STATE.READY);\n }\n\n if (\n (\n [\n SIGNAL_STATE.HANDSHAKE,\n SIGNAL_STATE.HANDSHAKE_PARTIAL,\n ] as SignalState[]\n ).includes(state)\n ) {\n updateStatus(SESSION_STATE.LINKING);\n }\n\n if (state === SIGNAL_STATE.ENCRYPTED) {\n startTransport();\n }\n\n if (state === SIGNAL_STATE.ERROR) {\n log(\"signaling error — marking session disconnected\");\n lastError ??= \"Signaling failed or timed out\";\n updateStatus(SESSION_STATE.DISCONNECTED);\n }\n };\n\n return {\n connect: async () => {\n updateStatus(SESSION_STATE.SIGNALING);\n log(\"connecting to session, isHost:\", isHost);\n\n signal.on(\"message\", onSignalMessage);\n signal.on(\"state_change\", onSignalStateChange);\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n clearLinkDeadline();\n signal.off(\"message\", onSignalMessage);\n signal.off(\"state_change\", onSignalStateChange);\n transport?.emitter.off(\"state_change\", onTransportStateChange);\n transport?.emitter.off(\"error\", onTransportError);\n await Promise.all([\n transport?.teardown(),\n signal.teardown(),\n ]);\n updateStatus(SESSION_STATE.DISCONNECTED);\n },\n getState() {\n return {\n status,\n signaling: signal.getState(),\n peerInfo: peerCaps?.info,\n error: lastError,\n };\n },\n getHandshakeParameters() {\n return {\n version: OPENLV_PROTOCOL_VERSION,\n sessionId,\n h: hash,\n k: handshakeKey.toString(),\n p: protocol,\n s: server,\n };\n },\n waitForLink: async () => new Promise<void>((resolve, reject) => {\n const handler = (state?: SessionStateObject) => {\n if (state?.status === SESSION_STATE.CONNECTED) {\n emitter.off(\"state_change\", handler);\n resolve();\n }\n else if (state?.status === SESSION_STATE.DISCONNECTED) {\n emitter.off(\"state_change\", handler);\n reject(new Error(lastError ?? \"Session failed to connect\"));\n }\n };\n\n // Subscribe before inspecting the current status so a transition\n // between the check and the subscription cannot be missed.\n emitter.on(\"state_change\", handler);\n handler({ status });\n }),\n async send(\n message: object,\n ackTimeout: number = 10_000,\n responseTimeout: number = 60 * 60_000,\n ) {\n if (!transport || signal.getState().state !== SIGNAL_STATE.ENCRYPTED) {\n throw new Error(\"Session not ready\");\n }\n\n const messageId = crypto.randomUUID();\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId,\n payload: message,\n };\n\n await transport.send(sessionMessage);\n\n return awaitCorrelatedResponse(messages, messageId, ackTimeout, responseTimeout);\n },\n _internal: {\n signal,\n get transport() {\n return transport;\n },\n },\n emitter,\n };\n};\n\n/**\n * Connect to a session from its openlv:// URL\n */\nexport const connectSession = async (\n connectionUrl: string,\n onMessage: (message: object) => Promise<object | string>,\n transports: TransportLayerFn[],\n options?: SessionOptions,\n): Promise<Session> => {\n const initParameters = decodeConnectionURL(connectionUrl);\n\n const signaling = await dynamicSignalingLayer(initParameters.p);\n\n if (!signaling) {\n throw new Error(`Invalid signaling protocol: ${initParameters.p}`);\n }\n\n return createSession(initParameters, signaling, transports, onMessage, options);\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createScope,\n decodeConnectionURL,\n type Observable,\n observable,\n OPENLV_PROTOCOL_VERSION,\n type SessionHandshakeParameters,\n type SessionLinkParameters,\n} from \"@openlv/core\";\nimport {\n type Cipher,\n createHandshakeCipher,\n generateHandshakeKey,\n generateKeyPair,\n generateSessionId,\n hashPublicKey,\n} from \"@openlv/core/encryption\";\nimport {\n type PeerCapabilities,\n type PeerInfo,\n type SignalingLayer,\n Status as SignalStatus,\n validatePeerInfo,\n} from \"@openlv/signaling\";\nimport {\n Status as TransportStatus,\n type TransportLayer,\n type TransportLayerFunction,\n type TransportMessage,\n} from \"@openlv/transport\";\nimport { EventEmitter } from \"eventemitter3\";\n\nimport { loadSignaling } from \"./dynamic.js\";\nimport type { SessionEvents } from \"./events.js\";\nimport { awaitCorrelatedResponse, type CorrelationEvents } from \"./messages/correlate.js\";\nimport { isSessionMessage, type SessionMessage } from \"./messages/index.js\";\nimport { log } from \"./utils/log.js\";\n\nexport { loadSignaling, loadTransport } from \"./dynamic.js\";\nexport { RequestFailedError } from \"./messages/correlate.js\";\nexport type { SessionMessage } from \"./messages/index.js\";\n\n// `peerInfo` is part of the session's surface, so consumers must be able to\n// name its type without reaching into @openlv/signaling.\nexport type { PeerInfo } from \"@openlv/signaling\";\n\nexport const SessionStatus = {\n CREATED: \"created\",\n SIGNALING: \"signaling\",\n READY: \"ready\",\n LINKING: \"linking\",\n CONNECTED: \"connected\",\n DISCONNECTED: \"disconnected\",\n} as const;\nexport type SessionStatus = (typeof SessionStatus)[keyof typeof SessionStatus];\n\nexport type SessionOptions = {\n info?: PeerInfo;\n};\n\n/**\n * an OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport type Session = {\n status: Observable<SessionStatus>;\n signalStatus: Observable<SignalStatus>;\n error: Observable<string | undefined>;\n peerInfo: Observable<PeerInfo | undefined>;\n getHandshakeParameters(): SessionHandshakeParameters;\n connect(): Promise<void>;\n close(): Promise<void>;\n send(\n message: unknown,\n ackTimeout?: number,\n responseTimeout?: number,\n ): Promise<unknown>;\n emitter: EventEmitter<SessionEvents>;\n _internal: {\n signal: SignalingLayer;\n /** Instantiated when signaling reaches ENCRYPTED and a transport is selected. */\n transport: TransportLayer | undefined;\n };\n};\n\n/**\n * OpenLV Session\n *\n * https://openlv.sh/api/session\n */\nexport const createSession = async (\n initParameters: SessionLinkParameters,\n transportLayers: TransportLayerFunction[],\n onMessage: (message: unknown) => Promise<unknown>,\n options?: SessionOptions,\n): Promise<Session> => {\n if (transportLayers.length === 0) {\n throw new Error(\"At least one transport is required\");\n }\n\n if (options?.info) {\n const problem = validatePeerInfo(options.info);\n\n if (problem) throw new Error(`Invalid session info: ${problem}`);\n }\n\n const emitter = new EventEmitter<SessionEvents>();\n const messages = new EventEmitter<CorrelationEvents>();\n const sessionId\n = \"sessionId\" in initParameters\n ? initParameters.sessionId\n : generateSessionId();\n const keyPair = generateKeyPair();\n const k = \"k\" in initParameters ? initParameters.k : generateHandshakeKey();\n const ownHash = await hashPublicKey(keyPair.publicKey);\n const hash = (\"h\" in initParameters && initParameters.h) || ownHash;\n const isHost = hash === ownHash;\n const protocol = initParameters.p;\n const server = initParameters.s;\n const capabilities: PeerCapabilities = {\n transports: transportLayers.map(layer => layer.transportId),\n ...(options?.info && { info: options.info }),\n };\n\n const [status, setStatus] = observable<SessionStatus>(SessionStatus.CREATED);\n const [lastError, setLastError] = observable<string | undefined>(undefined);\n const [peerInfo, setPeerInfo] = observable<PeerInfo | undefined>(undefined);\n\n const setLastErrorIfUnset = (reason: string) => {\n if (lastError.get() === undefined) setLastError(reason);\n };\n\n const updateStatus = (newStatus: SessionStatus) => {\n log(\"updateStatus\", newStatus);\n setStatus(newStatus);\n };\n\n status.subscribe((current) => {\n if (current === SessionStatus.DISCONNECTED) {\n messages.emit(\"ended\", lastError.get() ?? \"Session closed\");\n }\n });\n\n const signalLayer = await loadSignaling(protocol);\n const signaling = await signalLayer({\n topic: sessionId,\n url: server,\n });\n const signal = await signaling({\n h: hash,\n handshakeCipher: await createHandshakeCipher(k),\n keyPair,\n capabilities,\n isHost,\n });\n\n const scope = createScope();\n\n scope.add(signal.teardown);\n\n signal.peer.subscribe((peer) => {\n const role = isHost ? \"host\" : \"client\";\n\n log(`rpKey discovered by ${role}`, peer?.peerKey);\n });\n\n signal.peerCapabilities.subscribe((received) => {\n log(\"peer capabilities received\", received);\n setPeerInfo(received?.info);\n });\n\n let transport: TransportLayer | undefined;\n\n const createTransport = (layer: TransportLayerFunction, cipher: Cipher): TransportLayer => layer.create({\n cipher,\n isHost,\n onmessage: async (message) => {\n log(\"Session: received message from transport\", message);\n\n if (!isSessionMessage(message)) {\n log(\"dropping invalid session message\", message);\n\n return;\n }\n\n if (message[\"type\"] === \"close\") {\n await teardown();\n\n return;\n }\n\n if (message[\"type\"] === \"request\") {\n const messageId = message[\"messageId\"];\n\n try {\n // Immediately acknowledge receipt so the sender's ack-timeout does\n // not fire while the handler (which may await user interaction) runs.\n await transport?.send({ type: \"ack\", messageId } satisfies SessionMessage);\n\n let answer: SessionMessage;\n\n try {\n // Notify observers before processing (e.g. wallet UI can show a\n // pending indicator before the handler resolves).\n emitter.emit(\"request\", message[\"payload\"]);\n\n answer = {\n type: \"response\",\n messageId,\n payload: await onMessage(message[\"payload\"]),\n };\n }\n catch (error) {\n // Only the fact of failure goes to the peer; the thrown value\n // may carry wallet-private details and stays in the local log.\n log(\"request handler threw\", error);\n answer = { type: \"failed\", messageId };\n }\n\n await transport?.send(answer);\n }\n catch (error) {\n log(\"failed to respond to peer request\", error);\n }\n }\n\n else {\n // What is left is an ack, a response or a failure: the send() correlator takes all three.\n messages.emit(\"message\", message);\n }\n },\n async subsend(message) {\n log(\"Session: sending trans msg to signal\", message);\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId: crypto.randomUUID(),\n payload: message,\n };\n\n await signal.send(sessionMessage);\n },\n });\n\n /**\n * Sessions resumed with a known peer key never exchange capabilities and\n * keep the first configured transport.\n */\n const selectTransportLayer = (): TransportLayerFunction | undefined => {\n const peerCaps = signal.peerCapabilities.get();\n\n if (!peerCaps) return transportLayers[0];\n\n // The host's preferred transport that the client also supports; both\n // peers compute the same result independently -- no confirmation round trip.\n const hostPreference = isHost ? capabilities.transports : peerCaps.transports;\n const clientSupported = new Set(isHost ? peerCaps.transports : capabilities.transports);\n const selected = hostPreference.find(transportId => clientSupported.has(transportId));\n\n return transportLayers.find(layer => layer.transportId === selected);\n };\n\n // Once both peers are present (signaling encrypted) the transport should\n // connect promptly; if it cannot (e.g. no ICE candidates on a restricted\n // network) fail loudly instead of sitting in \"linking\" forever.\n const TRANSPORT_LINK_TIMEOUT_MS = 45_000;\n // From transport start until the session connects or ends\n const linking = scope.child();\n\n linking.closeWhen(status, value => value === SessionStatus.CONNECTED || value === SessionStatus.DISCONNECTED);\n\n const onTransportStateChange = (transportStatus: TransportStatus) => {\n log(\"transport state change\", transportStatus);\n\n if (transportStatus === TransportStatus.CONNECTED) {\n setLastError(undefined);\n updateStatus(SessionStatus.CONNECTED);\n }\n\n if (transportStatus === TransportStatus.ERROR) {\n setLastErrorIfUnset(\"Peer-to-peer transport failed\");\n updateStatus(SessionStatus.DISCONNECTED);\n }\n };\n\n const onTransportError = (reason?: string) => {\n if (reason) setLastError(reason);\n };\n\n const startTransport = () => {\n if (!transport) {\n const layer = selectTransportLayer();\n\n if (!layer) {\n log(\"no common transport with peer\", capabilities.transports, signal.peerCapabilities.get()?.transports);\n setLastErrorIfUnset(\"No common transport with peer\");\n updateStatus(SessionStatus.DISCONNECTED);\n\n return;\n }\n\n log(\"selected transport\", layer.transportId);\n const peer = signal.peer.get();\n\n if (!peer) {\n throw new Error(\"Relying party public key not found\");\n }\n\n transport = createTransport(layer, peer.cipher);\n scope.add(transport.teardown);\n scope.add(transport.status.subscribe(onTransportStateChange));\n scope.listen(transport.emitter, \"error\", onTransportError);\n linking.timeout(TRANSPORT_LINK_TIMEOUT_MS, () => {\n log(\"transport failed to connect in time\");\n setLastErrorIfUnset(\"Timed out establishing the peer-to-peer connection\");\n updateStatus(SessionStatus.DISCONNECTED);\n });\n }\n\n Promise.resolve(transport.setup()).catch((error) => {\n log(\"transport setup failed\", error);\n setLastErrorIfUnset(error instanceof Error ? error.message : \"Transport setup failed\");\n updateStatus(SessionStatus.DISCONNECTED);\n });\n };\n\n const onSignalMessage = async (message: object) => {\n log(\"Session: received message from signaling\", message);\n\n const sessionMessage = message as SessionMessage;\n\n if (sessionMessage.type === \"response\") {\n messages.emit(\"message\", sessionMessage);\n }\n else if (sessionMessage.type === \"request\") {\n if (!transport) {\n log(\"dropping negotiation message: transport not selected yet\");\n\n return;\n }\n\n try {\n await transport.handle(sessionMessage.payload as TransportMessage);\n }\n catch (error) {\n // Negotiation payloads come from the (untrusted) relay; a malformed\n // one must not become an unhandled rejection.\n log(\"failed to handle negotiation message\", error);\n }\n }\n };\n\n const onSignalStateChange = (signalStatus: SignalStatus) => {\n log(\"signal state change\", signalStatus);\n\n if (signalStatus === SignalStatus.READY) {\n updateStatus(SessionStatus.READY);\n }\n\n if (\n (\n [\n SignalStatus.HANDSHAKE,\n SignalStatus.HANDSHAKE_PARTIAL,\n ] as SignalStatus[]\n ).includes(signalStatus)\n ) {\n updateStatus(SessionStatus.LINKING);\n }\n\n if (signalStatus === SignalStatus.ENCRYPTED) {\n startTransport();\n }\n\n if (signalStatus === SignalStatus.ERROR) {\n log(\"signaling error -- marking session disconnected\");\n setLastErrorIfUnset(\"Signaling failed or timed out\");\n updateStatus(SessionStatus.DISCONNECTED);\n }\n };\n\n const teardown = async () => {\n await scope.close();\n updateStatus(SessionStatus.DISCONNECTED);\n };\n\n return {\n connect: async () => {\n updateStatus(SessionStatus.SIGNALING);\n log(\"connecting to session, isHost:\", isHost);\n\n scope.listen(signal, \"message\", onSignalMessage);\n scope.add(signal.status.subscribe(onSignalStateChange));\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n await transport?.send({ type: \"close\" } satisfies SessionMessage)\n .catch(error => log(\"failed to send close to peer\", error));\n await teardown();\n },\n status,\n signalStatus: signal.status,\n error: lastError,\n peerInfo,\n getHandshakeParameters() {\n return {\n version: OPENLV_PROTOCOL_VERSION,\n sessionId,\n h: hash,\n k,\n p: protocol,\n s: server,\n };\n },\n async send(\n message: unknown,\n ackTimeout: number = 10_000,\n responseTimeout: number = 60 * 60_000,\n ) {\n if (!transport || signal.status.get() !== SignalStatus.ENCRYPTED) {\n throw new Error(\"Session not ready\");\n }\n\n const messageId = crypto.randomUUID();\n const sessionMessage: SessionMessage = {\n type: \"request\",\n messageId,\n payload: message,\n };\n\n await transport.send(sessionMessage);\n\n return awaitCorrelatedResponse(messages, messageId, ackTimeout, responseTimeout);\n },\n _internal: {\n signal,\n get transport() {\n return transport;\n },\n },\n emitter,\n };\n};\n\n/**\n * Connect to a session from its openlv:// URL\n */\nexport const connectSession = async (\n connectionUrl: string,\n onMessage: (message: unknown) => Promise<unknown>,\n transports: TransportLayerFunction[],\n options?: SessionOptions,\n): Promise<Session> => {\n const initParameters = decodeConnectionURL(connectionUrl);\n\n return createSession(initParameters, transports, onMessage, options);\n};\n"]}
@@ -1,20 +1,19 @@
1
1
  import { decodeConnectionURL, encodeConnectionURL } from "@openlv/core";
2
- import { ntfy } from "@openlv/signaling/ntfy";
3
2
  import { webrtc } from "@openlv/transport/webrtc";
4
3
  import { describe, expect, test } from "vitest";
5
- import { connectSession, createSession } from "./index.js";
4
+ import { connectSession, createSession, RequestFailedError, SessionStatus } from "./index.js";
6
5
  describe("Session", () => {
7
6
  test("Should be able to create a session", async () => {
8
7
  const sessionA = await createSession({
9
8
  sessionId: "mytestsession111",
10
9
  p: "ntfy",
11
10
  s: "https://ntfy.sh/",
12
- }, ntfy, [webrtc()], async (message) => {
11
+ }, [webrtc()], async (message) => {
13
12
  console.log("sessionA received message", message);
14
13
  return { result: "success" };
15
14
  });
16
15
  expect(sessionA).toBeDefined();
17
- console.log(sessionA.getState());
16
+ console.log(sessionA.status.get());
18
17
  await sessionA.connect();
19
18
  const handshakeParametersA = sessionA.getHandshakeParameters();
20
19
  const encodedUrl = encodeConnectionURL(handshakeParametersA);
@@ -26,17 +25,22 @@ describe("Session", () => {
26
25
  console.log("Connecting to session B");
27
26
  const sessionB = await connectSession(encodedUrl, async (message) => {
28
27
  console.log("sessionB received message", message);
28
+ if (JSON.stringify(message).includes("boom")) {
29
+ throw new Error("failed to sign with privkey 12345");
30
+ }
29
31
  return { result: "success" };
30
32
  }, [webrtc()]);
31
- console.log(sessionB.getState());
33
+ console.log(sessionB.status.get());
32
34
  await sessionB.connect();
33
- await Promise.all([sessionA.waitForLink(), sessionB.waitForLink()]);
35
+ await Promise.all([sessionA, sessionB].map(session => session.status.until(current => current === SessionStatus.CONNECTED)));
34
36
  //
35
- console.log("A", sessionA.getState());
36
- console.log("B", sessionB.getState());
37
+ console.log("A", sessionA.status.get());
38
+ console.log("B", sessionB.status.get());
37
39
  const response = await sessionA.send({ data: "test" });
38
40
  console.log("response", response);
39
41
  expect(response).toEqual({ result: "success" });
42
+ // A throwing handler fails the request at once instead of leaving it to time out.
43
+ await expect(sessionA.send({ data: "boom" })).rejects.toBeInstanceOf(RequestFailedError);
40
44
  });
41
45
  });
42
46
  //# sourceMappingURL=index.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3D,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAI,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC;YACE,SAAS,EAAE,kBAAkB;YAC7B,CAAC,EAAE,MAAM;YACT,CAAC,EAAE,kBAAkB;SACtB,EACD,IAAI,EACJ,CAAC,MAAM,EAAE,CAAC,EACV,KAAK,EAAE,OAAO,EAAE,EAAE;YAChB,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;YAElD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC/B,CAAC,CACF,CAAC;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEjC,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEzB,MAAM,oBAAoB,GAAG,QAAQ,CAAC,sBAAsB,EAAE,CAAC;QAC/D,MAAM,UAAU,GAAG,mBAAmB,CAAC,oBAAoB,CAAC,CAAC;QAE7D,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAExB,MAAM,UAAU,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QAEnD,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAEjD,EAAE;QACF,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAExD,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAEvC,MAAM,QAAQ,GAAG,MAAM,cAAc,CACnC,UAAU,EACV,KAAK,EAAE,OAAO,EAAE,EAAE;YAChB,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;YAElD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC/B,CAAC,EACD,CAAC,MAAM,EAAE,CAAC,CACX,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QACjC,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;QAEzB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QAEpE,EAAE;QACF,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEtC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAEvD,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAElC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { decodeConnectionURL, encodeConnectionURL } from \"@openlv/core\";\nimport { ntfy } from \"@openlv/signaling/ntfy\";\nimport { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { connectSession, createSession } from \"./index.js\";\n\ndescribe(\"Session\", () => {\n test(\"Should be able to create a session\", async () => {\n const sessionA = await createSession(\n {\n sessionId: \"mytestsession111\",\n p: \"ntfy\",\n s: \"https://ntfy.sh/\",\n },\n ntfy,\n [webrtc()],\n async (message) => {\n console.log(\"sessionA received message\", message);\n\n return { result: \"success\" };\n },\n );\n\n expect(sessionA).toBeDefined();\n console.log(sessionA.getState());\n\n await sessionA.connect();\n\n const handshakeParametersA = sessionA.getHandshakeParameters();\n const encodedUrl = encodeConnectionURL(handshakeParametersA);\n\n console.log(encodedUrl);\n\n const decodedUrl = decodeConnectionURL(encodedUrl);\n\n expect(decodedUrl).toEqual(handshakeParametersA);\n\n //\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n console.log(\"Connecting to session B\");\n\n const sessionB = await connectSession(\n encodedUrl,\n async (message) => {\n console.log(\"sessionB received message\", message);\n\n return { result: \"success\" };\n },\n [webrtc()],\n );\n\n console.log(sessionB.getState());\n await sessionB.connect();\n\n await Promise.all([sessionA.waitForLink(), sessionB.waitForLink()]);\n\n //\n console.log(\"A\", sessionA.getState());\n console.log(\"B\", sessionB.getState());\n\n const response = await sessionA.send({ data: \"test\" });\n\n console.log(\"response\", response);\n\n expect(response).toEqual({ result: \"success\" });\n });\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { decodeConnectionURL, encodeConnectionURL } from \"@openlv/core\";\nimport { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { connectSession, createSession, RequestFailedError, SessionStatus } from \"./index.js\";\n\ndescribe(\"Session\", () => {\n test(\"Should be able to create a session\", async () => {\n const sessionA = await createSession(\n {\n sessionId: \"mytestsession111\",\n p: \"ntfy\",\n s: \"https://ntfy.sh/\",\n },\n [webrtc()],\n async (message) => {\n console.log(\"sessionA received message\", message);\n\n return { result: \"success\" };\n },\n );\n\n expect(sessionA).toBeDefined();\n console.log(sessionA.status.get());\n\n await sessionA.connect();\n\n const handshakeParametersA = sessionA.getHandshakeParameters();\n const encodedUrl = encodeConnectionURL(handshakeParametersA);\n\n console.log(encodedUrl);\n\n const decodedUrl = decodeConnectionURL(encodedUrl);\n\n expect(decodedUrl).toEqual(handshakeParametersA);\n\n //\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n console.log(\"Connecting to session B\");\n\n const sessionB = await connectSession(\n encodedUrl,\n async (message) => {\n console.log(\"sessionB received message\", message);\n\n if (JSON.stringify(message).includes(\"boom\")) {\n throw new Error(\"failed to sign with privkey 12345\");\n }\n\n return { result: \"success\" };\n },\n [webrtc()],\n );\n\n console.log(sessionB.status.get());\n await sessionB.connect();\n\n await Promise.all([sessionA, sessionB].map(\n session => session.status.until(current => current === SessionStatus.CONNECTED),\n ));\n\n //\n console.log(\"A\", sessionA.status.get());\n console.log(\"B\", sessionB.status.get());\n\n const response = await sessionA.send({ data: \"test\" });\n\n console.log(\"response\", response);\n\n expect(response).toEqual({ result: \"success\" });\n\n // A throwing handler fails the request at once instead of leaving it to time out.\n await expect(sessionA.send({ data: \"boom\" })).rejects.toBeInstanceOf(RequestFailedError);\n });\n});\n"]}
@@ -1,12 +1,20 @@
1
1
  import type { EventEmitter } from "eventemitter3";
2
- import type { SessionMessage } from "./index.js";
2
+ import type { SessionMessageAck, SessionMessageFailed, SessionMessageResponse } from "./index.js";
3
+ /** The peer's handler threw. The peer sends no detail, so there is none here. */
4
+ export declare class RequestFailedError extends Error {
5
+ name: string;
6
+ constructor();
7
+ }
8
+ export type CorrelationEvents = {
9
+ message: SessionMessageAck | SessionMessageFailed | SessionMessageResponse;
10
+ /** The session ended; every open request fails with `reason`. */
11
+ ended: (reason: string) => void;
12
+ };
3
13
  /**
4
14
  * Await the ack and response correlated to a sent request.
5
15
  *
6
16
  * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
7
17
  * received the message; after the ack the wait extends to `responseTimeoutMs`
8
- * — enough for user-interactive flows such as `eth_sendTransaction`.
18
+ * -- enough for user-interactive flows such as `eth_sendTransaction`.
9
19
  */
10
- export declare const awaitCorrelatedResponse: (messages: EventEmitter<{
11
- message: SessionMessage;
12
- }>, messageId: string, ackTimeoutMs: number, responseTimeoutMs: number) => Promise<unknown>;
20
+ export declare const awaitCorrelatedResponse: (messages: EventEmitter<CorrelationEvents>, messageId: string, ackTimeoutMs: number, responseTimeoutMs: number) => Promise<unknown>;
@@ -1,46 +1,54 @@
1
+ import { createScope } from "@openlv/core";
2
+ /** The peer's handler threw. The peer sends no detail, so there is none here. */
3
+ export class RequestFailedError extends Error {
4
+ constructor() {
5
+ super("The remote peer failed to handle the request");
6
+ Object.defineProperty(this, "name", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: "RequestFailedError"
11
+ });
12
+ }
13
+ }
1
14
  /**
2
15
  * Await the ack and response correlated to a sent request.
3
16
  *
4
17
  * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it
5
18
  * received the message; after the ack the wait extends to `responseTimeoutMs`
6
- * — enough for user-interactive flows such as `eth_sendTransaction`.
19
+ * -- enough for user-interactive flows such as `eth_sendTransaction`.
7
20
  */
8
- export const awaitCorrelatedResponse = (messages, messageId, ackTimeoutMs, responseTimeoutMs) => new Promise((resolve, reject) => {
9
- let isAckReceived = false;
10
- // eslint-disable-next-line prefer-const
11
- let ackTimer;
12
- let responseTimer;
13
- const cleanup = () => {
14
- clearTimeout(ackTimer);
15
- clearTimeout(responseTimer);
16
- messages.off("message", handler);
17
- };
18
- const handler = (message) => {
19
- if (message.messageId !== messageId)
20
- return;
21
- if (message.type === "ack" && !isAckReceived) {
22
- isAckReceived = true;
23
- clearTimeout(ackTimer);
24
- // The other side confirmed receipt; wait for the full response.
25
- responseTimer = setTimeout(() => {
26
- cleanup();
27
- reject(new Error("Request timed out: no response after acknowledgement"));
28
- }, responseTimeoutMs);
29
- return;
30
- }
31
- if (message.type === "response") {
32
- cleanup();
33
- resolve(message.payload);
34
- }
35
- };
36
- messages.on("message", handler);
37
- // Short window for the ack — tells us the peer is alive and processing.
38
- ackTimer = setTimeout(() => {
39
- if (isAckReceived) {
40
- return;
41
- }
42
- cleanup();
43
- reject(new Error("Request timed out: remote peer did not acknowledge"));
44
- }, ackTimeoutMs);
45
- });
21
+ export const awaitCorrelatedResponse = async (messages, messageId, ackTimeoutMs, responseTimeoutMs) => {
22
+ const request = createScope();
23
+ // Short window for the ack -- tells us the peer is alive and processing.
24
+ const ackWait = request.child();
25
+ const timeout = (scope, delayMs, reason) => new Promise((_resolve, reject) => {
26
+ scope.timeout(delayMs, () => reject(new Error(`Request timed out: ${reason}`)));
27
+ });
28
+ const acknowledged = new Promise((resolve) => {
29
+ ackWait.listen(messages, "message", (message) => {
30
+ if (message.messageId === messageId && message.type === "ack")
31
+ resolve();
32
+ });
33
+ });
34
+ const response = new Promise((resolve, reject) => {
35
+ request.listen(messages, "ended", (reason) => reject(new Error(reason)));
36
+ request.listen(messages, "message", (message) => {
37
+ if (message.messageId !== messageId)
38
+ return;
39
+ if (message.type === "failed")
40
+ reject(new RequestFailedError());
41
+ else if (message.type === "response")
42
+ resolve(message.payload);
43
+ });
44
+ });
45
+ try {
46
+ await Promise.race([acknowledged, response, timeout(ackWait, ackTimeoutMs, "remote peer did not acknowledge")]);
47
+ await ackWait.close();
48
+ return await Promise.race([response, timeout(request, responseTimeoutMs, "no response after acknowledgement")]);
49
+ }
50
+ finally {
51
+ await request.close();
52
+ }
53
+ };
46
54
  //# sourceMappingURL=correlate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"correlate.js","sourceRoot":"","sources":["../../src/messages/correlate.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,QAAoD,EACpD,SAAiB,EACjB,YAAoB,EACpB,iBAAyB,EACP,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACrD,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,wCAAwC;IACxC,IAAI,QAAmD,CAAC;IACxD,IAAI,aAAwD,CAAC;IAE7D,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,YAAY,CAAC,QAAQ,CAAC,CAAC;QACvB,YAAY,CAAC,aAAa,CAAC,CAAC;QAC5B,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,OAAuB,EAAE,EAAE;QAC1C,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;YAAE,OAAO;QAE5C,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7C,aAAa,GAAG,IAAI,CAAC;YACrB,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvB,gEAAgE;YAChE,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,OAAO,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC,CAAC;YAC5E,CAAC,EAAE,iBAAiB,CAAC,CAAC;YAEtB,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAChC,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;IAEF,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAEhC,wEAAwE;IACxE,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE;QACzB,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,OAAO,EAAE,CAAC;QACV,MAAM,CAAC,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC,CAAC;IAC1E,CAAC,EAAE,YAAY,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC","sourcesContent":["import type { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionMessage } from \"./index.js\";\n\n/**\n * Await the ack and response correlated to a sent request.\n *\n * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it\n * received the message; after the ack the wait extends to `responseTimeoutMs`\n * — enough for user-interactive flows such as `eth_sendTransaction`.\n */\nexport const awaitCorrelatedResponse = (\n messages: EventEmitter<{ message: SessionMessage; }>,\n messageId: string,\n ackTimeoutMs: number,\n responseTimeoutMs: number,\n): Promise<unknown> => new Promise((resolve, reject) => {\n let isAckReceived = false;\n // eslint-disable-next-line prefer-const\n let ackTimer: ReturnType<typeof setTimeout> | undefined;\n let responseTimer: ReturnType<typeof setTimeout> | undefined;\n\n const cleanup = () => {\n clearTimeout(ackTimer);\n clearTimeout(responseTimer);\n messages.off(\"message\", handler);\n };\n\n const handler = (message: SessionMessage) => {\n if (message.messageId !== messageId) return;\n\n if (message.type === \"ack\" && !isAckReceived) {\n isAckReceived = true;\n clearTimeout(ackTimer);\n // The other side confirmed receipt; wait for the full response.\n responseTimer = setTimeout(() => {\n cleanup();\n reject(new Error(\"Request timed out: no response after acknowledgement\"));\n }, responseTimeoutMs);\n\n return;\n }\n\n if (message.type === \"response\") {\n cleanup();\n resolve(message.payload);\n }\n };\n\n messages.on(\"message\", handler);\n\n // Short window for the ack — tells us the peer is alive and processing.\n ackTimer = setTimeout(() => {\n if (isAckReceived) {\n return;\n }\n\n cleanup();\n reject(new Error(\"Request timed out: remote peer did not acknowledge\"));\n }, ackTimeoutMs);\n});\n"]}
1
+ {"version":3,"file":"correlate.js","sourceRoot":"","sources":["../../src/messages/correlate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AAKvD,iFAAiF;AACjF,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAG3C;QACE,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAH/C;;;;mBAAO,oBAAoB;WAAC;IAIrC,CAAC;CACF;AAQD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAC1C,QAAyC,EACzC,SAAiB,EACjB,YAAoB,EACpB,iBAAyB,EACP,EAAE;IACpB,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,yEAAyE;IACzE,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAEhC,MAAM,OAAO,GAAG,CAAC,KAAY,EAAE,OAAe,EAAE,MAAc,EAAE,EAAE,CAAC,IAAI,OAAO,CAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE;QACzG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,sBAAsB,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACjD,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,OAAqC,EAAE,EAAE;YAC5E,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK;gBAAE,OAAO,EAAE,CAAC;QAC3E,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACjF,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,OAAqC,EAAE,EAAE;YAC5E,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;gBAAE,OAAO;YAE5C,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,CAAC,IAAI,kBAAkB,EAAE,CAAC,CAAC;iBAC3D,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU;gBAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,iCAAiC,CAAC,CAAC,CAAC,CAAC;QAChH,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QAEtB,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,mCAAmC,CAAC,CAAC,CAAC,CAAC;IAClH,CAAC;YACO,CAAC;QACP,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC,CAAC","sourcesContent":["import { createScope, type Scope } from \"@openlv/core\";\nimport type { EventEmitter } from \"eventemitter3\";\n\nimport type { SessionMessageAck, SessionMessageFailed, SessionMessageResponse } from \"./index.js\";\n\n/** The peer's handler threw. The peer sends no detail, so there is none here. */\nexport class RequestFailedError extends Error {\n override name = \"RequestFailedError\";\n\n constructor() {\n super(\"The remote peer failed to handle the request\");\n }\n}\n\nexport type CorrelationEvents = {\n message: SessionMessageAck | SessionMessageFailed | SessionMessageResponse;\n /** The session ended; every open request fails with `reason`. */\n ended: (reason: string) => void;\n};\n\n/**\n * Await the ack and response correlated to a sent request.\n *\n * Two-phase timeout: the peer must ack within `ackTimeoutMs`, confirming it\n * received the message; after the ack the wait extends to `responseTimeoutMs`\n * -- enough for user-interactive flows such as `eth_sendTransaction`.\n */\nexport const awaitCorrelatedResponse = async (\n messages: EventEmitter<CorrelationEvents>,\n messageId: string,\n ackTimeoutMs: number,\n responseTimeoutMs: number,\n): Promise<unknown> => {\n const request = createScope();\n // Short window for the ack -- tells us the peer is alive and processing.\n const ackWait = request.child();\n\n const timeout = (scope: Scope, delayMs: number, reason: string) => new Promise<never>((_resolve, reject) => {\n scope.timeout(delayMs, () => reject(new Error(`Request timed out: ${reason}`)));\n });\n const acknowledged = new Promise<void>((resolve) => {\n ackWait.listen(messages, \"message\", (message: CorrelationEvents[\"message\"]) => {\n if (message.messageId === messageId && message.type === \"ack\") resolve();\n });\n });\n const response = new Promise<unknown>((resolve, reject) => {\n request.listen(messages, \"ended\", (reason: string) => reject(new Error(reason)));\n request.listen(messages, \"message\", (message: CorrelationEvents[\"message\"]) => {\n if (message.messageId !== messageId) return;\n\n if (message.type === \"failed\") reject(new RequestFailedError());\n else if (message.type === \"response\") resolve(message.payload);\n });\n });\n\n try {\n await Promise.race([acknowledged, response, timeout(ackWait, ackTimeoutMs, \"remote peer did not acknowledge\")]);\n await ackWait.close();\n\n return await Promise.race([response, timeout(request, responseTimeoutMs, \"no response after acknowledgement\")]);\n }\n finally {\n await request.close();\n }\n};\n"]}
@@ -0,0 +1,18 @@
1
+ import { EventEmitter } from "eventemitter3";
2
+ import { describe, expect, test } from "vitest";
3
+ import { awaitCorrelatedResponse, RequestFailedError } from "./correlate.js";
4
+ describe("awaitCorrelatedResponse", () => {
5
+ test.each([
6
+ ["before any ack", false],
7
+ ["after the ack", true],
8
+ ])("rejects at once when the peer reports failure %s", async (_name, hasAck) => {
9
+ const messages = new EventEmitter();
10
+ const pending = awaitCorrelatedResponse(messages, "request-1", 60000, 60000);
11
+ if (hasAck)
12
+ messages.emit("message", { type: "ack", messageId: "request-1" });
13
+ messages.emit("message", { type: "failed", messageId: "another-request" });
14
+ messages.emit("message", { type: "failed", messageId: "request-1" });
15
+ await expect(pending).rejects.toBeInstanceOf(RequestFailedError);
16
+ });
17
+ });
18
+ //# sourceMappingURL=correlate.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlate.test.js","sourceRoot":"","sources":["../../src/messages/correlate.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,uBAAuB,EAA0B,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAErG,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,IAAI,CAAC,IAAI,CAAC;QACR,CAAC,gBAAgB,EAAE,KAAK,CAAC;QACzB,CAAC,eAAe,EAAE,IAAI,CAAC;KACxB,CAAC,CAAC,kDAAkD,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QAC7E,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAqB,CAAC;QACvD,MAAM,OAAO,GAAG,uBAAuB,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAM,EAAE,KAAM,CAAC,CAAC;QAE/E,IAAI,MAAM;YAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAE9E,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAC3E,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAErE,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { EventEmitter } from \"eventemitter3\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { awaitCorrelatedResponse, type CorrelationEvents, RequestFailedError } from \"./correlate.js\";\n\ndescribe(\"awaitCorrelatedResponse\", () => {\n test.each([\n [\"before any ack\", false],\n [\"after the ack\", true],\n ])(\"rejects at once when the peer reports failure %s\", async (_name, hasAck) => {\n const messages = new EventEmitter<CorrelationEvents>();\n const pending = awaitCorrelatedResponse(messages, \"request-1\", 60_000, 60_000);\n\n if (hasAck) messages.emit(\"message\", { type: \"ack\", messageId: \"request-1\" });\n\n messages.emit(\"message\", { type: \"failed\", messageId: \"another-request\" });\n messages.emit(\"message\", { type: \"failed\", messageId: \"request-1\" });\n\n await expect(pending).rejects.toBeInstanceOf(RequestFailedError);\n });\n});\n"]}
@@ -1,15 +1,24 @@
1
- export type SessionMessage = SessionMessageRequest | SessionMessageResponse | SessionMessageAck;
1
+ export type SessionMessage = SessionMessageRequest | SessionMessageResponse | SessionMessageAck | SessionMessageFailed | SessionMessageClose;
2
2
  export type SessionMessageRequest = {
3
3
  type: "request";
4
4
  messageId: string;
5
- payload: object | string;
5
+ payload: unknown;
6
6
  };
7
7
  export type SessionMessageResponse = {
8
8
  type: "response";
9
9
  messageId: string;
10
- payload: object | string;
10
+ payload: unknown;
11
11
  };
12
12
  export type SessionMessageAck = {
13
13
  type: "ack";
14
14
  messageId: string;
15
15
  };
16
+ /** The request's handler threw; carries no detail on purpose. */
17
+ export type SessionMessageFailed = {
18
+ type: "failed";
19
+ messageId: string;
20
+ };
21
+ export type SessionMessageClose = {
22
+ type: "close";
23
+ };
24
+ export declare const isSessionMessage: (message: object) => message is SessionMessage;
@@ -1,2 +1,12 @@
1
- export {};
1
+ export const isSessionMessage = (message) => {
2
+ if (!("type" in message) || typeof message.type !== "string")
3
+ return false;
4
+ if (message.type === "close")
5
+ return true;
6
+ if (!("messageId" in message) || typeof message.messageId !== "string")
7
+ return false;
8
+ return message.type === "ack"
9
+ || message.type === "failed"
10
+ || ((message.type === "request" || message.type === "response") && "payload" in message);
11
+ };
2
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"","sourcesContent":["export type SessionMessage\n = | SessionMessageRequest\n | SessionMessageResponse\n | SessionMessageAck;\n\nexport type SessionMessageRequest = {\n type: \"request\";\n messageId: string;\n payload: object | string;\n};\n\nexport type SessionMessageResponse = {\n type: \"response\";\n messageId: string;\n payload: object | string;\n};\n\nexport type SessionMessageAck = {\n type: \"ack\";\n messageId: string;\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAA6B,EAAE;IAC7E,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE3E,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1C,IAAI,CAAC,CAAC,WAAW,IAAI,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAErF,OAAO,OAAO,CAAC,IAAI,KAAK,KAAK;WACxB,OAAO,CAAC,IAAI,KAAK,QAAQ;WACzB,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,SAAS,IAAI,OAAO,CAAC,CAAC;AAC7F,CAAC,CAAC","sourcesContent":["export type SessionMessage\n = | SessionMessageRequest\n | SessionMessageResponse\n | SessionMessageAck\n | SessionMessageFailed\n | SessionMessageClose;\n\nexport type SessionMessageRequest = {\n type: \"request\";\n messageId: string;\n payload: unknown;\n};\n\nexport type SessionMessageResponse = {\n type: \"response\";\n messageId: string;\n payload: unknown;\n};\n\nexport type SessionMessageAck = {\n type: \"ack\";\n messageId: string;\n};\n\n/** The request's handler threw; carries no detail on purpose. */\nexport type SessionMessageFailed = {\n type: \"failed\";\n messageId: string;\n};\n\nexport type SessionMessageClose = {\n type: \"close\";\n};\n\nexport const isSessionMessage = (message: object): message is SessionMessage => {\n if (!(\"type\" in message) || typeof message.type !== \"string\") return false;\n\n if (message.type === \"close\") return true;\n\n if (!(\"messageId\" in message) || typeof message.messageId !== \"string\") return false;\n\n return message.type === \"ack\"\n || message.type === \"failed\"\n || ((message.type === \"request\" || message.type === \"response\") && \"payload\" in message);\n};\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isSessionMessage } from "./index.js";
3
+ describe("isSessionMessage", () => {
4
+ it("accepts every session message type", () => {
5
+ expect(isSessionMessage({ type: "request", messageId: "1", payload: null })).toBe(true);
6
+ expect(isSessionMessage({ type: "response", messageId: "1", payload: "0x1" })).toBe(true);
7
+ expect(isSessionMessage({ type: "ack", messageId: "1" })).toBe(true);
8
+ expect(isSessionMessage({ type: "close" })).toBe(true);
9
+ });
10
+ it("rejects messages without a message id or payload", () => {
11
+ expect(isSessionMessage({ type: "ack" })).toBe(false);
12
+ expect(isSessionMessage({ type: "request", messageId: "1" })).toBe(false);
13
+ expect(isSessionMessage({ type: "response", messageId: 1, payload: {} })).toBe(false);
14
+ expect(isSessionMessage({ type: "unknown", messageId: "1", payload: {} })).toBe(false);
15
+ });
16
+ });
17
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/messages/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxF,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1F,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrE,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtD,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1E,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtF,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\n\nimport { isSessionMessage } from \"./index.js\";\n\ndescribe(\"isSessionMessage\", () => {\n it(\"accepts every session message type\", () => {\n expect(isSessionMessage({ type: \"request\", messageId: \"1\", payload: null })).toBe(true);\n expect(isSessionMessage({ type: \"response\", messageId: \"1\", payload: \"0x1\" })).toBe(true);\n expect(isSessionMessage({ type: \"ack\", messageId: \"1\" })).toBe(true);\n expect(isSessionMessage({ type: \"close\" })).toBe(true);\n });\n\n it(\"rejects messages without a message id or payload\", () => {\n expect(isSessionMessage({ type: \"ack\" })).toBe(false);\n expect(isSessionMessage({ type: \"request\", messageId: \"1\" })).toBe(false);\n expect(isSessionMessage({ type: \"response\", messageId: 1, payload: {} })).toBe(false);\n expect(isSessionMessage({ type: \"unknown\", messageId: \"1\", payload: {} })).toBe(false);\n });\n});\n"]}
@@ -1,25 +1,21 @@
1
1
  import { webrtc } from "@openlv/transport/webrtc";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { createSession } from "./index.js";
4
- // The guards under test throw before any signaling connection is attempted.
5
- const unreachableSignaling = () => {
6
- throw new Error("signaling should not be reached");
7
- };
8
4
  const linkParameters = { p: "ntfy", s: "https://ntfy.sh/" };
9
5
  const onMessage = async () => ({});
10
6
  describe("createSession input validation", () => {
11
7
  it("rejects an empty transport list", async () => {
12
- await expect(createSession(linkParameters, unreachableSignaling, [], onMessage)).rejects.toThrow("At least one transport is required");
8
+ await expect(createSession(linkParameters, [], onMessage)).rejects.toThrow("At least one transport is required");
13
9
  });
14
10
  it("rejects info a receiver would silently drop", async () => {
15
- await expect(createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {
11
+ await expect(createSession(linkParameters, [webrtc()], onMessage, {
16
12
  info: {
17
13
  identity: "com.example.dapp",
18
14
  name: "Example dApp",
19
15
  icon: "x".repeat(10000),
20
16
  },
21
17
  })).rejects.toThrow(/icon/);
22
- await expect(createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {
18
+ await expect(createSession(linkParameters, [webrtc()], onMessage, {
23
19
  info: { identity: "", name: "Example dApp" },
24
20
  })).rejects.toThrow(/identity/);
25
21
  });
@@ -1 +1 @@
1
- {"version":3,"file":"options.test.js","sourceRoot":"","sources":["../src/options.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,4EAA4E;AAC5E,MAAM,oBAAoB,GAAsB,GAAG,EAAE;IACnD,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACrD,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC;AAC5D,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAEnC,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,oBAAoB,EAAE,EAAE,EAAE,SAAS,CAAC,CACnE,CAAC,OAAO,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,oBAAoB,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACzE,IAAI,EAAE;gBACJ,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,KAAM,CAAC;aACzB;SACF,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE1B,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,oBAAoB,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACzE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;SAC7C,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import type { SignalingProtocol } from \"@openlv/signaling\";\nimport { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { createSession } from \"./index.js\";\n\n// The guards under test throw before any signaling connection is attempted.\nconst unreachableSignaling: SignalingProtocol = () => {\n throw new Error(\"signaling should not be reached\");\n};\n\nconst linkParameters = { p: \"ntfy\", s: \"https://ntfy.sh/\" };\nconst onMessage = async () => ({});\n\ndescribe(\"createSession input validation\", () => {\n it(\"rejects an empty transport list\", async () => {\n await expect(\n createSession(linkParameters, unreachableSignaling, [], onMessage),\n ).rejects.toThrow(\"At least one transport is required\");\n });\n\n it(\"rejects info a receiver would silently drop\", async () => {\n await expect(\n createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {\n info: {\n identity: \"com.example.dapp\",\n name: \"Example dApp\",\n icon: \"x\".repeat(10_000),\n },\n }),\n ).rejects.toThrow(/icon/);\n\n await expect(\n createSession(linkParameters, unreachableSignaling, [webrtc()], onMessage, {\n info: { identity: \"\", name: \"Example dApp\" },\n }),\n ).rejects.toThrow(/identity/);\n });\n});\n"]}
1
+ {"version":3,"file":"options.test.js","sourceRoot":"","sources":["../src/options.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC;AAC5D,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAEnC,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,EAAE,EAAE,SAAS,CAAC,CAC7C,CAAC,OAAO,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACnD,IAAI,EAAE;gBACJ,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,KAAM,CAAC;aACzB;SACF,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAE1B,MAAM,MAAM,CACV,aAAa,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE;YACnD,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;SAC7C,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { webrtc } from \"@openlv/transport/webrtc\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { createSession } from \"./index.js\";\n\nconst linkParameters = { p: \"ntfy\", s: \"https://ntfy.sh/\" };\nconst onMessage = async () => ({});\n\ndescribe(\"createSession input validation\", () => {\n it(\"rejects an empty transport list\", async () => {\n await expect(\n createSession(linkParameters, [], onMessage),\n ).rejects.toThrow(\"At least one transport is required\");\n });\n\n it(\"rejects info a receiver would silently drop\", async () => {\n await expect(\n createSession(linkParameters, [webrtc()], onMessage, {\n info: {\n identity: \"com.example.dapp\",\n name: \"Example dApp\",\n icon: \"x\".repeat(10_000),\n },\n }),\n ).rejects.toThrow(/icon/);\n\n await expect(\n createSession(linkParameters, [webrtc()], onMessage, {\n info: { identity: \"\", name: \"Example dApp\" },\n }),\n ).rejects.toThrow(/identity/);\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlv/session",
3
- "version": "0.1.3",
3
+ "version": "0.3.1",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
@@ -37,13 +37,13 @@
37
37
  "eventemitter3": "^5.0.1",
38
38
  "globals": "^16.2.0",
39
39
  "ts-pattern": "^5",
40
- "@openlv/core": "0.1.2",
41
- "@openlv/signaling": "0.1.3",
42
- "@openlv/transport": "0.1.2"
40
+ "@openlv/core": "0.3.0",
41
+ "@openlv/signaling": "0.3.1",
42
+ "@openlv/transport": "0.3.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@vitest/browser-playwright": "^4.0.8",
46
- "playwright": "1.58.1",
46
+ "playwright": "1.60.0",
47
47
  "typescript": "^5",
48
48
  "vitest": "^4"
49
49
  },
@@ -1,7 +0,0 @@
1
- /**
2
- * Pick the transport once both capability lists are known: the first entry
3
- * in the host's preference list that the client also supports. Both peers
4
- * compute the same result independently — no confirmation round trip.
5
- * Unknown identifiers are skipped, not fatal.
6
- */
7
- export declare const selectTransportId: (isHost: boolean, ownTransports: string[], peerTransports: string[]) => string | undefined;
@@ -1,12 +0,0 @@
1
- /**
2
- * Pick the transport once both capability lists are known: the first entry
3
- * in the host's preference list that the client also supports. Both peers
4
- * compute the same result independently — no confirmation round trip.
5
- * Unknown identifiers are skipped, not fatal.
6
- */
7
- export const selectTransportId = (isHost, ownTransports, peerTransports) => {
8
- const hostPreference = isHost ? ownTransports : peerTransports;
9
- const clientSupported = new Set(isHost ? peerTransports : ownTransports);
10
- return hostPreference.find(transportId => clientSupported.has(transportId));
11
- };
12
- //# sourceMappingURL=transports.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transports.js","sourceRoot":"","sources":["../src/transports.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,MAAe,EACf,aAAuB,EACvB,cAAwB,EACJ,EAAE;IACtB,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc,CAAC;IAC/D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAEzE,OAAO,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;AAC9E,CAAC,CAAC","sourcesContent":["/**\n * Pick the transport once both capability lists are known: the first entry\n * in the host's preference list that the client also supports. Both peers\n * compute the same result independently — no confirmation round trip.\n * Unknown identifiers are skipped, not fatal.\n */\nexport const selectTransportId = (\n isHost: boolean,\n ownTransports: string[],\n peerTransports: string[],\n): string | undefined => {\n const hostPreference = isHost ? ownTransports : peerTransports;\n const clientSupported = new Set(isHost ? peerTransports : ownTransports);\n\n return hostPreference.find(transportId => clientSupported.has(transportId));\n};\n"]}
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { selectTransportId } from "./transports.js";
3
- describe("selectTransportId", () => {
4
- it("picks the first host preference the client supports", () => {
5
- expect(selectTransportId(true, ["ws", "wrtc"], ["wrtc", "ws"])).toBe("ws");
6
- expect(selectTransportId(true, ["wrtc", "ws"], ["ws"])).toBe("ws");
7
- });
8
- it("computes the same result from both perspectives", () => {
9
- const hostTransports = ["ws", "wrtc"];
10
- const clientTransports = ["wrtc", "ws"];
11
- expect(selectTransportId(true, hostTransports, clientTransports))
12
- .toBe(selectTransportId(false, clientTransports, hostTransports));
13
- });
14
- it("skips identifiers the client has never heard of", () => {
15
- expect(selectTransportId(true, ["quic", "wrtc"], ["wrtc"])).toBe("wrtc");
16
- });
17
- it("returns undefined when there is no common transport", () => {
18
- expect(selectTransportId(true, ["ws"], ["wrtc"])).toBeUndefined();
19
- expect(selectTransportId(true, ["wrtc"], [])).toBeUndefined();
20
- });
21
- });
22
- //# sourceMappingURL=transports.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transports.test.js","sourceRoot":"","sources":["../src/transports.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3E,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAExC,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAAC;aAC9D,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,EAAE,cAAc,CAAC,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QAClE,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\n\nimport { selectTransportId } from \"./transports.js\";\n\ndescribe(\"selectTransportId\", () => {\n it(\"picks the first host preference the client supports\", () => {\n expect(selectTransportId(true, [\"ws\", \"wrtc\"], [\"wrtc\", \"ws\"])).toBe(\"ws\");\n expect(selectTransportId(true, [\"wrtc\", \"ws\"], [\"ws\"])).toBe(\"ws\");\n });\n\n it(\"computes the same result from both perspectives\", () => {\n const hostTransports = [\"ws\", \"wrtc\"];\n const clientTransports = [\"wrtc\", \"ws\"];\n\n expect(selectTransportId(true, hostTransports, clientTransports))\n .toBe(selectTransportId(false, clientTransports, hostTransports));\n });\n\n it(\"skips identifiers the client has never heard of\", () => {\n expect(selectTransportId(true, [\"quic\", \"wrtc\"], [\"wrtc\"])).toBe(\"wrtc\");\n });\n\n it(\"returns undefined when there is no common transport\", () => {\n expect(selectTransportId(true, [\"ws\"], [\"wrtc\"])).toBeUndefined();\n expect(selectTransportId(true, [\"wrtc\"], [])).toBeUndefined();\n });\n});\n"]}