API Reference

Everything defineActor accepts, in one place. See Quick Start for the shortest working example.

defineActor and the actor runtime are portable — browser, service worker, Node, Bun. The only Node-only surface is subprocess remote actors, behind posipaki/remote/node — see Remote Actors.

defineActor

import { defineActor, defineMessages } from "posipaki";

const actor = defineActor(config);

defineActor(config) compiles a declarative config object into an ActorDefinition with a .spawn() method. The config describes the actor's initial state, message handlers, helper methods and lifecycle hooks.


Config fields

name

Type: string · optional

Preferred process name. Used by this.fork() when no explicit child name is given.

inMessages / outMessages

Type: defineMessages<T>() · optional

Type tags for the actor's message contracts. Hand them a discriminated union on type:

type MPoke = { type: "POKE" };
type MPing = { type: "PING"; step: number };
type MTick = { type: "TICK"; count: number };

const counter = defineActor({
  inMessages: defineMessages<MPoke | MPing>(),
  outMessages: defineMessages<MTick>(),
  // ...
});

inMessages drives the narrowing of msg in handlers and the accepted argument of proc.send(...). outMessages types this.emit(...) and the parent's view of this actor's messages. Without them, messages fall back to the loose Message type.

setup

Type: (args) => State | Promise<State> · optional

Returns the initial state. this.state is not available here — build the state from args (and this.name / this.id if needed). Runs once, after beforeStart and before afterStart. If omitted, the actor starts with null state.

setup(args) {
  return { count: 0, max: args.max };
},

handlers

Type: { [type]: (msg, sender) => void | Promise<void> } · required

The message dispatch table. The key is a message type; msg is narrowed to the matching union member and sender is a SenderInfo. Handlers run one at a time — an incoming message is buffered until the current handler finishes. Handlers can be async.

handlers: {
  POKE(msg, sender) {
    // msg: { type: "POKE" }
    this.state.count++;
  },
  async PING(msg) {
    // msg: { type: "PING"; step: number }
    this.state.count += msg.step;
  },
},

If a message has no handler, onUnhandled is called instead.

The special STOP type is handled automatically and is implied to be part of in-messages type. See onStopRequested() in lifecycle section to learn more about it.

methods

Type: { [name]: (...args) => unknown } · optional

Helper methods merged onto this, so handlers can share logic without repeating it:

methods: {
  increment() { this.state.count++; },
  beDone() {
    this.emit({ type: "TICK", count: this.state.count });
    this.exit("max reached");
  },
},
handlers: {
  POKE() { this.increment(); },
},

plugins

Type: ActorPlugin[] | (parents) => ActorPlugin[] · optional

Reusable units of actor behaviour that transform the config before assembly. See Plugins. A plugin is a function (config) => config; the array form replaces the chain, the function form receives the parent's plugins and returns the child's. When plugins is omitted, a child inherits the parent's chain. Plugins are deduplicated by function name.

plugins: [debugLogger()],                       // replace the chain
plugins: (parents) => [...parents, inspect()],  // extend the chain

Lifecycle hooks

Hooks are additive, when defined by plugins and actor itself, several can register for the same point and they fire in registration order. Any hook may return stopPropagation() to skip the rest of the chain. Hooks fire in this order:

beforeStart → setup → afterStart → [message loop] → beforeEnd → afterEnd

See Lifecycle for more in-depth explaination of process lifecycle.

beforeStart / afterStart

Type: () => void | Promise<void>

beforeStart fires once this is built but before setup (this.state is never). Use it to register the process before forking any child. afterStart fires after setup, with this.state available and after proc.read() resolved.

onStopRequested

Type: () => HookResult | Promise<HookResult>

Fired when a STOP arrives. The hook may call this.agreeToStop() to accept it; if it doesn't, the actor keeps running. When no onStopRequested is defined, a STOP stops the actor immediately. In either case no messages, including ones already in the mailbox will be processed.

beforeEnd / afterEnd

Type: (reason?) => HookResult | Promise<HookResult>

beforeEnd fires when the message loop finishes, with the exit reason. afterEnd fires after that, as the process tears down.

onError

Type: (error?) => HookResult | ErrorResult | Promise<HookResult | ErrorResult>

Fired when a handler or hook throws, with the error. Whether that error is fatal depends on the handler:

onError(err) {
  this.log.error(`boom: ${(err as Error).message}`);
  return propagateError(); // observed, not handled
}

With chainHook, the actor's own onError has the last word: its propagateError() is honoured even when a plugin's onError returned nothing.

onEmit / onMessage

Type: (msg, sender) => HookResult | Promise<HookResult>

onEmit observes every message the actor sends to its parent. onMessage observes every incoming message before dispatch; returning stopPropagation() skips the handler.

onUnhandled

Type: (msg, sender) => void | Promise<void>

Fired when a message arrives with no matching handler.

onChildExit

Type: (name, reason) => HookResult | Promise<HookResult>

Fired when a forked child exits. name is the child's tree-prefixed process name (parent:child); reason is its ExitMessage.

onOrphan

Type: (orphan) => OrphanDecision | void

Fired for each orphan a child leaves behind in its EXIT. Return a policy:

See Lifecycle page to learn more about orphan processes.


The handler context (this)

Inside handlers, methods and every hook, this is an ActorContext:


Spawning

const proc = await counter.spawn(args, {
  name?,          // the process name
  toParent?,      // adopt an existing process as the parent
  addPlugins?,    // plugins for this instance only
  parentName?,    // identity to report to the parent
  parentId?,
  connection?,    // the FrameChannel this process speaks over
  awaitReady?,    // false to return before the initial state exists
});
await proc.ready();
proc.send({ type: "POKE" });

spawn returns an AsyncProcess handle:

spawnAsChild(ctx, args, opts) spawns the actor as a child of an existing process instead of a standalone root.

The AsyncProcess handle here is the same object the raw spawnAsync returns — see Low-Level API for the process model underneath defineActor.


Remote actors

Remote actors extend the model across a boundary. The seam is two functions:

A spawner is a transport factory: () => Promise<Channel> on the server, (args) => Promise<Channel> on the client. It obtains the transport and does the version handshake, leaving the seam transport-agnostic.

For a server that accepts many connections, reactor(listener, actor, options) accepts from a listener and serves a session per connection. posipaki/remote also carries the frame vocabulary, FrameChannel (the connection a process holds as ctx.connection), and the stream store that moves typed arrays, blobs and AsyncIterable<Uint8Array> streams across a connection. See Remote actors.

To build your own transport, spawner or protocol, these primitives are exported from posipaki/remote:

posipaki/remote is portable; the FIFO subprocess pieces (defineSubprocessActor, commandSpawner, fifoArgvSpawner, FifoUtf8NlineTransport) live in posipaki/remote/node (Node/Bun only). See Remote Actors. For building your own transport, spawner or protocol, see Building your own transport.


Types