Lifecycle

How an actor starts, runs, and — most importantly — stops. This page covers the hook order, the cooperative stop protocol, force-stop, and what happens to children and grandchildren when an actor goes down.

The hook order

Every defineActor runs its hooks in a fixed order:

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

Stopping, nicely

Stopping is cooperative. A STOP message arrives and the actor decides whether to accept it:

onStopRequested() {
  // finish up, flush, clean up…
  this.agreeToStop();
},

this.exit(reason?) skips the dance and stops right away. In every case the beforeEnd / afterEnd hooks run (with the exit reason), and the actor emits EXIT to its parent.

STOP is a message

STOP is not special-cased into the runtime — it is just another message in the inbox, processed in the same serial queue as everything else, one message at a time. If the actor is blocked inside a handler (say, awaiting a slow promise), the STOP waits in the buffer until that handler settles. A stuck actor never reaches its STOP, which is exactly why force-stop exists.

Force stop

Force-stop is the escalation for an actor that won't cooperate:

How stop propagates to children

When an actor exits, it does not just vanish — it stops its children first:

  1. Every child gets a nice STOP, with a 1s timeout.
  2. Children that stop are done. Children that refuse are collected.
  3. The exiting actor emits EXIT to its parent, carrying the refusers as orphans.

So a tree tears down from the top: the parent stops its children, who stop their own children, and so on.

Orphans

An orphan is a process whose parent exited while it was still running (it refused to stop). It is handed up in the parent's EXIT, and the grandparent decides its fate with onOrphan:

onOrphan(orphan) {
  return "adopt"; // see the table below
},
policy effect
"adopt" promote the orphan to a child; its buffered messages are drained
"force-stop" hard-kill it (the default when onOrphan is not defined)
"unparent" drop its buffer, keep it running in orphans
"leave" keep buffering, propagate it up on your own exit

A tree that outlives its parent

A worker forks a helper that refuses to stop. When the worker dies, the helper becomes an orphan and the supervisor adopts it:

import { defineActor, defineMessages } from "posipaki";

// Refuses to stop — it will outlive its parent and become an orphan.
const helper = defineActor({
  name: "helper",
  inMessages: defineMessages<never>(),
  outMessages: defineMessages<never>(),
  setup: () => ({}),
  handlers: {},
  onStopRequested() {
    // no agreeToStop() here — keep running
  },
});

const worker = defineActor({
  name: "worker",
  inMessages: defineMessages<{ type: "DIE" }>(),
  outMessages: defineMessages<never>(),
  async setup() {
    await this.fork(helper, null);
    return {};
  },
  handlers: {
    DIE() {
      this.exit("done");
    },
  },
});

const supervisor = defineActor({
  name: "main",
  inMessages: defineMessages<{ type: "DIE" }>(),
  async setup() {
    const workerProc = await this.fork(worker, null);
    return { worker: workerProc };
  },
  handlers: {
    DIE(msg) {
      this.state.worker.send(msg);
    },
  },
  onOrphan(orphan) {
    // The helper survived worker's exit. Take it over.
    return "adopt";
  },
});

const proc = await supervisor.spawn(null);
await proc.ready();
proc.send({ type: "DIE" });
await proc.wait();

An actor that never emits can declare outMessages: defineMessages<never>() — the EXIT/STOP frames are framework messages and don't count as out-messages.