GitHub
teilweisepackages/state-machine/src

State Machine

@ralphschuler/state-machine

Modelliert erlaubte Zustandswechsel explizit und hält Guards, Aktionen sowie Beobachter von der Fachlogik getrennt.

FSMguardstransitions

01 · Problem

Wofür braucht man das?

Wenn Statuswechsel über viele if-Zweige verteilt sind, entstehen unmögliche Zustände, versteckte Seiteneffekte und schwer reproduzierbare Fehler.

02 · Denkmodell

Das mentale Modell

Eine endliche Zustandsmaschine liest das Paar aus aktuellem Zustand und Ereignis, prüft optional einen Guard und wechselt genau einmal zum Zielzustand.

Im Repository

StateMachine, Konfigurationen und Transitionstypen sind exportiert. Asynchrone Guards werden jedoch nicht korrekt abgewartet, Entry-Aktionen greifen teils auf den alten Zustand zu und Aktionsrückgaben werden als Folgeereignis interpretiert. Eine Ereigniswarteschlange und sauberes Abmelden fehlen.

03 · Kontrollfluss

Was passiert in welcher Reihenfolge?

  1. Aktuellen Zustand und Ereignis gegen die Transitionstabelle auflösen.
  2. Optionalen Guard vollständig abwarten und bei false ohne Änderung zurückkehren.
  3. Exit-Aktion ausführen, Zielzustand atomisch setzen und Entry-Aktion starten.
  4. Beobachter nach erfolgreichem Wechsel mit Vorher/Nachher-Daten benachrichtigen.
  5. Gleichzeitige Ereignisse über eine Queue serialisieren.

04 · Bauteile

Die entscheidenden Verträge

StateMachine Verwaltet Zustand, Transitionen und State-Change-Observer.
addTransition Registriert Ereignis, Ziel, Guard und optionale Aktion.
transition Versucht einen Zustandswechsel für ein Ereignis.
subscribe Beobachtet erfolgreiche Zustandswechsel.

05 · Build it yourself

Selbst implementieren

Für den Kern reichen eine typisierte Tabelle und eine einzige asynchrone send-Methode.

  1. Zustände und Ereignisse als String-Unions definieren.
  2. Transitionen je Zustand und Ereignis in einer Tabelle ablegen.
  3. Guard abwarten und erst danach den Zustand in einem Schritt aktualisieren.
minimal.ts · unabhängig vom Package
type Transition<S, P> = {
  target: S;
  guard?: (payload: P) => boolean | Promise<boolean>;
};

class Machine<S extends string, E extends string, P> {
  constructor(
    private state: S,
    private table: Record<S, Partial<Record<E, Transition<S, P>>>>,
  ) {}

  async send(event: E, payload: P) {
    const next = this.table[this.state][event];
    if (!next || (next.guard && !(await next.guard(payload)))) return this.state;
    this.state = next.target;
    return this.state;
  }
}

06 · Verifizieren

Was du testen solltest

  • Unbekannte Ereignisse lassen den Zustand unverändert.
  • Synchrone und asynchrone Guards blockieren oder erlauben dieselbe Transition korrekt.
  • Parallel gesendete Ereignisse werden in definierter Reihenfolge verarbeitet.

07 · Grenzen

Kompromisse und Stolperfallen

  • Explizite Tabellen machen Verhalten sichtbar, werden bei vielen orthogonalen Zuständen aber groß.
  • Asynchrone Aktionen sind praktisch, brauchen jedoch eine klare Queue- und Fehlerstrategie.
  • Historie und Persistenz gehören als Adapter um den kleinen Kern, nicht in jede Transition.
Wichtig

Entry- und Exit-Aktionen sollten keine weiteren Ereignisse rekursiv auslösen; lege Folgeereignisse in eine Queue.

08 · Weiterdenken

Quellcode und Nachbarn

Originalcode auf GitHub ansehen