@fluojs/socket.io 1.0.0-beta.1

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.
@@ -0,0 +1,780 @@
1
+ let _initClass;
2
+ function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
3
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
4
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
5
+ function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
+ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
+ import { Inject } from '@fluojs/core';
9
+ import { getClassDiMetadata } from '@fluojs/core/internal';
10
+ import { Server as BunEngineServer } from '@socket.io/bun-engine';
11
+ import { APPLICATION_LOGGER, COMPILED_MODULES, HTTP_APPLICATION_ADAPTER, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
12
+ import { getWebSocketGatewayMetadata, getWebSocketHandlerMetadataEntries } from '@fluojs/websockets';
13
+ import { Server } from 'socket.io';
14
+ import { SOCKETIO_OPTIONS_INTERNAL } from './options-token.internal.js';
15
+ const DEFAULT_SOCKETIO_SHUTDOWN_TIMEOUT_MS = 5_000;
16
+ const DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET = 128;
17
+ const DEFAULT_SOCKETIO_ENGINE_PATH = '/socket.io/';
18
+ const DEFAULT_SOCKETIO_MAX_HTTP_BUFFER_SIZE = 1_048_576;
19
+ function isFinitePositiveInteger(value) {
20
+ return typeof value === 'number' && Number.isInteger(value) && Number.isFinite(value) && value > 0;
21
+ }
22
+ function normalizeGuardRejection(result, defaultMessage) {
23
+ if (result === undefined || result === true) {
24
+ return undefined;
25
+ }
26
+ if (result === false) {
27
+ return {
28
+ message: defaultMessage
29
+ };
30
+ }
31
+ return result;
32
+ }
33
+ function createGuardError(rejection, defaultMessage) {
34
+ const error = new Error(rejection.message ?? defaultMessage);
35
+ if (rejection.data !== undefined) {
36
+ error.data = rejection.data;
37
+ }
38
+ return error;
39
+ }
40
+ function normalizeRejectedGuardError(error, defaultMessage) {
41
+ if (error instanceof Error) {
42
+ return {
43
+ data: error.data,
44
+ message: error.message || defaultMessage
45
+ };
46
+ }
47
+ if (typeof error === 'object' && error !== null) {
48
+ const candidate = error;
49
+ return {
50
+ data: candidate.data,
51
+ disconnect: candidate.disconnect,
52
+ message: candidate.message ?? defaultMessage
53
+ };
54
+ }
55
+ return {
56
+ message: defaultMessage
57
+ };
58
+ }
59
+ function scopeFromProvider(provider) {
60
+ if (typeof provider === 'function') {
61
+ return getClassDiMetadata(provider)?.scope ?? 'singleton';
62
+ }
63
+ if ('useClass' in provider) {
64
+ const classProvider = provider;
65
+ return classProvider.scope ?? getClassDiMetadata(classProvider.useClass)?.scope ?? 'singleton';
66
+ }
67
+ return 'scope' in provider ? provider.scope ?? 'singleton' : 'singleton';
68
+ }
69
+ function methodKeyToName(methodKey) {
70
+ return typeof methodKey === 'symbol' ? methodKey.toString() : methodKey;
71
+ }
72
+ function isClassProvider(provider) {
73
+ return typeof provider === 'object' && provider !== null && 'useClass' in provider;
74
+ }
75
+ function normalizeGatewayPath(path) {
76
+ if (path === '/') {
77
+ return '/';
78
+ }
79
+ const normalized = `/${path.replace(/^\/+/, '').replace(/\/+$/, '')}`;
80
+ return normalized === '' ? '/' : normalized;
81
+ }
82
+ function isNodeHttpServerLike(value) {
83
+ if (typeof value !== 'object' || value === null) {
84
+ return false;
85
+ }
86
+ const candidate = value;
87
+ return typeof candidate.close === 'function' && typeof candidate.emit === 'function' && typeof candidate.listeners === 'function' && typeof candidate.on === 'function' && typeof candidate.removeAllListeners === 'function';
88
+ }
89
+ function normalizeCorsForBunEngine(cors) {
90
+ if (cors === undefined) {
91
+ return undefined;
92
+ }
93
+ if (typeof cors === 'function') {
94
+ throw new Error('Socket.IO Bun bootstrap does not support CORS delegate functions. Use static CORS options with @fluojs/platform-bun.');
95
+ }
96
+ const origin = cors.origin;
97
+ if (typeof origin === 'function') {
98
+ throw new Error('Socket.IO Bun bootstrap does not support function-based cors.origin handlers. Use static origin values with @fluojs/platform-bun.');
99
+ }
100
+ if (Array.isArray(origin) && origin.some(value => typeof value === 'boolean')) {
101
+ throw new Error('Socket.IO Bun bootstrap does not support boolean entries inside cors.origin arrays. Use string or RegExp origins with @fluojs/platform-bun.');
102
+ }
103
+ return {
104
+ allowedHeaders: cors.allowedHeaders,
105
+ credentials: cors.credentials,
106
+ exposedHeaders: cors.exposedHeaders,
107
+ maxAge: cors.maxAge,
108
+ methods: cors.methods,
109
+ origin: Array.isArray(origin) ? origin.filter(value => typeof value === 'string' || value instanceof RegExp) : origin
110
+ };
111
+ }
112
+ function hasBunRealtimeBindingHost(adapter) {
113
+ return 'configureRealtimeBinding' in adapter && typeof adapter.configureRealtimeBinding === 'function';
114
+ }
115
+ function isFetchStyleRealtimeCapability(capability) {
116
+ return capability.kind === 'fetch-style' && capability.contract === 'raw-websocket-expansion';
117
+ }
118
+ function resolveSocketIoBootstrapRuntime(adapter) {
119
+ if (typeof adapter.getRealtimeCapability !== 'function') {
120
+ throw new Error('Socket.IO bootstrap requires an HTTP adapter with getRealtimeCapability(). Use a platform adapter that exposes a server-backed realtime capability or @fluojs/platform-bun for the official Bun engine path.');
121
+ }
122
+ const capability = adapter.getRealtimeCapability();
123
+ if (capability.kind === 'server-backed') {
124
+ return {
125
+ capability,
126
+ kind: 'server-backed'
127
+ };
128
+ }
129
+ if (!isFetchStyleRealtimeCapability(capability)) {
130
+ throw new Error(`Socket.IO bootstrap requires a server-backed realtime capability. ${capability.reason}`);
131
+ }
132
+ if (capability.support !== 'supported') {
133
+ throw new Error(`Socket.IO Bun bootstrap requires supported fetch-style websocket hosting. ${capability.reason}`);
134
+ }
135
+ if (!hasBunRealtimeBindingHost(adapter)) {
136
+ throw new Error('Socket.IO Bun bootstrap requires the selected adapter to expose Bun realtime binding configuration. Use @fluojs/platform-bun for the official Bun engine path.');
137
+ }
138
+ return {
139
+ bindingHost: adapter,
140
+ capability,
141
+ kind: 'bun'
142
+ };
143
+ }
144
+ function extractPayload(args) {
145
+ if (args.length === 0) {
146
+ return undefined;
147
+ }
148
+ const effectiveArgs = typeof args.at(-1) === 'function' ? args.slice(0, -1) : args;
149
+ if (effectiveArgs.length === 0) {
150
+ return undefined;
151
+ }
152
+ return effectiveArgs.length === 1 ? effectiveArgs[0] : effectiveArgs;
153
+ }
154
+
155
+ /**
156
+ * Lifecycle service that boots Socket.IO gateways, exposes the raw server, and implements room helpers.
157
+ *
158
+ * @remarks
159
+ * This service preserves the README-level gateway contracts for namespace discovery, buffered pre-ready events,
160
+ * Bun engine hosting, and graceful shutdown behavior.
161
+ */
162
+ let _SocketIoLifecycleSer;
163
+ class SocketIoLifecycleService {
164
+ static {
165
+ [_SocketIoLifecycleSer, _initClass] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, HTTP_APPLICATION_ADAPTER, SOCKETIO_OPTIONS_INTERNAL)], []).c;
166
+ }
167
+ attachments = [];
168
+ bunEngine;
169
+ io;
170
+ namespaceContext = new AsyncLocalStorage();
171
+ socketRegistry = new Map();
172
+ shutdownPromise;
173
+ wired = false;
174
+ constructor(runtimeContainer, compiledModules, logger, adapter, moduleOptions) {
175
+ this.runtimeContainer = runtimeContainer;
176
+ this.compiledModules = compiledModules;
177
+ this.logger = logger;
178
+ this.adapter = adapter;
179
+ this.moduleOptions = moduleOptions;
180
+ }
181
+
182
+ /**
183
+ * Returns the managed Socket.IO server instance, creating and binding it on first access.
184
+ *
185
+ * @returns The shared Socket.IO `Server` used by this adapter instance.
186
+ * @throws {Error} When the selected realtime capability cannot expose the server contract Socket.IO requires.
187
+ */
188
+ getServer() {
189
+ if (this.io) {
190
+ return this.io;
191
+ }
192
+ const runtime = resolveSocketIoBootstrapRuntime(this.adapter);
193
+ if (runtime.kind === 'server-backed') {
194
+ const httpServer = runtime.capability.server;
195
+ if (!isNodeHttpServerLike(httpServer)) {
196
+ throw new Error('Socket.IO bootstrap requires the selected realtime capability to expose a Node HTTP/S server instance.');
197
+ }
198
+ this.io = new Server(httpServer, this.createServerOptions());
199
+ return this.io;
200
+ }
201
+ this.io = new Server(this.createServerOptions());
202
+ this.installBunSocketIoBinding(runtime, this.io);
203
+ return this.io;
204
+ }
205
+
206
+ /**
207
+ * Discovers gateway classes and binds their handlers to the resolved namespaces once per application lifecycle.
208
+ */
209
+ async onApplicationBootstrap() {
210
+ if (this.wired) {
211
+ return;
212
+ }
213
+ const descriptors = this.discoverGatewayDescriptors();
214
+ if (descriptors.length === 0) {
215
+ return;
216
+ }
217
+ this.assertNoServerBackedGatewayOptIn(descriptors);
218
+ const io = this.getServer();
219
+ const attachments = this.prepareNamespaceAttachments(io, descriptors);
220
+ for (const attachment of attachments) {
221
+ this.bindNamespaceHandlers(attachment);
222
+ }
223
+ this.attachments = attachments;
224
+ this.wired = true;
225
+ }
226
+
227
+ /**
228
+ * Shuts down Socket.IO resources during application shutdown.
229
+ */
230
+ async onApplicationShutdown() {
231
+ await this.shutdown();
232
+ }
233
+
234
+ /**
235
+ * Shuts down Socket.IO resources when the containing module is destroyed.
236
+ */
237
+ async onModuleDestroy() {
238
+ await this.shutdown();
239
+ }
240
+
241
+ /**
242
+ * Adds a socket to one room, using the current gateway namespace or an explicit namespace override.
243
+ *
244
+ * @param socketId Socket identifier to move into the room.
245
+ * @param room Room identifier to join.
246
+ * @param namespacePath Optional namespace path required when the helper runs outside gateway handler context.
247
+ */
248
+ joinRoom(socketId, room, namespacePath) {
249
+ const socket = this.resolveSocket(socketId);
250
+ if (socket) {
251
+ void socket.join(room);
252
+ return;
253
+ }
254
+ this.resolveRequiredNamespace(namespacePath).in(socketId).socketsJoin(room);
255
+ }
256
+
257
+ /**
258
+ * Removes a socket from one room, using the current gateway namespace or an explicit namespace override.
259
+ *
260
+ * @param socketId Socket identifier to remove from the room.
261
+ * @param room Room identifier to leave.
262
+ * @param namespacePath Optional namespace path required when the helper runs outside gateway handler context.
263
+ */
264
+ leaveRoom(socketId, room, namespacePath) {
265
+ const socket = this.resolveSocket(socketId);
266
+ if (socket) {
267
+ void socket.leave(room);
268
+ return;
269
+ }
270
+ this.resolveRequiredNamespace(namespacePath).in(socketId).socketsLeave(room);
271
+ }
272
+
273
+ /**
274
+ * Emits one event payload to every socket currently joined to a room.
275
+ *
276
+ * @param room Room identifier that should receive the event.
277
+ * @param event Socket.IO event name emitted to the room.
278
+ * @param data Payload delivered with the event.
279
+ * @param namespacePath Optional namespace path required when the helper runs outside gateway handler context.
280
+ */
281
+ broadcastToRoom(room, event, data, namespacePath) {
282
+ this.resolveRequiredNamespace(namespacePath).to(room).emit(event, data);
283
+ }
284
+
285
+ /**
286
+ * Returns the current room set tracked for one connected socket.
287
+ *
288
+ * @param socketId Socket identifier to inspect.
289
+ * @returns A snapshot of the rooms currently joined by that socket.
290
+ */
291
+ getRooms(socketId) {
292
+ const socket = this.socketRegistry.get(socketId);
293
+ if (!socket) {
294
+ return new Set();
295
+ }
296
+ return new Set(socket.rooms);
297
+ }
298
+ createServerOptions() {
299
+ const options = {};
300
+ options.cors = this.resolveCorsOptions();
301
+ options.maxHttpBufferSize = this.resolveMaxHttpBufferSize();
302
+ if (this.moduleOptions.transports !== undefined) {
303
+ options.transports = this.moduleOptions.transports;
304
+ }
305
+ return options;
306
+ }
307
+ createBunEngineOptions() {
308
+ const options = {
309
+ path: DEFAULT_SOCKETIO_ENGINE_PATH
310
+ };
311
+ options.cors = normalizeCorsForBunEngine(this.resolveCorsOptions());
312
+ return options;
313
+ }
314
+ resolveCorsOptions() {
315
+ return this.moduleOptions.cors ?? {
316
+ credentials: false,
317
+ origin: false
318
+ };
319
+ }
320
+ resolveMaxHttpBufferSize() {
321
+ const configured = this.moduleOptions.engine?.maxHttpBufferSize;
322
+ if (!isFinitePositiveInteger(configured)) {
323
+ return DEFAULT_SOCKETIO_MAX_HTTP_BUFFER_SIZE;
324
+ }
325
+ return configured;
326
+ }
327
+ installBunSocketIoBinding(runtime, io) {
328
+ if (this.bunEngine) {
329
+ return;
330
+ }
331
+ const engine = new BunEngineServer(this.createBunEngineOptions());
332
+ io.bind(engine);
333
+ runtime.bindingHost.configureRealtimeBinding(this.createBunSocketIoBinding(engine));
334
+ this.bunEngine = engine;
335
+ }
336
+ createBunSocketIoBinding(engine) {
337
+ const handler = engine.handler();
338
+ return {
339
+ fetch: async (request, server) => {
340
+ const pathname = this.tryResolvePathname(request.url);
341
+ if (!this.matchesSocketIoEnginePath(pathname)) {
342
+ return undefined;
343
+ }
344
+ return await engine.handleRequest(request, server);
345
+ },
346
+ idleTimeout: handler.idleTimeout,
347
+ maxRequestBodySize: handler.maxRequestBodySize,
348
+ websocket: {
349
+ close: handler.websocket.close,
350
+ maxPayloadLength: handler.websocket.maxPayloadLength,
351
+ message: handler.websocket.message,
352
+ open: handler.websocket.open
353
+ }
354
+ };
355
+ }
356
+ tryResolvePathname(url) {
357
+ try {
358
+ return new URL(url).pathname;
359
+ } catch {
360
+ return undefined;
361
+ }
362
+ }
363
+ matchesSocketIoEnginePath(pathname) {
364
+ return pathname === '/socket.io' || pathname === DEFAULT_SOCKETIO_ENGINE_PATH;
365
+ }
366
+ assertNoServerBackedGatewayOptIn(descriptors) {
367
+ const runtime = resolveSocketIoBootstrapRuntime(this.adapter);
368
+ if (runtime.kind !== 'bun') {
369
+ return;
370
+ }
371
+ const descriptor = descriptors.find(entry => entry.serverBacked !== undefined);
372
+ if (!descriptor) {
373
+ return;
374
+ }
375
+ throw new Error(`@WebSocketGateway({ serverBacked }) is not supported on @fluojs/socket.io when using @fluojs/platform-bun. Gateway path ${descriptor.path} must use the official Bun engine host instead.`);
376
+ }
377
+ prepareNamespaceAttachments(io, descriptors) {
378
+ const attachmentsByPath = new Map();
379
+ for (const descriptor of descriptors) {
380
+ const current = attachmentsByPath.get(descriptor.path);
381
+ if (current) {
382
+ current.descriptors.push(descriptor);
383
+ continue;
384
+ }
385
+ attachmentsByPath.set(descriptor.path, {
386
+ descriptors: [descriptor],
387
+ namespace: descriptor.path === '/' ? io.of('/') : io.of(descriptor.path),
388
+ path: descriptor.path
389
+ });
390
+ }
391
+ return Array.from(attachmentsByPath.values());
392
+ }
393
+ resolveNamespace(path) {
394
+ return this.attachments.find(attachment => attachment.path === path)?.namespace;
395
+ }
396
+ resolveContextNamespace() {
397
+ const namespaceName = this.namespaceContext.getStore();
398
+ if (!namespaceName) {
399
+ return undefined;
400
+ }
401
+ return this.resolveNamespace(namespaceName);
402
+ }
403
+ resolveRequiredNamespace(namespacePath) {
404
+ const namespace = namespacePath ? this.resolveNamespace(normalizeGatewayPath(namespacePath)) : this.resolveContextNamespace();
405
+ if (!namespace) {
406
+ throw new Error('Socket.IO room helpers require an explicit namespace outside gateway handler context.');
407
+ }
408
+ return namespace;
409
+ }
410
+ resolveSocket(socketId) {
411
+ const registered = this.socketRegistry.get(socketId);
412
+ if (registered) {
413
+ return registered;
414
+ }
415
+ for (const attachment of this.attachments) {
416
+ const socket = attachment.namespace.sockets.get(socketId);
417
+ if (socket) {
418
+ this.socketRegistry.set(socketId, socket);
419
+ return socket;
420
+ }
421
+ }
422
+ return undefined;
423
+ }
424
+ bindNamespaceHandlers(attachment) {
425
+ if (this.moduleOptions.auth?.connection) {
426
+ attachment.namespace.use((socket, next) => {
427
+ void this.runConnectionGuard(attachment.path, socket).then(() => next()).catch(error => {
428
+ next(error instanceof Error ? error : new Error('Socket.IO connection rejected.'));
429
+ });
430
+ });
431
+ }
432
+ attachment.namespace.on('connection', socket => {
433
+ void this.bindConnectionHandlers(attachment.descriptors, socket);
434
+ });
435
+ }
436
+ async runConnectionGuard(path, socket) {
437
+ const guard = this.moduleOptions.auth?.connection;
438
+ if (!guard) {
439
+ return;
440
+ }
441
+ const rejection = normalizeGuardRejection(await guard({
442
+ activeConnectionCount: this.socketRegistry.size,
443
+ namespacePath: path,
444
+ request: socket.request,
445
+ socket
446
+ }), 'Socket.IO connection rejected.');
447
+ if (rejection) {
448
+ throw createGuardError(rejection, 'Socket.IO connection rejected.');
449
+ }
450
+ }
451
+ async bindConnectionHandlers(descriptors, socket) {
452
+ const request = socket.request;
453
+ const resolved = await this.resolveConnectionGateways(descriptors);
454
+ const state = this.createConnectionHandlerState();
455
+ this.socketRegistry.set(socket.id, socket);
456
+ this.attachConnectionListeners(state, resolved, socket, request);
457
+ await this.runConnectHandlers(resolved, socket, request);
458
+ state.handlersReady = true;
459
+ await this.replayBufferedConnectionEvents(state, resolved, socket, request);
460
+ }
461
+ createConnectionHandlerState() {
462
+ return {
463
+ bufferedDisconnect: undefined,
464
+ bufferedMessages: [],
465
+ handlersReady: false
466
+ };
467
+ }
468
+ maxPendingMessagesPerSocket() {
469
+ return isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
470
+ }
471
+ attachConnectionListeners(state, resolved, socket, request) {
472
+ socket.onAny((event, ...args) => {
473
+ const ack = typeof args.at(-1) === 'function' ? args.at(-1) : undefined;
474
+ if (!state.handlersReady) {
475
+ const limit = this.maxPendingMessagesPerSocket();
476
+ const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
477
+ if (state.bufferedMessages.length >= limit) {
478
+ if (policy === 'close') {
479
+ socket.disconnect(true);
480
+ state.bufferedMessages = [];
481
+ this.socketRegistry.delete(socket.id);
482
+ this.logger.warn(`Socket.IO connection ${socket.id} exceeded pending message buffer limit (${String(limit)}). Connection terminated.`, 'SocketIoLifecycleService');
483
+ return;
484
+ }
485
+ if (policy === 'drop-newest') {
486
+ this.logger.warn(`Socket.IO connection ${socket.id} dropped an incoming message due to pending buffer limit (${String(limit)}).`, 'SocketIoLifecycleService');
487
+ return;
488
+ }
489
+ state.bufferedMessages.shift();
490
+ this.logger.warn(`Socket.IO connection ${socket.id} dropped the oldest pending message because buffer limit (${String(limit)}) was reached.`, 'SocketIoLifecycleService');
491
+ }
492
+ state.bufferedMessages.push({
493
+ acknowledgement: ack,
494
+ event,
495
+ payload: extractPayload(args)
496
+ });
497
+ return;
498
+ }
499
+ void this.handleMessage(resolved, socket, request, event, extractPayload(args), ack);
500
+ });
501
+ socket.on('error', error => {
502
+ this.socketRegistry.delete(socket.id);
503
+ this.logger.error('Socket.IO gateway socket emitted an error.', error, 'SocketIoLifecycleService');
504
+ });
505
+ socket.on('disconnect', (reason, description) => {
506
+ if (!state.handlersReady) {
507
+ state.bufferedDisconnect = {
508
+ description,
509
+ reason
510
+ };
511
+ return;
512
+ }
513
+ void this.handleDisconnect(resolved, socket, reason, description).finally(() => {
514
+ this.socketRegistry.delete(socket.id);
515
+ });
516
+ });
517
+ }
518
+ async replayBufferedConnectionEvents(state, resolved, socket, request) {
519
+ for (const message of state.bufferedMessages) {
520
+ await this.handleMessage(resolved, socket, request, message.event, message.payload, message.acknowledgement);
521
+ }
522
+ state.bufferedMessages = [];
523
+ if (state.bufferedDisconnect) {
524
+ const disconnectEvent = state.bufferedDisconnect;
525
+ state.bufferedDisconnect = undefined;
526
+ await this.handleDisconnect(resolved, socket, disconnectEvent.reason, disconnectEvent.description);
527
+ this.socketRegistry.delete(socket.id);
528
+ return;
529
+ }
530
+ if (socket.disconnected) {
531
+ this.socketRegistry.delete(socket.id);
532
+ }
533
+ }
534
+ async resolveConnectionGateways(descriptors) {
535
+ const resolved = [];
536
+ for (const descriptor of descriptors) {
537
+ const instance = await this.resolveGatewayInstance(descriptor);
538
+ if (instance !== undefined) {
539
+ resolved.push({
540
+ descriptor,
541
+ instance
542
+ });
543
+ }
544
+ }
545
+ return resolved;
546
+ }
547
+ async runConnectHandlers(resolved, socket, request) {
548
+ for (const {
549
+ descriptor,
550
+ instance
551
+ } of resolved) {
552
+ await this.runHandlers(instance, descriptor, 'connect', socket, request);
553
+ }
554
+ }
555
+ async handleMessage(resolved, socket, request, event, payload, acknowledgement) {
556
+ const hasMatchingHandlers = resolved.some(({
557
+ descriptor
558
+ }) => this.selectMessageHandlers(descriptor, event).length > 0);
559
+ if (!hasMatchingHandlers) {
560
+ return;
561
+ }
562
+ let rejection;
563
+ try {
564
+ rejection = await this.resolveMessageGuardRejection(socket, request, event, payload);
565
+ } catch (error) {
566
+ this.logger.error(`Socket.IO message guard for event ${event} on socket ${socket.id} rejected unexpectedly.`, error, 'SocketIoLifecycleService');
567
+ rejection = normalizeRejectedGuardError(error, 'Socket.IO message rejected.');
568
+ }
569
+ if (rejection) {
570
+ this.reportRejectedMessage(socket, event, acknowledgement, rejection);
571
+ return;
572
+ }
573
+ for (const {
574
+ descriptor,
575
+ instance
576
+ } of resolved) {
577
+ const handlers = this.selectMessageHandlers(descriptor, event);
578
+ for (const handler of handlers) {
579
+ await this.invokeGatewayMethod(instance, descriptor, handler, [payload, socket, request, acknowledgement]);
580
+ }
581
+ }
582
+ }
583
+ async resolveMessageGuardRejection(socket, request, event, payload) {
584
+ const guard = this.moduleOptions.auth?.message;
585
+ if (!guard) {
586
+ return undefined;
587
+ }
588
+ const rejection = normalizeGuardRejection(await guard({
589
+ activeConnectionCount: this.socketRegistry.size,
590
+ event,
591
+ namespacePath: socket.nsp.name,
592
+ payload,
593
+ request,
594
+ socket
595
+ }), 'Socket.IO message rejected.');
596
+ return rejection;
597
+ }
598
+ reportRejectedMessage(socket, event, acknowledgement, rejection) {
599
+ this.logger.warn(`Socket.IO message ${event} for socket ${socket.id} was rejected before handler dispatch.`, 'SocketIoLifecycleService');
600
+ if (acknowledgement) {
601
+ acknowledgement({
602
+ data: rejection.data,
603
+ error: rejection.message ?? 'Socket.IO message rejected.'
604
+ });
605
+ }
606
+ if (rejection.disconnect === true) {
607
+ socket.disconnect(true);
608
+ }
609
+ }
610
+ selectMessageHandlers(descriptor, event) {
611
+ return descriptor.handlers.filter(handler => handler.type === 'message' && (handler.event === undefined || handler.event === event));
612
+ }
613
+ async handleDisconnect(resolved, socket, reason, description) {
614
+ for (const {
615
+ descriptor,
616
+ instance
617
+ } of resolved) {
618
+ await this.runHandlers(instance, descriptor, 'disconnect', socket, reason, description);
619
+ }
620
+ }
621
+ async runHandlers(instance, descriptor, type, ...args) {
622
+ const handlers = descriptor.handlers.filter(handler => handler.type === type);
623
+ for (const handler of handlers) {
624
+ await this.invokeGatewayMethod(instance, descriptor, handler, args);
625
+ }
626
+ }
627
+ async invokeGatewayMethod(instance, descriptor, handler, args) {
628
+ const value = instance[handler.methodKey];
629
+ if (typeof value !== 'function') {
630
+ this.logger.warn(`Socket.IO gateway handler ${descriptor.targetName}.${handler.methodName} is not callable and was skipped.`, 'SocketIoLifecycleService');
631
+ return;
632
+ }
633
+ try {
634
+ await this.namespaceContext.run(descriptor.path, async () => await Promise.resolve(value.call(instance, ...args)));
635
+ } catch (error) {
636
+ this.logger.error(`Socket.IO gateway handler ${descriptor.targetName}.${handler.methodName} failed.`, error, 'SocketIoLifecycleService');
637
+ }
638
+ }
639
+ async resolveGatewayInstance(descriptor) {
640
+ try {
641
+ return await this.runtimeContainer.resolve(descriptor.token);
642
+ } catch (error) {
643
+ this.logger.error(`Failed to resolve Socket.IO gateway ${descriptor.targetName} from module ${descriptor.moduleName}.`, error, 'SocketIoLifecycleService');
644
+ return undefined;
645
+ }
646
+ }
647
+ discoverGatewayDescriptors() {
648
+ const seenTargets = new Set();
649
+ const descriptors = [];
650
+ for (const candidate of this.discoveryCandidates()) {
651
+ const gatewayMetadata = getWebSocketGatewayMetadata(candidate.targetType);
652
+ if (!gatewayMetadata) {
653
+ continue;
654
+ }
655
+ if (this.shouldSkipGatewayCandidate(candidate, seenTargets)) {
656
+ continue;
657
+ }
658
+ seenTargets.add(candidate.targetType);
659
+ descriptors.push(this.createGatewayDescriptor(candidate, gatewayMetadata.path));
660
+ }
661
+ return descriptors;
662
+ }
663
+ shouldSkipGatewayCandidate(candidate, seenTargets) {
664
+ if (candidate.scope !== 'singleton') {
665
+ this.logger.warn(`${candidate.targetType.name} in module ${candidate.moduleName} declares @WebSocketGateway() but is registered with ${candidate.scope} scope. Socket.IO gateways are registered only for singleton providers.`, 'SocketIoLifecycleService');
666
+ return true;
667
+ }
668
+ return seenTargets.has(candidate.targetType);
669
+ }
670
+ createGatewayDescriptor(candidate, path) {
671
+ const metadata = getWebSocketGatewayMetadata(candidate.targetType);
672
+ const entries = getWebSocketHandlerMetadataEntries(candidate.targetType.prototype);
673
+ return {
674
+ handlers: entries.map(entry => ({
675
+ event: entry.metadata.event,
676
+ methodKey: entry.propertyKey,
677
+ methodName: methodKeyToName(entry.propertyKey),
678
+ type: entry.metadata.type
679
+ })),
680
+ moduleName: candidate.moduleName,
681
+ path: normalizeGatewayPath(path),
682
+ serverBacked: metadata?.serverBacked,
683
+ targetName: candidate.targetType.name,
684
+ token: candidate.token
685
+ };
686
+ }
687
+ discoveryCandidates() {
688
+ const candidates = [];
689
+ for (const compiledModule of this.compiledModules) {
690
+ for (const provider of compiledModule.definition.providers ?? []) {
691
+ if (typeof provider === 'function') {
692
+ candidates.push({
693
+ moduleName: compiledModule.type.name,
694
+ scope: scopeFromProvider(provider),
695
+ targetType: provider,
696
+ token: provider
697
+ });
698
+ continue;
699
+ }
700
+ if (isClassProvider(provider)) {
701
+ candidates.push({
702
+ moduleName: compiledModule.type.name,
703
+ scope: scopeFromProvider(provider),
704
+ targetType: provider.useClass,
705
+ token: provider.provide
706
+ });
707
+ }
708
+ }
709
+ for (const controller of compiledModule.definition.controllers ?? []) {
710
+ candidates.push({
711
+ moduleName: compiledModule.type.name,
712
+ scope: scopeFromProvider(controller),
713
+ targetType: controller,
714
+ token: controller
715
+ });
716
+ }
717
+ }
718
+ return candidates;
719
+ }
720
+ resolveShutdownTimeoutMs() {
721
+ const configured = this.moduleOptions.shutdown?.timeoutMs;
722
+ if (typeof configured !== 'number' || !Number.isFinite(configured) || configured <= 0) {
723
+ return DEFAULT_SOCKETIO_SHUTDOWN_TIMEOUT_MS;
724
+ }
725
+ return Math.floor(configured);
726
+ }
727
+ async shutdown() {
728
+ if (this.shutdownPromise) {
729
+ await this.shutdownPromise;
730
+ return;
731
+ }
732
+ this.shutdownPromise = this.runShutdownLifecycle();
733
+ await this.shutdownPromise;
734
+ }
735
+ async runShutdownLifecycle() {
736
+ const io = this.io;
737
+ this.attachments = [];
738
+ this.wired = false;
739
+ if (!io) {
740
+ this.socketRegistry.clear();
741
+ return;
742
+ }
743
+ try {
744
+ await this.closeServerWithTimeout(io, this.resolveShutdownTimeoutMs());
745
+ } catch (error) {
746
+ this.logger.error(`Failed to close Socket.IO server within ${String(this.resolveShutdownTimeoutMs())}ms.`, error, 'SocketIoLifecycleService');
747
+ } finally {
748
+ this.io = undefined;
749
+ this.bunEngine = undefined;
750
+ if (hasBunRealtimeBindingHost(this.adapter)) {
751
+ this.adapter.configureRealtimeBinding(undefined);
752
+ }
753
+ this.socketRegistry.clear();
754
+ }
755
+ }
756
+ closeServerWithTimeout(io, timeoutMs) {
757
+ return new Promise((resolve, reject) => {
758
+ let settled = false;
759
+ const timeout = setTimeout(() => {
760
+ if (settled) {
761
+ return;
762
+ }
763
+ settled = true;
764
+ reject(new Error(`Timed out while closing Socket.IO server after ${String(timeoutMs)}ms.`));
765
+ }, timeoutMs);
766
+ io.close(() => {
767
+ if (settled) {
768
+ return;
769
+ }
770
+ settled = true;
771
+ clearTimeout(timeout);
772
+ resolve();
773
+ });
774
+ });
775
+ }
776
+ static {
777
+ _initClass();
778
+ }
779
+ }
780
+ export { _SocketIoLifecycleSer as SocketIoLifecycleService };