@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/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) => void;
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) => void;
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
- this.sendOrQueuePayload(client.socket, { event, data });
290
- return true;
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
- this.notifyError(normalizeToError(error, "Failed to emit event to client."));
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
- this.notifyError(normalizeToError(error, "Failed to emit client event."));
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 {