{"id":15636480,"url":"https://github.com/zackradisic/tyfsm","last_synced_at":"2025-04-14T19:07:43.073Z","repository":{"id":114945111,"uuid":"570295539","full_name":"zackradisic/tyfsm","owner":"zackradisic","description":"(wip) simple and typesafe finite automata based state management library. Inspired by zustand and xstate","archived":false,"fork":false,"pushed_at":"2022-12-16T17:16:23.000Z","size":400,"stargazers_count":124,"open_issues_count":0,"forks_count":1,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-14T19:07:37.944Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/zackradisic.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-24T20:26:08.000Z","updated_at":"2025-02-22T08:36:44.000Z","dependencies_parsed_at":null,"dependency_job_id":"dbe4e080-b6dd-4a77-a749-9ed650c2f899","html_url":"https://github.com/zackradisic/tyfsm","commit_stats":{"total_commits":2,"total_committers":1,"mean_commits":2.0,"dds":0.0,"last_synced_commit":"0a6f76179a419df477f7fb224c4357551a6b2f02"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zackradisic%2Ftyfsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zackradisic%2Ftyfsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zackradisic%2Ftyfsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zackradisic%2Ftyfsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zackradisic","download_url":"https://codeload.github.com/zackradisic/tyfsm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248943456,"owners_count":21186958,"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","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":[],"created_at":"2024-10-03T11:04:21.048Z","updated_at":"2025-04-14T19:07:43.042Z","avatar_url":"https://github.com/zackradisic.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tyfsm (wip)\n\nsimple and typesafe finite-state machines. Inspired by zustand and xstate.\n\n## Background\n\nI like [automata-based programming](https://en.wikipedia.org/wiki/Automata-based_programming). Certain problems are solved elegantly when modelled as finite-state machines, particularly\nthose related to the logic of user interfaces. It's a great tool in a programmer's toolbox.\n\nThe most popular js/ts fsm library is [XState](https://github.com/statelyai/xstate). It's pretty good but verbose and difficult to learn, it's a big investment to drop in a team setting. Furthermore, type-safety in XState seems like an afterthought.\n\nFor me one of the biggest benefits of finite-state machines is that they enable you to make illegal states unrepresentable, and a large chunk of that is missing from xstate. So that's why I built this library\n\n## Features\n\n`tyfsm`'s state machines are centered around leveraging the type-narrowing features of Typescript's [discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions).\n\nThe core of the state machine is a discriminated union, where each state is a single variant of the union. This gives us neat type-safety narrowing features.\n\n## Example\n\nYou define your state machine in Typescript's type system:\n\n```typescript\ntype WebsocketMachine = StateMachine\u003c\n  // First define your states and the data each state carries\n  {\n    idle: {\n      addr: string;\n    };\n    connecting: {\n      socket: WebSocket;\n      addr: string;\n    };\n    connected: {\n      socket: WebSocket;\n      addr: string;\n    };\n    error: {\n      errorMessage: string;\n    };\n  },\n  // Now define the valid transitions between states\n  {\n    idle: [\"connecting\"];\n    connecting: [\"error\", \"connected\"];\n    connected: [\"idle\", \"error\"];\n    error: [\"idle\"];\n  }\n\u003e;\n\n// Write a utility to make selecting states easy\ntype State\u003cK extends WebsocketMachine[\"allStates\"]\u003e = SelectStates\u003c\n  WebsocketMachine,\n  K\n\u003e;\n\n// Now define actions for the machine\ntype Actions = {\n  // This action can only be called in the `idle` state and transitions\n  // the machine into the `connecting` state\n  connect: (state: State\u003c\"idle\"\u003e) =\u003e State\u003c\"connecting\"\u003e;\n\n  // This action can only be called in the `connecting` or `connected` state and transitions\n  // the machine into the `idle` state\n  disconnect: (state: State\u003c\"connecting\" | \"connected\"\u003e) =\u003e State\u003c\"idle\"\u003e;\n};\n```\n\nOnce you've modelled your state machine, you can create a store in a similar way like in zustand:\n\n```typescript\n// Create the initial state\nconst initial: State\u003c\"idle\"\u003e = {\n  kind: \"idle\",\n  addr: \"ws://localhost:8302\",\n};\n\nexport const useWebsocketStore = create\u003cWebsocketMachine, Actions\u003e(\n  initial,\n  // Create the machine's actions:\n  //\n  // `get` is a function that returns the current state of the machine\n  // `transition` is a function that transitions the machine, with the following parameters:\n  //    * the current state\n  //    * the next state\n  //    * the data for the next state\n  (get, transition) =\u003e ({\n    connect(idleState) {\n      const socket = new WebSocket(idleState.addr);\n\n      socket.addEventListener(\"error\", () =\u003e {\n        // Get the current state of the machine, it is important to not use\n        // `idleState` from the above scope because the state may have changed in\n        // between the time the outer function returns and this callback runs.\n        const currentState = get();\n        if (currentState.kind === \"connecting\") {\n          transition(currentState.kind, \"error\", {\n            errorMessage: \"Failed to connect\",\n          });\n        }\n      });\n\n      socket.addEventListener(\"open\", () =\u003e {\n        // Same treatment as above\n        const currentState = get();\n        if (currentState.kind === \"connecting\") {\n          transition(currentState.kind, \"connected\", {\n            socket,\n            addr: currentState.addr,\n          });\n        }\n      });\n\n      return transition(idleState.kind, \"connecting\", {\n        socket,\n        addr: idleState.addr,\n      });\n    },\n    disconnect(state) {\n      state.socket.close();\n      return transition(state.kind, \"idle\", {\n        addr: state.addr,\n      });\n    },\n  })\n);\n```\n\nNow you can use it in React:\n\n```tsx\nconst App = () =\u003e {\n  const { state, actions } = useWebsocketStore();\n\n  switch (state.kind) {\n    case \"idle\": {\n      return \u003cbutton onClick={() =\u003e actions.connect(state)}\u003eConnect\u003c/button\u003e;\n    }\n    case \"connecting\": {\n      return \u003cp\u003eConnecting...\u003c/p\u003e;\n    }\n    case \"connected\": {\n      return (\n        \u003cbutton onClick={() =\u003e actions.disconnect(state)}\u003eDisconnect\u003c/button\u003e\n      );\n    }\n    case \"error\": {\n      return \u003cp\u003eSomething went wrong: {state.errorMessage}\u003c/p\u003e;\n    }\n  }\n};\n```\n\nNote that all the actions are type-safe. You can only call `actions.connect(state)` when `state` is in the idle state. Similarly, the `errorMessage` property is only available on the\nstate object when the machine is in the error state.\n\n## todo\n\n- iterate on API and design\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzackradisic%2Ftyfsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzackradisic%2Ftyfsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzackradisic%2Ftyfsm/lists"}