Skip to content

camcima/finita

Repository files navigation

finita

CI CodeQL codecov npm version License: MIT TypeScript Node.js

A full-featured finite state machine (FSM) library for TypeScript.

This library is a TypeScript port of metabor/statemachine, the PHP state machine library created by Oliver Tischlinger. The original library was the engine behind Bob -- the backend component of the Alice & Bob retail system that Rocket Internet used to power its e-commerce ventures worldwide. The author of this TypeScript port and Oliver worked together at Rocket Internet, where the state machine proved itself at scale across multiple global operations.

Features

  • Type-Safe Subjects -- generic TSubject parameter gives you typed access to the domain object, with unknown default for backward compatibility
  • Async-First -- all conditions, observers, and mutexes support async operations via MaybePromise<T> return types
  • States, Transitions, Events -- define complex workflows declaratively
  • Conditions (Guards) -- control when transitions are allowed, with composable AND/OR/NOT logic
  • Observers (Commands) -- execute side effects when events fire or states change
  • Automatic Transitions -- transitions that fire when a condition becomes true, without an explicit event
  • Transition Selectors -- pluggable strategies for resolving ambiguous transitions (score-based, weight-based)
  • Mutex/Locking -- concurrency control with pluggable lock adapters
  • Factory Pattern -- create pre-configured state machines from subject objects
  • Process Merging -- combine state collections with optional name prefixing
  • Graph Visualization -- build graph data structures for rendering with GraphViz or other tools
  • Setup Helper -- fluent API for building state machines from configuration
  • Zero Dependencies -- no runtime dependencies

Installation

pnpm add @camcima/finita

Quick Start

stateDiagram-v2
    [*] --> draft
    draft --> published : publish
    published --> archived : archive
    archived --> draft : reopen
Loading
import { State, Transition, Process, Statemachine } from "@camcima/finita";

// Define states
const draft = new State("draft");
const published = new State("published");
const archived = new State("archived");

// Define transitions
draft.addTransition(new Transition(published, "publish"));
published.addTransition(new Transition(archived, "archive"));
archived.addTransition(new Transition(draft, "reopen"));

// Create process and state machine
const process = new Process("article-workflow", draft);
const article = { title: "Hello World" };
const sm = new Statemachine(article, process);

console.log(sm.getCurrentState().getName()); // 'draft'

await sm.triggerEvent("publish");
console.log(sm.getCurrentState().getName()); // 'published'

await sm.triggerEvent("archive");
console.log(sm.getCurrentState().getName()); // 'archived'

// Note: use top-level await (supported in ES modules) or wrap in an async function.

Typed Usage

Use the TSubject generic parameter for type-safe access to your domain object -- no more casts:

import {
  State,
  Transition,
  Process,
  Statemachine,
  CallbackCondition,
} from "@camcima/finita";

interface Order {
  id: number;
  total: number;
}

const pending = new State("pending");
const approved = new State("approved");

// subject is typed as Order -- no cast needed
const canApprove = new CallbackCondition<Order>(
  "canApprove",
  (order) => order.total <= 1000,
);
pending.addTransition(new Transition(approved, "review", canApprove));

const process = new Process("order", pending);
const sm = new Statemachine<Order>({ id: 1, total: 500 }, process);

const order = sm.getSubject(); // typed as Order
console.log(order.total); // 500

Concepts

A state machine consists of:

Concept Description
State A named node in the workflow graph. Holds transitions, events, and metadata.
Transition A directed edge from one state to another, optionally guarded by a condition and triggered by an event.
Event A named trigger attached to a state. When invoked, it fires observers (commands) and initiates transitions.
Condition A guard that determines whether a transition is active.
Process A named collection of states that defines a complete workflow, starting from an initial state.
Statemachine The runtime orchestrator that manages the current state, triggers events, checks conditions, and notifies observers.
Observer A callback that reacts to events or state changes.
classDiagram
    direction LR
    Process *-- State : contains
    State *-- Transition : has outgoing
    State *-- Event : has named
    Transition --> State : targets
    Transition --> Condition : guarded by
    Event --> Observer : notifies
    Statemachine --> Process : uses
    Statemachine --> State : tracks current
    Statemachine --> Observer : notifies on change
Loading

Documentation

Detailed documentation for every component:

  • Core -- State, Transition, Event, Process, Statemachine, Dispatcher, StateCollection
  • Conditions -- Tautology, Contradiction, CallbackCondition, Timeout, AndComposite, OrComposite, Not
  • Observers -- CallbackObserver, StatefulStatusChanger, OnEnterObserver, TransitionLogger
  • Filters -- ActiveTransitionFilter, FilterStateByEvent, FilterStateByTransition, FilterStateByFinalState, FilterTransitionByEvent
  • Selectors -- OneOrNoneActiveTransition, ScoreTransition, WeightTransition
  • Mutex -- NullMutex, LockAdapterMutex, MutexFactory
  • Factory -- Factory, SingleProcessDetector, AbstractNamedProcessDetector, StatefulStateNameDetector
  • Utilities -- SetupHelper, StateCollectionMerger
  • Graph -- GraphBuilder
  • Errors -- WrongEventForStateError, LockCanNotBeAcquiredError, DuplicateStateError
  • Interfaces -- All TypeScript interfaces

Examples

A complete working example (order processing with prepayment and postpayment workflows) is available in the finita-example repository.

Architecture

src/
  index.ts                 # Barrel export
  MaybePromise.ts          # MaybePromise<T> = T | Promise<T> utility type
  Event.ts                 # Event implementation
  State.ts                 # State implementation
  Transition.ts            # Transition implementation
  StateCollection.ts       # Named collection of states
  Process.ts               # Process (workflow definition)
  Statemachine.ts          # Runtime state machine
  Dispatcher.ts            # Deferred event dispatcher
  interfaces/              # All TypeScript interfaces
  condition/               # Condition (guard) implementations
  observer/                # Observer implementations
  filter/                  # State and transition filters
  selector/                # Transition selection strategies
  mutex/                   # Locking implementations
  factory/                 # State machine factory pattern
  util/                    # SetupHelper, StateCollectionMerger
  graph/                   # Graph visualization builder
  error/                   # Custom error classes

Security

CI

  • CodeQL -- static analysis for security vulnerabilities (push, PR, weekly)
  • OSV-Scanner -- dependency vulnerability scanning against the OSV database (push, PR, weekly)
  • Dependabot -- automated PRs for dependency and GitHub Actions updates (weekly)

Local (via Lefthook)

  • Gitleaks -- scans staged files for secrets on every commit (auto-skipped if not installed)

Manual checks

pnpm run security:audit    # dependency audit
pnpm run security:secrets  # Scan full repo for secrets (requires gitleaks)

Install Gitleaks: https://github.com/gitleaks/gitleaks#installing

Development

# Install dependencies
pnpm install

# Run tests
pnpm test

# Run tests in watch mode
pnpm run test:watch

# Type check
pnpm run lint

# Build
pnpm run build

License

MIT

About

A full-featured, type-safe finite state machine (FSM) library for TypeScript

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors