Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/Machine.constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ function Machine (machineDefinition) {
// a flag tracking whether or not the machine is running synchronously
this._runningSynchronously = false;

// a flag tracking whether or not the machine's fn has "yielded" yet
this._hasFnYieldedYet = false;

// a flag tracking whether debug logging is enabled.
this._isLogEnabled = (!!process.env.NODE_MACHINE_LOG) || false;

Expand Down
18 changes: 11 additions & 7 deletions lib/private/help-exec-machine-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ module.exports = function helpExecMachineInstance (liveMachine) {
_cache.expirationDate = new Date( (new Date()) - _cache.ttl);

}//</else :: cache settings are valid>

//>-


// ██╗ ██████╗ ██████╗ ██╗ ██╗ ██╗ ██╗██████╗ ██████╗ ███████╗███████╗██╗ ██╗██╗ ████████╗
Expand Down Expand Up @@ -887,6 +887,15 @@ module.exports = function helpExecMachineInstance (liveMachine) {
// -- </IN ORDER TO DO THAT> --
}//</catch :: uncaught error thrown by custom implementation in this machine's `fn` function>

//--•
// IWMIH, it means that the `fn` did not throw.
//
// We'll track that the `fn` has "yielded"-- meaning that anything synchronous is finished.
// > This keeps track of whether or not we might need to introduce an artificial delay to
// > guarantee standardized flow control. This is only relevant if the machine is not running
// > synchronously-- and the check we're referring to here is in `intercept-exit-callbacks.js`.
liveMachine._hasFnYieldedYet = true;

} catch(e) {

// If something ELSE above threw an error *that we can catch* (i.e. outside of any asynchronous callbacks),
Expand All @@ -895,11 +904,6 @@ module.exports = function helpExecMachineInstance (liveMachine) {

}//</catch :: unexpected unhandled internal error in machine runner>

});//</doing cache lookup if relevant, then continuing on to do more stuff ^^>


// _∏_

// Done.
});//</after doing cache lookup (if relevant) and after continuing on to do more stuff ^^>

};
69 changes: 41 additions & 28 deletions lib/private/intercept-exit-callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,39 +159,52 @@ module.exports = function interceptExitCallbacks (callbacks, _cache, hash, liveM
}//</catch :: error inferring type schema or coercing against it>
}//</if :: `example` is NOT undefined>
}//</if exit coercion is enabled>

