@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.
- package/README.ko.md +30 -10
- package/README.md +30 -10
- package/dist/bun/bun-service.d.ts +3 -1
- package/dist/bun/bun-service.d.ts.map +1 -1
- package/dist/bun/bun-service.js +75 -26
- package/dist/bun/bun-types.d.ts +15 -4
- package/dist/bun/bun-types.d.ts.map +1 -1
- package/dist/bun.d.ts +2 -1
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js +2 -1
- package/dist/cloudflare-workers/cloudflare-workers-service.d.ts +4 -0
- package/dist/cloudflare-workers/cloudflare-workers-service.d.ts.map +1 -1
- package/dist/cloudflare-workers/cloudflare-workers-service.js +108 -33
- package/dist/cloudflare-workers/cloudflare-workers-types.d.ts +2 -2
- package/dist/cloudflare-workers/cloudflare-workers-types.d.ts.map +1 -1
- package/dist/cloudflare-workers.d.ts +2 -1
- package/dist/cloudflare-workers.d.ts.map +1 -1
- package/dist/cloudflare-workers.js +2 -1
- package/dist/decorators.d.ts +2 -2
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +4 -4
- package/dist/deno/deno-service.d.ts +4 -0
- package/dist/deno/deno-service.d.ts.map +1 -1
- package/dist/deno/deno-service.js +85 -16
- package/dist/deno/deno-types.d.ts +2 -2
- package/dist/deno/deno-types.d.ts.map +1 -1
- package/dist/deno.d.ts +2 -1
- package/dist/deno.d.ts.map +1 -1
- package/dist/deno.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/shared.d.ts +6 -3
- package/dist/internal/shared.d.ts.map +1 -1
- package/dist/internal/shared.js +26 -6
- package/dist/internal.d.ts +7 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +6 -0
- package/dist/module.d.ts +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +23 -3
- package/dist/node/node-module.d.ts.map +1 -1
- package/dist/node/node-module.js +6 -2
- package/dist/node/node-service-token.d.ts +20 -0
- package/dist/node/node-service-token.d.ts.map +1 -0
- package/dist/node/node-service-token.js +29 -0
- package/dist/node/node-service.d.ts +5 -3
- package/dist/node/node-service.d.ts.map +1 -1
- package/dist/node/node-service.js +26 -8
- package/dist/node/node-types.d.ts +2 -2
- package/dist/node/node-types.d.ts.map +1 -1
- package/dist/node/node.d.ts +1 -1
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +1 -1
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +2 -1
- package/dist/service.d.ts +1 -8
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +1 -8
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- 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 (!
|
|
81
|
-
throw new Error('Cloudflare Workers WebSocket gateway bootstrap requires the selected adapter to expose
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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> = (
|
|
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,
|
|
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
|
|
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,
|
|
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"}
|
package/dist/decorators.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
36
|
+
* handlePing(payload: unknown, socket: { send(data: string): void }) {
|
|
37
|
+
* socket.send(JSON.stringify({ event: 'pong', data: payload }));
|
|
38
38
|
* }
|
|
39
39
|
* }
|
|
40
40
|
* ```
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/decorators.js
CHANGED
|
@@ -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
|
|
26
|
-
const 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
|
-
*
|
|
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":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAgBzI,OAAO,KAAK,EAA8B,oBAAoB,EAA6B,MAAM,aAAa,CAAC;AAC/G,OAAO,KAAK,EAKV,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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> = (
|
|
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,
|
|
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
package/dist/deno.d.ts.map
CHANGED
|
@@ -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,
|
|
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