Skip to content

Handlers

Write the code that runs when someone calls one of your actions.

A handler is the code that runs when an action is called. You register it on the runtime and it answers incoming calls. Every style receives a second, per-invocation ctx argument.

server.ts
const userHandler = act_user.wrapAsLocalHandler({
getUser: async ({ userId }, ctx) => {
const user = await db.users.find(userId);
if (!user) throw err_user.fromId("not_found", { userId });
return user;
},
updateName: async ({ userId, name }) => {
await db.users.update(userId, { name });
return { success: true };
},
});

This form is exhaustive: adding an action to the domain makes TypeScript ask for its implementation. Each callback receives the typed input directly.

ctx.runtime is the exact runtime executing the callback, and ctx.run(request, options) dispatches a nested action through that runtime. ctx.origin is the request’s claimed origin; ctx.authenticatedOrigin is present only when the transport authenticated it. ctx.securityLevel and ctx.reliability expose the receiver-owned security and delivery facts. A nested action gets its own context; connection or authorization facts are never copied into it.

When you use serveChannel({ channelCases }), the same context also has connection, typed state, setState, clearState, pushBack, and broadcast. Socket-only writes throw connection_required on an HTTP request, while connection/state reads are nullable.

Use forDomainActionCases when one handler deliberately covers only part of a domain and you want the full primed action:

server.ts
import { createLocalHandler } from "@nice-code/action";
const userHandler = createLocalHandler().forDomainActionCases(act_user, {
getUser: async (action, ctx) => {
return db.users.find(action.input.userId);
},
});
const userHandler = createLocalHandler()
.forAction(act_user.action.getUser, async ({ input }, ctx) => db.users.find(input.userId));

wrapAsPartialLocalHandler works like wrapAsLocalHandler, but you only implement some of a domain’s actions. This is handy for a client that answers a few calls itself and lets the rest travel on to the server.

const partial = act_user.wrapAsPartialLocalHandler({
getUser: async ({ userId }) => cache.get(userId), // the rest are forwarded over the connection
});

Once you’ve written a handler, pass it to serveChannel on the listening side. To answer toConnector pushes on the dialing side, pass it to connectChannel’s onPush.

One runtime may have an action-specific executor and a domain fallback; the specific route wins. Two different local executors at the same route priority are rejected during setup with duplicate_action_executor, so handler composition cannot silently shadow an implementation.