//>-
(function maybeWait(proceed){

// In order to allow for synchronous usage, `sync` must be explicitly `true`.

// Now, we'll potentially introduce an artificial delay, if necessary.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// This is to be sure that our flow control always works like this:
// ```
// //1
// foo().exec(function (err){
// //3
// });
// //2
// ```
//
// And NEVER like this:
// ```
// //1
// foo().exec(function (err){
// //2
// });
// //3
// ```
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(function maybeArtificiallyWait(proceed){

// If the machine ran synchronously (i.e. with `.execSync()`), then there's no
// need to introduce an artificial delay.
if (liveMachine._runningSynchronously) {
return proceed();
}
}//-•

//--•
// Otherwise, use setImmediate() (or `setTimeout(...,0)` if necesary) to
// ensure that at least one tick goes by.
//
// This is to be sure that it works like:
// ```
// //1
// foo().exec(function (err){
// //3
// });
// //2
// ```

// Otherwise, we know that the machine was run asynchronously with `.exec()`.
//
// And never like:
// ```
// //1
// foo().exec(function (err){
// //2
// });
// //3
// ```
// But if its `fn` has already "yielded", that means that it did not call this
// exit synchronously. So in that case, we can continue without delay.
if (liveMachine._hasFnYieldedYet) {
return proceed();
}//-•


// Otherwise, the `fn` has not "yielded" yet, meaning that it called this exit
// synchronously. So we'll need to introduce an artificial delay.
//
// > FUTURE: allow machines to declare some property that skips this-- saying that
// > they guarantee that they are ACTUALLY asynchronous (perhaps `sync: false`).
// To do that, we'll use setImmediate() (or `setTimeout(...,0)` if necesary) to
// ensure that at least one tick goes by.
if (typeof setImmediate === 'function') {
setImmediate(function (){
return proceed();
Expand All @@ -203,7 +216,7 @@ module.exports = function interceptExitCallbacks (callbacks, _cache, hash, liveM
});
}

})(function afterMaybeWaiting() {
})(function afterMaybeArtificiallyWaiting() {

// Ensure that the catchall error exit (`error`) always has a value
// (i.e. so node callback expectations are fulfilled)
Expand Down Expand Up @@ -370,7 +383,7 @@ module.exports = function interceptExitCallbacks (callbacks, _cache, hash, liveM
return fn.call(liveMachine._configuredEnvironment, value);


});//</self-calling function :: _maybeWait()>
});//</self-calling function :: _maybeArtificiallyWait>
});//</running self-calling function :: _cacheIfAppropriate()>
};//</interceptor callback definition>

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "machine",
"version": "13.0.0-19",
"version": "13.0.0-20",
"description": "Configure and execute machines",
"main": "index.js",
"scripts": {
Expand Down
184 changes: 184 additions & 0 deletions test/flow-control.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Module dependencies
*/
var util = require('util');
var assert = require('assert');
var Machine = require('../');



describe('flow control & artificial delay', function (){


describe('given a machine with an asynchronous implementation', function () {

describe('that declares itself synchronous', function () {
var NM_DEF_FIXTURE = {
sync: true,
inputs: {},
fn: function (inputs, exits){
var sum = 1;
setTimeout(function (){
sum++;
return exits.success(sum);
}, 50);
}
};
describe('calling .execSync()', function () {
it('should throw a predictable error', function (){
try {
Machine.build(NM_DEF_FIXTURE).execSync();
} catch (e) {
// console.log('->',e);
assert.equal(e.code,'E_MACHINE_INCONSISTENT');
return;
}//-•

throw new Error('Expected an error, but instead it was successful.');
});//</it>
});//</describe :: calling .execSync()>
describe('calling .exec()', function () {
// FUTURE: make this cause an error instead of working-- eg. make this test pass:
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
it.skip('should throw a predictable error', function (done){
Machine.build(NM_DEF_FIXTURE).exec(function (err){
if (err) {
// console.log('->',err);
try {
assert.equal(err.code,'E_MACHINE_INCONSISTENT');
} catch (e) { return done(e); }
return done();
}
return done(new Error('Expected an error, but instead it was successful.'));
});//<.exec()>
});//</it>
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
});//</describe :: calling .exec()>
});//</describe :: that declares itself synchronous>

describe('that DOES NOT declare itself synchronous', function () {
var NM_DEF_FIXTURE = {
inputs: {},
fn: function (inputs, exits){
var sum = 1;
setTimeout(function (){
sum++;
return exits.success(sum);
}, 50);
}
};
describe('calling .execSync()', function () {
it('should throw a predictable error', function (){
try {
Machine.build(NM_DEF_FIXTURE).execSync();
} catch (e) {
// console.log('->',e);
assert.equal(e.code,'E_USAGE');
return;
}//-•

throw new Error('Expected an error, but instead it was successful.');
});//</it>
});//</describe :: calling .execSync()>
describe('calling .exec()', function () {
it('should succeed, and should yield before triggering callback', function (done){

var didYield;
Machine.build(NM_DEF_FIXTURE).exec(function (err){
if (err) { return done(err); }

try {
assert(didYield, new Error('Should have "yielded"!'));
} catch (e) { return done(e); }

return done();
});//<.exec()>
didYield = true;

});//</it>
});//</describe :: calling .exec()>
});//</describe :: that DOES NOT declare itself synchronous>

});//</describe :: given a machine with an asynchronous implementation>


describe('given a machine with a synchronous implementation', function () {

describe('that declares itself synchronous', function () {
var NM_DEF_FIXTURE = {
sync: true,
inputs: {},
fn: function (inputs, exits){
var sum = 1+1;
return exits.success(sum);
}
};
describe('calling .execSync()', function () {
it('should succeed', function (){
Machine.build(NM_DEF_FIXTURE).execSync();
});//</it>
});//</describe :: calling .execSync()>
describe('calling .exec()', function () {
it('should succeed, and should yield before triggering callback', function (done){

var didYield;
Machine.build(NM_DEF_FIXTURE).exec(function (err){
if (err) { return done(err); }

try {
assert(didYield, new Error('Should have "yielded"!'));
} catch (e) { return done(e); }

return done();
});//<.exec()>
didYield = true;

});//</it>
});//</describe :: calling .exec()>
});//</describe :: that declares itself synchronous>

describe('that DOES NOT declare itself synchronous', function () {
var NM_DEF_FIXTURE = {
inputs: {},
fn: function (inputs, exits){
var sum = 1+1;
return exits.success(sum);
}
};
describe('calling .execSync()', function () {
it('should throw a predictable error', function (){
try {
Machine.build(NM_DEF_FIXTURE).execSync();
} catch (e) {
// console.log('->',e);
assert.equal(e.code,'E_USAGE');
return;
}//-•

throw new Error('Expected an error, but instead it was successful.');
});//</it>
});//</describe :: calling .execSync()>
describe('calling .exec()', function () {
it('should succeed, and should yield before triggering callback', function (done){

var didYield;
Machine.build(NM_DEF_FIXTURE).exec(function (err){
if (err) { return done(err); }

try {
assert(didYield, new Error('Should have "yielded"!'));
} catch (e) { return done(e); }

return done();
});//<.exec()>
didYield = true;

});//</it>
});//</describe :: calling .exec()>
});//</describe :: that DOES NOT declare itself synchronous>

});//</describe :: given a machine with a synchronous implementation>


});//</describe :: flow control & artificial delay>