Skip to content
@tde.io/plexis  ·  npm install @tde.io/plexis  ·  npm  ·  GitHub

Designing the Phase Graph

A domain’s phase graph is declared in the setup function. Getting the structure right — especially initial, terminal phases, and reachability — determines whether the flow models your business domain cleanly and whether strict mode stays silent.

The initial field in the setup return value names the phase the domain starts in when first created. It must match a phase you registered with when().

import { defineDomain, when, on, target, terminal } from '@tde.io/plexis';
const subscription = defineDomain('subscription', () => {
when('trial', () => {
on('activate', target('active'));
on('expire', target('expired'));
});
when('active', () => {
on('cancel', target('cancelled'));
});
when('expired', terminal());
when('cancelled', terminal());
// 'trial' is the starting point — must be a registered phase
return { context: {}, initial: 'trial' };
});
console.log(subscription.phase); // 'trial'

If initial names a phase that was not registered, the runtime throws PlexisError with code UNKNOWN_INITIAL_PHASE when the domain is created.

A terminal phase is one the domain can enter but never leave. Declare it by passing terminal() as the second argument to when(). The domain will not follow any event while in a terminal phase (following returns status: 'ignored').

when('fulfilled', terminal());
when('cancelled', terminal());

Terminal phases do not need events registered inside them. Any on() calls inside a terminal phase’s setup function are ignored at runtime.

An unreachable phase is one that no event points to and that is not the initial phase. The domain can never enter it through normal operation.

const broken = defineDomain('broken', () => {
when('active', () => {
on('done', target('done'));
});
when('done', terminal());
// Nobody moves to 'orphan' — it is unreachable
when('orphan', terminal());
return { context: {}, initial: 'active' };
});

Without strict mode, unreachable phases are silently ignored. With strict: true, the runtime rejects them at creation time.

Set strict: true in the setup return value to turn graph integrity problems into errors at definition time rather than silent runtime surprises.

const order = defineDomain('order', () => {
when('pending', () => {
on('submit', target('processing'));
});
when('processing', terminal());
// 'ghost' is unreachable
when('ghost', terminal());
return { context: {}, initial: 'pending', strict: true };
// Throws PlexisError at creation: UNREACHABLE_PHASE 'ghost'
});

Strict mode rejects:

  • An initial that names an unregistered phase.
  • Event targets that name unregistered phases.
  • Phases that are not reachable from initial and are not initial themselves.

After construction, domain.graph.describe() returns a GraphDescriptor with all nodes and edges. Use it to audit reachability in tests or tooling.

const graph = subscription.graph.describe();
const unreachable = graph.nodes.filter(
(n) =>
!graph.entryNodes.some((e) => e.nodeId === n.id) &&
!graph.edges.some((e) => e.to.nodeId === n.id),
);
console.log(unreachable.map((n) => n.id)); // []

For the full DomainGraph API, see the Domain Instance reference and Graph Introspection in Patterns.