UNPKG

bun-ws-router

Version:

A simple and efficient WebSocket router for Bun with Zod/Valibot message validation.

141 lines 5.57 kB
/* 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