Introspection
posipaki/plugins/tree-introspectionregisters reflection methods for looking into a live actor tree: walk the process tree, read raw state, find a descendant, or shut an actor down — all from outside the message loop.
Install
import { defineActor } from "posipaki";
import { inspect } from "posipaki/plugins/tree-introspection";
const actor = defineActor({
plugins: [inspect()],
handlers: {},
});
const proc = await actor.spawn(null);
await proc.ready();
The methods appear on proc.$reflection. See Reflection for
how reflection methods work, and Plugins for how plugins add them.
The four methods
| method | returns | what it does |
|---|---|---|
inspect.getTree(prefix?) |
Promise<TreeNode> |
a recursive view of the process tree |
inspect.getState() |
Promise<unknown> |
the raw internal state |
inspect.find(pname) |
Promise<AnyProcess | null> |
a descendant process by its full tree name |
inspect.exit() |
Promise<void> |
exit this actor from inside |
All four are async like every reflection method — see Reflection.
getTree
getTree() returns a TreeNode:
interface TreeNode {
pname: string;
parentName: string | null;
children: TreeNode[];
status: "running" | "no introspection";
}
const worker = defineActor({ name: "worker", plugins: [inspect()], handlers: {} });
const main = defineActor({
name: "main",
plugins: [inspect()],
async setup() {
await this.fork(worker);
return {};
},
handlers: {},
});
const proc = await main.spawn(null);
await proc.ready();
const tree = await proc.$reflection["inspect.getTree"]();
// {
// pname: "main",
// parentName: null,
// status: "running",
// children: [
// { pname: "main:worker", parentName: "main", children: [], status: "running" },
// ],
// }
The tree is built recursively: a child that also has inspect() installed
contributes its own subtree. A child without the plugin still appears, but
as a flat leaf marked "no introspection" — its descendants are not walked.
A child that is itself a remoteClient proxy contributes its subtree too: the
proxy has inspect.getTree when the far side announced it, and the walker waits
for that answer like any other. The names inside it are the far side's own —
the handle's pname here and the process's pname there are two different
strings for one thing.
Pass a prefix to keep only the nodes whose tree-prefixed name starts with it:
await proc.$reflection["inspect.getTree"]("main:worker"); // root + the worker subtree
The root is always included; the prefix filters the children.
getState
getState() returns the actor's raw internal state. This differs from
proc.state: when a state object has both a public and a private field,
proc.state exposes only the public half, while getState() returns the
whole thing.
const actor = defineActor({
plugins: [inspect()],
setup: () => ({ public: null, private: { secret: 42 } }),
handlers: {},
});
const proc = await actor.spawn(null);
await proc.ready();
proc.state; // null — the public half only
await proc.$reflection["inspect.getState"](); // { public: null, private: { secret: 42 } }
find
find(pname) returns the live process handle for a descendant by its full
tree-prefixed name, or null:
const child = await proc.$reflection["inspect.find"]("main:worker");
child?.send({ type: "POKE" });
It walks the tree directly, so it reaches children that do not have
inspect() installed too.
exit
exit() ends the actor from inside, as if it called this.exit("inspector"):
await proc.$reflection["inspect.exit"]();
await proc.wait();