Skip to content

StoneCypher/jssm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,535 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jssm 5.141.0

Try the live editor · Documentation · Discord · Issues

Easy. Tiny. Fast. Finite state machines as one-liner strings, for TypeScript and JavaScript. Renders to PNG, SVG, and JPEG. Runs in Node, browsers, and Deno. MIT licensed.

import { sm } from 'jssm';

const TrafficLight = sm`Red -> Green -> Yellow -> Red;`;

That's it. Using it is equally easy:

TrafficLight.state();      // 'Red'
TrafficLight.go('Green');  // true
TrafficLight.state();      // 'Green'

The point of a state machine is to refuse to do things that aren't correct:

TrafficLight.go('Red');    // false  - Green doesn't go to Red, only Yellow
TrafficLight.go('Blue');   // throws - Blue doesn't exist at all

A more involved machine, with main paths, forced paths, and per-state styling, renders to:

A styled four-state traffic light

const TrafficLightWithOff = sm`
  Red 'next' => Green 'next' => Yellow 'next' => Red;
  [Red Yellow Green] ~> Off -> Red;

  flow: left;

  state Red    : { background-color: pink;        corners: rounded; };
  state Yellow : { background-color: lightyellow; corners: rounded; };
  state Green  : { background-color: lightgreen;  corners: rounded; };

  state Off    : {
    background-color : steelblue;
    text-color       : white;
    shape            : octagon;
    linestyle        : dashed;
  };
`;

The same string is the runtime and the diagram. They cannot drift apart.

Try it in the live editor · Documentation · Discord · Issues


Install

npm install jssm

The package ships pure ES6, a CommonJS ES5 bundle, an IIFE for browsers, and TypeScript typings. A Deno build is included. Node 10 or newer.


Visualization

jssm ships with a visualization subpath that renders state machines to SVG using Graphviz (via @viz-js/viz).

import { sm }                  from 'jssm';
import { fsl_to_svg_string }   from 'jssm/viz';

const svg = await fsl_to_svg_string('a -> b;');

The viz subpath is opt-in - importing only from jssm does not pull in @viz-js/viz. See the Visualization doc page for browser, ESM, and IIFE usage patterns.


Command-line interface

jssm ships a CLI for rendering FSL machines to images and other formats.

Installation

npm install -g jssm

This installs three binaries: fsl (the dispatcher), jssm (alias for fsl), and fsl-render (the render plugin).

Render

Render a single machine to SVG (default):

fsl render machine.fsl
# → machine.svg next to input

Specify a format:

fsl render machine.fsl --target=png --width=800
fsl render machine.fsl --target=dot --stdout > machine.dot

Render multiple machines:

fsl render *.fsl --target=svg --out-dir=./diagrams

Pipe FSL via stdin:

cat machine.fsl | fsl render --target=dot | dot -Tpng > out.png

Plugin architecture

Every fsl-<name> executable on PATH is dispatched when you run fsl <name>. Third-party plugins follow the same contract as first-party fsl-render. See notes/superpowers/specs/2026-05-12-fsl-cli-design.md for the contract.

Library API

The same render functions are available programmatically:

import { render, renderSet } from 'jssm/cli';

const result = await render(fslText, { target: 'svg' });
if (result.kind === 'text') console.log(result.content);

Web Components

jssm ships Lit-based web components for use in plain HTML or as a base for framework wrappers.

CDN one-liner (with an import map for @viz-js/viz):

<script type="module" src="https://cdn.jsdelivr.net/npm/jssm/dist/cdn/viz.js"></script>
<jssm-viz fsl="Off -> On -> Off;"></jssm-viz>

npm one-liner:

import 'jssm/wc/viz/define';
// then use <jssm-viz fsl="..."> or its synonym <fsl-viz fsl="..."> anywhere

Full documentation: src/doc_md/WebComponents.md.


60-second tour

Actions let a machine advance without the caller knowing the next state:

const Light = sm`Red 'next' -> Green 'next' -> Yellow 'next' -> Red;`;

Light.action('next');  // true
Light.state();         // 'Green'
Light.action('next');  // true
Light.state();         // 'Yellow'

Three arrow types distinguish kinds of transition:

const Light = sm`
  Red => Green => Yellow => Red;     // => main path
  [Red Yellow Green] ~> Off -> Red;  // ~> forced, -> legal
`;

-> is a legal transition. => is a legal transition that is also part of the main path. ~> is a transition that requires force_transition - useful for emergency stops, resets, and other rarities.

Hooks observe and gate transitions:

