├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.gz 2 | .DS_Store 3 | coverage/ 4 | node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "0.10" 3 | - "1" 4 | sudo: false 5 | language: node_js 6 | script: "npm run test:cov" 7 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fsm-event 2 | [![NPM version][npm-image]][npm-url] 3 | [![build status][travis-image]][travis-url] 4 | [![Test coverage][coveralls-image]][coveralls-url] 5 | [![Downloads][downloads-image]][downloads-url] 6 | [![js-standard-style][standard-image]][standard-url] 7 | 8 | Stateful finite state machine wrapper around 9 | [`fsm`](https://github.com/dominictarr/fsm). Emits events when transitioning 10 | states. 11 | 12 | ## Installation 13 | ```bash 14 | $ npm install fsm-event 15 | ``` 16 | 17 | ## Usage 18 | ```js 19 | const fsm = require('fsm-event') 20 | 21 | const m = fsm('START', { 22 | START: { pause: 'PAUSED' }, 23 | PAUSED: { resume: 'START' } 24 | }) 25 | 26 | m.on('START:leave', cb => console.log('leaving start!'); cb()) 27 | m.on('PAUSED', () => console.log('paused state!')) 28 | 29 | m('pause') 30 | // 'leaving start' 31 | // 'paused state!' 32 | ``` 33 | 34 | ## API 35 | ### m = fsm([start,] events) 36 | Create a state machine. `start` defaults to `START`. 37 | 38 | ### m.on(event, cb) 39 | Attach a listener to the state machine. See [events](#Events) for an overview 40 | of all events. 41 | 42 | ### m(event) 43 | Transition states in the state machine. Must be a valid transition defined on 44 | initalization. Will throw if an invalid transition is triggered. Alias: 45 | `m.emit(event)`. 46 | 47 | ## Events 48 | Each state transition triggers 3 events. __important:__ When listening to 49 | `:enter` or `:leave` events, the callback must be called so that the state 50 | machine can proceed to the next state. 51 | ```txt 52 | error incorrect transition 53 | when new state is entered 54 | :enter when transitioning into state 55 | :leave when transitioning away from state 56 | done when state transition finished 57 | ``` 58 | 59 | ## Why? 60 | Most state machines have overly complicated interfaces for managing state. The 61 | fsm state machine is simple but doesn't manage state for you, so I wrote a 62 | wrapper around it that manages state in an event-driven way. The initial use 63 | case was to manage complex, stateful UI elements but it can be used anywhere. 64 | 65 | ## See Also 66 | - [fsm](https://github.com/dominictarr/fsm) 67 | - [javascript-state-machine](https://github.com/jakesgordon/javascript-state-machine) 68 | - [statement](https://github.com/timoxley/statement) 69 | - [stream-fsm](https://www.npmjs.com/package/stream-fsm) 70 | 71 | ## License 72 | [MIT](https://tldrlegal.com/license/mit-license) 73 | 74 | [npm-image]: https://img.shields.io/npm/v/fsm-event.svg?style=flat-square 75 | [npm-url]: https://npmjs.org/package/fsm-event 76 | [travis-image]: https://img.shields.io/travis/yoshuawuyts/fsm-event/master.svg?style=flat-square 77 | [travis-url]: https://travis-ci.org/yoshuawuyts/fsm-event 78 | [coveralls-image]: https://img.shields.io/coveralls/yoshuawuyts/fsm-event.svg?style=flat-square 79 | [coveralls-url]: https://coveralls.io/r/yoshuawuyts/fsm-event?branch=master 80 | [downloads-image]: http://img.shields.io/npm/dm/fsm-event.svg?style=flat-square 81 | [downloads-url]: https://npmjs.org/package/fsm-event 82 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 83 | [standard-url]: https://github.com/feross/standard 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | const assert = require('assert') 3 | const fsm = require('fsm') 4 | 5 | module.exports = fsmEvent 6 | 7 | // create an fsmEvent instance 8 | // obj -> fn 9 | function fsmEvent (start, events) { 10 | if (typeof start === 'object') { 11 | events = start 12 | start = 'START' 13 | } 14 | assert.equal(typeof start, 'string') 15 | assert.equal(typeof events, 'object') 16 | assert.ok(events[start], 'invalid starting state ' + start) 17 | assert.ok(fsm.validate(events)) 18 | 19 | const emitter = new EventEmitter() 20 | emit._graph = fsm.reachable(events) 21 | emit._emitter = emitter 22 | emit._events = events 23 | emit._state = start 24 | emit.emit = emit 25 | emit.on = on 26 | 27 | return emit 28 | 29 | // set a state listener 30 | // str, fn -> null 31 | function on (event, cb) { 32 | emitter.on(event, cb) 33 | } 34 | 35 | // change the state 36 | // str -> null 37 | function emit (str) { 38 | const nwState = emit._events[emit._state][str] 39 | if (!reach(emit._state, nwState, emit._graph)) { 40 | const err = 'invalid transition: ' + emit._state + ' -> ' + str 41 | return emitter.emit('error', err) 42 | } 43 | 44 | const leaveEv = emit._state + ':leave' 45 | const enterEv = nwState + ':enter' 46 | 47 | if (!emit._state) return enter() 48 | return leave() 49 | 50 | function leave () { 51 | if (!emitter._events[leaveEv]) enter() 52 | else emitter.emit(leaveEv, enter) 53 | } 54 | 55 | function enter () { 56 | if (!emitter._events[enterEv]) done() 57 | else emitter.emit(enterEv, done) 58 | } 59 | 60 | function done () { 61 | emit._state = nwState 62 | emitter.emit(nwState) 63 | emitter.emit('done') 64 | } 65 | } 66 | } 67 | 68 | // check if state can reach in reach 69 | // str, str, obj -> bool 70 | function reach (curr, next, reachable) { 71 | if (!next) return false 72 | if (!curr) return true 73 | 74 | const here = reachable[curr] 75 | if (!here || !here[next]) return false 76 | return here[next].length === 1 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fsm-event", 3 | "version": "2.1.0", 4 | "description": "Stateful finite state machine wrapper around fsm", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && NODE_ENV=test node test", 8 | "test:cov": "standard && NODE_ENV=test istanbul cover test.js" 9 | }, 10 | "repository": "yoshuawuyts/fsm-event", 11 | "keywords": [ 12 | "state machine", 13 | "state", 14 | "simple", 15 | "unix", 16 | "minimal", 17 | "event" 18 | ], 19 | "license": "MIT", 20 | "dependencies": { 21 | "fsm": "^1.0.2" 22 | }, 23 | "devDependencies": { 24 | "istanbul": "^0.3.13", 25 | "standard": "^3.7.3", 26 | "tape": "^4.0.0" 27 | }, 28 | "files": [ 29 | "LICENSE", 30 | "index.js", 31 | "README.md" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const fsm = require('./') 3 | 4 | test('should validate input values', function (t) { 5 | t.plan(4) 6 | 7 | t.throws(fsm.bind(null, 123, 123), /string/) 8 | t.throws(fsm.bind(null, 'UP', 123), /object/) 9 | 10 | const state1 = { 11 | UP: {down: 'DOWN'}, 12 | DOWN: {up: 'UP'} 13 | } 14 | 15 | t.doesNotThrow(fsm.bind(null, 'UP', state1)) 16 | 17 | const state2 = { 18 | UP: {down: 'DOWN'}, 19 | DOWN: {up: 'END'} 20 | } 21 | 22 | t.throws(fsm.bind(null, 'UP', state2), /state/) 23 | }) 24 | 25 | test('m.on() should attach events', function (t) { 26 | t.plan(2) 27 | 28 | const m = fsm('UP', { 29 | UP: {down: 'DOWN'}, 30 | DOWN: {up: 'UP'} 31 | }) 32 | 33 | m.on('DOWN', function () { 34 | t.pass('cb called') 35 | }) 36 | 37 | t.equal(typeof m._emitter._events['DOWN'], 'function') 38 | m('down') 39 | }) 40 | 41 | test('m.emit() should catch invalid state transitions', function (t) { 42 | t.plan(3) 43 | 44 | const m = fsm('UP', { 45 | UP: {down: 'DOWN'}, 46 | DOWN: {up: 'UP'} 47 | }) 48 | 49 | t.equal(m._state, 'UP') 50 | m('down') 51 | t.equal(m._state, 'DOWN') 52 | m.on('error', function (err) { 53 | t.equal(err, 'invalid transition: DOWN -> END') 54 | }) 55 | m('END') 56 | }) 57 | 58 | test('m.emit() should set the state', function (t) { 59 | t.plan(3) 60 | 61 | const m = fsm('UP', { 62 | UP: {down: 'DOWN'}, 63 | DOWN: {up: 'UP'} 64 | }) 65 | 66 | m.on('DOWN', function () { 67 | t.pass('cb called') 68 | }) 69 | 70 | t.equal(m._state, 'UP') 71 | m('down') 72 | t.equal(m._state, 'DOWN') 73 | }) 74 | 75 | test('m.emit() should emit enter events', function (t) { 76 | t.plan(2) 77 | 78 | const m = fsm('UP', { 79 | UP: {down: 'DOWN'}, 80 | DOWN: {up: 'UP'} 81 | }) 82 | 83 | m.on('UP:enter', function (cb) { 84 | t.pass('enter') 85 | cb() 86 | }) 87 | 88 | m.on('UP', function () { 89 | t.pass('UP') 90 | }) 91 | 92 | m.emit('down') 93 | m.emit('up') 94 | }) 95 | 96 | test('m.emit() should emit events in sequence', function (t) { 97 | t.plan(5) 98 | 99 | var i = 0 100 | const m = fsm('DOWN', { 101 | UP: {down: 'DOWN'}, 102 | DOWN: {up: 'UP'} 103 | }) 104 | 105 | m.on('UP:enter', function (cb) { 106 | t.equal(++i, 1) 107 | cb() 108 | }) 109 | 110 | m.on('UP', function () { 111 | t.equal(++i, 2) 112 | }) 113 | 114 | m.on('UP:leave', function (cb) { 115 | t.equal(++i, 3) 116 | cb() 117 | }) 118 | 119 | m.on('DOWN:enter', function (cb) { 120 | t.equal(++i, 4) 121 | cb() 122 | }) 123 | 124 | m.on('DOWN', function () { 125 | t.equal(++i, 5) 126 | }) 127 | 128 | m.emit('up') 129 | m.emit('down') 130 | }) 131 | 132 | test('should emit an end event when done', function (t) { 133 | t.plan(1) 134 | const m = fsm('DOWN', { 135 | UP: {down: 'DOWN'}, 136 | DOWN: {up: 'UP'} 137 | }) 138 | m.on('done', function () { 139 | t.pass('called') 140 | }) 141 | m.emit('up') 142 | }) 143 | --------------------------------------------------------------------------------