API Reference

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

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. Plugins are inherited by all children by default. When child actor defines their plugins as an array, they will be added to the list of plugins passed by parent and deduplicated on name.

When defined as function, the deicision to inherit or discard parent's plugins is deffered to the function.


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 | Promise<HookResult>

Fired when a handler or hook throws, with the error.

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?, toParent?, addPlugins? });
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.


Types