SkillAgentSearch skills...

Statebot

Describe the states and allowed transitions of a program using a flowchart-like syntax. Switch to states directly, or by wiring-up events. Statebot is an FSM.

Install / Use

/learn @shuckster/Statebot
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<h1 align="center"> statebot 🤖 </h1> <p align="center"> <a href="https://github.com/shuckster/statebot/blob/master/LICENSE"> <img alt="MIT license" src="https://img.shields.io/npm/l/statebot?style=plastic" /></a> <a href="https://bundlephobia.com/result?p=statebot"> <img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/statebot?style=plastic" /></a> <a href="https://www.npmjs.com/package/statebot"> <img alt="Version" src="https://img.shields.io/npm/v/statebot?style=plastic" /></a> </p>

Describe the states and allowed transitions of a program using a flowchart-like syntax. Switch to states directly, or by wiring-up events. Statebot is an FSM.

import { Statebot } from 'statebot'

const machine = Statebot('traffic-lights', {
  chart: `
    go ->
      prepare-to-stop ->
      stop

    // ...gotta keep that traffic flowing
    stop ->
      prepare-to-go ->
      go
  `
})

machine.performTransitions({
  'stop -> prepare-to-go -> go':   { on: 'timer' },
  'go -> prepare-to-stop -> stop': { on: 'timer' },
})

machine.onEvent('timer', () => {
  redrawTrafficLights()
})

function redrawTrafficLights() {
  machine.inState({
    'stop': () =>
      console.log('Draw red light'),

    'prepare-to-go': () =>
      console.log('Draw red + yellow lights'),

    'go': () =>
      console.log('Draw green light'),

    'prepare-to-stop': () =>
      console.log('Draw yellow light'),
  })
}

setInterval(machine.Emit('timer'), 2000)

CodeSandbox of the above example.

Since v3.1.0, Mermaid state-diagram support:

---
title: Traffic Lights
---
stateDiagram
direction LR
  go --> prepareToStop
    prepareToStop --> stop

  %% ...gotta keep that traffic flowing
  stop --> prepareToGo
    prepareToGo --> go
import { Statebot, mermaid } from 'statebot'

const machine = Statebot('traffic-lights', {
  chart: mermaid`
    stateDiagram
    direction LR
      go --> prepareToStop
        prepareToStop --> stop

      %% ...gotta keep that traffic flowing
      stop --> prepareToGo
        prepareToGo --> go
  `
})

It's around 5K gzipped, runs in Node and the browser, and is a shell-script too.

There are Hooks for these frameworks, too:

There is a lot of prior-art out there, most notably XState by David Khourshid, but I hope Statebot can offer a small contribution in the field of writing code that is easier to understand six-months after it has been written.

Installation

npm i statebot
<script src="https://unpkg.com/statebot@3.1.3/dist/browser/statebot.min.js"></script>

Quick Start:

React example:

(You can play around with this in a CodeSandbox.)

import React, { useState, useEffect } from 'react'
import { Statebot } from 'statebot'

// Statebot is framework agnostic. To use it with React,
// you might use something like this 3-line Hook:
function useStatebot(bot) {
  const [state, setState] = useState(bot.currentState())
  useEffect(() => bot.onSwitched(setState), [bot])
  return state
}

const loader$bot = Statebot('loader', {
  chart: `
    idle ->
      loading -> (loaded | failed) ->
      idle
  `
})

loader$bot.performTransitions(({ Emit }) => ({
  'idle -> loading': {
    on: 'start-loading',
    then: () => setTimeout(Emit('success'), 1000)
  },
  'loading -> loaded': {
    on: 'success'
  },
  'loading -> failed': {
    on: 'error'
  }
}))

const { Enter, Emit, inState } = loader$bot

function LoadingButton() {
  const state = useStatebot(loader$bot)

  return (
    <button
      className={state}
      onClick={Emit('start-loading')}
      disabled={inState('loading')}
    >
      {inState({
        'idle': 'Load',
        'loading': 'Please wait...',
        'loaded': 'Done!',
      })}
      ({state})
    </button>
  )
}

Node.js example:

const { Statebot } = require('statebot')

// Describe states + transitions
const machine = Statebot('promise-like', {
  chart: `

    idle ->
      // This one behaves a bit like a Promise
      pending ->
        (resolved | rejected) ->
      done

  `,
  startIn: 'pending'
})

// Handle events...
machine.performTransitions({
  'pending -> resolved': {
    on: 'success'
  }
})

