Files
suaveplan/packages/api-stack/api/api-lifecycle/src/fsm-factory/fsm-factory.md
2026-05-27 23:29:53 +01:00

2.9 KiB

fsm-factory

(no submodules)

Purpose

Binds the @suaveplan/api-lifecycle definitions together — states, transitions, side-effects, and the per-plugin mutex — into a running FSM backed by @suaveplan/fsm's FSMInterpreter. The factory is the single entry point api-host driver imports; everything else in this package is reachable through the returned PluginFsm handle.

Features

  • Builds a FSMDefinition from the 13-state union and the 22-edge legal transitions table.
  • Wraps every transition(trigger) call in a mutex acquire/release pair so concurrent calls observe FIFO ordering.
  • Pre-resolves the destination via transitions.resolve so illegal pairs throw LIFECYCLE.ILLEGAL_TRANSITION (not the upstream FSMError), keeping the error namespace clean.
  • Appends the matching SideEffectEntry to the FSM's append-only log on every commit; the host runtime reads the log and dispatches the ops.
  • onTransition listener API for admin UI subscriptions; returns an unsubscribe function.

Usage: Basic

import { createPluginFsm } from "@suaveplan/api-lifecycle/fsm-factory";

const fsm = createPluginFsm({ plugin: "billing" });
await fsm.transition("install");
await fsm.transition("installCompleted");
console.log(fsm.state); // "installed"

Usage: Advanced

Wire a custom mutex (e.g. Redis-backed) plus an external subscriber that forwards transition snapshots to OpenTelemetry:

import { createPluginFsm, type PluginFsm } from "@suaveplan/api-lifecycle/fsm-factory";

declare const redisMutex: { acquire(p: string): Promise<{ release(): void }>; tryAcquire(p: string): { release(): void } | null };
const fsm: PluginFsm = createPluginFsm({
    plugin: "billing",
    initial: "uninstalled",
    mutex: redisMutex,
});

const off = fsm.onTransition(({ from, to, trigger }) => {
    span.addEvent("plugin.transition", { from, to, trigger });
});

await fsm.transition("install");
off();

API Reference

  • createPluginFsm(options): PluginFsm — entry point.
  • interface CreatePluginFsmOptions — plugin, optional initial, transitions, mutex.
  • interface PluginFsm — state, sideEffects, transition(trigger), onTransition(listener).
  • interface PluginFsmContext — plugin, sideEffects (carried by the underlying FSM definition for traceability).

Implementation Notes

The factory builds the FSMDefinition.states map exhaustively from PLUGIN_STATES so every state has a StateDefinition — @suaveplan/fsm otherwise throws INVALID_STATE when entering a state that was never declared. The transition-table iteration deliberately scans edges() once per state during construction; the resulting per-state on map is then served O(1) by FSMInterpreter. The mutex acquire happens before the transitions.resolve call so a queued caller never sees the FSM in a half-mutated state.