@frsty/wsrpc
v0.0.1
Published
Type-safe RPC and event streaming over WebSockets
Maintainers
Readme
@frsty/wsrpc
Type-safe RPC and event streaming over WebSockets. Define procedures on the server, get fully-typed send and on on the client — no codegen, no schema duplication.
Install
npm install @frsty/wsrpcSupports any schema library that implements Standard Schema (zod, valibot, arktype, etc.).
Quick start
Server
import { z } from 'zod'
import { createProcedure, WebsocketHandler } from '@frsty/wsrpc/server'
type Ctx = { userId: string; room: string }
const base = createProcedure<Ctx>()
const router = {
sendMessage: base
.input(z.object({ text: z.string() }))
.handler(function* (c) {
yield c.replyAll('message', { text: c.input.text, from: c.userId })
yield c.reply('ack', { delivered: true })
return { ok: true }
}),
}
export const handler = new WebsocketHandler(router, {
*onOpen({ ctx }) {
yield ctx.replyAll('presence', { userId: ctx.userId, status: 'online' })
},
*onClose({ ctx }) {
yield ctx.replyAll('presence', { userId: ctx.userId, status: 'offline' })
},
})
export type AppHandler = typeof handlerBun adapter
import { handler } from './router'
type WsData = { userId: string; room: string; conn: unknown }
const server = Bun.serve<WsData>({
port: 3000,
fetch(req, srv) {
const url = new URL(req.url)
srv.upgrade(req, {
data: {
userId: url.searchParams.get('userId') ?? 'anon',
room: url.searchParams.get('room') ?? 'lobby',
conn: null,
},
})
},
websocket: {
open(ws) {
ws.subscribe(`room:${ws.data.room}`)
ws.data.conn = handler.connection({
ctx: { userId: ws.data.userId, room: ws.data.room },
send: (data) => ws.send(data),
broadcast: (data) => server.publish(`room:${ws.data.room}`, data),
})
ws.data.conn.handleOpen()
},
message(ws, raw) { ws.data.conn.handleMessage(raw) },
close(ws) {
ws.data.conn.handleClose()
ws.unsubscribe(`room:${ws.data.room}`)
},
},
})Client
import { createClient } from '@frsty/wsrpc/client'
import type { AppHandler } from './router'
const client = createClient<AppHandler>('ws://localhost:3000?userId=alice&room=lobby', {
reconnect: true,
})
// Typed event listeners
client.on('presence', (data) => console.log(data.userId, data.status))
client.on('message', (data) => console.log(data.from, data.text))
// Typed RPC calls
const result = await client.send('sendMessage', { text: 'hello' })
// result: { ok: true }Concepts
Procedures
Handlers are generator functions. yield emits events to connected clients, return sends the RPC response back to the caller.
.handler(function* (c) {
yield c.reply('progress', { pct: 50 }) // → only the caller
yield c.replyAll('announcement', { msg: '…' }) // → everyone (via broadcast)
return { done: true } // → RPC response to caller
})Input validation
Pass any Standard Schema-compatible validator to .input():
import { z } from 'zod'
import * as v from 'valibot'
import { type } from 'arktype'
base.input(z.object({ text: z.string() }))
base.input(v.object({ text: v.string() }))
base.input(type({ text: 'string' }))Lifecycle hooks
onOpen, onClose, and onError are generator functions on WebsocketHandler. They receive the typed ctx and can yield events the same way procedures do.
Context
ctx is the typed object your adapter passes into handler.connection(). It's merged with reply and replyAll before being handed to each handler.
type Ctx = { userId: string; role: 'admin' | 'user' }
const base = createProcedure<Ctx>()
base.handler(function* (c) {
if (c.role === 'admin') yield c.replyAll('notice', { msg: 'admin here' })
return { ok: true }
})API
@frsty/wsrpc/server
| Export | Description |
|---|---|
| createProcedure<Ctx>() | Creates a procedure builder bound to a context type |
| WebsocketHandler | Wraps a router and lifecycle hooks, exposes .connection() |
| ConnectionOpts | Type for the argument to .connection() — wire your framework here |
| Router, Lifecycle, HandlerCtx | Supporting types |
@frsty/wsrpc/client
| Export | Description |
|---|---|
| createClient<Handler>(url, opts) | Creates a typed client |
| ClientOptions | Reconnect, lifecycle callbacks |
@frsty/wsrpc/server/internal
Low-level types for building framework adapters: ConnectionOpts, Router, LifecycleFn, AllGenerator, safeJsonParse.
@frsty/wsrpc/client/internal
Low-level types for building custom transports: InferHandler, InferRouterTypes, EventMap, AnyWebsocketHandler, safeJsonParse.
License
MIT
