@aegis-fluxion/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/dist/index.cjs +424 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +424 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ interface SecureEnvelope<TData = unknown> {
|
|
|
5
5
|
event: string;
|
|
6
6
|
data: TData;
|
|
7
7
|
}
|
|
8
|
+
interface SecureAckOptions {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
type SecureAckCallback = (error: Error | null, response?: unknown) => void;
|
|
8
12
|
interface SecureServerHeartbeatOptions {
|
|
9
13
|
enabled?: boolean;
|
|
10
14
|
intervalMs?: number;
|
|
@@ -31,6 +35,7 @@ interface SecureServerClient {
|
|
|
31
35
|
id: string;
|
|
32
36
|
socket: WebSocket;
|
|
33
37
|
request: IncomingMessage;
|
|
38
|
+
emit: (event: string, data: unknown, callbackOrOptions?: SecureAckCallback | SecureAckOptions, maybeCallback?: SecureAckCallback) => boolean | Promise<unknown>;
|
|
34
39
|
join: (room: string) => boolean;
|
|
35
40
|
leave: (room: string) => boolean;
|
|
36
41
|
leaveAll: () => number;
|
|
@@ -39,11 +44,11 @@ interface SecureServerRoomOperator {
|
|
|
39
44
|
emit: (event: string, data: unknown) => SecureServer;
|
|
40
45
|
}
|
|
41
46
|
type SecureErrorHandler = (error: Error) => void;
|
|
42
|
-
type SecureServerEventHandler = (data: unknown, client: SecureServerClient) =>
|
|
47
|
+
type SecureServerEventHandler = (data: unknown, client: SecureServerClient) => unknown | Promise<unknown>;
|
|
43
48
|
type SecureServerConnectionHandler = (client: SecureServerClient) => void;
|
|
44
49
|
type SecureServerDisconnectHandler = (client: SecureServerClient, code: number, reason: string) => void;
|
|
45
50
|
type SecureServerReadyHandler = (client: SecureServerClient) => void;
|
|
46
|
-
type SecureClientEventHandler = (data: unknown) =>
|
|
51
|
+
type SecureClientEventHandler = (data: unknown) => unknown | Promise<unknown>;
|
|
47
52
|
type SecureClientConnectHandler = () => void;
|
|
48
53
|
type SecureClientDisconnectHandler = (code: number, reason: string) => void;
|
|
49
54
|
type SecureClientReadyHandler = () => void;
|
|
@@ -76,6 +81,7 @@ declare class SecureServer {
|
|
|
76
81
|
private readonly sharedSecretBySocket;
|
|
77
82
|
private readonly encryptionKeyBySocket;
|
|
78
83
|
private readonly pendingPayloadsBySocket;
|
|
84
|
+
private readonly pendingRpcRequestsBySocket;
|
|
79
85
|
private readonly heartbeatStateBySocket;
|
|
80
86
|
private readonly roomMembersByName;
|
|
81
87
|
private readonly roomNamesByClientId;
|
|
@@ -94,6 +100,9 @@ declare class SecureServer {
|
|
|
94
100
|
off(event: string, handler: SecureServerEventHandler): this;
|
|
95
101
|
emit(event: string, data: unknown): this;
|
|
96
102
|
emitTo(clientId: string, event: string, data: unknown): boolean;
|
|
103
|
+
emitTo(clientId: string, event: string, data: unknown, callback: SecureAckCallback): boolean;
|
|
104
|
+
emitTo(clientId: string, event: string, data: unknown, options: SecureAckOptions): Promise<unknown>;
|
|
105
|
+
emitTo(clientId: string, event: string, data: unknown, options: SecureAckOptions, callback: SecureAckCallback): boolean;
|
|
97
106
|
to(room: string): SecureServerRoomOperator;
|
|
98
107
|
close(code?: number, reason?: string): void;
|
|
99
108
|
private resolveHeartbeatConfig;
|
|
@@ -108,6 +117,11 @@ declare class SecureServer {
|
|
|
108
117
|
private dispatchCustomEvent;
|
|
109
118
|
private sendRaw;
|
|
110
119
|
private sendEncryptedEnvelope;
|
|
120
|
+
private sendRpcRequest;
|
|
121
|
+
private handleRpcResponse;
|
|
122
|
+
private handleRpcRequest;
|
|
123
|
+
private executeRpcRequestHandler;
|
|
124
|
+
private rejectPendingRpcRequests;
|
|
111
125
|
private notifyConnection;
|
|
112
126
|
private notifyReady;
|
|
113
127
|
private notifyError;
|
|
@@ -140,6 +154,7 @@ declare class SecureClient {
|
|
|
140
154
|
private readonly errorHandlers;
|
|
141
155
|
private handshakeState;
|
|
142
156
|
private pendingPayloadQueue;
|
|
157
|
+
private readonly pendingRpcRequests;
|
|
143
158
|
constructor(url: string, options?: SecureClientOptions);
|
|
144
159
|
get readyState(): number | null;
|
|
145
160
|
isConnected(): boolean;
|
|
@@ -156,6 +171,9 @@ declare class SecureClient {
|
|
|
156
171
|
off(event: "error", handler: SecureErrorHandler): this;
|
|
157
172
|
off(event: string, handler: SecureClientEventHandler): this;
|
|
158
173
|
emit(event: string, data: unknown): boolean;
|
|
174
|
+
emit(event: string, data: unknown, callback: SecureAckCallback): boolean;
|
|
175
|
+
emit(event: string, data: unknown, options: SecureAckOptions): Promise<unknown>;
|
|
176
|
+
emit(event: string, data: unknown, options: SecureAckOptions, callback: SecureAckCallback): boolean;
|
|
159
177
|
private resolveReconnectConfig;
|
|
160
178
|
private scheduleReconnect;
|
|
161
179
|
private computeReconnectDelay;
|
|
@@ -169,6 +187,11 @@ declare class SecureClient {
|
|
|
169
187
|
private notifyReady;
|
|
170
188
|
private notifyError;
|
|
171
189
|
private sendEncryptedEnvelope;
|
|
190
|
+
private sendRpcRequest;
|
|
191
|
+
private handleRpcResponse;
|
|
192
|
+
private handleRpcRequest;
|
|
193
|
+
private executeRpcRequestHandler;
|
|
194
|
+
private rejectPendingRpcRequests;
|
|
172
195
|
private createClientHandshakeState;
|
|
173
196
|
private sendInternalHandshake;
|
|
174
197
|
private handleInternalHandshake;
|
|
@@ -176,4 +199,4 @@ declare class SecureClient {
|
|
|
176
199
|
private flushPendingPayloadQueue;
|
|
177
200
|
}
|
|
178
201
|
|
|
179
|
-
export { SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
|
|
202
|
+
export { type SecureAckCallback, type SecureAckOptions, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import WebSocket, { WebSocketServer } from 'ws';
|
|
|
5
5
|
var DEFAULT_CLOSE_CODE = 1e3;
|
|
6
6
|
var DEFAULT_CLOSE_REASON = "";
|
|
7
7
|
var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
8
|
+
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
9
|
+
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
8
10
|
var READY_EVENT = "ready";
|
|
9
11
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
10
12
|
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
@@ -19,6 +21,7 @@ var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
|
19
21
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
20
22
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
21
23
|
var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
|
|
24
|
+
var DEFAULT_RPC_TIMEOUT_MS = 5e3;
|
|
22
25
|
function normalizeToError(error, fallbackMessage) {
|
|
23
26
|
if (error instanceof Error) {
|
|
24
27
|
return error;
|
|
@@ -81,7 +84,88 @@ function decodeCloseReason(reason) {
|
|
|
81
84
|
return reason.toString("utf8");
|
|
82
85
|
}
|
|
83
86
|
function isReservedEmitEvent(event) {
|
|
84
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === READY_EVENT;
|
|
87
|
+
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
88
|
+
}
|
|
89
|
+
function isPromiseLike(value) {
|
|
90
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
91
|
+
}
|
|
92
|
+
function normalizeRpcTimeout(timeoutMs) {
|
|
93
|
+
const resolvedTimeoutMs = timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
|
|
94
|
+
if (!Number.isFinite(resolvedTimeoutMs) || resolvedTimeoutMs <= 0) {
|
|
95
|
+
throw new Error("ACK timeoutMs must be a positive number.");
|
|
96
|
+
}
|
|
97
|
+
return resolvedTimeoutMs;
|
|
98
|
+
}
|
|
99
|
+
function parseRpcRequestPayload(data) {
|
|
100
|
+
if (typeof data !== "object" || data === null) {
|
|
101
|
+
throw new Error("Invalid RPC request payload format.");
|
|
102
|
+
}
|
|
103
|
+
const payload = data;
|
|
104
|
+
if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
|
|
105
|
+
throw new Error("RPC request payload must include a non-empty id.");
|
|
106
|
+
}
|
|
107
|
+
if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
|
|
108
|
+
throw new Error("RPC request payload must include a non-empty event.");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id: payload.id,
|
|
112
|
+
event: payload.event,
|
|
113
|
+
data: payload.data
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseRpcResponsePayload(data) {
|
|
117
|
+
if (typeof data !== "object" || data === null) {
|
|
118
|
+
throw new Error("Invalid RPC response payload format.");
|
|
119
|
+
}
|
|
120
|
+
const payload = data;
|
|
121
|
+
if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
|
|
122
|
+
throw new Error("RPC response payload must include a non-empty id.");
|
|
123
|
+
}
|
|
124
|
+
if (typeof payload.ok !== "boolean") {
|
|
125
|
+
throw new Error("RPC response payload must include a boolean ok field.");
|
|
126
|
+
}
|
|
127
|
+
if (payload.error !== void 0 && typeof payload.error !== "string") {
|
|
128
|
+
throw new Error("RPC response payload error must be a string when provided.");
|
|
129
|
+
}
|
|
130
|
+
const parsedPayload = {
|
|
131
|
+
id: payload.id,
|
|
132
|
+
ok: payload.ok,
|
|
133
|
+
data: payload.data
|
|
134
|
+
};
|
|
135
|
+
if (payload.error !== void 0) {
|
|
136
|
+
parsedPayload.error = payload.error;
|
|
137
|
+
}
|
|
138
|
+
return parsedPayload;
|
|
139
|
+
}
|
|
140
|
+
function resolveAckArguments(callbackOrOptions, maybeCallback) {
|
|
141
|
+
if (callbackOrOptions === void 0 && maybeCallback === void 0) {
|
|
142
|
+
return {
|
|
143
|
+
expectsAck: false,
|
|
144
|
+
timeoutMs: DEFAULT_RPC_TIMEOUT_MS
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (typeof callbackOrOptions === "function") {
|
|
148
|
+
if (maybeCallback !== void 0) {
|
|
149
|
+
throw new Error("ACK callback was provided more than once.");
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
expectsAck: true,
|
|
153
|
+
callback: callbackOrOptions,
|
|
154
|
+
timeoutMs: DEFAULT_RPC_TIMEOUT_MS
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const options = callbackOrOptions;
|
|
158
|
+
if (options !== void 0 && (typeof options !== "object" || options === null)) {
|
|
159
|
+
throw new Error("ACK options must be an object.");
|
|
160
|
+
}
|
|
161
|
+
if (maybeCallback !== void 0 && typeof maybeCallback !== "function") {
|
|
162
|
+
throw new Error("ACK callback must be a function.");
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...maybeCallback ? { callback: maybeCallback } : {},
|
|
166
|
+
expectsAck: true,
|
|
167
|
+
timeoutMs: normalizeRpcTimeout(options?.timeoutMs)
|
|
168
|
+
};
|
|
85
169
|
}
|
|
86
170
|
function createEphemeralHandshakeState() {
|
|
87
171
|
const ecdh = createECDH(HANDSHAKE_CURVE);
|
|
@@ -179,6 +263,7 @@ var SecureServer = class {
|
|
|
179
263
|
sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
|
|
180
264
|
encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
|
|
181
265
|
pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
|
|
266
|
+
pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
|
|
182
267
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
183
268
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
184
269
|
roomNamesByClientId = /* @__PURE__ */ new Map();
|
|
@@ -277,7 +362,8 @@ var SecureServer = class {
|
|
|
277
362
|
}
|
|
278
363
|
return this;
|
|
279
364
|
}
|
|
280
|
-
emitTo(clientId, event, data) {
|
|
365
|
+
emitTo(clientId, event, data, callbackOrOptions, maybeCallback) {
|
|
366
|
+
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
281
367
|
try {
|
|
282
368
|
if (isReservedEmitEvent(event)) {
|
|
283
369
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
@@ -286,10 +372,37 @@ var SecureServer = class {
|
|
|
286
372
|
if (!client) {
|
|
287
373
|
throw new Error(`Client with id ${clientId} was not found.`);
|
|
288
374
|
}
|
|
289
|
-
|
|
290
|
-
|
|
375
|
+
if (!ackArgs.expectsAck) {
|
|
376
|
+
this.sendOrQueuePayload(client.socket, { event, data });
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
const ackPromise = this.sendRpcRequest(
|
|
380
|
+
client.socket,
|
|
381
|
+
event,
|
|
382
|
+
data,
|
|
383
|
+
ackArgs.timeoutMs
|
|
384
|
+
);
|
|
385
|
+
if (ackArgs.callback) {
|
|
386
|
+
ackPromise.then((response) => {
|
|
387
|
+
ackArgs.callback?.(null, response);
|
|
388
|
+
}).catch((error) => {
|
|
389
|
+
ackArgs.callback?.(
|
|
390
|
+
normalizeToError(error, `ACK callback failed for client ${client.id}.`)
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
return ackPromise;
|
|
291
396
|
} catch (error) {
|
|
292
|
-
|
|
397
|
+
const normalizedError = normalizeToError(error, "Failed to emit event to client.");
|
|
398
|
+
this.notifyError(normalizedError);
|
|
399
|
+
if (ackArgs.callback) {
|
|
400
|
+
ackArgs.callback(normalizedError);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (ackArgs.expectsAck) {
|
|
404
|
+
return Promise.reject(normalizedError);
|
|
405
|
+
}
|
|
293
406
|
return false;
|
|
294
407
|
}
|
|
295
408
|
}
|
|
@@ -312,6 +425,10 @@ var SecureServer = class {
|
|
|
312
425
|
try {
|
|
313
426
|
this.stopHeartbeatLoop();
|
|
314
427
|
for (const client of this.clientsById.values()) {
|
|
428
|
+
this.rejectPendingRpcRequests(
|
|
429
|
+
client.socket,
|
|
430
|
+
new Error("Server closed before ACK response was received.")
|
|
431
|
+
);
|
|
315
432
|
if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
|
|
316
433
|
client.socket.close(code, reason);
|
|
317
434
|
}
|
|
@@ -364,9 +481,14 @@ var SecureServer = class {
|
|
|
364
481
|
lastPingAt: 0
|
|
365
482
|
};
|
|
366
483
|
if (heartbeatState.awaitingPong && now - heartbeatState.lastPingAt >= this.heartbeatConfig.timeoutMs) {
|
|
484
|
+
this.rejectPendingRpcRequests(
|
|
485
|
+
socket,
|
|
486
|
+
new Error(`Heartbeat timeout while waiting for client ${client.id} ACK response.`)
|
|
487
|
+
);
|
|
367
488
|
this.sharedSecretBySocket.delete(socket);
|
|
368
489
|
this.encryptionKeyBySocket.delete(socket);
|
|
369
490
|
this.pendingPayloadsBySocket.delete(socket);
|
|
491
|
+
this.pendingRpcRequestsBySocket.delete(socket);
|
|
370
492
|
this.handshakeStateBySocket.delete(socket);
|
|
371
493
|
this.heartbeatStateBySocket.delete(socket);
|
|
372
494
|
socket.terminate();
|
|
@@ -413,6 +535,7 @@ var SecureServer = class {
|
|
|
413
535
|
this.clientIdBySocket.set(socket, clientId);
|
|
414
536
|
this.handshakeStateBySocket.set(socket, handshakeState);
|
|
415
537
|
this.pendingPayloadsBySocket.set(socket, []);
|
|
538
|
+
this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
|
|
416
539
|
this.heartbeatStateBySocket.set(socket, {
|
|
417
540
|
awaitingPong: false,
|
|
418
541
|
lastPingAt: 0
|
|
@@ -480,6 +603,14 @@ var SecureServer = class {
|
|
|
480
603
|
return;
|
|
481
604
|
}
|
|
482
605
|
const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
|
|
606
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
|
|
607
|
+
this.handleRpcResponse(client.socket, decryptedEnvelope.data);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_REQUEST_EVENT) {
|
|
611
|
+
void this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
483
614
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data, client);
|
|
484
615
|
} catch (error) {
|
|
485
616
|
this.notifyError(normalizeToError(error, "Failed to process incoming server message."));
|
|
@@ -494,6 +625,11 @@ var SecureServer = class {
|
|
|
494
625
|
this.sharedSecretBySocket.delete(client.socket);
|
|
495
626
|
this.encryptionKeyBySocket.delete(client.socket);
|
|
496
627
|
this.pendingPayloadsBySocket.delete(client.socket);
|
|
628
|
+
this.rejectPendingRpcRequests(
|
|
629
|
+
client.socket,
|
|
630
|
+
new Error(`Client ${client.id} disconnected before ACK response was received.`)
|
|
631
|
+
);
|
|
632
|
+
this.pendingRpcRequestsBySocket.delete(client.socket);
|
|
497
633
|
this.heartbeatStateBySocket.delete(client.socket);
|
|
498
634
|
const decodedReason = decodeCloseReason(reason);
|
|
499
635
|
for (const handler of this.disconnectHandlers) {
|
|
@@ -519,7 +655,17 @@ var SecureServer = class {
|
|
|
519
655
|
}
|
|
520
656
|
for (const handler of handlers) {
|
|
521
657
|
try {
|
|
522
|
-
handler(data, client);
|
|
658
|
+
const handlerResult = handler(data, client);
|
|
659
|
+
if (isPromiseLike(handlerResult)) {
|
|
660
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
661
|
+
this.notifyError(
|
|
662
|
+
normalizeToError(
|
|
663
|
+
error,
|
|
664
|
+
`Server event handler failed for event ${event}.`
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
523
669
|
} catch (error) {
|
|
524
670
|
this.notifyError(
|
|
525
671
|
normalizeToError(
|
|
@@ -558,6 +704,112 @@ var SecureServer = class {
|
|
|
558
704
|
this.notifyError(normalizeToError(error, "Failed to send encrypted server payload."));
|
|
559
705
|
}
|
|
560
706
|
}
|
|
707
|
+
sendRpcRequest(socket, event, data, timeoutMs) {
|
|
708
|
+
if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
|
|
709
|
+
throw new Error("Client socket is not connected for ACK request.");
|
|
710
|
+
}
|
|
711
|
+
const pendingRequests = this.pendingRpcRequestsBySocket.get(socket) ?? /* @__PURE__ */ new Map();
|
|
712
|
+
this.pendingRpcRequestsBySocket.set(socket, pendingRequests);
|
|
713
|
+
const requestId = randomUUID();
|
|
714
|
+
return new Promise((resolve, reject) => {
|
|
715
|
+
const timeoutHandle = setTimeout(() => {
|
|
716
|
+
pendingRequests.delete(requestId);
|
|
717
|
+
reject(new Error(`ACK response timed out after ${timeoutMs}ms for event "${event}".`));
|
|
718
|
+
}, timeoutMs);
|
|
719
|
+
timeoutHandle.unref?.();
|
|
720
|
+
pendingRequests.set(requestId, {
|
|
721
|
+
resolve,
|
|
722
|
+
reject,
|
|
723
|
+
timeoutHandle
|
|
724
|
+
});
|
|
725
|
+
this.sendOrQueuePayload(socket, {
|
|
726
|
+
event: INTERNAL_RPC_REQUEST_EVENT,
|
|
727
|
+
data: {
|
|
728
|
+
id: requestId,
|
|
729
|
+
event,
|
|
730
|
+
data
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
handleRpcResponse(socket, data) {
|
|
736
|
+
try {
|
|
737
|
+
const responsePayload = parseRpcResponsePayload(data);
|
|
738
|
+
const pendingRequests = this.pendingRpcRequestsBySocket.get(socket);
|
|
739
|
+
if (!pendingRequests) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const pendingRequest = pendingRequests.get(responsePayload.id);
|
|
743
|
+
if (!pendingRequest) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
747
|
+
pendingRequests.delete(responsePayload.id);
|
|
748
|
+
if (responsePayload.ok) {
|
|
749
|
+
pendingRequest.resolve(responsePayload.data);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
pendingRequest.reject(
|
|
753
|
+
new Error(responsePayload.error ?? "ACK request failed without an error message.")
|
|
754
|
+
);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
this.notifyError(normalizeToError(error, "Failed to process server ACK response."));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async handleRpcRequest(client, data) {
|
|
760
|
+
let rpcRequestPayload;
|
|
761
|
+
try {
|
|
762
|
+
rpcRequestPayload = parseRpcRequestPayload(data);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
this.notifyError(normalizeToError(error, "Invalid server ACK request payload."));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
const ackResponse = await this.executeRpcRequestHandler(
|
|
769
|
+
rpcRequestPayload.event,
|
|
770
|
+
rpcRequestPayload.data,
|
|
771
|
+
client
|
|
772
|
+
);
|
|
773
|
+
this.sendEncryptedEnvelope(client.socket, {
|
|
774
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
775
|
+
data: {
|
|
776
|
+
id: rpcRequestPayload.id,
|
|
777
|
+
ok: true,
|
|
778
|
+
data: ackResponse
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
} catch (error) {
|
|
782
|
+
const normalizedError = normalizeToError(error, "Server ACK request handler failed.");
|
|
783
|
+
this.sendEncryptedEnvelope(client.socket, {
|
|
784
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
785
|
+
data: {
|
|
786
|
+
id: rpcRequestPayload.id,
|
|
787
|
+
ok: false,
|
|
788
|
+
error: normalizedError.message
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
this.notifyError(normalizedError);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async executeRpcRequestHandler(event, data, client) {
|
|
795
|
+
const handlers = this.customEventHandlers.get(event);
|
|
796
|
+
if (!handlers || handlers.size === 0) {
|
|
797
|
+
throw new Error(`No handler is registered for ACK request event "${event}".`);
|
|
798
|
+
}
|
|
799
|
+
const firstHandler = handlers.values().next().value;
|
|
800
|
+
return Promise.resolve(firstHandler(data, client));
|
|
801
|
+
}
|
|
802
|
+
rejectPendingRpcRequests(socket, error) {
|
|
803
|
+
const pendingRequests = this.pendingRpcRequestsBySocket.get(socket);
|
|
804
|
+
if (!pendingRequests) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
for (const pendingRequest of pendingRequests.values()) {
|
|
808
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
809
|
+
pendingRequest.reject(error);
|
|
810
|
+
}
|
|
811
|
+
pendingRequests.clear();
|
|
812
|
+
}
|
|
561
813
|
notifyConnection(client) {
|
|
562
814
|
for (const handler of this.connectionHandlers) {
|
|
563
815
|
try {
|
|
@@ -659,6 +911,24 @@ var SecureServer = class {
|
|
|
659
911
|
id: clientId,
|
|
660
912
|
socket,
|
|
661
913
|
request,
|
|
914
|
+
emit: (event, data, callbackOrOptions, maybeCallback) => {
|
|
915
|
+
if (callbackOrOptions === void 0 && maybeCallback === void 0) {
|
|
916
|
+
return this.emitTo(clientId, event, data);
|
|
917
|
+
}
|
|
918
|
+
if (typeof callbackOrOptions === "function") {
|
|
919
|
+
return this.emitTo(clientId, event, data, callbackOrOptions);
|
|
920
|
+
}
|
|
921
|
+
if (maybeCallback) {
|
|
922
|
+
return this.emitTo(
|
|
923
|
+
clientId,
|
|
924
|
+
event,
|
|
925
|
+
data,
|
|
926
|
+
callbackOrOptions ?? {},
|
|
927
|
+
maybeCallback
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
|
|
931
|
+
},
|
|
662
932
|
join: (room) => this.joinClientToRoom(clientId, room),
|
|
663
933
|
leave: (room) => this.leaveClientFromRoom(clientId, room),
|
|
664
934
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
@@ -770,6 +1040,7 @@ var SecureClient = class {
|
|
|
770
1040
|
errorHandlers = /* @__PURE__ */ new Set();
|
|
771
1041
|
handshakeState = null;
|
|
772
1042
|
pendingPayloadQueue = [];
|
|
1043
|
+
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
773
1044
|
get readyState() {
|
|
774
1045
|
return this.socket?.readyState ?? null;
|
|
775
1046
|
}
|
|
@@ -878,7 +1149,8 @@ var SecureClient = class {
|
|
|
878
1149
|
}
|
|
879
1150
|
return this;
|
|
880
1151
|
}
|
|
881
|
-
emit(event, data) {
|
|
1152
|
+
emit(event, data, callbackOrOptions, maybeCallback) {
|
|
1153
|
+
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
882
1154
|
try {
|
|
883
1155
|
if (isReservedEmitEvent(event)) {
|
|
884
1156
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
@@ -886,6 +1158,20 @@ var SecureClient = class {
|
|
|
886
1158
|
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
887
1159
|
throw new Error("Client socket is not connected.");
|
|
888
1160
|
}
|
|
1161
|
+
if (ackArgs.expectsAck) {
|
|
1162
|
+
const ackPromise = this.sendRpcRequest(event, data, ackArgs.timeoutMs);
|
|
1163
|
+
if (ackArgs.callback) {
|
|
1164
|
+
ackPromise.then((response) => {
|
|
1165
|
+
ackArgs.callback?.(null, response);
|
|
1166
|
+
}).catch((error) => {
|
|
1167
|
+
ackArgs.callback?.(
|
|
1168
|
+
normalizeToError(error, `ACK callback failed for event "${event}".`)
|
|
1169
|
+
);
|
|
1170
|
+
});
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
return ackPromise;
|
|
1174
|
+
}
|
|
889
1175
|
const envelope = { event, data };
|
|
890
1176
|
if (!this.isHandshakeReady()) {
|
|
891
1177
|
this.pendingPayloadQueue.push(envelope);
|
|
@@ -894,7 +1180,15 @@ var SecureClient = class {
|
|
|
894
1180
|
this.sendEncryptedEnvelope(envelope);
|
|
895
1181
|
return true;
|
|
896
1182
|
} catch (error) {
|
|
897
|
-
|
|
1183
|
+
const normalizedError = normalizeToError(error, "Failed to emit client event.");
|
|
1184
|
+
this.notifyError(normalizedError);
|
|
1185
|
+
if (ackArgs.callback) {
|
|
1186
|
+
ackArgs.callback(normalizedError);
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
if (ackArgs.expectsAck) {
|
|
1190
|
+
return Promise.reject(normalizedError);
|
|
1191
|
+
}
|
|
898
1192
|
return false;
|
|
899
1193
|
}
|
|
900
1194
|
}
|
|
@@ -1036,6 +1330,14 @@ var SecureClient = class {
|
|
|
1036
1330
|
return;
|
|
1037
1331
|
}
|
|
1038
1332
|
const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
|
|
1333
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
|
|
1334
|
+
this.handleRpcResponse(decryptedEnvelope.data);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_REQUEST_EVENT) {
|
|
1338
|
+
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1039
1341
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data);
|
|
1040
1342
|
} catch (error) {
|
|
1041
1343
|
this.notifyError(normalizeToError(error, "Failed to process incoming client message."));
|
|
@@ -1046,6 +1348,9 @@ var SecureClient = class {
|
|
|
1046
1348
|
this.socket = null;
|
|
1047
1349
|
this.handshakeState = null;
|
|
1048
1350
|
this.pendingPayloadQueue = [];
|
|
1351
|
+
this.rejectPendingRpcRequests(
|
|
1352
|
+
new Error("Client disconnected before ACK response was received.")
|
|
1353
|
+
);
|
|
1049
1354
|
const decodedReason = decodeCloseReason(reason);
|
|
1050
1355
|
for (const handler of this.disconnectHandlers) {
|
|
1051
1356
|
try {
|
|
@@ -1071,7 +1376,17 @@ var SecureClient = class {
|
|
|
1071
1376
|
}
|
|
1072
1377
|
for (const handler of handlers) {
|
|
1073
1378
|
try {
|
|
1074
|
-
handler(data);
|
|
1379
|
+
const handlerResult = handler(data);
|
|
1380
|
+
if (isPromiseLike(handlerResult)) {
|
|
1381
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
1382
|
+
this.notifyError(
|
|
1383
|
+
normalizeToError(
|
|
1384
|
+
error,
|
|
1385
|
+
`Client event handler failed for event ${event}.`
|
|
1386
|
+
)
|
|
1387
|
+
);
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1075
1390
|
} catch (error) {
|
|
1076
1391
|
this.notifyError(
|
|
1077
1392
|
normalizeToError(error, `Client event handler failed for event ${event}.`)
|
|
@@ -1126,6 +1441,106 @@ var SecureClient = class {
|
|
|
1126
1441
|
this.notifyError(normalizeToError(error, "Failed to send encrypted client payload."));
|
|
1127
1442
|
}
|
|
1128
1443
|
}
|
|
1444
|
+
sendRpcRequest(event, data, timeoutMs) {
|
|
1445
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1446
|
+
throw new Error("Client socket is not connected for ACK request.");
|
|
1447
|
+
}
|
|
1448
|
+
const requestId = randomUUID();
|
|
1449
|
+
return new Promise((resolve, reject) => {
|
|
1450
|
+
const timeoutHandle = setTimeout(() => {
|
|
1451
|
+
this.pendingRpcRequests.delete(requestId);
|
|
1452
|
+
reject(new Error(`ACK response timed out after ${timeoutMs}ms for event "${event}".`));
|
|
1453
|
+
}, timeoutMs);
|
|
1454
|
+
timeoutHandle.unref?.();
|
|
1455
|
+
this.pendingRpcRequests.set(requestId, {
|
|
1456
|
+
resolve,
|
|
1457
|
+
reject,
|
|
1458
|
+
timeoutHandle
|
|
1459
|
+
});
|
|
1460
|
+
const rpcRequestEnvelope = {
|
|
1461
|
+
event: INTERNAL_RPC_REQUEST_EVENT,
|
|
1462
|
+
data: {
|
|
1463
|
+
id: requestId,
|
|
1464
|
+
event,
|
|
1465
|
+
data
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
if (!this.isHandshakeReady()) {
|
|
1469
|
+
this.pendingPayloadQueue.push(rpcRequestEnvelope);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
this.sendEncryptedEnvelope(rpcRequestEnvelope);
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
handleRpcResponse(data) {
|
|
1476
|
+
try {
|
|
1477
|
+
const responsePayload = parseRpcResponsePayload(data);
|
|
1478
|
+
const pendingRequest = this.pendingRpcRequests.get(responsePayload.id);
|
|
1479
|
+
if (!pendingRequest) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
1483
|
+
this.pendingRpcRequests.delete(responsePayload.id);
|
|
1484
|
+
if (responsePayload.ok) {
|
|
1485
|
+
pendingRequest.resolve(responsePayload.data);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
pendingRequest.reject(
|
|
1489
|
+
new Error(responsePayload.error ?? "ACK request failed without an error message.")
|
|
1490
|
+
);
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
this.notifyError(normalizeToError(error, "Failed to process client ACK response."));
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
async handleRpcRequest(data) {
|
|
1496
|
+
let rpcRequestPayload;
|
|
1497
|
+
try {
|
|
1498
|
+
rpcRequestPayload = parseRpcRequestPayload(data);
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
this.notifyError(normalizeToError(error, "Invalid client ACK request payload."));
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
try {
|
|
1504
|
+
const ackResponse = await this.executeRpcRequestHandler(
|
|
1505
|
+
rpcRequestPayload.event,
|
|
1506
|
+
rpcRequestPayload.data
|
|
1507
|
+
);
|
|
1508
|
+
this.sendEncryptedEnvelope({
|
|
1509
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
1510
|
+
data: {
|
|
1511
|
+
id: rpcRequestPayload.id,
|
|
1512
|
+
ok: true,
|
|
1513
|
+
data: ackResponse
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
const normalizedError = normalizeToError(error, "Client ACK request handler failed.");
|
|
1518
|
+
this.sendEncryptedEnvelope({
|
|
1519
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
1520
|
+
data: {
|
|
1521
|
+
id: rpcRequestPayload.id,
|
|
1522
|
+
ok: false,
|
|
1523
|
+
error: normalizedError.message
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
this.notifyError(normalizedError);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
async executeRpcRequestHandler(event, data) {
|
|
1530
|
+
const handlers = this.customEventHandlers.get(event);
|
|
1531
|
+
if (!handlers || handlers.size === 0) {
|
|
1532
|
+
throw new Error(`No handler is registered for ACK request event "${event}".`);
|
|
1533
|
+
}
|
|
1534
|
+
const firstHandler = handlers.values().next().value;
|
|
1535
|
+
return Promise.resolve(firstHandler(data));
|
|
1536
|
+
}
|
|
1537
|
+
rejectPendingRpcRequests(error) {
|
|
1538
|
+
for (const pendingRequest of this.pendingRpcRequests.values()) {
|
|
1539
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
1540
|
+
pendingRequest.reject(error);
|
|
1541
|
+
}
|
|
1542
|
+
this.pendingRpcRequests.clear();
|
|
1543
|
+
}
|
|
1129
1544
|
createClientHandshakeState() {
|
|
1130
1545
|
const { ecdh, localPublicKey } = createEphemeralHandshakeState();
|
|
1131
1546
|
return {
|