@fluojs/websockets 1.0.0-beta.6 → 1.0.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.
package/README.ko.md CHANGED
@@ -102,7 +102,7 @@ WebSocketModule.forRoot({
102
102
  });
103
103
  ```
104
104
 
105
- 옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수, inbound payload 크기, pending message buffer, shutdown cleanup에 bounded default를 적용합니다. 기본값은 `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, Node backpressure `maxBufferedAmountBytes: 1 MiB`와 drop behavior입니다. 또한 server-backed Node listener는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat timer를 활성화합니다. 공식 fetch-style runtime module(`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, `@fluojs/websockets/cloudflare-workers`)은 애플리케이션 shutdown 시 추적 중인 websocket 클라이언트를 닫고 `shutdown.timeoutMs` 범위 안에서 `@OnDisconnect()` cleanup이 마무리될 수 있도록 bounded 기회를 제공합니다.
105
+ 옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수, inbound payload 크기, pending message buffer, shutdown cleanup에 bounded default를 적용합니다. 기본값은 `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, Node backpressure `maxBufferedAmountBytes: 1 MiB`와 drop behavior입니다. 또한 server-backed Node listener는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat timer를 활성화합니다. Node shutdown은 shutdown이 시작된 in-flight async upgrade를 거절하고, 애플리케이션 shutdown 시 추적 중인 websocket 클라이언트를 닫고, `shutdown.timeoutMs` 범위 안에서 `@OnDisconnect()` cleanup이 마무리될 수 있도록 bounded 기회를 제공합니다. 공식 fetch-style runtime module(`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, `@fluojs/websockets/cloudflare-workers`)도 애플리케이션 shutdown 중 동일한 bounded close와 disconnect cleanup 동작을 제공합니다.
106
106
 
107
107
  ## 바이너리 페이로드
108
108
 
@@ -117,6 +117,7 @@ Gateway `@OnMessage()` handler는 지원 런타임 전반에서 하나의 정규
117
117
  - `WebSocketModule`: WebSocket 통합을 위한 루트 모듈입니다.
118
118
  - `WebSocketModule.forRoot({ upgrade, limits, backpressure, buffer, heartbeat, shutdown })`: pre-upgrade guard와 bounded runtime default를 구성합니다.
119
119
  - `WebSocketGatewayLifecycleService`: 기본 Node.js 기반 lifecycle service token을 위한 루트 alias입니다.
120
+ - `WebSocketRoomService`: websocket room join, leave, broadcast, 조회를 위해 runtime lifecycle service가 구현하는 Room management contract입니다.
120
121
  - Metadata helper와 symbol: `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`.
121
122
 
122
123
  ## 런타임별 서브패스
package/README.md CHANGED
@@ -102,7 +102,7 @@ WebSocketModule.forRoot({
102
102
  });
103
103
  ```
104
104
 
105
- When omitted, `@fluojs/websockets` applies bounded defaults for concurrent connections, inbound payload size, pending message buffers, and shutdown cleanup. Default settings are `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, and Node backpressure `maxBufferedAmountBytes: 1 MiB` with drop behavior. Server-backed Node listeners enable heartbeat timers unless you explicitly set `heartbeat.enabled` to `false`. The official fetch-style runtime modules (`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, and `@fluojs/websockets/cloudflare-workers`) close tracked websocket clients during application shutdown and give `@OnDisconnect()` cleanup a bounded chance to finish within `shutdown.timeoutMs`.
105
+ When omitted, `@fluojs/websockets` applies bounded defaults for concurrent connections, inbound payload size, pending message buffers, and shutdown cleanup. Default settings are `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, and Node backpressure `maxBufferedAmountBytes: 1 MiB` with drop behavior. Server-backed Node listeners enable heartbeat timers unless you explicitly set `heartbeat.enabled` to `false`. Node shutdown rejects in-flight async upgrades once shutdown begins, will close tracked websocket clients during application shutdown, and gives `@OnDisconnect()` cleanup a bounded chance to finish within `shutdown.timeoutMs`. The official fetch-style runtime modules (`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, and `@fluojs/websockets/cloudflare-workers`) provide the same bounded close and disconnect cleanup behavior during application shutdown.
106
106
 
