Initial Commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# `fsm-factory`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user