🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@fluojs/websockets

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/websockets - npm Package Compare versions

Comparing version
1.0.0-beta.3
to
1.0.0-beta.4
+11
-0
dist/bun/bun-service.d.ts

@@ -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

@@ -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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"}

@@ -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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"}

@@ -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) {

@@ -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": {

@@ -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`

@@ -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`