EnglishGitHub
Prototyppackages/state-machine/src

State Machine

@ralphschuler/state-machine

Serialisiert Übergänge, wartet Guards ab und betritt den korrekten Zielzustand.

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

Serialisiert Übergänge, wartet Guards ab und betritt den korrekten Zielzustand. Aktionen können externe Effekte haben. Rekursiv ausgelöste Events innerhalb einer aktiven Aktion nicht abwarten; Folgeereignisse nach ihrem Ende einplanen.

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 / addTransition / transitionKonfiguriert und serialisiert Guard-geschützte Zustandsübergänge.
subscribe / getCurrentState / isInEndStateBeobachtet mit explizitem Abmelden und liest den Zustand.

05 · Selbst bauen

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.
Konzeptskizze · kann ausgelassene Helfer verwenden
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 solltest du testen?

  • 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.

Selbst ausprobieren

Formuliere die erste Prüfaussage als fehlschlagenden Test, implementiere die kleinste Korrektur und ergänze einen Grenzfall.

Unbekannte Ereignisse lassen den Zustand unverändert.

Ansatz vergleichen

Dein Test sollte beim gezielten Verletzen der Invariante fehlschlagen und nach der Korrektur wieder bestehen. Halte den Aufbau deterministisch und prüfe ein beobachtbares Ergebnis.

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

Aktionen können externe Effekte haben. Rekursiv ausgelöste Events innerhalb einer aktiven Aktion nicht abwarten; Folgeereignisse nach ihrem Ende einplanen.

08 · Weiterdenken

Quellcode und verwandte Ideen

yarn tsx packages/state-machine/examples/basic.ts