PersistentFSM
PersistentFSM ist die event-sourced Variante von
FSM. Jeder Zustandsübergang wird zu
einem persistierten Event — beim Neustart spielt die FSM sie
ab und setzt im exakten Zustand fort, in dem sie war.
import { PersistentFSM } from 'actor-ts';
type State = 'created' | 'submitted' | 'approved' | 'rejected';type Data = { id: string; reviewer?: string; reason?: string };type Msg = { kind: 'submit' } | { kind: 'approve'; reviewer: string } | { kind: 'reject'; reason: string };
class OrderApproval extends PersistentFSM<State, Data, Msg> { readonly persistenceId = `order-${this.orderId}`;
constructor(private readonly orderId: string) { super('created', { id: orderId });
this.when('created', (data, msg) => { if (msg.kind === 'submit') return this.goto('submitted', data); return this.stay(data); });
this.when('submitted', (data, msg) => { if (msg.kind === 'approve') return this.goto('approved', { ...data, reviewer: msg.reviewer }); if (msg.kind === 'reject') return this.goto('rejected', { ...data, reason: msg.reason }); return this.stay(data); });
// Terminale Zustände — keine weiteren Übergänge. this.when('approved', (data) => this.stay(data)); this.when('rejected', (data) => this.stay(data)); }}Ein Neustart spielt die persistierten Übergänge (Events) ab, sodass die FSM genau dort weitermacht, wo die vorherige Inkarnation aufgehört hat — einschließlich des aktuellen Zustandsnamens, der Daten und etwaiger laufender Seiteneffekt-Buchhaltung.
Was persistiert wird
Abschnitt betitelt „Was persistiert wird“Jeder Übergang (goto) wird als StateChangeEvent
persistiert:
type StateChangeEvent<SName, SData> = { from: SName; to: SName; data: SData; ts: number;};Das Framework schreibt ein Event pro Übergang. stay(...)
persistiert nicht — nur Zustandsänderungen werden
aufgezeichnet.
Das ist eine bewusste Wahl: Stays sind normalerweise
Daten-Updates, die keine Durability für FSM-Korrektheit
brauchen. Wenn du Daten-Updates auch persistiert brauchst,
übergehe explizit (goto('same-state', newData)) — aber das
erzeugt Noise-Events.
Recovery
Abschnitt betitelt „Recovery“preStartliest jedesStateChangeEventfür diepersistenceIdaus dem Journal.- Spielt jedes Event der Reihe nach ab — wendet
from → toplus die gespeicherten Daten an. - Finaler Zustand + Daten sind das Ergebnis des Replays;
die FSM ist jetzt mit diesem Zustand für
onReceiveverfügbar.
Dieselbe Idiomatik wie PersistentActor — Replay baut deterministisch die exakte Position der Zustandsmaschine wieder auf.
when-Handler laufen während des Replays nicht — sie sind für
die Command-Behandlung (den nächsten Übergang entscheiden),
nicht für die Zustands-Ableitung. Der Übergang selbst ist das,
was gespeichert wird; Replay setzt nur den Zustand.
Konfiguration
Abschnitt betitelt „Konfiguration“class OrderApproval extends PersistentFSM<S, D, M> { abstract readonly persistenceId: string;
// Optionale Overrides: snapshotPolicy(): SnapshotPolicy<...> { return everyNEvents(50); } eventAdapter(): EventAdapter<...> | undefined { return undefined; } // ... gleiche wie PersistentActor}Erbt dieselbe Persistenz-Maschinerie wie PersistentActor:
persistenceId— der Event-Stream-Key.snapshotPolicy— periodische Snapshots zur Replay-Begrenzung.eventAdapter/snapshotAdapter— Schema-Migration.tagsFor— Events für Projektions-Konsum taggen.
Snapshots für FSMs
Abschnitt betitelt „Snapshots für FSMs“override snapshotPolicy() { return everyNEvents(50); }Für lange laufende FSMs, die viele Übergänge akkumulieren,
begrenzen Snapshots den Replay. Der Snapshot serialisiert
{ stateName, stateData } — ein kompakter Blob.
Wähle das Intervall basierend auf der Übergangs-Frequenz:
- Wenige Übergänge pro Actor-Lebensdauer (~10) — kein Snapshot nötig.
- Viele Übergänge (100+) — Snapshot alle 50-100 Events.
Siehe Snapshots für die allgemeine Policy-Guidance.
Seiteneffekte pro Übergang
Abschnitt betitelt „Seiteneffekte pro Übergang“class OrderApproval extends PersistentFSM<State, Data, Msg> { // ...
override async onEnter(state: State, data: Data): Promise<void> { if (state === 'submitted') { await this.notifyReviewer(data); // ← Seiteneffekt bei Übergang } }}onEnter-Callbacks laufen nur bei Zustandsänderungen — sowohl
zur Übergangszeit als auch NICHT während des Replays. Das ist
der Schlüssel-Unterschied zu PersistentActor: Persistente FSMs
behandeln den Zustands-Eintritt als Seiteneffekt-Hook, und die
Recovery überspringt sie.
Das bedeutet, Seiteneffekte können in onEnter leben, ohne
Sorge vor Duplizierung beim Neustart. Das ist ein bedeutsamer
ergonomischer Gewinn gegenüber PersistentActors “Seiteneffekte
nur im Persist-Callback.”
Event-Flow während eines Übergangs: 1. Handler gibt goto(newState, newData) zurück 2. Framework persistiert StateChangeEvent 3. onExit(oldState, oldData) feuert 4. onEnter(newState, newData) feuert ← Seiteneffekte hier 5. Antwort / weitere Arbeit läuft weiter
Event-Flow während der Recovery: 1. Persistierte Events lesen 2. Für jedes: Zustand + Daten aktualisieren (keine Callbacks) 3. onReceive setzt fort — erstes neues Command läuft normalLange laufendes Workflow-Beispiel
Abschnitt betitelt „Lange laufendes Workflow-Beispiel“Eine Multi-Step-Saga:
type SagaState = | 'reserved-inventory' | 'charged-payment' | 'scheduled-shipping' | 'completed' | 'compensating-inventory' | 'compensating-payment' | 'failed';
class OrderSaga extends PersistentFSM<SagaState, Data, Msg> { // Jeder Übergang wird persistiert. Ein Crash mitten in der Saga // setzt vom letzten persistierten Zustand fort — keine Arbeit geht verloren.
this.when('reserved-inventory', (data, msg) => { if (msg.kind === 'payment-success') return this.goto('charged-payment', { ... }); if (msg.kind === 'payment-failure') return this.goto('compensating-inventory', { ... }); return this.stay(data); });
override async onEnter(state, data) { if (state === 'charged-payment') { await scheduleShipping(data); // sicher — feuert einmal pro echtem Übergang } if (state === 'compensating-inventory') { await releaseInventory(data); } }}Die Kombination aus persistierten Übergängen + nicht-abgespielten onEnter-Callbacks macht dieses Muster viel sauberer als PersistentActor-mit-Event-Handlern für zustandsmaschinen-förmige Workflows.
Stolperfallen
Abschnitt betitelt „Stolperfallen“Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- FSM im Überblick — das größere Bild.
- FSM (In-Memory) — die nicht-persistente Variante.
- PersistentActor — die zugrunde liegende Event-Sourcing-Engine.
- Snapshots — das Replay-Fenster begrenzen.
- Migration im Überblick — zur Weiterentwicklung von State-Daten-Schemas über die Zeit.
Die PersistentFSM-API-Referenz
deckt die vollständige Oberfläche ab.