Skip to content

@studnicky/fsm ​

Type-safe abstract FSM base class with async effect interpretation and named machine registry.

What it is ​

A composable finite-state-machine primitive: a consumer supplies states, events, a reducer, and optional effect handling while the package supplies legal transition execution, mailbox management, and lifecycle observation. It does not prescribe a domain state chart or workflow.

What it is for ​

Northstar Books uses it to make the allowed transitions of a fulfilment or return flow explicit and testable. Consumers own the order states, guards, effects, and external integrations; the package protects the mechanical transition and effect-dispatch boundary.

Northstar Books examples ​

The runnable lifecycle-hooks example advances a state machine and records paired machine and interpreter events. It maps to Northstar Books observing each legal fulfilment-state change, proving that instrumentation can attach to transitions without embedding a logging product or changing reducer behavior.

Public entrypoints ​

Import pathUse it when
@studnicky/fsm/nodeA Northstar Books server runs typed order, return, or fulfilment state machines.
@studnicky/fsm/browserA browser client runs the same portable state-machine contract for local interaction state.
@studnicky/fsm/entitiesAn adapter validates the package's interpreter history and registry metrics data.
@studnicky/fsm/interfacesTypeScript code shares state, transition, effect, registry, and history contracts.

Install ​

bash
pnpm add @studnicky/fsm

Requires @studnicky:registry=https://npm.pkg.github.com in .npmrc.

Usage ​

Define a StateMachine subclass with getInitialState and reduce, then drive it with EffectInterpreter. The interpreter manages the mailbox, dispatches effects, and notifies subscribers on every state change:

Pipeline effects ​

A reducer can emit a data-only PipelineEffectInterface<TEvent> with variant: 'pipeline' and a typed event. PipelineEffectHandler.create(pipeline) runs its injected PipelineInterface<TEvent>, then dispatches the pipeline result through the interpreter mailbox. The reducer validates that follow-up event and chooses the declared next state; the pipeline neither holds a machine nor selects a state. A rejected pipeline follows the ordinary effect-handler failure path after the intermediate state commits.

Northstar Books fulfilment workflow ​

Northstar Books accepts web orders only through declared fulfilment transitions: a paid order moves to preparing, a warehouse result moves it to allocated or back to review, and a shipment confirmation moves it to shipped. The reducer is the authority for those legal states and rejects events that do not apply to the current order.

A preparing transition can emit a pipeline effect that validates the order, calculates its price, and requests warehouse allocation. The interpreter runs that pipeline and feeds its resulting event back through the mailbox; the reducer then decides the next declared state. This describes a web/server integration pattern rather than output from the runnable traffic-light example above.

MachineRegistry: named registry ​

Northstar Books might run several independent toggles or small machines side by side — a feature flag here, a sync indicator there — and each one needs a name other code can look it up by, without those machines bleeding into each other across unrelated parts of the app. MachineRegistry.create() gives you exactly that: a fresh, isolated namespace. The example below registers one interpreter under 'toggle-a', drives it entirely through the registry rather than the original object, proves a second registration under the same name throws, and shows that unregistering it frees the name again:

ts
import type { FsmStepInterface } from '../src/index.js';
import type { ToggleEventEntity } from './entities/ToggleEventEntity.js';
import type { ToggleStateEntity } from './entities/ToggleStateEntity.js';

import { EffectInterpreter, MachineAlreadyRegisteredError, MachineRegistry, StateMachine } from '../src/index.js';

class Toggle extends StateMachine<ToggleStateEntity.Type, ToggleEventEntity.Type> {
  static make(): Toggle { return new Toggle(); }

  getInitialState(): ToggleStateEntity.Type {
    return { 'variant': 'off' };
  }

  reduce(state: ToggleStateEntity.Type, event: ToggleEventEntity.Type): FsmStepInterface<ToggleStateEntity.Type> {
    if (event.type === 'toggle') {
      return { 'effects': [], 'state': { 'variant': state.variant === 'off' ? 'on' : 'off' } };
    }
    return { 'effects': [], 'state': state };
  }
}

const interpreter: EffectInterpreter<ToggleStateEntity.Type, ToggleEventEntity.Type> = EffectInterpreter.create(Toggle.make(), { 'machineId': 'toggle-a' });
interpreter.start();

const registry = MachineRegistry.create<ToggleStateEntity.Type, ToggleEventEntity.Type>();

