@fluojs/websockets 1.0.7 → 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 (62) hide show
  1. package/README.ko.md +30 -10
  2. package/README.md +30 -10
  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 +15 -4
  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 +108 -33
  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/module.d.ts +1 -1
  39. package/dist/module.d.ts.map +1 -1
  40. package/dist/module.js +23 -3
  41. package/dist/node/node-module.d.ts.map +1 -1
  42. package/dist/node/node-module.js +6 -2
  43. package/dist/node/node-service-token.d.ts +20 -0
  44. package/dist/node/node-service-token.d.ts.map +1 -0
  45. package/dist/node/node-service-token.js +29 -0
  46. package/dist/node/node-service.d.ts +5 -3
  47. package/dist/node/node-service.d.ts.map +1 -1
  48. package/dist/node/node-service.js +26 -8
  49. package/dist/node/node-types.d.ts +2 -2
  50. package/dist/node/node-types.d.ts.map +1 -1
  51. package/dist/node/node.d.ts +1 -1
  52. package/dist/node/node.d.ts.map +1 -1
  53. package/dist/node/node.js +1 -1
  54. package/dist/node.d.ts +1 -0
  55. package/dist/node.d.ts.map +1 -1
  56. package/dist/node.js +2 -1
  57. package/dist/service.d.ts +1 -8
  58. package/dist/service.d.ts.map +1 -1
  59. package/dist/service.js +1 -8
  60. package/dist/types.d.ts +26 -2
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +21 -12
@@ -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();
@@ -114,6 +111,9 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
114
111
  status: 404
115
112
  });
116
113
  }
114
+ if (this.isShuttingDown) {
115
+ return createCloudflareWorkersWebSocketShutdownResponse();
116
+ }
117
117
  const rejection = await this.resolveUpgradeRejection(request, targetPath);
118
118
  if (rejection) {
119
119
  return new Response(rejection.body ?? null, {
@@ -123,9 +123,7 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
123
123
  }
124
124
  if (this.isShuttingDown) {
125
125
  this.releaseUpgradeReservation();
126
- return new Response('WebSocket server is shutting down.', {
127
- status: 503
128
- });
126
+ return createCloudflareWorkersWebSocketShutdownResponse();
129
127
  }
130
128
  let response;
131
129
  let serverSocket;
@@ -140,7 +138,7 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
140
138
  }
141
139
  serverSocket.accept();
142
140
  void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(serverSocket, request, matchedDescriptors)).catch(error => {
143
- this.unregisterSocket(this.findSocketId(serverSocket));
141
+ this.unregisterTrackedSocketWithDeferredStateCleanup(this.findSocketId(serverSocket));
144
142
  this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT);
145
143
  serverSocket.close(1011, 'Internal server error');
146
144
  });
@@ -186,11 +184,13 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
186
184
  bufferedDisconnect: undefined,
187
185
  bufferedMessages: [],
188
186
  bufferedMessagesStartIndex: 0,
187
+ cleanupScheduled: false,
189
188
  connectLifecycleSettled: false,
190
189
  connectLifecyclePromise: connectLifecycle.promise,
191
190
  descriptors,
192
191
  disconnectLifecycleSettled: false,
193
192
  disconnectLifecyclePromise: disconnectLifecycle.promise,
193
+ disconnectDispatchScheduled: false,
194
194
  enqueuedMessageCount: 0,
195
195
  handlerQueue: Promise.resolve(),
196
196
  handlersReady: false,
@@ -201,7 +201,8 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
201
201
  resolveConnectLifecycle: connectLifecycle.resolve,
202
202
  resolveDisconnectLifecycle: disconnectLifecycle.resolve,
203
203
  resolved: [],
204
- socketId: crypto.randomUUID()
204
+ socketId: crypto.randomUUID(),
205
+ terminalFailureHandled: false
205
206
  };
206
207
  }
207
208
  settleConnectLifecycle(state) {
@@ -230,11 +231,11 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
230
231
  this.enqueueMessageDispatch(state, socket, request, event.data);
231
232
  });
232
233
  socket.addEventListener('error', event => {
233
- this.unregisterSocket(state.socketId);
234
+ this.closeSocketAfterTerminalFailure(state, socket, 'Socket error');
234
235
  this.logger.error('WebSocket gateway socket emitted an error.', event, LIFECYCLE_LOG_CONTEXT);
235
236
  });
