Low-Level API
defineActoris a convenience over a small core: a process is an async generator that yields state and receives stamped messages. This page covers that core —spawnAsync,AsyncProcess,ProcessCtx, and the message loop.
A process is a generator
The lowest-level building block is a function with this shape:
type AsyncProcessFn<Args, State, InMsg, OutMsg> = (
ctx: ProcessCtx<Args, State, InMsg, OutMsg>,
args: Args,
) => AsyncGenerator<State | null, void, WithSender<InMsg | StopMessage>>;
Three ideas:
yieldsets the state once — the framework reads the first yielded value asproc.stateand never reassigns it; later yields are ignored.yieldalso receives — the value of ayieldexpression is the next message as aWithSendertuple:[msg, sender].returnto exit — when the generator returns, the process emitsEXITto its parent.
Your first process
import { spawnAsync, runDispatchAsync } from "posipaki";
async function* greeter(ctx) {
const state = { seen: 0 };
yield state; // initial state
yield* runDispatchAsync(
ctx.pname,
async ([msg, sender]) => {
state.seen++;
ctx.toParent({ type: "GREETED", who: sender.fromName });
},
() => state.seen >= 1, // exit after one message
);
}
const proc = spawnAsync(
greeter,
"greeter",
(msg) => console.log(msg.type), // observe out-messages
)(null);
await proc.ready();
proc.send({ type: "HELLO" });
await proc.wait(); // resolves on generator return
runDispatchAsync is the standard message loop: it yields null (no state
change) and feeds each incoming [msg, sender] to the reducer until the
readyFn returns true. The next section unpacks it.
Inside runDispatchAsync
runDispatchAsync reduces to this (minus the debug logging):
async function* runDispatchAsync(fn, readyFn = () => false) {
while (!readyFn()) {
const msg = yield null; // suspend; receive the next [msg, sender]
await fn(msg);
}
}
So a process can drop the helper and drive the generator by hand. The greeter
again, driven by yield alone:
async function* greeter(ctx) {
const state = { seen: 0 };
yield state; // initial state → proc.state
while (state.seen < 1) {
const [msg, sender] = yield null; // no new snapshot; receive a message
state.seen++;
ctx.toParent({ type: "GREETED", who: sender.fromName });
}
}
A bare yield does two things at once:
- Sets the state once — the framework reads the first yielded value as
proc.stateand never reassigns it. To expose changes, mutate the state object in place;proc.stateholds the same reference, so the mutation is visible.yield nullis the idiom for "no new snapshot" — after the first yield the value is ignored either way. - Receives a message — the
yieldexpression evaluates to the next incoming message as[msg, sender].
That is the entire message loop. runDispatchAsync layers the reducer callback
and the exit predicate on top of it.
ProcessCtx
Every process receives a ctx:
pname/id— process name (string) and id (symbol).parentName/parentId— the parent's identity,nullfor a root.connection— theFrameChannelthis process was spawned over, ornull. See Remote actors.sendSelf(msg)— enqueue a message to yourself (e.g. from a timer).toParent(msg)— emit an out-message to the parent.fork(fn, name)— a curried child spawner:fork(fn, "w")(args).forkSync(fn, name)— fork a sync generator (wrapped viaasyncify).children/orphans— live lists of child processes.adopt(child)— claim another process as your child (also used for orphan adoption).monitor(child)— tap another process's messages without ownership.afterExit— a best-effort teardown hook that runs afterEXITis sent.
The process handle
spawnAsync returns an AsyncProcess:
ready()— resolves once the initial state is published.send(msg)— enqueue a message.state— the live state.subscribe("state" | "message", cb)— observe state changes or out-messages; returns an unsubscribe function.pause()/resume()— freeze / resume message dispatch.wait()— resolves when the generator returns.stop({ force? })— sendSTOPthe nice way; escalate to force if it refuses.forceStop()— hard-kill: abandon the generator, nofinally, noEXIT.
Messages are processed one at a time
send enqueues and schedules a tick. If a reducer is mid-await, later
messages buffer until it settles — the same serial queue defineActor gives
you.
STOP and EXIT
At this level nothing is special-cased. STOP is just a message — your
generator decides what to do with it (typically return). EXIT is what a
process emits when it returns, carrying any still-running children as
orphans. See Lifecycle for the full stop story.
Sync processes are deprecated
spawn, Process and runDispatch still exist for synchronous generators but
are deprecated — use spawnAsync, AsyncProcess and runDispatchAsync.
asyncify wraps a sync generator into an async one when needed.
defineActor builds on this
defineActor compiles its config into an AsyncProcessFn and drives it with
spawnAsync, layering hooks, plugins and typed messages on top. See
API Reference.