diff --git a/lib/Machine.constructor.js b/lib/Machine.constructor.js index b994dc7..fc9d234 100644 --- a/lib/Machine.constructor.js +++ b/lib/Machine.constructor.js @@ -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; diff --git a/lib/private/help-exec-machine-instance.js b/lib/private/help-exec-machine-instance.js index 79bf6df..90d4396 100644 --- a/lib/private/help-exec-machine-instance.js +++ b/lib/private/help-exec-machine-instance.js @@ -389,7 +389,7 @@ module.exports = function helpExecMachineInstance (liveMachine) { _cache.expirationDate = new Date( (new Date()) - _cache.ttl); }// - + //>- // ██╗ ██████╗ ██████╗ ██╗ ██╗ ██╗ ██╗██████╗ ██████╗ ███████╗███████╗██╗ ██╗██╗ ████████╗ @@ -887,6 +887,15 @@ module.exports = function helpExecMachineInstance (liveMachine) { // -- -- }// + //--• + // 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), @@ -895,11 +904,6 @@ module.exports = function helpExecMachineInstance (liveMachine) { }// - });// - - - // _∏_ - - // Done. + });// }; diff --git a/lib/private/intercept-exit-callbacks.js b/lib/private/intercept-exit-callbacks.js index d6864a4..4db0c71 100644 --- a/lib/private/intercept-exit-callbacks.js +++ b/lib/private/intercept-exit-callbacks.js @@ -159,39 +159,52 @@ module.exports = function interceptExitCallbacks (callbacks, _cache, hash, liveM }// }// }// - //>- - (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(); @@ -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) @@ -370,7 +383,7 @@ module.exports = function interceptExitCallbacks (callbacks, _cache, hash, liveM return fn.call(liveMachine._configuredEnvironment, value); - });// + });// });// };// diff --git a/package.json b/package.json index 31d89a5..7f431a6 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test/flow-control.test.js b/test/flow-control.test.js new file mode 100644 index 0000000..14a7cf0 --- /dev/null +++ b/test/flow-control.test.js @@ -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.'); + });// + });// + 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()> + });// + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + });// + });// + + 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.'); + });// + });// + 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; + + });// + });// + });// + + });// + + + 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(); + });// + });// + 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; + + });// + });// + });// + + 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.'); + });// + });// + 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; + + });// + });// + });// + + });// + + +});// +