@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.cjs
CHANGED
|
@@ -11,6 +11,8 @@ var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
|
11
11
|
var DEFAULT_CLOSE_CODE = 1e3;
|
|
12
12
|
var DEFAULT_CLOSE_REASON = "";
|
|
13
13
|
var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
14
|
+
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
15
|
+
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
14
16
|
var READY_EVENT = "ready";
|
|
15
17
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
16
18
|
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
@@ -25,6 +27,7 @@ var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
|
25
27
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
26
28
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
27
29
|
var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
|
|
30
|
+
var DEFAULT_RPC_TIMEOUT_MS = 5e3;
|
|
28
31
|
function normalizeToError(error, fallbackMessage) {
|
|
29
32
|
if (error instanceof Error) {
|
|
30
33
|
return error;
|
|
@@ -87,7 +90,88 @@ function decodeCloseReason(reason) {
|
|
|
87
90
|
return reason.toString("utf8");
|
|
88
91
|
}
|
|
89
92
|
function isReservedEmitEvent(event) {
|
|
90
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === READY_EVENT;
|
|
93
|
+
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
94
|
+
}
|
|
95
|
+
function isPromiseLike(value) {
|
|
96
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
97
|
+
}
|
|
98
|
+
function normalizeRpcTimeout(timeoutMs) {
|
|
99
|
+
const resolvedTimeoutMs = timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
|
|
100
|
+
if (!Number.isFinite(resolvedTimeoutMs) || resolvedTimeoutMs <= 0) {
|
|
101
|
+
throw new Error("ACK timeoutMs must be a positive number.");
|
|
102
|
+
}
|
|
103
|
+
return resolvedTimeoutMs;
|
|
104
|
+
}
|
|
105
|
+
function parseRpcRequestPayload(data) {
|
|
106
|
+
if (typeof data !== "object" || data === null) {
|
|
107
|
+
throw new Error("Invalid RPC request payload format.");
|
|
108
|
+
}
|
|
109
|
+
const payload = data;
|
|
110
|
+
if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
|
|
111
|
+
throw new Error("RPC request payload must include a non-empty id.");
|
|
112
|
+
}
|
|
113
|
+
if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
|
|
114
|
+
throw new Error("RPC request payload must include a non-empty event.");
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: payload.id,
|
|
118
|
+
event: payload.event,
|
|
119
|
+
data: payload.data
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parseRpcResponsePayload(data) {
|
|
123
|
+
if (typeof data !== "object" || data === null) {
|
|
124
|
+
throw new Error("Invalid RPC response payload format.");
|
|
125
|
+
}
|
|
126
|
+
const payload = data;
|
|
127
|
+
if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
|
|
128
|
+
throw new Error("RPC response payload must include a non-empty id.");
|
|
129
|
+
}
|
|
130
|
+
if (typeof payload.ok !== "boolean") {
|
|
131
|
+
throw new Error("RPC response payload must include a boolean ok field.");
|
|
132
|
+
}
|
|
133
|
+
if (payload.error !== void 0 && typeof payload.error !== "string") {
|
|
134
|
+
throw new Error("RPC response payload error must be a string when provided.");
|
|
135
|
+
}
|
|
136
|
+
const parsedPayload = {
|
|
137
|
+
id: payload.id,
|
|
138
|
+
ok: payload.ok,
|
|
139
|
+
data: payload.data
|
|
140
|
+
};
|
|
141
|
+
if (payload.error !== void 0) {
|
|
142
|
+
parsedPayload.error = payload.error;
|
|
143
|
+
}
|
|
144
|
+
return parsedPayload;
|
|
145
|
+
}
|
|
146
|
+
function resolveAckArguments(callbackOrOptions, maybeCallback) {
|
|
147
|
+
if (callbackOrOptions === void 0 && maybeCallback === void 0) {
|
|
148
|
+
return {
|
|
149
|
+
expectsAck: false,
|
|
150
|
+
timeoutMs: DEFAULT_RPC_TIMEOUT_MS
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (typeof callbackOrOptions === "function") {
|
|
154
|
+
if (maybeCallback !== void 0) {
|
|
155
|
+
throw new Error("ACK callback was provided more than once.");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
expectsAck: true,
|
|
159
|
+
callback: callbackOrOptions,
|
|
160
|
+
timeoutMs: DEFAULT_RPC_TIMEOUT_MS
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const options = callbackOrOptions;
|
|
164
|
+
if (options !== void 0 && (typeof options !== "object" || options === null)) {
|
|
165
|
+
throw new Error("ACK options must be an object.");
|
|
166
|
+
}
|
|
167
|
+
if (maybeCallback !== void 0 && typeof maybeCallback !== "function") {
|
|
168
|
+
throw new Error("ACK callback must be a function.");
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
...maybeCallback ? { callback: maybeCallback } : {},
|
|
172
|
+
expectsAck: true,
|
|
173
|
+
timeoutMs: normalizeRpcTimeout(options?.timeoutMs)
|
|
174
|
+
};
|
|
91
175
|
}
|
|
92
176
|
function createEphemeralHandshakeState() {
|
|
93
177
|
const ecdh = crypto.createECDH(HANDSHAKE_CURVE);
|
|
@@ -185,6 +269,7 @@ var SecureServer = class {
|
|
|
185
269
|
sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
|
|
186
270
|
encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
|
|
187
271
|
pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
|
|
272
|
+
pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
|
|
188
273
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
189
274
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
190
275
|
roomNamesByClientId = /* @__PURE__ */ new Map();
|
|
@@ -283,7 +368,8 @@ var SecureServer = class {
|
|
|
283
368
|
}
|
|
284
369
|
return this;
|
|
285
370
|
}
|
|
286
|
-
emitTo(clientId, event, data) {
|
|
371
|
+
emitTo(clientId, event, data, callbackOrOptions, maybeCallback) {
|
|
372
|
+
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
287
373
|
try {
|
|
288
374
|
if (isReservedEmitEvent(event)) {
|
|
289
375
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
@@ -292,10 +378,37 @@ var SecureServer = class {
|
|
|
292
378
|
if (!client) {
|
|
293
379
|
throw new Error(`Client with id ${clientId} was not found.`);
|
|
294
380
|
}
|
|
295
|
-
|
|
296
|
-
|
|
381
|
+
if (!ackArgs.expectsAck) {
|
|
382
|
+
this.sendOrQueuePayload(client.socket, { event, data });
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
const ackPromise = this.sendRpcRequest(
|
|
386
|
+
client.socket,
|
|
387
|
+
event,
|
|
388
|
+
data,
|
|
389
|
+
ackArgs.timeoutMs
|
|
390
|
+
);
|
|
391
|
+
if (ackArgs.callback) {
|
|
392
|
+
ackPromise.then((response) => {
|
|
393
|
+
ackArgs.callback?.(null, response);
|
|
394
|
+
}).catch((error) => {
|
|
395
|
+
ackArgs.callback?.(
|
|
396
|
+
normalizeToError(error, `ACK callback failed for client ${client.id}.`)
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
return ackPromise;
|
|
297
402
|
} catch (error) {
|
|
298
|
-
|
|
403
|
+
const normalizedError = normalizeToError(error, "Failed to emit event to client.");
|
|
404
|
+
this.notifyError(normalizedError);
|
|
405
|
+
if (ackArgs.callback) {
|
|
406
|
+
ackArgs.callback(normalizedError);
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
if (ackArgs.expectsAck) {
|
|
410
|
+
return Promise.reject(normalizedError);
|
|
411
|
+
}
|
|
299
412
|
return false;
|
|
300
413
|
}
|
|
301
414
|
}
|
|
@@ -318,6 +431,10 @@ var SecureServer = class {
|
|
|
318
431
|
try {
|
|
319
432
|
this.stopHeartbeatLoop();
|
|
320
433
|
for (const client of this.clientsById.values()) {
|
|
434
|
+
this.rejectPendingRpcRequests(
|
|
435
|
+
client.socket,
|
|
436
|
+
new Error("Server closed before ACK response was received.")
|
|
437
|
+
);
|
|
321
438
|
if (client.socket.readyState === WebSocket__default.default.OPEN || client.socket.readyState === WebSocket__default.default.CONNECTING) {
|
|
322
439
|
client.socket.close(code, reason);
|
|
323
440
|
}
|
|
@@ -370,9 +487,14 @@ var SecureServer = class {
|
|
|
370
487
|
lastPingAt: 0
|
|
371
488
|
};
|
|
372
489
|
if (heartbeatState.awaitingPong && now - heartbeatState.lastPingAt >= this.heartbeatConfig.timeoutMs) {
|
|
490
|
+
this.rejectPendingRpcRequests(
|
|
491
|
+
socket,
|
|
492
|
+
new Error(`Heartbeat timeout while waiting for client ${client.id} ACK response.`)
|
|
493
|
+
);
|
|
373
494
|
this.sharedSecretBySocket.delete(socket);
|
|
374
495
|
this.encryptionKeyBySocket.delete(socket);
|
|
375
496
|
this.pendingPayloadsBySocket.delete(socket);
|
|
497
|
+
this.pendingRpcRequestsBySocket.delete(socket);
|
|
376
498
|
this.handshakeStateBySocket.delete(socket);
|
|
377
499
|
this.heartbeatStateBySocket.delete(socket);
|
|
378
500
|
socket.terminate();
|
|
@@ -419,6 +541,7 @@ var SecureServer = class {
|
|
|
419
541
|
this.clientIdBySocket.set(socket, clientId);
|
|
420
542
|
this.handshakeStateBySocket.set(socket, handshakeState);
|
|
421
543
|
this.pendingPayloadsBySocket.set(socket, []);
|
|
544
|
+
this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
|
|
422
545
|
this.heartbeatStateBySocket.set(socket, {
|
|
423
546
|
awaitingPong: false,
|
|
424
547
|
lastPingAt: 0
|
|
@@ -486,6 +609,14 @@ var SecureServer = class {
|
|
|
486
609
|
return;
|
|
487
610
|
}
|
|
488
611
|
const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
|
|
612
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
|
|
613
|
+
this.handleRpcResponse(client.socket, decryptedEnvelope.data);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_REQUEST_EVENT) {
|
|
617
|
+
void this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
489
620
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data, client);
|
|
490
621
|
} catch (error) {
|
|
491
622
|
this.notifyError(normalizeToError(error, "Failed to process incoming server message."));
|
|
@@ -500,6 +631,11 @@ var SecureServer = class {
|
|
|
500
631
|
this.sharedSecretBySocket.delete(client.socket);
|
|
501
632
|
this.encryptionKeyBySocket.delete(client.socket);
|
|
502
633
|
this.pendingPayloadsBySocket.delete(client.socket);
|
|
634
|
+
this.rejectPendingRpcRequests(
|
|
635
|
+
client.socket,
|
|
636
|
+
new Error(`Client ${client.id} disconnected before ACK response was received.`)
|
|
637
|
+
);
|
|
638
|
+
this.pendingRpcRequestsBySocket.delete(client.socket);
|
|
503
639
|
this.heartbeatStateBySocket.delete(client.socket);
|
|
504
640
|
const decodedReason = decodeCloseReason(reason);
|
|
505
641
|
for (const handler of this.disconnectHandlers) {
|
|
@@ -525,7 +661,17 @@ var SecureServer = class {
|
|
|
525
661
|
}
|
|
526
662
|
for (const handler of handlers) {
|
|
527
663
|
try {
|
|
528
|
-
handler(data, client);
|
|
664
|
+
const handlerResult = handler(data, client);
|
|
665
|
+
if (isPromiseLike(handlerResult)) {
|
|
666
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
667
|
+
this.notifyError(
|
|
668
|
+
normalizeToError(
|
|
669
|
+
error,
|
|
670
|
+
`Server event handler failed for event ${event}.`
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
529
675
|
} catch (error) {
|
|
530
676
|
this.notifyError(
|
|
531
677
|
normalizeToError(
|
|
@@ -564,6 +710,112 @@ var SecureServer = class {
|
|
|
564
710
|
this.notifyError(normalizeToError(error, "Failed to send encrypted server payload."));
|
|
565
711
|
}
|
|
566
712
|
}
|
|
713
|
+
sendRpcRequest(socket, event, data, timeoutMs) {
|
|
714
|
+
if (socket.readyState !== WebSocket__default.default.OPEN && socket.readyState !== WebSocket__default.default.CONNECTING) {
|
|
715
|
+
throw new Error("Client socket is not connected for ACK request.");
|
|
716
|
+
}
|
|
717
|
+
const pendingRequests = this.pendingRpcRequestsBySocket.get(socket) ?? /* @__PURE__ */ new Map();
|
|
718
|
+
this.pendingRpcRequestsBySocket.set(socket, pendingRequests);
|
|
719
|
+
const requestId = crypto.randomUUID();
|
|
720
|
+
return new Promise((resolve, reject) => {
|
|
721
|
+
const timeoutHandle = setTimeout(() => {
|
|
722
|
+
pendingRequests.delete(requestId);
|
|
723
|
+
reject(new Error(`ACK response timed out after ${timeoutMs}ms for event "${event}".`));
|
|
724
|
+
}, timeoutMs);
|
|
725
|
+
timeoutHandle.unref?.();
|
|
726
|
+
pendingRequests.set(requestId, {
|
|
727
|
+
resolve,
|
|
728
|
+
reject,
|
|
729
|
+
timeoutHandle
|
|
730
|
+
});
|
|
731
|
+
this.sendOrQueuePayload(socket, {
|
|
732
|
+
event: INTERNAL_RPC_REQUEST_EVENT,
|
|
733
|
+
data: {
|
|
734
|
+
id: requestId,
|
|
735
|
+
event,
|
|
736
|
+
data
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
handleRpcResponse(socket, data) {
|
|
742
|
+
try {
|
|
743
|
+
const responsePayload = parseRpcResponsePayload(data);
|
|
744
|
+
const pendingRequests = this.pendingRpcRequestsBySocket.get(socket);
|
|
745
|
+
if (!pendingRequests) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const pendingRequest = pendingRequests.get(responsePayload.id);
|
|
749
|
+
if (!pendingRequest) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
753
|
+
pendingRequests.delete(responsePayload.id);
|
|
754
|
+
if (responsePayload.ok) {
|
|
755
|
+
pendingRequest.resolve(responsePayload.data);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
pendingRequest.reject(
|
|
759
|
+
new Error(responsePayload.error ?? "ACK request failed without an error message.")
|
|
760
|
+
);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
this.notifyError(normalizeToError(error, "Failed to process server ACK response."));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async handleRpcRequest(client, data) {
|
|
766
|
+
let rpcRequestPayload;
|
|
767
|
+
try {
|
|
768
|
+
rpcRequestPayload = parseRpcRequestPayload(data);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
this.notifyError(normalizeToError(error, "Invalid server ACK request payload."));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const ackResponse = await this.executeRpcRequestHandler(
|
|
775
|
+
rpcRequestPayload.event,
|
|
776
|
+
rpcRequestPayload.data,
|
|
777
|
+
client
|
|
778
|
+
);
|
|
779
|
+
this.sendEncryptedEnvelope(client.socket, {
|
|
780
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
781
|
+
data: {
|
|
782
|
+
id: rpcRequestPayload.id,
|
|
783
|
+
ok: true,
|
|
784
|
+
data: ackResponse
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
} catch (error) {
|
|
788
|
+
const normalizedError = normalizeToError(error, "Server ACK request handler failed.");
|
|
789
|
+
this.sendEncryptedEnvelope(client.socket, {
|
|
790
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
791
|
+
data: {
|
|
792
|
+
id: rpcRequestPayload.id,
|
|
793
|
+
ok: false,
|
|
794
|
+
error: normalizedError.message
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
this.notifyError(normalizedError);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async executeRpcRequestHandler(event, data, client) {
|
|
801
|
+
const handlers = this.customEventHandlers.get(event);
|
|
802
|
+
if (!handlers || handlers.size === 0) {
|
|
803
|
+
throw new Error(`No handler is registered for ACK request event "${event}".`);
|
|
804
|
+
}
|
|
805
|
+
const firstHandler = handlers.values().next().value;
|
|
806
|
+
return Promise.resolve(firstHandler(data, client));
|
|
807
|
+
}
|
|
808
|
+
rejectPendingRpcRequests(socket, error) {
|
|
809
|
+
const pendingRequests = this.pendingRpcRequestsBySocket.get(socket);
|
|
810
|
+
if (!pendingRequests) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
for (const pendingRequest of pendingRequests.values()) {
|
|
814
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
815
|
+
pendingRequest.reject(error);
|
|
816
|
+
}
|
|
817
|
+
pendingRequests.clear();
|
|
818
|
+
}
|
|
567
819
|
notifyConnection(client) {
|
|
568
820
|
for (const handler of this.connectionHandlers) {
|
|
569
821
|
try {
|
|
@@ -665,6 +917,24 @@ var SecureServer = class {
|
|
|
665
917
|
id: clientId,
|
|
666
918
|
socket,
|
|
667
919
|
request,
|
|
920
|
+
emit: (event, data, callbackOrOptions, maybeCallback) => {
|
|
921
|
+
if (callbackOrOptions === void 0 && maybeCallback === void 0) {
|
|
922
|
+
return this.emitTo(clientId, event, data);
|
|
923
|
+
}
|
|
924
|
+
if (typeof callbackOrOptions === "function") {
|
|
925
|
+
return this.emitTo(clientId, event, data, callbackOrOptions);
|
|
926
|
+
}
|
|
927
|
+
if (maybeCallback) {
|
|
928
|
+
return this.emitTo(
|
|
929
|
+
clientId,
|
|
930
|
+
event,
|
|
931
|
+
data,
|
|
932
|
+
callbackOrOptions ?? {},
|
|
933
|
+
maybeCallback
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
|
|
937
|
+
},
|
|
668
938
|
join: (room) => this.joinClientToRoom(clientId, room),
|
|
669
939
|
leave: (room) => this.leaveClientFromRoom(clientId, room),
|
|
670
940
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
@@ -776,6 +1046,7 @@ var SecureClient = class {
|
|
|
776
1046
|
errorHandlers = /* @__PURE__ */ new Set();
|
|
777
1047
|
handshakeState = null;
|
|
778
1048
|
pendingPayloadQueue = [];
|
|
1049
|
+
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
779
1050
|
get readyState() {
|
|
780
1051
|
return this.socket?.readyState ?? null;
|
|
781
1052
|
}
|
|
@@ -884,7 +1155,8 @@ var SecureClient = class {
|
|
|
884
1155
|
}
|
|
885
1156
|
return this;
|
|
886
1157
|
}
|
|
887
|
-
emit(event, data) {
|
|
1158
|
+
emit(event, data, callbackOrOptions, maybeCallback) {
|
|
1159
|
+
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
888
1160
|
try {
|
|
889
1161
|
if (isReservedEmitEvent(event)) {
|
|
890
1162
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
@@ -892,6 +1164,20 @@ var SecureClient = class {
|
|
|
892
1164
|
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
893
1165
|
throw new Error("Client socket is not connected.");
|
|
894
1166
|
}
|
|
1167
|
+
if (ackArgs.expectsAck) {
|
|
1168
|
+
const ackPromise = this.sendRpcRequest(event, data, ackArgs.timeoutMs);
|
|
1169
|
+
if (ackArgs.callback) {
|
|
1170
|
+
ackPromise.then((response) => {
|
|
1171
|
+
ackArgs.callback?.(null, response);
|
|
1172
|
+
}).catch((error) => {
|
|
1173
|
+
ackArgs.callback?.(
|
|
1174
|
+
normalizeToError(error, `ACK callback failed for event "${event}".`)
|
|
1175
|
+
);
|
|
1176
|
+
});
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
return ackPromise;
|
|
1180
|
+
}
|
|
895
1181
|
const envelope = { event, data };
|
|
896
1182
|
if (!this.isHandshakeReady()) {
|
|
897
1183
|
this.pendingPayloadQueue.push(envelope);
|
|
@@ -900,7 +1186,15 @@ var SecureClient = class {
|
|
|
900
1186
|
this.sendEncryptedEnvelope(envelope);
|
|
901
1187
|
return true;
|
|
902
1188
|
} catch (error) {
|
|
903
|
-
|
|
1189
|
+
const normalizedError = normalizeToError(error, "Failed to emit client event.");
|
|
1190
|
+
this.notifyError(normalizedError);
|
|
1191
|
+
if (ackArgs.callback) {
|
|
1192
|
+
ackArgs.callback(normalizedError);
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
if (ackArgs.expectsAck) {
|
|
1196
|
+
return Promise.reject(normalizedError);
|
|
1197
|
+
}
|
|
904
1198
|
return false;
|
|
905
1199
|
}
|
|
906
1200
|
}
|
|
@@ -1042,6 +1336,14 @@ var SecureClient = class {
|
|
|
1042
1336
|
return;
|
|
1043
1337
|
}
|
|
1044
1338
|
const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
|
|
1339
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
|
|
1340
|
+
this.handleRpcResponse(decryptedEnvelope.data);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (decryptedEnvelope.event === INTERNAL_RPC_REQUEST_EVENT) {
|
|
1344
|
+
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1045
1347
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data);
|
|
1046
1348
|
} catch (error) {
|
|
1047
1349
|
this.notifyError(normalizeToError(error, "Failed to process incoming client message."));
|
|
@@ -1052,6 +1354,9 @@ var SecureClient = class {
|
|
|
1052
1354
|
this.socket = null;
|
|
1053
1355
|
this.handshakeState = null;
|
|
1054
1356
|
this.pendingPayloadQueue = [];
|
|
1357
|
+
this.rejectPendingRpcRequests(
|
|
1358
|
+
new Error("Client disconnected before ACK response was received.")
|
|
1359
|
+
);
|
|
1055
1360
|
const decodedReason = decodeCloseReason(reason);
|
|
1056
1361
|
for (const handler of this.disconnectHandlers) {
|
|
1057
1362
|
try {
|
|
@@ -1077,7 +1382,17 @@ var SecureClient = class {
|
|
|
1077
1382
|
}
|
|
1078
1383
|
for (const handler of handlers) {
|
|
1079
1384
|
try {
|
|
1080
|
-
handler(data);
|
|
1385
|
+
const handlerResult = handler(data);
|
|
1386
|
+
if (isPromiseLike(handlerResult)) {
|
|
1387
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
1388
|
+
this.notifyError(
|
|
1389
|
+
normalizeToError(
|
|
1390
|
+
error,
|
|
1391
|
+
`Client event handler failed for event ${event}.`
|
|
1392
|
+
)
|
|
1393
|
+
);
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1081
1396
|
} catch (error) {
|
|
1082
1397
|
this.notifyError(
|
|
1083
1398
|
normalizeToError(error, `Client event handler failed for event ${event}.`)
|
|
@@ -1132,6 +1447,106 @@ var SecureClient = class {
|
|
|
1132
1447
|
this.notifyError(normalizeToError(error, "Failed to send encrypted client payload."));
|
|
1133
1448
|
}
|
|
1134
1449
|
}
|
|
1450
|
+
sendRpcRequest(event, data, timeoutMs) {
|
|
1451
|
+
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
1452
|
+
throw new Error("Client socket is not connected for ACK request.");
|
|
1453
|
+
}
|
|
1454
|
+
const requestId = crypto.randomUUID();
|
|
1455
|
+
return new Promise((resolve, reject) => {
|
|
1456
|
+
const timeoutHandle = setTimeout(() => {
|
|
1457
|
+
this.pendingRpcRequests.delete(requestId);
|
|
1458
|
+
reject(new Error(`ACK response timed out after ${timeoutMs}ms for event "${event}".`));
|
|
1459
|
+
}, timeoutMs);
|
|
1460
|
+
timeoutHandle.unref?.();
|
|
1461
|
+
this.pendingRpcRequests.set(requestId, {
|
|
1462
|
+
resolve,
|
|
1463
|
+
reject,
|
|
1464
|
+
timeoutHandle
|
|
1465
|
+
});
|
|
1466
|
+
const rpcRequestEnvelope = {
|
|
1467
|
+
event: INTERNAL_RPC_REQUEST_EVENT,
|
|
1468
|
+
data: {
|
|
1469
|
+
id: requestId,
|
|
1470
|
+
event,
|
|
1471
|
+
data
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
if (!this.isHandshakeReady()) {
|
|
1475
|
+
this.pendingPayloadQueue.push(rpcRequestEnvelope);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
this.sendEncryptedEnvelope(rpcRequestEnvelope);
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
handleRpcResponse(data) {
|
|
1482
|
+
try {
|
|
1483
|
+
const responsePayload = parseRpcResponsePayload(data);
|
|
1484
|
+
const pendingRequest = this.pendingRpcRequests.get(responsePayload.id);
|
|
1485
|
+
if (!pendingRequest) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
1489
|
+
this.pendingRpcRequests.delete(responsePayload.id);
|
|
1490
|
+
if (responsePayload.ok) {
|
|
1491
|
+
pendingRequest.resolve(responsePayload.data);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
pendingRequest.reject(
|
|
1495
|
+
new Error(responsePayload.error ?? "ACK request failed without an error message.")
|
|
1496
|
+
);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
this.notifyError(normalizeToError(error, "Failed to process client ACK response."));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async handleRpcRequest(data) {
|
|
1502
|
+
let rpcRequestPayload;
|
|
1503
|
+
try {
|
|
1504
|
+
rpcRequestPayload = parseRpcRequestPayload(data);
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
this.notifyError(normalizeToError(error, "Invalid client ACK request payload."));
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
const ackResponse = await this.executeRpcRequestHandler(
|
|
1511
|
+
rpcRequestPayload.event,
|
|
1512
|
+
rpcRequestPayload.data
|
|
1513
|
+
);
|
|
1514
|
+
this.sendEncryptedEnvelope({
|
|
1515
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
1516
|
+
data: {
|
|
1517
|
+
id: rpcRequestPayload.id,
|
|
1518
|
+
ok: true,
|
|
1519
|
+
data: ackResponse
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
const normalizedError = normalizeToError(error, "Client ACK request handler failed.");
|
|
1524
|
+
this.sendEncryptedEnvelope({
|
|
1525
|
+
event: INTERNAL_RPC_RESPONSE_EVENT,
|
|
1526
|
+
data: {
|
|
1527
|
+
id: rpcRequestPayload.id,
|
|
1528
|
+
ok: false,
|
|
1529
|
+
error: normalizedError.message
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
this.notifyError(normalizedError);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async executeRpcRequestHandler(event, data) {
|
|
1536
|
+
const handlers = this.customEventHandlers.get(event);
|
|
1537
|
+
if (!handlers || handlers.size === 0) {
|
|
1538
|
+
throw new Error(`No handler is registered for ACK request event "${event}".`);
|
|
1539
|
+
}
|
|
1540
|
+
const firstHandler = handlers.values().next().value;
|
|
1541
|
+
return Promise.resolve(firstHandler(data));
|
|
1542
|
+
}
|
|
1543
|
+
rejectPendingRpcRequests(error) {
|
|
1544
|
+
for (const pendingRequest of this.pendingRpcRequests.values()) {
|
|
1545
|
+
clearTimeout(pendingRequest.timeoutHandle);
|
|
1546
|
+
pendingRequest.reject(error);
|
|
1547
|
+
}
|
|
1548
|
+
this.pendingRpcRequests.clear();
|
|
1549
|
+
}
|
|
1135
1550
|
createClientHandshakeState() {
|
|
1136
1551
|
const { ecdh, localPublicKey } = createEphemeralHandshakeState();
|
|
1137
1552
|
return {
|