Introspection

posipaki/plugins/tree-introspection registers 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();