@fluojs/websockets 1.0.8 → 2.0.0

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 (48) hide show
  1. package/README.ko.md +19 -6
  2. package/README.md +19 -6
  3. package/dist/bun/bun-service.d.ts +3 -1
  4. package/dist/bun/bun-service.d.ts.map +1 -1
  5. package/dist/bun/bun-service.js +75 -26
  6. package/dist/bun/bun-types.d.ts +5 -3
  7. package/dist/bun/bun-types.d.ts.map +1 -1
  8. package/dist/bun.d.ts +2 -1
  9. package/dist/bun.d.ts.map +1 -1
  10. package/dist/bun.js +2 -1
  11. package/dist/cloudflare-workers/cloudflare-workers-service.d.ts +4 -0
  12. package/dist/cloudflare-workers/cloudflare-workers-service.d.ts.map +1 -1
  13. package/dist/cloudflare-workers/cloudflare-workers-service.js +89 -20
  14. package/dist/cloudflare-workers/cloudflare-workers-types.d.ts +2 -2
  15. package/dist/cloudflare-workers/cloudflare-workers-types.d.ts.map +1 -1
  16. package/dist/cloudflare-workers.d.ts +2 -1
  17. package/dist/cloudflare-workers.d.ts.map +1 -1
  18. package/dist/cloudflare-workers.js +2 -1
  19. package/dist/decorators.d.ts +2 -2
  20. package/dist/decorators.d.ts.map +1 -1
  21. package/dist/decorators.js +4 -4
  22. package/dist/deno/deno-service.d.ts +4 -0
  23. package/dist/deno/deno-service.d.ts.map +1 -1
  24. package/dist/deno/deno-service.js +85 -16
  25. package/dist/deno/deno-types.d.ts +2 -2
  26. package/dist/deno/deno-types.d.ts.map +1 -1
  27. package/dist/deno.d.ts +2 -1
  28. package/dist/deno.d.ts.map +1 -1
  29. package/dist/deno.js +2 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/internal/shared.d.ts +6 -3
  33. package/dist/internal/shared.d.ts.map +1 -1
  34. package/dist/internal/shared.js +26 -6
  35. package/dist/internal.d.ts +7 -0
  36. package/dist/internal.d.ts.map +1 -0
  37. package/dist/internal.js +6 -0
  38. package/dist/node/node-service.d.ts +3 -2
  39. package/dist/node/node-service.d.ts.map +1 -1
  40. package/dist/node/node-service.js +21 -5
  41. package/dist/node/node-types.d.ts +2 -2
  42. package/dist/node/node-types.d.ts.map +1 -1
  43. package/dist/node.d.ts +1 -0
  44. package/dist/node.d.ts.map +1 -1
  45. package/dist/node.js +2 -1
  46. package/dist/types.d.ts +26 -2
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +21 -13
@@ -14,9 +14,6 @@ const DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES = 1_048_576;
14
14
  const DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS = 5_000;
15
15
  const LIFECYCLE_LOG_CONTEXT = 'WebSocketGatewayLifecycleService';
16
16
  const WEBSOCKET_OPEN_READY_STATE = 1;
17
- function hasCloudflareWorkerWebSocketBindingHost(adapter) {
18
- return 'configureWebSocketBinding' in adapter && typeof adapter.configureWebSocketBinding === 'function';
19
- }
20
17
  function isWebSocketUpgradeRequest(request) {
21
18
  return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
22
19
  }
@@ -72,15 +69,15 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
72
69
  return;
73
70
  }
74
71
  assertNoFetchStyleServerBackedGatewayOptIn(descriptors, 'cloudflare-workers');
75
- resolveSupportedFetchStyleRealtimeCapability(this.adapter, {
72
+ const capability = resolveSupportedFetchStyleRealtimeCapability(this.adapter, {
76
73
  packageSubpath: 'cloudflare-workers',
77
74
  platformPackage: '@fluojs/platform-cloudflare-workers',
78
75
  runtimeName: 'Cloudflare Workers'
79
76
  });
80
- if (!hasCloudflareWorkerWebSocketBindingHost(this.adapter)) {
81
- throw new Error('Cloudflare Workers WebSocket gateway bootstrap requires the selected adapter to expose Cloudflare websocket binding configuration. Use @fluojs/platform-cloudflare-workers with @fluojs/websockets/cloudflare-workers.');
77
+ if (!capability.bindingInstallation) {
78
+ throw new Error('Cloudflare Workers WebSocket gateway bootstrap requires the selected adapter to expose websocket binding installation through its realtime capability. Use @fluojs/platform-cloudflare-workers with @fluojs/websockets/cloudflare-workers.');
82
79
  }
83
- this.adapter.configureWebSocketBinding(this.createBinding(descriptors));
80
+ capability.bindingInstallation.install(this.createBinding(descriptors));
84
81
  }
85
82
  async onApplicationShutdown() {
86
83
  await this.shutdown();
@@ -141,7 +138,7 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
141
138
  }
142
139
  serverSocket.accept();
143
140
  void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(serverSocket, request, matchedDescriptors)).catch(error => {
144
- this.unregisterSocket(this.findSocketId(serverSocket));
141
+ this.unregisterTrackedSocketWithDeferredStateCleanup(this.findSocketId(serverSocket));
145
142
  this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT);
146
143
  serverSocket.close(1011, 'Internal server error');
147
144
  });
