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

@nicholaswmin/fsm

v1.15.4

Published

a finite-state machine

Downloads

191

Readme

tests ccovt

fsm

A finite-state machine

... is an abstract machine that can be in one of a finite number of states.
The change from one state to another is called a transition.

This package constructs simple FSM's which express their logic declaratively & safely.[^1]

~1KB, zero dependencies, opinionated

Basic

Extras

API

Meta

Install

npm i @nicholaswmin/fsm

Example

A turnstile gate that opens with a coin.
When opened you can push through it; after which it closes again:

import { fsm } from '@nicholaswmin/fsm'

// define states & transitions:

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
})

// transition: coin
turnstile.coin()
// state: opened

// transition: push
turnstile.push()
// state: closed

console.log(turnstile.state)
// "closed"

Each step is broken down below.

Initialisation

An FSM with 2 possible states, each listing a single transition:

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
})
  • state: closed: allows transition: coin which sets: state: opened
  • state: opened: allows transition: push which sets: state: closed

Transition

A transition can be called as a method:

const turnstile = fsm({
  // defined 'coin' transition
  closed: { coin: 'opened' },

  // defined 'push' transition
  opened: { push: 'closed' }
})

turnstile.coin()
// state: opened

turnstile.push()
// state: closed

The current state must list the transition, otherwise an Error is thrown:

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
})

turnstile.push()
// TransitionError: 
// current state: "closed" has no transition: "push"

Current state

The fsm.state property indicates the current state:

const turnstile = fsm({
  closed: { foo: 'opened' },
  opened: { bar: 'closed' }
})

console.log(turnstile.state)
// "closed"

Hook methods

Hooks are optional methods, called at specific transition phases.

They must be set as hooks methods; an Object passed as 2nd argument of fsm(states, hooks).

Transition hooks

Called before the state is changed & can optionally cancel a transition.

Must be named: on<transition-name>, where <transition-name> is an actual transition name.

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
}, {
  onCoin: function() {
    console.log('got a coin')
  },
  
  onPush: function() {
    console.log('got pushed')
  }
})

turnstile.coin()
// "got a coin"

turnstile.push()
// "got pushed"

State hooks

Called after the state is changed.

Must be named: on<state-name>, where <state-name> is an actual state name.

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
}, {
  onOpened: function() {
    console.log('its open')
  },

  onClosed: function() {
    console.log('its closed')
  }
})

turnstile.coin()
// "its open"

turnstile.push()
// "its closed"

Hook arguments

Transition methods can pass arguments to relevant hooks, assumed to be variadic: [^2]

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
}, {
  onCoin(one, two) {
    return console.log(one, two)
  }
})

turnstile.coin('foo', 'bar')
// foo, bar

Transition cancellations

Transition hooks can cancel the transition by returning false.

Cancelled transitions don't change the state nor call any state hooks.

example: cancel transition to state: opened if the coin is less than 50c

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
}, {
  onCoin(coin) {
    return coin >= 50
  }
})

turnstile.coin(30)
// state: closed

// state still "closed",

// add more money?

turnstile.coin(50)
// state: opened

note: must explicitly return false, not just falsy.

Asynchronous transitions

Mark relevant hooks as async and await the transition:

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
}, {
  async onCoin(coins) {
    // simulate something async
    await new Promise(res => setTimeout(res.bind(null, true), 2000))
  }
})

await turnstile.coin()
// 2 seconds pass ...

// state: opened

Serialising to JSON

Simply use JSON.stringify:

const hooks = {
  onCoin() { console.log('got a coin') }
  onPush() { console.log('pushed ...') }
}

const turnstile = fsm({
  closed: { coin: 'opened' },
  opened: { push: 'closed' }
}, hooks)

turnstile.coin()
// got a coin

const json = JSON.stringify(turnstile)

... then revive with:

const revived = fsm(json, hooks)
// state: opened 

revived.push()
// pushed ..
// state: closed

note: hooks are not serialised so they must be passed again when reviving, as shown above.

FSM as a mixin

Passing an Object as hooks to: fsm(states, hooks) assigns FSM behaviour on the provided object.

Useful in cases where an object must function as an FSM, in addition to some other behaviour.[^3]

example: A Turnstile functioning as both an EventEmitter & an FSM

class Turnstile extends EventEmitter {
  constructor() {
    super()

    fsm({
      closed: { coin: 'opened' },
      opened: { push: 'closed' }
    }, this)
  }
}

const turnstile = new Turnstile()

// works as EventEmitter.

turnstile.emit('foo')

// works as an FSM as well.

turnstile.coin()

// state: opened

this concept is similar to a mixin.

API

fsm(states, hooks)

Construct an FSM

| name | type | desc. | default | |----------|----------|---------------------------------|----------| | states | object | a state-transition table | required | | hooks | object | implements transition hooks | this |

states must have the following abstract shape:

state: { 
  transition: 'next-state',
  transition: 'next-state' 
},
state: { transition: 'next-state' }
  • The 1st state in states is set as the initial state.
  • Each state can list zero, one or many transitions.
  • The next-state must exist as a state.

fsm(json, hooks)

Revive an instance from it's JSON.

Arguments

| name | type | desc. | default | |----------|----------|-------------------------------|----------| | json | string | JSON.stringify(fsm) result | required |

fsm.state

The current state. Read-only.

| name | type | default | |----------|----------|---------------| | state | string | current state |

Tests

unit tests:

node --run test

these tests require that certain coverage thresholds are met.

Contributing

Contribution Guide

Publishing

  • collect all changes in a pull-request
  • merge to main when all ok

then from a clean main:

# list current releases
gh release list

Choose the next Semver, i.e: 1.3.1, then:

gh release create 1.3.1

note: dont prefix releases/tags with v, just x.x.x is enough.

The Github release triggers the npm:publish workflow,
publishing the new version to npm.

It then attaches a Build Provenance statement on the Release Notes.

That's all.

Authors

N.Kyriakides; @nicholaswmin

License

The MIT License

Footnotes

[^1]: A finite-state machine can only exist in one and always-valid state.
It requires declaring all possible states & the rules under which it can transition from one state to another.

[^2]: A function that accepts an infinite number of arguments.
Also called: functions of "n-arity" where "arity" = number of arguments.

  i.e: nullary: `f = () => {}`, unary: `f = x => {}`,
  binary: `f = (x, y) => {}`, ternary `f = (a,b,c) => {}`, 
  n-ary/variadic: `f = (...args) => {}`
  

[^3]: FSMs are rare but perfect candidates for inheritance because usually something is-an FSM.
However, Javascript doesn't support multiple inheritance so inheriting FSM would create issues when inheriting other behaviours.

  *Composition* is also problematic since it namespaces the behaviour, 
  causing it to lose it's expressiveness.  
  i.e `light.fsm.turnOn` feels misplaced compared to `light.turnOn`.