• Stars
    star
    123
  • Rank 290,145 (Top 6 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 2 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

(wip) simple and typesafe finite automata based state management library. Inspired by zustand and xstate

tyfsm (wip)

simple and typesafe finite-state machines. Inspired by zustand and xstate.

Background

I like 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. 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.

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:

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<K extends WebsocketMachine["allStates"]> = 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:

// Create the initial state
const initial: State<"idle"> = {
  kind: "idle",
  addr: "ws://localhost:8302",
};

export const useWebsocketStore = create<WebsocketMachine, Actions>(
  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:

const App = () => {
  const { state, actions } = useWebsocketStore();

  switch (state.kind) {
    case "idle": {
      return <button onClick={() => actions.connect(state)}>Connect</button>;
    }
    case "connecting": {
      return <p>Connecting...</p>;
    }
    case "connected": {
      return (
        <button onClick={() => actions.disconnect(state)}>Disconnect</button>
      );
    }
    case "error": {
      return <p>Something went wrong: {state.errorMessage}</p>;
    }
  }
};

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

More Repositories

1

aussieplusplus

Programming language from down under
Rust
612
star
2

tyvm

An experimental bytecode interpreter / type-checker for type-level Typescript
Zig
430
star
3

go-playground-wasm

A version of play.golang.org that runs completely in the browser
TypeScript
183
star
4

glyph

My own personal code editor built with Rust + OpenGL
Rust
157
star
5

rust-vs-zig

Comparing unsafe Rust vs Zig by writing a bytecode interpreter with GC in both langs
Rust
154
star
6

node-soundcloud-downloader

A SoundCloud API v2 wrapper for Node.js
TypeScript
134
star
7

tether

WIP high-performance code editor inspired by Doom Emacs and neovim. Comes with explosions.
Zig
65
star
8

lofi-cli

Listen to ChilledCow's lofi hip-hop stream from the command line
JavaScript
27
star
9

soundcloud-api

A SoundCloud API v2 wrapper for Go
Go
26
star
10

write-your-own-zod

Write your own Zod from scratch
TypeScript
14
star
11

force-directed-graph

experiment
Rust
11
star
12

bun-macros-flappy-bird

C
5
star
13

prisma2gql

prisma schema 2 graphql schema generator
Haskell
4
star
14

iszacksleeping

A way to actually contact me and hold me accountable to a sleep schedule
TypeScript
3
star
15

toilet-paper-twitter

a stupid graphics experiment
HTML
3
star
16

cheatsheets

TypeScript
2
star
17

rasta

Building a software rasterizer for fun
Rust
2
star
18

downloadsound.cloud-api-go

Go
1
star
19

c-template

Simple C project starter template
C
1
star
20

rust-emscripten-bug

Rust
1
star
21

youtube-rooms-frontend

TypeScript
1
star
22

learning-crdts-rust

Learning and implementing CRDTs in Rust
Rust
1
star
23

downloadsound.cloud-api

TypeScript
1
star
24

youtube-rooms

Go
1
star
25

downloadsound.cloud

TypeScript
1
star
26

sticky

Fixing my broken right arrow key
Objective-C
1
star