bun-ws-router
Version:
A simple and efficient WebSocket router for Bun with Zod/Valibot message validation.
141 lines • 5.57 kB
JavaScript
/* SPDX-FileCopyrightText: 2025-present Kriasoft */
/* SPDX-License-Identifier: MIT */
import { v7 as randomUUIDv7 } from "uuid";
import { ConnectionHandler } from "./connection";
import { MessageRouter } from "./message";
/**
* WebSocket router for Bun that provides type-safe message routing with validation.
* Routes incoming messages to handlers based on message type.
*
* @template T - Application-specific data to store with each WebSocket connection.
* Always includes a clientId property generated automatically.
*/
export class WebSocketRouter {
connectionHandler = new ConnectionHandler();
messageRouter;
validator;
constructor(validator) {
this.validator = validator;
this.messageRouter = new MessageRouter(validator);
}
/**
* Merges open, close, and message handlers from another WebSocketRouter instance.
*/
addRoutes(router) {
// Merge open handlers
const otherConnectionHandler = router.connectionHandler;
otherConnectionHandler.openHandlers.forEach((handler) => {
this.connectionHandler.addOpenHandler(handler);
});
// Merge close handlers
otherConnectionHandler.closeHandlers.forEach((handler) => {
this.connectionHandler.addCloseHandler(handler);
});
// Merge message handlers
const thisMessageRouter = this
.messageRouter;
const otherMessageRouter = router.messageRouter;
otherMessageRouter.messageHandlers.forEach((value, key) => {
thisMessageRouter.messageHandlers.set(key, value);
});
return this;
}
/**
* Upgrades an HTTP request to a WebSocket connection.
*/
upgrade(req, options) {
const { server, data, headers } = options;
const clientId = randomUUIDv7();
const upgraded = server.upgrade(req, {
data: { clientId, ...data },
headers: {
"x-client-id": clientId,
...headers,
},
});
if (!upgraded) {
return new Response("Failed to upgrade the request to a WebSocket connection", {
status: 500,
headers: {
"Content-Type": "text/plain",
},
});
}
return new Response(null, { status: 101 });
}
onOpen(handler) {
this.connectionHandler.addOpenHandler(handler);
return this;
}
onClose(handler) {
this.connectionHandler.addCloseHandler(handler);
return this;
}
onMessage(schema, handler) {
this.messageRouter.addMessageHandler(schema, handler);
return this;
}
/**
* Returns a WebSocket handler that can be used with `Bun.serve`.
*/
get websocket() {
return {
open: this.handleOpen.bind(this),
message: this.handleMessage.bind(this),
close: this.handleClose.bind(this),
};
}
// ———————————————————————————————————————————————————————————————————————————
// Private methods
// ———————————————————————————————————————————————————————————————————————————
handleOpen(ws) {
const send = this.createSendFunction(ws);
this.connectionHandler.handleOpen(ws, send);
}
handleClose(ws, code, reason) {
const send = this.createSendFunction(ws);
this.connectionHandler.handleClose(ws, code, reason, send);
}
handleMessage(ws, message) {
const send = this.createSendFunction(ws);
this.messageRouter.handleMessage(ws, message, send);
}
/**
* Creates a send function for a specific WebSocket connection.
* This function allows handlers to send typed messages with proper validation.
*/
createSendFunction(ws) {
return (schema,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
meta = {}) => {
try {
// Extract the message type from the schema
const messageType = this.validator.getMessageType(schema);
// Create the message object with the required structure
const message = {
type: messageType,
meta: {
clientId: ws.data.clientId,
timestamp: Date.now(),
...meta,
},
...(payload !== undefined && { payload }),
};
// Validate the constructed message against the schema
const validationResult = this.validator.safeParse(schema, message);
if (!validationResult.success) {
console.error(`[ws] Failed to send message of type "${messageType}": Validation error`, validationResult.error);
return;
}
// Send the validated message
ws.send(JSON.stringify(validationResult.data));
}
catch (error) {
console.error(`[ws] Error sending message:`, error);
}
};
}
}
//# sourceMappingURL=router.js.map