diff --git a/.gitignore b/.gitignore index 2ff1e06..e9d88de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ npm-debug.log* .DS_Store .nyc_output +package-lock.json diff --git a/index.js b/index.js index e0346c6..6c20e8f 100644 --- a/index.js +++ b/index.js @@ -11,20 +11,56 @@ function Nanostate (initialState, transitions) { this.transitions = transitions this.state = initialState + this.submachines = {} + this._submachine = null Nanobus.call(this) } Nanostate.prototype = Object.create(Nanobus.prototype) +Nanostate.prototype.constructor = Nanostate + Nanostate.prototype.emit = function (eventName) { - var nextState = this.transitions[this.state][eventName] + var nextState = this._next(eventName) assert.ok(nextState, `nanostate.emit: invalid transition ${this.state} -> ${eventName}`) + if (this._submachine && Object.keys(this.transitions).indexOf(nextState) !== -1) { + this._unregister() + } + this.state = nextState Nanobus.prototype.emit.call(this, eventName) } +Nanostate.prototype.event = function (eventName, machine) { + this.submachines[eventName] = machine +} + Nanostate.parallel = function (transitions) { return new Parallelstate(transitions) } + +Nanostate.prototype._unregister = function () { + if (this._submachine) { + this._submachine._unregister() + this._submachine = null + } +} + +Nanostate.prototype._next = function (eventName) { + if (this._submachine) { + var nextState = this._submachine._next(eventName) + if (nextState) { + return nextState + } + } + + var submachine = this.submachines[eventName] + if (submachine) { + this._submachine = submachine + return submachine.state + } + + return this.transitions[this.state][eventName] +} diff --git a/test.js b/test.js deleted file mode 100644 index 0f5692d..0000000 --- a/test.js +++ /dev/null @@ -1,94 +0,0 @@ -var tape = require('tape') - -var nanostate = require('./') - -tape('sets an initial state', function (assert) { - var machine = nanostate('green', { - green: { timer: 'yellow' }, - yellow: { timer: 'red' }, - red: { timer: 'green' } - }) - assert.equal(machine.state, 'green') - assert.end() -}) - -tape('change state', function (assert) { - var machine = nanostate('green', { - green: { timer: 'yellow' }, - yellow: { timer: 'red' }, - red: { timer: 'green' } - }) - - move(assert, machine, [ - ['timer', 'yellow'], - ['timer', 'red'], - ['timer', 'green'] - ]) - assert.end() -}) - -// Move the machine a bunch of states. -function move (assert, machine, states) { - states.forEach(function (tuple) { - var initial = machine.state - var expected = tuple[1] - machine.emit(tuple[0]) - assert.equal(machine.state, expected, `from ${initial} to ${expected}`) - }) -} - -var createParallelTransitions = () => ({ - bold: nanostate('off', { - on: { 'toggle': 'off' }, - off: { 'toggle': 'on' } - }), - underline: nanostate('off', { - on: { 'toggle': 'off' }, - off: { 'toggle': 'on' } - }), - italics: nanostate('off', { - on: { 'toggle': 'off' }, - off: { 'toggle': 'on' } - }), - list: nanostate('none', { - none: { bullets: 'bullets', numbers: 'numbers' }, - bullets: { none: 'none', numbers: 'numbers' }, - numbers: { bullets: 'bullets', none: 'none' } - }) -}) - -tape('create parallel state', (assert) => { - var machine = nanostate.parallel(createParallelTransitions()) - - machine.emit('bold:toggle') - assert.deepEqual(machine.state, { - bold: 'on', underline: 'off', italics: 'off', list: 'none' - }) - - assert.end() -}) - -tape('change states in parallel machine', (assert) => { - var machine = nanostate.parallel(createParallelTransitions()) - - machine.emit('underline:toggle') - machine.emit('list:numbers') - assert.deepEqual(machine.state, { - bold: 'off', underline: 'on', italics: 'off', list: 'numbers' - }) - - machine.emit('bold:toggle') - machine.emit('underline:toggle') - machine.emit('italics:toggle') - machine.emit('list:bullets') - assert.deepEqual(machine.state, { - bold: 'on', underline: 'off', italics: 'on', list: 'bullets' - }) - - machine.emit('list:none') - assert.deepEqual(machine.state, { - bold: 'on', underline: 'off', italics: 'on', list: 'none' - }) - - assert.end() -}) diff --git a/test/hierarchical.js b/test/hierarchical.js new file mode 100644 index 0000000..ccb5a48 --- /dev/null +++ b/test/hierarchical.js @@ -0,0 +1,54 @@ +var tape = require('tape') + +var nanostate = require('../') +var move = require('./move') + +tape('change to substate and back', function (assert) { + var machine = nanostate('green', { + green: { timer: 'yellow' }, + yellow: { timer: 'red' }, + red: { timer: 'green' } + }) + + machine.event('powerOutage', nanostate('flashingRed', { + flashingRed: { powerRestored: 'green' } + })) + + move(assert, machine, [ + ['timer', 'yellow'], + ['powerOutage', 'flashingRed'], + ['powerRestored', 'green'] + ]) + + assert.end() +}) + +tape('move down two levels', function (assert) { + var trafficLights = nanostate('green', { + green: { timer: 'yellow' }, + yellow: { timer: 'red' }, + red: { timer: 'green' } + }) + + var powerOutage = nanostate('flashingRed', { + flashingRed: { powerRestored: 'green' } + }) + + var apocalypse = nanostate('darkness', { + darkness: { worldSaved: 'green' } + }) + + trafficLights.event('powerOutage', powerOutage) + powerOutage.event('apocalypse', apocalypse) + + move(assert, trafficLights, [ + ['powerOutage', 'flashingRed'], + ['apocalypse', 'darkness'], + ['worldSaved', 'green'] + ]) + + assert.equal(trafficLights._submachine, null, 'first level submachine is unregistered') + assert.equal(powerOutage._submachine, null, 'second level submachine is unregistered') + + assert.end() +}) diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..ef47d6f --- /dev/null +++ b/test/index.js @@ -0,0 +1,3 @@ +require('./nanostate') +require('./parallel') +require('./hierarchical') diff --git a/test/move.js b/test/move.js new file mode 100644 index 0000000..3d0645e --- /dev/null +++ b/test/move.js @@ -0,0 +1,11 @@ +module.exports = move + +// Move the machine a bunch of states. +function move (assert, machine, states) { + states.forEach(function (tuple) { + var initial = machine.state + var expected = tuple[1] + machine.emit(tuple[0]) + assert.equal(machine.state, expected, `from ${initial} to ${expected}`) + }) +} diff --git a/test/nanostate.js b/test/nanostate.js new file mode 100644 index 0000000..bbf49d0 --- /dev/null +++ b/test/nanostate.js @@ -0,0 +1,29 @@ +var tape = require('tape') + +var nanostate = require('../') +var move = require('./move') + +tape('sets an initial state', function (assert) { + var machine = nanostate('green', { + green: { timer: 'yellow' }, + yellow: { timer: 'red' }, + red: { timer: 'green' } + }) + assert.equal(machine.state, 'green') + assert.end() +}) + +tape('change state', function (assert) { + var machine = nanostate('green', { + green: { timer: 'yellow' }, + yellow: { timer: 'red' }, + red: { timer: 'green' } + }) + + move(assert, machine, [ + ['timer', 'yellow'], + ['timer', 'red'], + ['timer', 'green'] + ]) + assert.end() +}) diff --git a/test/parallel.js b/test/parallel.js new file mode 100644 index 0000000..cb00841 --- /dev/null +++ b/test/parallel.js @@ -0,0 +1,61 @@ +var tape = require('tape') + +var nanostate = require('../') + +tape('create parallel state', (assert) => { + var machine = nanostate.parallel(createParallelTransitions()) + + machine.emit('bold:toggle') + assert.deepEqual(machine.state, { + bold: 'on', underline: 'off', italics: 'off', list: 'none' + }) + + assert.end() +}) + +tape('change states in parallel machine', (assert) => { + var machine = nanostate.parallel(createParallelTransitions()) + + machine.emit('underline:toggle') + machine.emit('list:numbers') + assert.deepEqual(machine.state, { + bold: 'off', underline: 'on', italics: 'off', list: 'numbers' + }) + + machine.emit('bold:toggle') + machine.emit('underline:toggle') + machine.emit('italics:toggle') + machine.emit('list:bullets') + assert.deepEqual(machine.state, { + bold: 'on', underline: 'off', italics: 'on', list: 'bullets' + }) + + machine.emit('list:none') + assert.deepEqual(machine.state, { + bold: 'on', underline: 'off', italics: 'on', list: 'none' + }) + + assert.end() +}) + +function createParallelTransitions () { + return { + bold: nanostate('off', { + on: { 'toggle': 'off' }, + off: { 'toggle': 'on' } + }), + underline: nanostate('off', { + on: { 'toggle': 'off' }, + off: { 'toggle': 'on' } + }), + italics: nanostate('off', { + on: { 'toggle': 'off' }, + off: { 'toggle': 'on' } + }), + list: nanostate('none', { + none: { bullets: 'bullets', numbers: 'numbers' }, + bullets: { none: 'none', numbers: 'numbers' }, + numbers: { bullets: 'bullets', none: 'none' } + }) + } +}