https://github.com/zackradisic/tyfsm
(wip) simple and typesafe finite automata based state management library. Inspired by zustand and xstate
https://github.com/zackradisic/tyfsm
Last synced: about 1 year ago
JSON representation
(wip) simple and typesafe finite automata based state management library. Inspired by zustand and xstate
- Host: GitHub
- URL: https://github.com/zackradisic/tyfsm
- Owner: zackradisic
- License: mit
- Created: 2022-11-24T20:26:08.000Z (over 3 years ago)
- Default Branch: master
- Last Pushed: 2022-12-16T17:16:23.000Z (over 3 years ago)
- Last Synced: 2025-04-14T19:07:37.944Z (about 1 year ago)
- Language: TypeScript
- Size: 391 KB
- Stars: 124
- Watchers: 5
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# tyfsm (wip)
simple and typesafe finite-state machines. Inspired by zustand and xstate.
## Background
I like [automata-based programming](https://en.wikipedia.org/wiki/Automata-based_programming). Certain problems are solved elegantly when modelled as finite-state machines, particularly
those related to the logic of user interfaces. It's a great tool in a programmer's toolbox.
The 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.
For 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
## Features
`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).
The 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.
## Example
You define your state machine in Typescript's type system:
```typescript
type WebsocketMachine = StateMachine<
// First define your states and the data each state carries
{
idle: {
addr: string;
};
connecting: {
socket: WebSocket;
addr: string;
};
connected: {
socket: WebSocket;
addr: string;
};
error: {
errorMessage: string;
};
},
// Now define the valid transitions between states
{
idle: ["connecting"];
connecting: ["error", "connected"];
connected: ["idle", "error"];
error: ["idle"];
}
>;
// Write a utility to make selecting states easy
type State = SelectStates<
WebsocketMachine,
K
>;
// Now define actions for the machine
type Actions = {
// This action can only be called in the `idle` state and transitions
// the machine into the `connecting` state
connect: (state: State<"idle">) => State<"connecting">;
// This action can only be called in the `connecting` or `connected` state and transitions
// the machine into the `idle` state
disconnect: (state: State<"connecting" | "connected">) => State<"idle">;
};
```
Once you've modelled your state machine, you can create a store in a similar way like in zustand:
```typescript
// Create the initial state
const initial: State<"idle"> = {
kind: "idle",
addr: "ws://localhost:8302",
};
export const useWebsocketStore = create(
initial,
// Create the machine's actions:
//
// `get` is a function that returns the current state of the machine
// `transition` is a function that transitions the machine, with the following parameters:
// * the current state
// * the next state
// * the data for the next state
(get, transition) => ({
connect(idleState) {
const socket = new WebSocket(idleState.addr);
socket.addEventListener("error", () => {
// Get the current state of the machine, it is important to not use
// `idleState` from the above scope because the state may have changed in
// between the time the outer function returns and this callback runs.
const currentState = get();
if (currentState.kind === "connecting") {
transition(currentState.kind, "error", {
errorMessage: "Failed to connect",
});
}
});
socket.addEventListener("open", () => {
// Same treatment as above
const currentState = get();
if (currentState.kind === "connecting") {
transition(currentState.kind, "connected", {
socket,
addr: currentState.addr,
});
}
});
return transition(idleState.kind, "connecting", {
socket,
addr: idleState.addr,
});
},
disconnect(state) {
state.socket.close();
return transition(state.kind, "idle", {
addr: state.addr,
});
},
})
);
```
Now you can use it in React:
```tsx
const App = () => {
const { state, actions } = useWebsocketStore();
switch (state.kind) {
case "idle": {
return actions.connect(state)}>Connect;
}
case "connecting": {
return
Connecting...
;
}
case "connected": {
return (
actions.disconnect(state)}>Disconnect
);
}
case "error": {
return Something went wrong: {state.errorMessage}
;
}
}
};
```
Note 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
state object when the machine is in the error state.
## todo
- iterate on API and design