107
107
  ## Binary Payloads
108
108
 
@@ -117,6 +117,7 @@ Gateway `@OnMessage()` handlers receive one normalized payload contract across s
117
117
  - `WebSocketModule`: Root module for WebSocket integration.
118
118
  - `WebSocketModule.forRoot({ upgrade, limits, backpressure, buffer, heartbeat, shutdown })`: Configures pre-upgrade guards and bounded runtime defaults.
119
119
  - `WebSocketGatewayLifecycleService`: Root alias for the default Node.js-backed lifecycle service token.
120
+ - `WebSocketRoomService`: Room management contract implemented by runtime lifecycle services for joining, leaving, broadcasting to, and inspecting websocket rooms.
120
121
  - Metadata helpers and symbols: `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`.
121
122
 
122
123
  ## Runtime-Specific Subpaths
@@ -18,7 +18,9 @@ export declare class NodeWebSocketGatewayLifecycleService implements OnApplicati
18
18
  private readonly moduleOptions;
19
19
  private attachments;
20
20
  private heartbeatTimer;
21
+ private isShuttingDown;
21
22
  private ownedUpgradeServers;
23
+ private readonly pendingUpgradeOperations;
22
24
  private pendingUpgradeReservations;
23
25
  private readonly pingPending;
24
26
  private readonly pingSentAt;
@@ -26,6 +28,7 @@ export declare class NodeWebSocketGatewayLifecycleService implements OnApplicati
26
28
  private shutdownPromise;
27
29
  private readonly socketRegistry;
28
30
  private readonly socketRooms;
31
+ private readonly socketStates;
29
32
  private upgradeListener;
30
33
  private upgradeServer;
31
34
  constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, adapter: HttpApplicationAdapter, moduleOptions: WebSocketModuleOptions);
@@ -58,6 +61,8 @@ export declare class NodeWebSocketGatewayLifecycleService implements OnApplicati
58
61
  private registerSocketConnection;
59
62
  private finalizeConnectionBinding;
60
63
  private createConnectionHandlerState;
64
+ private settleConnectLifecycle;
65
+ private settleDisconnectLifecycle;
61
66
  private getBufferedMessageCount;
62
67
  private getQueuedMessageCount;
63
68
  private maybeCompactBufferedMessages;
@@ -85,13 +90,17 @@ export declare class NodeWebSocketGatewayLifecycleService implements OnApplicati
85
90
  private closeServerWithTimeout;
86
91
  private shutdown;
87
92
  private runShutdownLifecycle;
93
+ private trackPendingUpgradeOperation;
94
+ private awaitPendingUpgradeOperations;
88
95
  private stopHeartbeat;
89
96
  private detachUpgradeServerListener;
90
97
  private closeOwnedUpgradeServers;
91
98
  private closeOwnedUpgradeServerWithTimeout;
92
99
  private listenOwnedGatewayServer;
93
100
  private closeGatewayAttachments;
94
- private terminateAttachmentClients;
101
+ private closeAttachmentClients;
102
+ private awaitHandlerQueueDrain;
103
+ private scheduleSocketStateCleanup;
95
104
  private closeGatewayAttachment;
96
105
  private clearConnectionTrackingState;
97
106
  /**
@@ -124,6 +133,8 @@ export declare class NodeWebSocketGatewayLifecycleService implements OnApplicati
124
133
  */
125
134
  getRooms(socketId: string): ReadonlySet<string>;
126
135
  private startHeartbeat;
136
+ private unregisterSocketWithDeferredStateCleanup;
137
+ private unregisterTrackedSocketWithDeferredStateCleanup;
127
138
  private unregisterSocket;
128
139
  }