// Registry is empty before registration
assert.equal(registry.has('toggle-a'), false);
assert.deepEqual(registry.list(), []);

registry.register('toggle-a', interpreter);
assert.equal(registry.has('toggle-a'), true);
assert.deepEqual(registry.list(), ['toggle-a']);

// Duplicate registration throws MachineAlreadyRegisteredError
assert.throws(
  () => { registry.register('toggle-a', interpreter); },
  MachineAlreadyRegisteredError
);

// Retrieve and drive the machine through the registry
const found = registry.get('toggle-a');
assert.ok(found !== undefined);

await found.send({ 'type': 'toggle' });
assert.equal(interpreter.getState().variant, 'on');

await found.send({ 'type': 'toggle' });
assert.equal(interpreter.getState().variant, 'off');

// Unknown name returns undefined
assert.equal(registry.get('unknown'), undefined);

registry.unregister('toggle-a');
assert.equal(registry.has('toggle-a'), false);

interpreter.stop();

console.log('Registry state:', registry.list());

Error handling ​

A reducer is just a function, and functions can misbehave — throw on a bad event, get read before the interpreter is ready, or get sent to after it has already shut down. Rather than let any of that surface as a bare, unrecognizable exception, the FSM wraps each failure in a named error you can catch for. This example triggers all three: a reducer that throws mid-transition (wrapped as ReducerThrewError), a getState() call before start() (InterpreterNotStartedError), and a send() after stop() (InterpreterNotRunningError):

ts
import type { FsmStepInterface } from '../src/index.js';
import type { BrokenEventEntity } from './entities/BrokenEventEntity.js';
import type { BrokenStateEntity } from './entities/BrokenStateEntity.js';
import type { SimpleEventEntity } from './entities/SimpleEventEntity.js';
import type { SimpleStateEntity } from './entities/SimpleStateEntity.js';

import {
  EffectInterpreter,
  InterpreterNotRunningError,
  InterpreterNotStartedError,
  ReducerThrewError,
  StateMachine
} from '../src/index.js';

// --- ReducerThrewError ---

class BrokenMachine extends StateMachine<BrokenStateEntity.Type, BrokenEventEntity.Type> {
  static make(): BrokenMachine { return new BrokenMachine(); }

  getInitialState(): BrokenStateEntity.Type {
    return { 'variant': 'active' };
  }

  reduce(_state: BrokenStateEntity.Type, event: BrokenEventEntity.Type): FsmStepInterface<BrokenStateEntity.Type> {
    if (event.type === 'boom') { throw RuntimeError.create('reducer exploded'); }
    return { 'effects': [], 'state': _state };
  }
}

const broken: BrokenMachine = BrokenMachine.make();
const initialState: BrokenStateEntity.Type = { 'variant': 'active' };

// StateMachine.transition wraps reducer throws in ReducerThrewError
assert.throws(
  () => { broken.transition(initialState, { 'type': 'boom' }); },
  ReducerThrewError
);

console.log('ReducerThrewError thrown and caught');

// --- InterpreterNotStartedError ---

class SimpleMachine extends StateMachine<SimpleStateEntity.Type, SimpleEventEntity.Type> {
  static make(): SimpleMachine { return new SimpleMachine(); }
  getInitialState(): SimpleStateEntity.Type { return { 'variant': 'idle' }; }
  reduce(state: SimpleStateEntity.Type): FsmStepInterface<SimpleStateEntity.Type> { return { 'effects': [], 'state': state }; }
}

const notStarted: EffectInterpreter<SimpleStateEntity.Type, SimpleEventEntity.Type> = EffectInterpreter.create(SimpleMachine.make());

// getState before start() throws InterpreterNotStartedError
assert.throws(
  () => { notStarted.getState(); },
  InterpreterNotStartedError
);

console.log('InterpreterNotStartedError thrown and caught');

// --- InterpreterNotRunningError ---

const stopped: EffectInterpreter<SimpleStateEntity.Type, SimpleEventEntity.Type> = EffectInterpreter.create(SimpleMachine.make());
stopped.start();
stopped.stop();

// send after stop() throws InterpreterNotRunningError
await assert.rejects(
  async () => { await stopped.send({ 'type': 'noop' }); },
  InterpreterNotRunningError
);

console.log('InterpreterNotRunningError thrown and caught');

Observability hooks ​

