UNPKG

bun-ws-router

Version:

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

107 lines 4.77 kB
import type { ServerWebSocket } from "bun"; import type { ZodObject, ZodType } from "zod"; import { z, ZodLiteral } from "zod"; /** * Type-safe function for sending validated messages through WebSocket. * Extracts payload/meta types from ZodObject shape for compile-time validation. */ export type SendFunction = <Schema extends MessageSchemaType>(schema: Schema, data: Schema["shape"] extends { payload: infer P; } ? P extends ZodType ? z.infer<P> : unknown : unknown, meta?: z.infer<Schema["shape"]["meta"]>) => void; /** * Handler context with type-safe payload/meta access from schema definition. * Uses intersection types to add payload only when schema defines it, avoiding * optional payload field that would require runtime checks. * * @see specs/adrs.md#ADR-001 - keyof check for discriminated unions */ export type MessageContext<Schema extends MessageSchemaType, Data> = { /** WebSocket connection with custom data */ ws: ServerWebSocket<Data>; /** Message type literal from schema */ type: Schema["shape"]["type"]["value"]; /** Message metadata inferred from schema */ meta: z.infer<Schema["shape"]["meta"]>; /** Server receive timestamp (milliseconds since epoch) - authoritative for server logic */ receivedAt: number; /** Type-safe send function for validated messages */ send: SendFunction; } & ("payload" extends keyof Schema["shape"] ? Schema["shape"]["payload"] extends ZodType ? { payload: z.infer<Schema["shape"]["payload"]>; } : Record<string, never> : Record<string, never>); export type MessageHandler<Schema extends MessageSchemaType, Data> = (context: MessageContext<Schema, Data>) => void | Promise<void>; /** * Base constraint for all message schemas created by messageSchema(). * type is ZodLiteral for exact string matching during routing. * meta uses ZodType to allow MessageMetadataSchema extensions. */ export type MessageSchemaType = ZodObject<{ type: ZodLiteral<string>; meta: ZodType; payload?: ZodType; }>; export interface MessageHandlerEntry<Data = unknown> { schema: MessageSchemaType; handler: MessageHandler<MessageSchemaType, Data>; } /** * Type helpers for client-side type inference (ADR-002). * Used by typed client adapters to extract message types from schemas. */ /** * Infer full inbound message type (as received by handlers). * * Includes optional timestamp/correlationId (may be present from client), * plus schema-defined extended meta and payload (if defined). * * @example * ```typescript * const HelloOk = messageSchema("HELLO_OK", { text: z.string() }); * type Msg = InferMessage<typeof HelloOk>; * // { type: "HELLO_OK", meta: { timestamp?: number, correlationId?: string }, payload: { text: string } } * * client.on(HelloOk, (msg) => { * msg.type // "HELLO_OK" (literal type) * msg.meta.timestamp // number | undefined * msg.payload.text // string * }); * ``` */ export type InferMessage<S extends MessageSchemaType> = z.infer<S>; /** * Infer payload type from schema, or never if no payload defined. * * Returns `never` (not `undefined`) for no-payload schemas to enable * clean overload discrimination in send() and request() methods. * * @example * ```typescript * const WithPayload = messageSchema("MSG", { id: z.number() }); * const NoPayload = messageSchema("PING"); * * type P1 = InferPayload<typeof WithPayload>; // { id: number } * type P2 = InferPayload<typeof NoPayload>; // never * ``` */ export type InferPayload<S extends MessageSchemaType> = "payload" extends keyof S["shape"] ? S["shape"]["payload"] extends ZodType ? z.infer<S["shape"]["payload"]> : never : never; /** * Infer extended meta fields for outbound messages. * * Omits auto-injected fields (timestamp, correlationId) which are provided * via opts.meta or opts.correlationId. Only includes schema-defined extended meta. * * Used to enforce required extended meta fields at compile time for send/request. * * @example * ```typescript * const RoomMsg = messageSchema("CHAT", { text: z.string() }, { roomId: z.string() }); * type Meta = InferMeta<typeof RoomMsg>; // { roomId: string } * // timestamp and correlationId are omitted (auto-injected by client) * * client.send(RoomMsg, { text: "hi" }, { meta: { roomId: "general" } }); * ``` */ export type InferMeta<S extends MessageSchemaType> = "meta" extends keyof S["shape"] ? Omit<z.infer<S["shape"]["meta"]>, "timestamp" | "correlationId"> : Record<string, never>; /** Re-export shared types that are validator-agnostic. See: shared/types.ts */ export type { CloseHandler, CloseHandlerContext, OpenHandler, OpenHandlerContext, UpgradeOptions, WebSocketData, WebSocketRouterOptions, } from "../shared/types"; //# sourceMappingURL=types.d.ts.map