@@ -187,11 +184,13 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
187
184
  bufferedDisconnect: undefined,
188
185
  bufferedMessages: [],
189
186
  bufferedMessagesStartIndex: 0,
187
+ cleanupScheduled: false,
190
188
  connectLifecycleSettled: false,
191
189
  connectLifecyclePromise: connectLifecycle.promise,
192
190
  descriptors,
193
191
  disconnectLifecycleSettled: false,
194
192
  disconnectLifecyclePromise: disconnectLifecycle.promise,
193
+ disconnectDispatchScheduled: false,
195
194
  enqueuedMessageCount: 0,
196
195
  handlerQueue: Promise.resolve(),
197
196
  handlersReady: false,
@@ -202,7 +201,8 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
202
201
  resolveConnectLifecycle: connectLifecycle.resolve,
203
202
  resolveDisconnectLifecycle: disconnectLifecycle.resolve,
204
203
  resolved: [],
205
- socketId: crypto.randomUUID()
204
+ socketId: crypto.randomUUID(),
205
+ terminalFailureHandled: false
206
206
  };
207
207
  }
208
208
  settleConnectLifecycle(state) {
@@ -231,11 +231,11 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
231
231
  this.enqueueMessageDispatch(state, socket, request, event.data);
232
232
  });
233
233
  socket.addEventListener('error', event => {
234
- this.unregisterSocket(state.socketId);
234
+ this.closeSocketAfterTerminalFailure(state, socket, 'Socket error');
235
235
  this.logger.error('WebSocket gateway socket emitted an error.', event, LIFECYCLE_LOG_CONTEXT);
236
236
  });
237
237
  socket.addEventListener('close', event => {
238
- this.unregisterSocket(state.socketId);
238
+ this.unregisterSocketWithDeferredStateCleanup(state);
239
239
  const closeEvent = event;
240
240
  const disconnectEvent = {
241
241
  code: closeEvent.code,
@@ -300,13 +300,16 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
300
300
  this.logger.warn(`WebSocket connection ${state.socketId} dropped the oldest pending message due to buffer limit (${String(limit)}).`, LIFECYCLE_LOG_CONTEXT);
301
301
  }
302
302
  enqueueMessageDispatch(state, socket, request, message) {
303
+ if (state.terminalFailureHandled) {
304
+ return;
305
+ }
303
306
  const limit = isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
304
307
  const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
305
308
  if (this.getQueuedMessageCount(state) >= limit) {
306
309
  if (policy === 'close') {
307
310
  socket.close(1013, 'Ready-state message queue limit exceeded');
308
311
  this.clearQueuedMessages(state);
309
- this.unregisterSocket(state.socketId);
312
+ this.unregisterSocketWithDeferredStateCleanup(state);
310
313
  this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
311
314
  return;
312
315
  }
@@ -341,7 +344,19 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
341
344
  continue;
342
345
  }
343
346
  const normalizedMessage = await this.normalizeMessage(nextMessage);
344
- await dispatchGatewayMessage(state.resolved, socket, request, normalizedMessage, this.logger, LIFECYCLE_LOG_CONTEXT);
347
+ const outcome = await dispatchGatewayMessage(state.resolved, socket, request, normalizedMessage, state.socketId, this.moduleOptions.replies?.mode, message => {
348
+ try {
349
+ socket.send(message);
350
+ return true;
351
+ } catch {
352
+ this.closeSocketAfterTerminalFailure(state, socket, 'Send failed');
353
+ return false;
354
+ }
355
+ }, this.logger, LIFECYCLE_LOG_CONTEXT);
356
+ if (outcome === 'terminal') {
357
+ this.clearQueuedMessages(state);
358
+ return;
359
+ }
345
360
  }
346
361
  this.clearQueuedMessages(state);
347
362
  }
@@ -352,6 +367,10 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
352
367
  return message;
353
368
  }
