statetransition-mixin
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -38,3 +38,3 @@ { | ||
"homepage": "https://github.com/arlac77/statetransition-mixin#readme", | ||
"version": "1.1.0" | ||
"version": "1.2.0" | ||
} |
@@ -7,25 +7,58 @@ /* jslint node: true, esnext: true */ | ||
const actions = {}; | ||
const states = {}; | ||
Object.keys(as).forEach(name => { | ||
const a = as[name]; | ||
const ts = {}; | ||
Object.keys(a).forEach(tn => { | ||
ts[tn] = a[tn]; | ||
a[tn].name = tn; | ||
function addState(name, transition) { | ||
if (!states[name]) { | ||
states[name] = { | ||
name: name, | ||
transitions: {} | ||
}; | ||
} | ||
if (transition) { | ||
states[name].transitions[transition.initial] = transition; | ||
} | ||
return states[name]; | ||
} | ||
Object.keys(as).forEach(actionName => { | ||
const a = as[actionName]; | ||
const initialTransitions = {}; | ||
const duringTransitions = {}; | ||
Object.keys(a).forEach(initialState => { | ||
const t = a[initialState]; | ||
initialTransitions[initialState] = t; | ||
duringTransitions[t.during] = t; | ||
t.initial = initialState; | ||
t.name = `${actionName}:${t.initial}->${t.target}`; | ||
addState(t.initial, t); | ||
addState(t.during, t); | ||
addState(t.target); | ||
}); | ||
actions[name] = { | ||
name: name, | ||
transitions: ts | ||
actions[actionName] = { | ||
name: actionName, | ||
initial: initialTransitions, | ||
during: duringTransitions | ||
}; | ||
}); | ||
return actions; | ||
/* | ||
console.log(`${JSON.stringify(actions,undefined,1)}`); | ||
console.log(`${JSON.stringify(states,undefined,1)}`); | ||
*/ | ||
return [actions, states]; | ||
}; | ||
module.exports.StateTransitionMixin = (superclass, actions, currentState) => class extends superclass { | ||
/** | ||
* Called when state action is not allowed | ||
* @param {Object} action | ||
* @return {Promise} rejecting with an Error | ||
*/ | ||
constructor() { | ||
super(); | ||
this._state = currentState; | ||
} | ||
/** | ||
* Called when state transition action is not allowed | ||
* @param {Object} action | ||
* @return {Promise} rejecting with an Error | ||
*/ | ||
illegalStateTransition(action) { | ||
@@ -36,3 +69,3 @@ return Promise.reject(new Error(`Can't ${action.name} ${this} in ${this.state} state`)); | ||
/** | ||
* Called when the state transtinio implementation Promise rejects. | ||
* Called when the state transtion implementation promise rejects. | ||
* Resets the transition | ||
@@ -61,8 +94,8 @@ * @return {Promise} rejecting promise | ||
get state() { | ||
return currentState; | ||
return this._state; | ||
} | ||
set state(newState) { | ||
if (newState !== currentState) { | ||
this.stateChanged(currentState, newState); | ||
currentState = newState; | ||
if (newState !== this._state) { | ||
this.stateChanged(this._state, newState); | ||
this._state = newState; | ||
} | ||
@@ -76,10 +109,18 @@ } | ||
return new Promise(function (fullfill, reject) { | ||
const p = promise.then((fullfilled, rejected) => { | ||
fullfilled(this); | ||
}); | ||
const th = setTimeout(() => { | ||
//console.log(`Not resolved within ${timeout}ms`); | ||
reject(new Error(`Not resolved within ${timeout}ms`)) | ||
}, timeout); | ||
setTimeout(function () { | ||
reject(`Not resolved within ${timeout}s`); | ||
return promise.then((fullfilled, rejected) => { | ||
//console.log(`AA ${fullfilled} : ${rejected}`); | ||
clearTimeout(th); | ||
}, timeout * 1000); | ||
if (fullfilled) { | ||
fullfill(fullfilled); | ||
} | ||
if (rejected) { | ||
reject(rejected); | ||
} | ||
}); | ||
}); | ||
@@ -111,4 +152,5 @@ } | ||
*/ | ||
module.exports.defineActionMethods = function (object, actions) { | ||
//console.log(`${JSON.stringify(actions,undefined,1)}`); | ||
module.exports.defineActionMethods = function (object, actionsAndStates) { | ||
const actions = actionsAndStates[0]; | ||
const states = actionsAndStates[1]; | ||
@@ -127,26 +169,28 @@ Object.keys(actions).forEach(actionName => { | ||
value: function () { | ||
if (this._transition) { | ||
switch (this.state) { | ||
case this._transition.during: | ||
return this._transitionPromise; | ||
case this._transition.target: | ||
return Promise.resolve(this); | ||
} | ||
} | ||
if (action.transitions[this.state]) { | ||
this._transition = action.transitions[this.state]; | ||
// normal start | ||
if (action.initial[this.state]) { | ||
this._transition = action.initial[this.state]; | ||
this.state = this._transition.during; | ||
this._transitionPromise = this[privateActionName]().then( | ||
resolved => { | ||
this.state = this._transition.target; | ||
this._transitionPromise = undefined; | ||
this._transition = undefined; | ||
return this; | ||
}, rejected => this.stateTransitionRejection(rejected)); | ||
this._transitionPromise = rejectUnlessResolvedWithin(this[privateActionName](), this._transition | ||
.timeout) | ||
.then( | ||
resolved => { | ||
this.state = this._transition.target; | ||
this._transitionPromise = undefined; | ||
this._transition = undefined; | ||
return this; | ||
}, rejected => this.stateTransitionRejection(rejected)); | ||
return this._transitionPromise; | ||
} else { | ||
return this.illegalStateTransition(action); | ||
} else if (this._transition) { | ||
if (action.during[this._transition.during]) { | ||
//console.log(`XXX ${this.state} ${action.during[this._transition.during]}`); | ||
return this._transitionPromise; | ||
} | ||
} | ||
return this.illegalStateTransition(action); | ||
} | ||
@@ -153,0 +197,0 @@ }); |
@@ -17,3 +17,3 @@ /* global describe, it, xit */ | ||
during: "starting", | ||
timeout: 10 | ||
timeout: 200 | ||
} | ||
@@ -25,3 +25,3 @@ }, | ||
during: "stopping", | ||
timeout: 5 | ||
timeout: 100 | ||
}, | ||
@@ -31,3 +31,3 @@ starting: { | ||
during: "stopping", | ||
timeout: 10 | ||
timeout: 100 | ||
} | ||
@@ -39,9 +39,12 @@ } | ||
var shouldReject = false; | ||
class StatefullClass extends stm.StateTransitionMixin(BaseClass, actions, 'stopped') { | ||
constructor(startTime, shouldReject) { | ||
super(); | ||
this.startTime = startTime; | ||
this.shouldReject = shouldReject; | ||
} | ||
_start() { | ||
return new Promise((f, r) => { | ||
setTimeout(() => { | ||
if (shouldReject) { | ||
if (this.shouldReject) { | ||
r(new Error("always reject")); | ||
@@ -51,5 +54,9 @@ } else { | ||
} | ||
}, 10); | ||
}, this.startTime); | ||
}); | ||
} | ||
toString() { | ||
return `sample: ${this.state}` | ||
} | ||
} | ||
@@ -60,38 +67,45 @@ | ||
describe('states', function () { | ||
const o1 = new StatefullClass(); | ||
describe('static', function () { | ||
const o = new StatefullClass(10, false); | ||
it('has initial state', function () { | ||
assert.equal(o1.state, 'stopped'); | ||
}); | ||
it('has initial state', function () { | ||
o.state = 'stopped'; | ||
assert.equal(o.state, 'stopped'); | ||
}); | ||
it('has action methods', function () { | ||
assert.isDefined(o1.stop()); | ||
assert.isDefined(o1.start()); | ||
assert.isDefined(o1._start()); | ||
assert.isDefined(o1._stop()); | ||
it('has action methods', function () { | ||
assert.isDefined(o.stop()); | ||
assert.isDefined(o.start()); | ||
assert.isDefined(o._start()); | ||
assert.isDefined(o._stop()); | ||
}); | ||
}); | ||
it('can be started', function (done) { | ||
o1.start().then(() => { | ||
assert.equal(o1.state, 'running'); | ||
done(); | ||
}, done); | ||
}); | ||
describe('start-stop', function () { | ||
const o = new StatefullClass(10, false); | ||
it('and stoped', function (done) { | ||
o1.stop().then(() => { | ||
assert.equal(o1.state, 'stopped'); | ||
done(); | ||
}, done); | ||
it('can be started', function (done) { | ||
o.start().then(() => { | ||
assert.equal(o.state, 'running'); | ||
done(); | ||
}, done); | ||
}); | ||
it('and stoped', function (done) { | ||
o.stop().then(() => { | ||
assert.equal(o.state, 'stopped'); | ||
done(); | ||
}, done); | ||
}); | ||
}); | ||
it('can be started while starting', function (done) { | ||
assert.equal(o1.state, 'stopped'); | ||
const o = new StatefullClass(10, false); | ||
o1.start().then(() => {}); | ||
o.start().then(() => {}); | ||
assert.equal(o1.state, 'starting'); | ||
assert.equal(o.state, 'starting'); | ||
o1.start().then(() => { | ||
assert.equal(o1.state, 'running'); | ||
o.start().then(() => { | ||
assert.equal(o.state, 'running'); | ||
done(); | ||
@@ -101,23 +115,32 @@ }, done).catch(done); | ||
xit('can be stopped while starting', function (done) { | ||
o1.stop().then(() => { | ||
assert.equal(o1.state, 'stopped'); | ||
it('can be stopped while starting', function (done) { | ||
const o = new StatefullClass(100, false); | ||
o1.start().then(() => {}); | ||
o.start().then(() => {}); | ||
assert.equal(o1.state, 'starting'); | ||
assert.equal(o.state, 'starting'); | ||
o1.stop().then(() => { | ||
assert.equal(o1.state, 'stopped'); | ||
o.stop().then(() => { | ||
assert.equal(o.state, 'stopped'); | ||
done(); | ||
}, done).catch(done); | ||
}); | ||
describe('failures', function () { | ||
it('handle timeout while starting', function (done) { | ||
const o = new StatefullClass(1000, false); | ||
o.start().then(() => {}).catch(e => { | ||
assert.equal(o.state, 'failed'); | ||
done(); | ||
}, done).catch(done); | ||
}); | ||
}); | ||
}); | ||
it('handle failure while starting', function (done) { | ||
o1.stop().then(() => { | ||
shouldReject = true; | ||
assert.equal(o1.state, 'stopped'); | ||
o1.start().then(() => {}).catch(e => { | ||
assert.equal(o1.state, 'failed'); | ||
it('handle failure while starting', function (done) { | ||
const o = new StatefullClass(10, true); | ||
o.start().then((f, r) => { | ||
console.log(`${f} ${r}`); | ||
}).catch(e => { | ||
//console.log(`catch: ${e}`); | ||
assert.equal(o.state, 'failed'); | ||
done(); | ||
@@ -127,2 +150,3 @@ }); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
21207
294