UNPKG

bun-ws-router

Version:

Lightweight client/server WebSocket router for Bun with type-safe Zod/Valibot validation.

128 lines (109 loc) 3.83 kB
// SPDX-FileCopyrightText: 2025-present Kriasoft // SPDX-License-Identifier: MIT /** * Multi-handler dispatch for client-side message routing. * See @specs/client.md#multiple-handlers and @specs/rules.md#server-client-asymmetry. */ import type { AnyMessageSchema, MessageHandler } from "./types.js"; export class HandlerRegistry { // Multi-handler storage: type → array of handlers private handlers = new Map<string, MessageHandler[]>(); // Schema storage for validation: type → schema private schemas = new Map<string, AnyMessageSchema>(); /** * Registers a handler for a message type. * Returns unsubscribe function that removes only this handler. */ register<S extends AnyMessageSchema>( schema: S, handler: MessageHandler, extractType: (schema: S) => string, ): () => void { const type = extractType(schema); // Store schema for validation (first registration wins) if (!this.schemas.has(type)) { this.schemas.set(type, schema); } // Add handler to array const handlers = this.handlers.get(type) ?? []; handlers.push(handler); this.handlers.set(type, handlers); // Return unsubscribe function return () => { const current = this.handlers.get(type); if (current) { const index = current.indexOf(handler); if (index !== -1) { current.splice(index, 1); } } }; } /** * Dispatches message to registered handlers. * Executes in registration order with error isolation. * Returns true if handlers were found, false otherwise. */ dispatch(msg: { type: string; [key: string]: unknown }): boolean { const handlers = this.handlers.get(msg.type); if (!handlers || handlers.length === 0) { return false; // No handlers found } // Stable iteration: snapshot handlers array before dispatch // (allows unsubscribe during dispatch without affecting current iteration) const snapshot = [...handlers]; for (const handler of snapshot) { try { handler(msg); } catch (error) { console.error(`[Client] Handler error for ${msg.type}:`, error); // Continue dispatching remaining handlers (error isolation) } } return true; // Handlers executed } /** * Gets schema for validation by message type. */ getSchema(type: string): AnyMessageSchema | undefined { return this.schemas.get(type); } /** * Validates message against registered schema. * Returns { success: true, data } if validation succeeds. * Returns { success: false, reason: "no-schema" } if no schema registered. * Returns { success: false, reason: "validation-failed" } if validation fails. */ validate( msg: unknown, safeParse: ( schema: AnyMessageSchema, data: unknown, ) => { success: boolean; data?: unknown; error?: unknown }, ): | { success: true; data: unknown } | { success: false; reason: "no-schema" } | { success: false; reason: "validation-failed"; error: unknown } { if (!msg || typeof msg !== "object" || !("type" in msg)) { return { success: false, reason: "validation-failed", error: null }; } const type = (msg as { type: unknown }).type; if (typeof type !== "string") { return { success: false, reason: "validation-failed", error: null }; } const schema = this.getSchema(type); if (!schema) { return { success: false, reason: "no-schema" }; // No schema registered } const result = safeParse(schema, msg); if (!result.success) { console.warn(`[Client] Validation failed for ${type}:`, result.error); return { success: false, reason: "validation-failed", error: result.error, }; } return { success: true, data: result.data }; } }