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.
createHandlers
Section titled “createHandlers”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.
Connection Interface
Section titled “Connection Interface”Your adapter must provide objects that implement Connection:
interface Connection { /** Send a serialized message down the transport. */ send(message: string): void; /** Stable identifier for lifecycle hooks. Assigned by the adapter. */ id: string; /** Authenticated user identity, when the transport provides it. */ userId?: string | null; /** Verified auth claims or other connection-scoped metadata. */ claims?: Record<string, unknown>; /** Optional routing scope attached by the transport layer. */ scope?: Record<string, string>;}The id must be unique per connection and stable for its lifetime. The optional userId, claims, and scope fields flow through from the transport (e.g. the gateway’s JWT verification) into method ctx and actor lifecycle hooks, so actors can use them for authorization.
Wiring to a Custom Runtime
Section titled “Wiring to a Custom Runtime”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); });});Message Routing
Section titled “Message Routing”The handler routes messages based on their type field:
| Message Type | Action |
|---|---|
rpc | Invoke method, send rpc:result back |
event:sub | Subscribe connection to actor events |
event:unsub | Unsubscribe from events |
state:sub | Subscribe to state + send initial snapshot |
state:unsub | Unsubscribe from state patches |
ActorManager
Section titled “ActorManager”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";Inspecting State
Section titled “Inspecting State”const { manager } = handlers;
// Number of hot actor instancesconsole.log(manager.size);
// List all hot actorsconst actors = manager.list();// [{ actorName: "counter", actorId: "main" }, ...]Destroying Actors
Section titled “Destroying Actors”// Destroy a specific actor (calls onDeactivate if defined)await manager.destroy("counter", "main");
// Destroy all actors (for shutdown or redeploy)await manager.destroyAll();Lifecycle Events
Section titled “Lifecycle Events”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.
State Initialization
Section titled “State Initialization”When an actor instance is first created, the manager initializes state by:
- Validating
{}against the state schema (works with schemas that have defaults) - If that fails, validating
undefined(works with top-level.default()) - If both fail, using
{}as a fallback
After initialization, the actor’s onActivate hook is called if defined.