@openlv/session 0.2.0 → 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.
package/dist/events.d.ts CHANGED
@@ -4,5 +4,5 @@ export type SessionEvents = {
4
4
  * is sent. Useful for observability, logging, or UI integration without
5
5
  * needing to intercept the `onMessage` callback.
6
6
  */
7
- request: (payload: object | string) => void;
7
+ request: (payload: unknown) => void;
8
8
  };
@@ -1 +1 @@
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: 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
@@ -4,6 +4,8 @@ import { type TransportLayer, type TransportLayerFunction } from "@openlv/transp
4
4
  import { EventEmitter } from "eventemitter3";
5
5
  import type { SessionEvents } from "./events.js";
6
6
  export { loadSignaling, loadTransport } from "./dynamic.js";
7
+ export { RequestFailedError } from "./messages/correlate.js";
8
+ export type { SessionMessage } from "./messages/index.js";
7
9
  export type { PeerInfo } from "@openlv/signaling";
8
10
  export declare const SessionStatus: {
9
11
  readonly CREATED: "created";
@@ -30,7 +32,7 @@ export type Session = {
30
32
  getHandshakeParameters(): SessionHandshakeParameters;
31
33
  connect(): Promise<void>;
32
34
  close(): Promise<void>;
33
- send(message: object, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
35
+ send(message: unknown, ackTimeout?: number, responseTimeout?: number): Promise<unknown>;
34
36
  emitter: EventEmitter<SessionEvents>;
35
37
  _internal: {
36
38
  signal: SignalingLayer;
@@ -43,8 +45,8 @@ export type Session = {
43
45
  *
44
46
  * https://openlv.sh/api/session
45
47
  */
46
- export declare const createSession: (initParameters: SessionLinkParameters, transportLayers: TransportLayerFunction[], 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>;
47
49
  /**
48
50
  * Connect to a session from its openlv:// URL
49
51
  */
50
- export declare const connectSession: (connectionUrl: string, onMessage: (message: object) => Promise<object | string>, transports: TransportLayerFunction[], 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,12 +1,14 @@
1
- import { decodeConnectionURL, observable, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
- import { deriveSymmetricKey, generateHandshakeKey, generateSessionId, initEncryptionKeys, initHash, } from "@openlv/core/encryption";
1
+ import { createScope, decodeConnectionURL, observable, OPENLV_PROTOCOL_VERSION, } from "@openlv/core";
2
+ import { createHandshakeCipher, generateHandshakeKey, generateKeyPair, generateSessionId, hashPublicKey, } from "@openlv/core/encryption";
3
3
  import { Status as SignalStatus, validatePeerInfo, } from "@openlv/signaling";
4
4
  import { Status as TransportStatus, } from "@openlv/transport";
5
5
  import { EventEmitter } from "eventemitter3";
6
6
  import { loadSignaling } from "./dynamic.js";
7
7
  import { awaitCorrelatedResponse } from "./messages/correlate.js";
8
+ import { isSessionMessage } from "./messages/index.js";
8
9
  import { log } from "./utils/log.js";
9
10
  export { loadSignaling, loadTransport } from "./dynamic.js";
11
+ export { RequestFailedError } from "./messages/correlate.js";
10
12
  export const SessionStatus = {
11
13
  CREATED: "created",
12
14
  SIGNALING: "signaling",
@@ -34,11 +36,11 @@ export const createSession = async (initParameters, transportLayers, onMessage,
34
36
  const sessionId = "sessionId" in initParameters
35
37
  ? initParameters.sessionId
36
38
  : generateSessionId();
37
- const { encryptionKey, decryptionKey: { decrypt }, } = await initEncryptionKeys(initParameters);
38
- const handshakeKey = "k" in initParameters
39
- ? await deriveSymmetricKey(initParameters.k)
40
- : await generateHandshakeKey();
41
- const { hash, isHost } = await initHash("h" in initParameters ? initParameters.h : undefined, encryptionKey);
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;
42
44
  const protocol = initParameters.p;
43
45
  const server = initParameters.s;
44
46
  const capabilities = {
@@ -56,74 +58,78 @@ export const createSession = async (initParameters, transportLayers, onMessage,
56
58
  log("updateStatus", newStatus);
57
59
  setStatus(newStatus);
58
60
  };
61
+ status.subscribe((current) => {
62
+ if (current === SessionStatus.DISCONNECTED) {
63
+ messages.emit("ended", lastError.get() ?? "Session closed");
64
+ }
65
+ });
59
66
  const signalLayer = await loadSignaling(protocol);
60
67
  const signaling = await signalLayer({
61
68
  topic: sessionId,
62
69
  url: server,
63
70
  });
64
- const canEncrypt = () => signal.peerKey.get() !== undefined;
65
71
  const signal = await signaling({
66
72
  h: hash,
67
- canEncrypt,
68
- async encrypt(message) {
69
- const key = signal.peerKey.get();
70
- if (!key) {
71
- throw new Error("Relying party public key not found");
72
- }
73
- return await key.encrypt(message);
74
- },
75
- decrypt,
76
- publicKey: encryptionKey,
77
- k: handshakeKey,
73
+ handshakeCipher: await createHandshakeCipher(k),
74
+ keyPair,
78
75
  capabilities,
79
76
  isHost,
80
77
  });
81
- signal.peerKey.subscribe((key) => {
78
+ const scope = createScope();
79
+ scope.add(signal.teardown);
80
+ signal.peer.subscribe((peer) => {
82
81
  const role = isHost ? "host" : "client";
83
- log(`rpKey discovered by ${role}`, key);
82
+ log(`rpKey discovered by ${role}`, peer?.peerKey);
84
83
  });
85
84
  signal.peerCapabilities.subscribe((received) => {
86
85
  log("peer capabilities received", received);
87
86
  setPeerInfo(received?.info);
88
87
  });
89
88
  let transport;
90
- const createTransport = (layer) => layer.create({
91
- async encrypt(message) {
92
- const key = signal.peerKey.get();
93
- if (!key) {
94
- throw new Error("Relying party public key not found");
95
- }
96
- return await key.encrypt(message);
97
- },
98
- decrypt,
89
+ const createTransport = (layer, cipher) => layer.create({
90
+ cipher,
99
91
  isHost,
100
92
  onmessage: async (message) => {
101
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
+ }
102
102
  if (message["type"] === "request") {
103
103
  const messageId = message["messageId"];
104
104
  try {
105
105
  // Immediately acknowledge receipt so the sender's ack-timeout does
106
106
  // not fire while the handler (which may await user interaction) runs.
107
107
  await transport?.send({ type: "ack", messageId });
108
- // Notify observers before processing (e.g. wallet UI can show a
109
- // pending indicator before the handler resolves).
110
- emitter.emit("request", message["payload"]);
111
- const data = await onMessage(message["payload"])
112
- // A throwing handler must still answer, otherwise the peer waits
113
- // out its full response timeout.
114
- .catch(() => ({ error: { code: -32603, message: "Internal error" } }));
115
- await transport?.send({
116
- type: "response",
117
- messageId,
118
- payload: data,
119
- });
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);
120
126
  }
121
127
  catch (error) {
122
128
  log("failed to respond to peer request", error);
123
129
  }
124
130
  }
125
- if (message["type"] === "response" || message["type"] === "ack") {
126
- // 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.
127
133
  messages.emit("message", message);
128
134
  }
129
135
  },
@@ -156,21 +162,16 @@ export const createSession = async (initParameters, transportLayers, onMessage,
156
162
  // connect promptly; if it cannot (e.g. no ICE candidates on a restricted
157
163
  // network) fail loudly instead of sitting in "linking" forever.
158
164
  const TRANSPORT_LINK_TIMEOUT_MS = 45000;
159
- let linkDeadline;
160
- let unsubscribeTransportStatus;
161
- const clearLinkDeadline = () => {
162
- clearTimeout(linkDeadline);
163
- linkDeadline = undefined;
164
- };
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);
165
168
  const onTransportStateChange = (transportStatus) => {
166
169
  log("transport state change", transportStatus);
167
170
  if (transportStatus === TransportStatus.CONNECTED) {
168
- clearLinkDeadline();
169
171
  setLastError(undefined);
170
172
  updateStatus(SessionStatus.CONNECTED);
171
173
  }
172
174
  if (transportStatus === TransportStatus.ERROR) {
173
- clearLinkDeadline();
174
175
  setLastErrorIfUnset("Peer-to-peer transport failed");
175
176
  updateStatus(SessionStatus.DISCONNECTED);
176
177
  }
@@ -189,20 +190,22 @@ export const createSession = async (initParameters, transportLayers, onMessage,
189
190
  return;
190
191
  }
191
192
  log("selected transport", layer.transportId);
192
- transport = createTransport(layer);
193
- unsubscribeTransportStatus = transport.status.subscribe(onTransportStateChange);
194
- 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
+ });
195
206
  }
196
- linkDeadline ?? (linkDeadline = setTimeout(() => {
197
- if (status.get() === SessionStatus.CONNECTED)
198
- return;
199
- log("transport failed to connect in time");
200
- setLastErrorIfUnset("Timed out establishing the peer-to-peer connection");
201
- updateStatus(SessionStatus.DISCONNECTED);
202
- }, TRANSPORT_LINK_TIMEOUT_MS));
203
207
  Promise.resolve(transport.setup()).catch((error) => {
204
208
  log("transport setup failed", error);
205
- clearLinkDeadline();
206
209
  setLastErrorIfUnset(error instanceof Error ? error.message : "Transport setup failed");
207
210
  updateStatus(SessionStatus.DISCONNECTED);
208
211
  });
@@ -248,27 +251,23 @@ export const createSession = async (initParameters, transportLayers, onMessage,
248
251
  updateStatus(SessionStatus.DISCONNECTED);
249
252
  }
250
253
  };
251
- let unsubscribeSignalStatus;
254
+ const teardown = async () => {
255
+ await scope.close();
256
+ updateStatus(SessionStatus.DISCONNECTED);
257
+ };
252
258
  return {
253
259
  connect: async () => {
254
260
  updateStatus(SessionStatus.SIGNALING);
255
261
  log("connecting to session, isHost:", isHost);
256
- signal.on("message", onSignalMessage);
257
- unsubscribeSignalStatus = signal.status.subscribe(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
- unsubscribeSignalStatus?.();
265
- unsubscribeTransportStatus?.();
266
- transport?.emitter.off("error", onTransportError);
267
- await Promise.all([
268
- transport?.teardown(),
269
- signal.teardown(),
270
- ]);
271
- updateStatus(SessionStatus.DISCONNECTED);
268
+ await transport?.send({ type: "close" })
269
+ .catch(error => log("failed to send close to peer", error));
270
+ await teardown();
272
271
  },
273
272
  status,
274
273
  signalStatus: signal.status,
@@ -279,7 +278,7 @@ export const createSession = async (initParameters, transportLayers, onMessage,
279
278
  version: OPENLV_PROTOCOL_VERSION,
280
279
  sessionId,
281
280
  h: hash,
282
- k: handshakeKey.toString(),
281
+ k,
283
282
  p: protocol,
284
283
  s: server,
285
284
  };
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 type Observable,\n observable,\n OPENLV_PROTOCOL_VERSION,\n type SessionHandshakeParameters,\n type SessionLinkParameters,\n} from \"@openlv/core\";\nimport {\n deriveSymmetricKey,\n generateHandshakeKey,\n generateSessionId,\n initEncryptionKeys,\n initHash,\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 } from \"./messages/correlate.js\";\nimport type { SessionMessage } from \"./messages/index.js\";\nimport { log } from \"./utils/log.js\";\n\nexport { loadSignaling, loadTransport } from \"./dynamic.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(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 transportLayers: TransportLayerFunction[],\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 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 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 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 const signalLayer = await loadSignaling(protocol);\n const signaling = await signalLayer({\n topic: sessionId,\n url: server,\n });\n const canEncrypt = () => signal.peerKey.get() !== undefined;\n const signal = await signaling({\n h: hash,\n canEncrypt,\n async encrypt(message) {\n const key = signal.peerKey.get();\n\n if (!key) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return await key.encrypt(message);\n },\n decrypt,\n publicKey: encryptionKey,\n k: handshakeKey,\n capabilities,\n isHost,\n });\n\n signal.peerKey.subscribe((key) => {\n const role = isHost ? \"host\" : \"client\";\n\n log(`rpKey discovered by ${role}`, key);\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): TransportLayer => layer.create({\n async encrypt(message) {\n const key = signal.peerKey.get();\n\n if (!key) {\n throw new Error(\"Relying party public key not found\");\n }\n\n return await key.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 = (): 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 let linkDeadline: ReturnType<typeof setTimeout> | undefined;\n let unsubscribeTransportStatus: (() => void) | undefined;\n\n const clearLinkDeadline = () => {\n clearTimeout(linkDeadline);\n linkDeadline = undefined;\n };\n\n const onTransportStateChange = (transportStatus: TransportStatus) => {\n log(\"transport state change\", transportStatus);\n\n if (transportStatus === TransportStatus.CONNECTED) {\n clearLinkDeadline();\n setLastError(undefined);\n updateStatus(SessionStatus.CONNECTED);\n }\n\n if (transportStatus === TransportStatus.ERROR) {\n clearLinkDeadline();\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 transport = createTransport(layer);\n unsubscribeTransportStatus = transport.status.subscribe(onTransportStateChange);\n transport.emitter.on(\"error\", onTransportError);\n }\n\n linkDeadline ??= setTimeout(() => {\n if (status.get() === SessionStatus.CONNECTED) return;\n\n log(\"transport failed to connect in time\");\n setLastErrorIfUnset(\"Timed out establishing the peer-to-peer connection\");\n updateStatus(SessionStatus.DISCONNECTED);\n }, TRANSPORT_LINK_TIMEOUT_MS);\n\n Promise.resolve(transport.setup()).catch((error) => {\n log(\"transport setup failed\", error);\n clearLinkDeadline();\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 let unsubscribeSignalStatus: (() => void) | undefined;\n\n return {\n connect: async () => {\n updateStatus(SessionStatus.SIGNALING);\n log(\"connecting to session, isHost:\", isHost);\n\n signal.on(\"message\", onSignalMessage);\n unsubscribeSignalStatus = signal.status.subscribe(onSignalStateChange);\n\n await signal.setup();\n },\n async close() {\n log(\"session teardown\");\n clearLinkDeadline();\n signal.off(\"message\", onSignalMessage);\n unsubscribeSignalStatus?.();\n unsubscribeTransportStatus?.();\n transport?.emitter.off(\"error\", onTransportError);\n await Promise.all([\n transport?.teardown(),\n signal.teardown(),\n ]);\n updateStatus(SessionStatus.DISCONNECTED);\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: handshakeKey.toString(),\n p: protocol,\n s: server,\n };\n },\n async send(\n message: object,\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: object) => Promise<object | string>,\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
+ {"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,7 +1,7 @@
1
1
  import { decodeConnectionURL, encodeConnectionURL } from "@openlv/core";
2
2
  import { webrtc } from "@openlv/transport/webrtc";
3
3
  import { describe, expect, test } from "vitest";
4
- import { connectSession, createSession, SessionStatus } from "./index.js";
4
+ import { connectSession, createSession, RequestFailedError, SessionStatus } from "./index.js";
5
5
  describe("Session", () => {
6
6
  test("Should be able to create a session", async () => {
7
7
  const sessionA = await createSession({
@@ -25,6 +25,9 @@ describe("Session", () => {
25
25
  console.log("Connecting to session B");
26
26
  const sessionB = await connectSession(encodedUrl, async (message) => {
27
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
+ }
28
31
  return { result: "success" };
29
32
  }, [webrtc()]);
30
33
  console.log(sessionB.status.get());
@@ -36,6 +39,8 @@ describe("Session", () => {
36
39
  const response = await sessionA.send({ data: "test" });
37
40
  console.log("response", response);
38
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);
39
44
  });
40
45
  });
41
46
  //# sourceMappingURL=index.test.js.map
@@ -1 +1 @@
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, 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 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});\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,5 +1,15 @@
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
  *
@@ -7,6 +17,4 @@ import type { SessionMessage } from "./index.js";
7
17
  * received the message; after the ack the wait extends to `responseTimeoutMs`
8
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,3 +1,16 @@
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
  *
@@ -5,42 +18,37 @@
5
18
  * received the message; after the ack the wait extends to `responseTimeoutMs`
6
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);
21
+ export const awaitCorrelatedResponse = async (messages, messageId, ackTimeoutMs, responseTimeoutMs) => {
22
+ const request = createScope();
37
23
  // 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
- });
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,yEAAyE;IACzE,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 @@
1
+ export {};
@@ -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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlv/session",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "keywords": [
5
5
  "open-lavatory"
6
6
  ],
@@ -37,9 +37,9 @@
37
37
  "eventemitter3": "^5.0.1",
38
38
  "globals": "^16.2.0",
39
39
  "ts-pattern": "^5",
40
- "@openlv/core": "0.2.0",
41
- "@openlv/signaling": "0.2.0",
42
- "@openlv/transport": "0.2.0"
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",