Giter Club home page Giter Club logo

ts-checked-fsm's Introduction

ts-checked-fsm

Community Supported

ts-checked-fsm provides compile tile validation of state machine transitions leveraging Typescript's powerful type system.

Overview

This library provides a builder pattern API for you declare a finite state machine as a set of states, actions, and transitions, and action handlers. The API is somewhat comparable to other state machine libraries, like XState with one major difference: ts-checked-fsm validates that your state machine is internally consistent and will fail compilation if not.

Examples of things that fail to compile:

  • You declare transitions between non-existent states
  • You declare the same state more than once
  • You declare a handler for the same state and action more than once
  • You declare a handler for a state or action that doesn't exist
  • A handler returns type that doesn't match a declared state
  • A handler for state c returns a state n for which there is no transition from c to n
  • You forget a handler for a any non-terminal state

The library uses Error branding and intentionally causes failed type assignments to give you quasi-human-readable error messages. There is a ton of type system devil-magic going on here to make all of this happen.

Example

  type MoneyPayload = {
      moneyInserted: number,
  };

  type ChangePayload = {
      changeRemaining: number,
  };

  type InsertMoneyActionPayload = {
      money: number,
  };

  const { nextState } = stateMachine()
      .state('idle')
      .state<'get-money', MoneyPayload>('get-money')
      .state<'vend', ChangePayload>('vend')
      .state<'dispense-change', ChangePayload>('dispense-change')
      .transition('idle', 'get-money')
      .transition('get-money', 'get-money')
      .transition('get-money', 'vend')
      .transition('vend', 'dispense-change')
      .transition('dispense-change', 'dispense-change')
      .transition('dispense-change', 'idle')
      .action<'insert-money', InsertMoneyActionPayload>('insert-money')
      .action<'vend-soda'>('vend-soda')
      .action<'clock-tick'>('clock-tick')
      .actionHandler('idle', 'insert-money', (_c, a) => {
          return {
              stateName: 'get-money',
              moneyInserted: a.money,
          } as const;
      })
      .actionHandler('get-money', 'insert-money', (c, a) => {
          return {
              stateName: 'get-money',
              moneyInserted: c.moneyInserted + a.money
          } as const;
      })
      .actionHandler('get-money', 'vend-soda', (c, _a) => {
          return c.moneyInserted >= 50 ? {
              stateName: 'vend',
              changeRemaining: c.moneyInserted - 50
          } as const : c;
      })
      .actionHandler('vend', 'clock-tick', (c, _a) => {
          return {
              stateName: 'dispense-change',
              changeRemaining: c.changeRemaining
          } as const;
      })
      .actionHandler('dispense-change', 'clock-tick', (c, _a) => {
          const coinVal = c.changeRemaining >= 25
              ? 25
              : c.changeRemaining >= 10
              ? 10
              : c.changeRemaining >= 5
              ? 5
              : 1;

          return c.changeRemaining - coinVal > 0 ? {
              stateName: 'dispense-change',
              changeRemaining: c.changeRemaining - coinVal
          } as const : {
              stateName: 'idle'
          } as const;
      })
      .done();

      let n = nextState({stateName: 'idle'}, { actionName: 'clock-tick'});
      // Idle state doesn't repsond to clock-tick, so state is unchanged
      expect(n).toEqual({stateName: 'idle'});
      n = nextState({stateName: 'idle'}, { actionName: 'insert-money', money: 25})
      expect(n).toEqual({stateName: 'get-money', moneyInserted: 25});
      n = nextState(n, { actionName: 'insert-money', money: 25});
      expect(n).toEqual({stateName: 'get-money', moneyInserted: 50});
      n = nextState(n, { actionName: 'insert-money', money: 27});
      expect(n).toEqual({stateName: 'get-money', moneyInserted: 77});
      n = nextState(n, { actionName: 'vend-soda'});
      expect(n).toEqual({stateName: 'vend', changeRemaining: 27});
      n = nextState(n, { actionName: 'clock-tick'});
      expect(n).toEqual({stateName: 'dispense-change', changeRemaining: 27});
      n = nextState(n, { actionName: 'clock-tick'});
      expect(n).toEqual({stateName: 'dispense-change', changeRemaining: 2});
      n = nextState(n, { actionName: 'clock-tick'});
      expect(n).toEqual({stateName: 'dispense-change', changeRemaining: 1});
      n = nextState(n, { actionName: 'clock-tick'});
      expect(n).toEqual({stateName: 'idle'});

Notes

  • Self transition are not implicit. You must explicitly declare them if a handler for state x is allowed to return state x.
  • You don't have to declare handlers for final states (i.e. those that have no transitions out of them). If fact, it's illegal to do so since they have no valid transitions out of them!
  • As shown in the example, states and actions can have a payload.
  • For handlers that can return multiple state types depending on some condition, every state must be a legal transition.

How it works

This library uses clever constructions using Typescript's type system. More details in this blog post.

Get started

Requirements:

Typescript 4.0+ or equivalent bundler loader (e.g. ts-loader for webpack).

Installation

Add ts-checked-fsm as a dependency in your package.json.

Building

Setup

Before any of the following tasks, you need to install dependencies:

yarn install

While untested, you can probably substitute npm for yarn and things will probably work.

Compilation

yarn run build

Output appears in lib folder

Contributions

Code contributions and improvements by the community are welcomed! See the LICENSE file for current open-source licensing and use information.

Before we can accept pull requests from contributors, we require a signed Contributor License Agreement (CLA),

ts-checked-fsm's People

Contributors

rickwebiii avatar altano avatar siemienik avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.