Skip to content

Persistent FSM — finite-state machine + event sourcing combined #52

@pathosDev

Description

@pathosDev

We ship FSM (`src/fsm/FSM.ts`) and PersistentActor separately. The combination — a state machine whose transitions are persisted as events and replayed on recovery — is its own well-defined pattern (Akka has `PersistentFSM`).

Why it earns its own actor: order workflows, reservation systems, payment flows, claim adjudication. The natural model is "current state + transition events"; today users either roll their own (mixing FSM message dispatch with manual `persist` calls) or pick one or the other.

API sketch:

```ts
type State = 'pending' | 'paid' | 'shipped' | 'cancelled';
type Event = { kind: 'paid' } | { kind: 'shipped' } | { kind: 'cancelled' };
type Cmd = { kind: 'pay' } | { kind: 'ship' } | { kind: 'cancel' };

class OrderFsm extends PersistentFSM<Cmd, Event, State, OrderData> {
readonly persistenceId = 'order-1';
initialState(): State { return 'pending'; }
initialData(): OrderData { return { items: [], total: 0 }; }

override transitions = {
pending: {
pay: { event: { kind: 'paid' }, next: 'paid' },
cancel: { event: { kind: 'cancelled' }, next: 'cancelled' },
},
paid: {
ship: { event: { kind: 'shipped' }, next: 'shipped' },
},
};

applyEvent(state: State, data: OrderData, e: Event): { state: State; data: OrderData } {
/* user-defined: how each event mutates state + data */
}
}
```

The base class:

  • Routes commands through the user-declared transition table.
  • On a valid transition, calls `persist(event)` then `applyEvent(...)` to update both state and domain data.
  • On recovery, replays events through `applyEvent` to rebuild state + data.
  • Honours the snapshot policy from `PersistentActor`.

Components:

File Task
`src/fsm/PersistentFSM.ts` (new) Base class + transition-table validation.
`tests/unit/fsm/PersistentFSM.test.ts` (new) Round-trip, recovery from journal, invalid-transition rejection.
`examples/fsm/order-workflow.ts` (new) End-to-end demo of an order pipeline.

Estimate: 3-4 days.

Verification:

  • Round-trip: drive an order through pending → paid → shipped, restart actor, recovered state is `'shipped'` with the right data.
  • Invalid transition: `ship` from `'pending'` is rejected; no event is persisted.

Out of scope:

  • Time-based transitions (`stateTimeout` from Akka) — separate issue if requested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions