npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typed-state-machine

v1.0.0

Published

A type-safe state machine library

Downloads

23

Readme

Type State Machine

A simple, type-safe state machine library, heavily inspired by Tinder's state machine.

Installation

NPM:

npm install typed-state-machine

Yarn:

yarn add typed-state-machine

Example

import { StateMachine } from "typed-state-machine";

type State =
  | { type: "locked"; credit: number }
  | { type: "unlocked" }
  | { type: "broken"; oldState: State };

type Event =
  | { type: "insertCoin"; value: number }
  | { type: "admitPerson" }
  | { type: "machineDidFail" }
  | { type: "machineRepairDidComplete" };

const FARE_PRICE = 50;

const fsm = new StateMachine<State, Event>(({ initialState, state }) => {
  initialState({ type: "locked", credit: 0 });

  state("locked", ({ onEnter, on }) => {
    onEnter(() => {
      console.log("Locked.");
    });
    on("insertCoin", ({ event, state, transitionTo }) => {
      const newCredit = state.credit + event.value;
      if (newCredit >= FARE_PRICE) {
        return transitionTo({ type: "unlocked" }, () => {
          console.log("Open doors");
        });
      }

      return transitionTo({ type: "locked", credit: newCredit });
    });
    on("admitPerson", ({ dontTransition }) => {
      return dontTransition(() => {
        console.log("Sound alarm");
      });
    });
    on("machineDidFail", ({ state, transitionTo }) => {
      return transitionTo({ type: "broken", oldState: state }, () => {
        console.log("Order repair");
      });
    });
  });

  state("unlocked", ({ on }) => {
    on("admitPerson", ({ transitionTo }) => {
      return transitionTo({ type: "locked", credit: 0 });
    });
  });

  state("broken", ({ on, onExit }) => {
    onExit(() => {
      console.log("No longer broken.");
    });
    on("machineRepairDidComplete", ({ state, transitionTo }) => {
      return transitionTo(state.oldState);
    });
  });
});

Documentation

Before instantiating a state machine, one should first define all possible state and event types:

type State =
  | { type: "locked"; credit: number }
  | { type: "unlocked" }
  | { type: "broken"; oldState: State };

type Event =
  | { type: "insertCoin"; value: number }
  | { type: "admitPerson" }
  | { type: "machineDidFail" }
  | { type: "machineRepairDidComplete" };

Each union variant should contain a mandatory field type alongside an optional set of fields associated with each state or event. By explicitly defining states and events, TypeScript will provide proper type-checking and autocomplete, making it harder to make a type-related mistake.

A state machine could then be created by instantiating the StateMachine class. The constructor takes in two generic type parameters, State and Event (both of which extend { type: string }):

const fsm = new StateMachine<State, Event>(({ initialState, state }) => {
  initialState({ type: "locked", credit: 0 });

  state("locked", ({ onEnter, on }) => {});

  state("unlocked", ({ on }) => {});

  state("broken", ({ on, onExit }) => {});
});

Contrary to most other state machine libraries, this library doesn't use an object literal to configure the state machine. Instead, it uses a callback-based configuration approach (that closely matches the way that the Tinder state machine is used). This approach allows one to declare local variables within a specific state without having to pollute the scope accessible to other states. This is useful, for example, if one would like to use a timer within a specific state:

state("timeSensitiveState", ({ onEnter, onExit, on }) => {
  let timer!: ReturnType<typeof setTimeout>;

  onEnter(() => {
    timer = setTimeout(() => {
      // do something
    }, 5000);
  });
  onExit(() => {
    clearTimeout(timer);
  });
  // handle events
});

The constructor for StateMachine takes in a single argument, configCallback which in turn provides an argument object with two functions: initialState and state. The initialState function is used to define the initial state. It is mandatory to be called within the configCallback. It takes one argument of the generic type State:

initialState({ type: "locked", credit: 0 });

The state function is used to configure a specific state. It takes two arguments, the first one being the value of one of the type properties in the generic State union; the second being the stateDefinitionCallback, which in turn provides an argument object with three functions, onEnter, onExit and on:

state("locked", ({ onEnter, onExit, on }) => {
  onEnter(({ state }) => {});
  onExit(({ state }) => {});
  on("admitPerson", ({ dontTransition }) => {
    return dontTransition();
  });
});

The onEnter function defines the callback that gets triggered once the machine enters the state. The callback provides an argument object with the property state, which refers to the current state object. The onExit function is identical to onEnter, except that it gets triggered once the machine exits the state.

The on function is used to configure what happens when an event gets dispatched for the state. It takes two arguments, the first being the value of one of the type properties in the generic Event union; the second being the onEventCallback, which in turn provides an argument object with four properties, state, event, transitionTo and dontTransition. The callback should return either a call to transitionTo or a call to dontTransition:

on("admitPerson", ({ state, event, transitionTo }) => {
  console.log(state, event);
  return transitionTo({ type: "locked", credit: 0 });
});
on("admitPerson", ({ state, event, dontTransition }) => {
  console.log(state, event);
  return dontTransition();
});

The state and event properties provide access to the current state object and the event dispatched event object respectively.

Returning a call to transitionTo will make the machine transition to a specific state. The function takes 1 or 2 arguments. The first (mandatory) argument defines the state to transition to. The second (optional) argument can be used to define a side effect callback that will be scheduled for execution by the event loop (asynchronously, making it so that the side effect will never run BEFORE the transition finishes).

Returning a call to dontTransition will not make the machine transition. The function takes 1 optional argument, which is the side effect callback described above.

Once created, an event could be dispatched using the transition method of a StateMachine instance. The first argument takes an event object:

fsm.transition({ type: "unlocked" });

You can access the current state object via the state getter:

console.log(fsm.state);

You can also subscribe to the state machine via the subscribe method. The first argument takes a listener callback that provides three arguments, previous, current and event. The method returns a function that unsubscribes the listener when called:

const unsubscribe = fsm.subscribe((previous, current, event) => {
  console.log(previous, current, event);
});

The previous argument holds the previous state object. On the very first emit (which gets triggered when the subscribe method itself gets called), the value is null. The current argument holds the current state object. The event argument holds the event object that triggered the transition. On the very first emit, the value is null.

You can then call the returned function (stored in unsubscribe in this example) to unsubscribe the listener:

unsubscribe();

License

MIT