const m = sm`Red 'next' -> Green 'next' -> Yellow 'next' -> Red;`
  .hook('Red', 'Green', () => console.log('GO'))            // specific edge
  .hook_entry('Red', () => console.log('STOP'))             // entering a state
  .hook_action('Yellow', 'Red', 'next', () => allowed());   // gate a specific action; return false to block

Pre-hooks fire before the state changes and may return false to refuse the transition. Post-hooks fire after. Four *_everything hooks (hook_pre_everything, hook_everything, hook_pre_post_everything, hook_post_everything) bracket the entire pipeline if you want a single observation point.

Refusals and errors are deliberately different. An illegal transition returns false. An unknown state throws. Branching code can rely on the distinction.


Why jssm

The big win: most state-machine libraries make you write a gargantuan JSON document, or call a builder API a few dozen times, to define a single machine. jssm machines are short, readable, arrow-driven strings - so they are easy to write, easy to read, easy to debug, and easy to share.

That decision shows up everywhere downstream:

  • A DSL with features other state-machine libraries don't have. Three arrow types distinguish legal, main-path, and forced transitions. Array notation collapses repeated edges - [Red Yellow Green] ~> Off replaces three lines. Named actions, per-state styling, named edges, validators, and live visualization all live in the same string the runtime parses.

  • Definition strings stay tiny, which makes machines easy to debug. The traffic light is one line; the full eight-step ATM walkthrough is thirty. Small enough to read top-to-bottom, diff in code review, paste into a bug report, or drop into the live editor for a rendered diagram you can step through.

  • Fast. Tens of millions of transitions per second on commodity hardware. See src/buildjs/benchmark.cjs or run npm run benny against your own machine.

  • More thoroughly tested than any other JavaScript state-machine library. 6,450 tests at 100.0% line coverage (report), plus fuzz testing via fast-check, with parser test data across ten natural languages and Emoji.


Documentation


API at a glance

Method Purpose
sm`...` Build a machine from DSL
.state() The current state
.transition(state) Move to a state. Returns false if illegal, throws if unknown.
.force_transition(state) Move to a state across a ~> forced edge
.action(name) Trigger a named action. The next state is derived from the current state.
.valid_transition(state) · .valid_action(name) Test whether a transition or action is legal from the current state, without taking it
.hook(from, to, fn) Run on a specific edge. Pre-hook; return false to block.
.hook_entry(state, fn) · .hook_exit(state, fn) Run when entering or leaving a state
.hook_action(from, to, action, fn) Run when a named action causes a specific edge
.hook_pre_everything(fn) · .hook_everything(fn) Bracket the pre-hook pipeline
.hook_pre_post_everything(fn) · .hook_post_everything(fn) Bracket the post-hook pipeline

The full surface - including history, validators, factories, data, and the graph-introspection methods - is in the generated API docs.


Status

In production use since May 2017. Current series is 5.x and the DSL is stable; the runtime API has been additive for several years. MIT licensed end to end. Test data and parser cases are included for English, German, French, Spanish, Hebrew, Russian, Ukrainian, Belarusian, Bengali, Portuguese, and Emoji.


Community

Discord community

Questions, design discussions, bug reports, and "what would you do for X?" are all welcome on Discord. Issues that need a paper trail go in the issue tracker.


Comparisons

A direct, head-to-head comparison with the other actively-maintained JS state machine libraries - XState, Stately.js, Finity, machina.js, and others - is in progress and will live in FeatureComparison.md.

A list of related projects, without commentary, is at the bottom of that file.


Contributing

Issues and PRs are welcome. The cheapest useful contribution is a language test case: open a PR with english.json translated into your language. Translating traffic_light.fsl into a separate repo and publishing it goes a step further.


Acknowledgements

Michael Morgan has debated significant sections of the notation, invented several concepts and operators, helped with the parser and system nomenclature, and published the first non-author FSL machine. Vat Raghavan participated extensively in language design and implemented several features. Forest Belton provided guidance, bugfixes, and parser commentary. Jordan Harbrand suggested two interesting features and gave strong feedback on the initial tutorial draft.

Translation contributors:

If your contribution is missing here, please open an issue.



Stats, coverage, and badges

6,450 tests, run 57,237 times.

  • 5,937 specs with 100.0% coverage
  • 513 fuzz tests with 3.4% coverage
  • 5,555 TypeScript lines - 1.2 tests per line, 10.3 generated tests per line

Actions Status NPM version NPM downloads License Coveralls status Open issues

About

Fast, easy Javascript finite state machines with visualizations; enjoy a one liner FSM instead of pages. MIT; Typescripted; 100% test coverage. Implements the FSL language.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors