@rlanz/socket 0.0.1-2 → 0.0.1-4

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 CHANGED
@@ -23,26 +23,39 @@ Create a socket config file and tune the WebSocket path or heartbeat.
23
23
 
24
24
  ```ts
25
25
  // config/socket.ts
26
- import { defineConfig } from '@adonisjs/core/config'
26
+ import { defineConfig } from '@rlanz/socket'
27
27
 
28
28
  export default defineConfig({
29
29
  websocket: {
30
30
  path: '/socket',
31
- pingInterval: 25_000,
32
- pingTimeout: 5_000,
31
+ pingInterval: '25s',
32
+ pingTimeout: '5s',
33
+ maxBufferedAmount: 16 * 1024 * 1024,
33
34
  },
34
35
  })
35
36
  ```
36
37
 
37
38
  - `websocket.path` defaults to `/socket`.
38
- - `websocket.pingInterval` sends a WebSocket ping every configured number of milliseconds.
39
+ - `websocket.middleware` runs AdonisJS HTTP middleware for the WebSocket upgrade request.
40
+ - `websocket.pingInterval` sends a WebSocket ping every configured duration.
39
41
  - `websocket.pingTimeout` closes connections that do not answer in time.
40
42
  - If only one heartbeat value is configured, the other one uses the default shown above.
43
+ - `websocket.maxPayload` limits inbound message payloads; it defaults to 1 MiB.
44
+ - `websocket.maxQueuedMessages` limits unresolved non-ping protocol messages per socket; it defaults
45
+ to `100`.
46
+ - `websocket.maxMessagesPerInterval` limits inbound protocol messages per
47
+ `websocket.messageRateInterval`; they default to `1000` messages per `1s`.
48
+ - `websocket.maxBufferedAmount` closes slow outbound sockets whose WebSocket buffer is already above
49
+ the configured byte limit before an event is sent; it defaults to 16 MiB.
50
+
51
+ `websocket.middleware` is different from channel middleware. Use it to prepare request-scoped
52
+ services such as sessions or auth during the initial HTTP upgrade; use `static middlewares` on a
53
+ channel class to authorize or enrich individual channel subscriptions.
41
54
 
42
55
  ### Authentication
43
56
 
44
- Authenticate sockets during the HTTP upgrade with `websocket.authenticate`. The hook receives the
45
- upgrade request, the parsed client auth payload, and an AdonisJS HTTP context.
57
+ Authenticate sockets during the HTTP upgrade with `websocket.authenticate`. The hook receives an
58
+ AdonisJS HTTP context and returns the authenticated user.
46
59
 
47
60
  Define `websocket.middleware` when your WebSocket upgrade authentication depends on AdonisJS
48
61
  middleware such as sessions or auth initialization. These middleware run only for the HTTP upgrade
@@ -54,34 +67,34 @@ channel itself through `static middlewares`.
54
67
 
55
68
  ```ts
56
69
  // config/socket.ts
57
- import { defineConfig } from '@adonisjs/core/config'
70
+ import type { HttpContext } from '@adonisjs/core/http'
71
+ import { authenticateWithAdonisAuth, defineConfig } from '@rlanz/socket'
58
72
 
59
- export default defineConfig({
73
+ type User = { id: string | number; name: string }
74
+
75
+ export default defineConfig<User, HttpContext>({
60
76
  websocket: {
61
77
  middleware: [
62
78
  () => import('@adonisjs/session/session_middleware'),
63
79
  () => import('@adonisjs/auth/initialize_auth_middleware'),
64
80
  ],
65
81
 
66
- async authenticate({ auth, httpContext }) {
67
- await httpContext!.auth.authenticateUsing()
68
- const user = httpContext!.auth.getUserOrFail()
69
-
70
- return {
71
- user,
72
- auth,
73
- data: {
74
- connectedAt: new Date(),
75
- },
76
- }
77
- },
82
+ authenticate: authenticateWithAdonisAuth<User, HttpContext>(),
78
83
  },
79
84
  })
80
85
  ```
81
86
 
