@aegis-fluxion/core 0.3.0 → 0.5.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/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";
@@ -13,12 +15,15 @@ var GCM_AUTH_TAG_LENGTH = 16;
13
15
  var ENCRYPTION_KEY_LENGTH = 32;
14
16
  var ENCRYPTED_PACKET_VERSION = 1;
15
17
  var ENCRYPTED_PACKET_PREFIX_LENGTH = 1 + GCM_IV_LENGTH + GCM_AUTH_TAG_LENGTH;
18
+ var BINARY_PAYLOAD_MARKER = "__afxBinaryPayload";
19
+ var BINARY_PAYLOAD_VERSION = 1;
16
20
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
17
21
  var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
18
22
  var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
19
23
  var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
20
24
  var DEFAULT_RECONNECT_FACTOR = 2;
21
25
  var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
26
+ var DEFAULT_RPC_TIMEOUT_MS = 5e3;
22
27
  function normalizeToError(error, fallbackMessage) {
23
28
  if (error instanceof Error) {
24
29
  return error;
@@ -52,7 +57,103 @@ function rawDataToBuffer(rawData) {
52
57
  }
53
58
  return Buffer.from(rawData);
54
59
  }
55
- function serializeEnvelope(event, data) {
60
+ function isBlobValue(value) {
61
+ return typeof Blob !== "undefined" && value instanceof Blob;
62
+ }
63
+ function isPlainObject(value) {
64
+ if (typeof value !== "object" || value === null) {
65
+ return false;
66
+ }
67
+ const prototype = Object.getPrototypeOf(value);
68
+ return prototype === Object.prototype || prototype === null;
69
+ }
70
+ function encodeBinaryPayload(kind, payloadBuffer, mimeType) {
71
+ const encodedPayload = {
72
+ [BINARY_PAYLOAD_MARKER]: BINARY_PAYLOAD_VERSION,
73
+ kind,
74
+ base64: payloadBuffer.toString("base64")
75
+ };
76
+ if (mimeType !== void 0 && mimeType.length > 0) {
77
+ encodedPayload.mimeType = mimeType;
78
+ }
79
+ return encodedPayload;
80
+ }
81
+ async function encodeEnvelopeData(value) {
82
+ if (Buffer.isBuffer(value)) {
83
+ return encodeBinaryPayload("buffer", value);
84
+ }
85
+ if (value instanceof Uint8Array) {
86
+ const typedArrayBuffer = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
87
+ return encodeBinaryPayload("uint8array", typedArrayBuffer);
88
+ }
89
+ if (isBlobValue(value)) {
90
+ const blobBuffer = Buffer.from(await value.arrayBuffer());
91
+ return encodeBinaryPayload("blob", blobBuffer, value.type);
92
+ }
93
+ if (Array.isArray(value)) {
94
+ return Promise.all(value.map((item) => encodeEnvelopeData(item)));
95
+ }
96
+ if (isPlainObject(value)) {
97
+ const encodedEntries = await Promise.all(
98
+ Object.entries(value).map(async ([key, entryValue]) => {
99
+ return [key, await encodeEnvelopeData(entryValue)];
100
+ })
101
+ );
102
+ return Object.fromEntries(encodedEntries);
103
+ }
104
+ return value;
105
+ }
106
+ function isEncodedBinaryPayload(value) {
107
+ if (!isPlainObject(value)) {
108
+ return false;
109
+ }
110
+ if (value[BINARY_PAYLOAD_MARKER] !== BINARY_PAYLOAD_VERSION) {
111
+ return false;
112
+ }
113
+ if (value.kind !== "buffer" && value.kind !== "uint8array" && value.kind !== "blob") {
114
+ return false;
115
+ }
116
+ if (typeof value.base64 !== "string") {
117
+ return false;
118
+ }
119
+ if (value.mimeType !== void 0 && typeof value.mimeType !== "string") {
120
+ return false;
121
+ }
122
+ return true;
123
+ }
124
+ function decodeEnvelopeData(value) {
125
+ if (Array.isArray(value)) {
126
+ return value.map((item) => decodeEnvelopeData(item));
127
+ }
128
+ if (isEncodedBinaryPayload(value)) {
129
+ const binaryBuffer = Buffer.from(value.base64, "base64");
130
+ if (value.kind === "buffer") {
131
+ return binaryBuffer;
132
+ }
133
+ if (value.kind === "uint8array") {
134
+ return Uint8Array.from(binaryBuffer);
135
+ }
136
+ if (typeof Blob === "undefined") {
137
+ return binaryBuffer;
138
+ }
139
+ return new Blob([binaryBuffer], {
140
+ type: value.mimeType ?? ""
141
+ });
142
+ }
143
+ if (isPlainObject(value)) {
144
+ const decodedEntries = Object.entries(value).map(([key, entryValue]) => {
145
+ return [key, decodeEnvelopeData(entryValue)];
146
+ });
147
+ return Object.fromEntries(decodedEntries);
148
+ }
149
+ return value;
150
+ }
151
+ async function serializeEnvelope(event, data) {
152
+ const encodedData = await encodeEnvelopeData(data);
153
+ const envelope = { event, data: encodedData };
154
+ return JSON.stringify(envelope);
155
+ }
156
+ function serializePlainEnvelope(event, data) {
56
157
  const envelope = { event, data };
57
158
  return JSON.stringify(envelope);
58
159
  }
@@ -64,7 +165,7 @@ function parseEnvelope(rawData) {
64
165
  }
65
166
  return {
66
167
  event: parsed.event,
67
- data: parsed.data
168
+ data: decodeEnvelopeData(parsed.data)
68
169
  };
69
170
  }
70
171
  function parseEnvelopeFromText(decodedPayload) {
@@ -74,14 +175,95 @@ function parseEnvelopeFromText(decodedPayload) {
74
175
  }
75
176
  return {
76
177
  event: parsed.event,
77
- data: parsed.data
178
+ data: decodeEnvelopeData(parsed.data)
78
179
  };
79
180
  }
80
181
  function decodeCloseReason(reason) {
81
182
  return reason.toString("utf8");
82
183
  }
83
184
  function isReservedEmitEvent(event) {
84
- return event === INTERNAL_HANDSHAKE_EVENT || event === READY_EVENT;
185
+ return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
186
+ }
187
+ function isPromiseLike(value) {
188
+ return typeof value === "object" && value !== null && "then" in value;
189
+ }
190
+ function normalizeRpcTimeout(timeoutMs) {
191
+ const resolvedTimeoutMs = timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
192
+ if (!Number.isFinite(resolvedTimeoutMs) || resolvedTimeoutMs <= 0) {
193
+ throw new Error("ACK timeoutMs must be a positive number.");
194
+ }
195
+ return resolvedTimeoutMs;
196
+ }
197
+ function parseRpcRequestPayload(data) {
198
+ if (typeof data !== "object" || data === null) {
199
+ throw new Error("Invalid RPC request payload format.");
200
+ }
201
+ const payload = data;
202
+ if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
203
+ throw new Error("RPC request payload must include a non-empty id.");
204
+ }
205
+ if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
206
+ throw new Error("RPC request payload must include a non-empty event.");
207
+ }
208
+ return {
209
+ id: payload.id,
210
+ event: payload.event,
211
+ data: payload.data
212
+ };
213
+ }
214
+ function parseRpcResponsePayload(data) {
215
+ if (typeof data !== "object" || data === null) {
216
+ throw new Error("Invalid RPC response payload format.");
217
+ }
218
+ const payload = data;
219
+ if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
220
+ throw new Error("RPC response payload must include a non-empty id.");
221
+ }
222
+ if (typeof payload.ok !== "boolean") {
223
+ throw new Error("RPC response payload must include a boolean ok field.");
224
+ }
225
+ if (payload.error !== void 0 && typeof payload.error !== "string") {
226
+ throw new Error("RPC response payload error must be a string when provided.");
227
+ }
228
+ const parsedPayload = {
229
+ id: payload.id,
230
+ ok: payload.ok,
231
+ data: payload.data
232
+ };
233
+ if (payload.error !== void 0) {
234
+ parsedPayload.error = payload.error;
235
+ }
236
+ return parsedPayload;
237
+ }
238
+ function resolveAckArguments(callbackOrOptions, maybeCallback) {
239
+ if (callbackOrOptions === void 0 && maybeCallback === void 0) {
240
+ return {
241
+ expectsAck: false,
242
+ timeoutMs: DEFAULT_RPC_TIMEOUT_MS
243
+ };
244
+ }
245
+ if (typeof callbackOrOptions === "function") {
246
+ if (maybeCallback !== void 0) {
247
+ throw new Error("ACK callback was provided more than once.");
248
+ }
249
+ return {
250
+ expectsAck: true,
251
+ callback: callbackOrOptions,
252
+ timeoutMs: DEFAULT_RPC_TIMEOUT_MS
253
+ };
254
+ }
255
+ const options = callbackOrOptions;
256
+ if (options !== void 0 && (typeof options !== "object" || options === null)) {
257
+ throw new Error("ACK options must be an object.");
258
+ }
259
+ if (maybeCallback !== void 0 && typeof maybeCallback !== "function") {
260
+ throw new Error("ACK callback must be a function.");
261
+ }
262
+ return {
263
+ ...maybeCallback ? { callback: maybeCallback } : {},
264
+ expectsAck: true,
265
+ timeoutMs: normalizeRpcTimeout(options?.timeoutMs)
266
+ };
85
267
  }
