Finite State Machines (FSMs) stored in Mongo.
The usage of this module is straightforward:
- specify your machine model,
- create new or load pre-existing machine instance,
- perform transitions and have the state automatically updated in the storage.
Note the version 3.0.0-rc.X
is not yet 100% stable and breaking changes are possible.
npm install @patrikstas/finite-state-machine
or
yarn add @patrikstas/finite-state-machine
Clone the repo and run the demo
git clone https://github.com/Patrik-Stas/js-finite-state-machine.git
cd js-finite-state-machine
npm install
npm run demo
First we need to specify our state machine. Our state machines are defined by:
- initial state
- states
- transitions
Example:
const semaphoreDefinition = {
type: 'semaphore',
initialState: 'off',
states: {
off: 'off',
red: 'red',
orange: 'orange',
green: 'green'
},
transitions: {
next: 'next',
disable: 'disable',
enable: 'enable'
},
definitionStates: {
off: { metadata: { 'can-pass': false } },
red: { metadata: { 'can-pass': false } },
orange: { metadata: { 'can-pass': false } },
green: { metadata: { 'can-pass': true } }
},
definitionTransitions: {
next: [
{ from: 'red', to: 'orange' },
{ from: 'orange', to: 'green' },
{ from: 'green', to: 'red' }],
disable: [
{ from: 'red', to: 'off' },
{ from: 'orange', to: 'off' },
{ from: 'green', to: 'off' }
],
enable: [
{ from: 'off', to: 'red' }
]
}
}
See in code at docs/semaphore.js.
In general you are supposed to create and load FSM(s) (Finite State Machine(s)) using FSM Manager
. It
encompasses common logic and check for such operations. It expects to be supplied Storage strategy
.
Storage strategy is a module which implements CRUD operations against particular storage technology.
The library comes by default with 3 implementations (in-memory storage, mongodb, redis).
However this might not fit your need so feel free to implement your own!
Okay, let's first create some in-memory state machine. We'll first create in-memory strategy and then use it to instantiate FSM Manager.
const { semaphoreDefinition } = require('./semaphore')
const { createStrategyMemory, createFsmManager } = require('../src')
async function runExample () {
let strategyMemory = createStrategyMemory()
const fsmManager = createFsmManager(strategyMemory, semaphoreDefinition)
const semaphore = await fsmManager.fsmCreate('id-1')
console.log(`Semaphore1 is in state ${await semaphore.getState()}.`)
}
runExample()
Executable at docs/step1-creating.js
As you can see, you need 2 pieces to create FSM Manager:
- Storage strategy (how and where are machines data serialized/deserialized)
- FSM Definition (what does the FSM Model looks like)
Now that we have FSM Manager, let's create some machine. The machine will only perform a transitions as far as it's valid transition according to current machine state and machine definition.
const { semaphoreDefinition } = require('./semaphore')
const { createStrategyMemory, createFsmManager } = require('../src')
async function runExample () {
let strategyMemory = createStrategyMemory()
const fsmManager = createFsmManager(strategyMemory, semaphoreDefinition)
const semaphore = await fsmManager.fsmCreate('id-1')
console.log(`Semaphore1 is in state ${await semaphore.getState()}.`)
// function doTransition(transitionName) invokes transitions. If the transition
// is valid according to provided FSM definition, machine changes its state.
await semaphore.doTransition('enable')
console.log(`Semaphore1 is in state ${await semaphore.getState()}.`)
}
runExample()
Executable at docs/step2-transitioning.js
The code prints
Semaphore1 is in state off.
Semaphore1 is in state red.
When we call fsmCreate
, the machine with given ID is persisted in the storage, starting in state
specified by initialState
from the machine definition. The function returns us an object which is
used to further access and manipulate this particular machine representation in the storage.
When we call doTransition
, current state is loaded from storage, checks are performed whether requested
transition is possible (according to supplied machine definition). If yes, updated state is persisted in storage.
As FSM Manager handles machine persistence, you can create machine, do some transitions and forget about. Then later ask FSM Manager for the machine with the same ID and you have it back!
const { semaphoreDefinition } = require('./semaphore')
const { createStrategyMemory, createFsmManager } = require('../src')
async function runExample () {
let strategyMemory = createStrategyMemory()
const fsmManager = createFsmManager(strategyMemory, semaphoreDefinition)
let semaphore = await fsmManager.fsmCreate('id-1')
console.log(`Semaphore1 is in state ${await semaphore.getState()}.`)
await semaphore.doTransition('enable')
sema1state = await semaphore.getState()
console.log(`Semaphore1 is in state ${sema1state}.`)
// let's forget about our semaphore instance for now
delete semaphore
// just to find it later again!
const semaphoreReloaded = await fsmManager.fsmLoad('id-1')
sema1state = await semaphoreReloaded.getState()
console.log(`Reloaded Semaphore1 is in state ${sema1state}.`)
}
runExample()
Executable at docs/step3-reloading.js
The code prints
Semaphore1 is in state red.
Reloaded Semaphore1 is in state red.
const { semaphoreDefinition } = require('./semaphore')
const { createStrategyMemory, createFsmManager } = require('../src')
async function runExample () {
let strategyMemory = createStrategyMemory()
const fsmManager = createFsmManager(strategyMemory, semaphoreDefinition)
const semaphore = await fsmManager.fsmCreate('id-1')
console.log(await semaphore.getState())
await semaphore.doTransition('enable')
await semaphore.doTransition('next')
const semaphoreReloaded = await fsmManager.fsmLoad('id-1')
const history = await semaphoreReloaded.getHistory()
console.log(`Current history of Semaphore id-1 is: ${JSON.stringify(history, null, 2)}`)
// Current history of Semaphore1 is: [
// {
// "state": "off",
// "transition": "enable"
// },
// {
// "state": "red",
// "transition": "next"
// }
}
runExample()
Executable at docs/step4-history.js
The whole thing becomes more useful once we start to use persistent storage implementations! The module comes with 3 reference implementations of storage layer - memory, MongoDB and Redis. We've previously used in-memory storage for case of simplicity. Let's try Mongo and Redis.
const { createStrategyMongo } = require('../src')
const { semaphoreDefinition } = require('./semaphore')
const { createFsmManager } = require('../src')
const util = require('util')
const MongoClient = require('mongodb')
async function runExample () {
const MONGO_URL = process.env.MONGO_URL || 'mongodb:https://localhost:27017'
const asyncMongoConnect = util.promisify(MongoClient.connect)
const mongoHost = await asyncMongoConnect(MONGO_URL)
const mongoDatabase = await mongoHost.db(`FSM-DEMO`)
const collection = await mongoDatabase.collection(`FSM-DEMO-${Date.now()}`)
// and we can use it exactly like we did previously
const strategy = createStrategyMongo(collection)
const fsmManager = createFsmManager(strategy, semaphoreDefinition)
let semaphore = await fsmManager.fsmCreate('id1')
await semaphore.doTransition('enable')
console.log(`Semaphore is in state ${await semaphore.getState()}.`)
}
runExample()
Executable at docs/step5a-mongo-storage.js
The code prints
Semaphore1 is in state red.
const { createStrategyRedis } = require('../src')
const { semaphoreDefinition } = require('./semaphore')
const { createFsmManager } = require('../src')
const redis = require('redis')
async function runExample () {
const REDIS_URL = process.env.REDIS_URL || 'redis:https://localhost:6379'
const redisClient = redis.createClient(REDIS_URL)
// and we can use it exactly like we did previously
const strategy = createStrategyRedis(redisClient, 'fsm-demo')
const fsmManager = createFsmManager(strategy, semaphoreDefinition)
let semaphore = await fsmManager.fsmCreate('id1')
await semaphore.doTransition('enable')
console.log(`Semaphore is in state ${await semaphore.getState()}.`)
}
runExample()
Executable at docs/step5b-redis-storage.js
The code prints
Semaphore1 is in state red.
TODO: Example for creating .dot
file and rendering as image.
All provided storage implementations share the same interface, but it's very likely you will have just a slightly different needs. Maybe you want to organize the data in database in a different way, or maybe you want to identify each machine by 2 keys, instead of only single id.
In that case I encourage you to look into src/core/fsm-storage
reference implementations and use them
as starters for your implementations.
Please take look at test/core/fsm-identified-by-id.spec.js
to see more examples for deeper understanding
of this FSM implementation.
- add package.json commands to run tutorial steps
- add bin command to package generate and render dotfiles
- add examples for fsm manager and querying multiple machines
- note in tutorial that state machine also keep utime of creation and last update