@aegis-fluxion/core 0.7.0 → 0.7.2

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
@@ -26,6 +26,59 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
26
26
  var DEFAULT_RECONNECT_FACTOR = 2;
27
27
  var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
28
28
  var DEFAULT_RPC_TIMEOUT_MS = 5e3;
29
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 1e3;
30
+ var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION = 120;
31
+ var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP = 300;
32
+ var DEFAULT_RATE_LIMIT_THROTTLE_MS = 150;
33
+ var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
34
+ var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
35
+ var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
36
+ var DEFAULT_RATE_LIMIT_CLOSE_REASON = "Rate limit exceeded. Please retry later.";
37
+ var SECURE_SERVER_ADAPTER_MESSAGE_VERSION = 1;
38
+ function normalizeSecureServerAdapterMessage(value) {
39
+ if (!isPlainObject(value)) {
40
+ throw new Error("SecureServer adapter message must be a plain object.");
41
+ }
42
+ if (value.version !== SECURE_SERVER_ADAPTER_MESSAGE_VERSION) {
43
+ throw new Error(
44
+ `Unsupported SecureServer adapter message version: ${String(value.version)}.`
45
+ );
46
+ }
47
+ if (typeof value.originServerId !== "string" || value.originServerId.trim().length === 0) {
48
+ throw new Error("SecureServer adapter message originServerId must be a non-empty string.");
49
+ }
50
+ if (value.scope !== "broadcast" && value.scope !== "room") {
51
+ throw new Error('SecureServer adapter message scope must be either "broadcast" or "room".');
52
+ }
53
+ if (typeof value.event !== "string" || value.event.trim().length === 0) {
54
+ throw new Error("SecureServer adapter message event must be a non-empty string.");
55
+ }
56
+ if (typeof value.emittedAt !== "number" || !Number.isFinite(value.emittedAt)) {
57
+ throw new Error("SecureServer adapter message emittedAt must be a finite number.");
58
+ }
59
+ if (value.scope === "room") {
60
+ if (typeof value.room !== "string" || value.room.trim().length === 0) {
61
+ throw new Error("SecureServer adapter message room must be a non-empty string.");
62
+ }
63
+ return {
64
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
65
+ originServerId: value.originServerId,
66
+ scope: value.scope,
67
+ event: value.event,
68
+ data: value.data,
69
+ emittedAt: value.emittedAt,
70
+ room: value.room.trim()
71
+ };
72
+ }
73
+ return {
74
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
75
+ originServerId: value.originServerId,
76
+ scope: value.scope,
77
+ event: value.event,
78
+ data: value.data,
79
+ emittedAt: value.emittedAt
80
+ };
81
+ }
29
82
  function normalizeToError(error, fallbackMessage) {
30
83
  if (error instanceof Error) {
31
84
  return error;
@@ -59,6 +112,11 @@ function rawDataToBuffer(rawData) {
59
112
  }
60
113
  return Buffer.from(rawData);
61
114
  }
115
+ function delay(ms) {
116
+ return new Promise((resolve) => {
117
+ setTimeout(resolve, ms);
118
+ });
119
+ }
62
120
  function isBlobValue(value) {
63
121
  return typeof Blob !== "undefined" && value instanceof Blob;
64
122
  }
@@ -349,8 +407,11 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
349
407
  return plaintext.toString("utf8");
350
408
  }
