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.