{"id":23280989,"url":"https://github.com/nicholaswmin/fsm","last_synced_at":"2026-02-25T17:36:04.319Z","repository":{"id":258795616,"uuid":"873281873","full_name":"nicholaswmin/fsm","owner":"nicholaswmin","description":"stupid simple, finite-state machine 􀍟","archived":false,"fork":false,"pushed_at":"2025-01-06T01:57:01.000Z","size":61,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-06T20:56:07.373Z","etag":null,"topics":["automata-theory","declarative-programming","finite-state-machine"],"latest_commit_sha":null,"homepage":"https://nicholaswmin.github.io/fsm/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nicholaswmin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-10-15T22:46:11.000Z","updated_at":"2025-08-16T19:42:00.000Z","dependencies_parsed_at":"2025-05-27T04:11:17.157Z","dependency_job_id":"a63f8c57-ce95-450d-9f7a-d9b21863ac69","html_url":"https://github.com/nicholaswmin/fsm","commit_stats":null,"previous_names":["nicholaswmin/fsm"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/nicholaswmin/fsm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Ffsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Ffsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Ffsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Ffsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nicholaswmin","download_url":"https://codeload.github.com/nicholaswmin/fsm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicholaswmin%2Ffsm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29832966,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T17:17:09.781Z","status":"ssl_error","status_checked_at":"2026-02-25T17:16:50.421Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automata-theory","declarative-programming","finite-state-machine"],"created_at":"2024-12-19T23:39:59.054Z","updated_at":"2026-02-25T17:36:04.289Z","avatar_url":"https://github.com/nicholaswmin.png","language":"JavaScript","readme":"[![tests][testb]][tests] [![ccovt][cocov]](#tests)\n\n# fsm\n\n\u003e A [finite-state machine][fsm]  \n\u003e\n\u003e ... is an abstract machine that can be in one of a finite number of states.    \n\u003e The change from one `state` to another is called a `transition`.\n\nThis package constructs simple FSM's which express their logic \ndeclaratively \u0026 safely.[^1].  \nWe use this internally on production.\n  \n`~1KB`, zero dependencies, [opinionated][dgoals], and tested throught the nose;  \nit eschews fancy features for robustness, ease of use and zero-maintenace  \neven across major Node.js updates.\n\n### Basic\n\n- [Install](#install)\n- [Example](#example)\n  - [Initialisation](#initialisation)\n  - [Transition](#transition)\n  - [Current state](#current-state)\n\n### Extras\n\n- [Hooks](#hook-methods)\n- [Transition cancellations](#transition-cancellations)\n- [Asynchronous transitions](#asynchronous-transitions)\n- [Serialising to JSON](#serialising-to-json)\n- [As a mixin](#fsm-as-a-mixin)\n\n### API\n\n- [`fsm(states, hooks)`](#fsmstates-hooks)\n- [`fsm(json, hooks)`](#fsmjson-hooks)\n- [`fsm.state`](#fsmstate)\n\n### Meta\n\n- [Tests](#tests)\n- [Publishing](#publishing)\n- [Authors](#authors)\n- [License](#license)\n\n## Install \n\n```bash\nnpm i @nicholaswmin/fsm\n```\n\n## Example\n\n\u003e A [turnstile][turn] gate that opens with a coin.  \n\u003e When opened you can push through it; after which it closes again:\n\n```js\nimport { fsm } from '@nicholaswmin/fsm'\n\n// define states \u0026 transitions:\n\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n})\n\n// transition: coin\nturnstile.coin()\n// state: opened\n\n// transition: push\nturnstile.push()\n// state: closed\n\nconsole.log(turnstile.state)\n// \"closed\"\n```\n\nEach step is broken down below.\n\n## Initialisation\n\nAn FSM with 2 possible `states`, each listing a single `transition`:\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n})\n```\n\n- `state: closed`: allows `transition: coin` which sets: `state: opened`\n- `state: opened`: allows `transition: push` which sets: `state: closed`\n\n## Transition\n\nA `transition` can be called as a method:\n\n```js\nconst turnstile = fsm({\n  // defined 'coin' transition\n  closed: { coin: 'opened' },\n\n  // defined 'push' transition\n  opened: { push: 'closed' }\n})\n\nturnstile.coin()\n// state: opened\n\nturnstile.push()\n// state: closed\n```\n\nThe current `state` must list the transition, otherwise an `Error` is thrown:\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n})\n\nturnstile.push()\n// TransitionError: \n// current state: \"closed\" has no transition: \"push\"\n```\n\n## Current state\n\nThe `fsm.state` property indicates the current `state`:\n\n```js\nconst turnstile = fsm({\n  closed: { foo: 'opened' },\n  opened: { bar: 'closed' }\n})\n\nconsole.log(turnstile.state)\n// \"closed\"\n```\n\n## Hook methods\n\nHooks are optional methods, called at specific transition phases.  \n\nThey must be set as `hooks` methods; an `Object` passed as 2nd argument of \n`fsm(states, hooks)`.\n\n### Transition hooks\n\nCalled *before* the state is changed \u0026 can optionally \n[cancel a transition](#transition-cancellations).\n\nMust be named: `on\u003ctransition-name\u003e`, where `\u003ctransition-name\u003e` is an actual \n`transition` name.\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n}, {\n  onCoin: function() {\n    console.log('got a coin')\n  },\n  \n  onPush: function() {\n    console.log('got pushed')\n  }\n})\n\nturnstile.coin()\n// \"got a coin\"\n\nturnstile.push()\n// \"got pushed\"\n```\n\n### State hooks\n\nCalled *after* the state is changed.\n\nMust be named: `on\u003cstate-name\u003e`, where `\u003cstate-name\u003e` is an actual `state` name.\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n}, {\n  onOpened: function() {\n    console.log('its open')\n  },\n\n  onClosed: function() {\n    console.log('its closed')\n  }\n})\n\nturnstile.coin()\n// \"its open\"\n\nturnstile.push()\n// \"its closed\"\n```\n\n### Hook arguments \n\nTransition methods can pass arguments to relevant hooks, assumed to be\nvariadic: [^2]\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n}, {\n  onCoin(one, two) {\n    return console.log(one, two)\n  }\n})\n\nturnstile.coin('foo', 'bar')\n// foo, bar\n```\n\n## Transition cancellations\n\n[Transition hooks](#transition-hooks) can cancel the transition by returning \n`false`.\n\nCancelled transitions don't change the *state* nor call any \n[state hooks](#state-hooks).\n\n\u003e example: cancel transition to `state: opened` if the coin is less than `50c`\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n}, {\n  onCoin(coin) {\n    return coin \u003e= 50\n  }\n})\n\nturnstile.coin(30)\n// state: closed\n\n// state still \"closed\",\n\n// add more money?\n\nturnstile.coin(50)\n// state: opened\n```\n\n\u003e note: must explicitly return `false`, not just [`falsy`][falsy].\n\n## Asynchronous transitions\n\nMark relevant hooks as [`async`][async] and [`await`][await] the transition:\n\n```js\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n}, {\n  async onCoin(coins) {\n    // simulate something async\n    await new Promise(res =\u003e setTimeout(res.bind(null, true), 2000))\n  }\n})\n\nawait turnstile.coin()\n// 2 seconds pass ...\n\n// state: opened\n```\n\n## Serialising to JSON\n\nSimply use [`JSON.stringify`][JSON.stringify]:\n\n```js\nconst hooks = {\n  onCoin() { console.log('got a coin') }\n  onPush() { console.log('pushed ...') }\n}\n\nconst turnstile = fsm({\n  closed: { coin: 'opened' },\n  opened: { push: 'closed' }\n}, hooks)\n\nturnstile.coin()\n// got a coin\n\nconst json = JSON.stringify(turnstile)\n```\n\n... then revive with:\n\n```js\nconst revived = fsm(json, hooks)\n// state: opened \n\nrevived.push()\n// pushed ..\n// state: closed\n```\n\n\u003e note: `hooks` are not serialised so they must be passed again when reviving, \n\u003e as shown above.\n\n## FSM as a `mixin`\n\nPassing an `Object` as `hooks` to: `fsm(states, hooks)` assigns FSM behaviour \non the provided object.\n\nUseful in cases where an object must function as an FSM, in addition to some \nother behaviour.[^3]\n\n\u003e example: A `Turnstile` functioning as both an [`EventEmitter`][ee] \u0026 an `FSM`\n\n```js\nclass Turnstile extends EventEmitter {\n  constructor() {\n    super()\n\n    fsm({\n      closed: { coin: 'opened' },\n      opened: { push: 'closed' }\n    }, this)\n  }\n}\n\nconst turnstile = new Turnstile()\n\n// works as EventEmitter.\n\nturnstile.emit('foo')\n\n// works as an FSM as well.\n\nturnstile.coin()\n\n// state: opened\n```\n\n\u003e this concept is similar to a [`mixin`][mixin].\n\n\n## API\n\n### `fsm(states, hooks)`\n\nConstruct an `FSM`\n\n| name     | type     | desc.                           | default  |\n|----------|----------|---------------------------------|----------|\n| `states` | `object` | a [state-transition table][stt] | required |\n| `hooks`  | `object` | implements transition hooks     | `this`   |\n\n`states` must have the following abstract shape:\n\n```js\nstate: { \n  transition: 'next-state',\n  transition: 'next-state' \n},\nstate: { transition: 'next-state' }\n```\n\n- The 1st state in `states` is set as the *initial* state.    \n- Each `state` can list zero, one or many transitions.   \n- The `next-state` must exist as a `state`.  \n\n### `fsm(json, hooks)` \n\nRevive an instance from it's [JSON][json].   \n\n#### Arguments\n\n| name     | type     | desc.                         | default  |\n|----------|----------|-------------------------------|----------|\n| `json`   | `string` | `JSON.stringify(fsm)` result  | required |\n\n### `fsm.state` \n\nThe current `state`. Read-only.    \n\n| name     | type     | default       |\n|----------|----------|---------------|\n| `state`  | `string` | current state | \n\n## Tests\n\n\u003e unit tests:\n\n```bash\nnode --run test\n```\n\n\u003e these tests *require* that certain [coverage thresholds][ccov-thresh] are met.\n\n## Contributing\n\n[Contribution Guide][contr-guide]\n\n## Publishing \n\n- collect all changes in a pull-request\n- merge to `main` when all ok\n\nthen from a clean `main`:\n\n```bash\n# list current releases\ngh release list\n``` \n\nChoose the next [Semver][semver], i.e: `1.3.1`, then:\n\n```bash\ngh release create 1.3.1\n```\n\n\u003e **note:** dont prefix releases/tags with `v`, just `x.x.x` is enough.\n\nThe Github release triggers the [`npm:publish workflow`][npmpubworkflow],  \npublishing the new version to [npm][npmproj].  \n\nIt then attaches a [Build Provenance][provenance] statement on the \n[Release Notes][rel-notes].\n\nThat's all.\n  \n## Authors\n\n[N.Kyriakides; @nicholaswmin][author]\n\n## License \n\nThe [MIT License][license]\n\n### Footnotes \n\n[^1]: A finite-state machine can only exist in *one* and *always-valid* state.  \n      It requires declaring all possible states \u0026 the rules under which it can \n      transition from one state to another.  \n\n[^2]: A function that accepts an infinite number of arguments.   \n      Also called: functions of *\"n-arity\"* where \"arity\" = number of arguments. \n      \n      i.e: nullary: `f = () =\u003e {}`, unary: `f = x =\u003e {}`,\n      binary: `f = (x, y) =\u003e {}`, ternary `f = (a,b,c) =\u003e {}`, \n      n-ary/variadic: `f = (...args) =\u003e {}`\n      \n[^3]: FSMs are rare but perfect candidates for *inheritance* because usually\n      something `is-an` FSM.  \n      However, Javascript doesn't support *multiple inheritance* so inheriting \n      `FSM` would create issues when inheriting other behaviours.\n\n      *Composition* is also problematic since it namespaces the behaviour, \n      causing it to lose it's expressiveness.  \n      i.e `light.fsm.turnOn` feels misplaced compared to `light.turnOn`.\n      \n\n[testb]: https://github.com/nicholaswmin/fsm/actions/workflows/tests:unit.yml/badge.svg\n[tests]: https://github.com/nicholaswmin/fsm/actions/workflows/tests:unit.yml\n[cocov]: https://img.shields.io/badge/coverage-%3E%2095%25-blue\n[ccovt]: https://github.com/nicholaswmin/fsm/blob/6db5c1c5ede6edb15cb1b8431a4716163a7410d4/package.json#L11\n\n[turn]: https://en.wikipedia.org/wiki/Finite-state_machine#Example:_coin-operated_turnstile\n[fsm]: https://en.wikipedia.org/wiki/Finite-state_machine\n[stt]: https://en.wikipedia.org/wiki/State-transition_table\n[dfsm]: https://en.wikipedia.org/wiki/Deterministic_finite_automaton\n[automata]: https://en.wikipedia.org/wiki/Automata_theory\n[mixin]: https://developer.mozilla.org/en-US/docs/Glossary/Mixin\n[alternatives]: https://www.npmjs.com/search?q=fsm\n[async]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function\n[await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await\n[promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise\n[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify\n[json]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON\n[mixin]: https://developer.mozilla.org/en-US/docs/Glossary/Mixin\n[falsy]: https://developer.mozilla.org/en-US/docs/Glossary/Falsy\n[ee]: https://nodejs.org/docs/latest/api/events.html#class-eventemitter\n\n[npmproj]: https://www.npmjs.com/package/@nicholaswmin/fsm\n[semver]: https://semver.org/\n[npmpubworkflow]: https://github.com/nicholaswmin/fsm/actions/workflows/npm:publish.yml\n[provenance]: https://docs.npmjs.com/generating-provenance-statements/\n[rel-notes]: https://github.com/nicholaswmin/fsm/releases/latest\n\n[prov]: https://search.sigstore.dev/?logIndex=136020643\n[contr-guide]: https://github.com/nicholaswmin/fsm/blob/main/.github/CONTRIBUTING.md\n[ccov-thresh]: https://github.com/nicholaswmin/fsm/blob/main/package.json#L11\n[dgoals]: https://github.com/nicholaswmin/fsm/blob/main/.github/CONTRIBUTING.md#design-goals\n[author]: https://github.com/nicholaswmin\n[license]: https://raw.githubusercontent.com/nicholaswmin/fsm/refs/heads/main/LICENSE\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicholaswmin%2Ffsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnicholaswmin%2Ffsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicholaswmin%2Ffsm/lists"}