@rlanz/socket 0.0.1-1 → 0.0.1-10

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.
Files changed (67) hide show
  1. package/README.md +572 -81
  2. package/build/base_channel-DKmR3Z0l.d.ts +476 -0
  3. package/build/chunk-55EIQ2VV.js +152 -0
  4. package/build/chunk-55EIQ2VV.js.map +1 -0
  5. package/build/chunk-6PJ4ALJD.js +115 -0
  6. package/build/chunk-6PJ4ALJD.js.map +1 -0
  7. package/build/chunk-BLRAKPXW.js +7 -0
  8. package/build/chunk-BLRAKPXW.js.map +1 -0
  9. package/build/chunk-E6ILCRL3.js +182 -0
  10. package/build/chunk-E6ILCRL3.js.map +1 -0
  11. package/build/{chunk-XUDFDJME.js → chunk-G75J233Z.js} +12 -9
  12. package/build/chunk-G75J233Z.js.map +1 -0
  13. package/build/chunk-IUQ3GYZZ.js +140 -0
  14. package/build/chunk-IUQ3GYZZ.js.map +1 -0
  15. package/build/chunk-K243S3QP.js +131 -0
  16. package/build/chunk-K243S3QP.js.map +1 -0
  17. package/build/chunk-NUPBNOP3.js +61 -0
  18. package/build/chunk-NUPBNOP3.js.map +1 -0
  19. package/build/chunk-O6UYCGTV.js +914 -0
  20. package/build/chunk-O6UYCGTV.js.map +1 -0
  21. package/build/framework-16ypv4Ss.d.ts +13 -0
  22. package/build/index-8jB5RG6v.d.ts +150 -0
  23. package/build/index.d.ts +29 -8
  24. package/build/index.js +39 -15
  25. package/build/index.js.map +1 -1
  26. package/build/providers/socket_provider.d.ts +6 -3
  27. package/build/providers/socket_provider.js +2156 -45
  28. package/build/providers/socket_provider.js.map +1 -1
  29. package/build/services/socket.d.ts +6 -3
  30. package/build/{shared_types-Dw9AphfO.d.ts → shared_types-DE86M136.d.ts} +5 -4
  31. package/build/socket_service-C1JM64_9.d.ts +106 -0
  32. package/build/src/assembler_hook.d.ts +15 -0
  33. package/build/src/assembler_hook.js +238 -0
  34. package/build/src/assembler_hook.js.map +1 -0
  35. package/build/src/client/index.d.ts +3 -118
  36. package/build/src/client/index.js +6 -696
  37. package/build/src/client/index.js.map +1 -1
  38. package/build/src/client/react.d.ts +30 -0
  39. package/build/src/client/react.js +86 -0
  40. package/build/src/client/react.js.map +1 -0
  41. package/build/src/client/types.d.ts +62 -9
  42. package/build/src/client/vue.d.ts +32 -0
  43. package/build/src/client/vue.js +100 -0
  44. package/build/src/client/vue.js.map +1 -0
  45. package/build/src/decorators.d.ts +7 -4
  46. package/build/src/decorators.js +1 -1
  47. package/build/src/health_check.d.ts +6 -3
  48. package/build/src/health_check.js +14 -3
  49. package/build/src/health_check.js.map +1 -1
  50. package/build/src/otel.d.ts +1 -0
  51. package/build/src/otel.js +187 -32
  52. package/build/src/otel.js.map +1 -1
  53. package/build/src/testing.d.ts +45 -0
  54. package/build/src/testing.js +7 -0
  55. package/build/src/testing.js.map +1 -0
  56. package/build/src/types/tracing_channels.d.ts +1 -2
  57. package/build/src/types.d.ts +4 -2
  58. package/package.json +36 -11
  59. package/build/chunk-5X4SKZQS.js +0 -1787
  60. package/build/chunk-5X4SKZQS.js.map +0 -1
  61. package/build/chunk-GKAD2UOA.js +0 -18
  62. package/build/chunk-GKAD2UOA.js.map +0 -1
  63. package/build/chunk-HK7Z65DA.js +0 -19
  64. package/build/chunk-HK7Z65DA.js.map +0 -1
  65. package/build/chunk-XUDFDJME.js.map +0 -1
  66. package/build/socket_service-jxIaH5Rs.d.ts +0 -144
  67. package/build/types-C0rDwbry.d.ts +0 -393
