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