Every stateful class exposes protected hook methods that fire at each significant stage. Override them in a subclass to add logging, tracing, or metrics without changing any public behaviour.

StateMachine hooks ​

HookWhen it firesArgs
onTransition(from, to, event)After a successful state-variant change, before the step is returnedfrom: TState, to: TState, event: TEvent
onEnterState(state)When entering a new state variant (fires after onTransition)state: TState
onExitState(state)When leaving the current state variant (fires before onTransition)state: TState
onTransitionRejected(state, event, reason)When reduce throws — no valid transition / guard failedstate: TState, event: TEvent, reason: string

onTransition, onEnterState, and onExitState are only called when the state variant changes. Self-loops (same variant returned) fire none of them.

onEnterState may return void or a Promise<void>. The other StateMachine hooks — onTransition, onExitState, onTransitionRejected — remain synchronous (void).

EffectInterpreter hooks ​

HookWhen it firesArgs
onStart(state)After start() sets the initial statestate: TState
onStop(state)After stop() halts event processingstate: TState | undefined
onEnqueue(event)When an event is added to the mailbox by send()event: TEvent
onTransition(from, to, event)When the interpreter commits a state-variant changefrom: TState, to: TState, event: TEvent
onEnterState(state)After committing a new state variantstate: TState
onExitState(state)Before committing the new state, while still in the old variantstate: TState
onEffectStart(effect)Before invoking an effect handlereffect: TEffect
onEffectSuccess(effect)After an effect handler resolves successfullyeffect: TEffect
onEffectError(effect, error)When an effect handler throwseffect: TEffect, error: Error

onEnterState may return void or a Promise<void>. The other EffectInterpreter hooks remain synchronous (void).

MachineRegistry hooks ​

MachineRegistry exposes protected instance hooks. Override them in a subclass and call register, unregister, and get on that registry instance.

HookWhen it firesArgs
onRegister(id)After a named interpreter is successfully registeredid: string
onUnregister(id)After unregister() is called (fires even if the key did not exist)id: string
onResolveMiss(id)When get() returns undefined for an unknown idid: string

onRegister may return void or a Promise<void>. onUnregister and onResolveMiss remain synchronous (void).

Example — traced traffic light ​

Here is all three layers wired up with their hooks at once: a traffic-light machine logging its own transitions, an interpreter logging effect dispatch around it, and a registry logging registration events — the full stack a Northstar Books ops dashboard would tap into to watch a fulfilment machine live. Watch the paired [fsm:machine] and [fsm:interp] log lines as the light advances red → green → amber → red, a chime effect firing on the amber transition, and a deliberate lookup miss against an unregistered id along the way.

ts
import type { EffectHandlerInterface, FsmStepInterface } from '../src/index.js';
import type { TrafficEffectEntity } from './entities/TrafficEffectEntity.js';
import type { TrafficEventEntity } from './entities/TrafficEventEntity.js';
import type { TrafficStateEntity } from './entities/TrafficStateEntity.js';

import { EffectInterpreter, MachineRegistry, StateMachine } from '../src/index.js';

// --- Observed StateMachine subclass ---

class ObservedTrafficMachine extends StateMachine<TrafficStateEntity.Type, TrafficEventEntity.Type, TrafficEffectEntity.Type> {
  static make(): ObservedTrafficMachine { return new ObservedTrafficMachine(); }

  getInitialState(): TrafficStateEntity.Type { return { 'variant': 'red' }; }

  reduce(
    state: TrafficStateEntity.Type,
    event: TrafficEventEntity.Type
  ): FsmStepInterface<TrafficStateEntity.Type, TrafficEffectEntity.Type> {
    if (event.type === 'advance') {
      if (state.variant === 'red')   { return { 'effects': [], 'state': { 'variant': 'green' } }; }
      if (state.variant === 'green') { return { 'effects': [{ 'tone': 'chime', 'variant': 'playSound' }], 'state': { 'variant': 'amber' } }; }
      if (state.variant === 'amber') { return { 'effects': [], 'state': { 'variant': 'red' } }; }
    }
    return { 'effects': [], 'state': state };
  }

  protected override onTransition(from: TrafficStateEntity.Type, to: TrafficStateEntity.Type, event: TrafficEventEntity.Type): void {
    console.log(`[fsm:machine] transition  ${from.variant} --[${event.type}]--> ${to.variant}`);
  }

