Skip to content

Custom Handlers

If you’re not using Bun, or need full control over the WebSocket lifecycle, you can use createHandlers() to get runtime-agnostic callbacks.

import { createHandlers } from "@zocket/server";
import { app } from "./app";
const handlers = createHandlers(app);

This returns a HandlerCallbacks object:

interface HandlerCallbacks {
onConnection(conn: Connection): void;
onMessage(conn: Connection, raw: string): void;
onClose(conn: Connection): void;
}

Your adapter must provide objects that implement Connection:

interface Connection {
send(message: string): void;
id: string; // Stable identifier for lifecycle hooks
}

The id must be unique per connection and stable for its lifetime.

Here’s a sketch for wiring to a generic WebSocket server:

import { createHandlers } from "@zocket/server";
import { app } from "./app";
const handlers = createHandlers(app);
let connId = 0;
myWebSocketServer.on("connection", (ws) => {
const conn = {
id: `custom_${++connId}`,
send: (msg: string) => ws.send(msg),
};
handlers.onConnection(conn);
ws.on("message", (data: string) => {
handlers.onMessage(conn, data);
});
ws.on("close", () => {
handlers.onClose(conn);
});
});

The handler routes messages based on their type field:

Message TypeAction
rpcInvoke method, send rpc:result back
event:subSubscribe connection to actor events
event:unsubUnsubscribe from events
state:subSubscribe to state + send initial snapshot
state:unsubUnsubscribe from state patches

Behind createHandlers, an ActorManager owns all actor instances:

  • getOrCreate(actorName, actorId) — lazily creates actor instances with schema-initialized state
  • removeConnection(conn) — broadcasts disconnection to all actor instances the connection interacted with

Actor instances are stored in a Map<"actorName:actorId", ActorInstance>.

When an actor instance is first created, the manager initializes state by:

  1. Validating {} against the state schema (works with schemas that have defaults)
  2. If that fails, validating undefined (works with top-level .default())
  3. If both fail, using {} as a fallback