354
369
  enqueueDisconnectDispatch(state, socket, disconnectEvent) {
370
+ if (state.disconnectDispatchScheduled) {
371
+ return;
372
+ }
373
+ state.disconnectDispatchScheduled = true;
355
374
  state.handlerQueue = state.handlerQueue.then(async () => {
356
375
  await dispatchGatewayDisconnect(state.resolved, socket, disconnectEvent.code, disconnectEvent.reason, state.socketId, this.logger, LIFECYCLE_LOG_CONTEXT);
357
376
  }).catch(error => {
@@ -557,10 +576,7 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
557
576
  }
558
577
  async closeActiveSockets(timeoutMs) {
559
578
  const activeSockets = [...this.socketRegistry.entries()];
560
- if (activeSockets.length === 0) {
561
- return;
562
- }
563
- const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined);
579
+ const activeStates = [...this.socketStates.values()];
564
580
  for (const [, socket] of activeSockets) {
565
581
  if (socket.readyState === WEBSOCKET_OPEN_READY_STATE) {
566
582
  socket.close(1001, 'Server shutting down');
@@ -568,6 +584,15 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
568
584
  }
569
585
  await this.awaitHandlerQueueDrain(activeStates, timeoutMs);
570
586
  }
587
+ scheduleSocketStateCleanup(state) {
588
+ if (state.cleanupScheduled) {
589
+ return;
590
+ }
591
+ state.cleanupScheduled = true;
592
+ void Promise.all([state.connectLifecyclePromise, state.disconnectLifecyclePromise]).finally(() => {
593
+ this.socketStates.delete(state.socketId);
594
+ });
595
+ }
571
596
  async awaitHandlerQueueDrain(states, timeoutMs) {
572
597
  if (states.length === 0) {
573
598
  return;
@@ -604,6 +629,10 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
604
629
  });
605
630
  }
606
631
  joinRoom(socketId, room) {
632
+ const socket = this.socketRegistry.get(socketId);
633
+ if (!socket || socket.readyState !== WEBSOCKET_OPEN_READY_STATE) {
634
+ return;
635
+ }
607
636
  let rooms = this.socketRooms.get(socketId);
608
637
  if (!rooms) {
609
638
  rooms = new Set();
@@ -646,7 +675,13 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
646
675
  try {
647
676
  socket.send(message);
648
677
  } catch (error) {
649
- this.unregisterSocket(socketId);
678
+ const state = this.socketStates.get(socketId);
679
+ if (state) {
680
+ this.closeSocketAfterTerminalFailure(state, socket, 'Send failed');
681
+ } else {
682
+ socket.close(1011, 'Send failed');
683
+ this.unregisterSocket(socketId);
684
+ }
650
685
  this.logger.warn(`WebSocket connection ${socketId} failed to send a room broadcast and was removed. ${error instanceof Error ? error.message : 'Unknown error.'}`, LIFECYCLE_LOG_CONTEXT);
651
686
  }
652
687
  }
@@ -658,12 +693,46 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
658
693
  }
659
694
  return new Set(rooms);
660
695
  }