  protected override onEnterState(state: TrafficStateEntity.Type): void | Promise<void> {
    console.log(`[fsm:machine] enter       state=${state.variant}`);
  }

  protected override onExitState(state: TrafficStateEntity.Type): void {
    console.log(`[fsm:machine] exit        state=${state.variant}`);
  }

  protected override onTransitionRejected(state: TrafficStateEntity.Type, event: TrafficEventEntity.Type, reason: string): void {
    console.log(`[fsm:machine] rejected    state=${state.variant} event=${event.type} reason=${reason}`);
  }
}

// --- Observed EffectInterpreter subclass ---

class ObservedInterpreter extends EffectInterpreter<TrafficStateEntity.Type, TrafficEventEntity.Type, TrafficEffectEntity.Type> {
  static readonly handler: EffectHandlerInterface<TrafficEffectEntity.Type> = (effect) => {
    soundsPlayed.push(effect.tone);
  };

  static makeObserved(
    machine: ObservedTrafficMachine,
    handler: EffectHandlerInterface<TrafficEffectEntity.Type>
  ): ObservedInterpreter {
    return new ObservedInterpreter({ 'handler': handler, 'machine': machine, 'machineId': 'traffic-light' });
  }

  protected override onStart(state: TrafficStateEntity.Type): void {
    console.log(`[fsm:interp]  start       initialState=${state.variant}`);
  }

  protected override onStop(state: TrafficStateEntity.Type | undefined): void {
    console.log(`[fsm:interp]  stop        lastState=${state?.variant ?? 'unknown'}`);
  }

  protected override onEnqueue(event: TrafficEventEntity.Type): void {
    console.log(`[fsm:interp]  enqueue     event=${event.type}`);
  }

  protected override onTransition(from: TrafficStateEntity.Type, to: TrafficStateEntity.Type, event: TrafficEventEntity.Type): void {
    console.log(`[fsm:interp]  transition  ${from.variant} --[${event.type}]--> ${to.variant}`);
  }

  protected override onEnterState(state: TrafficStateEntity.Type): void | Promise<void> {
    console.log(`[fsm:interp]  enter       state=${state.variant}`);
  }

  protected override onExitState(state: TrafficStateEntity.Type): void {
    console.log(`[fsm:interp]  exit        state=${state.variant}`);
  }

  protected override onEffectStart(effect: TrafficEffectEntity.Type): void {
    console.log(`[fsm:interp]  effectStart variant=${effect.variant} tone=${effect.tone}`);
  }

  protected override onEffectSuccess(effect: TrafficEffectEntity.Type): void {
    console.log(`[fsm:interp]  effectOk    variant=${effect.variant}`);
  }

  protected override onEffectError(effect: TrafficEffectEntity.Type, error: Error): void {
    console.log(`[fsm:interp]  effectError variant=${effect.variant} error=${error.message}`);
  }
}

// --- Observed MachineRegistry subclass ---

class ObservedRegistry extends MachineRegistry<TrafficStateEntity.Type, TrafficEventEntity.Type> {
  static make(): ObservedRegistry {
    return new ObservedRegistry();
  }

  protected override onRegister(id: string): void | Promise<void> {
    console.log(`[fsm:registry] register   id=${id}`);
  }

  protected override onUnregister(id: string): void {
    console.log(`[fsm:registry] unregister id=${id}`);
  }

  protected override onResolveMiss(id: string): void {
    console.log(`[fsm:registry] miss       id=${id}`);
  }
}

// --- Scenario: drive the traffic light through a full cycle ---

const soundsPlayed: string[] = [];
const machine = ObservedTrafficMachine.make();
const interpreter = ObservedInterpreter.makeObserved(machine, ObservedInterpreter.handler);

const observedRegistry = ObservedRegistry.make();

console.log('\n--- Registering interpreter ---');
observedRegistry.register('traffic-light', interpreter);

console.log('\n--- Starting interpreter ---');
interpreter.start();

console.log('\n--- Sending events: advance × 3 ---');
await interpreter.send({ 'type': 'advance' }); // red → green
await interpreter.send({ 'type': 'advance' }); // green → amber (plays sound)
await interpreter.send({ 'type': 'advance' }); // amber → red

console.log('\n--- Probing a missing registry key ---');
observedRegistry.get('no-such-machine');

console.log('\n--- Stopping interpreter ---');
interpreter.stop();

console.log('\n--- Unregistering ---');
observedRegistry.unregister('traffic-light');

console.log('\nFinal state:', interpreter.getState().variant);
console.log('Sounds played:', soundsPlayed);

The base class never calls any logger or metrics library. All hooks are no-ops by default.

API ​

Import FSM classes and package errors from @studnicky/fsm/node; import type contracts from @studnicky/fsm/interfaces and schema namespaces from @studnicky/fsm/entities.

ExportTypeDescription
StateMachine<TState, TEvent, TEffect>abstract classBase FSM; implement getInitialState and reduce
EffectInterpreter<TState, TEvent, TEffect>classDrives a machine; configure a singular handler through create(machine, { handler })
InterpreterHistory<TState, TEvent, TEffect>classBounded recorder of one interpreter's variant-changing transitions
MachineRegistry<TState, TEvent>classInstantiable named registry of interpreters
FsmStepInterface<TState, TEffect>interfaceReadonly { state, effects } contract returned by reduce
FsmTransitionInterface<TState, TEvent, TEffect>interfaceCallable contract for standalone transition functions
EffectHandlerInterface<TEffect, TEvent>interfaceSingular callable effect handler with an in-drain dispatch(event) capability
PipelineEffectHandlervalueCreates an effect handler that runs an injected typed pipeline and dispatches its resulting event
PipelineEffectInterface<TEvent>interfaceData-only pipeline effect descriptor containing the event supplied to its pipeline
EffectInterpreterConstructorOptionsInterface<TState, TEvent, TEffect>interfaceParameter contract for EffectInterpreter's protected constructor; annotate a subclass constructor's parameter with it
InterpreterHistoryRecordInterface<TState, TEvent>interfaceReadonly transition-history record contract
RegisteredInterpreterInterface<TState, TEvent>interfaceInterpreter contract accepted by MachineRegistry
InterpreterHistoryRecordMetadataEntitynamespaceSchema-derived transition-record timestamp contract
RegisteredInterpreterMetricsEntitynamespaceSchema-derived hook-error count contract
FsmError and package errorsclassesFsmConfigError, interpreter lifecycle errors, mailbox capacity errors, registry errors, reducer defects, termination, and rejected transitions

StateMachine<TState, TEvent, TEffect> ​

MemberSignatureDescription
getInitialState() => TStateReturns the machine's initial state
reduce(state, event) => FsmStepInterface<TState, TEffect>Pure transition function
transition(state, event) => FsmStepInterface<TState, TEffect>Calls reduce; wraps reducer defects in ReducerThrewError

EffectInterpreter<TState, TEvent, TEffect> ​

MemberSignatureDescription
start() => voidInitialises state; must be called before send
stop() => voidHalts event processing
getState() => TStateReturns current state; throws if not started
send(event: TEvent) => Promise<void>Enqueues event and drains mailbox
subscribe(observer) => () => voidRegisters a state observer; returns unsubscribe fn

InterpreterHistory<TState, TEvent, TEffect> ​

Sometimes you don't want a subclass just to remember the last few transitions — you want that memory built in. InterpreterHistory is an EffectInterpreter that keeps its own bounded ring of transition records, evicting the oldest once it's full. The example below caps that ring at 2 and sends 3 advances through a traffic light, so the very first transition (red → green) falls out of the recorded history, leaving only the two most recent:

ts
import type { FsmStepInterface } from '../src/index.js';
import type { TrafficEventEntity } from './entities/TrafficEventEntity.js';
import type { TrafficStateEntity } from './entities/TrafficStateEntity.js';

import { InterpreterHistory, StateMachine } from '../src/index.js';

// --- Domain types ---

class TrafficMachine extends StateMachine<TrafficStateEntity.Type, TrafficEventEntity.Type> {
  static make(): TrafficMachine { return new TrafficMachine(); }

  getInitialState(): TrafficStateEntity.Type { return { 'variant': 'red' }; }

  reduce(state: TrafficStateEntity.Type, event: TrafficEventEntity.Type): FsmStepInterface<TrafficStateEntity.Type> {
    if (event.type === 'advance') {
      if (state.variant === 'red')   { return { 'effects': [], 'state': { 'variant': 'green' } }; }
      if (state.variant === 'green') { return { 'effects': [], 'state': { 'variant': 'amber' } }; }
      if (state.variant === 'amber') { return { 'effects': [], 'state': { 'variant': 'red' } }; }
    }
    return { 'effects': [], 'state': state };
  }
}

