Skip to content

Commit

Permalink
feat: state transitions are now time guarded if timeout is reached du…
Browse files Browse the repository at this point in the history
…ring transition then the transition aborts with a failure
  • Loading branch information
arlac77 committed Dec 28, 2015
1 parent b568569 commit 8e204a3
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 94 deletions.
138 changes: 91 additions & 47 deletions StateTransitionMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,68 @@

module.exports.prepareActions = function (as) {
const actions = {};
const states = {};

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(name => {
const a = as[name];
const ts = {};
Object.keys(a).forEach(tn => {
ts[tn] = a[tn];
a[tn].name = tn;
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) {
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
* @return {Promise} rejecting promise
*/
Expand All @@ -56,12 +89,12 @@ module.exports.StateTransitionMixin = (superclass, actions, currentState) => cla
}

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;
}
}
};
Expand All @@ -70,14 +103,22 @@ function rejectUnlessResolvedWithin(promise, timeout) {
if (timeout === 0) return promise;

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);
}
});
});
}

Expand All @@ -104,8 +145,9 @@ function thisResolverPromise() {
* @param {Object} object where we define the metods
* @param {Object} actions object describing the state transitions
*/
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];

Object.keys(actions).forEach(actionName => {
const action = actions[actionName];
Expand All @@ -119,30 +161,32 @@ module.exports.defineActionMethods = function (object, actions) {

Object.defineProperty(object, 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);
}
});
});
Expand Down
118 changes: 71 additions & 47 deletions tests/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,107 +14,131 @@ const actions = stm.prepareActions({
stopped: {
target: "running",
during: "starting",
timeout: 10
timeout: 200
}
},
stop: {
running: {
target: "stopped",
during: "stopping",
timeout: 5
timeout: 100
},
starting: {
target: "stopped",
during: "stopping",
timeout: 10
timeout: 100
}
}
});

class BaseClass {}

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"));
} else {
f(this);
}
}, 10);
}, this.startTime);
});
}

toString() {
return `sample: ${this.state}`
}
}

stm.defineActionMethods(StatefullClass.prototype, actions);

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();
}, 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();
});
});
});

});

0 comments on commit 8e204a3

Please sign in to comment.