82
- Return `false`, `null`, or throw to reject the upgrade with `401 Unauthorized`. The returned `user`
83
- is available as `socket.user`, `data` is stored on `socket.raw.data`, and the HTTP context remains
84
- available as `socket.raw.httpContext`.
87
+ Return `false`, `null`, or throw to reject the upgrade with `401 Unauthorized`. The returned user is
88
+ available as `socket.user`, and the HTTP context remains available as `socket.raw.httpContext`.
89
+
90
+ Override `websocket.authenticate` when you need custom behavior:
91
+
92
+ ```ts
93
+ async authenticate({ httpContext }) {
94
+ await httpContext!.auth.authenticateUsing()
95
+ return httpContext!.auth.getUserOrFail()
96
+ }
97
+ ```
85
98
 
86
99
  ### Horizontal Sync
87
100
 
@@ -89,8 +102,8 @@ Configure an [`@boringnode/bus`](https://github.com/boringnode/bus) transport to
89
102
 
90
103
  ```ts
91
104
  // config/socket.ts
92
- import { defineConfig } from '@adonisjs/core/config'
93
105
  import { redis } from '@boringnode/bus/transports/redis'
106
+ import { defineConfig } from '@rlanz/socket'
94
107
 
95
108
  export default defineConfig({
96
109
  transport: {
@@ -99,14 +112,14 @@ export default defineConfig({
99
112
  port: 6379,
100
113
  }),
101
114
  channel: 'socket::broadcast',
102
- presenceTimeout: 100,
115
+ presenceTimeout: '100ms',
103
116
  },
104
117
  })
105
118
  ```
106
119
 
107
120
  When configured, `socket.to(channel).emit(...)`, `socket.to(channel).except(socketId).emit(...)`, `socket.toUser(userId).emit(...)`, and `socket.broadcast(...)` are delivered locally and published to the bus so other instances can deliver them to their own connected sockets.
108
121
 
109
- The default bus channel is `socket::broadcast`. Presence channels also use the bus to build distributed snapshots during subscribe, duplicate subscribe, join, and leave updates. `presenceTimeout` controls how long an instance waits for other instances to answer a presence snapshot request; it defaults to `100` milliseconds.
122
+ The default bus channel is `socket::broadcast`. Presence channels also use the bus to build distributed snapshots during subscribe, duplicate subscribe, join, and leave updates. `presenceTimeout` controls how long an instance waits for other instances to answer a presence snapshot request; it defaults to `100ms`.
110
123
 
111
124
  ### Health Checks
112
125
 
@@ -337,7 +350,6 @@ import { Socket } from '@rlanz/socket/client'
337
350
  const socket = new Socket({
338
351
  url: 'http://localhost:3333',
339
352
  path: '/socket',
340
- auth: { token: 'secret' },
341
353
  autoReconnect: true,
342
354
  reconnectDelay: 250,
343
355
  reconnectMaxDelay: 5000,
@@ -399,7 +411,11 @@ Set `autoReconnect: false` to disable reconnect attempts.
399
411
  import { BaseChannel, ChannelRouter, PresenceManager, SocketService } from '@rlanz/socket'
400
412
  import type { AuthenticatedSocket, SocketConfig } from '@rlanz/socket/types'
401
413
  import SocketProvider from '@rlanz/socket/provider'
414
+ import socket from '@rlanz/socket/services/main'
402
415
  import { onMessage } from '@rlanz/socket/decorators'
416
+ import { SocketHealthCheck } from '@rlanz/socket/health_check'
417
+ import { SocketInstrumentation } from '@rlanz/socket/otel'
418
+ import { SocketFake } from '@rlanz/socket/testing'
403
419
  import { Socket, Channel } from '@rlanz/socket/client'
404
420
  import type { SocketOptions, PresenceData } from '@rlanz/socket/client/types'
405
421
  ```
@@ -408,7 +424,12 @@ Available exports:
408
424
 
409
425
  - `@rlanz/socket`
410
426
  - `@rlanz/socket/provider`
427
+ - `@rlanz/socket/services/main`
411
428
  - `@rlanz/socket/decorators`
429
+ - `@rlanz/socket/health_check`
430
+ - `@rlanz/socket/otel`
431
+ - `@rlanz/socket/testing`
412
432
  - `@rlanz/socket/types`
433
+ - `@rlanz/socket/types/tracing_channels`
413
434
  - `@rlanz/socket/client`
414
435
  - `@rlanz/socket/client/types`
@@ -797,22 +797,37 @@ function isClientMessageType(value) {
797
797
  // src/message_queue.ts
798
798
  var MessageQueue = class {
799
799
  #queues = /* @__PURE__ */ new Map();
800
- enqueue(key, task) {
800
+ #depths = /* @__PURE__ */ new Map();
801
+ enqueue(key, task, options = {}) {
802
+ const depth = this.#depths.get(key) ?? 0;
803
+ if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
804
+ return false;
805
+ }
806
+ this.#depths.set(key, depth + 1);
801
807
  const previous = this.#queues.get(key) ?? Promise.resolve();
802
808
  const next = previous.catch(() => {
803
809
  }).then(task).catch(() => {
804
810
  }).finally(() => {
811
+ const nextDepth = (this.#depths.get(key) ?? 1) - 1;
812
+ if (nextDepth <= 0) {
813
+ this.#depths.delete(key);
814
+ } else {
815
+ this.#depths.set(key, nextDepth);
816
+ }
805
817
  if (this.#queues.get(key) === next) {
806
818
  this.#queues.delete(key);
807
819
  }
808
820
  });
809
821
  this.#queues.set(key, next);
822
+ return true;
810
823
  }
811
824
  delete(key) {
812
825
  this.#queues.delete(key);
826
+ this.#depths.delete(key);
813
827
  }
814
828
  clear() {
815
829
  this.#queues.clear();
830
+ this.#depths.clear();
816
831
  }
817
832
  };
818
833
 
@@ -824,13 +839,28 @@ function userRoom(userId) {
824
839
  // src/socket_bus.ts
825
840
  import { randomUUID } from "crypto";
826
841
  import { Bus } from "@boringnode/bus";
842
+
843
+ // src/duration.ts
844
+ import { parse as parseDurationExpression } from "@lukeed/ms";
845
+ function parseDuration(name, duration) {
846
+ if (duration === void 0) {
847
+ return void 0;
848
+ }
849
+ const milliseconds = typeof duration === "number" ? duration : parseDurationExpression(duration);
850
+ if (typeof milliseconds === "undefined" || !Number.isFinite(milliseconds) || milliseconds <= 0) {
851
+ throw new Error(`${name} must be a positive duration`);
852
+ }
853
+ return milliseconds;
854
+ }
855
+
856
+ // src/socket_bus.ts
827
857
  var DEFAULT_CHANNEL = "socket::broadcast";
828
858
  var DEFAULT_PRESENCE_TIMEOUT = 100;
829
859
  var SocketBus = class {
830
860
  constructor(transport, handlers) {
831
861
  this.handlers = handlers;
832
862
  this.#channel = transport.channel ?? DEFAULT_CHANNEL;
833
- this.#presenceTimeout = transport.presenceTimeout ?? DEFAULT_PRESENCE_TIMEOUT;
863
+ this.#presenceTimeout = parseDuration("transport.presenceTimeout", transport.presenceTimeout) ?? DEFAULT_PRESENCE_TIMEOUT;
834
864
  this.#bus = new Bus(transport.driver(), { retryQueue: { enabled: true } });
835
865
  }
836
866
  #origin = randomUUID();
@@ -999,23 +1029,20 @@ var SocketUpgrader = class _SocketUpgrader {
999
1029
  return url.pathname === this.#path;
1000
1030
  }
1001
1031
  async #authenticate(request) {
1002
- const auth = this.#parseAuth(request);
1003
1032
  const config = this.config;
1004
1033
  if (!config) {
1005
- return { auth };
1034
+ return {};
1006
1035
  }
1007
1036
  if (!config.authenticate && !config.createContextOnUpgrade) {
1008
- return { auth };
1037
+ return {};
1009
1038
  }
1010
1039
  let httpContext;
1011
1040
  try {
1012
1041
  httpContext = await config.createContext?.(request);
1013
1042
  if (!config.authenticate) {
1014
- return { auth, httpContext };
1043
+ return { httpContext };
1015
1044
  }
1016
1045
  const ctx = {
1017
- request,
1018
- auth,
1019
1046
  httpContext
1020
1047
  };
1021
1048
  const result = await config.authenticate(ctx);
@@ -1023,51 +1050,62 @@ var SocketUpgrader = class _SocketUpgrader {
1023
1050
  return null;
1024
1051
  }
1025
1052
  return {
1026
- auth: result.auth ?? auth,
1027
- data: result.data,
1028
1053
  httpContext,
1029
- user: result.user
1054
+ user: result
1030
1055
  };
1031
1056
  } catch {
1032
1057
  return null;
1033
1058
  }
1034
1059
  }
1035
- #parseAuth(request) {
1036
- const url = new URL(request.url ?? "/", "ws://localhost");
1037
- const auth = url.searchParams.get("auth");
1038
- if (!auth) {
1039
- return void 0;
1040
- }
1041
- try {
1042
- const parsed = JSON.parse(auth);
1043
- return parsed && typeof parsed === "object" ? parsed : void 0;
1044
- } catch {
1045
- return void 0;
1046
- }
1047
- }
1048
1060
  };
1049
1061
 
1050
1062
  // src/socket_service.ts
1051
1063
  var DEFAULT_PING_INTERVAL = 25e3;
1052
1064
  var DEFAULT_PING_TIMEOUT = 5e3;
1065
+ var DEFAULT_MAX_PAYLOAD = 1024 * 1024;
1066
+ var DEFAULT_MAX_QUEUED_MESSAGES = 100;
1067
+ var DEFAULT_MAX_MESSAGES_PER_INTERVAL = 1e3;
1068
+ var DEFAULT_MESSAGE_RATE_INTERVAL = 1e3;
1069
+ var DEFAULT_MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024;
1070
+ var CLOSE_POLICY_VIOLATION = 1008;
1071
+ var CLOSE_MESSAGE_TOO_BIG = 1009;
1072
+ var CLOSE_OUTBOUND_BUFFER_LIMIT = "Socket outbound buffer limit exceeded";
1073
+ function serializeFrame(frame) {
1074
+ return JSON.stringify(frame);
1075
+ }
1076
+ function sendSerializedFrame(connection, serializedFrame) {
1077
+ if (connection.readyState !== WebSocket.OPEN) {
1078
+ return false;
1079
+ }
1080
+ connection.send(serializedFrame);
1081
+ return true;
1082
+ }
1053
1083
  function sendFrame(connection, frame) {
1084
+ return sendSerializedFrame(connection, serializeFrame(frame));
1085
+ }
1086
+ function sendSerializedFrameWithBackpressure(connection, serializedFrame, config) {
1054
1087
  if (connection.readyState !== WebSocket.OPEN) {
1055
- return;
1088
+ return false;
1056
1089
  }
1057
- connection.send(JSON.stringify(frame));
1090
+ if (connection.bufferedAmount > config.maxBufferedAmount) {
1091
+ closeConnection(connection, CLOSE_POLICY_VIOLATION, CLOSE_OUTBOUND_BUFFER_LIMIT);
1092
+ return false;
1093
+ }
1094
+ connection.send(serializedFrame);
1095
+ return true;
1058
1096
  }
1059
- function validateDuration(name, value) {
1097
+ function parsePositiveInteger(name, value, fallback) {
1060
1098
  if (value === void 0) {
1061
- return void 0;
1099
+ return fallback;
1062
1100
  }
1063
- if (!Number.isFinite(value) || value <= 0) {
1064
- throw new Error(`websocket.${name} must be a positive number of milliseconds`);
1101
+ if (!Number.isSafeInteger(value) || value <= 0) {
1102
+ throw new Error(`${name} must be a positive integer`);
1065
1103
  }
1066
1104
  return value;
1067
1105
  }
1068
1106
  function resolveHeartbeatConfig(config) {
1069
- const pingInterval = validateDuration("pingInterval", config?.pingInterval);
1070
- const pingTimeout = validateDuration("pingTimeout", config?.pingTimeout);
1107
+ const pingInterval = parseDuration("websocket.pingInterval", config?.pingInterval);
1108
+ const pingTimeout = parseDuration("websocket.pingTimeout", config?.pingTimeout);
1071
1109
  if (pingInterval === void 0 && pingTimeout === void 0) {
1072
1110
  return null;
1073
1111
  }
@@ -1076,6 +1114,47 @@ function resolveHeartbeatConfig(config) {
1076
1114
  timeout: pingTimeout ?? DEFAULT_PING_TIMEOUT
1077
1115
  };
1078
1116
  }
1117
+ function resolveInboundMessageConfig(config) {
1118
+ const messageRateInterval = parseDuration("websocket.messageRateInterval", config?.messageRateInterval) ?? DEFAULT_MESSAGE_RATE_INTERVAL;
1119
+ return {
1120
+ maxPayload: parsePositiveInteger(
1121
+ "websocket.maxPayload",
1122
+ config?.maxPayload,
1123
+ DEFAULT_MAX_PAYLOAD
1124
+ ),
1125
+ maxQueuedMessages: parsePositiveInteger(
1126
+ "websocket.maxQueuedMessages",
1127
+ config?.maxQueuedMessages,
1128
+ DEFAULT_MAX_QUEUED_MESSAGES
1129
+ ),
1130
+ maxMessagesPerInterval: parsePositiveInteger(
1131
+ "websocket.maxMessagesPerInterval",
1132
+ config?.maxMessagesPerInterval,
1133
+ DEFAULT_MAX_MESSAGES_PER_INTERVAL
1134
+ ),
1135
+ messageRateInterval
1136
+ };
1137
+ }
1138
+ function resolveOutboundMessageConfig(config) {
1139
+ return {
1140
+ maxBufferedAmount: parsePositiveInteger(
1141
+ "websocket.maxBufferedAmount",
1142
+ config?.maxBufferedAmount,
1143
+ DEFAULT_MAX_BUFFERED_AMOUNT
1144
+ )
1145
+ };
1146
+ }
1147
+ function getRawDataByteLength(data) {
1148
+ if (Array.isArray(data)) {
1149
+ return data.reduce((total, chunk) => total + chunk.byteLength, 0);
1150
+ }
1151
+ return data.byteLength;
1152
+ }
1153
+ function closeConnection(connection, code, reason) {
1154
+ if (connection.readyState === WebSocket.OPEN) {
1155
+ connection.close(code, reason);
1156
+ }
1157
+ }
1079
1158
  var SocketService = class extends Emittery {
1080
1159
  #server = null;
1081
1160
  #httpServer = null;
@@ -1098,6 +1177,8 @@ var SocketService = class extends Emittery {
1098
1177
  #heartbeatInterval = null;
1099
1178
  #heartbeatTimeouts = /* @__PURE__ */ new Map();
1100
1179
  #messageQueue = new MessageQueue();
1180
+ #messageRates = /* @__PURE__ */ new Map();
1181
+ #outboundMessageConfig = resolveOutboundMessageConfig(void 0);
1101
1182
  /**
1102
1183
  * Accesses the underlying ws server.
1103
1184
  */
@@ -1123,6 +1204,8 @@ var SocketService = class extends Emittery {
1123
1204
  );
1124
1205
  const websocketConfig = config.websocket;
1125
1206
  const heartbeatConfig = resolveHeartbeatConfig(websocketConfig);
1207
+ const inboundMessageConfig = resolveInboundMessageConfig(websocketConfig);
1208
+ this.#outboundMessageConfig = resolveOutboundMessageConfig(websocketConfig);
1126
1209
  try {
1127
1210
  if (config.transport) {
1128
1211
  this.#bus = new SocketBus(config.transport, {
@@ -1163,14 +1246,15 @@ var SocketService = class extends Emittery {
1163
1246
  this.#configurePresenceFetcher();
1164
1247
  }
1165
1248
  const websocketServer = new WebSocketServer({
1166
- noServer: true
1249
+ noServer: true,
1250
+ maxPayload: inboundMessageConfig.maxPayload
1167
1251
  });
1168
1252
  this.#server = websocketServer;
1169
1253
  this.#httpServer = httpServer;
1170
1254
  const upgrader = new SocketUpgrader(websocketServer, websocketConfig);
1171
1255
  this.#upgradeHandler = (request, socket, head) => {
1172
1256
  upgrader.handle(request, socket, head, (connection, upgradeRequest, accepted) => {
1173
- this.#handleConnection(connection, upgradeRequest, accepted);
1257
+ this.#handleConnection(connection, upgradeRequest, accepted, inboundMessageConfig);
1174
1258
  }).catch((error) => {
1175
1259
  logger.warn("failed to upgrade socket connection: %s", error);
1176
1260
  SocketUpgrader.reject(socket, 500, "Internal Server Error");
@@ -1186,7 +1270,7 @@ var SocketService = class extends Emittery {
1186
1270
  throw error;
1187
1271
  }
1188
1272
  }
1189
- #handleConnection(connection, request, upgrade) {
1273
+ #handleConnection(connection, request, upgrade, inboundMessageConfig) {
1190
1274
  const socket = this.#wrapSocket(connection, request, upgrade);
1191
1275
  connectChannel.traceSync(
1192
1276
  () => {
@@ -1195,20 +1279,37 @@ var SocketService = class extends Emittery {
1195
1279
  },
1196
1280
  { socketId: socket.id }
1197
1281
  );
1282
+ connection.on("error", () => {
1283
+ });
1198
1284
  connection.on("message", (data) => {
1285
+ if (getRawDataByteLength(data) > inboundMessageConfig.maxPayload) {
1286
+ closeConnection(connection, CLOSE_MESSAGE_TOO_BIG, "Message too big");
1287
+ return;
1288
+ }
1289
+ if (!this.#acceptMessageRate(socket.id, inboundMessageConfig)) {
1290
+ closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message rate limit exceeded");
1291
+ return;
1292
+ }
1199
1293
  const message = Message.fromTransport(data.toString());
1200
1294
  if (message.type === "ping") {
1201
1295
  sendFrame(socket.raw.connection, { id: message.id, type: "pong" });
1202
1296
  return;
1203
1297
  }
1204
- this.#messageQueue.enqueue(socket.id, async () => {
1205
- await this.#handleMessage(socket, message).catch((error) => {
1206
- sendFrame(connection, {
1207
- type: "error",
1208
- error: error instanceof Error ? error.message : "Unexpected socket error"
1298
+ const enqueued = this.#messageQueue.enqueue(
1299
+ socket.id,
1300
+ async () => {
1301
+ await this.#handleMessage(socket, message).catch((error) => {
1302
+ sendFrame(connection, {
1303
+ type: "error",
1304
+ error: error instanceof Error ? error.message : "Unexpected socket error"
1305
+ });
1209
1306
  });
1210
- });
1211
- });
1307
+ },
1308
+ { maxDepth: inboundMessageConfig.maxQueuedMessages }
1309
+ );
1310
+ if (!enqueued) {
1311
+ closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message queue limit exceeded");
1312
+ }
1212
1313
  });
1213
1314
  connection.on("pong", () => {
1214
1315
  this.#clearHeartbeatTimeout(socket.id);
@@ -1222,6 +1323,7 @@ var SocketService = class extends Emittery {
1222
1323
  await disconnectChannel.tracePromise(async () => {
1223
1324
  this.#clearHeartbeatTimeout(socket.id);
1224
1325
  this.#messageQueue.delete(socket.id);
1326
+ this.#messageRates.delete(socket.id);
1225
1327
  await this.#channelSubscriptions?.leaveAll(socket);
1226
1328
  this.#channelSubscriptions?.deleteSocket(socket.id);
1227
1329
  this.#sockets.delete(socket.id);
@@ -1306,8 +1408,7 @@ var SocketService = class extends Emittery {
1306
1408
  #wrapSocket(connection, request, upgrade) {
1307
1409
  const raw = {
1308
1410
  id: randomUUID2(),
1309
- data: upgrade.data ?? {},
1310
- auth: upgrade.auth,
1411
+ data: {},
1311
1412
  connection,
1312
1413
  request,
1313
1414
  httpContext: upgrade.httpContext
@@ -1316,8 +1417,8 @@ var SocketService = class extends Emittery {
1316
1417
  id: raw.id,
1317
1418
  user: upgrade.user,
1318
1419
  channels: /* @__PURE__ */ new Set(),
1319
- emit(event, data) {
1320
- sendFrame(connection, { type: "event", event, data });
1420
+ emit: (event, data) => {
1421
+ this.#sendEventFrame(connection, { type: "event", event, data });
1321
1422
  },
1322
1423
  joinUserRoom: (userId) => {
1323
1424
  this.#joinUserRoom(raw.id, userRoom(userId));
@@ -1424,15 +1525,25 @@ var SocketService = class extends Emittery {
1424
1525
  message.delivered = deliver();
1425
1526
  }, message);
1426
1527
  }
1528
+ #sendEventFrame(connection, frame) {
1529
+ return this.#sendSerializedEventFrame(connection, serializeFrame(frame));
1530
+ }
1531
+ #sendSerializedEventFrame(connection, serializedFrame) {
1532
+ return sendSerializedFrameWithBackpressure(
1533
+ connection,
1534
+ serializedFrame,
1535
+ this.#outboundMessageConfig
1536
+ );
1537
+ }
1427
1538
  #emitToChannelLocally(channel, event, data, except = []) {
1428
1539
  let delivered = 0;
1540
+ const serializedFrame = serializeFrame({ type: "event", channel, event, data });
1429
1541
  for (const socketId of this.#channelSubscriptions?.getSocketIds(channel) ?? []) {
1430
1542
  if (except.includes(socketId)) {
1431
1543
  continue;
1432
1544
  }
1433
1545
  const socket = this.#sockets.get(socketId);
1434
- if (socket) {
1435
- sendFrame(socket.raw.connection, { type: "event", channel, event, data });
1546
+ if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
1436
1547
  delivered += 1;
1437
1548
  }
1438
1549
  }
@@ -1440,10 +1551,10 @@ var SocketService = class extends Emittery {
1440
1551
  }
1441
1552
  #emitToUserRoomLocally(room, event, data) {
1442
1553
  let delivered = 0;
1554
+ const serializedFrame = serializeFrame({ type: "event", event, data });
1443
1555
  for (const socketId of this.#userRooms.get(room) ?? []) {
1444
1556
  const socket = this.#sockets.get(socketId);
1445
- if (socket) {
1446
- sendFrame(socket.raw.connection, { type: "event", event, data });
1557
+ if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
1447
1558
  delivered += 1;
1448
1559
  }
1449
1560
  }
@@ -1451,9 +1562,11 @@ var SocketService = class extends Emittery {
1451
1562
  }
1452
1563
  #broadcastLocally(event, data) {
1453
1564
  let delivered = 0;
1565
+ const serializedFrame = serializeFrame({ type: "event", event, data });
1454
1566
  for (const socket of this.#sockets.values()) {
1455
- sendFrame(socket.raw.connection, { type: "event", event, data });
1456
- delivered += 1;
1567
+ if (this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
1568
+ delivered += 1;
1569
+ }
1457
1570
  }
1458
1571
  return delivered;
1459
1572
  }
@@ -1576,6 +1689,19 @@ var SocketService = class extends Emittery {
1576
1689
  }
1577
1690
  this.#heartbeatTimeouts.clear();
1578
1691
  }
1692
+ #acceptMessageRate(socketId, config) {
1693
+ const now = Date.now();
1694
+ const rate = this.#messageRates.get(socketId);
1695
+ if (!rate || now - rate.intervalStartedAt >= config.messageRateInterval) {
1696
+ this.#messageRates.set(socketId, { count: 1, intervalStartedAt: now });
1697
+ return true;
1698
+ }
1699
+ if (rate.count >= config.maxMessagesPerInterval) {
1700
+ return false;
1701
+ }
1702
+ rate.count += 1;
1703
+ return true;
1704
+ }
1579
1705
  #cleanup() {
1580
1706
  this.#stopHeartbeat();
1581
1707
  for (const socket of this.#sockets.values()) {
@@ -1585,6 +1711,7 @@ var SocketService = class extends Emittery {
1585
1711
  this.#userRooms.clear();
1586
1712
  this.#socketUserRoom.clear();
1587
1713
  this.#messageQueue.clear();
1714
+ this.#messageRates.clear();
1588
1715
  this.#channelSubscriptions?.clear();
1589
1716
  }
1590
1717
  async close() {
@@ -1791,4 +1918,4 @@ export {
1791
1918
  SocketService,
1792
1919
  ChannelRouter
1793
1920
  };
1794
- //# sourceMappingURL=chunk-NRWRLZAO.js.map
1921
+ //# sourceMappingURL=chunk-ILDK672E.js.map