236
237
  socket.addEventListener('close', event => {
237
- this.unregisterSocket(state.socketId);
238
+ this.unregisterSocketWithDeferredStateCleanup(state);
238
239
  const closeEvent = event;
239
240
  const disconnectEvent = {
240
241
  code: closeEvent.code,
@@ -299,13 +300,16 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
299
300
  this.logger.warn(`WebSocket connection ${state.socketId} dropped the oldest pending message due to buffer limit (${String(limit)}).`, LIFECYCLE_LOG_CONTEXT);
300
301
  }
301
302
  enqueueMessageDispatch(state, socket, request, message) {
303
+ if (state.terminalFailureHandled) {
304
+ return;
305
+ }
302
306
  const limit = isFinitePositiveInteger(this.moduleOptions.buffer?.maxPendingMessagesPerSocket) ? this.moduleOptions.buffer.maxPendingMessagesPerSocket : DEFAULT_MAX_PENDING_MESSAGES_PER_SOCKET;
303
307
  const policy = this.moduleOptions.buffer?.overflowPolicy ?? 'drop-oldest';
304
308
  if (this.getQueuedMessageCount(state) >= limit) {
305
309
  if (policy === 'close') {
306
310
  socket.close(1013, 'Ready-state message queue limit exceeded');
307
311
  this.clearQueuedMessages(state);
308
- this.unregisterSocket(state.socketId);
312
+ this.unregisterSocketWithDeferredStateCleanup(state);
309
313
  this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection closed.`, LIFECYCLE_LOG_CONTEXT);
310
314
  return;
311
315
  }
@@ -340,7 +344,19 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
340
344
  continue;
341
345
  }
342
346
  const normalizedMessage = await this.normalizeMessage(nextMessage);
343
- 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
+ }
344
360
  }
345
361
  this.clearQueuedMessages(state);
346
362
  }
@@ -351,6 +367,10 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
351
367
  return message;
352
368
  }
353
369
  enqueueDisconnectDispatch(state, socket, disconnectEvent) {
370
+ if (state.disconnectDispatchScheduled) {
371
+ return;
372
+ }
373
+ state.disconnectDispatchScheduled = true;
354
374
  state.handlerQueue = state.handlerQueue.then(async () => {
355
375
  await dispatchGatewayDisconnect(state.resolved, socket, disconnectEvent.code, disconnectEvent.reason, state.socketId, this.logger, LIFECYCLE_LOG_CONTEXT);
356
376
  }).catch(error => {
@@ -407,12 +427,6 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
407
427
  return '';
408
428
  }
409
429
  async resolveUpgradeRejection(request, path) {
410
- if (this.isShuttingDown) {
411
- return {
412
- body: 'WebSocket server is shutting down.',
413
- status: 503
414
- };
415
- }
416
430
  if (!this.tryReserveUpgradeSlot()) {
417
431
  return {
418
432
  body: 'WebSocket connection limit exceeded.',
@@ -506,9 +520,6 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
506
520
  }
507
521
  async runShutdownLifecycle() {
508
522
  this.isShuttingDown = true;
509
- if (hasCloudflareWorkerWebSocketBindingHost(this.adapter)) {
510
- this.adapter.configureWebSocketBinding(undefined);
511
- }
512
523
  const shutdownTimeoutMs = this.resolveShutdownTimeoutMs();
513
524
  await this.awaitPendingUpgradeOperations(shutdownTimeoutMs);
514
525
  await this.closeActiveSockets(shutdownTimeoutMs);
@@ -565,10 +576,7 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
565
576
  }
566
577
  async closeActiveSockets(timeoutMs) {
567
578
  const activeSockets = [...this.socketRegistry.entries()];
568
- if (activeSockets.length === 0) {
569
- return;
570
- }
571
- const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined);
579
+ const activeStates = [...this.socketStates.values()];
572
580
  for (const [, socket] of activeSockets) {
573
581
  if (socket.readyState === WEBSOCKET_OPEN_READY_STATE) {
574
582
  socket.close(1001, 'Server shutting down');
@@ -576,6 +584,15 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
576
584
  }
577
585
  await this.awaitHandlerQueueDrain(activeStates, timeoutMs);
578
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
+ }
579
596
  async awaitHandlerQueueDrain(states, timeoutMs) {
580
597
  if (states.length === 0) {
581
598
  return;
@@ -612,6 +629,10 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
612
629
  });
613
630
  }
614
631
  joinRoom(socketId, room) {
632
+ const socket = this.socketRegistry.get(socketId);
633
+ if (!socket || socket.readyState !== WEBSOCKET_OPEN_READY_STATE) {
634
+ return;
635
+ }
615
636
  let rooms = this.socketRooms.get(socketId);
616
637
  if (!rooms) {
617
638
  rooms = new Set();
@@ -654,7 +675,13 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
654
675
  try {
655
676
  socket.send(message);
656
677
  } catch (error) {
657
- 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
+ }
658
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);
659
686
  }
660
687
  }
@@ -666,12 +693,46 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
666
693
  }
667
694
  return new Set(rooms);
668
695
  }
669
- 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 = {}) {
670
729
  if (!socketId) {
671
730
  return;
672
731
  }
673
732
  this.socketRegistry.delete(socketId);
674
- this.socketStates.delete(socketId);
733
+ if (options.deleteState !== false) {
734
+ this.socketStates.delete(socketId);
735
+ }
675
736
  const rooms = this.socketRooms.get(socketId);
676
737
  if (rooms) {
677
738
  for (const room of rooms) {
@@ -688,4 +749,18 @@ class CloudflareWorkersWebSocketGatewayLifecycleService {
688
749
  _initClass();
689
750
  }
690
751
  }
691
- export { _CloudflareWorkersWeb as CloudflareWorkersWebSocketGatewayLifecycleService };
752
+ export { _CloudflareWorkersWeb as CloudflareWorkersWebSocketGatewayLifecycleService };
753
+ function createCloudflareWorkersWebSocketShutdownResponse() {
754
+ return new Response(JSON.stringify({
755
+ error: {
756
+ code: 'SERVICE_UNAVAILABLE',
757
+ message: 'Server is shutting down.',
758
+ status: 503
759
+ }
760
+ }), {
761
+ headers: {
762
+ 'content-type': 'application/json'
763
+ },
764
+ status: 503
765
+ });
766
+ }
@@ -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 {};