u-machine
v0.5.1
Published
Finite state machine micro helper
Downloads
11
Maintainers
Readme
Finite state machine micro helper for Node.js
If you need a state-machine-like behavior, and feel that great frameworks like the one of Jake Gordon, machina of Jim Cowart or Stately.js of Florian Schäfer is too much for you, then this helper may be just what you are looking for.
Based on the KISS and YAGNI principles, the core of this module is just a couple of functions.
Use cases
- OAuth client for authorization with Facebook, Google, Yandex and Vkontakte.
How to use
npm install u-machine
const machine = require('u-machine')
Pass any object to the machine
. It returns a runner function which will be the entry point for all events. The object is required to have just one property states
where states defined as functions:
const run = machine({
states: {
initial: function () {},
// ...
any_name: function () {}
}
}
The machine creates other properties:
current - A current state function
prior - The last state function the machine made a transition from
machine - Reference to the runner function
Running
Run the machine passing events to it. Events may be just any stuff you want. The machine passes all parameters you throw to it to a function corresponding to the current state.
const run = machine({...})
run() // event is undefined
run({}, [], function () {})
Initial state
The definition of initial state for the state machine is required. There are two ways to define the initial state. One way is to name any state as initial
:
machine({
states: {
initial: function () {
// some code
}
}
})
Other way is to create initial
function which returns the initial state:
machine({
initial: function () {
return this.states.stop
},
states: {
stop: function () {
// some code
}
}
})
If the both ways are mixed then the initial
state will be used:
const run = machine({
initial: function () {
console.log('This function will not be called')
},
states: {
initial: function () {
console.log('The initial state')
}
}
})
run() // The initial state
Current state
You may wonder how to get the current state the machine at. Since the states are the functions you may use named state function and then use .current.name
property (make sure it's supported). Alternatively, if you use anonymous functions, you can pass states to machine.deanonymize
method. It creates named
properties equal the states functions names.
const o = {
states: {
initial: function () {},
final: function () {}
}
}
machine.deanonymize(o.states) // [ 'initial', 'final' ]
machine(o)
o.current.named // 'initial'
Transitions
To make a transition to another state the state function should return a state the machine jumps to. If no state is returned then the machine remains at the same state.
const run = machine({
initial: function () {
return this.states.stop
},
states: {
stop: function () {
// some code
return this.states.run
},
run: function () {
// will stay here forever
}
}
})
// The current state is 'stop'
run() // Makes transition from 'stop' to 'run'
run() // Makes transition from 'run' to 'run'
In state functions the keyword this
always refers to the object you created the machine with:
const obj = {
states: {
initial: function (text) {
console.log(this === obj, text)
}
}
}
const run = machine(obj)
run('A') // true 'A'
run.call({}, 'B') // true 'B'
Your machine may have many event sources. If you need to observe all of them, create a transition
function in your object. The events you pass to the machine will also be passed to this function after they are processed in the current state:
const run = machine({
states: {
initial: function (o) {
o.n += 1
}
},
transition: function (o) {
console.log(o.n)
}
})
run({n: 1}) // 2
Logging (debugging) transitions
Inside the transition function the keyword this
refers to the object with states description. The machine jumps from the this.prior
state to the this.current
state. However, if states are anonymous functions, it is hard to understand which actually the prior and current states are.
Here is a solution you may use:
const obj = {
states: {
initial: function () {
return this.states.run
},
run: function () {}
},
transition: function () {
console.log('transition from', this.prior.named,
'to', this.current.named)
}
}
const run = machine(obj)
machine.deanonymize(obj.states)
run() // transition from initial to run
run() // transition from run to run
The deanonymize
method creates properties for functions with the same values as the object's keys. You can change default property name named
to another passing it as a second argument:
machine.deanonymize(obj.states, 'stateName')
External and internal events
Let's say we want the machine to count up to 10. Below is the example where we both control the counter and fire events externally:
const obj = {
counter: 0,
states: {
initial: function () {
return this.states.run
},
run: function () {
this.counter += 1
}
},
transition: function () {
console.log(this.counter)
}
}
const run = machine(obj)
while (obj.counter < 10) {
run()
}
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
It may be more convenient to let the machine control the logic, and fire events to itself internally. The events entry function is accessible by the machine
property:
const run = machine({
counter: 0,
states: {
initial: function () {
setImmediate(this.machine)
return this.states.run
},
run: function () {
this.counter += 1
if (this.counter < 10) {
setImmediate(this.machine)
}
}
},
transition: function () {
console.log(this.counter)
}
})
run() // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Another way, where we fire events at a single point:
const run = machine({
counter: 0,
states: {
initial: function () {
return this.states.run
},
run: function () {
this.counter += 1
}
},
transition: function () {
console.log(this.counter)
if (this.counter < 10) {
setImmediate(this.machine)
}
}
})
run() // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Named events
In many cases we have one type of events in one state. If in one state we need to distinguish events then the name of event can be passed to the runner function.
const run = machine({
states: {
initial: function (event) {
switch (event) {
case 'foo': return this.states.final
case 'bar': break
}
},
final: function (event) {
switch (event) {
case 'foo': break
case 'bar': return this.states.initial
}
}
}
})
run('foo')
run('bar')
To make this approach simplier and robust this module provides a helper. First the example:
const machine = require('u-machine')
const events = require('u-machine/events')
const run = machine({
states: {
initial: function (event) {
console.log(event)
}
}
})
events(run, ['foo', 'bar'])
run.foo() // 'foo'
run.bar() // 'bar'
Syntax:
let object = events(function *runner*, *events*[, *object*])
runner - The function which will be called when one of event function is called.
events - Array of events' names. This names are used to created named functions, and will be passed as the first argument to the runner function.
object - Optional object to which the named function will be attached as methods. If omitted, the methods will be attached to the runner function.
Multiple instances
The simplest way to have multiple instances:
const o = {
name: 'Run',
states: {
initial: function () {
console.log(Object.getPrototypeOf(this).name, this.name)
}
}
}
const a = Object.create(o)
const b = Object.create(o)
a.name = 'Fast'
b.name = 'Long'
machine(a)() // Run Fast
machine(b)() // Run Long