351
409
  var SecureServer = class {
410
+ instanceId = randomUUID();
352
411
  socketServer;
412
+ adapter = null;
353
413
  heartbeatConfig;
414
+ rateLimitConfig;
354
415
  heartbeatIntervalHandle = null;
355
416
  clientsById = /* @__PURE__ */ new Map();
356
417
  clientIdBySocket = /* @__PURE__ */ new Map();
@@ -369,19 +430,80 @@ var SecureServer = class {
369
430
  heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
370
431
  roomMembersByName = /* @__PURE__ */ new Map();
371
432
  roomNamesByClientId = /* @__PURE__ */ new Map();
433
+ clientIpByClientId = /* @__PURE__ */ new Map();
434
+ rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
435
+ rateLimitBucketsByIp = /* @__PURE__ */ new Map();
372
436
  constructor(options) {
373
- const { heartbeat, ...socketServerOptions } = options;
437
+ const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
374
438
  this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
439
+ this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
375
440
  this.socketServer = new WebSocketServer(socketServerOptions);
376
441
  this.bindSocketServerEvents();
377
442
  this.startHeartbeatLoop();
443
+ if (adapter) {
444
+ void this.setAdapter(adapter).catch(() => {
445
+ return void 0;
446
+ });
447
+ }
378
448
  }
379
449
  get clientCount() {
380
450
  return this.clientsById.size;
381
451
  }
452
+ get serverId() {
453
+ return this.instanceId;
454
+ }
382
455
  get clients() {
383
456
  return this.clientsById;
384
457
  }
458
+ async setAdapter(adapter) {
459
+ const previousAdapter = this.adapter;
460
+ if (previousAdapter === adapter) {
461
+ return;
462
+ }
463
+ try {
464
+ if (previousAdapter?.detach) {
465
+ await Promise.resolve(previousAdapter.detach(this));
466
+ }
467
+ this.adapter = null;
468
+ if (!adapter) {
469
+ return;
470
+ }
471
+ await Promise.resolve(adapter.attach(this));
472
+ this.adapter = adapter;
473
+ } catch (error) {
474
+ const normalizedError = normalizeToError(
475
+ error,
476
+ "Failed to set SecureServer adapter."
477
+ );
478
+ this.notifyError(normalizedError);
479
+ throw normalizedError;
480
+ }
481
+ }
482
+ async handleAdapterMessage(message) {
483
+ try {
484
+ const normalizedMessage = normalizeSecureServerAdapterMessage(message);
485
+ if (normalizedMessage.originServerId === this.instanceId) {
486
+ return;
487
+ }
488
+ if (normalizedMessage.scope === "broadcast") {
489
+ this.emitLocally(normalizedMessage.event, normalizedMessage.data);
490
+ return;
491
+ }
492
+ if (!normalizedMessage.room) {
493
+ return;
494
+ }
495
+ this.emitToRoom(
496
+ normalizedMessage.room,
497
+ normalizedMessage.event,
498
+ normalizedMessage.data,
499
+ false
500
+ );
501
+ } catch (error) {
502
+ this.notifyError(
503
+ normalizeToError(error, "Failed to process SecureServer adapter message.")
504
+ );
505
+ }
506
+ }
385
507
  on(event, handler) {
386
508
  try {
387
509
  if (event === "connection") {
@@ -468,12 +590,12 @@ var SecureServer = class {
468
590
  if (isReservedEmitEvent(event)) {
469
591
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
470
592
  }
471
- const envelope = { event, data };
472
- for (const client of this.clientsById.values()) {
473
- void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
474
- return void 0;
475
- });
476
- }
593
+ this.emitLocally(event, data);
594
+ this.publishAdapterMessage({
595
+ scope: "broadcast",
596
+ event,
597
+ data
598
+ });
477
599
  } catch (error) {
478
600
  this.notifyError(normalizeToError(error, "Failed to emit server event."));
479
601
  }
@@ -530,7 +652,7 @@ var SecureServer = class {
530
652
  return {
531
653
  emit: (event, data) => {
532
654
  try {
533
- this.emitToRoom(normalizedRoom, event, data);
655
+ this.emitToRoom(normalizedRoom, event, data, true);
534
656
  } catch (error) {
535
657
  this.notifyError(
536
658
  normalizeToError(error, `Failed to emit event to room ${normalizedRoom}.`)
@@ -543,6 +665,15 @@ var SecureServer = class {
543
665
  close(code = DEFAULT_CLOSE_CODE, reason = DEFAULT_CLOSE_REASON) {
544
666
  try {
545
667
  this.stopHeartbeatLoop();
668
+ const activeAdapter = this.adapter;
669
+ this.adapter = null;
670
+ if (activeAdapter?.detach) {
671
+ void Promise.resolve(activeAdapter.detach(this)).catch((error) => {
672
+ this.notifyError(
673
+ normalizeToError(error, "Failed to detach SecureServer adapter during close.")
674
+ );
675
+ });
676
+ }
546
677
  for (const client of this.clientsById.values()) {
547
678
  this.rejectPendingRpcRequests(
548
679
  client.socket,
@@ -553,6 +684,9 @@ var SecureServer = class {
553
684
  client.socket.close(code, reason);
554
685
  }
555
686
  }
687
+ this.rateLimitBucketsByClientId.clear();
688
+ this.rateLimitBucketsByIp.clear();
689
+ this.clientIpByClientId.clear();
556
690
  this.socketServer.close();
557
691
  } catch (error) {
558
692
  this.notifyError(normalizeToError(error, "Failed to close server."));
@@ -573,6 +707,211 @@ var SecureServer = class {
573
707
  timeoutMs
574
708
  };
575
709
  }
710
+ resolveRateLimitConfig(rateLimitOptions) {
711
+ const windowMs = rateLimitOptions?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
712
+ const maxEventsPerConnection = rateLimitOptions?.maxEventsPerConnection ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION;
713
+ const maxEventsPerIp = rateLimitOptions?.maxEventsPerIp ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP;
714
+ const action = rateLimitOptions?.action ?? "throttle";
715
+ const throttleMs = rateLimitOptions?.throttleMs ?? DEFAULT_RATE_LIMIT_THROTTLE_MS;
716
+ const maxThrottleMs = rateLimitOptions?.maxThrottleMs ?? DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS;
717
+ const disconnectAfterViolations = rateLimitOptions?.disconnectAfterViolations ?? DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS;
718
+ const disconnectCode = rateLimitOptions?.disconnectCode ?? DEFAULT_RATE_LIMIT_CLOSE_CODE;
719
+ const disconnectReason = rateLimitOptions?.disconnectReason ?? DEFAULT_RATE_LIMIT_CLOSE_REASON;
720
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
721
+ throw new Error("Server rateLimit windowMs must be a positive number.");
722
+ }
723
+ if (!Number.isFinite(maxEventsPerConnection) || maxEventsPerConnection <= 0) {
724
+ throw new Error(
725
+ "Server rateLimit maxEventsPerConnection must be a positive number."
726
+ );
727
+ }
728
+ if (!Number.isFinite(maxEventsPerIp) || maxEventsPerIp <= 0) {
729
+ throw new Error("Server rateLimit maxEventsPerIp must be a positive number.");
730
+ }
731
+ if (action !== "throttle" && action !== "disconnect") {
732
+ throw new Error('Server rateLimit action must be either "throttle" or "disconnect".');
733
+ }
734
+ if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
735
+ throw new Error("Server rateLimit throttleMs must be a positive number.");
736
+ }
737
+ if (!Number.isFinite(maxThrottleMs) || maxThrottleMs <= 0) {
738
+ throw new Error("Server rateLimit maxThrottleMs must be a positive number.");
739
+ }
740
+ if (maxThrottleMs < throttleMs) {
741
+ throw new Error(
742
+ "Server rateLimit maxThrottleMs must be greater than or equal to throttleMs."
743
+ );
744
+ }
745
+ if (!Number.isInteger(disconnectAfterViolations) || disconnectAfterViolations <= 0) {
746
+ throw new Error(
747
+ "Server rateLimit disconnectAfterViolations must be a positive integer."
748
+ );
749
+ }
750
+ if (!Number.isInteger(disconnectCode) || disconnectCode < 1e3 || disconnectCode > 4999) {
751
+ throw new Error("Server rateLimit disconnectCode must be a valid WebSocket close code.");
752
+ }
753
+ return {
754
+ enabled: rateLimitOptions?.enabled ?? true,
755
+ windowMs,
756
+ maxEventsPerConnection,
757
+ maxEventsPerIp,
758
+ action,
759
+ throttleMs,
760
+ maxThrottleMs,
761
+ disconnectAfterViolations,
762
+ disconnectCode,
763
+ disconnectReason
764
+ };
765
+ }
766
+ createRateLimitBucket(now) {
767
+ return {
768
+ windowStartedAt: now,
769
+ count: 0,
770
+ violationCount: 0,
771
+ throttleUntil: 0,
772
+ lastSeenAt: now
773
+ };
774
+ }
775
+ getOrCreateRateLimitBucket(map, key, now) {
776
+ const existingBucket = map.get(key);
777
+ if (existingBucket) {
778
+ return existingBucket;
779
+ }
780
+ const bucket = this.createRateLimitBucket(now);
781
+ map.set(key, bucket);
782
+ return bucket;
783
+ }
784
+ updateRateLimitBucket(bucket, now) {
785
+ if (now - bucket.windowStartedAt >= this.rateLimitConfig.windowMs) {
786
+ bucket.windowStartedAt = now;
787
+ bucket.count = 0;
788
+ bucket.violationCount = 0;
789
+ bucket.throttleUntil = 0;
790
+ }
791
+ bucket.count += 1;
792
+ bucket.lastSeenAt = now;
793
+ }
794
+ pruneRateLimitBucketMap(map, now, maxIdleMs) {
795
+ for (const [key, bucket] of map.entries()) {
796
+ if (now - bucket.lastSeenAt >= maxIdleMs) {
797
+ map.delete(key);
798
+ }
799
+ }
800
+ }
801
+ pruneRateLimitBuckets(now) {
802
+ const maxIdleMs = this.rateLimitConfig.windowMs * 4;
803
+ this.pruneRateLimitBucketMap(this.rateLimitBucketsByClientId, now, maxIdleMs);
804
+ this.pruneRateLimitBucketMap(this.rateLimitBucketsByIp, now, maxIdleMs);
805
+ }
806
+ normalizeIpAddress(ipAddress) {
807
+ let normalized = ipAddress.trim().toLowerCase();
808
+ if (normalized.startsWith("::ffff:")) {
809
+ normalized = normalized.slice(7);
810
+ }
811
+ if (normalized.startsWith("[") && normalized.endsWith("]")) {
812
+ normalized = normalized.slice(1, -1);
813
+ }
814
+ const zoneIndex = normalized.indexOf("%");
815
+ if (zoneIndex >= 0) {
816
+ normalized = normalized.slice(0, zoneIndex);
817
+ }
818
+ return normalized.length > 0 ? normalized : "unknown";
819
+ }
820
+ resolveClientIp(request) {
821
+ const forwardedHeader = request.headers["x-forwarded-for"];
822
+ const forwardedValue = Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader;
823
+ if (typeof forwardedValue === "string") {
824
+ const firstForwardedIp = forwardedValue.split(",").map((item) => item.trim()).find((item) => item.length > 0);
825
+ if (firstForwardedIp) {
826
+ return this.normalizeIpAddress(firstForwardedIp);
827
+ }
828
+ }
829
+ return this.normalizeIpAddress(request.socket.remoteAddress ?? "unknown");
830
+ }
831
+ isIpStillConnected(ipAddress) {
832
+ for (const connectedIp of this.clientIpByClientId.values()) {
833
+ if (connectedIp === ipAddress) {
834
+ return true;
835
+ }
836
+ }
837
+ return false;
838
+ }
839
+ evaluateIncomingRateLimit(client) {
840
+ const noLimitDecision = {
841
+ shouldDisconnect: false,
842
+ shouldDrop: false,
843
+ throttleDelayMs: 0
844
+ };
845
+ if (!this.rateLimitConfig.enabled) {
846
+ return noLimitDecision;
847
+ }
848
+ const now = Date.now();
849
+ const clientBucket = this.getOrCreateRateLimitBucket(
850
+ this.rateLimitBucketsByClientId,
851
+ client.id,
852
+ now
853
+ );
854
+ this.updateRateLimitBucket(clientBucket, now);
855
+ const clientIp = this.clientIpByClientId.get(client.id);
856
+ const ipBucket = clientIp ? this.getOrCreateRateLimitBucket(this.rateLimitBucketsByIp, clientIp, now) : null;
857
+ if (ipBucket) {
858
+ this.updateRateLimitBucket(ipBucket, now);
859
+ }
860
+ const activeThrottleUntil = Math.max(
861
+ clientBucket.throttleUntil,
862
+ ipBucket?.throttleUntil ?? 0
863
+ );
864
+ if (activeThrottleUntil > now) {
865
+ return {
866
+ shouldDisconnect: false,
867
+ shouldDrop: true,
868
+ throttleDelayMs: 0
869
+ };
870
+ }
871
+ const isConnectionLimitExceeded = clientBucket.count > this.rateLimitConfig.maxEventsPerConnection;
872
+ const isIpLimitExceeded = ipBucket ? ipBucket.count > this.rateLimitConfig.maxEventsPerIp : false;
873
+ if (!isConnectionLimitExceeded && !isIpLimitExceeded) {
874
+ if (this.rateLimitBucketsByClientId.size > 1024 || this.rateLimitBucketsByIp.size > 1024) {
875
+ this.pruneRateLimitBuckets(now);
876
+ }
877
+ return noLimitDecision;
878
+ }
879
+ if (isConnectionLimitExceeded) {
880
+ clientBucket.violationCount += 1;
881
+ }
882
+ if (ipBucket && isIpLimitExceeded) {
883
+ ipBucket.violationCount += 1;
884
+ }
885
+ const violationCount = Math.max(
886
+ clientBucket.violationCount,
887
+ ipBucket?.violationCount ?? 0
888
+ );
889
+ const shouldDisconnect = this.rateLimitConfig.action === "disconnect" || violationCount >= this.rateLimitConfig.disconnectAfterViolations;
890
+ if (shouldDisconnect) {
891
+ return {
892
+ shouldDisconnect: true,
893
+ shouldDrop: true,
894
+ throttleDelayMs: 0
895
+ };
896
+ }
897
+ const throttleDelayMs = Math.min(
898
+ this.rateLimitConfig.maxThrottleMs,
899
+ Math.max(
900
+ this.rateLimitConfig.throttleMs,
901
+ this.rateLimitConfig.throttleMs * violationCount
902
+ )
903
+ );
904
+ const throttleUntil = now + throttleDelayMs;
905
+ clientBucket.throttleUntil = throttleUntil;
906
+ if (ipBucket) {
907
+ ipBucket.throttleUntil = throttleUntil;
908
+ }
909
+ return {
910
+ shouldDisconnect: false,
911
+ shouldDrop: false,
912
+ throttleDelayMs
913
+ };
914
+ }
576
915
  startHeartbeatLoop() {
577
916
  if (!this.heartbeatConfig.enabled || this.heartbeatIntervalHandle) {
578
917
  return;
@@ -675,6 +1014,8 @@ var SecureServer = class {
675
1014
  try {
676
1015
  const clientId = randomUUID();
677
1016
  const handshakeState = this.createServerHandshakeState();
1017
+ const clientIp = this.resolveClientIp(request);
1018
+ connectionMetadata.set("network.ip", clientIp);
678
1019
  const client = this.createSecureServerClient(
679
1020
  clientId,
680
1021
  socket,
@@ -683,6 +1024,7 @@ var SecureServer = class {
683
1024
  );
684
1025
  this.clientsById.set(clientId, client);
685
1026
  this.clientIdBySocket.set(socket, clientId);
1027
+ this.clientIpByClientId.set(clientId, clientIp);
686
1028
  this.handshakeStateBySocket.set(socket, handshakeState);
687
1029
  this.pendingPayloadsBySocket.set(socket, []);
688
1030
  this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
@@ -717,6 +1059,35 @@ var SecureServer = class {
717
1059
  }
718
1060
  async handleIncomingMessage(client, rawData) {
719
1061
  try {
1062
+ const rateLimitDecision = this.evaluateIncomingRateLimit(client);
1063
+ if (rateLimitDecision.shouldDisconnect) {
1064
+ this.notifyError(
1065
+ new Error(
1066
+ `Rate limit disconnect triggered for client ${client.id}.`
1067
+ )
1068
+ );
1069
+ if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
1070
+ client.socket.close(
1071
+ this.rateLimitConfig.disconnectCode,
1072
+ this.rateLimitConfig.disconnectReason
1073
+ );
1074
+ }
1075
+ return;
1076
+ }
1077
+ if (rateLimitDecision.shouldDrop) {
1078
+ return;
1079
+ }
1080
+ if (rateLimitDecision.throttleDelayMs > 0) {
1081
+ this.notifyError(
1082
+ new Error(
1083
+ `Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
1084
+ )
1085
+ );
1086
+ await delay(rateLimitDecision.throttleDelayMs);
1087
+ if (client.socket.readyState !== WebSocket.OPEN) {
1088
+ return;
1089
+ }
1090
+ }
720
1091
  let envelope = null;
721
1092
  try {
722
1093
  envelope = parseEnvelope(rawData);
@@ -778,6 +1149,12 @@ var SecureServer = class {
778
1149
  client.leaveAll();
779
1150
  this.clientsById.delete(client.id);
780
1151
  this.clientIdBySocket.delete(client.socket);
1152
+ const disconnectedIp = this.clientIpByClientId.get(client.id);
1153
+ this.clientIpByClientId.delete(client.id);
1154
+ this.rateLimitBucketsByClientId.delete(client.id);
1155
+ if (disconnectedIp && !this.isIpStillConnected(disconnectedIp)) {
1156
+ this.rateLimitBucketsByIp.delete(disconnectedIp);
1157
+ }
781
1158
  this.handshakeStateBySocket.delete(client.socket);
782
1159
  this.sharedSecretBySocket.delete(client.socket);
783
1160
  this.encryptionKeyBySocket.delete(client.socket);
@@ -1165,6 +1542,48 @@ var SecureServer = class {
1165
1542
  leaveAll: () => this.leaveClientFromAllRooms(clientId)
1166
1543
  };
1167
1544
  }
1545
+ emitLocally(event, data) {
1546
+ const envelope = { event, data };
1547
+ for (const client of this.clientsById.values()) {
1548
+ void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1549
+ return void 0;
1550
+ });
1551
+ }
1552
+ }
1553
+ publishAdapterMessage(message) {
1554
+ if (!this.adapter) {
1555
+ return;
1556
+ }
1557
+ let adapterMessage;
1558
+ if (message.scope === "room") {
1559
+ if (!message.room) {
1560
+ return;
1561
+ }
1562
+ adapterMessage = {
1563
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
1564
+ originServerId: this.instanceId,
1565
+ scope: "room",
1566
+ event: message.event,
1567
+ data: message.data,
1568
+ emittedAt: Date.now(),
1569
+ room: message.room
1570
+ };
1571
+ } else {
1572
+ adapterMessage = {
1573
+ version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
1574
+ originServerId: this.instanceId,
1575
+ scope: "broadcast",
1576
+ event: message.event,
1577
+ data: message.data,
1578
+ emittedAt: Date.now()
1579
+ };
1580
+ }
1581
+ void Promise.resolve(this.adapter.publish(adapterMessage)).catch((error) => {
1582
+ this.notifyError(
1583
+ normalizeToError(error, "Failed to publish SecureServer adapter message.")
1584
+ );
1585
+ });
1586
+ }
1168
1587
  normalizeRoomName(room) {
1169
1588
  if (typeof room !== "string") {
1170
1589
  throw new Error("Room name must be a string.");
@@ -1230,22 +1649,29 @@ var SecureServer = class {
1230
1649
  }
1231
1650
  return roomNames.length;
1232
1651
  }
1233
- emitToRoom(room, event, data) {
1652
+ emitToRoom(room, event, data, replicate) {
1234
1653
  if (isReservedEmitEvent(event)) {
1235
1654
  throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
1236
1655
  }
1237
1656
  const roomMembers = this.roomMembersByName.get(room);
1238
- if (!roomMembers || roomMembers.size === 0) {
1239
- return;
1240
- }
1241
- const envelope = { event, data };
1242
- for (const clientId of roomMembers) {
1243
- const client = this.clientsById.get(clientId);
1244
- if (!client) {
1245
- continue;
1657
+ if (roomMembers && roomMembers.size > 0) {
1658
+ const envelope = { event, data };
1659
+ for (const clientId of roomMembers) {
1660
+ const client = this.clientsById.get(clientId);
1661
+ if (!client) {
1662
+ continue;
1663
+ }
1664
+ void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1665
+ return void 0;
1666
+ });
1246
1667
  }
1247
- void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
1248
- return void 0;
1668
+ }
1669
+ if (replicate) {
1670
+ this.publishAdapterMessage({
1671
+ scope: "room",
1672
+ room,
1673
+ event,
1674
+ data
1249
1675
  });
1250
1676
  }
1251
1677
  }
@@ -1848,6 +2274,6 @@ var SecureClient = class {
1848
2274
  }
1849
2275
  };
1850
2276
 
1851
- export { SecureClient, SecureServer };
2277
+ export { SecureClient, SecureServer, normalizeSecureServerAdapterMessage };
1852
2278
  //# sourceMappingURL=index.js.map
1853
2279
  //# sourceMappingURL=index.js.map