Domain
This guide walks through the most common domain patterns: defining phases and events, guarding events, running lifecycle hooks, subscribing to changes, and persisting flow state with snapshots. For complete signatures and field tables, see the Definition Functions, Registration Helpers, and Domain Instance reference pages.
A Domain models a durable business flow that persists across time. It has named phases (declared with when), events (on) that move it between phases, lifecycle hooks, and can invoke pipelines on events.
Defining a Domain
Section titled “Defining a Domain”Use defineDomain() to create a domain. The setup function runs synchronously and registers phases using the when() helper.
import { defineDomain, when, on, target, terminal } from '@tde.io/plexis';
type OrderContext = { orderId: string | null };
const order = defineDomain<OrderContext>('order', () => { when('pending', () => { on('submit', target('processing')); }); when('processing', () => { on('fulfill', target('fulfilled')); }); when('fulfilled', terminal());
return { context: { orderId: null }, initial: 'pending', } as const;});The as const annotation on the return value enables TypeScript to narrow the allowed event names in follow() and can().
Phases
Section titled “Phases”Register phases with when(id, fn | terminal()). The setup function receives no arguments; it calls scoped helpers to configure the phase:
| Helper | Description |
|---|---|
on(event, def) | Register a named event out of this phase |
enter(fn) | Runs when the domain enters this phase |
exit(fn) | Runs when the domain exits this phase |
Pass terminal() as the second argument to register a terminal phase with no further behavior.
Events
Section titled “Events”Register an event on a phase using on(event, target(id)) (simple form) or on(event, () => { ...; return target(id); }) (setup function form) inside a when setup:
when('pending', () => { // Setup function form — attach guard, action, pipeline on('submit', () => { guard((ctx) => ctx.orderId !== null); action(async (ctx) => ({ submitted: true })); return target('processing'); }); // Simple form — direct target on('cancel', target('cancelled'));});guard(fn) registers a guard predicate. If it returns false, the event is blocked — no hooks or actions run.
action(fn) registers an event action. It receives the current context and an ActionInput (event name, payload, trace ID) and returns a context patch merged as { ...prev, ...patch }.
pipeline(p) attaches a pipeline to run during the event.
Each of guard, action, and pipeline may be called at most once per on setup. A second call throws DUPLICATE_REGISTRATION.
Lifecycle Hooks
Section titled “Lifecycle Hooks”enter and exit run when the domain enters or leaves a phase. Call them inside the when setup function. Both receive the current context and a PhaseHookInput (event name, payload, trace ID). Both can return context patches.
when('processing', () => { enter(async (ctx, { event }) => { console.log(`Entered processing via ${event}`); return { enteredAt: Date.now() }; }); exit(async (ctx) => { console.log('Leaving processing'); }); on('fulfill', target('fulfilled'));});Execution order on follow(event, payload):
- Guard check — blocks if
false; no side effects run exitfor the current phaseonactiononpipeline- Phase change to target
enterfor the target phase- Phase entry pipeline
- History recorded, subscribers notified
Advancing the Domain with follow()
Section titled “Advancing the Domain with follow()”Call domain.follow(event, payload?) to trigger an event.
const result = await order.follow('submit');
console.log(result.status); // 'followed'console.log(result.from); // 'pending'console.log(result.to); // 'processing'console.log(result.context); // updated contextSee DomainFollowResult for the complete field reference.
Checking Whether an Event Is Allowed
Section titled “Checking Whether an Event Is Allowed”Use domain.can(event) to test whether an event can be followed from the current phase without triggering any side effects:
const canSubmit = await order.can('submit');Subscriptions and History
Section titled “Subscriptions and History”Subscribe to phase changes:
const unsub = order.subscribe((snapshot) => { console.log(snapshot.phase, snapshot.context);});
// Later:unsub();Retrieve the event history:
const entries = order.history();// Each entry: { from, to, event, payload?, context, timestamp, traceId }Snapshots
Section titled “Snapshots”snapshot() captures the current phase and context at a point in time. restore() winds the domain back to that captured point — useful for undo/redo, testing, and persisting flow state.
// Persist wizard flow to localStorage so the user can resume laterconst snap = order.snapshot();localStorage.setItem('order-flow', JSON.stringify(snap));
// On page reload — restore exactly where the user left offconst saved = localStorage.getItem('order-flow');if (saved) order.restore(JSON.parse(saved));In tests, snapshots let you jump the domain to any starting phase without replaying events:
const processingSnapshot = { phase: 'processing', context: { orderId: '123' }, historyLength: 1,};order.restore(processingSnapshot);
// Now test events from 'processing' directlyconst result = await order.follow('fulfill');Strict Mode
Section titled “Strict Mode”Pass strict: true in the setup return to throw a PlexisError when an unknown event is sent (instead of returning status: 'ignored'):
return { context: {}, initial: 'pending', strict: true } as const;TypeScript Event Inference
Section titled “TypeScript Event Inference”Using as const on the setup return value lets TypeScript infer the exact set of valid event names:
const order = defineDomain('order', () => { when('pending', () => { on('submit', target('processing')); }); when('processing', terminal()); return { context: {}, initial: 'pending' } as const;});
await order.follow('submit'); // ✓ OKawait order.follow('cancel'); // ✗ TypeScript error