@@ -1,13 +1,2084 @@
1
1
  import {
2
- ChannelRouter,
3
- PresenceManager,
4
- SocketService
5
- } from "../chunk-5X4SKZQS.js";
6
- import "../chunk-XUDFDJME.js";
7
- import "../chunk-GKAD2UOA.js";
2
+ SERVER_DISCONNECT_CODE
3
+ } from "../chunk-BLRAKPXW.js";
4
+ import {
5
+ BaseChannel,
6
+ SocketResponseError
7
+ } from "../chunk-K243S3QP.js";
8
+ import {
9
+ ChannelPatternSyntax
10
+ } from "../chunk-E6ILCRL3.js";
11
+ import "../chunk-G75J233Z.js";
12
+ import {
13
+ broadcastChannel,
14
+ channelMessageChannel,
15
+ connectChannel,
16
+ disconnectChannel,
17
+ subscribeChannel,
18
+ unsubscribeChannel
19
+ } from "../chunk-NUPBNOP3.js";
20
+ import {
21
+ SocketFake
22
+ } from "../chunk-IUQ3GYZZ.js";
23
+
24
+ // providers/socket_provider.ts
25
+ import { ServerResponse } from "http";
26
+
27
+ // src/socket_service.ts
28
+ import { randomUUID as randomUUID2 } from "crypto";
29
+ import Emittery from "emittery";
30
+ import { WebSocket, WebSocketServer } from "ws";
31
+
32
+ // src/channel_subscription_storage.ts
33
+ var ChannelSubscriptionStorage = class {
34
+ #subscriptions = /* @__PURE__ */ new Map();
35
+ #subscribers = /* @__PURE__ */ new Map();
36
+ has(socketId, channelName) {
37
+ return this.#subscriptions.get(socketId)?.has(channelName) ?? false;
38
+ }
39
+ get(socketId, channelName) {
40
+ return this.#subscriptions.get(socketId)?.get(channelName);
41
+ }
42
+ set(socketId, channelName, subscription) {
43
+ if (!this.#subscriptions.has(socketId)) {
44
+ this.#subscriptions.set(socketId, /* @__PURE__ */ new Map());
45
+ }
46
+ this.#subscriptions.get(socketId).set(channelName, subscription);
47
+ if (!this.#subscribers.has(channelName)) {
48
+ this.#subscribers.set(channelName, /* @__PURE__ */ new Set());
49
+ }
50
+ this.#subscribers.get(channelName).add(socketId);
51
+ }
52
+ delete(socketId, channelName) {
53
+ const subscriptions = this.#subscriptions.get(socketId);
54
+ if (!subscriptions) {
55
+ return;
56
+ }
57
+ subscriptions.delete(channelName);
58
+ if (subscriptions.size === 0) {
59
+ this.#subscriptions.delete(socketId);
60
+ }
61
+ const subscribers = this.#subscribers.get(channelName);
62
+ subscribers?.delete(socketId);
63
+ if (subscribers?.size === 0) {
64
+ this.#subscribers.delete(channelName);
65
+ }
66
+ }
67
+ deleteSocket(socketId) {
68
+ for (const channelName of this.channelNamesFor(socketId)) {
69
+ this.delete(socketId, channelName);
70
+ }
71
+ }
72
+ channelNamesFor(socketId) {
73
+ return [...this.#subscriptions.get(socketId)?.keys() ?? []];
74
+ }
75
+ countForSocket(socketId) {
76
+ return this.#subscriptions.get(socketId)?.size ?? 0;
77
+ }
78
+ socketIdsFor(channelName) {
79
+ return [...this.#subscribers.get(channelName) ?? []];
80
+ }
81
+ get channelsCount() {
82
+ return this.#subscribers.size;
83
+ }
84
+ clear() {
85
+ this.#subscriptions.clear();
86
+ this.#subscribers.clear();
87
+ }
88
+ };
89
+
90
+ // src/channel_subscriptions.ts
91
+ var ChannelSubscriptions = class _ChannelSubscriptions {
92
+ constructor(socketService, channelRouter, logger, presenceManager, limits) {
93
+ this.socketService = socketService;
94
+ this.channelRouter = channelRouter;
95
+ this.logger = logger;
96
+ this.presenceManager = presenceManager;
97
+ this.limits = limits;
98
+ }
99
+ #storage = new ChannelSubscriptionStorage();
100
+ #channelOperations = /* @__PURE__ */ new Map();
101
+ static #serializePresenceData(presenceData) {
102
+ return {
103
+ ...presenceData,
104
+ users: presenceData.users.map((user) => ({
105
+ ...user,
106
+ joinedAt: user.joinedAt.toISOString()
107
+ }))
108
+ };
109
+ }
110
+ setPresenceManager(presenceManager) {
111
+ this.presenceManager = presenceManager;
112
+ }
113
+ async subscribe(socket, channelName) {
114
+ return this.#subscribe(socket, channelName);
115
+ }
116
+ async #subscribe(socket, channelName) {
117
+ const existing = await this.#refreshExistingSubscription(socket, channelName);
118
+ if (existing) {
119
+ return existing;
120
+ }
121
+ if (this.limits && channelName.length > this.limits.maxChannelNameLength) {
122
+ return {
123
+ ack: { ok: false, error: "Channel name is too long" },
124
+ created: false
125
+ };
126
+ }
127
+ if (this.limits && this.#storage.channelNamesFor(socket.id).length >= this.limits.maxSubscriptionsPerSocket) {
128
+ return {
129
+ ack: { ok: false, error: "Socket subscription limit exceeded" },
130
+ created: false
131
+ };
132
+ }
133
+ const result = await this.channelRouter.authorize(socket, channelName);
134
+ if (!result.success) {
135
+ if ("cause" in result && !(result.cause instanceof SocketResponseError)) {
136
+ this.#warn(`failed to authorize channel "${channelName}": %s`, result.cause);
137
+ }
138
+ return {
139
+ ack: { ok: false, error: result.error },
140
+ created: false
141
+ };
142
+ }
143
+ if (this.#usesPresence(result.instance)) {
144
+ return this.#withChannelOperation(`${socket.id}\0${channelName}`, async () => {
145
+ const concurrentSubscription = await this.#refreshExistingSubscription(socket, channelName);
146
+ if (concurrentSubscription) {
147
+ return concurrentSubscription;
148
+ }
149
+ return this.#completeSubscription(socket, channelName, result);
150
+ });
151
+ }
152
+ return this.#completeSubscription(socket, channelName, result);
153
+ }
154
+ async #completeSubscription(socket, channelName, result) {
155
+ result.instance.$setContext(
156
+ this.socketService,
157
+ channelName,
158
+ result.params,
159
+ this.presenceManager
160
+ );
161
+ let presenceData = result.presenceData;
162
+ let presenceSnapshot = null;
163
+ let presenceJoined = false;
164
+ let serializationError;
165
+ try {
166
+ if (this.#usesPresence(result.instance)) {
167
+ const presenceInfo = result.instance.$getPresenceInfo(socket);
168
+ const joinedUser = this.presenceManager.join(channelName, socket.raw, presenceInfo);
169
+ const { joinedAt: _joinedAt, ...member } = joinedUser;
170
+ presenceJoined = true;
171
+ presenceSnapshot = await this.presenceManager.snapshot(channelName);
172
+ presenceData = presenceSnapshot;
173
+ const visibleUser = presenceSnapshot.users.find((user) => user.id === presenceInfo.id);
174
+ const wasPresent = Boolean(visibleUser && visibleUser !== joinedUser);
175
+ try {
176
+ JSON.stringify(presenceData);
177
+ } catch (error) {
178
+ serializationError = error;
179
+ throw error;
180
+ }
181
+ if (!wasPresent) {
182
+ await result.instance.onMemberJoin?.(socket, member, ...result.paramValues);
183
+ }
184
+ await result.instance.onJoin?.(socket, ...result.paramValues);
185
+ } else {
186
+ try {
187
+ JSON.stringify(presenceData);
188
+ } catch (error) {
189
+ serializationError = error;
190
+ throw error;
191
+ }
192
+ await result.instance.onJoin?.(socket, ...result.paramValues);
193
+ }
194
+ this.#storage.set(socket.id, channelName, {
195
+ channelName,
196
+ instance: result.instance,
197
+ paramValues: result.paramValues
198
+ });
199
+ if (presenceSnapshot) {
200
+ this.socketService.to(channelName).except(socket.id).emit("presence:update", _ChannelSubscriptions.#serializePresenceData(presenceSnapshot));
201
+ }
202
+ } catch (error) {
203
+ if (presenceJoined) {
204
+ this.presenceManager.leave(channelName, socket.raw);
205
+ const rollbackData = await this.presenceManager.snapshot(channelName).catch(() => null);
206
+ if (rollbackData) {
207
+ this.socketService.to(channelName).emit("presence:update", _ChannelSubscriptions.#serializePresenceData(rollbackData));
208
+ }
209
+ }
210
+ this.#storage.delete(socket.id, channelName);
211
+ if (serializationError) {
212
+ this.#warn(
213
+ `failed to serialize subscription response for channel "${channelName}": %s`,
214
+ serializationError
215
+ );
216
+ } else if (!(error instanceof SocketResponseError)) {
217
+ this.#warn(`failed to join channel "${channelName}": %s`, error);
218
+ }
219
+ return {
220
+ ack: {
221
+ ok: false,
222
+ error: serializationError ? "Subscription response is not serializable" : error instanceof SocketResponseError ? error.message : "Join failed"
223
+ },
224
+ created: false
225
+ };
226
+ }
227
+ return {
228
+ ack: { ok: true, presenceData },
229
+ created: true
230
+ };
231
+ }
232
+ async leave(socket, channelName) {
233
+ const subscription = this.#storage.get(socket.id, channelName);
234
+ if (!subscription) {
235
+ return false;
236
+ }
237
+ this.#storage.delete(socket.id, channelName);
238
+ if (this.#usesPresence(subscription.instance)) {
239
+ await this.#withChannelOperation(
240
+ `${socket.id}\0${channelName}`,
241
+ () => this.#cleanupAfterLeave(socket, channelName, subscription)
242
+ );
243
+ } else {
244
+ await this.#cleanupAfterLeave(socket, channelName, subscription);
245
+ }
246
+ return true;
247
+ }
248
+ async #cleanupAfterLeave(socket, channelName, subscription) {
249
+ if (this.#usesPresence(subscription.instance)) {
250
+ const presenceUser = this.presenceManager.getLocalUser(channelName, socket.id);
251
+ this.presenceManager.leave(channelName, socket.raw);
252
+ try {
253
+ const presenceData = await this.presenceManager.snapshot(channelName);
254
+ this.socketService.to(channelName).emit("presence:update", _ChannelSubscriptions.#serializePresenceData(presenceData));
255
+ if (presenceUser && !presenceData.users.some((user) => user.id === presenceUser.id)) {
256
+ const { joinedAt: _joinedAt, ...member } = presenceUser;
257
+ await subscription.instance.onMemberLeave?.(socket, member, ...subscription.paramValues);
258
+ }
259
+ } catch (error) {
260
+ this.#warn(`failed to update presence while leaving channel "${channelName}": %s`, error);
261
+ }
262
+ }
263
+ try {
264
+ await subscription.instance.onLeave?.(socket, ...subscription.paramValues);
265
+ } catch (error) {
266
+ this.#warn(`failed to leave channel "${channelName}": %s`, error);
267
+ }
268
+ }
269
+ async leaveAll(socket) {
270
+ for (const channelName of this.#storage.channelNamesFor(socket.id)) {
271
+ await this.leave(socket, channelName);
272
+ }
273
+ }
274
+ async handleMessage(socket, payload) {
275
+ const subscription = this.#storage.get(socket.id, payload.channel);
276
+ if (!subscription) {
277
+ return { ok: false, error: "Not subscribed" };
278
+ }
279
+ try {
280
+ const result = await subscription.instance.$handleMessage(socket, payload.event, payload.data);
281
+ try {
282
+ JSON.stringify(result);
283
+ } catch (error) {
284
+ this.#warn(
285
+ `failed to serialize handler response for channel "${payload.channel}": %s`,
286
+ error
287
+ );
288
+ return { ok: false, error: "Handler response is not serializable" };
289
+ }
290
+ return { ok: true, data: result };
291
+ } catch (error) {
292
+ if (!(error instanceof SocketResponseError)) {
293
+ this.#warn(`channel handler failed for "${payload.channel}": %s`, error);
294
+ }
295
+ return {
296
+ ok: false,
297
+ error: error instanceof SocketResponseError ? error.message : "Handler error"
298
+ };
299
+ }
300
+ }
301
+ relayWhisper(socket, payload) {
302
+ if (!this.#storage.has(socket.id, payload.channel)) {
303
+ return { ok: false, error: "Not subscribed" };
304
+ }
305
+ this.socketService.to(payload.channel).except(socket.id).emit(`client:${payload.event}`, payload.data);
306
+ return { ok: true };
307
+ }
308
+ deleteSocket(socketId) {
309
+ this.#storage.deleteSocket(socketId);
310
+ }
311
+ clear() {
312
+ this.#storage.clear();
313
+ this.#channelOperations.clear();
314
+ }
315
+ getSocketIds(channelName) {
316
+ return this.#storage.socketIdsFor(channelName);
317
+ }
318
+ subscriptionCountFor(socketId) {
319
+ return this.#storage.countForSocket(socketId);
320
+ }
321
+ get channelsCount() {
322
+ return this.#storage.channelsCount;
323
+ }
324
+ #warn(message, error) {
325
+ try {
326
+ this.logger.warn(message, error);
327
+ } catch {
328
+ }
329
+ }
330
+ async #withChannelOperation(operationKey, operation) {
331
+ const previous = this.#channelOperations.get(operationKey) ?? Promise.resolve();
332
+ const release = Promise.withResolvers();
333
+ const queued = previous.then(() => release.promise);
334
+ this.#channelOperations.set(operationKey, queued);
335
+ await previous;
336
+ try {
337
+ return await operation();
338
+ } finally {
339
+ release.resolve();
340
+ if (this.#channelOperations.get(operationKey) === queued) {
341
+ this.#channelOperations.delete(operationKey);
342
+ }
343
+ }
344
+ }
345
+ #usesPresence(instance) {
346
+ return Boolean(
347
+ instance.constructor.options?.presence && this.presenceManager
348
+ );
349
+ }
350
+ async #refreshExistingSubscription(socket, channelName) {
351
+ if (!this.#storage.has(socket.id, channelName)) {
352
+ return null;
353
+ }
354
+ let presenceData;
355
+ try {
356
+ presenceData = await this.#getPresenceData(socket, channelName);
357
+ } catch (error) {
358
+ this.#warn(`failed to refresh subscription for channel "${channelName}": %s`, error);
359
+ return {
360
+ ack: { ok: false, error: "Subscribe failed" },
361
+ created: false
362
+ };
363
+ }
364
+ try {
365
+ JSON.stringify(presenceData);
366
+ } catch (error) {
367
+ this.#warn(
368
+ `failed to serialize subscription response for channel "${channelName}": %s`,
369
+ error
370
+ );
371
+ return {
372
+ ack: { ok: false, error: "Subscription response is not serializable" },
373
+ created: false
374
+ };
375
+ }
376
+ return {
377
+ ack: {
378
+ ok: true,
379
+ presenceData
380
+ },
381
+ created: false
382
+ };
383
+ }
384
+ async #getPresenceData(socket, channelName) {
385
+ if (!this.presenceManager?.hasLocal(channelName, socket.id)) {
386
+ return void 0;
387
+ }
388
+ return this.presenceManager.snapshot(channelName);
389
+ }
390
+ };
391
+
392
+ // src/protocol/reply.ts
393
+ var INVALID_SOCKET_MESSAGE = "Invalid socket message";
394
+ var Reply = class _Reply {
395
+ constructor(id, ok, data, error) {
396
+ this.id = id;
397
+ this.ok = ok;
398
+ this.data = data;
399
+ this.error = error;
400
+ }
401
+ static ok(id, data) {
402
+ return new _Reply(id, true, data, void 0);
403
+ }
404
+ static error(id, error) {
405
+ return new _Reply(id, false, void 0, error);
406
+ }
407
+ static invalidMessage(id) {
408
+ if (id === void 0) {
409
+ return { type: "error", error: INVALID_SOCKET_MESSAGE };
410
+ }
411
+ return _Reply.error(id, INVALID_SOCKET_MESSAGE).toFrame();
412
+ }
413
+ static fromSubscribeResult(id, result) {
414
+ if (!result) {
415
+ return _Reply.error(id, "Socket service is not initialized");
416
+ }
417
+ if (!result.ack.ok) {
418
+ return _Reply.error(id, result.ack.error ?? "Subscribe failed");
419
+ }
420
+ const data = result.ack.presenceData ? {
421
+ presenceData: result.ack.presenceData
422
+ } : void 0;
423
+ return _Reply.ok(id, data);
424
+ }
425
+ static fromChannelAck(id, ack) {
426
+ return ack.ok ? _Reply.ok(id, ack.data) : _Reply.error(id, ack.error ?? "Handler error");
427
+ }
428
+ toFrame() {
429
+ const frame = {
430
+ id: this.id,
431
+ type: "ack",
432
+ ok: this.ok
433
+ };
434
+ if (this.data !== void 0) {
435
+ frame.data = this.data;
436
+ }
437
+ if (this.error !== void 0) {
438
+ frame.error = this.error;
439
+ }
440
+ return frame;
441
+ }
442
+ };
443
+
444
+ // src/protocol/message.ts
445
+ function isRecord(value) {
446
+ return typeof value === "object" && value !== null && !Array.isArray(value);
447
+ }
448
+ var Message = class _Message {
449
+ static fromTransport(raw) {
450
+ try {
451
+ return _Message.fromParsedTransport(JSON.parse(raw));
452
+ } catch {
453
+ return new InvalidMessage(void 0);
454
+ }
455
+ }
456
+ static fromParsedTransport(value) {
457
+ if (!isRecord(value)) {
458
+ return new InvalidMessage(void 0);
459
+ }
460
+ const id = typeof value.id === "string" ? value.id : void 0;
461
+ if ("id" in value && typeof value.id !== "string") {
462
+ return new InvalidMessage(void 0);
463
+ }
464
+ if (!isClientMessageType(value.type)) {
465
+ return new InvalidMessage(id);
466
+ }
467
+ if (value.type === "ping") {
468
+ return new PingMessage(id);
469
+ }
470
+ if (value.type === "subscribe" && typeof value.channel === "string") {
471
+ return new SubscribeMessage(id, value.channel);
472
+ }
473
+ if (value.type === "unsubscribe" && typeof value.channel === "string") {
474
+ return new UnsubscribeMessage(id, value.channel);
475
+ }
476
+ if ((value.type === "message" || value.type === "whisper") && typeof value.channel === "string" && typeof value.event === "string") {
477
+ return new ChannelProtocolMessage(id, value.type, value.channel, value.event, value.data);
478
+ }
479
+ return new InvalidMessage(id);
480
+ }
481
+ };
482
+ var InvalidMessage = class extends Message {
483
+ constructor(id) {
484
+ super();
485
+ this.id = id;
486
+ }
487
+ valid = false;
488
+ type = "invalid";
489
+ toRejectionFrame() {
490
+ return Reply.invalidMessage(this.id);
491
+ }
492
+ };
493
+ var PingMessage = class extends Message {
494
+ constructor(id) {
495
+ super();
496
+ this.id = id;
497
+ }
498
+ valid = true;
499
+ type = "ping";
500
+ };
501
+ var SubscribeMessage = class extends Message {
502
+ constructor(id, channel) {
503
+ super();
504
+ this.id = id;
505
+ this.channel = channel;
506
+ }
507
+ valid = true;
508
+ type = "subscribe";
509
+ };
510
+ var UnsubscribeMessage = class extends Message {
511
+ constructor(id, channel) {
512
+ super();
513
+ this.id = id;
514
+ this.channel = channel;
515
+ }
516
+ valid = true;
517
+ type = "unsubscribe";
518
+ };
519
+ var ChannelProtocolMessage = class extends Message {
520
+ constructor(id, type, channel, event, data) {
521
+ super();
522
+ this.id = id;
523
+ this.type = type;
524
+ this.channel = channel;
525
+ this.event = event;
526
+ this.data = data;
527
+ }
528
+ valid = true;
529
+ toChannelMessage() {
530
+ return {
531
+ channel: this.channel,
532
+ event: this.event,
533
+ data: this.data
534
+ };
535
+ }
536
+ };
537
+ function isClientMessageType(value) {
538
+ return value === "ping" || value === "subscribe" || value === "unsubscribe" || value === "message" || value === "whisper";
539
+ }
540
+
541
+ // src/message_queue.ts
542
+ var MessageQueue = class {
543
+ #queues = /* @__PURE__ */ new Map();
544
+ #depths = /* @__PURE__ */ new Map();
545
+ enqueue(key, task, options = {}) {
546
+ const depth = this.#depths.get(key) ?? 0;
547
+ if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
548
+ return false;
549
+ }
550
+ this.#depths.set(key, depth + 1);
551
+ const previous = this.#queues.get(key) ?? Promise.resolve();
552
+ const next = previous.catch(() => {
553
+ }).then(task).catch(() => {
554
+ }).finally(() => {
555
+ const nextDepth = (this.#depths.get(key) ?? 1) - 1;
556
+ if (nextDepth <= 0) {
557
+ this.#depths.delete(key);
558
+ } else {
559
+ this.#depths.set(key, nextDepth);
560
+ }
561
+ if (this.#queues.get(key) === next) {
562
+ this.#queues.delete(key);
563
+ }
564
+ });
565
+ this.#queues.set(key, next);
566
+ return true;
567
+ }
568
+ drain(key) {
569
+ return this.#queues.get(key) ?? Promise.resolve();
570
+ }
571
+ delete(key) {
572
+ this.#queues.delete(key);
573
+ this.#depths.delete(key);
574
+ }
575
+ clear() {
576
+ this.#queues.clear();
577
+ this.#depths.clear();
578
+ }
579
+ };
580
+
581
+ // src/socket_bus.ts
582
+ import { randomUUID } from "crypto";
583
+ import { Bus } from "@boringnode/bus";
584
+
585
+ // src/presence_manager.ts
586
+ var PRESENCE_DATA_KEY = "__rlanzSocketPresence";
587
+ var PresenceManager = class {
588
+ #presence = /* @__PURE__ */ new Map();
589
+ #fetchSockets = null;
590
+ /**
591
+ * Sets the socket fetcher used to build distributed snapshots.
592
+ */
593
+ setSocketFetcher(fetchSockets) {
594
+ this.#fetchSockets = fetchSockets;
595
+ }
596
+ /**
597
+ * Adds a user to a channel.
598
+ */
599
+ join(channel, socket, member) {
600
+ const user = {
601
+ ...member.data,
602
+ id: member.id,
603
+ joinedAt: /* @__PURE__ */ new Date()
604
+ };
605
+ const users = this.#presence.get(channel) ?? /* @__PURE__ */ new Map();
606
+ users.set(socket.id, user);
607
+ this.#presence.set(channel, users);
608
+ this.#setSocketPresence(socket, channel, user);
609
+ return user;
610
+ }
611
+ /**
612
+ * Removes a user from a channel.
613
+ */
614
+ leave(channel, socket) {
615
+ const channelPresence = this.#presence.get(channel);
616
+ if (channelPresence) {
617
+ channelPresence.delete(socket.id);
618
+ if (channelPresence.size === 0) {
619
+ this.#presence.delete(channel);
620
+ }
621
+ }
622
+ this.#deleteSocketPresence(socket, channel);
623
+ }
624
+ /**
625
+ * Removes a user from every channel.
626
+ * Returns the list of channels the user left.
627
+ */
628
+ leaveAll(socket) {
629
+ const leftChannels = [];
630
+ for (const [channel, users] of this.#presence) {
631
+ if (users.has(socket.id)) {
632
+ users.delete(socket.id);
633
+ leftChannels.push(channel);
634
+ if (users.size === 0) {
635
+ this.#presence.delete(channel);
636
+ }
637
+ this.#deleteSocketPresence(socket, channel);
638
+ }
639
+ }
640
+ return leftChannels;
641
+ }
642
+ /**
643
+ * Gets presence data for a channel.
644
+ */
645
+ async snapshot(channel) {
646
+ if (this.#fetchSockets) {
647
+ return this.#getFromSockets(channel);
648
+ }
649
+ return this.#localSnapshot(channel);
650
+ }
651
+ /**
652
+ * Gets the number of users in a channel.
653
+ */
654
+ async count(channel) {
655
+ return (await this.snapshot(channel)).count;
656
+ }
657
+ /**
658
+ * Checks whether a socket is locally present in a channel.
659
+ */
660
+ hasLocal(channel, socketId) {
661
+ return this.#presence.get(channel)?.has(socketId) || false;
662
+ }
663
+ /**
664
+ * Gets the presence user retained for one local socket.
665
+ */
666
+ getLocalUser(channel, socketId) {
667
+ return this.#presence.get(channel)?.get(socketId);
668
+ }
669
+ /**
670
+ * Lists every local channel where a socket is present.
671
+ */
672
+ getLocalChannelsForSocket(socketId) {
673
+ const channels = [];
674
+ for (const [channel, users] of this.#presence) {
675
+ if (users.has(socketId)) {
676
+ channels.push(channel);
677
+ }
678
+ }
679
+ return channels;
680
+ }
681
+ /**
682
+ * Lists sockets that are locally present in a channel.
683
+ */
684
+ getLocalSockets(channel) {
685
+ const users = this.#presence.get(channel);
686
+ if (!users) {
687
+ return [];
688
+ }
689
+ return Array.from(users.entries()).map(([socketId, user]) => ({
690
+ id: socketId,
691
+ data: {
692
+ [PRESENCE_DATA_KEY]: {
693
+ [channel]: user
694
+ }
695
+ }
696
+ }));
697
+ }
698
+ #localSnapshot(channel) {
699
+ return this.#createSnapshot(channel, Array.from(this.#presence.get(channel)?.entries() ?? []));
700
+ }
701
+ async #getFromSockets(channel) {
702
+ const sockets = await this.#fetchSockets(channel);
703
+ const users = sockets.flatMap((socket) => {
704
+ const user = socket.data?.[PRESENCE_DATA_KEY]?.[channel];
705
+ return user ? [[socket.id, user]] : [];
706
+ });
707
+ return this.#createSnapshot(channel, users);
708
+ }
709
+ #createSnapshot(channel, connections) {
710
+ const usersById = /* @__PURE__ */ new Map();
711
+ for (const connection of connections) {
712
+ const current = usersById.get(connection[1].id);
713
+ if (!current || this.#compareConnections(connection, current) < 0) {
714
+ usersById.set(connection[1].id, connection);
715
+ }
716
+ }
717
+ const users = [...usersById.values()].sort((left, right) => this.#compareConnections(left, right)).map(([, user]) => user);
718
+ return {
719
+ channel,
720
+ users,
721
+ count: users.length
722
+ };
723
+ }
724
+ #compareConnections([leftSocketId, leftUser], [rightSocketId, rightUser]) {
725
+ const joinedAtDifference = leftUser.joinedAt.getTime() - rightUser.joinedAt.getTime();
726
+ return joinedAtDifference || leftSocketId.localeCompare(rightSocketId);
727
+ }
728
+ #setSocketPresence(socket, channel, user) {
729
+ socket.data[PRESENCE_DATA_KEY] ??= {};
730
+ socket.data[PRESENCE_DATA_KEY][channel] = user;
731
+ }
732
+ #deleteSocketPresence(socket, channel) {
733
+ if (!socket.data[PRESENCE_DATA_KEY]) {
734
+ return;
735
+ }
736
+ delete socket.data[PRESENCE_DATA_KEY][channel];
737
+ if (Object.keys(socket.data[PRESENCE_DATA_KEY]).length === 0) {
738
+ delete socket.data[PRESENCE_DATA_KEY];
739
+ }
740
+ }
741
+ };
742
+
743
+ // src/presence_socket_frame.ts
744
+ var PresenceSocketFrame = class _PresenceSocketFrame {
745
+ constructor(transportValue, socketValue) {
746
+ this.transportValue = transportValue;
747
+ this.socketValue = socketValue;
748
+ }
749
+ /**
750
+ * Create one transport frame from a local socket presence entry.
751
+ */
752
+ static fromSocket(channel, socket) {
753
+ const user = socket.data[PRESENCE_DATA_KEY]?.[channel];
754
+ if (!user || Number.isNaN(user.joinedAt.getTime())) {
755
+ throw new Error(`Socket "${socket.id}" has no valid presence for channel "${channel}"`);
756
+ }
757
+ const transport = {
758
+ id: socket.id,
759
+ data: {
760
+ [PRESENCE_DATA_KEY]: {
761
+ [channel]: {
762
+ // Keep custom fields. Convert only the presence timestamp.
763
+ ...user,
764
+ joinedAt: user.joinedAt.toISOString()
765
+ }
766
+ }
767
+ }
768
+ };
769
+ return new _PresenceSocketFrame(transport, socket);
770
+ }
771
+ /**
772
+ * Validate one transport value and restore its presence timestamps.
773
+ */
774
+ static fromTransport(value) {
775
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
776
+ return null;
777
+ }
778
+ const transport = value;
779
+ if (typeof transport.id !== "string" || !transport.data || typeof transport.data !== "object" || Array.isArray(transport.data)) {
780
+ return null;
781
+ }
782
+ const data = transport.data;
783
+ const presence = data[PRESENCE_DATA_KEY];
784
+ if (presence === void 0) {
785
+ return new _PresenceSocketFrame(transport, {
786
+ id: transport.id,
787
+ data: { ...data }
788
+ });
789
+ }
790
+ if (!presence || typeof presence !== "object" || Array.isArray(presence)) {
791
+ return null;
792
+ }
793
+ const restoredPresence = {};
794
+ for (const [channel, candidate] of Object.entries(presence)) {
795
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
796
+ return null;
797
+ }
798
+ const user = candidate;
799
+ if (typeof user.id !== "string" || typeof user.joinedAt !== "string" || Number.isNaN(Date.parse(user.joinedAt))) {
800
+ return null;
801
+ }
802
+ restoredPresence[channel] = {
803
+ ...user,
804
+ id: user.id,
805
+ joinedAt: new Date(user.joinedAt)
806
+ };
807
+ }
808
+ return new _PresenceSocketFrame(transport, {
809
+ id: transport.id,
810
+ data: {
811
+ ...data,
812
+ [PRESENCE_DATA_KEY]: restoredPresence
813
+ }
814
+ });
815
+ }
816
+ /**
817
+ * Return the validated transport value.
818
+ */
819
+ toTransport() {
820
+ return this.transportValue;
821
+ }
822
+ /**
823
+ * Return the socket value with restored presence timestamps.
824
+ */
825
+ toSocket() {
826
+ return this.socketValue;
827
+ }
828
+ };
829
+
830
+ // src/duration.ts
831
+ import { parse as parseDurationExpression } from "@lukeed/ms";
832
+ function parseDuration(name, duration) {
833
+ if (duration === void 0) {
834
+ return void 0;
835
+ }
836
+ const milliseconds = typeof duration === "number" ? duration : parseDurationExpression(duration);
837
+ if (typeof milliseconds === "undefined" || !Number.isFinite(milliseconds) || milliseconds <= 0) {
838
+ throw new Error(`${name} must be a positive duration`);
839
+ }
840
+ return milliseconds;
841
+ }
842
+
843
+ // src/socket_bus.ts
844
+ var DEFAULT_CHANNEL = "socket::broadcast";
845
+ var DEFAULT_PRESENCE_TIMEOUT = 100;
846
+ var DEFAULT_RETRY_QUEUE_MAX_SIZE = 1e3;
847
+ function resolveRetryQueueConfig(config) {
848
+ const maxSize = config?.maxSize === void 0 ? DEFAULT_RETRY_QUEUE_MAX_SIZE : config.maxSize;
849
+ if (maxSize !== null && (!Number.isSafeInteger(maxSize) || maxSize <= 0)) {
850
+ throw new Error("transport.retryQueue.maxSize must be a positive integer or null");
851
+ }
852
+ return {
853
+ enabled: true,
854
+ maxSize,
855
+ ...config
856
+ };
857
+ }
858
+ function parseBusMessage(payload) {
859
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
860
+ return null;
861
+ }
862
+ const message = payload;
863
+ if (typeof message.origin !== "string") {
864
+ return null;
865
+ }
866
+ switch (message.type) {
867
+ case "channel:event":
868
+ return typeof message.channel === "string" && typeof message.event === "string" && Array.isArray(message.except) && message.except.every((socketId) => typeof socketId === "string") ? message : null;
869
+ case "broadcast:event":
870
+ return typeof message.event === "string" ? message : null;
871
+ case "presence:sockets:request":
872
+ return typeof message.requestId === "string" && typeof message.channel === "string" ? message : null;
873
+ case "presence:sockets:response":
874
+ if (typeof message.target !== "string" || typeof message.requestId !== "string" || !Array.isArray(message.sockets)) {
875
+ return null;
876
+ }
877
+ const sockets = Array.from(message.sockets, PresenceSocketFrame.fromTransport);
878
+ return sockets.every((socket) => socket !== null) ? { ...message, sockets } : null;
879
+ default:
880
+ return null;
881
+ }
882
+ }
883
+ var SocketBus = class {
884
+ constructor(transport, handlers) {
885
+ this.handlers = handlers;
886
+ this.#channel = transport.channel ?? DEFAULT_CHANNEL;
887
+ this.#presenceTimeout = parseDuration("transport.presenceTimeout", transport.presenceTimeout) ?? DEFAULT_PRESENCE_TIMEOUT;
888
+ this.#bus = new Bus(transport.driver(), {
889
+ retryQueue: resolveRetryQueueConfig(transport.retryQueue)
890
+ });
891
+ }
892
+ #origin = randomUUID();
893
+ #channel;
894
+ #bus;
895
+ #presenceTimeout;
896
+ #pendingPresenceRequests = /* @__PURE__ */ new Map();
897
+ async start() {
898
+ await this.#bus.subscribe(this.#channel, (payload) => {
899
+ try {
900
+ const message = parseBusMessage(payload);
901
+ if (!message || message.origin === this.#origin) {
902
+ return;
903
+ }
904
+ switch (message.type) {
905
+ case "channel:event":
906
+ this.handlers.channel(message);
907
+ return;
908
+ case "broadcast:event":
909
+ this.handlers.broadcast(message);
910
+ return;
911
+ case "presence:sockets:request":
912
+ void this.#respondToPresenceRequest(message).catch(() => {
913
+ });
914
+ return;
915
+ case "presence:sockets:response":
916
+ this.#handlePresenceResponse(message);
917
+ return;
918
+ }
919
+ } catch {
920
+ return;
921
+ }
922
+ });
923
+ }
924
+ publishChannel(channel, event, data, except) {
925
+ const message = {
926
+ type: "channel:event",
927
+ origin: this.#origin,
928
+ channel,
929
+ event,
930
+ data,
931
+ except: except ?? []
932
+ };
933
+ void this.#bus.publish(this.#channel, message);
934
+ }
935
+ publishBroadcast(event, data) {
936
+ const message = {
937
+ type: "broadcast:event",
938
+ origin: this.#origin,
939
+ event,
940
+ data
941
+ };
942
+ void this.#bus.publish(this.#channel, message);
943
+ }
944
+ fetchPresenceSockets(channel) {
945
+ const requestId = randomUUID();
946
+ return new Promise((resolve) => {
947
+ const timeout = setTimeout(() => {
948
+ const request = this.#pendingPresenceRequests.get(requestId);
949
+ if (!request) {
950
+ return;
951
+ }
952
+ this.#pendingPresenceRequests.delete(requestId);
953
+ resolve(request.sockets.map((socket) => socket.toSocket()));
954
+ }, this.#presenceTimeout);
955
+ this.#pendingPresenceRequests.set(requestId, {
956
+ sockets: [],
957
+ resolve,
958
+ timeout
959
+ });
960
+ const message = {
961
+ type: "presence:sockets:request",
962
+ origin: this.#origin,
963
+ requestId,
964
+ channel
965
+ };
966
+ void this.#bus.publish(this.#channel, message);
967
+ });
968
+ }
969
+ async close() {
970
+ for (const [requestId, request] of this.#pendingPresenceRequests) {
971
+ clearTimeout(request.timeout);
972
+ request.resolve(request.sockets.map((socket) => socket.toSocket()));
973
+ this.#pendingPresenceRequests.delete(requestId);
974
+ }
975
+ await this.#bus.disconnect();
976
+ }
977
+ async #respondToPresenceRequest(message) {
978
+ const response = {
979
+ type: "presence:sockets:response",
980
+ origin: this.#origin,
981
+ target: message.origin,
982
+ requestId: message.requestId,
983
+ sockets: this.handlers.presenceSockets(message.channel)
984
+ };
985
+ await this.#bus.publish(this.#channel, response);
986
+ }
987
+ #handlePresenceResponse(message) {
988
+ if (message.target !== this.#origin) {
989
+ return;
990
+ }
991
+ const request = this.#pendingPresenceRequests.get(message.requestId);
992
+ if (!request) {
993
+ return;
994
+ }
995
+ request.sockets.push(...message.sockets);
996
+ }
997
+ };
998
+
999
+ // src/socket_upgrader.ts
1000
+ var DEFAULT_WEBSOCKET_PATH = "/socket";
1001
+ var SocketUpgrader = class _SocketUpgrader {
1002
+ constructor(server, config, runWithHttpContext, reportError = () => {
1003
+ }) {
1004
+ this.server = server;
1005
+ this.config = config;
1006
+ this.runWithHttpContext = runWithHttpContext;
1007
+ this.reportError = reportError;
1008
+ this.#path = config?.path ?? DEFAULT_WEBSOCKET_PATH;
1009
+ }
1010
+ #path;
1011
+ async handle(request, socket, head, accept) {
1012
+ if (!this.#matchesPath(request)) {
1013
+ return false;
1014
+ }
1015
+ const result = await this.#prepareUpgrade(request);
1016
+ if ("rejected" in result) {
1017
+ if (result.rejected === "origin") {
1018
+ _SocketUpgrader.reject(socket, 403, "Forbidden");
1019
+ return true;
1020
+ }
1021
+ _SocketUpgrader.reject(socket, 401, "Unauthorized");
1022
+ return true;
1023
+ }
1024
+ this.server.handleUpgrade(request, socket, head, (connection) => {
1025
+ accept(connection, request, result.accepted);
1026
+ });
1027
+ return true;
1028
+ }
1029
+ static reject(socket, statusCode, reason) {
1030
+ if (!socket.writableEnded) {
1031
+ socket.write(`HTTP/1.1 ${statusCode} ${reason}\r
1032
+ \r
1033
+ `);
1034
+ }
1035
+ socket.destroy();
1036
+ }
1037
+ #matchesPath(request) {
1038
+ const url = new URL(request.url ?? "/", "ws://localhost");
1039
+ return url.pathname === this.#path;
1040
+ }
1041
+ #isOriginAllowed(request, httpContext) {
1042
+ const origin = request.headers.origin;
1043
+ if (!origin) {
1044
+ return true;
1045
+ }
1046
+ if (this.config?.origin !== void 0) {
1047
+ return this.#matchesOriginPolicy(origin, httpContext);
1048
+ }
1049
+ const host = request.headers.host;
1050
+ if (!host) {
1051
+ return false;
1052
+ }
1053
+ const forwardedHeader = request.headers["x-forwarded-proto"];
1054
+ const forwardedProtocol = (Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader)?.split(",")[0]?.trim();
1055
+ const protocol = forwardedProtocol === "http" || forwardedProtocol === "https" ? forwardedProtocol : request.socket.encrypted ? "https" : "http";
1056
+ try {
1057
+ return origin === new URL(`${protocol}://${host}`).origin;
1058
+ } catch {
1059
+ return false;
1060
+ }
1061
+ }
1062
+ #matchesOriginPolicy(origin, httpContext) {
1063
+ let originPolicy = this.config?.origin ?? false;
1064
+ if (typeof originPolicy === "function") {
1065
+ originPolicy = originPolicy(origin, httpContext);
1066
+ }
1067
+ if (originPolicy === true || originPolicy === "*") {
1068
+ return true;
1069
+ }
1070
+ if (originPolicy === false) {
1071
+ return false;
1072
+ }
1073
+ if (Array.isArray(originPolicy)) {
1074
+ return originPolicy.includes(origin);
1075
+ }
1076
+ return originPolicy.split(",").includes(origin);
1077
+ }
1078
+ async #prepareUpgrade(request) {
1079
+ return this.runWithHttpContext(
1080
+ request,
1081
+ async (httpContext) => {
1082
+ const accepted = await this.#authenticate(httpContext);
1083
+ return accepted ? { accepted } : { rejected: "authentication" };
1084
+ },
1085
+ (httpContext) => {
1086
+ return this.#isOriginAllowed(request, httpContext) ? void 0 : { rejected: "origin" };
1087
+ }
1088
+ );
1089
+ }
1090
+ async #authenticate(httpContext) {
1091
+ const config = this.config;
1092
+ if (!config?.authenticate) {
1093
+ return { httpContext };
1094
+ }
1095
+ try {
1096
+ const ctx = {
1097
+ httpContext
1098
+ };
1099
+ const result = await config.authenticate(ctx);
1100
+ if (result === false || result === null || result === void 0) {
1101
+ return null;
1102
+ }
1103
+ return {
1104
+ httpContext,
1105
+ user: result
1106
+ };
1107
+ } catch (error) {
1108
+ try {
1109
+ this.reportError("socket authentication failed: %s", error);
1110
+ } catch {
1111
+ }
1112
+ return null;
1113
+ }
1114
+ }
1115
+ };
1116
+
1117
+ // src/socket_service.ts
1118
+ var DEFAULT_PING_INTERVAL = 25e3;
1119
+ var DEFAULT_PING_TIMEOUT = 5e3;
1120
+ var DEFAULT_MAX_PAYLOAD = 1024 * 1024;
1121
+ var DEFAULT_MAX_QUEUED_MESSAGES = 100;
1122
+ var DEFAULT_MAX_MESSAGES_PER_INTERVAL = 1e3;
1123
+ var DEFAULT_MESSAGE_RATE_INTERVAL = 1e3;
1124
+ var DEFAULT_MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024;
1125
+ var DEFAULT_MAX_OUTBOUND_PAYLOAD = 1024 * 1024;
1126
+ var DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 100;
1127
+ var DEFAULT_MAX_CHANNEL_NAME_LENGTH = 255;
1128
+ var DEFAULT_SHUTDOWN_TIMEOUT = 5e3;
1129
+ var CLOSE_POLICY_VIOLATION = 1008;
1130
+ var CLOSE_MESSAGE_TOO_BIG = 1009;
1131
+ var CLOSE_OUTBOUND_BUFFER_LIMIT = "Socket outbound buffer limit exceeded";
1132
+ var CLOSE_OUTBOUND_MESSAGE_TOO_BIG = "Socket outbound message too big";
1133
+ var SERVER_DISCONNECT_REASON = "Socket server disconnected";
1134
+ async function settleBeforeDeadline(promise, deadline) {
1135
+ const remaining = deadline - Date.now();
1136
+ if (remaining <= 0) {
1137
+ void promise.catch(() => {
1138
+ });
1139
+ return null;
1140
+ }
1141
+ let timer;
1142
+ try {
1143
+ return await Promise.race([
1144
+ promise.then(
1145
+ (value) => ({ status: "fulfilled", value }),
1146
+ (reason) => ({ status: "rejected", reason })
1147
+ ),
1148
+ new Promise((resolve) => {
1149
+ timer = setTimeout(() => resolve(null), remaining);
1150
+ })
1151
+ ]);
1152
+ } finally {
1153
+ if (timer) clearTimeout(timer);
1154
+ }
1155
+ }
1156
+ function serializeFrame(frame) {
1157
+ return JSON.stringify(frame);
1158
+ }
1159
+ function sendFrame(connection, frame, config) {
1160
+ const serializedFrame = serializeFrame(frame);
1161
+ return sendSerializedFrameWithBackpressure(connection, serializedFrame, config);
1162
+ }
1163
+ function sendSerializedFrameWithBackpressure(connection, serializedFrame, config) {
1164
+ if (connection.readyState !== WebSocket.OPEN) {
1165
+ return false;
1166
+ }
1167
+ const payloadSize = Buffer.byteLength(serializedFrame);
1168
+ if (payloadSize > config.maxOutboundPayload) {
1169
+ closeConnection(connection, CLOSE_MESSAGE_TOO_BIG, CLOSE_OUTBOUND_MESSAGE_TOO_BIG);
1170
+ return false;
1171
+ }
1172
+ if (connection.bufferedAmount + payloadSize > config.maxBufferedAmount) {
1173
+ closeConnection(connection, CLOSE_POLICY_VIOLATION, CLOSE_OUTBOUND_BUFFER_LIMIT);
1174
+ return false;
1175
+ }
1176
+ connection.send(serializedFrame);
1177
+ return true;
1178
+ }
1179
+ function parsePositiveInteger(name, value, fallback) {
1180
+ if (value === void 0) {
1181
+ return fallback;
1182
+ }
1183
+ if (!Number.isSafeInteger(value) || value <= 0) {
1184
+ throw new Error(`${name} must be a positive integer`);
1185
+ }
1186
+ return value;
1187
+ }
1188
+ function resolveHeartbeatConfig(config) {
1189
+ const pingInterval = parseDuration("websocket.pingInterval", config?.pingInterval);
1190
+ const pingTimeout = parseDuration("websocket.pingTimeout", config?.pingTimeout);
1191
+ if (pingInterval === void 0 && pingTimeout === void 0) {
1192
+ return null;
1193
+ }
1194
+ return {
1195
+ interval: pingInterval ?? DEFAULT_PING_INTERVAL,
1196
+ timeout: pingTimeout ?? DEFAULT_PING_TIMEOUT
1197
+ };
1198
+ }
1199
+ function resolveInboundMessageConfig(config) {
1200
+ const messageRateInterval = parseDuration("websocket.messageRateInterval", config?.messageRateInterval) ?? DEFAULT_MESSAGE_RATE_INTERVAL;
1201
+ return {
1202
+ maxPayload: parsePositiveInteger(
1203
+ "websocket.maxPayload",
1204
+ config?.maxPayload,
1205
+ DEFAULT_MAX_PAYLOAD
1206
+ ),
1207
+ maxQueuedMessages: parsePositiveInteger(
1208
+ "websocket.maxQueuedMessages",
1209
+ config?.maxQueuedMessages,
1210
+ DEFAULT_MAX_QUEUED_MESSAGES
1211
+ ),
1212
+ maxMessagesPerInterval: parsePositiveInteger(
1213
+ "websocket.maxMessagesPerInterval",
1214
+ config?.maxMessagesPerInterval,
1215
+ DEFAULT_MAX_MESSAGES_PER_INTERVAL
1216
+ ),
1217
+ messageRateInterval,
1218
+ subscriptionLimits: {
1219
+ maxSubscriptionsPerSocket: parsePositiveInteger(
1220
+ "websocket.maxSubscriptionsPerSocket",
1221
+ config?.maxSubscriptionsPerSocket,
1222
+ DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET
1223
+ ),
1224
+ maxChannelNameLength: parsePositiveInteger(
1225
+ "websocket.maxChannelNameLength",
1226
+ config?.maxChannelNameLength,
1227
+ DEFAULT_MAX_CHANNEL_NAME_LENGTH
1228
+ )
1229
+ }
1230
+ };
1231
+ }
1232
+ function resolveOutboundMessageConfig(config) {
1233
+ return {
1234
+ maxBufferedAmount: parsePositiveInteger(
1235
+ "websocket.maxBufferedAmount",
1236
+ config?.maxBufferedAmount,
1237
+ DEFAULT_MAX_BUFFERED_AMOUNT
1238
+ ),
1239
+ maxOutboundPayload: parsePositiveInteger(
1240
+ "websocket.maxOutboundPayload",
1241
+ config?.maxOutboundPayload,
1242
+ DEFAULT_MAX_OUTBOUND_PAYLOAD
1243
+ )
1244
+ };
1245
+ }
1246
+ function getRawDataByteLength(data) {
1247
+ if (Array.isArray(data)) {
1248
+ return data.reduce((total, chunk) => total + chunk.byteLength, 0);
1249
+ }
1250
+ return data.byteLength;
1251
+ }
1252
+ function closeConnection(connection, code, reason) {
1253
+ if (connection.readyState === WebSocket.OPEN) {
1254
+ connection.close(code, reason);
1255
+ }
1256
+ }
1257
+ var SocketService = class extends Emittery {
1258
+ #server = null;
1259
+ #httpServer = null;
1260
+ #upgradeHandler = null;
1261
+ #sockets = /* @__PURE__ */ new Map();
1262
+ #channelSubscriptions = null;
1263
+ #presenceManager = null;
1264
+ #bus = null;
1265
+ #fake = null;
1266
+ #deliverySink = {
1267
+ dispatch: (emission) => {
1268
+ this.#deliverBroadcast(emission);
1269
+ }
1270
+ };
1271
+ #broadcastSink = this.#deliverySink;
1272
+ #status = "stopped";
1273
+ #lastError = null;
1274
+ #heartbeatInterval = null;
1275
+ #heartbeatTimeouts = /* @__PURE__ */ new Map();
1276
+ #messageQueue = new MessageQueue();
1277
+ #messageRates = /* @__PURE__ */ new Map();
1278
+ #socketFinalizations = /* @__PURE__ */ new Map();
1279
+ #bootGeneration = 0;
1280
+ #bootPromise = null;
1281
+ #closePromise = null;
1282
+ #outboundMessageConfig = resolveOutboundMessageConfig(void 0);
1283
+ #shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
1284
+ #logger = null;
1285
+ /**
1286
+ * Accesses the underlying ws server.
1287
+ */
1288
+ get server() {
1289
+ if (!this.#server) {
1290
+ throw new Error("WebSocket server is not initialized. Call boot() first.");
1291
+ }
1292
+ return this.#server;
1293
+ }
1294
+ setPresenceManager(presenceManager) {
1295
+ this.#presenceManager = presenceManager;
1296
+ this.#channelSubscriptions?.setPresenceManager(presenceManager);
1297
+ this.#configurePresenceFetcher();
1298
+ }
1299
+ boot(httpServer, config, channelRouter, logger, runWithHttpContext) {
1300
+ if (this.#status === "ready") return Promise.resolve();
1301
+ if (this.#bootPromise) return this.#bootPromise;
1302
+ if (this.#status === "stopping") {
1303
+ return Promise.reject(new Error("Socket service is stopping"));
1304
+ }
1305
+ this.#closePromise = null;
1306
+ this.#status = "starting";
1307
+ this.#lastError = null;
1308
+ this.#logger = logger;
1309
+ const generation = ++this.#bootGeneration;
1310
+ const bootPromise = this.#boot(
1311
+ httpServer,
1312
+ config,
1313
+ channelRouter,
1314
+ logger,
1315
+ runWithHttpContext,
1316
+ generation
1317
+ ).finally(() => {
1318
+ if (this.#bootPromise === bootPromise) this.#bootPromise = null;
1319
+ });
1320
+ this.#bootPromise = bootPromise;
1321
+ return bootPromise;
1322
+ }
1323
+ async #boot(httpServer, config, channelRouter, logger, runWithHttpContext, generation) {
1324
+ const websocketConfig = config.websocket;
1325
+ const heartbeatConfig = resolveHeartbeatConfig(websocketConfig);
1326
+ const inboundMessageConfig = resolveInboundMessageConfig(websocketConfig);
1327
+ this.#outboundMessageConfig = resolveOutboundMessageConfig(websocketConfig);
1328
+ this.#shutdownTimeout = parseDuration("websocket.shutdownTimeout", websocketConfig?.shutdownTimeout) ?? DEFAULT_SHUTDOWN_TIMEOUT;
1329
+ this.#channelSubscriptions = new ChannelSubscriptions(
1330
+ this,
1331
+ channelRouter,
1332
+ logger,
1333
+ this.#presenceManager ?? void 0,
1334
+ inboundMessageConfig.subscriptionLimits
1335
+ );
1336
+ try {
1337
+ if (config.transport) {
1338
+ this.#bus = new SocketBus(config.transport, {
1339
+ channel: (message) => {
1340
+ this.#traceBroadcast(
1341
+ {
1342
+ target: "channel",
1343
+ channel: message.channel,
1344
+ event: message.event,
1345
+ via: "bus",
1346
+ except: message.except
1347
+ },
1348
+ () => this.#emitToChannelLocally(
1349
+ message.channel,
1350
+ message.event,
1351
+ message.data,
1352
+ message.except
1353
+ )
1354
+ );
1355
+ },
1356
+ broadcast: (message) => {
1357
+ this.#traceBroadcast(
1358
+ { target: "global", event: message.event, via: "bus" },
1359
+ () => this.#broadcastLocally(message.event, message.data)
1360
+ );
1361
+ },
1362
+ presenceSockets: (channel) => {
1363
+ return this.#getLocalPresenceSockets(channel);
1364
+ }
1365
+ });
1366
+ await this.#bus.start();
1367
+ if (generation !== this.#bootGeneration || this.#status !== "starting") {
1368
+ throw new Error("Socket service boot interrupted by shutdown");
1369
+ }
1370
+ this.#configurePresenceFetcher();
1371
+ }
1372
+ const websocketServer = new WebSocketServer({
1373
+ noServer: true,
1374
+ maxPayload: inboundMessageConfig.maxPayload
1375
+ });
1376
+ this.#server = websocketServer;
1377
+ this.#httpServer = httpServer;
1378
+ const upgrader = new SocketUpgrader(
1379
+ websocketServer,
1380
+ websocketConfig,
1381
+ runWithHttpContext,
1382
+ (message, error) => this.#warn(message, error)
1383
+ );
1384
+ this.#upgradeHandler = (request, socket, head) => {
1385
+ upgrader.handle(request, socket, head, (connection, upgradeRequest, accepted) => {
1386
+ this.#handleConnection(connection, upgradeRequest, accepted, inboundMessageConfig);
1387
+ }).catch((error) => {
1388
+ this.#warn("failed to upgrade socket connection: %s", error);
1389
+ SocketUpgrader.reject(socket, 500, "Internal Server Error");
1390
+ });
1391
+ };
1392
+ httpServer.on("upgrade", this.#upgradeHandler);
1393
+ this.#startHeartbeat(heartbeatConfig);
1394
+ this.#status = "ready";
1395
+ } catch (error) {
1396
+ if (generation !== this.#bootGeneration || this.#status === "stopping") throw error;
1397
+ this.#lastError = error instanceof Error ? error : new Error("Socket service boot failed");
1398
+ this.#status = "failed";
1399
+ await this.close();
1400
+ throw error;
1401
+ }
1402
+ }
1403
+ #handleConnection(connection, request, upgrade, inboundMessageConfig) {
1404
+ if (this.#status !== "ready") {
1405
+ connection.terminate();
1406
+ return;
1407
+ }
1408
+ const socket = this.#wrapSocket(connection, request, upgrade);
1409
+ connectChannel.traceSync(
1410
+ () => {
1411
+ this.#sockets.set(socket.id, socket);
1412
+ this.#emitLifecycleEvent("connect", { socket });
1413
+ },
1414
+ { socketId: socket.id }
1415
+ );
1416
+ connection.on("error", () => {
1417
+ });
1418
+ connection.on("message", (data) => {
1419
+ if (getRawDataByteLength(data) > inboundMessageConfig.maxPayload) {
1420
+ closeConnection(connection, CLOSE_MESSAGE_TOO_BIG, "Message too big");
1421
+ return;
1422
+ }
1423
+ if (!this.#acceptMessageRate(socket.id, inboundMessageConfig)) {
1424
+ closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message rate limit exceeded");
1425
+ return;
1426
+ }
1427
+ const message = Message.fromTransport(data.toString());
1428
+ if (message.type === "ping") {
1429
+ sendFrame(
1430
+ socket.raw.connection,
1431
+ { id: message.id, type: "pong" },
1432
+ this.#outboundMessageConfig
1433
+ );
1434
+ return;
1435
+ }
1436
+ const enqueued = this.#messageQueue.enqueue(
1437
+ socket.id,
1438
+ async () => {
1439
+ await this.#handleMessage(socket, message).catch((error) => {
1440
+ try {
1441
+ this.#logger?.warn("unexpected socket message failure: %s", error);
1442
+ } catch {
1443
+ }
1444
+ sendFrame(
1445
+ connection,
1446
+ message.id === void 0 ? { type: "error", error: "Unexpected socket error" } : Reply.error(message.id, "Unexpected socket error").toFrame(),
1447
+ this.#outboundMessageConfig
1448
+ );
1449
+ });
1450
+ },
1451
+ { maxDepth: inboundMessageConfig.maxQueuedMessages }
1452
+ );
1453
+ if (!enqueued) {
1454
+ closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message queue limit exceeded");
1455
+ }
1456
+ });
1457
+ connection.on("pong", () => {
1458
+ this.#clearHeartbeatTimeout(socket.id);
1459
+ });
1460
+ connection.on("close", () => {
1461
+ void Promise.resolve().then(() => this.#finalizeSocket(socket)).catch((error) => this.markFailed(error));
1462
+ });
1463
+ }
1464
+ #finalizeSocket(socket) {
1465
+ const pending = this.#socketFinalizations.get(socket.id);
1466
+ if (pending) {
1467
+ return pending;
1468
+ }
1469
+ if (!this.#sockets.has(socket.id)) {
1470
+ return Promise.resolve();
1471
+ }
1472
+ const channelSubscriptions = this.#channelSubscriptions;
1473
+ const finalization = disconnectChannel.tracePromise(
1474
+ async () => {
1475
+ this.#clearHeartbeatTimeout(socket.id);
1476
+ this.#messageRates.delete(socket.id);
1477
+ await this.#messageQueue.drain(socket.id);
1478
+ this.#messageQueue.delete(socket.id);
1479
+ await channelSubscriptions?.leaveAll(socket);
1480
+ channelSubscriptions?.deleteSocket(socket.id);
1481
+ this.#sockets.delete(socket.id);
1482
+ this.#emitLifecycleEvent("disconnect", { socket, reason: "close" });
1483
+ },
1484
+ {
1485
+ socketId: socket.id,
1486
+ reason: "close",
1487
+ subscriptions: channelSubscriptions?.subscriptionCountFor(socket.id) ?? 0
1488
+ }
1489
+ ).finally(() => {
1490
+ this.#socketFinalizations.delete(socket.id);
1491
+ });
1492
+ this.#socketFinalizations.set(socket.id, finalization);
1493
+ return finalization;
1494
+ }
1495
+ async #handleMessage(socket, message) {
1496
+ if (!message.valid) {
1497
+ sendFrame(socket.raw.connection, message.toRejectionFrame(), this.#outboundMessageConfig);
1498
+ return;
1499
+ }
1500
+ switch (message.type) {
1501
+ case "ping":
1502
+ return;
1503
+ case "subscribe": {
1504
+ const traceMessage = {
1505
+ socketId: socket.id,
1506
+ channel: message.channel
1507
+ };
1508
+ await subscribeChannel.tracePromise(async () => {
1509
+ const result = await this.#channelSubscriptions?.subscribe(socket, message.channel);
1510
+ traceMessage.created = result?.created ?? false;
1511
+ traceMessage.ok = result?.ack.ok ?? false;
1512
+ if (!result?.ack.ok) {
1513
+ traceMessage.error = result?.ack.error ?? "Socket service is not initialized";
1514
+ }
1515
+ if (result?.created) {
1516
+ this.#emitLifecycleEvent("subscribe", { socket, channel: message.channel });
1517
+ }
1518
+ sendFrame(
1519
+ socket.raw.connection,
1520
+ Reply.fromSubscribeResult(message.id, result).toFrame(),
1521
+ this.#outboundMessageConfig
1522
+ );
1523
+ }, traceMessage);
1524
+ return;
1525
+ }
1526
+ case "unsubscribe": {
1527
+ const traceMessage = {
1528
+ socketId: socket.id,
1529
+ channel: message.channel
1530
+ };
1531
+ await unsubscribeChannel.tracePromise(async () => {
1532
+ traceMessage.removed = await this.#channelSubscriptions?.leave(socket, message.channel) ?? false;
1533
+ traceMessage.ok = true;
1534
+ this.#emitLifecycleEvent("unsubscribe", { socket, channel: message.channel });
1535
+ sendFrame(
1536
+ socket.raw.connection,
1537
+ Reply.ok(message.id).toFrame(),
1538
+ this.#outboundMessageConfig
1539
+ );
1540
+ }, traceMessage);
1541
+ return;
1542
+ }
1543
+ case "message":
1544
+ case "whisper": {
1545
+ await this.#handleChannelFrame(socket, message);
1546
+ return;
1547
+ }
1548
+ }
1549
+ }
1550
+ async #handleChannelFrame(socket, message) {
1551
+ const payload = message.toChannelMessage();
1552
+ const traceMessage = {
1553
+ socketId: socket.id,
1554
+ channel: payload.channel,
1555
+ event: payload.event
1556
+ };
1557
+ await channelMessageChannel.tracePromise(async () => {
1558
+ const ack = await this.#handleChannelPayload(socket, message.type, payload);
1559
+ traceMessage.ok = ack.ok;
1560
+ if (!ack.ok) {
1561
+ traceMessage.error = ack.error ?? "Handler error";
1562
+ }
1563
+ sendFrame(
1564
+ socket.raw.connection,
1565
+ Reply.fromChannelAck(message.id, ack).toFrame(),
1566
+ this.#outboundMessageConfig
1567
+ );
1568
+ }, traceMessage);
1569
+ }
1570
+ async #handleChannelPayload(socket, type, payload) {
1571
+ if (typeof payload.channel !== "string" || typeof payload.event !== "string") {
1572
+ return { ok: false, error: `Invalid channel ${type}` };
1573
+ }
1574
+ if (type === "message") {
1575
+ return this.#channelSubscriptions.handleMessage(socket, payload);
1576
+ }
1577
+ return this.#channelSubscriptions.relayWhisper(socket, payload);
1578
+ }
1579
+ #wrapSocket(connection, request, upgrade) {
1580
+ const raw = {
1581
+ id: randomUUID2(),
1582
+ data: {},
1583
+ connection,
1584
+ request,
1585
+ httpContext: upgrade.httpContext
1586
+ };
1587
+ return {
1588
+ id: raw.id,
1589
+ user: upgrade.user,
1590
+ getUserOrFail() {
1591
+ if (this.user === void 0) {
1592
+ throw new SocketResponseError("Unauthorized");
1593
+ }
1594
+ return this.user;
1595
+ },
1596
+ emit: (event, data) => {
1597
+ this.#sendSerializedEventFrame(connection, serializeFrame({ type: "event", event, data }));
1598
+ },
1599
+ disconnect() {
1600
+ connection.close(SERVER_DISCONNECT_CODE, SERVER_DISCONNECT_REASON);
1601
+ },
1602
+ raw
1603
+ };
1604
+ }
1605
+ to(channel) {
1606
+ return {
1607
+ emit: (event, data) => {
1608
+ this.#broadcastSink.dispatch({ target: "channel", channel, event, data });
1609
+ },
1610
+ except: (socketId) => ({
1611
+ emit: (event, data) => {
1612
+ this.#broadcastSink.dispatch({
1613
+ target: "channel",
1614
+ channel,
1615
+ event,
1616
+ data,
1617
+ except: [socketId]
1618
+ });
1619
+ }
1620
+ })
1621
+ };
1622
+ }
1623
+ broadcast(event, data) {
1624
+ this.#broadcastSink.dispatch({ target: "global", event, data });
1625
+ }
1626
+ #deliverBroadcast(emission) {
1627
+ switch (emission.target) {
1628
+ case "channel": {
1629
+ const channel = emission.channel;
1630
+ const serializedFrame = serializeFrame({
1631
+ type: "event",
1632
+ channel,
1633
+ event: emission.event,
1634
+ data: emission.data
1635
+ });
1636
+ this.#bus?.publishChannel(channel, emission.event, emission.data, emission.except);
1637
+ const traceMessage = {
1638
+ target: "channel",
1639
+ channel,
1640
+ event: emission.event,
1641
+ via: "local"
1642
+ };
1643
+ if (emission.except) {
1644
+ traceMessage.except = emission.except;
1645
+ }
1646
+ this.#traceBroadcast(
1647
+ traceMessage,
1648
+ () => this.#emitToChannelLocally(
1649
+ channel,
1650
+ emission.event,
1651
+ emission.data,
1652
+ emission.except,
1653
+ serializedFrame
1654
+ )
1655
+ );
1656
+ return;
1657
+ }
1658
+ case "global": {
1659
+ const serializedFrame = serializeFrame({
1660
+ type: "event",
1661
+ event: emission.event,
1662
+ data: emission.data
1663
+ });
1664
+ this.#bus?.publishBroadcast(emission.event, emission.data);
1665
+ this.#traceBroadcast(
1666
+ { target: "global", event: emission.event, via: "local" },
1667
+ () => this.#broadcastLocally(emission.event, emission.data, serializedFrame)
1668
+ );
1669
+ }
1670
+ }
1671
+ }
1672
+ #traceBroadcast(message, deliver) {
1673
+ broadcastChannel.traceSync(() => {
1674
+ message.delivered = deliver();
1675
+ }, message);
1676
+ }
1677
+ #sendSerializedEventFrame(connection, serializedFrame) {
1678
+ return sendSerializedFrameWithBackpressure(
1679
+ connection,
1680
+ serializedFrame,
1681
+ this.#outboundMessageConfig
1682
+ );
1683
+ }
1684
+ #emitToChannelLocally(channel, event, data, except = [], serializedFrame = serializeFrame({ type: "event", channel, event, data })) {
1685
+ let delivered = 0;
1686
+ for (const socketId of this.#channelSubscriptions?.getSocketIds(channel) ?? []) {
1687
+ if (except.includes(socketId)) {
1688
+ continue;
1689
+ }
1690
+ const socket = this.#sockets.get(socketId);
1691
+ if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
1692
+ delivered += 1;
1693
+ }
1694
+ }
1695
+ return delivered;
1696
+ }
1697
+ #broadcastLocally(event, data, serializedFrame = serializeFrame({ type: "event", event, data })) {
1698
+ let delivered = 0;
1699
+ for (const socket of this.#sockets.values()) {
1700
+ if (this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
1701
+ delivered += 1;
1702
+ }
1703
+ }
1704
+ return delivered;
1705
+ }
1706
+ #configurePresenceFetcher() {
1707
+ if (!this.#presenceManager || !this.#bus) {
1708
+ return;
1709
+ }
1710
+ this.#presenceManager.setSocketFetcher(async (channel) => {
1711
+ return [
1712
+ ...this.#presenceManager.getLocalSockets(channel),
1713
+ ...await this.#bus.fetchPresenceSockets(channel)
1714
+ ];
1715
+ });
1716
+ }
1717
+ #getLocalPresenceSockets(channel) {
1718
+ return (this.#presenceManager?.getLocalSockets(channel) ?? []).map((socket) => {
1719
+ return PresenceSocketFrame.fromSocket(channel, socket).toTransport();
1720
+ });
1721
+ }
1722
+ getSocket(socketId) {
1723
+ return this.#sockets.get(socketId);
1724
+ }
1725
+ get connectionsCount() {
1726
+ return this.#sockets.size;
1727
+ }
1728
+ get status() {
1729
+ return this.#status;
1730
+ }
1731
+ get ready() {
1732
+ return this.#status === "ready" && Boolean(this.#server);
1733
+ }
1734
+ fake() {
1735
+ const fake = new SocketFake(() => {
1736
+ if (this.#fake === fake) {
1737
+ this.restore();
1738
+ }
1739
+ });
1740
+ this.#fake = fake;
1741
+ this.#broadcastSink = fake;
1742
+ return fake;
1743
+ }
1744
+ restore() {
1745
+ this.#fake = null;
1746
+ this.#broadcastSink = this.#deliverySink;
1747
+ }
1748
+ markFailed(error) {
1749
+ this.#lastError = error instanceof Error ? error : new Error("Socket service failed");
1750
+ this.#status = "failed";
1751
+ }
1752
+ #warn(message, error) {
1753
+ try {
1754
+ this.#logger?.warn(message, error);
1755
+ } catch {
1756
+ }
1757
+ }
1758
+ #emitLifecycleEvent(eventName, event) {
1759
+ void this.emit(eventName, event).catch((error) => {
1760
+ try {
1761
+ this.#logger?.warn("socket %s listener failed: %s", eventName, error);
1762
+ } catch {
1763
+ }
1764
+ });
1765
+ }
1766
+ health() {
1767
+ return {
1768
+ status: this.#status,
1769
+ ready: this.ready,
1770
+ connections: this.#sockets.size,
1771
+ channels: this.#channelSubscriptions?.channelsCount ?? 0,
1772
+ ...this.#lastError ? { lastError: this.#lastError.message } : {}
1773
+ };
1774
+ }
1775
+ #startHeartbeat(config) {
1776
+ this.#stopHeartbeat();
1777
+ if (!config) {
1778
+ return;
1779
+ }
1780
+ this.#heartbeatInterval = setInterval(() => {
1781
+ this.#pingSockets(config.timeout);
1782
+ }, config.interval);
1783
+ }
1784
+ #pingSockets(timeout) {
1785
+ for (const socket of this.#sockets.values()) {
1786
+ const { connection } = socket.raw;
1787
+ if (connection.readyState !== WebSocket.OPEN) {
1788
+ this.#clearHeartbeatTimeout(socket.id);
1789
+ continue;
1790
+ }
1791
+ if (this.#heartbeatTimeouts.has(socket.id)) {
1792
+ continue;
1793
+ }
1794
+ const timeoutId = setTimeout(() => {
1795
+ this.#heartbeatTimeouts.delete(socket.id);
1796
+ connection.terminate();
1797
+ }, timeout);
1798
+ this.#heartbeatTimeouts.set(socket.id, timeoutId);
1799
+ try {
1800
+ connection.ping();
1801
+ } catch {
1802
+ this.#clearHeartbeatTimeout(socket.id);
1803
+ connection.terminate();
1804
+ }
1805
+ }
1806
+ }
1807
+ #clearHeartbeatTimeout(socketId) {
1808
+ const timeout = this.#heartbeatTimeouts.get(socketId);
1809
+ if (!timeout) {
1810
+ return;
1811
+ }
1812
+ clearTimeout(timeout);
1813
+ this.#heartbeatTimeouts.delete(socketId);
1814
+ }
1815
+ #stopHeartbeat() {
1816
+ if (this.#heartbeatInterval) {
1817
+ clearInterval(this.#heartbeatInterval);
1818
+ this.#heartbeatInterval = null;
1819
+ }
1820
+ for (const timeout of this.#heartbeatTimeouts.values()) {
1821
+ clearTimeout(timeout);
1822
+ }
1823
+ this.#heartbeatTimeouts.clear();
1824
+ }
1825
+ #acceptMessageRate(socketId, config) {
1826
+ const now = Date.now();
1827
+ const rate = this.#messageRates.get(socketId);
1828
+ if (!rate || now - rate.intervalStartedAt >= config.messageRateInterval) {
1829
+ this.#messageRates.set(socketId, { count: 1, intervalStartedAt: now });
1830
+ return true;
1831
+ }
1832
+ if (rate.count >= config.maxMessagesPerInterval) {
1833
+ return false;
1834
+ }
1835
+ rate.count += 1;
1836
+ return true;
1837
+ }
1838
+ #cleanup() {
1839
+ this.#stopHeartbeat();
1840
+ this.#sockets.clear();
1841
+ this.#messageQueue.clear();
1842
+ this.#messageRates.clear();
1843
+ this.#socketFinalizations.clear();
1844
+ this.#channelSubscriptions?.clear();
1845
+ }
1846
+ close() {
1847
+ if (!this.#closePromise) {
1848
+ if (this.#status === "starting" && this.#bootPromise) {
1849
+ this.#status = "stopping";
1850
+ this.#bootGeneration++;
1851
+ this.#closePromise = this.#close(this.#bootPromise);
1852
+ } else {
1853
+ this.#closePromise = this.#close();
1854
+ }
1855
+ }
1856
+ return this.#closePromise;
1857
+ }
1858
+ async #close(pendingBoot) {
1859
+ const deadline = Date.now() + this.#shutdownTimeout;
1860
+ if (pendingBoot) {
1861
+ const bootResult = await settleBeforeDeadline(pendingBoot, deadline);
1862
+ if (bootResult === null) {
1863
+ if (this.#bootPromise === pendingBoot) this.#bootPromise = null;
1864
+ this.#warn(
1865
+ "socket boot exceeded the %dms shutdown timeout; forcing cleanup",
1866
+ this.#shutdownTimeout
1867
+ );
1868
+ }
1869
+ }
1870
+ const server = this.#server;
1871
+ const bus = this.#bus;
1872
+ const httpServer = this.#httpServer;
1873
+ const upgradeHandler = this.#upgradeHandler;
1874
+ if (this.#status !== "failed") {
1875
+ this.#status = "stopping";
1876
+ }
1877
+ this.#server = null;
1878
+ this.#httpServer = null;
1879
+ this.#upgradeHandler = null;
1880
+ if (httpServer && upgradeHandler) {
1881
+ httpServer.off("upgrade", upgradeHandler);
1882
+ }
1883
+ const serverClose = server ? new Promise((resolve) => {
1884
+ server.close((error) => resolve(error ?? null));
1885
+ }) : Promise.resolve(null);
1886
+ this.#stopHeartbeat();
1887
+ const sockets = [...this.#sockets.values()];
1888
+ for (const socket of sockets) {
1889
+ const { connection } = socket.raw;
1890
+ connection.close(SERVER_DISCONNECT_CODE, SERVER_DISCONNECT_REASON);
1891
+ const terminateTimer = setTimeout(() => connection.terminate(), 50);
1892
+ connection.once("close", () => clearTimeout(terminateTimer));
1893
+ }
1894
+ const errors = [];
1895
+ const finalizations = sockets.map((socket) => {
1896
+ return Promise.resolve().then(() => this.#finalizeSocket(socket));
1897
+ });
1898
+ const results = await settleBeforeDeadline(Promise.allSettled(finalizations), deadline);
1899
+ if (results === null) {
1900
+ this.#warn(
1901
+ "socket finalization exceeded the %dms shutdown timeout; forcing cleanup",
1902
+ this.#shutdownTimeout
1903
+ );
1904
+ } else if (results.status === "fulfilled") {
1905
+ for (const result of results.value) {
1906
+ if (result.status === "rejected") {
1907
+ errors.push(result.reason);
1908
+ }
1909
+ }
1910
+ }
1911
+ this.#cleanup();
1912
+ this.#presenceManager?.setSocketFetcher(null);
1913
+ this.#bus = null;
1914
+ if (bus) {
1915
+ const busCloseResult = await settleBeforeDeadline(bus.close(), deadline);
1916
+ if (busCloseResult === null) {
1917
+ this.#warn(
1918
+ "socket transport exceeded the %dms shutdown timeout; forcing cleanup",
1919
+ this.#shutdownTimeout
1920
+ );
1921
+ } else if (busCloseResult.status === "rejected") {
1922
+ errors.push(busCloseResult.reason);
1923
+ }
1924
+ }
1925
+ const serverCloseResult = await settleBeforeDeadline(serverClose, deadline);
1926
+ if (serverCloseResult === null) {
1927
+ this.#warn(
1928
+ "socket server exceeded the %dms shutdown timeout; forcing cleanup",
1929
+ this.#shutdownTimeout
1930
+ );
1931
+ } else if (serverCloseResult.status === "fulfilled" && serverCloseResult.value) {
1932
+ errors.push(serverCloseResult.value);
1933
+ }
1934
+ if (errors.length > 0) {
1935
+ const error = errors.length === 1 && errors[0] instanceof Error ? errors[0] : new AggregateError(errors, "Socket service shutdown failed");
1936
+ this.markFailed(error);
1937
+ throw error;
1938
+ }
1939
+ if (this.#status !== "failed") {
1940
+ this.#status = "stopped";
1941
+ }
1942
+ }
1943
+ };
1944
+
1945
+ // src/channel_pattern.ts
1946
+ var ChannelPattern = class _ChannelPattern {
1947
+ constructor(value) {
1948
+ this.value = value;
1949
+ this.syntax = ChannelPatternSyntax.from(value);
1950
+ }
1951
+ syntax;
1952
+ /**
1953
+ * Parse one runtime channel pattern.
1954
+ */
1955
+ static from(value) {
1956
+ return new _ChannelPattern(value);
1957
+ }
1958
+ /**
1959
+ * Match one concrete channel name.
1960
+ */
1961
+ match(channelName) {
1962
+ return this.syntax.match(channelName);
1963
+ }
1964
+ /**
1965
+ * Return the most specific matching candidate. Static segments have the highest score.
1966
+ * Parameters score above optional parameters. Wildcards have the lowest score.
1967
+ * Insertion order is preserved for ties.
1968
+ */
1969
+ static firstMatch(channelName, candidates) {
1970
+ const sorted = [...candidates].sort((left, right) => {
1971
+ return _ChannelPattern.#compareSpecificity(left.pattern, right.pattern);
1972
+ });
1973
+ const matched = ChannelPatternSyntax.firstMatch(
1974
+ sorted.map(({ pattern: pattern2 }) => pattern2.syntax),
1975
+ channelName
1976
+ );
1977
+ if (!matched) return null;
1978
+ const { pattern, channel } = sorted[matched.index];
1979
+ return { pattern, channel, params: matched.params };
1980
+ }
1981
+ static #compareSpecificity(left, right) {
1982
+ return left.syntax.compareSpecificity(right.syntax);
1983
+ }
1984
+ };
1985
+
1986
+ // src/channel_router.ts
1987
+ var ChannelRouter = class {
1988
+ constructor(makeChannel = (channel) => new channel(), createMiddlewareRunner) {
1989
+ this.makeChannel = makeChannel;
1990
+ this.createMiddlewareRunner = createMiddlewareRunner;
1991
+ }
1992
+ /**
1993
+ * Registered channels keyed by their pattern.
1994
+ */
1995
+ #channels = /* @__PURE__ */ new Map();
1996
+ /**
1997
+ * Registers a channel.
1998
+ */
1999
+ register(channel) {
2000
+ const patternValue = channel.pattern;
2001
+ if (!patternValue) {
2002
+ throw new Error(`Channel ${channel.name} must define a static pattern`);
2003
+ }
2004
+ if (this.#channels.has(patternValue)) {
2005
+ throw new Error(`Channel pattern "${patternValue}" is already registered`);
2006
+ }
2007
+ const pattern = ChannelPattern.from(patternValue);
2008
+ this.#channels.set(pattern.value, { pattern, channel });
2009
+ }
2010
+ /**
2011
+ * Matches a channel name against registered patterns.
2012
+ */
2013
+ match(channelName) {
2014
+ const matched = ChannelPattern.firstMatch(channelName, this.#channels.values());
2015
+ if (!matched) {
2016
+ return null;
2017
+ }
2018
+ return {
2019
+ channel: matched.channel,
2020
+ pattern: matched.pattern.value,
2021
+ params: matched.params
2022
+ };
2023
+ }
2024
+ /**
2025
+ * Authorizes a socket before it joins a channel.
2026
+ */
2027
+ async authorize(socket, channelName) {
2028
+ const matched = this.match(channelName);
2029
+ if (!matched) {
2030
+ return {
2031
+ success: false,
2032
+ error: "Channel not found"
2033
+ };
2034
+ }
2035
+ const instance = await this.makeChannel(matched.channel);
2036
+ const paramValues = Object.values(matched.params);
2037
+ const ctx = {
2038
+ socket,
2039
+ channel: channelName,
2040
+ params: matched.params,
2041
+ presenceData: void 0,
2042
+ setPresenceData(data) {
2043
+ ctx.presenceData = data;
2044
+ }
2045
+ };
2046
+ try {
2047
+ await instance.$runMiddlewares(ctx, this.createMiddlewareRunner?.());
2048
+ } catch (error) {
2049
+ return {
2050
+ success: false,
2051
+ error: error instanceof SocketResponseError ? error.message : "Authorization failed",
2052
+ cause: error
2053
+ };
2054
+ }
2055
+ return {
2056
+ success: true,
2057
+ instance,
2058
+ params: matched.params,
2059
+ paramValues,
2060
+ presenceData: ctx.presenceData ?? null
2061
+ };
2062
+ }
2063
+ /**
2064
+ * Lists all registered channels.
2065
+ */
2066
+ list() {
2067
+ return [...this.#channels.values()].map(({ pattern, channel }) => ({
2068
+ pattern: pattern.value,
2069
+ name: channel.name,
2070
+ options: channel.options
2071
+ }));
2072
+ }
2073
+ /**
2074
+ * Number of registered channels.
2075
+ */
2076
+ get size() {
2077
+ return this.#channels.size;
2078
+ }
2079
+ };
8
2080
 