129
140
  //# sourceMappingURL=node-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-service.d.ts","sourceRoot":"","sources":["../../src/node/node-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-service.d.ts","sourceRoot":"","sources":["../../src/node/node-service.ts"],"names":[],"mappings":"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"}
@@ -103,6 +103,16 @@ function rejectUpgradeRequestWithStatus(socket, rejection) {
103
103
  socket.write(response);
104
104
  socket.destroy();
105
105
  }
106
+ function createCompletionSignal() {
107
+ let resolve;
108
+ const promise = new Promise(res => {
109
+ resolve = res;
110
+ });
111
+ return {
112
+ promise,
113
+ resolve
114
+ };
115
+ }
106
116
 
107
117
  /**
108
118
  * Lifecycle service that discovers WebSocket gateways, attaches upgrade listeners, and manages room state.
@@ -118,7 +128,9 @@ class NodeWebSocketGatewayLifecycleService {
118
128
  }
119
129
  attachments = [];
120
130
  heartbeatTimer;
131
+ isShuttingDown = false;
121
132
  ownedUpgradeServers = [];
133
+ pendingUpgradeOperations = new Set();
122
134
  pendingUpgradeReservations = 0;
123
135
  pingPending = new Set();
124
136
  pingSentAt = new Map();
@@ -126,6 +138,7 @@ class NodeWebSocketGatewayLifecycleService {
126
138
  shutdownPromise;
127
139
  socketRegistry = new Map();
128
140
  socketRooms = new Map();
141
+ socketStates = new Map();
129
142
  upgradeListener;
130
143
  upgradeServer;
131
144
  constructor(runtimeContainer, compiledModules, logger, adapter, moduleOptions) {
@@ -272,7 +285,7 @@ class NodeWebSocketGatewayLifecycleService {
272
285
  createUpgradeListener(upgradeServer, attachmentsByPath) {
273
286
  return (request, socket, head) => {
274
287
  socket.pause();
275
- void this.handleUpgradeRequest(upgradeServer, attachmentsByPath, request, socket, head).catch(error => {
288
+ void this.trackPendingUpgradeOperation(this.handleUpgradeRequest(upgradeServer, attachmentsByPath, request, socket, head)).catch(error => {
276
289
  this.logger.error('WebSocket upgrade admission failed.', error, 'WebSocketGatewayLifecycleService');
277
290
  rejectUpgradeRequestWithStatus(socket, {
278
291
  body: 'Internal server error',
@@ -328,13 +341,25 @@ class NodeWebSocketGatewayLifecycleService {
328
341
  const state = this.createConnectionHandlerState();
329
342
  this.registerSocketConnection(state, socket);
330
343
  this.attachConnectionListeners(state, socket, request);
331
- await this.resolveConnectionGateways(descriptors, state);
332
- await this.runConnectHandlers(state, socket, request);
333
- await this.finalizeConnectionBinding(state, socket, request);
344
+ try {
345
+ await this.resolveConnectionGateways(descriptors, state);
346
+ await this.runConnectHandlers(state, socket, request);
347
+ await this.finalizeConnectionBinding(state, socket, request);
348
+ if (this.isShuttingDown && socket.readyState === WebSocket.OPEN) {
349
+ socket.close(1001, 'Server shutting down');
350
+ await state.disconnectLifecyclePromise;
351
+ }
352
+ } finally {
353
+ if (!state.handlersReady && state.bufferedDisconnect) {
354
+ this.settleDisconnectLifecycle(state);
355
+ }
356
+ this.settleConnectLifecycle(state);
357
+ }
334
358
  }
335
359
  registerSocketConnection(state, socket) {
336
360
  this.releaseUpgradeReservation();
337
361
  this.socketRegistry.set(state.socketId, socket);
362
+ this.socketStates.set(state.socketId, state);
338
363
  }
339
364
  async finalizeConnectionBinding(state, socket, request) {
340
365
  state.handlersReady = true;
@@ -342,20 +367,43 @@ class NodeWebSocketGatewayLifecycleService {
342
367
  await state.handlerQueue;
343
368
  }
344
369
  createConnectionHandlerState() {
370
+ const connectLifecycle = createCompletionSignal();
371
+ const disconnectLifecycle = createCompletionSignal();
345
372
  return {
346
373
  bufferedDisconnect: undefined,
347
374
  bufferedMessages: [],
348
375
  bufferedMessagesStartIndex: 0,
376
+ cleanupScheduled: false,
377
+ connectLifecyclePromise: connectLifecycle.promise,
378
+ connectLifecycleSettled: false,
379
+ disconnectLifecyclePromise: disconnectLifecycle.promise,
380
+ disconnectLifecycleSettled: false,
349
381
  enqueuedMessageCount: 0,
350
382
  handlerQueue: Promise.resolve(),
351
383
  handlersReady: false,
352
384
  processingMessageQueue: false,
353
385
  queuedMessages: [],
354
386
  queuedMessagesStartIndex: 0,
387
+ resolveConnectLifecycle: connectLifecycle.resolve,
388
+ resolveDisconnectLifecycle: disconnectLifecycle.resolve,
355
389
  resolved: [],
356
390
  socketId: randomUUID()
357
391
  };
358
392
  }
393
+ settleConnectLifecycle(state) {
394
+ if (state.connectLifecycleSettled) {
395
+ return;
396
+ }
397
+ state.connectLifecycleSettled = true;
398
+ state.resolveConnectLifecycle();
399
+ }
400
+ settleDisconnectLifecycle(state) {
401
+ if (state.disconnectLifecycleSettled) {
402
+ return;
403
+ }
404
+ state.disconnectLifecycleSettled = true;
405
+ state.resolveDisconnectLifecycle();
406
+ }
359
407
  getBufferedMessageCount(state) {
360
408
  return state.bufferedMessages.length - (state.bufferedMessagesStartIndex ?? 0);
361
409
  }
@@ -394,7 +442,7 @@ class NodeWebSocketGatewayLifecycleService {
394
442
  if (policy === 'close') {
395
443
  socket.terminate();
396
444
  this.clearQueuedMessages(state);
397
- this.unregisterSocket(state.socketId);
445
+ this.unregisterSocketWithDeferredStateCleanup(state);
398
446
  this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection terminated.`, 'WebSocketGatewayLifecycleService');
399
447
  return;
400
448
  }
@@ -440,6 +488,8 @@ class NodeWebSocketGatewayLifecycleService {
440
488
  await this.handleDisconnect(state.resolved, socket, disconnectEvent.code, disconnectEvent.reason, state.socketId);
441
489
  }).catch(error => {
442
490
  this.logger.error('WebSocket gateway disconnect dispatch failed.', error, 'WebSocketGatewayLifecycleService');
491
+ }).finally(() => {
492
+ this.settleDisconnectLifecycle(state);
443
493
  });
444
494
  }
445
495
  attachConnectionListeners(state, socket, request) {
@@ -458,20 +508,27 @@ class NodeWebSocketGatewayLifecycleService {
458
508
  this.pingSentAt.delete(state.socketId);
459
509
  });
460
510
  socket.on('error', error => {
461
- this.unregisterSocket(state.socketId);
511
+ this.unregisterSocket(state.socketId, {
512
+ deleteState: false
513
+ });
514
+ this.scheduleSocketStateCleanup(state);
462
515
  this.logger.error('WebSocket gateway socket emitted an error.', error, 'WebSocketGatewayLifecycleService');
463
516
  });
464
517
  socket.on('close', (code, reason) => {
465
- this.unregisterSocket(state.socketId);
518
+ this.unregisterSocket(state.socketId, {
519
+ deleteState: false
520
+ });
466
521
  const disconnectEvent = {
467
522
  code,
468
523
  reason
469
524
  };
470
525
  if (!state.handlersReady) {
471
526
  state.bufferedDisconnect = disconnectEvent;
527
+ this.scheduleSocketStateCleanup(state);
472
528
  return;
473
529
  }
474
530
  this.enqueueDisconnectDispatch(state, socket, disconnectEvent);
531
+ this.scheduleSocketStateCleanup(state);
475
532
  });
476
533
  }
477
534
  bufferIncomingMessage(state, socket, data) {
@@ -528,13 +585,19 @@ class NodeWebSocketGatewayLifecycleService {
528
585
  }
529
586
  this.clearBufferedMessages(state);
530
587
  if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) {
531
- this.unregisterSocket(state.socketId);
588
+ this.unregisterSocketWithDeferredStateCleanup(state);
532
589
  }
533
590
  }
534
591
  async handleDisconnect(resolved, socket, code, reason, socketId) {
535
592
  await dispatchGatewayDisconnect(resolved, socket, code, reason.toString('utf8'), socketId, this.logger, 'WebSocketGatewayLifecycleService');
536
593
  }
537
594
  async resolveUpgradeRejection(request, path) {
595
+ if (this.isShuttingDown) {
596
+ return {
597
+ body: 'WebSocket server is shutting down.',
598
+ status: 503
599
+ };
600
+ }
538
601
  if (!this.tryReserveUpgradeSlot()) {
539
602
  return {
540
603
  body: 'WebSocket connection limit exceeded.',
@@ -550,6 +613,13 @@ class NodeWebSocketGatewayLifecycleService {
550
613
  activeConnectionCount: this.resolveReservedConnectionCount() - 1,
551
614
  path
552
615
  });
616
+ if (this.isShuttingDown) {
617
+ this.releaseUpgradeReservation();
618
+ return {
619
+ body: 'WebSocket server is shutting down.',
620
+ status: 503
621
+ };
622
+ }
553
623
  if (result === false) {
554
624
  this.releaseUpgradeReservation();
555
625
  return {
@@ -651,15 +721,59 @@ class NodeWebSocketGatewayLifecycleService {
651
721
  await this.shutdownPromise;
652
722
  }
653
723
  async runShutdownLifecycle() {
724
+ this.isShuttingDown = true;
654
725
  this.stopHeartbeat();
655
726
  this.detachUpgradeServerListener();
656
727
  const attachments = this.attachments.splice(0);
657
728
  const ownedUpgradeServers = this.ownedUpgradeServers.splice(0);
658
729
  const shutdownTimeoutMs = this.resolveShutdownTimeoutMs();
730
+ await this.awaitPendingUpgradeOperations(shutdownTimeoutMs);
659
731
  await this.closeGatewayAttachments(attachments, shutdownTimeoutMs);
660
732
  await this.closeOwnedUpgradeServers(ownedUpgradeServers, shutdownTimeoutMs);
661
733
  this.clearConnectionTrackingState();
662
734
  }
735
+ trackPendingUpgradeOperation(operation) {
736
+ let trackedOperation;
737
+ trackedOperation = operation.then(() => undefined, () => undefined).finally(() => {
738
+ if (trackedOperation) {
739
+ this.pendingUpgradeOperations.delete(trackedOperation);
740
+ }
741
+ });
742
+ this.pendingUpgradeOperations.add(trackedOperation);
743
+ return operation;
744
+ }
745
+ async awaitPendingUpgradeOperations(timeoutMs) {
746
+ if (this.pendingUpgradeOperations.size === 0) {
747
+ return;
748
+ }
749
+ await new Promise((resolve, reject) => {
750
+ let settled = false;
751
+ const timeout = setTimeout(() => {
752
+ if (settled) {
753
+ return;
754
+ }
755
+ settled = true;
756
+ reject(new Error(`Timed out while waiting for in-flight Node websocket upgrades after ${String(timeoutMs)}ms.`));
757
+ }, timeoutMs);
758
+ Promise.all([...this.pendingUpgradeOperations]).then(() => {
759
+ if (settled) {
760
+ return;
761
+ }
762
+ settled = true;
763
+ clearTimeout(timeout);
764
+ resolve();
765
+ }).catch(error => {
766
+ if (settled) {
767
+ return;
768
+ }
769
+ settled = true;
770
+ clearTimeout(timeout);
771
+ reject(error);
772
+ });
773
+ }).catch(error => {
774
+ this.logger.error(`Failed to wait for in-flight Node websocket upgrades within ${String(timeoutMs)}ms.`, error, 'WebSocketGatewayLifecycleService');
775
+ });
776
+ }
663
777
  stopHeartbeat() {
664
778
  if (!this.heartbeatTimer) {
665
779
  return;
@@ -725,14 +839,64 @@ class NodeWebSocketGatewayLifecycleService {
725
839
  }
726
840
  async closeGatewayAttachments(attachments, shutdownTimeoutMs) {
727
841
  await Promise.all(attachments.map(async attachment => {
728
- this.terminateAttachmentClients(attachment);
842
+ await this.closeAttachmentClients(attachment, shutdownTimeoutMs);
729
843
  await this.closeGatewayAttachment(attachment, shutdownTimeoutMs);
730
844
  }));
731
845
  }
732
- terminateAttachmentClients(attachment) {
846
+ async closeAttachmentClients(attachment, timeoutMs) {
847
+ const states = [...this.socketStates.values()];
733
848
  for (const client of attachment.server.clients) {
734
- client.terminate();
849
+ if (client.readyState === WebSocket.OPEN) {
850
+ client.close(1001, 'Server shutting down');
851
+ } else if (client.readyState === WebSocket.CONNECTING) {
852
+ client.terminate();
853
+ }
854
+ }
855
+ await this.awaitHandlerQueueDrain(states, timeoutMs);
856
+ }
857
+ async awaitHandlerQueueDrain(states, timeoutMs) {
858
+ if (states.length === 0) {
859
+ return;
860
+ }
861
+ await new Promise((resolve, reject) => {
862
+ let settled = false;
863
+ const timeout = setTimeout(() => {
864
+ if (settled) {
865
+ return;
866
+ }
867
+ settled = true;
868
+ reject(new Error(`Timed out while closing Node websocket connections after ${String(timeoutMs)}ms.`));
869
+ }, timeoutMs);
870
+ Promise.all(states.map(async state => {
871
+ await state.connectLifecyclePromise;
872
+ await state.disconnectLifecyclePromise;
873
+ })).then(() => {
874
+ if (settled) {
875
+ return;
876
+ }
877
+ settled = true;
878
+ clearTimeout(timeout);
879
+ resolve();
880
+ }).catch(error => {
881
+ if (settled) {
882
+ return;
883
+ }
884
+ settled = true;
885
+ clearTimeout(timeout);
886
+ reject(error);
887
+ });
888
+ }).catch(error => {
889
+ this.logger.error(`Failed to close Node websocket connections within ${String(timeoutMs)}ms.`, error, 'WebSocketGatewayLifecycleService');
890
+ });
891
+ }
892
+ scheduleSocketStateCleanup(state) {
893
+ if (state.cleanupScheduled) {
894
+ return;
735
895
  }
896
+ state.cleanupScheduled = true;
897
+ void Promise.all([state.connectLifecyclePromise, state.disconnectLifecyclePromise]).finally(() => {
898
+ this.socketStates.delete(state.socketId);
899
+ });
736
900
  }
737
901
  async closeGatewayAttachment(attachment, shutdownTimeoutMs) {
738
902
  try {
@@ -744,6 +908,7 @@ class NodeWebSocketGatewayLifecycleService {
744
908
  clearConnectionTrackingState() {
745
909
  this.socketRegistry.clear();
746
910
  this.socketRooms.clear();
911
+ this.socketStates.clear();
747
912
  this.roomSockets.clear();
748
913
  this.pingPending.clear();
749
914
  this.pingSentAt.clear();
@@ -814,7 +979,7 @@ class NodeWebSocketGatewayLifecycleService {
814
979
  if (socket.bufferedAmount > maxBufferedAmountBytes) {
815
980
  if (backpressurePolicy === 'close') {
816
981
  socket.terminate();
817
- this.unregisterSocket(socketId);
982
+ this.unregisterTrackedSocketWithDeferredStateCleanup(socketId);
818
983
  this.logger.warn(`WebSocket connection ${socketId} exceeded bufferedAmount threshold (${String(maxBufferedAmountBytes)} bytes). Connection terminated.`, 'WebSocketGatewayLifecycleService');
819
984
  continue;
820
985
  }
@@ -851,7 +1016,7 @@ class NodeWebSocketGatewayLifecycleService {
851
1016
  const elapsed = pingAt === undefined ? timeoutMs : now - pingAt;
852
1017
  if (elapsed >= timeoutMs) {
853
1018
  socket.terminate();
854
- this.unregisterSocket(socketId);
1019
+ this.unregisterTrackedSocketWithDeferredStateCleanup(socketId);
855
1020
  }
856
1021
  continue;
857
1022
  }
@@ -863,8 +1028,25 @@ class NodeWebSocketGatewayLifecycleService {
863
1028
  }
864
1029
  }, intervalMs);
865
1030
  }
866
- unregisterSocket(socketId) {
1031
+ unregisterSocketWithDeferredStateCleanup(state) {
1032
+ this.unregisterSocket(state.socketId, {
1033
+ deleteState: false
1034
+ });
1035
+ this.scheduleSocketStateCleanup(state);
1036
+ }
1037
+ unregisterTrackedSocketWithDeferredStateCleanup(socketId) {
1038
+ const state = this.socketStates.get(socketId);
1039
+ if (!state) {
1040
+ this.unregisterSocket(socketId);
1041
+ return;
1042
+ }
1043
+ this.unregisterSocketWithDeferredStateCleanup(state);
1044
+ }
1045
+ unregisterSocket(socketId, options = {}) {
867
1046
  this.socketRegistry.delete(socketId);
1047
+ if (options.deleteState !== false) {
1048
+ this.socketStates.delete(socketId);
1049
+ }
868
1050
  this.pingPending.delete(socketId);
869
1051
  this.pingSentAt.delete(socketId);
870
1052
  const rooms = this.socketRooms.get(socketId);
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "realtime",
10
10
  "upgrade"
11
11
  ],
12
- "version": "1.0.0-beta.6",
12
+ "version": "1.0.1",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -69,17 +69,18 @@
69
69
  ],
70
70
  "dependencies": {
71
71
  "ws": "^8.18.3",
72
- "@fluojs/core": "^1.0.0-beta.5",
73
- "@fluojs/http": "^1.0.0-beta.10",
74
- "@fluojs/runtime": "^1.0.0-beta.12",
75
- "@fluojs/di": "^1.0.0-beta.7"
72
+ "@fluojs/core": "^1.0.1",
73
+ "@fluojs/di": "^1.0.1",
74
+ "@fluojs/http": "^1.0.0",
75
+ "@fluojs/runtime": "^1.0.1"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@types/ws": "^8.18.1",
79
79
  "vitest": "^3.2.4",
80
- "@fluojs/platform-express": "^1.0.0-beta.7",
81
- "@fluojs/platform-bun": "^1.0.0-beta.6",
82
- "@fluojs/platform-fastify": "^1.0.0-beta.8"
80
+ "@fluojs/platform-bun": "^1.0.1",
81
+ "@fluojs/platform-express": "^1.0.1",
82
+ "@fluojs/platform-fastify": "^1.0.1",
83
+ "@fluojs/testing": "^1.0.1"
83
84
  },
84
85
  "scripts": {
85
86
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",