Parent-Child Communication

Actors form a tree. A parent spawns children, children report results back up, and both sides talk to each other with plain messages — no shared state, no callbacks, no event emitters.

main
└── main:worker

Every actor you fork gets a tree-prefixed name (parentName:childName) and is tracked on this.$child, keyed by that name.

A worker and its supervisor

Here is the whole picture — a worker that does jobs and reports back, and a supervisor that forks it, hands it jobs, and records the results:

import { defineActor, defineMessages } from "posipaki";

const worker = defineActor({
  name: "worker",
  inMessages: defineMessages<{ type: "JOB"; id: number }>(),
  outMessages: defineMessages<{ type: "RESULT"; id: number; ok: boolean }>(),
  setup: () => ({}),
  handlers: {
    JOB(msg) {
      // ... do the work ...
      this.emit({ type: "RESULT", id: msg.id, ok: true });
    },
  },
});

const supervisor = defineActor({
  name: "main",
  inMessages: defineMessages<
    | { type: "JOB"; id: number }
    | { type: "RESULT"; id: number; ok: boolean }
  >(),
  async setup() {
    // Fork in setup and keep the typed handle in state.
    const workerProc = await this.fork(worker, null);
    return { done: 0, lastFrom: "", worker: workerProc };
  },
  handlers: {
    JOB(msg) {
      this.state.worker.send(msg);
    },
    RESULT(msg, sender) {
      this.state.lastFrom = sender.fromName; // "main:worker"
      this.state.done++;
    },
  },
  onChildExit(name) {
    if (name === "main:worker") this.agreeToStop();
  },
});

const proc = await supervisor.spawn(null);
await proc.ready();
proc.send({ type: "JOB", id: 1 });
await proc.wait();

Talking up — emit

A child sends to its parent with this.emit(...). The message lands in the parent's handlers exactly like any other incoming message:

JOB(msg) {
  this.emit({ type: "RESULT", id: msg.id, ok: true });
},

The types line up: a child's outMessages must fit inside the parent's inMessages, because every message the child emits becomes an in-message of the parent. That is why the supervisor declares RESULT in its inMessages and handles it with a RESULT handler.

Who sent it — SenderInfo

Every handler's second argument is the sender:

RESULT(msg, sender) {
  this.state.lastFrom = sender.fromName; // "main:worker"
},

sender is { fromName: string; fromId: symbol }. For a child's emit, fromName is the child's tree name — so a parent with several children can tell them apart by sender.fromName without any extra bookkeeping.

Talking down — the typed handle

fork returns a typed process handle. Keep it in state (returned from setup) and send to the child through it:

async setup() {
  const workerProc = await this.fork(worker, null);
  return { done: 0, lastFrom: "", worker: workerProc };
},
handlers: {
  JOB(msg) {
    this.state.worker.send(msg); // fully typed
  },
},

The framework also mirrors every child into this.$child under its tree name, which is handy for introspection, but it is untyped (Record<string, AnyProcess>) — prefer the fork return value when you want type-checked sends.

When a child leaves — onChildExit

When a child exits, the parent is told:

onChildExit(name) {
  if (name === "main:worker") this.agreeToStop();
},

name is the child's tree-prefixed name. What happens to the child's own children (its grandchildren), force-stop vs graceful stop, and how orphans are handled are all covered on the lifecycle page.