// ...and/or transitions
machine.onTransitions({
  'pending -> resolved | rejected': () => {
    console.log('Sweet!')
  }
})

machine.onExiting('pending', toState => {
  console.log(`Off we go to: ${toState}`)
})

machine.canTransitionTo('done')
// false

machine.statesAvailableFromHere()
// ["resolved", "rejected"]

machine.emit('success')
// "Off we go to: resolved"
// "Sweet!"

Events

Statebot creates state-machines from charts, and we can switch states on events using performTransitions:

machine.performTransitions({
  'pending -> resolved': {
    on: 'data-loaded'
  }
})

// ^ This API is designed to read like this:
//   machine, perform transition "pending to
//   resolved" on "data-loaded".

Let's do a little more:

machine.performTransitions({
  'pending -> rejected': {
    on: ['data-error', 'timeout'],
    then: () => {
      console.warn('Did something happen?')
    }
  },

// ^ We can run something after a transition
//   happens with "then". Notice this will
//   happen after the "data-error" OR
//   "timeout" events.

  'resolved | rejected -> done': {
    on: 'finished'
  }

// ^ We can configure lots of transitions inside
//   one `performTransitions`. Here's one that
//   will switch from "resolved to done" OR
//   "rejected to done" when the "finished"
//   event is emitted.

})

// In this API, when events are emitted they
// can pass arguments to the "then" method.

// See the section below on "Passing data around".

We can also do stuff when states switch with onTransitions:

machine.onTransitions({
  'pending -> resolved': function () {
    console.log('Everything went lovely...')
    machine.enter('done')
  },

  'pending -> rejected': function () {
    console.warn('That did not go so well...')
    machine.enter('done')
  },

  'resolved | rejected -> done': function () {
    console.log('All finished')
  }
})

Let's do a little more:

machine.onTransitions(({ emit, Emit }) => ({
  'idle -> pending': function () {

// ^ This API is designed to read like this:
//   machine, on transition "idle to pending",
//   run a callback.

    getSomeData().then(
      (...args) => emit('data-loaded', ...args)
    )

// ^ emit() or Emit()? Which one to use? Maybe
//   you can infer the different meanings from
//   the .catch() of this Promise:

    .catch(Emit('data-error'))

// ^ Got it? Emit() is shorthand for:
//     (...args) => emit('event', ...args)
//
//   So emit() fires immediately, and Emit()
//   generates an emitter-method.

  }
}))

// In this API, the state-switching functions
// enter() and Enter() can pass arguments to
// these callbacks.

// See the section below on "Passing data around".

Both performTransitions and onTransitions take objects or functions that return objects in order to configure them.

Object:

machine.onTransitions({
  'idle -> pending': // etc...

Function:

machine.onTransitions(({ emit, enter, Emit, Enter }) => ({
  'idle -> pending': // etc...

In the case of a function, a single argument is passed-in: An object containing helpers for emitting events and entering states. In the above example we're pulling-in the helpers emit and enter, and also their corresponding factories: Emit and Enter.

Of course, you don't have to use an "implicit return":

machine.onTransitions(({ emit, Emit, enter, Enter }) => {
  // Setup, closure gubbins and so on...

  return {
    'idle -> pending': // etc...
  }
})

performTransitions hitches onto events, and onTransitions hitches onto state-transitions.

A Statebot FSM can have as many hitchers as you like, or none at all.

In any case, once an FSM is configured we are sometimes only interested in the state we are currently in, about to exit, or about to enter. There are hitchers for those, too:

machine.onExiting('pending', toState => {
  console.log('we are heading to:', toState)
})

machine.onEntered('done', fromState => {
  console.log('we came from:', fromState)
})

machine.currentState()
machine.previousState()

You can use the following snippet to tinker with the examples above:

function getSomeData() {
  return new Promise(
    (resolve, reject) => {
      setTimeout(resolve, 1000)

      // Randomly reject
      setTimeout(reject,
        500 + Math.round(Math.random() * 750)
      )
    }
  )
}

// Randomly timeout
setTimeout(() => machine.emit('timeout', true),
  750 + Math.round(Math.random() * 750)
)

machine.enter('pending')

Passing data around

Events can pass data to callbacks using emit:

machine.performTransit
View on GitHub
GitHub Stars29
CategoryDevelopment
Updated1y ago
Forks0

Languages

JavaScript

Security Score

80/100

Audited on Dec 7, 2024

No findings