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): Promise<void>;
onClose(conn: Connection): void;
manager: ActorManager;
}

Note that onMessage is async — if you need to know when processing is done (e.g., to ack a message queue delivery), await it.

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", async (data: string) => {
await 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

The manager property on HandlerCallbacks gives you direct access to the actor manager. You can also import and construct one yourself:

import { ActorManager } from "@zocket/server";
const { manager } = handlers;
// Number of hot actor instances
console.log(manager.size);
// List all hot actors
const actors = manager.list();
// [{ actorName: "counter", actorId: "main" }, ...]
// Destroy a specific actor (calls onDeactivate if defined)
await manager.destroy("counter", "main");
// Destroy all actors (for shutdown or redeploy)
await manager.destroyAll();

Subscribe to actor creation and destruction:

manager.on("actorCreated", ({ actorName, actorId }) => {
console.log(`Actor created: ${actorName}/${actorId}`);
});
manager.on("actorDestroyed", ({ actorName, actorId }) => {
console.log(`Actor destroyed: ${actorName}/${actorId}`);
});

The on() method returns an unsubscribe function.

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

After initialization, the actor’s onActivate hook is called if defined.