9
2081
  // providers/socket_provider.ts
10
- import { ServerResponse } from "http";
11
2082
  var SocketProvider = class {
12
2083
  constructor(app) {
13
2084
  this.app = app;
@@ -17,7 +2088,16 @@ var SocketProvider = class {
17
2088
  */
18
2089
  register() {
19
2090
  this.app.container.singleton("socket.router", () => {
20
- return new ChannelRouter();
2091
+ return new ChannelRouter(
2092
+ (channel) => this.app.container.make(channel),
2093
+ () => {
2094
+ const resolver = this.app.container.createResolver();
2095
+ return async (Middleware, ctx, next) => {
2096
+ const instance = await resolver.make(Middleware);
2097
+ await resolver.call(instance, "handle", [ctx, next]);
2098
+ };
2099
+ }
2100
+ );
21
2101
  });
22
2102
  this.app.container.singleton("socket.presence", () => {
23
2103
  return new PresenceManager();
@@ -53,7 +2133,13 @@ var SocketProvider = class {
53
2133
  return;
54
2134
  }
55
2135
  try {
56
- await socket.boot(httpServer, this.#withHttpContextFactory(config, server), router, logger);
2136
+ await socket.boot(
2137
+ httpServer,
2138
+ config,
2139
+ router,
2140
+ logger,
2141
+ this.#createUpgradeContextRunner(config, server)
2142
+ );
57
2143
  logger.info("server started");
58
2144
  } catch (error) {
59
2145
  socket.markFailed(error);
@@ -77,51 +2163,76 @@ var SocketProvider = class {
77
2163
  /**
78
2164
  * Creates an AdonisJS HTTP context for WebSocket upgrade requests.
79
2165
  */
80
- #withHttpContextFactory(config, server) {
81
- if (!config.websocket?.authenticate || config.websocket.createContext) {
82
- return config;
83
- }
84
- return {
85
- ...config,
86
- websocket: {
87
- ...config.websocket,
88
- createContext: (request) => {
89
- const response = new ServerResponse(request);
90
- const adonisRequest = server.createRequest(request, response);
91
- const adonisResponse = server.createResponse(request, response);
92
- return server.createHttpContext(
93
- adonisRequest,
94
- adonisResponse,
95
- this.app.container.createResolver()
96
- );
97
- }
2166
+ #createUpgradeContextRunner(config, server) {
2167
+ const middleware = config.websocket?.middleware ?? [];
2168
+ return async (request, handler, shortCircuitBeforeMiddleware) => {
2169
+ const response = new ServerResponse(request);
2170
+ const adonisRequest = server.createRequest(request, response);
2171
+ const adonisResponse = server.createResponse(request, response);
2172
+ const httpContext = server.createHttpContext(
2173
+ adonisRequest,
2174
+ adonisResponse,
2175
+ this.app.container.createResolver()
2176
+ );
2177
+ const earlyResult = await shortCircuitBeforeMiddleware?.(httpContext);
2178
+ if (earlyResult !== void 0) {
2179
+ return earlyResult;
98
2180
  }
2181
+ return this.#runUpgradeMiddleware(server, middleware, httpContext, handler);
99
2182
  };
100
2183
  }
2184
+ /**
2185
+ * Runs middleware attached to WebSocket upgrade requests.
2186
+ */
2187
+ async #runUpgradeMiddleware(server, middleware, httpContext, handler) {
2188
+ if (middleware.length === 0) {
2189
+ return handler(httpContext);
2190
+ }
2191
+ const middlewareClasses = await Promise.all(
2192
+ middleware.map(async (one) => {
2193
+ if (one.prototype?.handle) {
2194
+ return one;
2195
+ }
2196
+ const moduleExports = await one();
2197
+ return moduleExports.default;
2198
+ })
2199
+ );
2200
+ let outcome;
2201
+ await server.pipeline(middlewareClasses).errorHandler((error) => {
2202
+ throw error;
2203
+ }).finalHandler(async () => {
2204
+ outcome = { value: await handler(httpContext) };
2205
+ }).run(httpContext);
2206
+ if (!outcome) {
2207
+ throw new Error("WebSocket upgrade middleware must call next()");
2208
+ }
2209
+ return outcome.value;
2210
+ }
101
2211
  /**
102
2212
  * Scans and registers channel classes.
103
2213
  */
104
2214
  async #registerChannels(router) {
105
2215
  const logger = await this.#resolveLogger();
106
- const { glob } = await import("fs/promises");
107
- const { resolve } = await import("path");
108
- const config = this.app.config.get("socket", {});
109
- const patterns = config.channels?.patterns ?? ["app/channels/**/*_channel.{ts,js}"];
110
- for (const channelPattern of patterns) {
111
- const pattern = this.app.makePath(channelPattern);
112
- for await (const file of glob(pattern)) {
113
- try {
114
- const absolutePath = resolve(file);
115
- const module = await import(`file://${absolutePath}`);
116
- const channelClass = module.default;
117
- if (channelClass?.pattern) {
118
- router.register(channelClass);
119
- logger.info("registered channel: %s", channelClass.pattern);
120
- }
121
- } catch (error) {
122
- logger.warn("failed to load channel from %s: %s", file, error);
123
- }
2216
+ let generated;
2217
+ try {
2218
+ generated = await this.app.import("#generated/socket_channels");
2219
+ } catch (error) {
2220
+ throw new Error("Failed to load generated socket channels", { cause: error });
2221
+ }
2222
+ if (!Array.isArray(generated.socketChannels)) {
2223
+ throw new Error("Generated socket channel manifest must export a socketChannels array");
2224
+ }
2225
+ for (const [index, channelClass] of generated.socketChannels.entries()) {
2226
+ const source = `generated channel at index ${index}`;
2227
+ if (typeof channelClass !== "function" || !(channelClass.prototype instanceof BaseChannel) || !channelClass.pattern) {
2228
+ throw new Error(`Socket channel ${source} must default export a BaseChannel with a pattern`);
2229
+ }
2230
+ try {
2231
+ router.register(channelClass);
2232
+ } catch (error) {
2233
+ throw new Error(`Failed to register socket channel from ${source}`, { cause: error });
124
2234
  }
2235
+ logger.info("registered channel: %s", channelClass.pattern);
125
2236
  }
126
2237
  logger.info("total channels registered: %d", router.size);
127
2238
  }