diff --git a/src/jsm.js b/src/jsm.js index f83c8de..a2e6098 100644 --- a/src/jsm.js +++ b/src/jsm.js @@ -7,171 +7,181 @@ var mixin = require('./util/mixin'), //------------------------------------------------------------------------------------------------- function JSM(context, config) { - this.context = context; - this.config = config; - this.state = config.init.from; - this.observers = [context]; + this.context = context; + this.config = config; + this.state = config.init.from; + this.observers = [context]; } //------------------------------------------------------------------------------------------------- mixin(JSM.prototype, { - init: function(args) { - mixin(this.context, this.config.data.apply(this.context, args)); - plugin.hook(this, 'init'); - if (this.config.init.active) - return this.fire(this.config.init.name, []); - }, - - is: function(state) { - return Array.isArray(state) ? (state.indexOf(this.state) >= 0) : (this.state === state); - }, - - isPending: function() { - return this.pending; - }, - - can: function(transition) { - return !this.isPending() && !!this.seek(transition); - }, - - cannot: function(transition) { - return !this.can(transition); - }, - - allStates: function() { - return this.config.allStates(); - }, - - allTransitions: function() { - return this.config.allTransitions(); - }, - - transitions: function() { - return this.config.transitionsFor(this.state); - }, - - seek: function(transition, args) { - var wildcard = this.config.defaults.wildcard, - entry = this.config.transitionFor(this.state, transition), - to = entry && entry.to; - if (typeof to === 'function') - return to.apply(this.context, args); - else if (to === wildcard) - return this.state - else - return to - }, - - fire: function(transition, args) { - return this.transit(transition, this.state, this.seek(transition, args), args); - }, - - transit: function(transition, from, to, args) { - - var lifecycle = this.config.lifecycle, - changed = this.config.options.observeUnchangedState || (from !== to); - - if (!to) - return this.context.onInvalidTransition(transition, from, to); - - if (this.isPending()) - return this.context.onPendingTransition(transition, from, to); - - this.config.addState(to); // might need to add this state if it's unknown (e.g. conditional transition or goto) - - this.beginTransit(); - - args.unshift({ // this context will be passed to each lifecycle event observer - transition: transition, - from: from, - to: to, - fsm: this.context - }); - - return this.observeEvents([ - this.observersForEvent(lifecycle.onBefore.transition), - this.observersForEvent(lifecycle.onBefore[transition]), - changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED, - this.observersForEvent(lifecycle.on.transition), - changed ? [ 'doTransit', [ this ] ] : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.onEnter[to]) : UNOBSERVED, - changed ? this.observersForEvent(lifecycle.on[to]) : UNOBSERVED, - this.observersForEvent(lifecycle.onAfter.transition), - this.observersForEvent(lifecycle.onAfter[transition]), - this.observersForEvent(lifecycle.on[transition]) - ], args); - }, - - beginTransit: function() { this.pending = true; }, - endTransit: function(result) { this.pending = false; return result; }, - failTransit: function(result) { this.pending = false; throw result; }, - doTransit: function(lifecycle) { this.state = lifecycle.to; }, - - observe: function(args) { - if (args.length === 2) { - var observer = {}; - observer[args[0]] = args[1]; - this.observers.push(observer); + init: async function (args) { + mixin(this.context, this.config.data.apply(this.context, args)); + plugin.hook(this, 'init'); + if (this.config.init.active) + return await this.fire(this.config.init.name, []); + }, + + is: function (state) { + return Array.isArray(state) ? (state.indexOf(this.state) >= 0) : (this.state === state); + }, + + isPending: function () { + return this.pending; + }, + + can: function (transition) { + return !this.isPending() && !!this.seek(transition); + }, + + cannot: function (transition) { + return !this.can(transition); + }, + + allStates: function () { + return this.config.allStates(); + }, + + allTransitions: function () { + return this.config.allTransitions(); + }, + + transitions: function () { + return this.config.transitionsFor(this.state); + }, + + seek: function (transition, args) { + var wildcard = this.config.defaults.wildcard, + entry = this.config.transitionFor(this.state, transition), + to = entry && entry.to; + if (typeof to === 'function') + return to.apply(this.context, args); + else if (to === wildcard) + return this.state + else + return to + }, + + fire: async function (transition, args) { + let r = await this.transit(transition, this.state, this.seek(transition, args), args); + return r; + }, + + transit: async function (transition, from, to, args) { + + var lifecycle = this.config.lifecycle, + changed = this.config.options.observeUnchangedState || (from !== to); + + if (!to) + return this.context.onInvalidTransition(transition, from, to); + + if (this.isPending()) + return this.context.onPendingTransition(transition, from, to); + + this.config.addState(to); // might need to add this state if it's unknown (e.g. conditional transition or goto) + + this.beginTransit(); + + args.unshift({ // this context will be passed to each lifecycle event observer + transition: transition, + from: from, + to: to, + fsm: this.context + }); + + let r = await this.observeEvents([ + this.observersForEvent(lifecycle.onBefore.transition), + this.observersForEvent(lifecycle.onBefore[transition]), + changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED, + changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED, + this.observersForEvent(lifecycle.on.transition), + changed ? ['doTransit', [this]] : UNOBSERVED, + changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED, + changed ? this.observersForEvent(lifecycle.onEnter[to]) : UNOBSERVED, + changed ? this.observersForEvent(lifecycle.on[to]) : UNOBSERVED, + this.observersForEvent(lifecycle.onAfter.transition), + this.observersForEvent(lifecycle.onAfter[transition]), + this.observersForEvent(lifecycle.on[transition]) + ], args); + return r; + }, + + beginTransit: function () { this.pending = true; }, + endTransit: function (result) { this.pending = false; return result; }, + failTransit: function (result) { this.pending = false; throw result; }, + doTransit: function (lifecycle) { this.state = lifecycle.to; }, + + observe: function (args) { + if (args.length === 2) { + var observer = {}; + observer[args[0]] = args[1]; + this.observers.push(observer); + } + else { + this.observers.push(args[0]); + } + }, + + observersForEvent: function (event) { // TODO: this could be cached + var n = 0, max = this.observers.length, observer, result = []; + for (; n < max; n++) { + observer = this.observers[n]; + if (observer[event]) + result.push(observer); + } + return [event, result, true] + }, + + observeEvents: async function (events, args, previousEvent, previousResult) { + if (events.length === 0) { + return this.endTransit(previousResult === undefined ? true : previousResult); + } + + var event = events[0][0], + observers = events[0][1], + pluggable = events[0][2]; + + args[0].event = event; + if (event && pluggable && event !== previousEvent) + plugin.hook(this, 'lifecycle', args); + + if (observers.length === 0) { + events.shift(); + return await this.observeEvents(events, args, event, previousResult); + } + else { + var observer = observers.shift(); + let result = true; + + if (observer[event].constructor.name === 'AsyncFunction') { + result = await observer[event].apply(observer, args); + } + else { + result = observer[event].apply(observer, args); + } + + if (result && typeof result.then === 'function') { + result = await result.then((r) => r).catch((f) => false); + } + + if (result === false) { + return this.endTransit(false); + } + else { + return await this.observeEvents(events, args, event, result); + } + } + }, + + onInvalidTransition: function (transition, from, to) { + throw new Exception("transition is invalid in current state", transition, from, to, this.state); + }, + + onPendingTransition: function (transition, from, to) { + throw new Exception("transition is invalid while previous transition is still in progress", transition, from, to, this.state); } - else { - this.observers.push(args[0]); - } - }, - - observersForEvent: function(event) { // TODO: this could be cached - var n = 0, max = this.observers.length, observer, result = []; - for( ; n < max ; n++) { - observer = this.observers[n]; - if (observer[event]) - result.push(observer); - } - return [ event, result, true ] - }, - - observeEvents: function(events, args, previousEvent, previousResult) { - if (events.length === 0) { - return this.endTransit(previousResult === undefined ? true : previousResult); - } - - var event = events[0][0], - observers = events[0][1], - pluggable = events[0][2]; - - args[0].event = event; - if (event && pluggable && event !== previousEvent) - plugin.hook(this, 'lifecycle', args); - - if (observers.length === 0) { - events.shift(); - return this.observeEvents(events, args, event, previousResult); - } - else { - var observer = observers.shift(), - result = observer[event].apply(observer, args); - if (result && typeof result.then === 'function') { - return result.then(this.observeEvents.bind(this, events, args, event)) - .catch(this.failTransit.bind(this)) - } - else if (result === false) { - return this.endTransit(false); - } - else { - return this.observeEvents(events, args, event, result); - } - } - }, - - onInvalidTransition: function(transition, from, to) { - throw new Exception("transition is invalid in current state", transition, from, to, this.state); - }, - - onPendingTransition: function(transition, from, to) { - throw new Exception("transition is invalid while previous transition is still in progress", transition, from, to, this.state); - } });