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

create-state-machine

v2.0.0

Published

Implementation of the State Pattern in JavaScript.

Downloads

1,402

Readme

create-state-machine

An implementation of state pattern in JavaScript, designed to help removing state-dependent variables.

Please see the motivation and example section on when and why you should use this pattern.

Usage

const { createStateMachine } = require('create-state-machine')

API

createStateMachine(initialState)

Creates a state machine with the given initialState. It also calls initialState.enter() if exists.

stateMachine.getState()

Returns the current state of the state machine.

stateMachine.setState(nextState)

Changes the state of the state machine.

If will call the exit() method on the previous state if defined, and then call the enter() method of the next state, if defined.

Motivation and Example

State pattern is very useful when your system has to exist in different states. Without the state pattern, your variables may be used by some states, but are irrelevant in other states.

For example, consider a message buffer that queues up a messages.

  • When a subscriber connects to it, the buffer should flush all messages to that subscriber.
  • Once connected, new messages should also go directly to the subscriber, bypassing the queue.
  • If the subscriber disconnects, then it should go back to queueing mode.
  • Only a single subscriber is allowed.

Normal version

Here’s how it’s likely to be implemented without using state pattern.

// example/createMessageBuffer.normal.js
export function createMessageBuffer (onMove) {
  let _connected = false
  let _subscriber = null
  let _queue = [ ]

  return {
    push (message) {
      if (_connected) {
        _subscriber(message)
      } else {
        _queue.push(message)
      }
    },
    connect (subscriber) {
      if (_connected) throw new Error('Already connected!')
      _connected = true
      _subscriber = subscriber
      _queue.forEach((message) => subscriber(message))
      _queue = null
    },
    disconnect () {
      if (!_connected) throw new Error('Not connected!')
      _connected = false
      _subscriber = null
      _queue = [ ]
    },
    isConnected () {
      return _connected
    }
  }
}

export default createMessageBuffer

In this version, there are many state-dependent variables.

  • The _queue is only used in disconnected state.
  • The _subscriber is only used in connected state.

But these variables exist under the same scope, although they are used in some states but not the others.

This is a code smell. You have to keep track of the current state and which variables are related to that state when you read/modify the code. Also, if not careful, your system may get into an inconsistent state (e.g. _connected is false but _queue is null).

Solving this problem with a state machine.

Now, let’s see what happens if you use a state machine.

// example/createMessageBuffer.state.js
import createStateMachine from '../'
export function createMessageBuffer (onMove) {
  const { getState, setState } = createStateMachine(disconnectedState())

  function disconnectedState () {
    const queue = [ ]
    return {
      connected: false,
      push (message) {
        queue.push(message)
      },
      connect (subscriber) {
        queue.forEach((message) => subscriber(message))
        setState(connectedState(subscriber))
      },
      disconnect () {
        throw new Error('Not connected!')
      }
    }
  }

  function connectedState (subscriber) {
    return {
      connected: true,
      push (message) {
        subscriber(message)
      },
      connect () {
        throw new Error('Already connected!')
      },
      disconnect () {
        setState(disconnectedState())
      }
    }
  }

  return {
    push (message) {
      return getState().push(message)
    },
    connect (subscriber) {
      return getState().connect(subscriber)
    },
    disconnect () {
      return getState().disconnect()
    },
    isConnected () {
      return getState().connected
    }
  }
}

export default createMessageBuffer

In this version, each state has its own closure.

  • The disconnected state only has access to the queue.
  • The connected state only has access to the subscriber.
  • There is no need to reassign any variable. We only use const; no need for var or let.
  • There are no more conditionals.

This makes your code cleaner and easier to reason about.

The test

// example/messageBufferTest.js
export default (createMessageBuffer) => {
  describe('a message buffer', () => {
    it('should flush messages to subscriber', () => {
      const buffer = createMessageBuffer()
      buffer.push(1)
      buffer.push(2)
      buffer.push(3)

      const subscriber = createSubscriber()
      assert.deepEqual(subscriber.getMessages(), [ ])
      buffer.connect(subscriber)
      assert.deepEqual(subscriber.getMessages(), [ 1, 2, 3 ])
      buffer.push(4)
      assert.deepEqual(subscriber.getMessages(), [ 1, 2, 3, 4 ])
    })

    it('should queue messages to the next subscriber when one disconnects', () => {
      const buffer = createMessageBuffer()
      buffer.push(1)
      buffer.connect(createSubscriber())
      buffer.push(2)
      buffer.disconnect()
      buffer.push(3)

      const subscriber = createSubscriber()
      buffer.connect(subscriber)
      assert.deepEqual(subscriber.getMessages(), [ 3 ])
      buffer.push(4)
      assert.deepEqual(subscriber.getMessages(), [ 3, 4 ])
    })

    it('should not allow multiple subscribers', () => {
      const buffer = createMessageBuffer()
      buffer.connect(createSubscriber())
      assert.throws(() => {
        buffer.connect(createSubscriber())
      })
    })

    it('should allow querying its status', () => {
      const buffer = createMessageBuffer()
      assert(!buffer.isConnected())
      buffer.connect(createSubscriber())
      assert(buffer.isConnected())
      buffer.disconnect()
      assert(!buffer.isConnected())
    })

    it('fails when disconnecting a disconnected buffer', () => {
      assert.throws(() => {
        createMessageBuffer().disconnect()
      })
    })
  })

  function createSubscriber () {
    const messages = [ ]
    function subscriber (message) {
      messages.push(message)
    }
    subscriber.getMessages = () => messages
    return subscriber
  }
}
// example/createMessageBuffer.normal.test.js
import messageBufferTest from './messageBufferTest'
import createMessageBuffer from './createMessageBuffer.normal'
messageBufferTest(createMessageBuffer)
// example/createMessageBuffer.state.test.js
import messageBufferTest from './messageBufferTest'
import createMessageBuffer from './createMessageBuffer.state'
messageBufferTest(createMessageBuffer)

The implementation

Here’s the entire implementation of createStateMachine. You see, it’s pretty simple!

// index.js
export function createStateMachine (initialState) {
  let _state
  function getState () {
    return _state
  }
  function setState (nextState) {
    if (nextState !== _state) {
      if (_state && _state.exit) _state.exit()
      _state = nextState
      if (_state.enter) _state.enter()
    }
  }
  setState(initialState)
  return { getState, setState }
}

export default createStateMachine