661
- unregisterSocket(socketId) {
696
+ closeSocketAfterTerminalFailure(state, socket, reason) {
697
+ if (state.terminalFailureHandled) {
698
+ return;
699
+ }
700
+ state.terminalFailureHandled = true;
701
+ this.clearQueuedMessages(state);
702
+ this.unregisterSocketWithDeferredStateCleanup(state);
703
+ socket.close(1011, reason);
704
+ const disconnectEvent = {
705
+ code: 1011,
706
+ reason
707
+ };
708
+ if (!state.handlersReady) {
709
+ state.bufferedDisconnect ??= disconnectEvent;
710
+ return;
711
+ }
712
+ this.enqueueDisconnectDispatch(state, socket, disconnectEvent);
713
+ }
714
+ unregisterSocketWithDeferredStateCleanup(state) {
715
+ this.unregisterSocket(state.socketId, {
716
+ deleteState: false
717
+ });
718
+ this.scheduleSocketStateCleanup(state);
719
+ }
720
+ unregisterTrackedSocketWithDeferredStateCleanup(socketId) {
721
+ const state = this.socketStates.get(socketId);
722
+ if (!state) {
723
+ this.unregisterSocket(socketId);
724
+ return;
725
+ }
726
+ this.unregisterSocketWithDeferredStateCleanup(state);
727
+ }
728
+ unregisterSocket(socketId, options = {}) {
662
729
  if (!socketId) {
663
730
  return;
664
731
  }
665
732
  this.socketRegistry.delete(socketId);
666
- this.socketStates.delete(socketId);
733
+ if (options.deleteState !== false) {
734
+ this.socketStates.delete(socketId);
735
+ }
667
736
  const rooms = this.socketRooms.get(socketId);
668
737
  if (rooms) {
669
738
  for (const room of rooms) {
@@ -1,4 +1,4 @@
1
- import type { WebSocketModuleOptions as SharedWebSocketModuleOptions, WebSocketUpgradeContext, WebSocketUpgradeGuard as SharedWebSocketUpgradeGuard, WebSocketUpgradeRejection } from '../types.js';
1
+ import type { WebSocketModuleOptions as SharedWebSocketModuleOptions, WebSocketUpgradeGuard as SharedWebSocketUpgradeGuard, WebSocketUpgradeContext, WebSocketUpgradeRejection } from '../types.js';
2
2
  /**
3
3
  * Defines the cloudflare worker web socket message type.
4
4
  */
@@ -45,7 +45,7 @@ export interface CloudflareWorkerWebSocketBindingHost {
45
45
  /**
46
46
  * Defines the typed on message handler type.
47
47
  */
48
- export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = (payload: TEvents[K], socket: CloudflareWorkerWebSocket, request: Request) => void | Promise<void>;
48
+ export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = import('../types.js').TypedOnMessageHandler<TEvents, K, CloudflareWorkerWebSocket, Request>;
49
49
  /**
50
50
  * Describes the web socket gateway context contract.
51
51
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare-workers-types.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,IAAI,4BAA4B,EACtD,uBAAuB,EACvB,qBAAqB,IAAI,2BAA2B,EACpD,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,gCAAgC,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,yBACf,SAAQ,IAAI,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,GAAG,qBAAqB,GAAG,MAAM,CAAC;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,MAAM,IAAI,IAAI,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,CAAC,EAAE,yBAAyB,CAAC;IAC7B,CAAC,EAAE,yBAAyB,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,sCAAsC;IACrD,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,yBAAyB,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,oCAAoC;IACnD,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,sCAAsC,CAAC;CACnE;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC/C,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,oCAAoC,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnG;AAED;;GAEG;AACH,MAAM,WAAW,oCAAoC;IACnD,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,SAAS,GAAG,IAAI,CAAC;CACxF;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,OAAO,IAAI,CACpG,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,OAAO,KACb,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,yBAAyB,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,2BAA2B,CAAC,OAAO,CAAC,CAAC;AAEzE,YAAY,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"cloudflare-workers-types.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,IAAI,4BAA4B,EACtD,qBAAqB,IAAI,2BAA2B,EACpD,uBAAuB,EACvB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,gCAAgC,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,yBACf,SAAQ,IAAI,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,GAAG,qBAAqB,GAAG,MAAM,CAAC;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,MAAM,IAAI,IAAI,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,CAAC,EAAE,yBAAyB,CAAC;IAC7B,CAAC,EAAE,yBAAyB,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,sCAAsC;IACrD,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,yBAAyB,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,oCAAoC;IACnD,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,sCAAsC,CAAC;CACnE;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC/C,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,oCAAoC,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnG;AAED;;GAEG;AACH,MAAM,WAAW,oCAAoC;IACnD,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,SAAS,GAAG,IAAI,CAAC;CACxF;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,OAAO,IAAI,OAAO,aAAa,EAAE,qBAAqB,CAC/I,OAAO,EACP,CAAC,EACD,yBAAyB,EACzB,OAAO,CACR,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,yBAAyB,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,2BAA2B,CAAC,OAAO,CAAC,CAAC;AAEzE,YAAY,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC"}
@@ -1,4 +1,5 @@
1
+ export * from './cloudflare-workers/cloudflare-workers.js';
1
2
  export * from './decorators.js';
2
3
  export * from './metadata.js';
3
- export * from './cloudflare-workers/cloudflare-workers.js';
4
+ export type { WebSocketEventEnvelope, WebSocketReplyMode } from './types.js';
4
5
  //# sourceMappingURL=cloudflare-workers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare-workers.d.ts","sourceRoot":"","sources":["../src/cloudflare-workers.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,4CAA4C,CAAC"}
1
+ {"version":3,"file":"cloudflare-workers.d.ts","sourceRoot":"","sources":["../src/cloudflare-workers.ts"],"names":[],"mappings":"AAAA,cAAc,4CAA4C,CAAC;AAC3D,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
@@ -1,3 +1,4 @@
1
+ export * from './cloudflare-workers/cloudflare-workers.js';
1
2
  export * from './decorators.js';
2
3
  export * from './metadata.js';
3
- export * from './cloudflare-workers/cloudflare-workers.js';
4
+ export {};
@@ -33,8 +33,8 @@ export declare function WebSocketGateway(options?: WebSocketGatewayOptions): Cla
33
33
  *
34
34
  * class ChatGateway {
35
35
  * @OnMessage('ping')
36
- * handlePing(payload: unknown) {
37
- * return payload;
36
+ * handlePing(payload: unknown, socket: { send(data: string): void }) {
37
+ * socket.send(JSON.stringify({ event: 'pong', data: payload }));
38
38
  * }
39
39
  * }
40
40
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAmC,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAG9G,KAAK,wBAAwB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAC1F,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AACnD,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AA8DrD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,kBAAkB,CAMpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,OAAO,SAAS,iBAAiB,GAAG,iBAAiB,EACrD,CAAC,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO,EACvC,KAAK,CAAC,EAAE,CAAC,GAAG,MAAM,GAAG,mBAAmB,CAKzC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,mBAAmB,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,mBAAmB,CAIlD"}
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAmC,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAG9G,KAAK,wBAAwB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAC1F,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,kBAAkB,GAAG,wBAAwB,CAAC;AACnD,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AAgErD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,kBAAkB,CAMpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,OAAO,SAAS,iBAAiB,GAAG,iBAAiB,EACrD,CAAC,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO,EACvC,KAAK,CAAC,EAAE,CAAC,GAAG,MAAM,GAAG,mBAAmB,CAKzC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,mBAAmB,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,mBAAmB,CAIlD"}
@@ -22,8 +22,8 @@ function defineStandardGatewayMetadata(metadata, options) {
22
22
  }
23
23
  function defineStandardHandlerMetadata(metadata, propertyKey, handlerMetadata) {
24
24
  const bag = getStandardMetadataBag(metadata);
25
- const current = bag[webSocketHandlerMetadataSymbol];
26
- const map = current ?? new Map();
25
+ const inherited = bag[webSocketHandlerMetadataSymbol];
26
+ const map = Object.hasOwn(bag, webSocketHandlerMetadataSymbol) ? inherited ?? new Map() : new Map(inherited);
27
27
  map.set(propertyKey, {
28
28
  event: handlerMetadata.event,
29
29
  type: handlerMetadata.type
@@ -79,8 +79,8 @@ export function WebSocketGateway(options = {}) {
79
79
  *
80
80
  * class ChatGateway {
81
81
  * @OnMessage('ping')
82
- * handlePing(payload: unknown) {
83
- * return payload;
82
+ * handlePing(payload: unknown, socket: { send(data: string): void }) {
83
+ * socket.send(JSON.stringify({ event: 'pong', data: payload }));
84
84
  * }
85
85
  * }
86
86
  * ```
@@ -61,11 +61,15 @@ export declare class DenoWebSocketGatewayLifecycleService implements OnApplicati
61
61
  private trackPendingUpgradeOperation;
62
62
  private awaitPendingUpgradeOperations;
63
63
  private closeActiveSockets;
64
+ private scheduleSocketStateCleanup;
64
65
  private awaitHandlerQueueDrain;
65
66
  joinRoom(socketId: string, room: string): void;
66
67
  leaveRoom(socketId: string, room: string): void;
67
68
  broadcastToRoom(room: string, event: string, data: unknown): void;
68
69
  getRooms(socketId: string): ReadonlySet<string>;
70
+ private closeSocketAfterTerminalFailure;
71
+ private unregisterSocketWithDeferredStateCleanup;
72
+ private unregisterTrackedSocketWithDeferredStateCleanup;
69
73
  private unregisterSocket;
70
74
  }
71
75
  //# sourceMappingURL=deno-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deno-service.d.ts","sourceRoot":"","sources":["../../src/deno/deno-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deno-service.d.ts","sourceRoot":"","sources":["../../src/deno/deno-service.ts"],"names":[],"mappings":"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"}
@@ -146,7 +146,7 @@ class DenoWebSocketGatewayLifecycleService {
146
146
  throw error;
147
147
  }
148
148
  void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(socket, request, descriptors)).catch(error => {
149
- this.unregisterSocket(this.findSocketId(socket));
149
+ this.unregisterTrackedSocketWithDeferredStateCleanup(this.findSocketId(socket));
150
150
  this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT);
151
151
  socket.close(1011, 'Internal server error');
152
152
  });
@@ -192,11 +192,13 @@ class DenoWebSocketGatewayLifecycleService {
192
192
  bufferedDisconnect: undefined,
193
193
  bufferedMessages: [],
194
194
  bufferedMessagesStartIndex: 0,
195
+ cleanupScheduled: false,
195
196
  connectLifecycleSettled: false,
196
197
  connectLifecyclePromise: connectLifecycle.promise,
197
198
  descriptors,
198
199
  disconnectLifecycleSettled: false,
199
200
  disconnectLifecyclePromise: disconnectLifecycle.promise,
201
+ disconnectDispatchScheduled: false,
200
202
  enqueuedMessageCount: 0,
201
203
  handlerQueue: Promise.resolve(),
202
204
  handlersReady: false,
@@ -207,7 +209,8 @@ class DenoWebSocketGatewayLifecycleService {
207
209
  resolveConnectLifecycle: connectLifecycle.resolve,
208
210
  resolveDisconnectLifecycle: disconnectLifecycle.resolve,
209
211
  resolved: [],
210
- socketId: crypto.randomUUID()
212
+ socketId: crypto.randomUUID(),
213
+ terminalFailureHandled: false
211
214
  };
212
215
  }
213
216
  settleConnectLifecycle(state) {
@@ -236,11 +239,11 @@ class DenoWebSocketGatewayLifecycleService {
236
239
  this.enqueueMessageDispatch(state, socket, request, event.data);
237
240
  });
238
241
  socket.addEventListener('error', event => {
239
- this.unregisterSocket(state.socketId);
242
+ this.closeSocketAfterTerminalFailure(state, socket, 'Socket error');
240
243
  this.logger.error('WebSocket gateway socket emitted an error.', event, LIFECYCLE_LOG_CONTEXT);
241
244
  });
242
245
  socket.addEventListener('close', event => {
243
- this.unregisterSocket(state.socketId);
246
+ this.unregisterSocketWithDeferredStateCleanup(state);
244
247
  const disconnectEvent = {
245
248
  code: event.code,
246
249
  reason: event.reason
@@ -304,13 +307,16 @@ class DenoWebSocketGatewayLifecycleService {
304
307
  this.logger.warn(`WebSocket connection ${state.socketId} dropped the oldest pending message due to buffer limit (${String(limit)}).`, LIFECYCLE_LOG_CONTEXT);
305
308
  }
306
309
  enqueueMessageDispatch(state, socket, request, message) {
310
+ if (state.terminalFailureHandled) {
311
+ return;
312
+ }
307
313
  const limit = isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
308
314
  const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
309
315
  if (this.getQueuedMessageCount(state) >= limit) {
310
316
  if (policy === 'close') {
311
317
  socket.close(1013, 'Ready-state message queue limit exceeded');
312
318
  this.clearQueuedMessages(state);
313
- this.unregisterSocket(state.socketId);
319
+ this.unregisterSocketWithDeferredStateCleanup(state);
314
320
  this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
315
321
  return;
316
322
  }
@@ -345,7 +351,19 @@ class DenoWebSocketGatewayLifecycleService {
345
351
  continue;
346
352
  }
347
353
  const normalizedMessage = await this.normalizeMessage(nextMessage);
348
- await dispatchGatewayMessage(state.resolved, socket, request, normalizedMessage, this.logger, LIFECYCLE_LOG_CONTEXT);
354
+ const outcome = await dispatchGatewayMessage(state.resolved, socket, request, normalizedMessage, state.socketId, this.moduleOptions.replies?.mode, message => {
355
+ try {
356
+ socket.send(message);
357
+ return true;
358
+ } catch {
359
+ this.closeSocketAfterTerminalFailure(state, socket, 'Send failed');
360
+ return false;
361
+ }
362
+ }, this.logger, LIFECYCLE_LOG_CONTEXT);
363
+ if (outcome === 'terminal') {
364
+ this.clearQueuedMessages(state);
365
+ return;
366
+ }
349
367
  }
350
368
  this.clearQueuedMessages(state);
351
369
  }
@@ -356,6 +374,10 @@ class DenoWebSocketGatewayLifecycleService {
356
374
  return message;
357
375
  }
358
376
  enqueueDisconnectDispatch(state, socket, disconnectEvent) {
377
+ if (state.disconnectDispatchScheduled) {
378
+ return;
379
+ }
380
+ state.disconnectDispatchScheduled = true;
359
381
  state.handlerQueue = state.handlerQueue.then(async () => {
360
382
  await dispatchGatewayDisconnect(state.resolved, socket, disconnectEvent.code, disconnectEvent.reason, state.socketId, this.logger, LIFECYCLE_LOG_CONTEXT);
361
383
  }).catch(error => {
@@ -511,9 +533,6 @@ class DenoWebSocketGatewayLifecycleService {
511
533
  }
512
534
  async runShutdownLifecycle() {
513
535
  this.isShuttingDown = true;
514
- if (hasDenoWebSocketBindingHost(this.adapter)) {
515
- this.adapter.configureWebSocketBinding(undefined);
516
- }
517
536
  const shutdownTimeoutMs = this.resolveShutdownTimeoutMs();
518
537
  await this.awaitPendingUpgradeOperations(shutdownTimeoutMs);
519
538
  await this.closeActiveSockets(shutdownTimeoutMs);
@@ -570,10 +589,7 @@ class DenoWebSocketGatewayLifecycleService {
570
589
  }
571
590
  async closeActiveSockets(timeoutMs) {
572
591
  const activeSockets = [...this.socketRegistry.entries()];
573
- if (activeSockets.length === 0) {
574
- return;
575
- }
576
- const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined);
592
+ const activeStates = [...this.socketStates.values()];
577
593
  for (const [, socket] of activeSockets) {
578
594
  if (socket.readyState === WEBSOCKET_OPEN_READY_STATE) {
579
595
  socket.close(1001, 'Server shutting down');
@@ -581,6 +597,15 @@ class DenoWebSocketGatewayLifecycleService {
581
597
  }
582
598
  await this.awaitHandlerQueueDrain(activeStates, timeoutMs);
583
599
  }
600
+ scheduleSocketStateCleanup(state) {
601
+ if (state.cleanupScheduled) {
602
+ return;
603
+ }
604
+ state.cleanupScheduled = true;
605
+ void Promise.all([state.connectLifecyclePromise, state.disconnectLifecyclePromise]).finally(() => {
606
+ this.socketStates.delete(state.socketId);
607
+ });
608
+ }
584
609
  async awaitHandlerQueueDrain(states, timeoutMs) {
585
610
  if (states.length === 0) {
586
611
  return;
@@ -617,6 +642,10 @@ class DenoWebSocketGatewayLifecycleService {
617
642
  });
618
643
  }
619
644
  joinRoom(socketId, room) {
645
+ const socket = this.socketRegistry.get(socketId);
646
+ if (!socket || socket.readyState !== WEBSOCKET_OPEN_READY_STATE) {
647
+ return;
648
+ }
620
649
  let rooms = this.socketRooms.get(socketId);
621
650
  if (!rooms) {
622
651
  rooms = new Set();
@@ -659,7 +688,13 @@ class DenoWebSocketGatewayLifecycleService {
659
688
  try {
660
689
  socket.send(message);
661
690
  } catch (error) {
662
- this.unregisterSocket(socketId);
691
+ const state = this.socketStates.get(socketId);
692
+ if (state) {
693
+ this.closeSocketAfterTerminalFailure(state, socket, 'Send failed');
694
+ } else {
695
+ socket.close(1011, 'Send failed');
696
+ this.unregisterSocket(socketId);
697
+ }
663
698
  this.logger.warn(`WebSocket connection ${socketId} failed to send a room broadcast and was removed. ${error instanceof Error ? error.message : 'Unknown error.'}`, LIFECYCLE_LOG_CONTEXT);
664
699
  }
665
700
  }
@@ -671,12 +706,46 @@ class DenoWebSocketGatewayLifecycleService {
671
706
  }
672
707
  return new Set(rooms);
673
708
  }
674
- unregisterSocket(socketId) {
709
+ closeSocketAfterTerminalFailure(state, socket, reason) {
710
+ if (state.terminalFailureHandled) {
711
+ return;
712
+ }
713
+ state.terminalFailureHandled = true;
714
+ this.clearQueuedMessages(state);
715
+ this.unregisterSocketWithDeferredStateCleanup(state);
716
+ socket.close(1011, reason);
717
+ const disconnectEvent = {
718
+ code: 1011,
719
+ reason
720
+ };
721
+ if (!state.handlersReady) {
722
+ state.bufferedDisconnect ??= disconnectEvent;
723
+ return;
724
+ }
725
+ this.enqueueDisconnectDispatch(state, socket, disconnectEvent);
726
+ }
727
+ unregisterSocketWithDeferredStateCleanup(state) {
728
+ this.unregisterSocket(state.socketId, {
729
+ deleteState: false
730
+ });
731
+ this.scheduleSocketStateCleanup(state);
732
+ }
733
+ unregisterTrackedSocketWithDeferredStateCleanup(socketId) {
734
+ const state = this.socketStates.get(socketId);
735
+ if (!state) {
736
+ this.unregisterSocket(socketId);
737
+ return;
738
+ }
739
+ this.unregisterSocketWithDeferredStateCleanup(state);
740
+ }
741
+ unregisterSocket(socketId, options = {}) {
675
742
  if (!socketId) {
676
743
  return;
677
744
  }
678
745
  this.socketRegistry.delete(socketId);
679
- this.socketStates.delete(socketId);
746
+ if (options.deleteState !== false) {
747
+ this.socketStates.delete(socketId);
748
+ }
680
749
  const rooms = this.socketRooms.get(socketId);
681
750
  if (rooms) {
682
751
  for (const room of rooms) {
@@ -1,4 +1,4 @@
1
- import type { WebSocketModuleOptions as SharedWebSocketModuleOptions, WebSocketUpgradeContext, WebSocketUpgradeGuard as SharedWebSocketUpgradeGuard, WebSocketUpgradeRejection } from '../types.js';
1
+ import type { WebSocketModuleOptions as SharedWebSocketModuleOptions, WebSocketUpgradeGuard as SharedWebSocketUpgradeGuard, WebSocketUpgradeContext, WebSocketUpgradeRejection } from '../types.js';
2
2
  /**
3
3
  * Defines the Deno websocket inbound message contract.
4
4
  *
@@ -42,7 +42,7 @@ export interface DenoWebSocketBindingHost<TSocket extends DenoServerWebSocket =
42
42
  /**
43
43
  * Defines the typed on message handler type.
44
44
  */
45
- export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = (payload: TEvents[K], socket: DenoServerWebSocket, request: Request) => void | Promise<void>;
45
+ export type TypedOnMessageHandler<TEvents extends Record<string, unknown>, K extends keyof TEvents> = import('../types.js').TypedOnMessageHandler<TEvents, K, DenoServerWebSocket, Request>;
46
46
  /**
47
47
  * Describes the web socket gateway context contract.
48
48
  */
@@ -1 +1 @@
1
- {"version":3,"file":"deno-types.d.ts","sourceRoot":"","sources":["../../src/deno/deno-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,IAAI,4BAA4B,EACtD,uBAAuB,EACvB,qBAAqB,IAAI,2BAA2B,EACpD,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,GAAG,qBAAqB,GAAG,MAAM,CAAC;IACzH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IACnG,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IACjG,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,0BAA0B,CAAC,OAAO,CAAC,CAAC;CAChE;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IAC7F,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,wBAAwB,CAAC,OAAO,CAAC,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChG;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IACjG,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;CACrF;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,OAAO,IAAI,CACpG,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,OAAO,KACb,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,2BAA2B,CAAC,OAAO,CAAC,CAAC;AAEzE,YAAY,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"deno-types.d.ts","sourceRoot":"","sources":["../../src/deno/deno-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,IAAI,4BAA4B,EACtD,qBAAqB,IAAI,2BAA2B,EACpD,uBAAuB,EACvB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,SAAS,EAAE,kBAAkB,GAAG,OAAO,GAAG,qBAAqB,GAAG,MAAM,CAAC;IACzH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IACnG,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IACjG,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,0BAA0B,CAAC,OAAO,CAAC,CAAC;CAChE;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IAC7F,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,wBAAwB,CAAC,OAAO,CAAC,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChG;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,mBAAmB,GAAG,mBAAmB;IACjG,yBAAyB,CAAC,OAAO,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;CACrF;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,OAAO,IAAI,OAAO,aAAa,EAAE,qBAAqB,CAC/I,OAAO,EACP,CAAC,EACD,mBAAmB,EACnB,OAAO,CACR,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,2BAA2B,CAAC,OAAO,CAAC,CAAC;AAEzE,YAAY,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC"}
package/dist/deno.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './decorators.js';
2
- export * from './metadata.js';
3
2
  export * from './deno/deno.js';
3
+ export * from './metadata.js';
4
+ export type { WebSocketEventEnvelope, WebSocketReplyMode } from './types.js';
4
5
  //# sourceMappingURL=deno.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deno.d.ts","sourceRoot":"","sources":["../src/deno.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"deno.d.ts","sourceRoot":"","sources":["../src/deno.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
package/dist/deno.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './decorators.js';
2
+ export * from './deno/deno.js';
2
3
  export * from './metadata.js';
3
- export * from './deno/deno.js';
4
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from './decorators.js';
2
2
  export * from './metadata.js';
3
3
  export * from './module.js';
4
- export * from './service.js';
5
- export type { WebSocketEventMap, WebSocketGatewayDescriptor, WebSocketGatewayHandlerDescriptor, WebSocketGatewayHandlerMetadata, WebSocketGatewayHandlerType, WebSocketGatewayMetadata, WebSocketGatewayOptions, WebSocketGatewayServerBackedOptions, WebSocketRoomService, WebSocketUpgradeContext, WebSocketUpgradeRejection, } from './types.js';
6
4
  export type { TypedOnMessageHandler, WebSocketGatewayContext, WebSocketModuleOptions, WebSocketUpgradeGuard, } from './node/node-types.js';
5
+ export * from './service.js';
6
+ export type { WebSocketEventEnvelope, WebSocketEventMap, WebSocketGatewayDescriptor, WebSocketGatewayHandlerDescriptor, WebSocketGatewayHandlerMetadata, WebSocketGatewayHandlerType, WebSocketGatewayMetadata, WebSocketGatewayOptions, WebSocketGatewayServerBackedOptions, WebSocketReplyMode, WebSocketRoomService, WebSocketUpgradeContext, WebSocketUpgradeRejection, } from './types.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,iCAAiC,EACjC,+BAA+B,EAC/B,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,mCAAmC,EACnC,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,0BAA0B,EAC1B,iCAAiC,EACjC,+BAA+B,EAC/B,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,mCAAmC,EACnC,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,YAAY,CAAC"}
@@ -2,7 +2,7 @@ import type { Token } from '@fluojs/core';
2
2
  import type { Container } from '@fluojs/di';
3
3
  import type { FetchStyleHttpAdapterRealtimeCapability, HttpApplicationAdapter } from '@fluojs/http';
4
4
  import type { ApplicationLogger, CompiledModule } from '@fluojs/runtime';
5
- import type { WebSocketGatewayDescriptor, WebSocketGatewayHandlerDescriptor } from '../types.js';
5
+ import type { WebSocketGatewayDescriptor, WebSocketGatewayHandlerDescriptor, WebSocketReplyMode } from '../types.js';
6
6
  /**
7
7
  * Describes the discovery candidate contract.
8
8
  */
@@ -98,11 +98,14 @@ export declare function resolveGatewayInstance(runtimeContainer: Container, desc
98
98
  * @param socket The socket.
99
99
  * @param request The request.
100
100
  * @param data The data.
101
+ * @param socketId Stable connection identity passed to message handlers.
102
+ * @param replyMode Optional policy for serializing valid handler return envelopes.
103
+ * @param sendReply Runtime-owned reply sender that reports terminal send failures.
101
104
  * @param logger The logger.
102
105
  * @param loggerContext The logger context.
103
- * @returns The dispatch gateway message result.
106
+ * @returns `terminal` when reply delivery failed; otherwise `completed`.
104
107
  */
105
- export declare function dispatchGatewayMessage<TSocket, TRequest>(resolved: readonly ResolvedGatewayInstance[], socket: TSocket, request: TRequest, data: SharedWebSocketIncomingMessage, logger: ApplicationLogger, loggerContext: string): Promise<void>;
108
+ export declare function dispatchGatewayMessage<TSocket, TRequest>(resolved: readonly ResolvedGatewayInstance[], socket: TSocket, request: TRequest, data: SharedWebSocketIncomingMessage, socketId: string, replyMode: WebSocketReplyMode | undefined, sendReply: (message: string) => boolean, logger: ApplicationLogger, loggerContext: string): Promise<'completed' | 'terminal'>;
106
109
  /**
107
110
  * Dispatch gateway disconnect.
108
111
  *