@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 +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +76 -77
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +6 -1
- package/dist/index.test.js.map +1 -1
- package/dist/messages/correlate.d.ts +12 -4
- package/dist/messages/correlate.js +45 -37
- package/dist/messages/correlate.js.map +1 -1
- package/dist/messages/correlate.test.d.ts +1 -0
- package/dist/messages/correlate.test.js +18 -0
- package/dist/messages/correlate.test.js.map +1 -0
- package/dist/messages/index.d.ts +12 -3
- package/dist/messages/index.js +11 -1
- package/dist/messages/index.js.map +1 -1
- package/dist/messages/index.test.d.ts +1 -0
- package/dist/messages/index.test.js +17 -0
- package/dist/messages/index.test.js.map +1 -0
- package/package.json +4 -4
package/dist/events.d.ts
CHANGED
package/dist/events.js.map
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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}`,
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
126
|
-
//
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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"]}
|
package/dist/index.test.js
CHANGED
|
@@ -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
|
package/dist/index.test.js.map
CHANGED
|
@@ -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,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;
|
|
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 {
|
|
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) =>
|
|
9
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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":"
|
|
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"]}
|
package/dist/messages/index.d.ts
CHANGED
|
@@ -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:
|
|
5
|
+
payload: unknown;
|
|
6
6
|
};
|
|
7
7
|
export type SessionMessageResponse = {
|
|
8
8
|
type: "response";
|
|
9
9
|
messageId: string;
|
|
10
|
-
payload:
|
|
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;
|
package/dist/messages/index.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
41
|
-
"@openlv/signaling": "0.
|
|
42
|
-
"@openlv/transport": "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",
|