86
268
  function createEphemeralHandshakeState() {
87
269
  const ecdh = createECDH(HANDSHAKE_CURVE);
@@ -179,6 +361,7 @@ var SecureServer = class {
179
361
  sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
180
362
  encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
181
363
  pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
364
+ pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
182
365
  heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
183
366
  roomMembersByName = /* @__PURE__ */ new Map();
184
367
  roomNamesByClientId = /* @__PURE__ */ new Map();
@@ -270,14 +453,17 @@ var SecureServer = class {
270
453
  }
271
454
  const envelope = { event, data };
272
455
  for (const client of this.clientsById.values()) {
273
- this.sendOrQueuePayload(client.socket, envelope);
456
+ void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
457
+ return void 0;
458
+ });
274
459
  }
275
460
  } catch (error) {
276
461
  this.notifyError(normalizeToError(error, "Failed to emit server event."));
277
462
  }
278
463
  return this;
279
464
  }
280
- emitTo(clientId, event, data) {
465
+ emitTo(clientId, event, data, callbackOrOptions, maybeCallback) {
466
+ const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
281
467
  try {
282
468
  if (isReservedEmitEvent(event)) {
283
469
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
@@ -286,10 +472,39 @@ var SecureServer = class {
286
472
  if (!client) {
287
473
  throw new Error(`Client with id ${clientId} was not found.`);
288
474
  }
289
- this.sendOrQueuePayload(client.socket, { event, data });
290
- return true;
475
+ if (!ackArgs.expectsAck) {
476
+ void this.sendOrQueuePayload(client.socket, { event, data }).catch(() => {
477
+ return void 0;
478
+ });
479
+ return true;
480
+ }
481
+ const ackPromise = this.sendRpcRequest(
482
+ client.socket,
483
+ event,
484
+ data,
485
+ ackArgs.timeoutMs
486
+ );
487
+ if (ackArgs.callback) {
488
+ ackPromise.then((response) => {
489
+ ackArgs.callback?.(null, response);
490
+ }).catch((error) => {
491
+ ackArgs.callback?.(
492
+ normalizeToError(error, `ACK callback failed for client ${client.id}.`)
493
+ );
494
+ });
495
+ return true;
496
+ }
497
+ return ackPromise;
291
498
  } catch (error) {
292
- this.notifyError(normalizeToError(error, "Failed to emit event to client."));
499
+ const normalizedError = normalizeToError(error, "Failed to emit event to client.");
500
+ this.notifyError(normalizedError);
501
+ if (ackArgs.callback) {
502
+ ackArgs.callback(normalizedError);
503
+ return false;
504
+ }
505
+ if (ackArgs.expectsAck) {
506
+ return Promise.reject(normalizedError);
507
+ }
293
508
  return false;
294
509
  }
295
510
  }
@@ -312,6 +527,10 @@ var SecureServer = class {
312
527
  try {
313
528
  this.stopHeartbeatLoop();
314
529
  for (const client of this.clientsById.values()) {
530
+ this.rejectPendingRpcRequests(
531
+ client.socket,
532
+ new Error("Server closed before ACK response was received.")
533
+ );
315
534
  if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
316
535
  client.socket.close(code, reason);
317
536
  }
@@ -364,9 +583,14 @@ var SecureServer = class {
364
583
  lastPingAt: 0
365
584
  };
366
585
  if (heartbeatState.awaitingPong && now - heartbeatState.lastPingAt >= this.heartbeatConfig.timeoutMs) {
586
+ this.rejectPendingRpcRequests(
587
+ socket,
588
+ new Error(`Heartbeat timeout while waiting for client ${client.id} ACK response.`)
589
+ );
367
590
  this.sharedSecretBySocket.delete(socket);
368
591
  this.encryptionKeyBySocket.delete(socket);
369
592
  this.pendingPayloadsBySocket.delete(socket);
593
+ this.pendingRpcRequestsBySocket.delete(socket);
370
594
  this.handshakeStateBySocket.delete(socket);
371
595
  this.heartbeatStateBySocket.delete(socket);
372
596
  socket.terminate();
@@ -413,6 +637,7 @@ var SecureServer = class {
413
637
  this.clientIdBySocket.set(socket, clientId);
414
638
  this.handshakeStateBySocket.set(socket, handshakeState);
415
639
  this.pendingPayloadsBySocket.set(socket, []);
640
+ this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
416
641
  this.heartbeatStateBySocket.set(socket, {
417
642
  awaitingPong: false,
418
643
  lastPingAt: 0
@@ -480,6 +705,14 @@ var SecureServer = class {
480
705
  return;
481
706
  }
482
707
  const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
708
+ if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
709
+ this.handleRpcResponse(client.socket, decryptedEnvelope.data);
710
+ return;
711
+ }
712
+ if (decryptedEnvelope.event === INTERNAL_RPC_REQUEST_EVENT) {
713
+ void this.handleRpcRequest(client, decryptedEnvelope.data);
714
+ return;
715
+ }
483
716
  this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data, client);
484
717
  } catch (error) {
485
718
  this.notifyError(normalizeToError(error, "Failed to process incoming server message."));
@@ -494,6 +727,11 @@ var SecureServer = class {
494
727
  this.sharedSecretBySocket.delete(client.socket);
495
728
  this.encryptionKeyBySocket.delete(client.socket);
496
729
  this.pendingPayloadsBySocket.delete(client.socket);
730
+ this.rejectPendingRpcRequests(
731
+ client.socket,
732
+ new Error(`Client ${client.id} disconnected before ACK response was received.`)
733
+ );
734
+ this.pendingRpcRequestsBySocket.delete(client.socket);
497
735
  this.heartbeatStateBySocket.delete(client.socket);
498
736
  const decodedReason = decodeCloseReason(reason);
499
737
  for (const handler of this.disconnectHandlers) {
@@ -519,7 +757,17 @@ var SecureServer = class {
519
757
  }
520
758
  for (const handler of handlers) {
521
759
  try {
522
- handler(data, client);
760
+ const handlerResult = handler(data, client);
761
+ if (isPromiseLike(handlerResult)) {
762
+ void Promise.resolve(handlerResult).catch((error) => {
763
+ this.notifyError(
764
+ normalizeToError(
765
+ error,
766
+ `Server event handler failed for event ${event}.`
767
+ )
768
+ );
769
+ });
770
+ }
523
771
  } catch (error) {
524
772
  this.notifyError(
525
773
  normalizeToError(
@@ -540,24 +788,138 @@ var SecureServer = class {
540
788
  this.notifyError(normalizeToError(error, "Failed to send server payload."));
541
789
  }
542
790
  }
543
- sendEncryptedEnvelope(socket, envelope) {
791
+ async sendEncryptedEnvelope(socket, envelope) {
792
+ if (socket.readyState !== WebSocket.OPEN) {
793
+ return;
794
+ }
795
+ const encryptionKey = this.encryptionKeyBySocket.get(socket);
796
+ if (!encryptionKey) {
797
+ const missingKeyError = new Error("Missing encryption key for connected socket.");
798
+ this.notifyError(missingKeyError);
799
+ throw missingKeyError;
800
+ }
544
801
  try {
545
- if (socket.readyState !== WebSocket.OPEN) {
802
+ const serializedEnvelope = await serializeEnvelope(envelope.event, envelope.data);
803
+ const encryptedPayload = encryptSerializedEnvelope(serializedEnvelope, encryptionKey);
804
+ socket.send(encryptedPayload);
805
+ } catch (error) {
806
+ const normalizedError = normalizeToError(error, "Failed to send encrypted server payload.");
807
+ this.notifyError(normalizedError);
808
+ throw normalizedError;
809
+ }
810
+ }
811
+ sendRpcRequest(socket, event, data, timeoutMs) {
812
+ if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
813
+ throw new Error("Client socket is not connected for ACK request.");
814
+ }
815
+ const pendingRequests = this.pendingRpcRequestsBySocket.get(socket) ?? /* @__PURE__ */ new Map();
816
+ this.pendingRpcRequestsBySocket.set(socket, pendingRequests);
817
+ const requestId = randomUUID();
818
+ return new Promise((resolve, reject) => {
819
+ const timeoutHandle = setTimeout(() => {
820
+ pendingRequests.delete(requestId);
821
+ reject(new Error(`ACK response timed out after ${timeoutMs}ms for event "${event}".`));
822
+ }, timeoutMs);
823
+ timeoutHandle.unref?.();
824
+ pendingRequests.set(requestId, {
825
+ resolve,
826
+ reject,
827
+ timeoutHandle
828
+ });
829
+ void this.sendOrQueuePayload(socket, {
830
+ event: INTERNAL_RPC_REQUEST_EVENT,
831
+ data: {
832
+ id: requestId,
833
+ event,
834
+ data
835
+ }
836
+ }).catch((error) => {
837
+ clearTimeout(timeoutHandle);
838
+ pendingRequests.delete(requestId);
839
+ reject(
840
+ normalizeToError(error, `Failed to dispatch ACK request for event "${event}".`)
841
+ );
842
+ });
843
+ });
844
+ }
845
+ handleRpcResponse(socket, data) {
846
+ try {
847
+ const responsePayload = parseRpcResponsePayload(data);
848
+ const pendingRequests = this.pendingRpcRequestsBySocket.get(socket);
849
+ if (!pendingRequests) {
546
850
  return;
547
851
  }
548
- const encryptionKey = this.encryptionKeyBySocket.get(socket);
549
- if (!encryptionKey) {
550
- throw new Error("Missing encryption key for connected socket.");
852
+ const pendingRequest = pendingRequests.get(responsePayload.id);
853
+ if (!pendingRequest) {
854
+ return;
551
855
  }
552
- const encryptedPayload = encryptSerializedEnvelope(
553
- serializeEnvelope(envelope.event, envelope.data),
554
- encryptionKey
856
+ clearTimeout(pendingRequest.timeoutHandle);
857
+ pendingRequests.delete(responsePayload.id);
858
+ if (responsePayload.ok) {
859
+ pendingRequest.resolve(responsePayload.data);
860
+ return;
861
+ }
862
+ pendingRequest.reject(
863
+ new Error(responsePayload.error ?? "ACK request failed without an error message.")
555
864
  );
556
- socket.send(encryptedPayload);
557
865
  } catch (error) {
558
- this.notifyError(normalizeToError(error, "Failed to send encrypted server payload."));
866
+ this.notifyError(normalizeToError(error, "Failed to process server ACK response."));
559
867
  }
560
868
  }
869
+ async handleRpcRequest(client, data) {
870
+ let rpcRequestPayload;
871
+ try {
872
+ rpcRequestPayload = parseRpcRequestPayload(data);
873
+ } catch (error) {
874
+ this.notifyError(normalizeToError(error, "Invalid server ACK request payload."));
875
+ return;
876
+ }
877
+ try {
878
+ const ackResponse = await this.executeRpcRequestHandler(
879
+ rpcRequestPayload.event,
880
+ rpcRequestPayload.data,
881
+ client
882
+ );
883
+ await this.sendEncryptedEnvelope(client.socket, {
884
+ event: INTERNAL_RPC_RESPONSE_EVENT,
885
+ data: {
886
+ id: rpcRequestPayload.id,
887
+ ok: true,
888
+ data: ackResponse
889
+ }
890
+ });
891
+ } catch (error) {
892
+ const normalizedError = normalizeToError(error, "Server ACK request handler failed.");
893
+ await this.sendEncryptedEnvelope(client.socket, {
894
+ event: INTERNAL_RPC_RESPONSE_EVENT,
895
+ data: {
896
+ id: rpcRequestPayload.id,
897
+ ok: false,
898
+ error: normalizedError.message
899
+ }
900
+ });
901
+ this.notifyError(normalizedError);
902
+ }
903
+ }
904
+ async executeRpcRequestHandler(event, data, client) {
905
+ const handlers = this.customEventHandlers.get(event);
906
+ if (!handlers || handlers.size === 0) {
907
+ throw new Error(`No handler is registered for ACK request event "${event}".`);
908
+ }
909
+ const firstHandler = handlers.values().next().value;
910
+ return Promise.resolve(firstHandler(data, client));
911
+ }
912
+ rejectPendingRpcRequests(socket, error) {
913
+ const pendingRequests = this.pendingRpcRequestsBySocket.get(socket);
914
+ if (!pendingRequests) {
915
+ return;
916
+ }
917
+ for (const pendingRequest of pendingRequests.values()) {
918
+ clearTimeout(pendingRequest.timeoutHandle);
919
+ pendingRequest.reject(error);
920
+ }
921
+ pendingRequests.clear();
922
+ }
561
923
  notifyConnection(client) {
562
924
  for (const handler of this.connectionHandlers) {
563
925
  try {
@@ -602,7 +964,7 @@ var SecureServer = class {
602
964
  sendInternalHandshake(socket, localPublicKey) {
603
965
  this.sendRaw(
604
966
  socket,
605
- serializeEnvelope(INTERNAL_HANDSHAKE_EVENT, {
967
+ serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
606
968
  publicKey: localPublicKey
607
969
  })
608
970
  );
@@ -635,23 +997,23 @@ var SecureServer = class {
635
997
  sendOrQueuePayload(socket, envelope) {
636
998
  if (!this.isClientHandshakeReady(socket)) {
637
999
  this.queuePayload(socket, envelope);
638
- return;
1000
+ return Promise.resolve();
639
1001
  }
640
- this.sendEncryptedEnvelope(socket, envelope);
1002
+ return this.sendEncryptedEnvelope(socket, envelope);
641
1003
  }
642
1004
  queuePayload(socket, envelope) {
643
1005
  const pendingPayloads = this.pendingPayloadsBySocket.get(socket) ?? [];
644
1006
  pendingPayloads.push(envelope);
645
1007
  this.pendingPayloadsBySocket.set(socket, pendingPayloads);
646
1008
  }
647
- flushQueuedPayloads(socket) {
1009
+ async flushQueuedPayloads(socket) {
648
1010
  const pendingPayloads = this.pendingPayloadsBySocket.get(socket);
649
1011
  if (!pendingPayloads || pendingPayloads.length === 0) {
650
1012
  return;
651
1013
  }
652
1014
  this.pendingPayloadsBySocket.delete(socket);
653
1015
  for (const envelope of pendingPayloads) {
654
- this.sendEncryptedEnvelope(socket, envelope);
1016
+ await this.sendEncryptedEnvelope(socket, envelope);
655
1017
  }
656
1018
  }
657
1019
  createSecureServerClient(clientId, socket, request) {
@@ -659,6 +1021,24 @@ var SecureServer = class {
659
1021
  id: clientId,
660
1022
  socket,
661
1023
  request,
1024
+ emit: (event, data, callbackOrOptions, maybeCallback) => {
1025
+ if (callbackOrOptions === void 0 && maybeCallback === void 0) {
1026
+ return this.emitTo(clientId, event, data);
1027
+ }
1028
+ if (typeof callbackOrOptions === "function") {
1029
+ return this.emitTo(clientId, event, data, callbackOrOptions);
1030
+ }
1031
+ if (maybeCallback) {
1032
+ return this.emitTo(
1033
+ clientId,
1034
+ event,
1035
+ data,
1036
+ callbackOrOptions ?? {},
1037
+ maybeCallback
1038
+ );
1039
+ }
1040
+ return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
1041
+ },
662
1042
  join: (room) => this.joinClientToRoom(clientId, room),
663
1043
  leave: (room) => this.leaveClientFromRoom(clientId, room),
664
1044
  leaveAll: () => this.leaveClientFromAllRooms(clientId)
@@ -743,7 +1123,9 @@ var SecureServer = class {
743
1123
  if (!client) {
744
1124
  continue;
745
1125
  }
746
- this.sendOrQueuePayload(client.socket, envelope);
1126
+ void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1127
+ return void 0;
1128
+ });
747
1129
  }
748
1130
  }
749
1131
  };
@@ -770,6 +1152,7 @@ var SecureClient = class {
770
1152
  errorHandlers = /* @__PURE__ */ new Set();
771
1153
  handshakeState = null;
772
1154
  pendingPayloadQueue = [];
1155
+ pendingRpcRequests = /* @__PURE__ */ new Map();
773
1156
  get readyState() {
774
1157
  return this.socket?.readyState ?? null;
775
1158
  }
@@ -878,7 +1261,8 @@ var SecureClient = class {
878
1261
  }
879
1262
  return this;
880
1263
  }
881
- emit(event, data) {
1264
+ emit(event, data, callbackOrOptions, maybeCallback) {
1265
+ const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
882
1266
  try {
883
1267
  if (isReservedEmitEvent(event)) {
884
1268
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
@@ -886,15 +1270,39 @@ var SecureClient = class {
886
1270
  if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
887
1271
  throw new Error("Client socket is not connected.");
888
1272
  }
1273
+ if (ackArgs.expectsAck) {
1274
+ const ackPromise = this.sendRpcRequest(event, data, ackArgs.timeoutMs);
1275
+ if (ackArgs.callback) {
1276
+ ackPromise.then((response) => {
1277
+ ackArgs.callback?.(null, response);
1278
+ }).catch((error) => {
1279
+ ackArgs.callback?.(
1280
+ normalizeToError(error, `ACK callback failed for event "${event}".`)
1281
+ );
1282
+ });
1283
+ return true;
1284
+ }
1285
+ return ackPromise;
1286
+ }
889
1287
  const envelope = { event, data };
890
1288
  if (!this.isHandshakeReady()) {
891
1289
  this.pendingPayloadQueue.push(envelope);
892
1290
  return true;
893
1291
  }
894
- this.sendEncryptedEnvelope(envelope);
1292
+ void this.sendEncryptedEnvelope(envelope).catch(() => {
1293
+ return void 0;
1294
+ });
895
1295
  return true;
896
1296
  } catch (error) {
897
- this.notifyError(normalizeToError(error, "Failed to emit client event."));
1297
+ const normalizedError = normalizeToError(error, "Failed to emit client event.");
1298
+ this.notifyError(normalizedError);
1299
+ if (ackArgs.callback) {
1300
+ ackArgs.callback(normalizedError);
1301
+ return false;
1302
+ }
1303
+ if (ackArgs.expectsAck) {
1304
+ return Promise.reject(normalizedError);
1305
+ }
898
1306
  return false;
899
1307
  }
900
1308
  }
@@ -1036,6 +1444,14 @@ var SecureClient = class {
1036
1444
  return;
1037
1445
  }
1038
1446
  const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
1447
+ if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
1448
+ this.handleRpcResponse(decryptedEnvelope.data);
1449
+ return;
1450
+ }
1451
+ if (decryptedEnvelope.event === INTERNAL_RPC_REQUEST_EVENT) {
1452
+ void this.handleRpcRequest(decryptedEnvelope.data);
1453
+ return;
1454
+ }
1039
1455
  this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data);
1040
1456
  } catch (error) {
1041
1457
  this.notifyError(normalizeToError(error, "Failed to process incoming client message."));
@@ -1046,6 +1462,9 @@ var SecureClient = class {
1046
1462
  this.socket = null;
1047
1463
  this.handshakeState = null;
1048
1464
  this.pendingPayloadQueue = [];
1465
+ this.rejectPendingRpcRequests(
1466
+ new Error("Client disconnected before ACK response was received.")
1467
+ );
1049
1468
  const decodedReason = decodeCloseReason(reason);
1050
1469
  for (const handler of this.disconnectHandlers) {
1051
1470
  try {
@@ -1071,7 +1490,17 @@ var SecureClient = class {
1071
1490
  }
1072
1491
  for (const handler of handlers) {
1073
1492
  try {
1074
- handler(data);
1493
+ const handlerResult = handler(data);
1494
+ if (isPromiseLike(handlerResult)) {
1495
+ void Promise.resolve(handlerResult).catch((error) => {
1496
+ this.notifyError(
1497
+ normalizeToError(
1498
+ error,
1499
+ `Client event handler failed for event ${event}.`
1500
+ )
1501
+ );
1502
+ });
1503
+ }
1075
1504
  } catch (error) {
1076
1505
  this.notifyError(
1077
1506
  normalizeToError(error, `Client event handler failed for event ${event}.`)
@@ -1108,23 +1537,133 @@ var SecureClient = class {
1108
1537
  }
1109
1538
  }
1110
1539
  }
1111
- sendEncryptedEnvelope(envelope) {
1540
+ async sendEncryptedEnvelope(envelope) {
1541
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1542
+ const socketStateError = new Error("Client socket is not connected.");
1543
+ this.notifyError(socketStateError);
1544
+ throw socketStateError;
1545
+ }
1546
+ const encryptionKey = this.handshakeState?.encryptionKey;
1547
+ if (!encryptionKey) {
1548
+ const missingKeyError = new Error("Missing encryption key for client payload encryption.");
1549
+ this.notifyError(missingKeyError);
1550
+ throw missingKeyError;
1551
+ }
1112
1552
  try {
1113
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1114
- throw new Error("Client socket is not connected.");
1553
+ const serializedEnvelope = await serializeEnvelope(envelope.event, envelope.data);
1554
+ const encryptedPayload = encryptSerializedEnvelope(serializedEnvelope, encryptionKey);
1555
+ this.socket.send(encryptedPayload);
1556
+ } catch (error) {
1557
+ const normalizedError = normalizeToError(error, "Failed to send encrypted client payload.");
1558
+ this.notifyError(normalizedError);
1559
+ throw normalizedError;
1560
+ }
1561
+ }
1562
+ sendRpcRequest(event, data, timeoutMs) {
1563
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1564
+ throw new Error("Client socket is not connected for ACK request.");
1565
+ }
1566
+ const requestId = randomUUID();
1567
+ return new Promise((resolve, reject) => {
1568
+ const timeoutHandle = setTimeout(() => {
1569
+ this.pendingRpcRequests.delete(requestId);
1570
+ reject(new Error(`ACK response timed out after ${timeoutMs}ms for event "${event}".`));
1571
+ }, timeoutMs);
1572
+ timeoutHandle.unref?.();
1573
+ this.pendingRpcRequests.set(requestId, {
1574
+ resolve,
1575
+ reject,
1576
+ timeoutHandle
1577
+ });
1578
+ const rpcRequestEnvelope = {
1579
+ event: INTERNAL_RPC_REQUEST_EVENT,
1580
+ data: {
1581
+ id: requestId,
1582
+ event,
1583
+ data
1584
+ }
1585
+ };
1586
+ if (!this.isHandshakeReady()) {
1587
+ this.pendingPayloadQueue.push(rpcRequestEnvelope);
1588
+ return;
1115
1589
  }
1116
- const encryptionKey = this.handshakeState?.encryptionKey;
1117
- if (!encryptionKey) {
1118
- throw new Error("Missing encryption key for client payload encryption.");
1590
+ void this.sendEncryptedEnvelope(rpcRequestEnvelope).catch((error) => {
1591
+ clearTimeout(timeoutHandle);
1592
+ this.pendingRpcRequests.delete(requestId);
1593
+ reject(
1594
+ normalizeToError(error, `Failed to dispatch ACK request for event "${event}".`)
1595
+ );
1596
+ });
1597
+ });
1598
+ }
1599
+ handleRpcResponse(data) {
1600
+ try {
1601
+ const responsePayload = parseRpcResponsePayload(data);
1602
+ const pendingRequest = this.pendingRpcRequests.get(responsePayload.id);
1603
+ if (!pendingRequest) {
1604
+ return;
1119
1605
  }
1120
- const encryptedPayload = encryptSerializedEnvelope(
1121
- serializeEnvelope(envelope.event, envelope.data),
1122
- encryptionKey
1606
+ clearTimeout(pendingRequest.timeoutHandle);
1607
+ this.pendingRpcRequests.delete(responsePayload.id);
1608
+ if (responsePayload.ok) {
1609
+ pendingRequest.resolve(responsePayload.data);
1610
+ return;
1611
+ }
1612
+ pendingRequest.reject(
1613
+ new Error(responsePayload.error ?? "ACK request failed without an error message.")
1123
1614
  );
1124
- this.socket.send(encryptedPayload);
1125
1615
  } catch (error) {
1126
- this.notifyError(normalizeToError(error, "Failed to send encrypted client payload."));
1616
+ this.notifyError(normalizeToError(error, "Failed to process client ACK response."));
1617
+ }
1618
+ }
1619
+ async handleRpcRequest(data) {
1620
+ let rpcRequestPayload;
1621
+ try {
1622
+ rpcRequestPayload = parseRpcRequestPayload(data);
1623
+ } catch (error) {
1624
+ this.notifyError(normalizeToError(error, "Invalid client ACK request payload."));
1625
+ return;
1626
+ }
1627
+ try {
1628
+ const ackResponse = await this.executeRpcRequestHandler(
1629
+ rpcRequestPayload.event,
1630
+ rpcRequestPayload.data
1631
+ );
1632
+ await this.sendEncryptedEnvelope({
1633
+ event: INTERNAL_RPC_RESPONSE_EVENT,
1634
+ data: {
1635
+ id: rpcRequestPayload.id,
1636
+ ok: true,
1637
+ data: ackResponse
1638
+ }
1639
+ });
1640
+ } catch (error) {
1641
+ const normalizedError = normalizeToError(error, "Client ACK request handler failed.");
1642
+ await this.sendEncryptedEnvelope({
1643
+ event: INTERNAL_RPC_RESPONSE_EVENT,
1644
+ data: {
1645
+ id: rpcRequestPayload.id,
1646
+ ok: false,
1647
+ error: normalizedError.message
1648
+ }
1649
+ });
1650
+ this.notifyError(normalizedError);
1651
+ }
1652
+ }
1653
+ async executeRpcRequestHandler(event, data) {
1654
+ const handlers = this.customEventHandlers.get(event);
1655
+ if (!handlers || handlers.size === 0) {
1656
+ throw new Error(`No handler is registered for ACK request event "${event}".`);
1657
+ }
1658
+ const firstHandler = handlers.values().next().value;
1659
+ return Promise.resolve(firstHandler(data));
1660
+ }
1661
+ rejectPendingRpcRequests(error) {
1662
+ for (const pendingRequest of this.pendingRpcRequests.values()) {
1663
+ clearTimeout(pendingRequest.timeoutHandle);
1664
+ pendingRequest.reject(error);
1127
1665
  }
1666
+ this.pendingRpcRequests.clear();
1128
1667
  }
1129
1668
  createClientHandshakeState() {
1130
1669
  const { ecdh, localPublicKey } = createEphemeralHandshakeState();
@@ -1145,7 +1684,7 @@ var SecureClient = class {
1145
1684
  throw new Error("Missing client handshake state.");
1146
1685
  }
1147
1686
  this.socket.send(
1148
- serializeEnvelope(INTERNAL_HANDSHAKE_EVENT, {
1687
+ serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
1149
1688
  publicKey: this.handshakeState.localPublicKey
1150
1689
  })
1151
1690
  );
@@ -1167,7 +1706,7 @@ var SecureClient = class {
1167
1706
  this.handshakeState.sharedSecret = sharedSecret;
1168
1707
  this.handshakeState.encryptionKey = deriveEncryptionKey(sharedSecret);
1169
1708
  this.handshakeState.isReady = true;
1170
- this.flushPendingPayloadQueue();
1709
+ void this.flushPendingPayloadQueue();
1171
1710
  this.notifyReady();
1172
1711
  } catch (error) {
1173
1712
  this.notifyError(normalizeToError(error, "Failed to complete client handshake."));
@@ -1176,14 +1715,14 @@ var SecureClient = class {
1176
1715
  isHandshakeReady() {
1177
1716
  return this.handshakeState?.isReady ?? false;
1178
1717
  }
1179
- flushPendingPayloadQueue() {
1718
+ async flushPendingPayloadQueue() {
1180
1719
  if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.isHandshakeReady()) {
1181
1720
  return;
1182
1721
  }
1183
1722
  const pendingPayloads = this.pendingPayloadQueue;
1184
1723
  this.pendingPayloadQueue = [];
1185
1724
  for (const envelope of pendingPayloads) {
1186
- this.sendEncryptedEnvelope(envelope);
1725
+ await this.sendEncryptedEnvelope(envelope);
1187
1726
  }
1188
1727
  }
1189
1728
  };