@fluojs/websockets
Advanced tools
@@ -15,2 +15,4 @@ import type { Container } from '@fluojs/di'; | ||
| private readonly moduleOptions; | ||
| private isShuttingDown; | ||
| private readonly pendingUpgradeOperations; | ||
| private pendingUpgradeReservations; | ||
@@ -21,2 +23,3 @@ private readonly roomSockets; | ||
| private readonly socketRooms; | ||
| private readonly socketStates; | ||
| constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, adapter: HttpApplicationAdapter, moduleOptions: WebSocketModuleOptions); | ||
@@ -32,2 +35,5 @@ onApplicationBootstrap(): Promise<void>; | ||
| private createConnectionHandlerState; | ||
| private settleOpenRegistration; | ||
| private settleConnectLifecycle; | ||
| private settleDisconnectLifecycle; | ||
| private getBufferedMessageCount; | ||
@@ -58,4 +64,9 @@ private getQueuedMessageCount; | ||
| private resolveIdleTimeoutSeconds; | ||
| private resolveShutdownTimeoutMs; | ||
| private shutdown; | ||
| private runShutdownLifecycle; | ||
| private trackPendingUpgradeOperation; | ||
| private awaitPendingUpgradeOperations; | ||
| private closeActiveSockets; | ||
| private awaitHandlerQueueDrain; | ||
| joinRoom(socketId: string, room: string): void; | ||
@@ -62,0 +73,0 @@ leaveRoom(socketId: string, room: string): void; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"bun-service.d.ts","sourceRoot":"","sources":["../../src/bun/bun-service.ts"],"names":[],"mappings":"AACA,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;AASzI,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAa3D,OAAO,KAAK,EAGV,oBAAoB,EACrB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAyG7D;;GAEG;AACH,qBACa,mCACX,YAAW,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,oBAAoB;IAS7F,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAXhC,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwD;IACvF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;gBAG3C,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,sBAAsB,EAC/B,aAAa,EAAE,sBAAsB;IAGlD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB7C,OAAO,CAAC,gCAAgC;IAclC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,OAAO,CAAC,aAAa;IAiCrB,OAAO,CAAC,sBAAsB;YAmBhB,oBAAoB;YAkDpB,sBAAsB;IAWpC,OAAO,CAAC,4BAA4B;IAqBpC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,sBAAsB;YAwDhB,iBAAiB;IAmB/B,OAAO,CAAC,yBAAyB;YAsBnB,yBAAyB;YAczB,kBAAkB;YASlB,yBAAyB;IASvC,OAAO,CAAC,8BAA8B;IAoBtC,OAAO,CAAC,wBAAwB;YAUlB,uBAAuB;IAmDrC,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,8BAA8B;IAItC,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,yBAAyB;IAMjC,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,yBAAyB;YAcnB,QAAQ;YAUR,oBAAoB;IAYlC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAmB9C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAc/C,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IA4BjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAU/C,OAAO,CAAC,gBAAgB;CAgBzB"} | ||
| {"version":3,"file":"bun-service.d.ts","sourceRoot":"","sources":["../../src/bun/bun-service.ts"],"names":[],"mappings":"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"} |
+185
-5
@@ -14,2 +14,3 @@ let _initClass; | ||
| const DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES = 1_048_576; | ||
| const DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS = 5_000; | ||
| const LIFECYCLE_LOG_CONTEXT = 'WebSocketGatewayLifecycleService'; | ||
@@ -44,2 +45,12 @@ function hasBunWebSocketBindingHost(adapter) { | ||
| } | ||
| function createCompletionSignal() { | ||
| let resolve; | ||
| const promise = new Promise(res => { | ||
| resolve = res; | ||
| }); | ||
| return { | ||
| promise, | ||
| resolve | ||
| }; | ||
| } | ||
@@ -54,2 +65,4 @@ /** | ||
| } | ||
| isShuttingDown = false; | ||
| pendingUpgradeOperations = new Set(); | ||
| pendingUpgradeReservations = 0; | ||
@@ -60,2 +73,3 @@ roomSockets = new Map(); | ||
| socketRooms = new Map(); | ||
| socketStates = new Map(); | ||
| constructor(runtimeContainer, compiledModules, logger, adapter, moduleOptions) { | ||
@@ -97,3 +111,3 @@ this.runtimeContainer = runtimeContainer; | ||
| return { | ||
| fetch: (request, server) => this.handleUpgradeRequest(request, server, descriptorsByPath), | ||
| fetch: (request, server) => this.trackPendingUpgradeOperation(this.handleUpgradeRequest(request, server, descriptorsByPath)), | ||
| websocket: { | ||
@@ -116,3 +130,3 @@ backpressureLimit: this.resolveBackpressureLimit(), | ||
| open: socket => { | ||
| void this.bindConnectionHandlers(socket).catch(error => { | ||
| void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(socket)).catch(error => { | ||
| this.unregisterSocket(socket.data.state.socketId); | ||
@@ -164,3 +178,10 @@ this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| } | ||
| if (this.isShuttingDown) { | ||
| this.releaseUpgradeReservation(); | ||
| return new Response('WebSocket server is shutting down.', { | ||
| status: 503 | ||
| }); | ||
| } | ||
| const state = this.createConnectionHandlerState(request, [...descriptors]); | ||
| void this.trackPendingUpgradeOperation(state.openRegistrationPromise); | ||
| let upgraded = false; | ||
@@ -174,2 +195,3 @@ try { | ||
| } catch (error) { | ||
| this.settleOpenRegistration(state); | ||
| this.releaseUpgradeReservation(); | ||
@@ -179,2 +201,3 @@ throw error; | ||
| if (!upgraded) { | ||
| this.settleOpenRegistration(state); | ||
| this.releaseUpgradeReservation(); | ||
@@ -193,7 +216,23 @@ return new Response(null, { | ||
| this.socketRegistry.set(state.socketId, socket); | ||
| await this.resolveConnectionGateways(state); | ||
| await this.runConnectHandlers(state, socket); | ||
| await this.finalizeConnectionBinding(state, socket); | ||
| this.socketStates.set(state.socketId, state); | ||
| this.settleOpenRegistration(state); | ||
| try { | ||
| await this.resolveConnectionGateways(state); | ||
| await this.runConnectHandlers(state, socket); | ||
| await this.finalizeConnectionBinding(state, socket); | ||
| if (this.isShuttingDown && socket.readyState === 1) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| await state.disconnectLifecyclePromise; | ||
| } | ||
| } finally { | ||
| if (!state.handlersReady && state.bufferedDisconnect) { | ||
| this.settleDisconnectLifecycle(state); | ||
| } | ||
| this.settleConnectLifecycle(state); | ||
| } | ||
| } | ||
| createConnectionHandlerState(request, descriptors) { | ||
| const connectLifecycle = createCompletionSignal(); | ||
| const disconnectLifecycle = createCompletionSignal(); | ||
| const openRegistration = createCompletionSignal(); | ||
| return { | ||
@@ -203,6 +242,12 @@ bufferedDisconnect: undefined, | ||
| bufferedMessagesStartIndex: 0, | ||
| connectLifecycleSettled: false, | ||
| connectLifecyclePromise: connectLifecycle.promise, | ||
| descriptors, | ||
| disconnectLifecycleSettled: false, | ||
| disconnectLifecyclePromise: disconnectLifecycle.promise, | ||
| enqueuedMessageCount: 0, | ||
| handlerQueue: Promise.resolve(), | ||
| handlersReady: false, | ||
| openRegistrationSettled: false, | ||
| openRegistrationPromise: openRegistration.promise, | ||
| processingMessageQueue: false, | ||
@@ -212,2 +257,5 @@ queuedMessages: [], | ||
| request, | ||
| resolveOpenRegistration: openRegistration.resolve, | ||
| resolveConnectLifecycle: connectLifecycle.resolve, | ||
| resolveDisconnectLifecycle: disconnectLifecycle.resolve, | ||
| resolved: [], | ||
@@ -217,2 +265,23 @@ socketId: crypto.randomUUID() | ||
| } | ||
| settleOpenRegistration(state) { | ||
| if (state.openRegistrationSettled) { | ||
| return; | ||
| } | ||
| state.openRegistrationSettled = true; | ||
| state.resolveOpenRegistration(); | ||
| } | ||
| settleConnectLifecycle(state) { | ||
| if (state.connectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.connectLifecycleSettled = true; | ||
| state.resolveConnectLifecycle(); | ||
| } | ||
| settleDisconnectLifecycle(state) { | ||
| if (state.disconnectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.disconnectLifecycleSettled = true; | ||
| state.resolveDisconnectLifecycle(); | ||
| } | ||
| getBufferedMessageCount(state) { | ||
@@ -346,2 +415,4 @@ return state.bufferedMessages.length - state.bufferedMessagesStartIndex; | ||
| this.logger.error('WebSocket gateway disconnect dispatch failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| }).finally(() => { | ||
| this.settleDisconnectLifecycle(state); | ||
| }); | ||
@@ -396,2 +467,8 @@ } | ||
| async resolveUpgradeRejection(request, path) { | ||
| if (this.isShuttingDown) { | ||
| return { | ||
| body: 'WebSocket server is shutting down.', | ||
| status: 503 | ||
| }; | ||
| } | ||
| if (!this.tryReserveUpgradeSlot()) { | ||
@@ -483,2 +560,9 @@ return { | ||
| } | ||
| resolveShutdownTimeoutMs() { | ||
| const configured = this.moduleOptions.shutdown?.timeoutMs; | ||
| if (typeof configured !== 'number' || !Number.isFinite(configured) || configured <= 0) { | ||
| return DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS; | ||
| } | ||
| return Math.floor(configured); | ||
| } | ||
| async shutdown() { | ||
@@ -493,2 +577,3 @@ if (this.shutdownPromise) { | ||
| async runShutdownLifecycle() { | ||
| this.isShuttingDown = true; | ||
| if (hasBunWebSocketBindingHost(this.adapter)) { | ||
@@ -498,7 +583,101 @@ const bunAdapter = this.adapter; | ||
| } | ||
| const shutdownTimeoutMs = this.resolveShutdownTimeoutMs(); | ||
| await this.awaitPendingUpgradeOperations(shutdownTimeoutMs); | ||
| await this.closeActiveSockets(shutdownTimeoutMs); | ||
| this.pendingUpgradeReservations = 0; | ||
| this.socketRegistry.clear(); | ||
| this.socketRooms.clear(); | ||
| this.socketStates.clear(); | ||
| this.roomSockets.clear(); | ||
| } | ||
| 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 Bun 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 Bun websocket upgrades within ${String(timeoutMs)}ms.`, error, LIFECYCLE_LOG_CONTEXT); | ||
| }); | ||
| } | ||
| async closeActiveSockets(timeoutMs) { | ||
| const activeSockets = [...this.socketRegistry.entries()]; | ||
| if (activeSockets.length === 0) { | ||
| return; | ||
| } | ||
| const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined); | ||
| for (const [, socket] of activeSockets) { | ||
| if (socket.readyState === 1) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| } | ||
| } | ||
| await this.awaitHandlerQueueDrain(activeStates, 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 Bun 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 Bun websocket connections within ${String(timeoutMs)}ms.`, error, LIFECYCLE_LOG_CONTEXT); | ||
| }); | ||
| } | ||
| joinRoom(socketId, room) { | ||
@@ -560,2 +739,3 @@ let rooms = this.socketRooms.get(socketId); | ||
| this.socketRegistry.delete(socketId); | ||
| this.socketStates.delete(socketId); | ||
| const rooms = this.socketRooms.get(socketId); | ||
@@ -562,0 +742,0 @@ if (rooms) { |
@@ -15,2 +15,4 @@ import type { Container } from '@fluojs/di'; | ||
| private readonly moduleOptions; | ||
| private isShuttingDown; | ||
| private readonly pendingUpgradeOperations; | ||
| private pendingUpgradeReservations; | ||
@@ -21,2 +23,3 @@ private readonly roomSockets; | ||
| private readonly socketRooms; | ||
| private readonly socketStates; | ||
| constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, adapter: HttpApplicationAdapter, moduleOptions: WebSocketModuleOptions); | ||
@@ -28,5 +31,8 @@ onApplicationBootstrap(): Promise<void>; | ||
| private createBinding; | ||
| private handleUpgradeRequest; | ||
| private groupDescriptorsByPath; | ||
| private bindConnectionHandlers; | ||
| private createConnectionHandlerState; | ||
| private settleConnectLifecycle; | ||
| private settleDisconnectLifecycle; | ||
| private attachConnectionListeners; | ||
@@ -56,4 +62,9 @@ private getBufferedMessageCount; | ||
| private resolveMaxPayloadBytes; | ||
| private resolveShutdownTimeoutMs; | ||
| private shutdown; | ||
| private runShutdownLifecycle; | ||
| private trackPendingUpgradeOperation; | ||
| private awaitPendingUpgradeOperations; | ||
| private closeActiveSockets; | ||
| private awaitHandlerQueueDrain; | ||
| joinRoom(socketId: string, room: string): void; | ||
@@ -60,0 +71,0 @@ leaveRoom(socketId: string, room: string): void; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"cloudflare-workers-service.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-service.ts"],"names":[],"mappings":"AACA,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;AAa3D,OAAO,KAAK,EAA8B,oBAAoB,EAA6B,MAAM,aAAa,CAAC;AAC/G,OAAO,KAAK,EAKV,sBAAsB,EACvB,MAAM,+BAA+B,CAAC;AA0GvC;;GAEG;AACH,qBACa,iDACX,YAAW,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,oBAAoB;IAS7F,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAXhC,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgD;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;gBAG3C,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,sBAAsB,EAC/B,aAAa,EAAE,sBAAsB;IAGlD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB7C,OAAO,CAAC,gCAAgC;IAclC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,OAAO,CAAC,aAAa;IAqDrB,OAAO,CAAC,sBAAsB;YAmBhB,sBAAsB;IAgBpC,OAAO,CAAC,4BAA4B;IAqBpC,OAAO,CAAC,yBAAyB;IAyCjC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,sBAAsB;YAyDhB,iBAAiB;YAqBjB,gBAAgB;IAU9B,OAAO,CAAC,yBAAyB;YAsBnB,yBAAyB;YAczB,kBAAkB;YAgBlB,yBAAyB;YAUzB,8BAA8B;IAqB5C,OAAO,CAAC,YAAY;YAUN,uBAAuB;IAmDrC,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,8BAA8B;IAItC,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,yBAAyB;IAMjC,OAAO,CAAC,sBAAsB;YAUhB,QAAQ;YAUR,oBAAoB;IAWlC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAmB9C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAc/C,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IA4BjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAU/C,OAAO,CAAC,gBAAgB;CAoBzB"} | ||
| {"version":3,"file":"cloudflare-workers-service.d.ts","sourceRoot":"","sources":["../../src/cloudflare-workers/cloudflare-workers-service.ts"],"names":[],"mappings":"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"} |
@@ -14,2 +14,3 @@ let _initClass; | ||
| const DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES = 1_048_576; | ||
| const DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS = 5_000; | ||
| const LIFECYCLE_LOG_CONTEXT = 'WebSocketGatewayLifecycleService'; | ||
@@ -48,2 +49,12 @@ const WEBSOCKET_OPEN_READY_STATE = 1; | ||
| } | ||
| function createCompletionSignal() { | ||
| let resolve; | ||
| const promise = new Promise(res => { | ||
| resolve = res; | ||
| }); | ||
| return { | ||
| promise, | ||
| resolve | ||
| }; | ||
| } | ||
@@ -58,2 +69,4 @@ /** | ||
| } | ||
| isShuttingDown = false; | ||
| pendingUpgradeOperations = new Set(); | ||
| pendingUpgradeReservations = 0; | ||
@@ -64,2 +77,3 @@ roomSockets = new Map(); | ||
| socketRooms = new Map(); | ||
| socketStates = new Map(); | ||
| constructor(runtimeContainer, compiledModules, logger, adapter, moduleOptions) { | ||
@@ -100,50 +114,57 @@ this.runtimeContainer = runtimeContainer; | ||
| return { | ||
| fetch: async (request, host) => { | ||
| if (!isWebSocketUpgradeRequest(request)) { | ||
| return new Response(null, { | ||
| status: 426 | ||
| }); | ||
| } | ||
| let targetPath; | ||
| try { | ||
| targetPath = normalizeGatewayPath(new URL(request.url).pathname); | ||
| } catch { | ||
| return new Response(null, { | ||
| status: 400 | ||
| }); | ||
| } | ||
| const matchedDescriptors = descriptorsByPath.get(targetPath); | ||
| if (!matchedDescriptors) { | ||
| return new Response(null, { | ||
| status: 404 | ||
| }); | ||
| } | ||
| const rejection = await this.resolveUpgradeRejection(request, targetPath); | ||
| if (rejection) { | ||
| return new Response(rejection.body ?? null, { | ||
| headers: rejection.headers, | ||
| status: rejection.status | ||
| }); | ||
| } | ||
| let response; | ||
| let serverSocket; | ||
| try { | ||
| ({ | ||
| response, | ||
| serverSocket | ||
| } = host.upgrade(request)); | ||
| } catch (error) { | ||
| this.releaseUpgradeReservation(); | ||
| throw error; | ||
| } | ||
| serverSocket.accept(); | ||
| void this.bindConnectionHandlers(serverSocket, request, matchedDescriptors).catch(error => { | ||
| this.unregisterSocket(this.findSocketId(serverSocket)); | ||
| this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| serverSocket.close(1011, 'Internal server error'); | ||
| }); | ||
| return response; | ||
| } | ||
| fetch: (request, host) => this.trackPendingUpgradeOperation(this.handleUpgradeRequest(request, host, descriptorsByPath)) | ||
| }; | ||
| } | ||
| async handleUpgradeRequest(request, host, descriptorsByPath) { | ||
| if (!isWebSocketUpgradeRequest(request)) { | ||
| return new Response(null, { | ||
| status: 426 | ||
| }); | ||
| } | ||
| let targetPath; | ||
| try { | ||
| targetPath = normalizeGatewayPath(new URL(request.url).pathname); | ||
| } catch { | ||
| return new Response(null, { | ||
| status: 400 | ||
| }); | ||
| } | ||
| const matchedDescriptors = descriptorsByPath.get(targetPath); | ||
| if (!matchedDescriptors) { | ||
| return new Response(null, { | ||
| status: 404 | ||
| }); | ||
| } | ||
| const rejection = await this.resolveUpgradeRejection(request, targetPath); | ||
| if (rejection) { | ||
| return new Response(rejection.body ?? null, { | ||
| headers: rejection.headers, | ||
| status: rejection.status | ||
| }); | ||
| } | ||
| if (this.isShuttingDown) { | ||
| this.releaseUpgradeReservation(); | ||
| return new Response('WebSocket server is shutting down.', { | ||
| status: 503 | ||
| }); | ||
| } | ||
| let response; | ||
| let serverSocket; | ||
| try { | ||
| ({ | ||
| response, | ||
| serverSocket | ||
| } = host.upgrade(request)); | ||
| } catch (error) { | ||
| this.releaseUpgradeReservation(); | ||
| throw error; | ||
| } | ||
| serverSocket.accept(); | ||
| void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(serverSocket, request, matchedDescriptors)).catch(error => { | ||
| this.unregisterSocket(this.findSocketId(serverSocket)); | ||
| this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| serverSocket.close(1011, 'Internal server error'); | ||
| }); | ||
| return response; | ||
| } | ||
| groupDescriptorsByPath(descriptors) { | ||
@@ -165,8 +186,22 @@ const descriptorsByPath = new Map(); | ||
| this.socketRegistry.set(state.socketId, socket); | ||
| this.socketStates.set(state.socketId, state); | ||
| this.attachConnectionListeners(state, socket, request); | ||
| await this.resolveConnectionGateways(state); | ||
| await this.runConnectHandlers(state, socket); | ||
| await this.finalizeConnectionBinding(state, socket, request); | ||
| try { | ||
| await this.resolveConnectionGateways(state); | ||
| await this.runConnectHandlers(state, socket); | ||
| await this.finalizeConnectionBinding(state, socket, request); | ||
| if (this.isShuttingDown && socket.readyState === WEBSOCKET_OPEN_READY_STATE) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| await state.disconnectLifecyclePromise; | ||
| } | ||
| } finally { | ||
| if (!state.handlersReady && state.bufferedDisconnect) { | ||
| this.settleDisconnectLifecycle(state); | ||
| } | ||
| this.settleConnectLifecycle(state); | ||
| } | ||
| } | ||
| createConnectionHandlerState(request, descriptors) { | ||
| const connectLifecycle = createCompletionSignal(); | ||
| const disconnectLifecycle = createCompletionSignal(); | ||
| return { | ||
@@ -176,3 +211,7 @@ bufferedDisconnect: undefined, | ||
| bufferedMessagesStartIndex: 0, | ||
| connectLifecycleSettled: false, | ||
| connectLifecyclePromise: connectLifecycle.promise, | ||
| descriptors, | ||
| disconnectLifecycleSettled: false, | ||
| disconnectLifecyclePromise: disconnectLifecycle.promise, | ||
| enqueuedMessageCount: 0, | ||
@@ -185,2 +224,4 @@ handlerQueue: Promise.resolve(), | ||
| request, | ||
| resolveConnectLifecycle: connectLifecycle.resolve, | ||
| resolveDisconnectLifecycle: disconnectLifecycle.resolve, | ||
| resolved: [], | ||
@@ -190,2 +231,16 @@ socketId: crypto.randomUUID() | ||
| } | ||
| settleConnectLifecycle(state) { | ||
| if (state.connectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.connectLifecycleSettled = true; | ||
| state.resolveConnectLifecycle(); | ||
| } | ||
| settleDisconnectLifecycle(state) { | ||
| if (state.disconnectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.disconnectLifecycleSettled = true; | ||
| state.resolveDisconnectLifecycle(); | ||
| } | ||
| attachConnectionListeners(state, socket, request) { | ||
@@ -328,2 +383,4 @@ socket.addEventListener('message', event => { | ||
| this.logger.error('WebSocket gateway disconnect dispatch failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| }).finally(() => { | ||
| this.settleDisconnectLifecycle(state); | ||
| }); | ||
@@ -379,2 +436,8 @@ } | ||
| async resolveUpgradeRejection(request, path) { | ||
| if (this.isShuttingDown) { | ||
| return { | ||
| body: 'WebSocket server is shutting down.', | ||
| status: 503 | ||
| }; | ||
| } | ||
| if (!this.tryReserveUpgradeSlot()) { | ||
@@ -456,2 +519,9 @@ return { | ||
| } | ||
| resolveShutdownTimeoutMs() { | ||
| const configured = this.moduleOptions.shutdown?.timeoutMs; | ||
| if (typeof configured !== 'number' || !Number.isFinite(configured) || configured <= 0) { | ||
| return DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS; | ||
| } | ||
| return Math.floor(configured); | ||
| } | ||
| async shutdown() { | ||
@@ -466,10 +536,108 @@ if (this.shutdownPromise) { | ||
| async runShutdownLifecycle() { | ||
| this.isShuttingDown = true; | ||
| if (hasCloudflareWorkerWebSocketBindingHost(this.adapter)) { | ||
| this.adapter.configureWebSocketBinding(undefined); | ||
| } | ||
| const shutdownTimeoutMs = this.resolveShutdownTimeoutMs(); | ||
| await this.awaitPendingUpgradeOperations(shutdownTimeoutMs); | ||
| await this.closeActiveSockets(shutdownTimeoutMs); | ||
| this.pendingUpgradeReservations = 0; | ||
| this.socketRegistry.clear(); | ||
| this.socketRooms.clear(); | ||
| this.socketStates.clear(); | ||
| this.roomSockets.clear(); | ||
| } | ||
| trackPendingUpgradeOperation(operation) { | ||
| if (typeof operation === 'function') { | ||
| return (...args) => this.trackPendingUpgradeOperation(operation(...args)); | ||
| } | ||
| 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 Cloudflare Worker 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 Cloudflare Worker websocket upgrades within ${String(timeoutMs)}ms.`, error, LIFECYCLE_LOG_CONTEXT); | ||
| }); | ||
| } | ||
| async closeActiveSockets(timeoutMs) { | ||
| const activeSockets = [...this.socketRegistry.entries()]; | ||
| if (activeSockets.length === 0) { | ||
| return; | ||
| } | ||
| const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined); | ||
| for (const [, socket] of activeSockets) { | ||
| if (socket.readyState === WEBSOCKET_OPEN_READY_STATE) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| } | ||
| } | ||
| await this.awaitHandlerQueueDrain(activeStates, 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 Cloudflare Worker 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 Cloudflare Worker websocket connections within ${String(timeoutMs)}ms.`, error, LIFECYCLE_LOG_CONTEXT); | ||
| }); | ||
| } | ||
| joinRoom(socketId, room) { | ||
@@ -535,2 +703,3 @@ let rooms = this.socketRooms.get(socketId); | ||
| this.socketRegistry.delete(socketId); | ||
| this.socketStates.delete(socketId); | ||
| const rooms = this.socketRooms.get(socketId); | ||
@@ -537,0 +706,0 @@ if (rooms) { |
@@ -15,2 +15,4 @@ import type { Container } from '@fluojs/di'; | ||
| private readonly moduleOptions; | ||
| private isShuttingDown; | ||
| private readonly pendingUpgradeOperations; | ||
| private pendingUpgradeReservations; | ||
@@ -21,2 +23,3 @@ private readonly roomSockets; | ||
| private readonly socketRooms; | ||
| private readonly socketStates; | ||
| constructor(runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, adapter: HttpApplicationAdapter, moduleOptions: WebSocketModuleOptions); | ||
@@ -28,5 +31,8 @@ onApplicationBootstrap(): Promise<void>; | ||
| private createBinding; | ||
| private handleUpgradeRequest; | ||
| private groupDescriptorsByPath; | ||
| private bindConnectionHandlers; | ||
| private createConnectionHandlerState; | ||
| private settleConnectLifecycle; | ||
| private settleDisconnectLifecycle; | ||
| private attachConnectionListeners; | ||
@@ -56,4 +62,9 @@ private getBufferedMessageCount; | ||
| private resolveMaxPayloadBytes; | ||
| private resolveShutdownTimeoutMs; | ||
| private shutdown; | ||
| private runShutdownLifecycle; | ||
| private trackPendingUpgradeOperation; | ||
| private awaitPendingUpgradeOperations; | ||
| private closeActiveSockets; | ||
| private awaitHandlerQueueDrain; | ||
| joinRoom(socketId: string, room: string): void; | ||
@@ -60,0 +71,0 @@ leaveRoom(socketId: string, room: string): void; |
@@ -1,1 +0,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,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEzI,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAa3D,OAAO,KAAK,EAA8B,oBAAoB,EAA6B,MAAM,aAAa,CAAC;AAC/G,OAAO,KAAK,EAKV,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AAsGzB;;GAEG;AACH,qBACa,oCACX,YAAW,sBAAsB,EAAE,qBAAqB,EAAE,eAAe,EAAE,oBAAoB;IAS7F,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAXhC,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;IACzE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;gBAG3C,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,sBAAsB,EAC/B,aAAa,EAAE,sBAAsB;IAGlD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB7C,OAAO,CAAC,gCAAgC;IAclC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,OAAO,CAAC,aAAa;IAoDrB,OAAO,CAAC,sBAAsB;YAmBhB,sBAAsB;IAgBpC,OAAO,CAAC,4BAA4B;IAqBpC,OAAO,CAAC,yBAAyB;IAwCjC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,sBAAsB;YAyDhB,iBAAiB;YAqBjB,gBAAgB;IAQ9B,OAAO,CAAC,yBAAyB;YAsBnB,yBAAyB;YAczB,kBAAkB;YAgBlB,yBAAyB;YAUzB,8BAA8B;IAqB5C,OAAO,CAAC,YAAY;YAUN,uBAAuB;IAmDrC,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,8BAA8B;IAItC,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,yBAAyB;IAMjC,OAAO,CAAC,sBAAsB;YAUhB,QAAQ;YAUR,oBAAoB;IAWlC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAmB9C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAc/C,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI;IA4BjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAU/C,OAAO,CAAC,gBAAgB;CAoBzB"} | ||
| {"version":3,"file":"deno-service.d.ts","sourceRoot":"","sources":["../../src/deno/deno-service.ts"],"names":[],"mappings":"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"} |
+217
-48
@@ -14,2 +14,3 @@ let _initClass; | ||
| const DEFAULT_MAX_WEBSOCKET_PAYLOAD_BYTES = 1_048_576; | ||
| const DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS = 5_000; | ||
| const LIFECYCLE_LOG_CONTEXT = 'WebSocketGatewayLifecycleService'; | ||
@@ -45,2 +46,12 @@ const WEBSOCKET_OPEN_READY_STATE = 1; | ||
| } | ||
| function createCompletionSignal() { | ||
| let resolve; | ||
| const promise = new Promise(res => { | ||
| resolve = res; | ||
| }); | ||
| return { | ||
| promise, | ||
| resolve | ||
| }; | ||
| } | ||
@@ -55,2 +66,4 @@ /** | ||
| } | ||
| isShuttingDown = false; | ||
| pendingUpgradeOperations = new Set(); | ||
| pendingUpgradeReservations = 0; | ||
@@ -61,2 +74,3 @@ roomSockets = new Map(); | ||
| socketRooms = new Map(); | ||
| socketStates = new Map(); | ||
| constructor(runtimeContainer, compiledModules, logger, adapter, moduleOptions) { | ||
@@ -97,49 +111,56 @@ this.runtimeContainer = runtimeContainer; | ||
| return { | ||
| fetch: async (request, host) => { | ||
| if (!isWebSocketUpgradeRequest(request)) { | ||
| return new Response(null, { | ||
| status: 426 | ||
| }); | ||
| } | ||
| let targetPath; | ||
| try { | ||
| targetPath = normalizeGatewayPath(new URL(request.url).pathname); | ||
| } catch { | ||
| return new Response(null, { | ||
| status: 400 | ||
| }); | ||
| } | ||
| const descriptors = descriptorsByPath.get(targetPath); | ||
| if (!descriptors) { | ||
| return new Response(null, { | ||
| status: 404 | ||
| }); | ||
| } | ||
| const rejection = await this.resolveUpgradeRejection(request, targetPath); | ||
| if (rejection) { | ||
| return new Response(rejection.body ?? null, { | ||
| headers: rejection.headers, | ||
| status: rejection.status | ||
| }); | ||
| } | ||
| let response; | ||
| let socket; | ||
| try { | ||
| ({ | ||
| response, | ||
| socket | ||
| } = host.upgrade(request)); | ||
| } catch (error) { | ||
| this.releaseUpgradeReservation(); | ||
| throw error; | ||
| } | ||
| void this.bindConnectionHandlers(socket, request, descriptors).catch(error => { | ||
| this.unregisterSocket(this.findSocketId(socket)); | ||
| this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| socket.close(1011, 'Internal server error'); | ||
| }); | ||
| return response; | ||
| } | ||
| fetch: (request, host) => this.trackPendingUpgradeOperation(this.handleUpgradeRequest(request, host, descriptorsByPath)) | ||
| }; | ||
| } | ||
| async handleUpgradeRequest(request, host, descriptorsByPath) { | ||
| if (!isWebSocketUpgradeRequest(request)) { | ||
| return new Response(null, { | ||
| status: 426 | ||
| }); | ||
| } | ||
| let targetPath; | ||
| try { | ||
| targetPath = normalizeGatewayPath(new URL(request.url).pathname); | ||
| } catch { | ||
| return new Response(null, { | ||
| status: 400 | ||
| }); | ||
| } | ||
| const descriptors = descriptorsByPath.get(targetPath); | ||
| if (!descriptors) { | ||
| return new Response(null, { | ||
| status: 404 | ||
| }); | ||
| } | ||
| const rejection = await this.resolveUpgradeRejection(request, targetPath); | ||
| if (rejection) { | ||
| return new Response(rejection.body ?? null, { | ||
| headers: rejection.headers, | ||
| status: rejection.status | ||
| }); | ||
| } | ||
| if (this.isShuttingDown) { | ||
| this.releaseUpgradeReservation(); | ||
| return new Response('WebSocket server is shutting down.', { | ||
| status: 503 | ||
| }); | ||
| } | ||
| let response; | ||
| let socket; | ||
| try { | ||
| ({ | ||
| response, | ||
| socket | ||
| } = host.upgrade(request)); | ||
| } catch (error) { | ||
| this.releaseUpgradeReservation(); | ||
| throw error; | ||
| } | ||
| void this.trackPendingUpgradeOperation(this.bindConnectionHandlers(socket, request, descriptors)).catch(error => { | ||
| this.unregisterSocket(this.findSocketId(socket)); | ||
| this.logger.error('WebSocket gateway open lifecycle failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| socket.close(1011, 'Internal server error'); | ||
| }); | ||
| return response; | ||
| } | ||
| groupDescriptorsByPath(descriptors) { | ||
@@ -161,8 +182,22 @@ const descriptorsByPath = new Map(); | ||
| this.socketRegistry.set(state.socketId, socket); | ||
| this.socketStates.set(state.socketId, state); | ||
| this.attachConnectionListeners(state, socket, request); | ||
| await this.resolveConnectionGateways(state); | ||
| await this.runConnectHandlers(state, socket); | ||
| await this.finalizeConnectionBinding(state, socket, request); | ||
| try { | ||
| await this.resolveConnectionGateways(state); | ||
| await this.runConnectHandlers(state, socket); | ||
| await this.finalizeConnectionBinding(state, socket, request); | ||
| if (this.isShuttingDown && socket.readyState === WEBSOCKET_OPEN_READY_STATE) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| await state.disconnectLifecyclePromise; | ||
| } | ||
| } finally { | ||
| if (!state.handlersReady && state.bufferedDisconnect) { | ||
| this.settleDisconnectLifecycle(state); | ||
| } | ||
| this.settleConnectLifecycle(state); | ||
| } | ||
| } | ||
| createConnectionHandlerState(request, descriptors) { | ||
| const connectLifecycle = createCompletionSignal(); | ||
| const disconnectLifecycle = createCompletionSignal(); | ||
| return { | ||
@@ -172,3 +207,7 @@ bufferedDisconnect: undefined, | ||
| bufferedMessagesStartIndex: 0, | ||
| connectLifecycleSettled: false, | ||
| connectLifecyclePromise: connectLifecycle.promise, | ||
| descriptors, | ||
| disconnectLifecycleSettled: false, | ||
| disconnectLifecyclePromise: disconnectLifecycle.promise, | ||
| enqueuedMessageCount: 0, | ||
@@ -181,2 +220,4 @@ handlerQueue: Promise.resolve(), | ||
| request, | ||
| resolveConnectLifecycle: connectLifecycle.resolve, | ||
| resolveDisconnectLifecycle: disconnectLifecycle.resolve, | ||
| resolved: [], | ||
@@ -186,2 +227,16 @@ socketId: crypto.randomUUID() | ||
| } | ||
| settleConnectLifecycle(state) { | ||
| if (state.connectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.connectLifecycleSettled = true; | ||
| state.resolveConnectLifecycle(); | ||
| } | ||
| settleDisconnectLifecycle(state) { | ||
| if (state.disconnectLifecycleSettled) { | ||
| return; | ||
| } | ||
| state.disconnectLifecycleSettled = true; | ||
| state.resolveDisconnectLifecycle(); | ||
| } | ||
| attachConnectionListeners(state, socket, request) { | ||
@@ -323,2 +378,4 @@ socket.addEventListener('message', event => { | ||
| this.logger.error('WebSocket gateway disconnect dispatch failed.', error, LIFECYCLE_LOG_CONTEXT); | ||
| }).finally(() => { | ||
| this.settleDisconnectLifecycle(state); | ||
| }); | ||
@@ -374,2 +431,8 @@ } | ||
| async resolveUpgradeRejection(request, path) { | ||
| if (this.isShuttingDown) { | ||
| return { | ||
| body: 'WebSocket server is shutting down.', | ||
| status: 503 | ||
| }; | ||
| } | ||
| if (!this.tryReserveUpgradeSlot()) { | ||
@@ -451,2 +514,9 @@ return { | ||
| } | ||
| resolveShutdownTimeoutMs() { | ||
| const configured = this.moduleOptions.shutdown?.timeoutMs; | ||
| if (typeof configured !== 'number' || !Number.isFinite(configured) || configured <= 0) { | ||
| return DEFAULT_WEBSOCKET_SHUTDOWN_TIMEOUT_MS; | ||
| } | ||
| return Math.floor(configured); | ||
| } | ||
| async shutdown() { | ||
@@ -461,10 +531,108 @@ if (this.shutdownPromise) { | ||
| async runShutdownLifecycle() { | ||
| this.isShuttingDown = true; | ||
| if (hasDenoWebSocketBindingHost(this.adapter)) { | ||
| this.adapter.configureWebSocketBinding(undefined); | ||
| } | ||
| const shutdownTimeoutMs = this.resolveShutdownTimeoutMs(); | ||
| await this.awaitPendingUpgradeOperations(shutdownTimeoutMs); | ||
| await this.closeActiveSockets(shutdownTimeoutMs); | ||
| this.pendingUpgradeReservations = 0; | ||
| this.socketRegistry.clear(); | ||
| this.socketRooms.clear(); | ||
| this.socketStates.clear(); | ||
| this.roomSockets.clear(); | ||
| } | ||
| trackPendingUpgradeOperation(operation) { | ||
| if (typeof operation === 'function') { | ||
| return (...args) => this.trackPendingUpgradeOperation(operation(...args)); | ||
| } | ||
| 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 Deno 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 Deno websocket upgrades within ${String(timeoutMs)}ms.`, error, LIFECYCLE_LOG_CONTEXT); | ||
| }); | ||
| } | ||
| async closeActiveSockets(timeoutMs) { | ||
| const activeSockets = [...this.socketRegistry.entries()]; | ||
| if (activeSockets.length === 0) { | ||
| return; | ||
| } | ||
| const activeStates = activeSockets.map(([socketId]) => this.socketStates.get(socketId)).filter(state => state !== undefined); | ||
| for (const [, socket] of activeSockets) { | ||
| if (socket.readyState === WEBSOCKET_OPEN_READY_STATE) { | ||
| socket.close(1001, 'Server shutting down'); | ||
| } | ||
| } | ||
| await this.awaitHandlerQueueDrain(activeStates, 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 Deno 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 Deno websocket connections within ${String(timeoutMs)}ms.`, error, LIFECYCLE_LOG_CONTEXT); | ||
| }); | ||
| } | ||
| joinRoom(socketId, room) { | ||
@@ -530,2 +698,3 @@ let rooms = this.socketRooms.get(socketId); | ||
| this.socketRegistry.delete(socketId); | ||
| this.socketStates.delete(socketId); | ||
| const rooms = this.socketRooms.get(socketId); | ||
@@ -532,0 +701,0 @@ if (rooms) { |
+7
-7
@@ -12,3 +12,3 @@ { | ||
| ], | ||
| "version": "1.0.0-beta.3", | ||
| "version": "1.0.0-beta.4", | ||
| "private": false, | ||
@@ -73,6 +73,6 @@ "license": "MIT", | ||
| "ws": "^8.18.3", | ||
| "@fluojs/di": "^1.0.0-beta.4", | ||
| "@fluojs/core": "^1.0.0-beta.2", | ||
| "@fluojs/http": "^1.0.0-beta.3", | ||
| "@fluojs/runtime": "^1.0.0-beta.4" | ||
| "@fluojs/di": "^1.0.0-beta.5", | ||
| "@fluojs/http": "^1.0.0-beta.4", | ||
| "@fluojs/runtime": "^1.0.0-beta.5" | ||
| }, | ||
@@ -82,5 +82,5 @@ "devDependencies": { | ||
| "vitest": "^3.2.4", | ||
| "@fluojs/platform-bun": "^1.0.0-beta.3", | ||
| "@fluojs/platform-express": "^1.0.0-beta.3", | ||
| "@fluojs/platform-fastify": "^1.0.0-beta.4" | ||
| "@fluojs/platform-bun": "^1.0.0-beta.4", | ||
| "@fluojs/platform-express": "^1.0.0-beta.4", | ||
| "@fluojs/platform-fastify": "^1.0.0-beta.5" | ||
| }, | ||
@@ -87,0 +87,0 @@ "scripts": { |
+3
-1
@@ -104,3 +104,3 @@ # @fluojs/websockets | ||
| 옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수와 인바운드 페이로드 크기에 대해 기본 제한값을 적용합니다. 또한 server-backed Node 리스너는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat 타이머를 활성화합니다. | ||
| 옵션을 생략하면 `@fluojs/websockets`는 동시 연결 수와 인바운드 페이로드 크기에 대해 기본 제한값을 적용합니다. 또한 server-backed Node 리스너는 `heartbeat.enabled`를 명시적으로 `false`로 두지 않는 한 heartbeat 타이머를 활성화합니다. 공식 fetch-style 런타임 모듈(`@fluojs/websockets/bun`, `@fluojs/websockets/deno`, `@fluojs/websockets/cloudflare-workers`)은 애플리케이션 shutdown 시 추적 중인 websocket 클라이언트를 닫고 `shutdown.timeoutMs` 범위 안에서 `@OnDisconnect()` cleanup이 마무리될 수 있도록 bounded 기회를 제공합니다. | ||
@@ -135,1 +135,3 @@ ## 공개 API 개요 | ||
| - `packages/websockets/src/bun/bun.test.ts` | ||
| - `packages/websockets/src/deno/deno.test.ts` | ||
| - `packages/websockets/src/cloudflare-workers/cloudflare-workers.test.ts` |
+3
-1
@@ -104,3 +104,3 @@ # @fluojs/websockets | ||
| When omitted, `@fluojs/websockets` now applies bounded defaults for concurrent connections and inbound payload size. Server-backed Node listeners also enable heartbeat timers unless you explicitly set `heartbeat.enabled` to `false`. | ||
| When omitted, `@fluojs/websockets` now applies bounded defaults for concurrent connections and inbound payload size. Server-backed Node listeners also 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`. | ||
@@ -135,1 +135,3 @@ ## Public API Overview | ||
| - `packages/websockets/src/bun/bun.test.ts` | ||
| - `packages/websockets/src/deno/deno.test.ts` | ||
| - `packages/websockets/src/cloudflare-workers/cloudflare-workers.test.ts` |
233401
9.38%4780
13.03%136
1.49%Updated
Updated