{"id":33180934,"url":"https://github.com/shuckster/statebot","last_synced_at":"2026-01-15T01:47:29.507Z","repository":{"id":41531045,"uuid":"255367877","full_name":"shuckster/statebot","owner":"shuckster","description":"Describe the states and allowed transitions of a program using a flowchart-like syntax. Switch to states directly, or by wiring-up events. Statebot is an FSM.","archived":false,"fork":false,"pushed_at":"2024-10-12T17:16:18.000Z","size":3496,"stargazers_count":29,"open_issues_count":5,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-11-20T21:07:29.788Z","etag":null,"topics":["code-organization","emitting-events","fsm","state-machine","state-management"],"latest_commit_sha":null,"homepage":"https://shuckster.github.io/statebot/","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/shuckster.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-13T15:34:38.000Z","updated_at":"2024-12-07T03:23:34.000Z","dependencies_parsed_at":"2024-05-02T15:07:14.245Z","dependency_job_id":null,"html_url":"https://github.com/shuckster/statebot","commit_stats":{"total_commits":393,"total_committers":2,"mean_commits":196.5,"dds":"0.16793893129770987","last_synced_commit":"70d84be657c0d45a6942804a55a2d234482b03fd"},"previous_names":[],"tags_count":53,"template":false,"template_full_name":null,"purl":"pkg:github/shuckster/statebot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuckster%2Fstatebot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuckster%2Fstatebot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuckster%2Fstatebot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuckster%2Fstatebot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shuckster","download_url":"https://codeload.github.com/shuckster/statebot/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shuckster%2Fstatebot/sbom","scorecard":{"id":821448,"data":{"date":"2025-08-11","repo":{"name":"github.com/shuckster/statebot","commit":"d8a58dfe35e18d9756dfe9846952145392bdd6ea"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/24 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 6 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"18 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-wrw9-m778-g6mc","Warn: Project is vulnerable to: GHSA-pp7h-53gx-mx7r","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-434g-2637-qmqr","Warn: Project is vulnerable to: GHSA-49q7-c7j4-3p7m","Warn: Project is vulnerable to: GHSA-977x-g7h5-7qgw","Warn: Project is vulnerable to: GHSA-f7q4-pwc6-w24p","Warn: Project is vulnerable to: GHSA-fc9h-whq2-v747","Warn: Project is vulnerable to: GHSA-vjh7-7g9h-fjfh","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-h7cp-r72f-jxh6","Warn: Project is vulnerable to: GHSA-v62p-rq8g-8h59","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-x6fg-f45m-jf5q","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-23T15:42:41.895Z","repository_id":41531045,"created_at":"2025-08-23T15:42:41.895Z","updated_at":"2025-08-23T15:42:41.895Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28441031,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-15T00:55:22.719Z","status":"ssl_error","status_checked_at":"2026-01-15T00:55:20.945Z","response_time":107,"last_error":"SSL_read: 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":["code-organization","emitting-events","fsm","state-machine","state-management"],"created_at":"2025-11-16T04:00:23.294Z","updated_at":"2026-01-15T01:47:29.432Z","avatar_url":"https://github.com/shuckster.png","language":"JavaScript","readme":"\u003ch1 align=\"center\"\u003e\n  statebot 🤖\n\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/shuckster/statebot/blob/master/LICENSE\"\u003e\n    \u003cimg\n      alt=\"MIT license\"\n      src=\"https://img.shields.io/npm/l/statebot?style=plastic\"\n    /\u003e\u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/result?p=statebot\"\u003e\n    \u003cimg\n      alt=\"npm bundle size\"\n      src=\"https://img.shields.io/bundlephobia/minzip/statebot?style=plastic\"\n    /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/statebot\"\u003e\n    \u003cimg\n      alt=\"Version\"\n      src=\"https://img.shields.io/npm/v/statebot?style=plastic\"\n    /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nDescribe the states and allowed transitions of a program using a flowchart-like syntax. Switch to states directly, or by wiring-up events. Statebot is an [FSM](https://en.wikipedia.org/wiki/Finite-state_machine).\n\n```js\nimport { Statebot } from 'statebot'\n\nconst machine = Statebot('traffic-lights', {\n  chart: `\n    go -\u003e\n      prepare-to-stop -\u003e\n      stop\n\n    // ...gotta keep that traffic flowing\n    stop -\u003e\n      prepare-to-go -\u003e\n      go\n  `\n})\n\nmachine.performTransitions({\n  'stop -\u003e prepare-to-go -\u003e go':   { on: 'timer' },\n  'go -\u003e prepare-to-stop -\u003e stop': { on: 'timer' },\n})\n\nmachine.onEvent('timer', () =\u003e {\n  redrawTrafficLights()\n})\n\nfunction redrawTrafficLights() {\n  machine.inState({\n    'stop': () =\u003e\n      console.log('Draw red light'),\n\n    'prepare-to-go': () =\u003e\n      console.log('Draw red + yellow lights'),\n\n    'go': () =\u003e\n      console.log('Draw green light'),\n\n    'prepare-to-stop': () =\u003e\n      console.log('Draw yellow light'),\n  })\n}\n\nsetInterval(machine.Emit('timer'), 2000)\n```\n\n[CodeSandbox](https://codesandbox.io/s/statebot-traffic-lights-kin5y) of the above example.\n\n[Since v3.1.0](https://github.com/shuckster/statebot/blob/master/CHANGELOG.md#310---2023-04-13), [Mermaid state-diagram](https://mermaid.js.org/syntax/stateDiagram.html) support:\n\n```mermaid\n---\ntitle: Traffic Lights\n---\nstateDiagram\ndirection LR\n  go --\u003e prepareToStop\n    prepareToStop --\u003e stop\n\n  %% ...gotta keep that traffic flowing\n  stop --\u003e prepareToGo\n    prepareToGo --\u003e go\n```\n\n```js\nimport { Statebot, mermaid } from 'statebot'\n\nconst machine = Statebot('traffic-lights', {\n  chart: mermaid`\n    stateDiagram\n    direction LR\n      go --\u003e prepareToStop\n        prepareToStop --\u003e stop\n\n      %% ...gotta keep that traffic flowing\n      stop --\u003e prepareToGo\n        prepareToGo --\u003e go\n  `\n})\n```\n\nIt's around 5K gzipped, runs in Node and the browser, and is a [shell-script](https://github.com/shuckster/statebot-sh/) too.\n\n- [Full documentation](https://shuckster.github.io/statebot/)\n- [Github Repo](https://github.com/shuckster/statebot)\n\nThere are Hooks for these frameworks, too:\n\n- [React Hooks](https://github.com/shuckster/statebot/tree/master/hooks/react)\n- [Mithril Hooks](https://github.com/shuckster/statebot/tree/master/hooks/mithril)\n\nThere is a lot of prior-art out there, most notably [XState](https://github.com/davidkpiano/xstate) by David Khourshid, but I hope Statebot can offer a small contribution in the field of writing code that is easier to understand six-months after it has been written.\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n  - [React example](#react-example)\n  - [Node.js example](#nodejs-example)\n- [Events](#events)\n- [Passing data around](#passing-data-around)\n- [Testing](#testing)\n- [Chart Syntax](#chart-syntax)\n  - [Examples of real charts](#examples-of-real-charts)\n- [Why?](#why)\n- [Contributing](#contributing)\n  - [Credits](#credits)\n  - [License](#license)\n\n# Installation\n\n```sh\nnpm i statebot\n```\n\n```js\n\u003cscript src=\"https://unpkg.com/statebot@3.1.3/dist/browser/statebot.min.js\"\u003e\u003c/script\u003e\n```\n\n## Quick Start:\n\n### React example:\n\n(You can play around with this in a [CodeSandbox](https://codesandbox.io/s/statebot-react-ot3xe?file=/src/Loader.js).)\n\n```jsx\nimport React, { useState, useEffect } from 'react'\nimport { Statebot } from 'statebot'\n\n// Statebot is framework agnostic. To use it with React,\n// you might use something like this 3-line Hook:\nfunction useStatebot(bot) {\n  const [state, setState] = useState(bot.currentState())\n  useEffect(() =\u003e bot.onSwitched(setState), [bot])\n  return state\n}\n\nconst loader$bot = Statebot('loader', {\n  chart: `\n    idle -\u003e\n      loading -\u003e (loaded | failed) -\u003e\n      idle\n  `\n})\n\nloader$bot.performTransitions(({ Emit }) =\u003e ({\n  'idle -\u003e loading': {\n    on: 'start-loading',\n    then: () =\u003e setTimeout(Emit('success'), 1000)\n  },\n  'loading -\u003e loaded': {\n    on: 'success'\n  },\n  'loading -\u003e failed': {\n    on: 'error'\n  }\n}))\n\nconst { Enter, Emit, inState } = loader$bot\n\nfunction LoadingButton() {\n  const state = useStatebot(loader$bot)\n\n  return (\n    \u003cbutton\n      className={state}\n      onClick={Emit('start-loading')}\n      disabled={inState('loading')}\n    \u003e\n      {inState({\n        'idle': 'Load',\n        'loading': 'Please wait...',\n        'loaded': 'Done!',\n      })}\n      ({state})\n    \u003c/button\u003e\n  )\n}\n```\n\n### Node.js example:\n\n```js\nconst { Statebot } = require('statebot')\n\n// Describe states + transitions\nconst machine = Statebot('promise-like', {\n  chart: `\n\n    idle -\u003e\n      // This one behaves a bit like a Promise\n      pending -\u003e\n        (resolved | rejected) -\u003e\n      done\n\n  `,\n  startIn: 'pending'\n})\n\n// Handle events...\nmachine.performTransitions({\n  'pending -\u003e resolved': {\n    on: 'success'\n  }\n})\n\n// ...and/or transitions\nmachine.onTransitions({\n  'pending -\u003e resolved | rejected': () =\u003e {\n    console.log('Sweet!')\n  }\n})\n\nmachine.onExiting('pending', toState =\u003e {\n  console.log(`Off we go to: ${toState}`)\n})\n\nmachine.canTransitionTo('done')\n// false\n\nmachine.statesAvailableFromHere()\n// [\"resolved\", \"rejected\"]\n\nmachine.emit('success')\n// \"Off we go to: resolved\"\n// \"Sweet!\"\n```\n\n# Events\n\n**Statebot** creates state-machines from `charts`, and we can switch states `on` events using `performTransitions`:\n\n```js\nmachine.performTransitions({\n  'pending -\u003e resolved': {\n    on: 'data-loaded'\n  }\n})\n\n// ^ This API is designed to read like this:\n//   machine, perform transition \"pending to\n//   resolved\" on \"data-loaded\".\n```\n\nLet's do a little more:\n\n```js\nmachine.performTransitions({\n  'pending -\u003e rejected': {\n    on: ['data-error', 'timeout'],\n    then: () =\u003e {\n      console.warn('Did something happen?')\n    }\n  },\n\n// ^ We can run something after a transition\n//   happens with \"then\". Notice this will\n//   happen after the \"data-error\" OR\n//   \"timeout\" events.\n\n  'resolved | rejected -\u003e done': {\n    on: 'finished'\n  }\n\n// ^ We can configure lots of transitions inside\n//   one `performTransitions`. Here's one that\n//   will switch from \"resolved to done\" OR\n//   \"rejected to done\" when the \"finished\"\n//   event is emitted.\n\n})\n\n// In this API, when events are emitted they\n// can pass arguments to the \"then\" method.\n\n// See the section below on \"Passing data around\".\n```\n\nWe can also do stuff when states switch with `onTransitions`:\n\n```js\nmachine.onTransitions({\n  'pending -\u003e resolved': function () {\n    console.log('Everything went lovely...')\n    machine.enter('done')\n  },\n\n  'pending -\u003e rejected': function () {\n    console.warn('That did not go so well...')\n    machine.enter('done')\n  },\n\n  'resolved | rejected -\u003e done': function () {\n    console.log('All finished')\n  }\n})\n```\n\nLet's do a little more:\n\n```js\nmachine.onTransitions(({ emit, Emit }) =\u003e ({\n  'idle -\u003e pending': function () {\n\n// ^ This API is designed to read like this:\n//   machine, on transition \"idle to pending\",\n//   run a callback.\n\n    getSomeData().then(\n      (...args) =\u003e emit('data-loaded', ...args)\n    )\n\n// ^ emit() or Emit()? Which one to use? Maybe\n//   you can infer the different meanings from\n//   the .catch() of this Promise:\n\n    .catch(Emit('data-error'))\n\n// ^ Got it? Emit() is shorthand for:\n//     (...args) =\u003e emit('event', ...args)\n//\n//   So emit() fires immediately, and Emit()\n//   generates an emitter-method.\n\n  }\n}))\n\n// In this API, the state-switching functions\n// enter() and Enter() can pass arguments to\n// these callbacks.\n\n// See the section below on \"Passing data around\".\n```\n\nBoth `performTransitions` and `onTransitions` take objects or functions that return objects in order to configure them.\n\nObject:\n\n```js\nmachine.onTransitions({\n  'idle -\u003e pending': // etc...\n```\n\nFunction:\n\n```js\nmachine.onTransitions(({ emit, enter, Emit, Enter }) =\u003e ({\n  'idle -\u003e pending': // etc...\n```\n\nIn the case of a function, a single argument is passed-in: An object containing helpers for **emitting events** and **entering states**. In the above example we're pulling-in the helpers `emit` and `enter`, and also their corresponding factories: `Emit` and `Enter`.\n\nOf course, you don't have to use an \"implicit return\":\n\n```js\nmachine.onTransitions(({ emit, Emit, enter, Enter }) =\u003e {\n  // Setup, closure gubbins and so on...\n\n  return {\n    'idle -\u003e pending': // etc...\n  }\n})\n```\n\n`performTransitions` hitches onto events, and `onTransitions` hitches onto state-transitions.\n\nA Statebot FSM can have as many `hitchers` as you like, or none at all.\n\nIn any case, once an FSM is configured we are sometimes only interested in the state we are currently `in`, about to `exit`, or about to `enter`. There are hitchers for those, too:\n\n```js\nmachine.onExiting('pending', toState =\u003e {\n  console.log('we are heading to:', toState)\n})\n\nmachine.onEntered('done', fromState =\u003e {\n  console.log('we came from:', fromState)\n})\n\nmachine.currentState()\nmachine.previousState()\n```\n\nYou can use the following snippet to tinker with the examples above:\n\n```js\nfunction getSomeData() {\n  return new Promise(\n    (resolve, reject) =\u003e {\n      setTimeout(resolve, 1000)\n\n      // Randomly reject\n      setTimeout(reject,\n        500 + Math.round(Math.random() * 750)\n      )\n    }\n  )\n}\n\n// Randomly timeout\nsetTimeout(() =\u003e machine.emit('timeout', true),\n  750 + Math.round(Math.random() * 750)\n)\n\nmachine.enter('pending')\n```\n\n# Passing data around\n\nEvents can pass data to callbacks using `emit`:\n\n```js\nmachine.performTransitions({\n  'pending -\u003e resolved': {\n    on: ['data-loaded'], // event name(s)\n    then: (...args) =\u003e {\n      console.log('Received:', args)\n    }\n  }\n})\n\nmachine.emit('data-loaded', 1, 2, 3)\n\n// Console output:\n// \u003e Received: [1, 2, 3]\n```\n\nThe state-switching method `enter` can pass data, too:\n\n```js\nmachine.onTransitions({\n  'idle -\u003e pending': (...args) =\u003e {\n    console.log('onTransitions:', args)\n  }\n})\n\nmachine.onEntering('pending',\n  (fromState, ...args) =\u003e {\n    console.log('onEntering:', args)\n  }\n)\n\nmachine.onExited('pending',\n  (toState, ...args) =\u003e {\n    console.log('onExited:', args)\n  }\n)\n\nmachine.enter('pending', 'a', 'b', 'c')\nmachine.enter('resolved', 3, 2, 1)\n\n// Console output:\n// \u003e onEntering: [\"a\", \"b\", \"c\"]\n// \u003e onTransitions: [\"a\", \"b\", \"c\"]\n// \u003e onExited: [3, 2, 1]\n```\n\n# Logging\n\nA Statebot FSM is a pretty noisy thing by default.\n\nYou can tone-it down using the `logLevel` argument in its options:\n\n```js\nconst machine = Statebot('example', {\n  // ...\n  logLevel: 2 // Everything except console.info()\n})\n```\n\n- A zero `0` here means silence\n- One `1` prints `console.warn()`'s\n- Two `2` prints warnings, plus `console.log()` + `console.table()`\n- Three `3` prints all the above, plus `console.info()`\n\n`3` is the default. Argument type-errors will always `throw`.\n\n# Testing\n\n`assertRoute` can be used to test if an FSM traced a particular route:\n\n```js\nconst { assertRoute } = require('statebot/assert')\n\nassertRoute(\n  machine, 'pending -\u003e resolved -\u003e done',\n  {\n    description: 'Data loaded with no issues',\n    fromState: 'idle',\n    timeoutInMs: 1000 * 20,\n    permittedDeviations: 0\n  }\n)\n.then(() =\u003e console.log('Assertion passed!'))\n.catch(err =\u003e console.error(`Hot fudge: ${err}`))\n\nmachine.enter('idle')\n```\n\nAs you can see, it returns a `Promise` that you can use with an assertion-library.\n\nThe method itself produces output using `console.table`:\n\n```bash\n[example] aId\u003c1\u003e: Data loaded with no issues: [FAILED]\n┌─────────┬────────────┬────────────┬───────────────────────┬──────────────────┐\n│ (index) │   states   │  expected  │         info          │       took       │\n├─────────┼────────────┼────────────┼───────────────────────┼──────────────────┤\n│    0    │  'pending' │ 'pending'  │ 'OKAY               ' │ '          2 ms' │\n│    1    │ 'rejected' │ 'resolved' │ 'WRONG STATE        ' │ '       0.73 s ' │\n│    2    │ 'rejected' │ 'resolved' │ 'TOO MANY DEVIATIONS' │ '              ' │\n│    3    │ 'rejected' │  '(done)'  │ 'TOO MANY DEVIATIONS' │ '              ' │\n│    4    │     ''     │     ''     │ '                   ' │ 'TOTAL: 0.74 s ' │\n└─────────┴────────────┴────────────┴───────────────────────┴──────────────────┘\n```\n\n`aId\u003c1\u003e` means `assertRoute` has run once so far.\n\nYou can also check if a certain route can be followed with `routeIsPossible`:\n\n```js\nconst { routeIsPossible } = require('statebot/assert')\n\nrouteIsPossible(machine, 'pending -\u003e resolved -\u003e pending')\n// false\n```\n\n# Chart Syntax\n\nStatebot charts are just strings, or arrays of strings:\n\n```js\nvar oneLiner = '-\u003e idle -\u003e done'\nvar multiLiner = [\n  '-\u003e idle',\n  'idle -\u003e done'\n]\n```\n\nWe can use [Template Literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) to make more readable charts in modern JavaScript:\n\n```js\nconst chart = `\n  -\u003e idle\n  idle -\u003e done\n`\n```\n\nCharts list all _states_ and the allowed _transitions_ between them using `-\u003e`.\n\n```js\nconst fsm = Statebot('just a name', {\n  chart: `\n\n    -\u003e idle\n    idle -\u003e done\n\n  `,\n  startIn: 'idle'\n})\n```\n\n**Careful now:** When `startIn` is absent the default state is inferred from the first state seen in the chart. Also, empty-strings are valid states: The first-line of this chart allows transitioning from `''` to `'idle'`, and the empty-string would be the starting-state if `startIn` were omitted.\n\nThe pipe-character `|` means _OR_:\n\nThis...\n\n```\n  pending -\u003e resolved | rejected\n  resolved | rejected -\u003e done\n```\n\n...is shorthand for this...\n\n```\n  pending -\u003e resolved\n  pending -\u003e rejected\n  resolved -\u003e done\n  rejected -\u003e done\n```\n\n...which is also equivalent to:\n\n```\n  pending -\u003e (resolved | rejected) -\u003e done\n```\n\nNotice the use of parentheses `()`. These are completely ignored by the parser and just provide syntactic sugar.\n\nAny lines ending with `-\u003e` or `|` are considered to be part of the next line.\n\nThis...\n\n```\n  pending -\u003e\n    resolved |\n      rejected\n```\n\n...is the same as this:\n\n```\n  pending -\u003e resolved | rejected\n```\n\nComments are allowed:\n\n```\n  // These comments\n  pending -\u003e\n    resolved |  // will be\n      rejected  // ignored\n```\n\nIndentation has no meaning.\n\n## Examples of real charts\n\nHere are some charts I've used with Statebot:\n\n### Web-server:\n\n```js\n  // Email config, static files\n  -\u003e booting -\u003e\n    email-config -\u003e image-sizes -\u003e templates -\u003e pages\n\n  // If pages are ready, start webserver\n  pages -\u003e webserver\n\n  // Problem...?\n        booting |\n   email-config |\n    image-sizes |\n      templates |\n          pages -\u003e unrecoverable\n\n  // A watchdog will restart the web-server\n  unrecoverable -\u003e  report-and-quit\n```\n\n### Email sender:\n\n```js\n  idle -\u003e send\n\n  // Let's wait a few seconds before committing...\n  (send | update) -\u003e\n    debounce -\u003e sending |\n      (update | cancel)\n\n  sending -\u003e (sent | failed)\n\n  // All done!\n  (sent | cancel | failed) -\u003e done\n```\n\n### Drag-and-drop:\n\n```js\n  idle -\u003e\n    drag-detect -\u003e\n      (dragging | clicked)\n\n  // Click detected!\n  clicked -\u003e idle\n\n  // Drag detected!\n  dragging -\u003e\n    drag-wait -\u003e dragged -\u003e drag-wait\n\n  // Drag finished...\n  (drag-wait | dragged) -\u003e\n    (drag-done | drag-cancel) -\u003e\n      idle\n```\n\nThe [documentation](https://shuckster.github.io/statebot/) has a few more examples.\n\n# Why?\n\n\u003e \"The initial mystery that attends any journey is: How did the traveller reach his starting-point in the first place?\" -Louise Bogan\n\nI wrote Statebot to learn about FSMs, but really I ended up learning more about writing programs in general, and what I do and do not like about the process.\n\nAbove all, state-machines are extremely useful when it comes to *reading old code*. Their innate robustness and predictability are an added bonus.\n\nUnderstanding the flow of a program I haven't come back to in a while is exactly what I enjoy the most about using state-machines.\n\nWith Statebot itself, code can be marshalled into a shape that \"fans-out\":\n\n1. The compact `chart` at the top describes the flow.\n\n2. The hitchers reveal how `transitions` happen.\n\n3. Finally, the `callbacks` pull-in all the business-logic. (This might still be a huge jumbled mess of course, but at least at this point I'll have a few leads into what's supposed to be going on!)\n\nFrankly, this does add a bit of redundancy when using Statebot.\n\nTransitions are repeated between charts and hitchers, and there can be a bit of to-ing and fro-ing to get them right. But for me, the pay-off of being able to jump-in to an old piece of code and grok it quickly is worth it.\n\nThe bottom-line with any tool is to use it sparingly and appropriately, and the same applies with Statebot.\n\n# Contributing\n\nI consider the API stable and would not like it to change much. I don't want it to become a store or data-manager. Many APIs exist that are dedicated to such tasks. I'd really like to keep Statebot lean.\n\nOf course, bug-fixes, forks, and integrations are very welcome! If you feel it has saved you a little pain while trying to grok your own old projects, please consider [buying me a coffee](https://www.buymeacoffee.com/shuckster). :)\n\n## Credits\n\nWith thanks to \\@szabeszg for the suggestion and [discussion](https://github.com/shuckster/statebot/discussions/2) around:\n\n- `nextState = peek(eventName)`\n- `canTransitionTo(state, { afterEmitting: event })`\n\n 🙏\n\nStatebot was inspired by a trawl through Wikipedia and Google, which in turn was inspired by [XState](https://github.com/davidkpiano/xstate) by David Khourshid. You should check it out.\n\nStatebot integrates [events](https://www.npmjs.com/package/events) for the browser-build.\n\n- Since Statebot 2.5.0 [mitt](https://npmjs.com/mitt) is also compatible.\n- Since Statebot 2.6.0 mitt is used internally.\n\nThe Statebot logo uses the \"You're Gone\" font from [Typodermic Fonts](https://typodermicfonts.com/youre-gone/). The logo was made with [Acorn](https://flyingmeat.com/acorn/). The documentation is written in [JSDoc](https://jsdoc.app/) and is built with [Typedoc](https://typedoc.org/).\n\nStatebot was written by [Conan Theobald](https://github.com/shuckster/).\n\n## License\n\nStatebot is [MIT licensed](./LICENSE).\n","funding_links":["https://www.buymeacoffee.com/shuckster"],"categories":["Libraries"],"sub_categories":["JavaScript"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshuckster%2Fstatebot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshuckster%2Fstatebot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshuckster%2Fstatebot/lists"}