{"id":13998618,"url":"https://github.com/JavaScriptRegenerated/yieldmachine","last_synced_at":"2025-07-23T06:32:39.796Z","repository":{"id":48569668,"uuid":"327153192","full_name":"JavaScriptRegenerated/yieldmachine","owner":"JavaScriptRegenerated","description":"Components for State Machines, using Generator Functions","archived":false,"fork":false,"pushed_at":"2023-01-01T07:37:56.000Z","size":841,"stargazers_count":34,"open_issues_count":34,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-26T22:57:31.613Z","etag":null,"topics":["generator-function","javascript","state-machine","typescript"],"latest_commit_sha":null,"homepage":"https://regenerated.dev/yieldmachine","language":"TypeScript","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/JavaScriptRegenerated.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-01-06T00:14:05.000Z","updated_at":"2025-04-12T07:32:59.000Z","dependencies_parsed_at":"2023-01-31T21:00:32.000Z","dependency_job_id":null,"html_url":"https://github.com/JavaScriptRegenerated/yieldmachine","commit_stats":null,"previous_names":["royalicing/yieldmachine"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/JavaScriptRegenerated/yieldmachine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JavaScriptRegenerated%2Fyieldmachine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JavaScriptRegenerated%2Fyieldmachine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JavaScriptRegenerated%2Fyieldmachine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JavaScriptRegenerated%2Fyieldmachine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JavaScriptRegenerated","download_url":"https://codeload.github.com/JavaScriptRegenerated/yieldmachine/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JavaScriptRegenerated%2Fyieldmachine/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266631358,"owners_count":23959419,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"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":["generator-function","javascript","state-machine","typescript"],"created_at":"2024-08-09T19:01:51.275Z","updated_at":"2025-07-23T06:32:39.465Z","avatar_url":"https://github.com/JavaScriptRegenerated.png","language":"TypeScript","readme":"\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003e👑 ⚙️ Yield Machine\u003c/h1\u003e\n  \u003cp\u003eComponents for State Machines, using Generator Functions\u003c/p\u003e\n  \u003cdiv\u003e\n    \u003ca href=\"https://bundlephobia.com/result?p=yieldmachine\"\u003e\n      \u003cimg src=\"https://badgen.net/bundlephobia/minzip/yieldmachine@0.4.12\" alt=\"minified and gzipped size\"\u003e\n      \u003cimg src=\"https://badgen.net/bundlephobia/min/yieldmachine@0.4.12\" alt=\"minified size\"\u003e\n      \u003cimg src=\"https://badgen.net/bundlephobia/dependency-count/yieldmachine@0.4.12\" alt=\"zero dependencies\"\u003e\n    \u003c/a\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n## Goals\n\n- States and machines can be reused — components for state machines.\n- Nest machines inside one another — aid reuse and clarity.\n- Interops with native JavaScript \u0026 browser features such as Promise, AbortSignal, and EventTarget.\n- Consistently use built-in browser features such as offline status, promises, fetch, IntersectionObserver, ResizeObserver, window.location. Manage these things in a consistent way with a consistent interface.\n\n### Problems that state machines solve\n\n- Making sure my code is 100% robust and doesn't fall into inconsistent states is hard.\n- It's easy to forget about error handling.\n- Built-in browser features (such as InteractionObserver) are powerful but a pain to manage correctly.\n- Managing various flavors of state is hard: the current URL, local storage, focused element, fetch response, caches, offline/online.\n\n## Install\n\nRequires Node.js 14 and up.\n\n```console\nnpm add yieldmachine\n```\n\n## Overview\n\nYou define your machine using a function. For example, you could define a state machine representing a light switch. We’ll name our function `Switch`.\n\n```ts\nfunction Switch() {\n\n}\n```\n\nInside you declare each state you want as a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*).\n\nOur `Switch` will have two states: `Off` and `On`. We return `Off` as that’s what we want as our initial state to be — our light is off by default.\n\n```ts\nimport { on, start } from \"yieldmachine\";\n\nfunction Switch() {\n  function* Off() {\n  }\n  function* On() {\n  }\n\n  return Off;\n}\n```\n\nOur `Switch` can be flicked on and off. The string `\"FLICK\"` is our event that will represent the flicking on and off of our switch.\n\nWhen our `Switch` is `Off` and it is sent a `FLICK` event, it transitions to `On`.\n\nAnd, when our `Switch` is `On` and it is sent a `FLICK` event, it transitions back to `Off`.\n\n```ts\nimport { on, start } from \"yieldmachine\";\n\nfunction Switch() {\n  function* Off() {\n    yield on(\"FLICK\", On);\n  }\n  function* On() {\n    yield on(\"FLICK\", Off);\n  }\n\n  return Off;\n}\n```\n\nNow our machine is ready to be run. We pass our `Switch` to the `start` function we import from `yieldmachine`, and it will run an instance of our machine. And as we send it `\"FLICK\"` message, you’ll see the `value` of our machine instance change.\n\n```ts\nconst machine = start(Switch);\nmachine.value; // { state: \"Off\", change: 0 }\nmachine.next(\"FLICK\");\nmachine.value; // { state: \"On\", change: 1 }\nmachine.next(\"FLICK\");\nmachine.value; // { state: \"Off\", change: 2 }\n```\n\n## Benefits of Generator Functions\n\n- Generator Functions are a built-in feature of JavaScript and TypeScript.\n- They have built-in syntax highlighting, autocompletion, and general rich language support in editors like Visual Studio Code.\n- Our states are represented by actual JavaScript functions.\n  - This means if we pass a state that’s either spelled incorrectly or isn’t in scope, our editor will tell us.\n  - Our states use the name of the function.\n  - Generator Functions can be reused, composed, and partially applied like any function. This increases the modularity and reuse of our machine parts.\n- Coming soon: our machine definitions can be serialized and deserialized. This means they could be generated on a back-end and sent to the front-end. They could be stored away in a database. They could even be generated dynamically on the fly.\n\n## Documentation\n\n### `start(machineDefinition: Function | GeneratorFunction, options: { signal?: AbortSignal })`\n\nStarts a machine, transitioning to its initially returned state.\n\n### `.value`\n\n#### `.value.state: string | Record\u003cstring, unknown\u003e`\n\nThe current state of the machine. If machines were nested then an object is returned with the parent machine as the key, and its current state as the value.\n\n#### `.value.change: number`\n\nThe number of times this machine has transitioned. Useful for consumers updating only when changes have been made.\n\n#### `.value.results: Promise\u003cunknown\u003e`\n\nThe result of calling functions passed to `entry()` or `exit()`.\n\n### `.next(eventName: string | symbol)`\n\nSends an event to the machine, transitioning if the event was recognised. Unrecognised events are ignored.\n\n### `.stop()`\n\nCleans up the machine.\n\n\n## Messages\n\n### `on(eventName: string | symbol, target: GeneratorFunction | Cond | Mapper)`\n\nTransitions to the target state when the given event occurs.\n\n```ts\nimport { on, start } from \"yieldmachine\";\n\nfunction Switch() {\n  function* Off() {\n    yield on(\"FLICK\", On);\n    yield on(\"TOGGLE\", On);\n  }\n  function* On() {\n    yield on(\"FLICK\", Off);\n    yield on(\"TOGGLE\", Off);\n  }\n\n  return Off;\n}\n\nconst machine = start(Switch);\nmachine.value.state; // \"Off\"\nmachine.next(\"FLICK\");\nmachine.value.state; // \"On\"\nmachine.next(\"TOGGLE\");\nmachine.value.state; // \"Off\"\n```\n\n#### `cond(predicate: (readContext: ReadContextCallback) =\u003e boolean, target: GeneratorFunction)`\n\nPassed as the 2nd argument to `on()` to conditionally transition to an event. Can read from context to help make its decision.\n\n### `entry(action: ({ signal }: { signal: AbortSignal }) =\u003e undefined | unknown | Promise\u003cunknown\u003e)`\n\nRuns the provided function when this state is entered. If the function returns a promise, its value is made available in the `.results` property of the machine, keyed by the name of this passed function.\n\nA signal is provided which is useful for passing to `fetch()` or `eventTarget.addEventListener()`. This signal is aborted on exit.\n\n```ts\nimport { start, on, enter } from \"yieldmachine\";\n\nlet onCount = 0;\nfunction recordOn() {\n  onCount++;\n}\n\nfunction Switch() {\n  function* Off() {\n    yield on(\"FLICK\", On);\n  }\n  function* On() {\n    yield entry(recordOn);\n    yield on(\"FLICK\", Off);\n  }\n\n  return Off;\n}\n\nconst machine = start(Switch);\nmachine.next(\"FLICK\");\nconsole.log(onCount, machine.value.state); // 1, \"ON\"\nmachine.next(\"FLICK\");\nconsole.log(onCount, machine.value.state); // 1, \"OFF\"\nmachine.next(\"FLICK\");\nconsole.log(onCount, machine.value.state); // 2, \"ON\"\n```\n\n### `exit(action: () =\u003e undefined | unknown | Promise\u003cunknown\u003e)`\n\nRuns the provided function when this state is exited.\n\n```ts\nimport { start, on, exit } from \"yieldmachine\";\n\nlet lastSessionEnded = null;\nfunction recordSessionEnd() {\n  lastSessionEnded = new Date();\n}\n\nfunction Session() {\n  function* SignedOut() {\n    yield on(\"AUTHENTICATE\", SignedIn);\n  }\n  function* SignedIn() {\n    yield exit(recordSessionEnd);\n    yield on(\"LOG_OFF\", SignedOut);\n  }\n\n  return SignedOut;\n}\n\nconst machine = start(Switch);\nconsole.log(lastSessionEnded, machine.value.state); // null, \"SignedOut\"\nmachine.next(\"AUTHENTICATE\");\nconsole.log(lastSessionEnded, machine.value.state); // null, \"SignedIn\"\nmachine.next(\"LOG_OFF\");\nconsole.log(lastSessionEnded, machine.value.state); // (current time), \"SignedOut\"\n```\n\n### `listenTo(sender: EventTarget, eventName: string | string[])`\n\nListens to an `EventTarget` — for example, an HTMLElement like a button.\n\nUses [`.addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) to listen to the event. The listener is removed when transitioning to a different state or when the machine is stopped, so no extra clean up is necessary.\n\n```ts\nfunction ButtonClickListener(button: HTMLButtonElement) {\n  function* initial() {\n    yield on(\"click\", clicked);\n    yield listenTo(button, \"click\");\n  }\n  function* clicked() {}\n\n  return initial;\n}\n\nconst button = document.createElement('button');\nconst machine = start(ButtonClickListener.bind(null, button));\n\nmachine.value; // { state: \"initial\", change: 0 }\nbutton.click();\nmachine.value; // { state: \"clicked\", change: 1 }\nbutton.click();\nmachine.value; // { state: \"initial\", change: 1 }\n```\n\n## Examples\n\n### HTTP Loader\n\n```javascript\nimport { entry, on, start } from \"yieldmachine\";\n\nconst exampleURL = new URL(\"https://example.org/\");\nfunction fetchData() {\n  return fetch(exampleURL);\n}\n\n// Define a machine just using functions\nfunction Loader() {\n  // Each state is a generator function\n  function* idle() {\n    yield on(\"FETCH\", loading);\n  }\n  // This is the ‘loading’ state\n  function* loading() {\n    // This function will be called when this state is entered.\n    // Its return value is available at `loader.results.fetchData`\n    yield entry(fetchData);\n    // If the promise succeeds, we will transition to the `success` state\n    // If the promise fails, we will transition to the `failure` state\n    yield on(\"SUCCESS\", success);\n    yield on(\"FAILURE\", failure);\n  }\n  // States that don’t yield anything are final\n  function* success() {}\n  // Or they can define transitions to other states\n  function* failure() {\n    // When the RETRY event happens, we transition from ‘failure’ to ‘loading’\n    yield on(\"RETRY\", loading);\n  }\n\n  // Return the initial state from your machine definition\n  return idle;\n}\n\nconst loader = start(Loader);\nloader.value; // { state: \"idle\", change: 0 }\n\nloader.next(\"FETCH\");\nloader.value; // { state: \"loading\", change: 1, results: Promise }\n\nloader.value.results.then((results) =\u003e {\n  console.log(\"Fetched\", results.fetchData); // Use response of fetch()\n  loader.value.state; // \"success\"\n});\n\n/* Or with await: */\n// const { fetchData } = await loader.value.results;\n// loader.value.state; // \"success\"\n```\n\n### Passing parameters to a machine with closures\n\n```javascript\nimport { entry, on, start } from \"yieldmachine\";\n\n// Function taking as many arguments as you like\nfunction GenericLoader(url) {\n  function fetchData() {\n    return fetch(url);\n  }\n\n  function* idle() {\n    yield on(\"FETCH\", loading);\n  }\n  function* loading() {\n    yield entry(fetchData);\n    yield on(\"SUCCESS\", success);\n    yield on(\"FAILURE\", failure);\n  }\n  function* success() {}\n  function* failure() {\n    yield on(\"RETRY\", loading);\n  }\n\n  return idle;\n}\n\n// Function taking no arguments that will define our machine\nfunction SpecificLoader() {\n  const exampleURL = new URL(\"https://example.org/\");\n  return GenericLoader(exampleURL);\n}\n\n// Start our specific loader machine\nconst loader = start(SpecificLoader);\nloader.value; // { state: \"idle\", change: 0 }\n\nloader.next(\"FETCH\");\nloader.value; // { state: \"loading\", change: 1, results: Promise }\n\nloader.value.results.then((results) =\u003e {\n  console.log(\"Fetched\", results.fetchData); // Use response of fetch()\n  loader.value.state; // \"success\"\n});\n```\n\n### `AbortController` wrapper that listens to \"abort\" event\n\n```ts\nfunction* AbortListener(controller: AbortController) {\n  function* Initial() {\n    yield on(\"abort\", Aborted);\n    yield listenTo(controller.signal, [\"abort\"]);\n  }\n  function* Aborted() {}\n\n  return new Map([\n    [() =\u003e controller.signal.aborted, Aborted],\n    [null, Initial],\n  ]);\n}\n\nconst aborter = new AbortController();\nconst machine = start(AbortListener.bind(null, aborter));\n\nmachine.value; // { state: \"initial\", change: 0 }\naborter.abort();\nmachine.value; // { state: \"aborted\", change: 1 }\n```\n\n----\n\n## Minifiers\n\nIf you use a minifier then your function name will be changed to a short name like `d` instead of `On`. To get around this, you can specify your states as methods (which are not usually minified) like so:\n\n```js\nfunction SwitchMachine() {\n  const { On, Off } = {\n    *Off() {\n      yield on(\"FLICK\", On);\n    },\n    *On() {\n      yield on(\"FLICK\", Off);\n    }\n  };\n  return Off;\n}\n```\n\n----\n\n## TODO\n\n- [ ] Parallel states by returning object for initial state\n- [ ] Assign data somehow?\n- [ ] Allow sending objects: `Event | { type: string }`\n- [ ] More examples!\n- [ ] Hook for React\n- [ ] Hook for Preact\n- [ ] Hook for Vue\n\n```js\nfunction *Parallel() {\n  function Light1() {\n    function* Off() {\n      yield on('toggle_switch_1', On);\n    }\n    function* On() {\n      yield on('toggle_switch_1', Off);\n    }\n    return Off;\n  }\n\n  function Light2() {\n    function* Off() {\n      yield on('toggle_switch_2', On);\n    }\n    function* On() {\n      yield on('toggle_switch_2', Off);\n    }\n    return Off;\n  }\n\n  return [\n    Light1,\n    Light2\n  ];\n}\n```\n\n```js\nfunction *ParallelWithANDState() {\n  function Light1() {\n    function* Off() {\n      yield on('toggle_switch_1', On);\n    }\n    function* On() {\n      yield on('toggle_switch_1', Off);\n    }\n    return Off;\n  }\n\n  function Light2() {\n    function* Off() {\n      yield on('toggle_switch_2', On);\n    }\n    function* On() {\n      yield on('toggle_switch_2', Off);\n    }\n    return Off;\n  }\n\n  // function* Light3() {\n  //   function* Off() {}\n  //   function* On() {}\n\n  //   return conds(new Map([\n  //     [hasState(Light1, 'Off'), Off],\n  //     [hasState(Light2, 'Off'), Off],\n  //     [true, On],\n  //   ]));\n  // }\n\n  function* Light3() {\n    const light1Off = yield readHasState(Light1, 'Off');\n    const light2Off = yield readHasState(Light2, 'Off');\n\n    function* Off() {}\n    function* On() {}\n    function* checking() {\n      yield cond(light1Off, Off);\n      yield cond(light2Off, Off);\n      yield always(On);\n    }\n\n    return checking;\n  }\n\n  // Alternative\n  function* Light3() {\n    const onCount = yield readCountSiblings('Off');\n\n    function* Off() {}\n    function* On() {}\n    function* checking() {\n      yield cond(onCount === 2, Off);\n      // yield cond(`${onCount} === ${2}`, Off);\n      // yield condIs(Off, onCount, 2);\n      yield always(On);\n    }\n\n    return checking;\n  }\n\n  return [\n    Light1,\n    Light2,\n    Light3\n  ];\n}\n```\n\n----\n\nFurther reading / inspiration:\n- [XState](https://xstate.js.org/)\n- [Robot](https://thisrobot.life/)\n- [Welcome to the world of Statecharts](https://statecharts.github.io/)\n    - [Resources](https://statecharts.github.io/resources.html)\n- [Apache Commons guide to SCXML](https://commons.apache.org/proper/commons-scxml/guide/scxml-documents.html)\n- [PingPong in P](https://github.com/p-org/P/wiki/PingPong-program)\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJavaScriptRegenerated%2Fyieldmachine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FJavaScriptRegenerated%2Fyieldmachine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJavaScriptRegenerated%2Fyieldmachine/lists"}