// A bounded ring of the last 2 transitions — older records are dropped.
const history = InterpreterHistory.create(
  TrafficMachine.make(),
  { 'capacity': 2, 'machineId': 'traffic-light' }
);

history.start();
await history.send({ 'type': 'advance' }); // red -> green
await history.send({ 'type': 'advance' }); // green -> amber
await history.send({ 'type': 'advance' }); // amber -> red, evicts the red -> green record

console.log('Recorded transitions (oldest first):');
const historyRecords = history.history();
const historyRecordsLength = historyRecords.length;
for (let index = 0; index < historyRecordsLength; index += 1) {
  const record = historyRecords[index]!;
  console.log(`  ${record.from.variant} --[${record.event.type}]--> ${record.to.variant} @ ${record.timestamp}`);
}

history.stop();

Each InterpreterHistoryRecordInterface<TState, TEvent> contains event, from, to, and timestamp. history() returns a fresh oldest-first snapshot. The internal ring retains at most capacity records and evicts the oldest when full. Successful sends that retain the current state variant are absent because the recorder follows EffectInterpreter.onTransition semantics.

The record timestamp composes from InterpreterHistoryRecordMetadataEntity, registered-interpreter metrics compose from RegisteredInterpreterMetricsEntity, and history capacity uses CircularBufferOptionsEntity.Type['capacity'] directly from @studnicky/circular-buffer/entities.

Try it ​

Run the examples below directly in the browser to see the FSM primitives in action.

Lifecycle hooks ​

Run the traffic light live and watch the two layers talk to each other: every advance fires a matched pair of log lines, one from the machine announcing its own state change and one from the interpreter announcing the same change plus any effect it dispatched.

Loading example…

Entities ​

@studnicky/fsm/entities exports interpreter history and registry metrics.

typescript
import { InterpreterHistoryRecordMetadataEntity } from "@studnicky/fsm/entities";

Exports ​

SymbolPurposeImport path
EffectHandlerInterfaceDefines effect execution callbacks.@studnicky/fsm/interfaces
PipelineEffectHandlerCreates an effect handler that runs a pipeline result through the interpreter mailbox.@studnicky/pipeline/fsm
PipelineEffectInterfaceDefines a data-only pipeline effect descriptor.@studnicky/pipeline/fsm/interfaces
PipelineEffectVariantEntityDefines the pipeline effect variant schema.@studnicky/pipeline/fsm/entities
EffectInterpreterExecutes state-machine effects.@studnicky/fsm/node
EffectInterpreterConstructorOptionsInterfaceDefines interpreter construction options.@studnicky/fsm/interfaces
FsmConfigErrorRepresents invalid FSM configuration.@studnicky/fsm/node
FsmErrorBase error for FSM failures.@studnicky/fsm/node
FsmStepInterfaceDefines a state-machine transition result.@studnicky/fsm/interfaces
FsmTransitionInterfaceDefines a state-machine transition.@studnicky/fsm/interfaces
InterpreterHistoryRetains state-machine transition history.@studnicky/fsm/node
InterpreterHistoryCollaboratorsInterfaceDefines the typed clock and handler collaborators InterpreterHistory.create accepts alongside schema-validated config; machine is a required positional parameter, not a bag field.@studnicky/fsm/interfaces
InterpreterHistoryRecordInterfaceDefines a recorded transition.@studnicky/fsm/interfaces
InterpreterNotRunningErrorSignals work submitted to a stopped interpreter.@studnicky/fsm/node
InterpreterNotStartedErrorSignals work submitted before an interpreter starts.@studnicky/fsm/node
MachineAlreadyRegisteredErrorSignals duplicate machine registration.@studnicky/fsm/node
MachineRegistryManages named state-machine interpreters.@studnicky/fsm/node
MachineTerminatedErrorSignals use of a terminated machine.@studnicky/fsm/node
MailboxCapacityExceededErrorSignals an interpreter mailbox overflow.@studnicky/fsm/node
ReducerThrewErrorWraps an error thrown by a reducer.@studnicky/fsm/node
RegisteredInterpreterInterfaceDefines a registered interpreter entry.@studnicky/fsm/interfaces
StateMachineDefines typed state transitions and effects.@studnicky/fsm/node
TransitionRejectedErrorSignals a rejected state transition.@studnicky/fsm/node

Source on GitHub