Writing a plugin
A plugin has a unique name and one setup(host) method. Setup returns a namespaced API, a snapshot
deriver, or both.
Minimal plugin
Section titled “Minimal plugin”import type { JourneyPlugin } from "@rxova/journey-core";
type CounterApi = { count(): number };type CounterSnapshot = { transitions: number };
export function createCounterPlugin(): JourneyPlugin<"counter", CounterApi, CounterSnapshot> { return { name: "counter", setup(host) { let transitions = 0;
host.onTransition(() => { transitions += 1; });
return { api: { count: () => transitions }, deriveSnapshot: (_snapshot, previous) => previous?.transitions === transitions ? previous : { transitions } }; } };}State belongs inside setup. Reusing one plugin object across machines must not share counters,
buffers, timers, or subscriptions.
Plugin host
Section titled “Plugin host”host.getSnapshot();host.structure;structure is a frozen view with kind, stepIds, initial, and flattened transitions. Each
transition exposes event, from, to, and whether it is guarded.
Observation taps
Section titled “Observation taps”host.onTransition(listener);host.onNavigationBlocked(listener);host.onStatusChange(listener);host.onContextChange(listener);host.onError(listener);Each returns an unsubscribe function. onTransition runs after post-commit hooks settle; the named
event taps follow the same payloads as machine subscriptions. Per-step entry and exit are read from
onTransition’s from/to, or subscribed on the machine itself with stepEnter / stepLeave.
Disposal
Section titled “Disposal”host.onDispose(() => clearTimeout(timer));Register cleanup for resources owned by the plugin. Disposal callbacks are run once and isolated from one another.
API contribution
Section titled “API contribution”The returned api appears only under the plugin name:
machine.plugins.counter.count();Do not expose mutable plugin internals. Return snapshots, copies, or readonly data from read APIs.
Snapshot contribution
Section titled “Snapshot contribution”deriveSnapshot(snapshot, previousExtension) runs during snapshot construction. Keep it pure and
return the previous object when its visible value has not changed:
deriveSnapshot: (_snapshot, previous) => previous?.transitions === transitions ? previous : { transitions };The value appears at snapshot.plugins.counter and can be observed with subscribe.
Snapshot derivation may run more than once around one lifecycle operation because the runtime refreshes plugin-derived state after observation taps.
Boundaries
Section titled “Boundaries”The V1 host is deliberately observe-only. A plugin cannot:
- cancel or rewrite navigation;
- mutate the core snapshot;
- dispatch graph events through the host;
- add unnamespaced methods to the machine.
Put domain transition behavior in the definition. Use plugins for recording, persistence, analysis, and integrations driven by observations.