@fluojs/websockets
Advanced tools
@@ -21,3 +21,5 @@ import type { Container } from '@fluojs/di'; | ||
| private heartbeatTimer; | ||
| private isShuttingDown; | ||
| private ownedUpgradeServers; | ||
| private readonly pendingUpgradeOperations; | ||
| private pendingUpgradeReservations; | ||
@@ -30,2 +32,3 @@ private readonly pingPending; | ||
| private readonly socketRooms; | ||
| private readonly socketStates; | ||
| private upgradeListener; | ||
@@ -63,2 +66,4 @@ private upgradeServer; | ||
| private createConnectionHandlerState; | ||
| private settleConnectLifecycle; | ||
| private settleDisconnectLifecycle; | ||
| private getBufferedMessageCount; | ||
@@ -91,2 +96,4 @@ private getQueuedMessageCount; | ||
| private runShutdownLifecycle; | ||
| private trackPendingUpgradeOperation; | ||
| private awaitPendingUpgradeOperations; | ||
| private stopHeartbeat; | ||
@@ -98,3 +105,5 @@ private detachUpgradeServerListener; | ||
| private closeGatewayAttachments; | ||
| private terminateAttachmentClients; | ||
| private closeAttachmentClients; | ||
| private awaitHandlerQueueDrain; | ||
| private scheduleSocketStateCleanup; | ||
| private closeGatewayAttachment; | ||
@@ -132,4 +141,6 @@ private clearConnectionTrackingState; | ||
| private startHeartbeat; | ||
| private unregisterSocketWithDeferredStateCleanup; | ||
| private unregisterTrackedSocketWithDeferredStateCleanup; | ||
| private unregisterSocket; | ||
| } | ||
| //# sourceMappingURL=node-service.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"node-service.d.ts","sourceRoot":"","sources":["../../src/node/node-service.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"node-service.d.ts","sourceRoot":"","sources":["../../src/node/node-service.ts"],"names":[],"mappings":"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"} |
+196
-14
@@ -106,2 +106,12 @@ let _initClass; | ||
| } | ||
| function createCompletionSignal() { | ||
| let resolve; | ||
| const promise = new Promise(res => { | ||
| resolve = res; | ||
| }); | ||
| return { | ||
| promise, | ||
| resolve | ||
| }; | ||
| } | ||
@@ -122,3 +132,5 @@ /** | ||
| heartbeatTimer; | ||
| isShuttingDown = false; | ||
| ownedUpgradeServers = []; | ||
| pendingUpgradeOperations = new Set(); | ||
| pendingUpgradeReservations = 0; | ||
@@ -131,2 +143,3 @@ pingPending = new Set(); | ||
| socketRooms = new Map(); | ||
| socketStates = new Map(); | ||
| upgradeListener; | ||
@@ -278,3 +291,3 @@ upgradeServer; | ||
| socket.pause(); | ||
| void this.handleUpgradeRequest(upgradeServer, attachmentsByPath, request, socket, head).catch(error => { | ||
| void this.trackPendingUpgradeOperation(this.handleUpgradeRequest(upgradeServer, attachmentsByPath, request, socket, head)).catch(error => { | ||
| this.logger.error('WebSocket upgrade admission failed.', error, 'WebSocketGatewayLifecycleService'); | ||
@@ -335,5 +348,16 @@ rejectUpgradeRequestWithStatus(socket, { | ||
| this.attachConnectionListeners(state, socket, request); | ||
| await this.resolveConnectionGateways(descriptors, state); | ||
| await this.runConnectHandlers(state, socket, request); | ||
| await this.finalizeConnectionBinding(state, socket, request); | ||
| try { | ||
| await this.resolveConnectionGateways(descriptors, state); | ||
| await this.runConnectHandlers(state, socket, request); | ||
| await this.finalizeConnectionBinding(state, socket, request); | ||
| if (this.isShuttingDown && socket.readyState === WebSocket.OPEN) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| await state.disconnectLifecyclePromise; | ||
| } | ||
| } finally { | ||
| if (!state.handlersReady && state.bufferedDisconnect) { | ||
| this.settleDisconnectLifecycle(state); | ||
| } | ||
| this.settleConnectLifecycle(state); | ||
| } | ||
| } | ||
@@ -343,2 +367,3 @@ registerSocketConnection(state, socket) { | ||
| this.socketRegistry.set(state.socketId, socket); | ||
| this.socketStates.set(state.socketId, state); | ||
| } | ||
@@ -351,2 +376,4 @@ async finalizeConnectionBinding(state, socket, request) { | ||
| createConnectionHandlerState() { | ||
| const connectLifecycle = createCompletionSignal(); | ||
| const disconnectLifecycle = createCompletionSignal(); | ||
| return { | ||
@@ -356,2 +383,7 @@ bufferedDisconnect: undefined, | ||
| bufferedMessagesStartIndex: 0, | ||
| cleanupScheduled: false, | ||
| connectLifecyclePromise: connectLifecycle.promise, | ||
| connectLifecycleSettled: false, | ||
| disconnectLifecyclePromise: disconnectLifecycle.promise, | ||
| disconnectLifecycleSettled: false, | ||
| enqueuedMessageCount: 0, | ||
@@ -363,2 +395,4 @@ handlerQueue: Promise.resolve(), | ||
| queuedMessagesStartIndex: 0, | ||
| resolveConnectLifecycle: connectLifecycle.resolve, | ||
| resolveDisconnectLifecycle: disconnectLifecycle.resolve, | ||
| resolved: [], | ||
@@ -368,2 +402,16 @@ socketId: randomUUID() | ||
| } | ||
| settleConnectLifecycle(state) { | ||
| if (state.connectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.connectLifecycleSettled = true; | ||
| state.resolveConnectLifecycle(); | ||
| } | ||
| settleDisconnectLifecycle(state) { | ||
| if (state.disconnectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.disconnectLifecycleSettled = true; | ||
| state.resolveDisconnectLifecycle(); | ||
| } | ||
| getBufferedMessageCount(state) { | ||
@@ -407,3 +455,3 @@ return state.bufferedMessages.length - (state.bufferedMessagesStartIndex ?? 0); | ||
| this.clearQueuedMessages(state); | ||
| this.unregisterSocket(state.socketId); | ||
| this.unregisterSocketWithDeferredStateCleanup(state); | ||
| this.logger.warn(`WebSocket connection ${state.socketId} exceeded ready-state message queue limit (${String(limit)}). Connection terminated.`, 'WebSocketGatewayLifecycleService'); | ||
@@ -454,2 +502,4 @@ return; | ||
| this.logger.error('WebSocket gateway disconnect dispatch failed.', error, 'WebSocketGatewayLifecycleService'); | ||
| }).finally(() => { | ||
| this.settleDisconnectLifecycle(state); | ||
| }); | ||
@@ -473,7 +523,12 @@ } | ||
| socket.on('error', error => { | ||
| this.unregisterSocket(state.socketId); | ||
| this.unregisterSocket(state.socketId, { | ||
| deleteState: false | ||
| }); | ||
| this.scheduleSocketStateCleanup(state); | ||
| this.logger.error('WebSocket gateway socket emitted an error.', error, 'WebSocketGatewayLifecycleService'); | ||
| }); | ||
| socket.on('close', (code, reason) => { | ||
| this.unregisterSocket(state.socketId); | ||
| this.unregisterSocket(state.socketId, { | ||
| deleteState: false | ||
| }); | ||
| const disconnectEvent = { | ||
@@ -485,5 +540,7 @@ code, | ||
| state.bufferedDisconnect = disconnectEvent; | ||
| this.scheduleSocketStateCleanup(state); | ||
| return; | ||
| } | ||
| this.enqueueDisconnectDispatch(state, socket, disconnectEvent); | ||
| this.scheduleSocketStateCleanup(state); | ||
| }); | ||
@@ -545,3 +602,3 @@ } | ||
| if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CONNECTING) { | ||
| this.unregisterSocket(state.socketId); | ||
| this.unregisterSocketWithDeferredStateCleanup(state); | ||
| } | ||
@@ -553,2 +610,8 @@ } | ||
| async resolveUpgradeRejection(request, path) { | ||
| if (this.isShuttingDown) { | ||
| return { | ||
| body: 'WebSocket server is shutting down.', | ||
| status: 503 | ||
| }; | ||
| } | ||
| if (!this.tryReserveUpgradeSlot()) { | ||
@@ -569,2 +632,9 @@ return { | ||
| }); | ||
| if (this.isShuttingDown) { | ||
| this.releaseUpgradeReservation(); | ||
| return { | ||
| body: 'WebSocket server is shutting down.', | ||
| status: 503 | ||
| }; | ||
| } | ||
| if (result === false) { | ||
@@ -671,2 +741,3 @@ this.releaseUpgradeReservation(); | ||
| async runShutdownLifecycle() { | ||
| this.isShuttingDown = true; | ||
| this.stopHeartbeat(); | ||
@@ -677,2 +748,3 @@ this.detachUpgradeServerListener(); | ||
| const shutdownTimeoutMs = this.resolveShutdownTimeoutMs(); | ||
| await this.awaitPendingUpgradeOperations(shutdownTimeoutMs); | ||
| await this.closeGatewayAttachments(attachments, shutdownTimeoutMs); | ||
@@ -682,2 +754,44 @@ await this.closeOwnedUpgradeServers(ownedUpgradeServers, shutdownTimeoutMs); | ||
| } | ||
| trackPendingUpgradeOperation(operation) { | ||
| let trackedOperation; | ||
| trackedOperation = operation.then(() => undefined, () => undefined).finally(() => { | ||
| if (trackedOperation) { | ||
| this.pendingUpgradeOperations.delete(trackedOperation); | ||
| } | ||
| }); | ||
| this.pendingUpgradeOperations.add(trackedOperation); | ||
| return operation; | ||
| } | ||
| async awaitPendingUpgradeOperations(timeoutMs) { | ||
| if (this.pendingUpgradeOperations.size === 0) { | ||
| return; | ||
| } | ||
| await new Promise((resolve, reject) => { | ||
| let settled = false; | ||
| const timeout = setTimeout(() => { | ||
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; | ||
| reject(new Error(`Timed out while waiting for in-flight Node websocket upgrades after ${String(timeoutMs)}ms.`)); | ||
| }, timeoutMs); | ||
| Promise.all([...this.pendingUpgradeOperations]).then(() => { | ||
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; | ||
| clearTimeout(timeout); | ||
| resolve(); | ||
| }).catch(error => { | ||
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; | ||
| clearTimeout(timeout); | ||
| reject(error); | ||
| }); | ||
| }).catch(error => { | ||
| this.logger.error(`Failed to wait for in-flight Node websocket upgrades within ${String(timeoutMs)}ms.`, error, 'WebSocketGatewayLifecycleService'); | ||
| }); | ||
| } | ||
| stopHeartbeat() { | ||
@@ -748,11 +862,61 @@ if (!this.heartbeatTimer) { | ||
| await Promise.all(attachments.map(async attachment => { | ||
| this.terminateAttachmentClients(attachment); | ||
| await this.closeAttachmentClients(attachment, shutdownTimeoutMs); | ||
| await this.closeGatewayAttachment(attachment, shutdownTimeoutMs); | ||
| })); | ||
| } | ||
| terminateAttachmentClients(attachment) { | ||
| async closeAttachmentClients(attachment, timeoutMs) { | ||
| const states = [...this.socketStates.values()]; | ||
| for (const client of attachment.server.clients) { | ||
| client.terminate(); | ||
| if (client.readyState === WebSocket.OPEN) { | ||
| client.close(1001, 'Server shutting down'); | ||
| } else if (client.readyState === WebSocket.CONNECTING) { | ||
| client.terminate(); | ||
| } | ||
| } | ||
| await this.awaitHandlerQueueDrain(states, timeoutMs); | ||
| } | ||
| async awaitHandlerQueueDrain(states, timeoutMs) { | ||
| if (states.length === 0) { | ||
| return; | ||
| } | ||
| await new Promise((resolve, reject) => { | ||
| let settled = false; | ||
| const timeout = setTimeout(() => { | ||
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; | ||
| reject(new Error(`Timed out while closing Node websocket connections after ${String(timeoutMs)}ms.`)); | ||
| }, timeoutMs); | ||
| Promise.all(states.map(async state => { | ||
| await state.connectLifecyclePromise; | ||
| await state.disconnectLifecyclePromise; | ||
| })).then(() => { | ||
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; | ||
| clearTimeout(timeout); | ||
| resolve(); | ||
| }).catch(error => { | ||
| if (settled) { | ||
| return; | ||
| } | ||
| settled = true; | ||
| clearTimeout(timeout); | ||
| reject(error); | ||
| }); | ||
| }).catch(error => { | ||
| this.logger.error(`Failed to close Node websocket connections within ${String(timeoutMs)}ms.`, error, 'WebSocketGatewayLifecycleService'); | ||
| }); | ||
| } | ||
| scheduleSocketStateCleanup(state) { | ||
| if (state.cleanupScheduled) { | ||
| return; | ||
| } | ||
| state.cleanupScheduled = true; | ||
| void Promise.all([state.connectLifecyclePromise, state.disconnectLifecyclePromise]).finally(() => { | ||
| this.socketStates.delete(state.socketId); | ||
| }); | ||
| } | ||
| async closeGatewayAttachment(attachment, shutdownTimeoutMs) { | ||
@@ -768,2 +932,3 @@ try { | ||
| this.socketRooms.clear(); | ||
| this.socketStates.clear(); | ||
| this.roomSockets.clear(); | ||
@@ -839,3 +1004,3 @@ this.pingPending.clear(); | ||
| socket.terminate(); | ||
| this.unregisterSocket(socketId); | ||
| this.unregisterTrackedSocketWithDeferredStateCleanup(socketId); | ||
| this.logger.warn(`WebSocket connection ${socketId} exceeded bufferedAmount threshold (${String(maxBufferedAmountBytes)} bytes). Connection terminated.`, 'WebSocketGatewayLifecycleService'); | ||
@@ -877,3 +1042,3 @@ continue; | ||
| socket.terminate(); | ||
| this.unregisterSocket(socketId); | ||
| this.unregisterTrackedSocketWithDeferredStateCleanup(socketId); | ||
| } | ||
@@ -890,4 +1055,21 @@ continue; | ||
| } | ||
| unregisterSocket(socketId) { | ||
| unregisterSocketWithDeferredStateCleanup(state) { | ||
| this.unregisterSocket(state.socketId, { | ||
| deleteState: false | ||
| }); | ||
| this.scheduleSocketStateCleanup(state); | ||
| } | ||
| unregisterTrackedSocketWithDeferredStateCleanup(socketId) { | ||
| const state = this.socketStates.get(socketId); | ||
| if (!state) { | ||
| this.unregisterSocket(socketId); | ||
| return; | ||
| } | ||
| this.unregisterSocketWithDeferredStateCleanup(state); | ||
| } | ||
| unregisterSocket(socketId, options = {}) { | ||
| this.socketRegistry.delete(socketId); | ||
| if (options.deleteState !== false) { | ||
| this.socketStates.delete(socketId); | ||
| } | ||
| this.pingPending.delete(socketId); | ||
@@ -894,0 +1076,0 @@ this.pingSentAt.delete(socketId); |
+8
-7
@@ -12,3 +12,3 @@ { | ||
| ], | ||
| "version": "1.0.0", | ||
| "version": "1.0.1", | ||
| "private": false, | ||
@@ -73,6 +73,6 @@ "license": "MIT", | ||
| "ws": "^8.18.3", | ||
| "@fluojs/core": "^1.0.0", | ||
| "@fluojs/di": "^1.0.0", | ||
| "@fluojs/core": "^1.0.1", | ||
| "@fluojs/di": "^1.0.1", | ||
| "@fluojs/http": "^1.0.0", | ||
| "@fluojs/runtime": "^1.0.0" | ||
| "@fluojs/runtime": "^1.0.1" | ||
| }, | ||
@@ -82,5 +82,6 @@ "devDependencies": { | ||
| "vitest": "^3.2.4", | ||
| "@fluojs/platform-bun": "^1.0.0", | ||
| "@fluojs/platform-express": "^1.0.0", | ||
| "@fluojs/platform-fastify": "^1.0.0" | ||
| "@fluojs/platform-bun": "^1.0.1", | ||
| "@fluojs/platform-express": "^1.0.1", | ||
| "@fluojs/platform-fastify": "^1.0.1", | ||
| "@fluojs/testing": "^1.0.1" | ||
| }, | ||
@@ -87,0 +88,0 @@ "scripts": { |
+2
-1
@@ -105,3 +105,3 @@ # @fluojs/websockets | ||
| 옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수, inbound payload 크기, pending message buffer, shutdown cleanup에 bounded default를 적용합니다. 기본값은 `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, Node backpressure `maxBufferedAmountBytes: 1 MiB`와 drop behavior입니다. 또한 server-backed Node listener는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat timer를 활성화합니다. 공식 fetch-style runtime module(`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, `@fluojs/websockets/cloudflare-workers`)은 애플리케이션 shutdown 시 추적 중인 websocket 클라이언트를 닫고 `shutdown.timeoutMs` 범위 안에서 `@OnDisconnect()` cleanup이 마무리될 수 있도록 bounded 기회를 제공합니다. | ||
| 옵션을 생략하면 `@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 동작을 제공합니다. | ||
@@ -121,2 +121,3 @@ ## 바이너리 페이로드 | ||
| - `WebSocketGatewayLifecycleService`: 기본 Node.js 기반 lifecycle service token을 위한 루트 alias입니다. | ||
| - `WebSocketRoomService`: websocket room join, leave, broadcast, 조회를 위해 runtime lifecycle service가 구현하는 Room management contract입니다. | ||
| - Metadata helper와 symbol: `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`. | ||
@@ -123,0 +124,0 @@ |
+2
-1
@@ -105,3 +105,3 @@ # @fluojs/websockets | ||
| When omitted, `@fluojs/websockets` applies bounded defaults for concurrent connections, inbound payload size, pending message buffers, and shutdown cleanup. Default settings are `maxConnections: 1000`, `maxPayloadBytes: 1 MiB`, `buffer.maxPendingMessagesPerSocket: 256`, `shutdown.timeoutMs: 5000`, Node heartbeat interval `30s`, and Node backpressure `maxBufferedAmountBytes: 1 MiB` with drop behavior. Server-backed Node listeners enable heartbeat timers unless you explicitly set `heartbeat.enabled` to `false`. The official fetch-style runtime modules (`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, and `@fluojs/websockets/cloudflare-workers`) close tracked websocket clients during application shutdown and give `@OnDisconnect()` cleanup a bounded chance to finish within `shutdown.timeoutMs`. | ||
| 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. | ||
@@ -121,2 +121,3 @@ ## Binary Payloads | ||
| - `WebSocketGatewayLifecycleService`: Root alias for the default Node.js-backed lifecycle service token. | ||
| - `WebSocketRoomService`: Room management contract implemented by runtime lifecycle services for joining, leaving, broadcasting to, and inspecting websocket rooms. | ||
| - Metadata helpers and symbols: `defineWebSocketGatewayMetadata`, `getWebSocketGatewayMetadata`, `defineWebSocketHandlerMetadata`, `getWebSocketHandlerMetadata`, `getWebSocketHandlerMetadataEntries`, `webSocketGatewayMetadataSymbol`, `webSocketHandlerMetadataSymbol`. | ||
@@ -123,0 +124,0 @@ |
244287
3.1%4988
4.03%142
0.71%6
20%Updated
Updated
Updated