Plugins
A plugin is a function that rewrites an actor's config before assembly. Plugins add lifecycle hooks, decorate the handler context, and register reflection methods — all by returning a new config.
A plugin is a function
type ActorPlugin<C = AnyConfig> = (config: C) => C | Promise<C>;
defineActor applies each plugin once, in order, at spawn time (not at
definition time). Each plugin receives the config assembled so far and returns
the next one. Because assembly happens per-spawn, a plugin factory can close
over per-process state.
The workhorse is mergeConfigs:
import { defineActor, mergeConfigs, type ActorPlugin } from "posipaki";
const announce: ActorPlugin = (config) =>
mergeConfigs(config, {
afterStart() {
console.log(`${this.name} started`);
},
beforeEnd(reason) {
console.log(`${this.name} ending:`, reason);
},
});
const actor = defineActor({
plugins: [announce],
handlers: {},
});
mergeConfigs chains hooks
mergeConfigs(base, overlay) returns a new config where every key matching
on*, before* or after* (onMessage, beforeEnd, afterStart, …) is
chained: the overlay hook runs first, then the base hook. Non-hook keys
(setup, handlers, methods, $decorate, …) are plain properties — the
overlay's value wins.
Because plugins apply in order and each chains onto the previous result, the last plugin in the array runs first — the familiar middleware order:
plugins: [a, b, c],
For an onMessage hook, the call order is c → b → a → actor's own hook.
stopPropagation
Any chained hook can short-circuit the rest of the chain by returning
stopPropagation():
import { mergeConfigs, stopPropagation, type ActorPlugin } from "posipaki";
const authGuard: ActorPlugin = (config) =>
mergeConfigs(config, {
onMessage(msg) {
if (msg.type === "SECRET") return stopPropagation();
},
});
A stopPropagation() from onMessage skips the remaining onMessage hooks
and the handler itself.
Decorating this
A plugin can put values on the handler context via the $decorate field. They
become properties on this inside handlers, methods and hooks:
const clock: ActorPlugin = (config) =>
mergeConfigs(config, {
$decorate: {
...config.$decorate,
now: () => Date.now(),
},
});
const actor = defineActor({
plugins: [clock],
handlers: {
PING() {
console.log(this.now());
},
},
});
Type the new property via declaration merging on ActorDecorated — see the
Type augmentation section below.
Reflection methods
Plugins can register callable methods on the process handle via
$reflectionMethods. They land on proc.$reflection, this inside a method is
the full actor context, and every method answers with a promise — locally and
over a wire alike:
const counters: ActorPlugin = (config) =>
mergeConfigs(config, {
$reflectionMethods: {
async "counters.get"() {
return this.state.count;
},
async "counters.reset"() {
this.state.count = 0;
},
},
});
const actor = defineActor({
plugins: [counters],
setup: () => ({ count: 0 }),
handlers: {},
});
const proc = await actor.spawn(null);
await proc.ready();
await proc.$reflection["counters.get"](); // 0
mergeConfigs deep-merges $reflectionMethods across plugins, so several
plugins can each register their own namespaced methods. Type them via
declaration merging on ActorReflection — see the Type augmentation section
below.
Type augmentation
Plugins that decorate this or add reflection methods want their additions to
be typed for consumers. posipaki ships two empty interfaces for that — augment
them via declaration merging in a .d.ts:
declare module "posipaki" {
// `this.<prop>` inside handlers, methods and hooks
interface ActorDecorated {
log: (message: string) => void;
now: () => number;
}
// `proc.$reflection.<method>`
interface ActorReflection {
"counters.get": () => Promise<number>;
"counters.reset": () => Promise<void>;
}
}
ActorDecorated keys become properties on this; ActorReflection keys become
methods on proc.$reflection. The merge is global — importing a plugin that
ships these augmentations types them for your whole project.
Installing
Plugins go on the config's plugins field:
plugins: [debugLogger(), inspect()],
A plugin that throws during assembly is caught and logged — the actor still spawns without that plugin's contribution.
Inheritance
Plugins are inherited down the actor tree. Each child decides how:
- Omit
plugins— the child inherits the parent's full chain. - Array
plugins: [a, b]— the child's chain is exactly[a, b]; the parent's plugins are dropped. - Function
plugins: (parents) => [...]— the child receives the parent's chain and returns its own:
const child = defineActor({
plugins: (parents) => [...parents, inspect()], // keep + extend
handlers: {},
});
On a standalone spawn there is no parent, so the function receives [].
spawn and fork also accept an addPlugins option — plugins appended after
the resolved chain that cannot be overridden:
const proc = await actor.spawn(null, { addPlugins: [collector.plugin] });
Plugins are deduplicated by function name, so the same plugin can be inherited and re-added without running twice. Anonymous plugin functions skip dedup (and emit a warning), so give your plugins names.
Bundled plugins
posipaki ships two plugins.
debug-logger
import { debugLogger } from "posipaki/plugins/debug-logger";
Decorates this.log (debug, info, warn, error, msg, lifecycle) and
observes lifecycle events and message traffic. Options:
ignore— message types to skip entirely.factory— a(name) => Loggerfunction; the default prints to the console. Swap it for a ringbuffer to gate output at runtime.msgFilter— shrink or drop a message before logging. The default shrinks fields larger than 256 chars / 16 items.
See Debug Logger for the full picture.
tree-introspection
import { inspect } from "posipaki/plugins/tree-introspection";
Registers reflection methods for poking at a live tree:
inspect.getTree(prefix?)— the process tree under this actor.inspect.getState()— this actor's current state.inspect.find(pname)— find a descendant process by name.inspect.exit()— exit this actor.
See Introspection for the full picture.
const proc = await actor.spawn(null);
const tree = await proc.$reflection["inspect.getTree"]();
Testing plugins
posipaki/testing is built on the plugin system — createCollector and
createRootTracker both return { plugin, … } objects installed via
addPlugins. See Testing.