diff --git a/dist/state-machine-history.js b/dist/state-machine-history.js index 19f7c4f..cd50b65 100644 --- a/dist/state-machine-history.js +++ b/dist/state-machine-history.js @@ -162,6 +162,10 @@ module.exports = function(options) { options = options || {}; } }, + cancel: function(instance, lifecycle) { + instance[past].pop() + }, + methods: {}, properties: {} diff --git a/dist/state-machine-history.min.js b/dist/state-machine-history.min.js index 9c7fb3f..b5e601a 100644 --- a/dist/state-machine-history.min.js +++ b/dist/state-machine-history.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("StateMachineHistory",[],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=1)}([function(t,e,n){"use strict";function r(t){if(0===t.length)return t;var e,n,r=t.split(/[_-]/);if(1===r.length&&r[0][0].toLowerCase()===r[0][0])return t;for(n=r[0].toLowerCase(),e=1;ec&&t[e].shift(),r.transition!==i&&r.transition!==s&&(t[n].length=0))},methods:{},properties:{}};return f.methods[o]=function(){this[e].length=0,this[n].length=0},f.properties[u]={get:function(){return this[e].length>1}},f.properties[p]={get:function(){return this[n].length>0}},f.methods[i]=function(){if(!this[u])throw Error("no history");var t=this[e].pop(),r=this[e].pop();this[n].push(t),this._fsm.transit(i,t,r,[])},f.methods[s]=function(){if(!this[p])throw Error("no history");var t=this.state,e=this[n].pop();this._fsm.transit(s,t,e,[])},f}}])}); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("StateMachineHistory",[],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=1)}([function(t,e,n){"use strict";function r(t){if(0===t.length)return t;var e,n,r=t.split(/[_-]/);if(1===r.length&&r[0][0].toLowerCase()===r[0][0])return t;for(n=r[0].toLowerCase(),e=1;ec&&t[e].shift(),r.transition!==i&&r.transition!==s&&(t[n].length=0))},cancel:function(t,n){t[e].pop()},methods:{},properties:{}};return f.methods[o]=function(){this[e].length=0,this[n].length=0},f.properties[u]={get:function(){return this[e].length>1}},f.properties[p]={get:function(){return this[n].length>0}},f.methods[i]=function(){if(!this[u])throw Error("no history");var t=this[e].pop(),r=this[e].pop();this[n].push(t),this._fsm.transit(i,t,r,[])},f.methods[s]=function(){if(!this[p])throw Error("no history");var t=this.state,e=this[n].pop();this._fsm.transit(s,t,e,[])},f}}])}); \ No newline at end of file diff --git a/dist/state-machine.js b/dist/state-machine.js index b8b9e37..3177183 100644 --- a/dist/state-machine.js +++ b/dist/state-machine.js @@ -465,7 +465,17 @@ mixin(JSM.prototype, { beginTransit: function() { this.pending = true; }, endTransit: function(result) { this.pending = false; return result; }, - failTransit: function(result) { this.pending = false; throw result; }, + failTransit: function(result, args) { + var from = args[0].from; + var to = args[0].to; + + if (this.state === to) { + this.state = from; + plugin.hook(this, 'cancel', args); + } + this.pending = false; + throw result; + }, doTransit: function(lifecycle) { this.state = lifecycle.to; }, observe: function(args) { @@ -489,6 +499,14 @@ mixin(JSM.prototype, { return [ event, result, true ] }, + callObserver: function(event, observer, args) { + try { + return observer[event].apply(observer, args); + } catch (error) { + this.failTransit.call(this, error, args); + } + }, + observeEvents: function(events, args, previousEvent, previousResult) { if (events.length === 0) { return this.endTransit(previousResult === undefined ? true : previousResult); @@ -507,11 +525,14 @@ mixin(JSM.prototype, { return this.observeEvents(events, args, event, previousResult); } else { - var observer = observers.shift(), - result = observer[event].apply(observer, args); + var observer = observers.shift(); + var result = this.callObserver.call(this, event, observer, args); if (result && typeof result.then === 'function') { + var jsm = this return result.then(this.observeEvents.bind(this, events, args, event)) - .catch(this.failTransit.bind(this)) + .catch(function (error) { + jsm.failTransit.call(jsm, error, args); + }); } else if (result === false) { return this.endTransit(false); diff --git a/dist/state-machine.min.js b/dist/state-machine.min.js index b9439bc..fc361d5 100644 --- a/dist/state-machine.min.js +++ b/dist/state-machine.min.js @@ -1 +1 @@ -!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachine",[],n):"object"==typeof exports?exports.StateMachine=n():t.StateMachine=n()}(this,function(){return function(t){function n(e){if(i[e])return i[e].exports;var s=i[e]={i:e,l:!1,exports:{}};return t[e].call(s.exports,s,s.exports,n),s.l=!0,s.exports}var i={};return n.m=t,n.c=i,n.i=function(t){return t},n.d=function(t,i,e){n.o(t,i)||Object.defineProperty(t,i,{configurable:!1,enumerable:!0,get:e})},n.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(i,"a",i),i},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=5)}([function(t,n,i){"use strict";t.exports=function(t,n){var i,e,s;for(i=1;i=0:this.state===t},isPending:function(){return this.pending},can:function(t){return!this.isPending()&&!!this.seek(t)},cannot:function(t){return!this.can(t)},allStates:function(){return this.config.allStates()},allTransitions:function(){return this.config.allTransitions()},transitions:function(){return this.config.transitionsFor(this.state)},seek:function(t,n){var i=this.config.defaults.wildcard,e=this.config.transitionFor(this.state,t),s=e&&e.to;return"function"==typeof s?s.apply(this.context,n):s===i?this.state:s},fire:function(t,n){return this.transit(t,this.state,this.seek(t,n),n)},transit:function(t,n,i,e){var s=this.config.lifecycle,r=this.config.options.observeUnchangedState||n!==i;return i?this.isPending()?this.context.onPendingTransition(t,n,i):(this.config.addState(i),this.beginTransit(),e.unshift({transition:t,from:n,to:i,fsm:this.context}),this.observeEvents([this.observersForEvent(s.onBefore.transition),this.observersForEvent(s.onBefore[t]),r?this.observersForEvent(s.onLeave.state):a,r?this.observersForEvent(s.onLeave[n]):a,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:a,r?this.observersForEvent(s.onEnter.state):a,r?this.observersForEvent(s.onEnter[i]):a,r?this.observersForEvent(s.on[i]):a,this.observersForEvent(s.onAfter.transition),this.observersForEvent(s.onAfter[t]),this.observersForEvent(s.on[t])],e)):this.context.onInvalidTransition(t,n,i)},beginTransit:function(){this.pending=!0},endTransit:function(t){return this.pending=!1,t},failTransit:function(t){throw this.pending=!1,t},doTransit:function(t){this.state=t.to},observe:function(t){if(2===t.length){var n={};n[t[0]]=t[1],this.observers.push(n)}else this.observers.push(t[0])},observersForEvent:function(t){for(var n,i=0,e=this.observers.length,s=[];i=0:this.state===t},isPending:function(){return this.pending},can:function(t){return!this.isPending()&&!!this.seek(t)},cannot:function(t){return!this.can(t)},allStates:function(){return this.config.allStates()},allTransitions:function(){return this.config.allTransitions()},transitions:function(){return this.config.transitionsFor(this.state)},seek:function(t,n){var i=this.config.defaults.wildcard,e=this.config.transitionFor(this.state,t),s=e&&e.to;return"function"==typeof s?s.apply(this.context,n):s===i?this.state:s},fire:function(t,n){return this.transit(t,this.state,this.seek(t,n),n)},transit:function(t,n,i,e){var s=this.config.lifecycle,r=this.config.options.observeUnchangedState||n!==i;return i?this.isPending()?this.context.onPendingTransition(t,n,i):(this.config.addState(i),this.beginTransit(),e.unshift({transition:t,from:n,to:i,fsm:this.context}),this.observeEvents([this.observersForEvent(s.onBefore.transition),this.observersForEvent(s.onBefore[t]),r?this.observersForEvent(s.onLeave.state):a,r?this.observersForEvent(s.onLeave[n]):a,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:a,r?this.observersForEvent(s.onEnter.state):a,r?this.observersForEvent(s.onEnter[i]):a,r?this.observersForEvent(s.on[i]):a,this.observersForEvent(s.onAfter.transition),this.observersForEvent(s.onAfter[t]),this.observersForEvent(s.on[t])],e)):this.context.onInvalidTransition(t,n,i)},beginTransit:function(){this.pending=!0},endTransit:function(t){return this.pending=!1,t},failTransit:function(t,n){var i=n[0].from,e=n[0].to;throw this.state===e&&(this.state=i,o.hook(this,"cancel",n)),this.pending=!1,t},doTransit:function(t){this.state=t.to},observe:function(t){if(2===t.length){var n={};n[t[0]]=t[1],this.observers.push(n)}else this.observers.push(t[0])},observersForEvent:function(t){for(var n,i=0,e=this.observers.length,s=[];i { }) //------------------------------------------------------------------------------------------------- + +test('exception thrown in handler cancels state transition', t => { + + var fsm = new StateMachine({ + transitions: [ + { name: 'step1', from: 'none', to: 'A' }, + { name: 'step2', from: 'A', to: 'B' } + ], + methods: { + onA:function () { + throw new Error('Error thrown in observer') + } + } + }); + + t.is(fsm.state, 'none') + t.is(fsm.can('step1'), true) + t.is(fsm.can('step2'), false) + + const error = t.throws(() => { + fsm.step1(); + }) + + t.is(error.message, 'Error thrown in observer') + t.is(fsm.state, 'none') + +}) + +test('exception with promise thrown in handler cancels state transition', async t => { + + var fsm = new StateMachine({ + transitions: [ + { name: 'step1', from: 'none', to: 'A' }, + { name: 'step2', from: 'A', to: 'B' } + ], + methods: { + onA: async function () { + return Promise.reject(new Error('Error thrown in observer')) + } + } + }); + + t.is(fsm.state, 'none') + t.is(fsm.can('step1'), true) + t.is(fsm.can('step2'), false) + + try{ + await fsm.step1() + t.is('Should fail', 'fail') + }catch(error){ + t.is(error.message, 'Error thrown in observer') + } + + t.is(fsm.state, 'none') +}) \ No newline at end of file diff --git a/test/plugin/history.js b/test/plugin/history.js index 00f73ef..bfbdd5d 100644 --- a/test/plugin/history.js +++ b/test/plugin/history.js @@ -491,3 +491,35 @@ test('history can be used with a state machine factory applied to existing class }) //------------------------------------------------------------------------------------------------- + +test('exception thrown in handler cancels state transition', t => { + + var fsm = new StateMachine({ + transitions: [ + { name: 'step1', from: 'none', to: 'A' }, + { name: 'step2', from: 'A', to: 'B' } + ], + plugins:[ + StateMachineHistory + ], + methods: { + onB:function () { + throw new Error('Error thrown in observer') + } + } + }); + + t.is(fsm.state, 'none'); t.deepEqual(fsm.history, [ ]); + t.is(fsm.can('step1'), true) + t.is(fsm.can('step2'), false) + + fsm.step1(); t.is(fsm.state, 'A'); t.deepEqual(fsm.history, [ 'A' ]); + + const error = t.throws(() => { + fsm.step2(); + }) + + t.is(fsm.state, 'A'); t.deepEqual(fsm.history, [ 'A' ]); + + t.is(error.message, 'Error thrown in observer') +}) \ No newline at end of file