Comparing version 13.0.0-2 to 13.0.0-3
@@ -5,40 +5,102 @@ /** | ||
var hashObject = require('object-hash'); | ||
var util = require('util'); | ||
var crypto = require('crypto'); | ||
var _ = require('lodash'); | ||
var rttc = require('rttc'); | ||
/** | ||
* [exports description] | ||
* @param {[type]} machine [description] | ||
* @param {Function} done [description] | ||
* @return {[type]} [description] | ||
* hashArgins() | ||
* | ||
* Compute a hash string from the configured argins in the provided live machine instance. | ||
* | ||
* > Note: | ||
* > + Argins which _might not be JSON serializable_ are not included when computing the hash. | ||
* > + Key order does not matter (no matter how deep). | ||
* > + This logic assumes argins have _already been validated and potentially coerced._ | ||
* > + It also assumes that default values have already been folded in, where appropriate. | ||
* | ||
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
* @param {LiveMachine} liveMachine | ||
* @returns {String} | ||
* The hash computed from the configured argins. | ||
*/ | ||
module.exports = function hash_machine (machine, done) { | ||
var hash; | ||
module.exports = function hashArgins (liveMachine) { | ||
if (_.isUndefined(liveMachine)) { | ||
throw new Error('Consistency violation: Expecting `liveMachine` to be provided as argument to hashArgins(). (But it was not.)'); | ||
} | ||
if (!_.isObject(liveMachine) || !_.isObject(liveMachine._configuredInputs)) { | ||
throw new Error('Consistency violation: Expecting `liveMachine` to be provided as argument to hashArgins(). Should have a property called `_configuredInputs`, which is a dictionary of all configured argins. But instead, I got this lousy thing:\n'+util.inspect(liveMachine, {depth:null})); | ||
} | ||
try { | ||
// console.log('CALCULATED HASH ON:',getUniquelyIdentifyingObj(machine)); | ||
// Build a modified copy of all argins that will be hashed, and sort their keys (recursively). | ||
var keySortifiedArginsToHash = _.reduce(liveMachine._configuredInputs, function (memo, argin, inputCodeName){ | ||
// see https://github.com/puleos/object-hash | ||
// TODO: optimize this, or at least allow for setImmediate (i.e. nextTick) | ||
// ( BUT ONLY WHEN `machine.runnincSynchronously` IS FALSE!! ) | ||
hash = hashObject(getUniquelyIdentifyingObj(machine)); | ||
// Figure out if the input's example indicates that this argin MIGHT NOT be JSON-serializable. | ||
// | ||
// > Note: We're just using `rebuild()` here (^^) because it's a good, safe iterator. | ||
var mightNotBeJSONSerializable; | ||
var inputDef = liveMachine.inputs[inputCodeName]; | ||
rttc.rebuild(inputDef.example, function (exemplarPiece){ | ||
if (exemplarPiece === '->' || exemplarPiece === '===') { | ||
mightNotBeJSONSerializable = true; | ||
} | ||
});//</rttc.rebuild()> | ||
// console.log('AND I GOT: ',hash); | ||
} | ||
catch (e) { | ||
// console.log('HASH CALCULATION ERR:',e); | ||
return done(e); | ||
} | ||
// If we don't know for sure this argin is JSON-serializable, then just skip it | ||
// and move on to the next. (It won't be included in the hash.) | ||
if (mightNotBeJSONSerializable) { | ||
return memo; | ||
} | ||
return done(null, hash); | ||
}; | ||
// --• | ||
// At this point, since we're assuming the argin has already been validated, we can safely trust | ||
// that it is JSON-serializable. | ||
// Build a modified ("deep-ish") clone of this argin with all of its keys sorted-- recursively deep. | ||
var sortifiedArgin = (function _sortKeysRecursive(val){ | ||
// --• misc | ||
if (!_.isObject(val)) { return val; } | ||
function getUniquelyIdentifyingObj(machine) { | ||
return { | ||
id: machine.identity || machine.fn.toString(), | ||
data: machine._configuredInputs | ||
}; | ||
} | ||
// --• array | ||
if (_.isArray(val)) { | ||
return _.map(val, function (item){ | ||
return _sortKeysRecursive(item); | ||
});//</_.map()> | ||
} | ||
// --• dictionary | ||
var sortedSubKeys = _.keys(val).sort(); | ||
return _.reduce(sortedSubKeys, function (memo, subKey) { | ||
memo[subKey] = _sortKeysRecursive(val[subKey]); | ||
return memo; | ||
}, {});//</_.reduce()> | ||
})(argin); | ||
// Track this sortified argin on our dictionary of stuff that will get hashed. | ||
memo[inputCodeName] = sortifiedArgin; | ||
// And continue. | ||
return memo; | ||
}, {});//</_.reduce() :: argins to hash> | ||
// Now encode that as a JSON string. | ||
var stringifiedStuffToHash = JSON.stringify(keySortifiedArginsToHash); | ||
// Finally, compute & return an MD5 hash. | ||
var computedHash = crypto.createHash('md5').update(stringifiedStuffToHash).digest('hex'); | ||
return computedHash; | ||
} catch (e) { throw new Error('Consistency violation: Attempted to hash provided argins (runtime input values) for caching purposes, but could not calculate hash. Details:\n'+e.stack); } | ||
}; |
@@ -352,10 +352,10 @@ /** | ||
else if (!_.isObject(callbacks)) { | ||
throw new Error('Machine must be configured with a single callback or an object of exit callbacks- not:'+configuredExits); | ||
throw new Error('Machine must be configured with either (A) a single, traditional Node.js-style callback function, or (B) a dictionary of exit callbacks. But instead, got:\n'+util.inspect(callbacks,{depth: null})); | ||
} | ||
// Handle exits obj | ||
// Handle exits dictionary | ||
else { | ||
// Make sure only declared exits are configured. | ||
var undeclaredExits = _.difference(_.keys(callbacks), _.keys(this.exits)); | ||
if (undeclaredExits.length) { | ||
throw new Error('The following exits were configured, but they aren\'t valid for this machine: `' + undeclaredExits.join(', ') + '`.'); | ||
if (undeclaredExits.length > 0) { | ||
throw new Error('One or more callbacks were configured for exits that are not recognized by this machine: `' + undeclaredExits.join(', ') + '`.'); | ||
} | ||
@@ -362,0 +362,0 @@ _.extend(this._configuredExits, callbacks); |
@@ -6,4 +6,5 @@ /** | ||
var util = require('util'); | ||
var Debug = require('debug'); | ||
var _ = require('lodash'); | ||
var Debug = require('debug'); | ||
var rttc = require('rttc'); | ||
var switchback = require('switchback'); | ||
@@ -13,3 +14,2 @@ var calculateHash = require('./hash-machine'); | ||
var validateConfiguredInputValues = require('./validate-configured-input-values'); | ||
var rttc = require('rttc'); | ||
var buildLamdaMachine = require('./build-lamda-machine'); | ||
@@ -19,11 +19,28 @@ | ||
/** | ||
* [exec description] | ||
* @param {[type]} configuredExits [description] | ||
* @chainable | ||
* .exec() | ||
* | ||
* Run this machine's `fn` and trigger the appropriate exit callback. | ||
* | ||
* | ||
* NOTE: | ||
* If the machine is synchronous, then an artificial `setTimeout(0)` will be introduced | ||
* to ensure that the relevant exit callback is called in a subsequent tick of the event | ||
* loop. | ||
* | ||
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
* | ||
* @param? {Dictionary|Function} done | ||
* An optional callback function or dictionary of exit-handling callback functions. | ||
* If provided, this callback (or set of callbacks) will be folded onto any existing | ||
* exit-handling callbacks which were already attached with `.setExits()`. | ||
* | ||
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
*/ | ||
module.exports = function Machine_prototype_exec (configuredExits) { | ||
module.exports = function Machine_prototype_exec (done) { | ||
var self = this; | ||
// Track timestamp | ||
// If duration-tracking is enabled, track current timestamp | ||
// as a JavaScript Date instance in `_execBeginTimestamp`. | ||
if (self._doTrackDuration){ | ||
@@ -33,109 +50,192 @@ self._execBeginTimestamp = new Date(); | ||
if (configuredExits) { | ||
this.setExits(configuredExits); | ||
// If `done` was provided... | ||
if (done !== undefined) { | ||
// Do a quick sanity check to make sure it _at least looks like_ a callback function or dictionary of callback functions. | ||
if (_.isArray(done) || (!_.isFunction(done) && !_.isObject(done))) { | ||
throw new Error('Invalid usage: If something is passed in to `.exec()`, it must either be:\n'+ | ||
' (1) a standard Node callback function, or\n'+ | ||
' (2) a dictionary of per-exit callback functions\n'+ | ||
'\n'+ | ||
'But instead, got:\n'+ | ||
util.inspect({depth: null})+ | ||
''); | ||
} | ||
this.setExits(done); | ||
}//</if :: `done` was provided> | ||
// >- | ||
// At this point, if a(ny) callback(s) were provided, we've folded them in. | ||
// | ||
// So before continuing, validate that we have an `error` callback of some sort. | ||
// If it is not, then get crazy and **throw** BEFORE calling the machine's `fn`. | ||
// | ||
// (Better to potentially terminate the process than open up the possibility of silently swallowing errors later.) | ||
if (_.isUndefined(self._configuredExits.error)) { | ||
var err_noErrorCallbackConfigured = new Error('Invalid usage: Cannot execute machine (`'+self.identity+'`) without providing any catchall error handling (e.g. an `error` callback).'); | ||
err_noErrorCallbackConfigured.code = 'E_NO_ERROR_CALLBACK_CONFIGURED'; | ||
throw err_noErrorCallbackConfigured; | ||
} | ||
// Also, as a sanity check, make sure it's a valid callback function and not something else crazy. | ||
if (!_.isFunction(self._configuredExits.error)) { | ||
throw new Error('Consistency violation: Cannot execute machine (`'+self.identity+'`) because its configured `error` callback is invalid. It should be a function, but instead, it\'s:\n'+util.inspect(self._configuredExits.error, {depth: null})); | ||
} | ||
var DEBUG_LOG_LINE_LEN = 45; | ||
var identity = self.identity; | ||
var paddedIdentity = _.padRight(_.trunc('machine-log:'+identity, {length: DEBUG_LOG_LINE_LEN, omission: ''}), DEBUG_LOG_LINE_LEN); | ||
Debug('machine:'+self.identity+':exec')(''); | ||
// Debug(paddedIdentity)(' -< '+(self.friendlyName||'')); | ||
Debug(paddedIdentity)(' •- '+(self.friendlyName||'')); | ||
// Log debug messages, if relevant. | ||
(function _writeDebugLogMsgs (){ | ||
var DEBUG_LOG_LINE_LEN = 45; | ||
var identity = self.identity; | ||
var paddedIdentity = _.padRight(_.trunc('machine-log:'+identity, {length: DEBUG_LOG_LINE_LEN, omission: ''}), DEBUG_LOG_LINE_LEN); | ||
// Only validate & coerce configured input values if `unsafeMode` is disabled. | ||
var coercedInputValues; | ||
Debug('machine:'+self.identity+':exec')(''); | ||
// Debug(paddedIdentity)(' -< '+(self.friendlyName||'')); | ||
Debug(paddedIdentity)(' •- '+(self.friendlyName||'')); | ||
})();//</just logged debug message> | ||
// -- | ||
// This local variable (`potentiallyCoercedArgins`) is used below to hold a dictionary | ||
// of argins (runtime input values) that have _potentially_ been coerced to match the expectations | ||
// of the input definitions of this machine. | ||
// | ||
// See `rttc.validate()` for more information about this form of coercion; and about how | ||
// *loose validation* works in general. | ||
var potentiallyCoercedArgins; | ||
// If `unsafeMode` is disabled... | ||
if (!self._unsafeMode) { | ||
var validationResults = validateConfiguredInputValues(self); | ||
coercedInputValues = validationResults.values; | ||
var errors = validationResults.errors; | ||
// If there are (still) `e.errors`, then we've got to call the error callback. | ||
// Perform loose validation on our argins (runtime input values). | ||
// This also generates potentially coerced values, which we may or may not actually use | ||
// (see below for more on that.) | ||
var looseValidationReport = validateConfiguredInputValues(self); | ||
potentiallyCoercedArgins = looseValidationReport.values; | ||
var errors = looseValidationReport.errors; | ||
// If there are (still) `e.errors` (meaning one or more argins were invalid), then... | ||
if (errors.length > 0) { | ||
// If runtime type checking is enabled, trigger the error exit | ||
// if any inputs are invalid. | ||
// If runtime type checking is enabled, then... | ||
if(self._runTimeTypeCheck) { | ||
// Fourth argument (`true`) tells switchback to run synchronously | ||
return switchback(self._configuredExits, undefined, undefined, true)((function (){ | ||
// var err = new Error(util.format('`%s` machine: %d error(s) encountered validating inputs:\n', self.identity, errors.length, util.inspect(errors))); | ||
var errMsg = (function (){ | ||
var prettyPrintedValidationErrorsStr = _.map(errors, function (rttcValidationErr){ | ||
return ' • '+rttcValidationErr.message; | ||
}).join('\n'); | ||
return 'Could not run `'+self.identity+'` due to '+errors.length+' '+ | ||
'validation error'+(errors.length>1?'s':'')+':\n'+prettyPrintedValidationErrorsStr; | ||
})(); | ||
var err = new Error(errMsg); | ||
err.code = 'E_MACHINE_RUNTIME_VALIDATION'; | ||
err.machineInstance = self; | ||
err.errors = errors; | ||
return err; | ||
})()); | ||
} | ||
} | ||
} | ||
// Build an appropriate runtime validation error. | ||
var err_machineRuntimeValidation = (function _buildMachineRuntimeValidationErr() { | ||
// If the `_inputCoercion` flag is enabled, configure the machine with the | ||
// newly coerced input values. | ||
var bulletPrefixedErrors = _.map(errors, function (rttcValidationErr){ return ' • '+rttcValidationErr.message; }); | ||
var prettyPrintedValidationErrorsStr = bulletPrefixedErrors.join('\n'); | ||
var errMsg = 'Could not run `'+self.identity+'` due to '+errors.length+' '+ | ||
'validation error'+(errors.length>1?'s':'')+':\n'+prettyPrintedValidationErrorsStr; | ||
var err_machineRuntimeValidation = new Error(errMsg); | ||
err_machineRuntimeValidation.code = 'E_MACHINE_RUNTIME_VALIDATION'; | ||
err_machineRuntimeValidation.machineInstance = self; | ||
err_machineRuntimeValidation.errors = errors; | ||
return err_machineRuntimeValidation; | ||
})();//</self-calling function :: built a E_MACHINE_RUNTIME_VALIDATON error> | ||
// -- | ||
// Build a switchback from the configured exits. | ||
// Fourth argument (`true`) means that the switchback will be built to run **synchronously.** | ||
var sb = switchback(self._configuredExits, undefined, undefined, true); | ||
// Trigger the callback with an error. | ||
sb(err_machineRuntimeValidation); | ||
return self; | ||
}//</if :: self._runTimeTypeCheck> | ||
}//<if :: any argins (runtime input values) are invalid> | ||
}//</if :: NOT in "unsafe" mode> | ||
// --• | ||
// If the `_inputCoercion` flag is enabled, configure this live machine instance | ||
// with the newly coerced argins. | ||
if (self._inputCoercion) { | ||
self.setInputs(coercedInputValues); | ||
self.setInputs(potentiallyCoercedArgins); | ||
} | ||
// Apply `defaultsTo` for input defs that use it. | ||
// TODO: consider whether `defaultsTo` values should be automatically | ||
// validated/coerced too. | ||
_.each(self.inputs, function (inputDef, inputName){ | ||
if (_.isUndefined(inputDef.defaultsTo)) { return; } | ||
if (_.isUndefined(self._configuredInputs[inputName])){ | ||
self._configuredInputs[inputName] = inputDef.defaultsTo; | ||
// TODO: consider whether `defaultsTo` values should be automatically validated/coerced too (that would need to go in Machine.build) | ||
_.each(self.inputs, function (inputDef, inputCodeName){ | ||
// Currently (see TODO above for "why currently") we build machines out of | ||
// default lamda input values that specify a contract here. | ||
// Otherwise the default functions would never get built into machine instances, | ||
// because we're not actually calling rttc.validate() on `defaultsTo` values | ||
// in general. | ||
if (!_.isUndefined(inputDef.contract) && (rttc.infer(inputDef.example) === 'lamda')) { | ||
try { | ||
// If there is no `defaultsTo` value, we obviously can't use it. | ||
if (inputDef.defaultsTo === undefined) { | ||
return; | ||
} | ||
// If lamda input def specifies a `defaultsTo`, and no input value was provided, go ahead | ||
// and instantiate the default function into a machine using the contract. | ||
self._configuredInputs[inputName] = buildLamdaMachine(inputDef.defaultsTo, inputName, self, self._rootMachine||self); | ||
} | ||
catch (e) { | ||
e.input = inputName; | ||
e.message = 'machine:'+self.identity+' => Invalid usage- the `defaultsTo` for lamda input "'+inputName+'" could not be built into a machine using the provided `contract`. Please check that the `contract` def and `defaultsTo` function are valid. The machine was not executed.\nError details:\n'+e.stack; | ||
throw e; | ||
// TODO: pull this `throw` into Machine.build() so it happens as early as possible | ||
} | ||
} | ||
// --• | ||
// Don't use the `defaultsTo` value if an argin was provided for this input. | ||
if (self._configuredInputs[inputCodeName] !== undefined) { | ||
return; | ||
} | ||
}); | ||
// Prune undefined configured exits | ||
self._configuredExits = (function pruneKeysWithUndefinedValues(obj) { | ||
// Prune undefined values from the specified object. | ||
_.each(obj, function (val, key) { | ||
if (val === undefined) { | ||
delete obj[key]; | ||
// --• Use the `defaultsTo` value as the runtime value (argin) for this input. | ||
self._configuredInputs[inputCodeName] = inputDef.defaultsTo; | ||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
// Note that this ^^ is currently using a direct reference. | ||
// TODO: Consider deep cloning the default value first to help prevent userland bugs due to | ||
// entanglement. Cloning would only occur the input's example does not contain any `===`s | ||
// or `->`s. (Easiest way to do that is with rttc.dehyrate().) Anyway, regardless of _how_ | ||
// this is implemented, it would need to be configurable. | ||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
// If this is a contract input, then attempt to build a submachine out of the provided `defaultsTo` function. | ||
if (!_.isUndefined(inputDef.contract) && (rttc.infer(inputDef.example) === 'lamda')) { | ||
try { | ||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
// Note: | ||
// Currently (see TODO above for "why currently") we build machines out of default lamda input values | ||
// that specify a contract here. | ||
// | ||
// Otherwise the default functions would never get built into machine instances, because we're not actually | ||
// calling rttc.validate() on `defaultsTo` values in general. | ||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | ||
// If lamda input def specifies a `defaultsTo`, and no input value was provided, go ahead | ||
// and instantiate the default function into a machine using the contract. | ||
self._configuredInputs[inputCodeName] = buildLamdaMachine(inputDef.defaultsTo, inputCodeName, self, self._rootMachine||self); | ||
} catch (e) { | ||
var err_couldNotBuildSubmachineForDefaultVal = new Error( | ||
'Could not execute machine (`'+self.identity+'`). '+ | ||
'This machine definition specifies a `defaultsTo` for a contract input (`'+inputCodeName+'`), '+ | ||
'but that `defaultsTo` function could not be built into a submachine using the provided `contract`. '+ | ||
'Please check that the `contract` dictionary and `defaultsTo` function are valid.\n'+ | ||
'Error details:\n'+e.stack | ||
); | ||
err_couldNotBuildSubmachineForDefaultVal.input = inputCodeName; | ||
throw err_couldNotBuildSubmachineForDefaultVal; // << TODO: pull this `throw` into Machine.build() so it happens as early as possible | ||
} | ||
}); | ||
return obj; | ||
})(self._configuredExits); | ||
}//</if this input has a contract and `example: '->'`> | ||
});//</_.each() :: input definition> | ||
// ******************************************************************************** | ||
// Should we implement Deferred/promise usage..? | ||
// No- not here. Better to keep things simple in this module and use | ||
// something like Bluebird to present this abstraction in a wrapper module. | ||
// Leaving this TODO here for other folks. Hit @mikermcneil up on | ||
// Twitter if you're interested in using machines via promises | ||
// and we'll figure something out. | ||
// ******************************************************************************** | ||
// Prune any configured exit callbacks that have `undefined` on the RHS. | ||
_.each(_.keys(self._configuredExits), function (exitCodeName) { | ||
if (self._configuredExits[exitCodeName] === undefined) { | ||
delete self._configuredExits[exitCodeName]; | ||
} | ||
});//</_.each() :: each key in dictionary of configured exit callbacks> | ||
// For convenience, set up a couple of local variables for use below: | ||
// | ||
// • `cacheSettings` - the configured cache settings for this machine | ||
var _cache = this._cacheSettings; | ||
// | ||
// • `Cache` - the configured cache model to use (a Waterline model) | ||
// (if not relevant, will be left as `undefined`) | ||
var Cache; | ||
// If `_cache` is not valid, null it out. | ||
if ( | ||
! ( | ||
// Validate cache settings. | ||
var areCacheSettingsValid = | ||
_.isObject(_cache) && | ||
@@ -146,9 +246,14 @@ _.isObject(_cache.model) && | ||
_.isFunction(_cache.model.destroy) && | ||
_.isFunction(_cache.model.count) | ||
) | ||
) { | ||
_.isFunction(_cache.model.count); | ||
// If cache settings are NOT valid, then set `_cache` | ||
// to `false` & leave `Cache` undefined. | ||
if (!areCacheSettingsValid) { | ||
_cache = false; | ||
} | ||
// Otherwise if _cache IS valid, normalize it and apply defaults | ||
// ‡ Otherwise cache settings ARE valid. So we'll use them. | ||
else { | ||
// Fold in default cache settings. | ||
_.defaults(_cache, { | ||
@@ -178,2 +283,5 @@ | ||
// Set local variable as a reference to the cache model for convenience. | ||
Cache = _cache.model; | ||
// Pre-calculate the expiration date so we only do it once | ||
@@ -183,27 +291,20 @@ // (and also so it uses a consistent timestamp since the code | ||
_cache.expirationDate = new Date( (new Date()) - _cache.ttl); | ||
} | ||
}//</else :: cache settings are valid> | ||
// Cache lookup | ||
// Below, we'll use a hash function to create a unique hash (aka checksum) for every distinct set | ||
// of argins. We'll store this in this local variable (`hash`). | ||
// | ||
// The cache uses a hash function to create a unique id for every distinct | ||
// input configuration (these hash sums are only unique per-machine-type.) | ||
// > Note that these hashes do not include the machine identity; meaning they are l-unique | ||
// > _per machine._ So the hash representing `{a:1,b:1}` is the same, whether you're passing | ||
// > those argins in to `.multiply()` or `.subtract()`. | ||
var hash; | ||
// | ||
// Note that this means the machine cache is global not to any particular | ||
// machine instance, but to the machine type itself-- that is, within the | ||
// scope of the cache model. | ||
// | ||
// e.g. | ||
// The cached result of a given set of inputs for a particular type of machine | ||
// will be the same for all instances of that machine using the same cache model | ||
// (which could be shared across devices/routes/processes/apps/servers/clouds/continents) | ||
// | ||
// Old cache entries are garbage collected every time a cache miss occurs | ||
// (also see `maxOldEntriesBuffer` option above for details) | ||
// Now attempt a cache lookup, if configured to do so, then run the machine. | ||
(function _cacheLookup (cb) { | ||
if (!_cache) return cb(); | ||
(function _doCacheLookupMaybe (cb) { | ||
if (!_cache) { return cb(); } | ||
@@ -215,3 +316,3 @@ // Run hash function to calculate appropriate `hash` criterion | ||
// (could not calculate unique hash for configured input values) | ||
if (err) return cb(err); | ||
if (err) { return cb(err); } | ||
@@ -221,30 +322,17 @@ // Hashsum was calculated successfully | ||
// Now hit the provided cache model | ||
// (remember- we know it's valid because we validated/normalized | ||
// our `_cache` variable ahead of time) | ||
_cache.model.find((function _buildFindCriteria(options){ | ||
// Get the criteria to pass to `.find()` when looking up | ||
// existing values in this cache for a particular hash. | ||
return { | ||
where: { | ||
createdAt: { | ||
'>': options.expirationDate | ||
}, | ||
hash: options.hash | ||
}, | ||
sort: 'createdAt DESC', | ||
limit: 1 | ||
}; | ||
})({ | ||
hash: hash, | ||
expirationDate: _cache.expirationDate | ||
})) | ||
// Now call `.find()` on the provided Cache model in order to look up the cached return value | ||
// for the hash representing this particular set of argins. | ||
Cache.find({ | ||
where: { | ||
createdAt: { '>': _cache.expirationDate }, | ||
hash: hash | ||
}, | ||
sort: 'createdAt DESC', | ||
limit: 1 | ||
}) | ||
.exec(function (err, cached) { | ||
// Cache lookup encountered fatal error | ||
if (err) { | ||
return cb(err); | ||
} | ||
if (err) { return cb(err); } | ||
// Cache hit | ||
else if (cached.length && typeof cached[0].data !== 'undefined') { | ||
// --• If this was a cache hit... | ||
if (cached.length && typeof cached[0].data !== 'undefined') { | ||
// console.log('cache hit', cached); | ||
@@ -256,8 +344,8 @@ var newestCacheEntry = cached[0]; | ||
// Cache miss | ||
// --• If this was a cache miss... | ||
return cb(); | ||
}); | ||
}); | ||
})(function afterCacheLookup(err){ | ||
});//</Cache.find() :: finding records in cache model> | ||
});//</calculateHash() :: calculating hash> | ||
})(function afterwards(err){ | ||
if (err) { | ||
@@ -269,18 +357,40 @@ // If cache lookup encounters a fatal error, emit a warning | ||
// Run the machine | ||
(function _runMachine () { | ||
// >- | ||
// Perform garbage collection on cache, if necessary. | ||
// | ||
// > Old cache entries are garbage collected every time a cache miss occurs. | ||
// > | ||
// > If `> maxOldEntriesBuffer` matching cache records exist, then | ||
// > it's time to clean up. Go ahead and delete all the old unused | ||
// > cache entries except the newest one. | ||
// > | ||
// > Note that we don't need to wait for garbage collection to run the | ||
// > machine. That happens below. | ||
if (_cache) { | ||
// Perform garbage collection on cache, if necessary. | ||
// | ||
// If `> maxOldEntriesBuffer` matching cache records exist, then | ||
// it's time to clean up. Go ahead and delete all the old unused | ||
// cache entries except the newest one | ||
// | ||
// Note that we don't need to wait for garbage collection to run the | ||
// machine. That happens below. | ||
// | ||
// (TODO: pull all this craziness out into a separate module/file) | ||
if (_cache) { | ||
Cache.count({ | ||
where: { | ||
createdAt: { | ||
'<=': _cache.expirationDate | ||
}, | ||
hash: hash | ||
} | ||
}).exec(function (err, numOldCacheEntries){ | ||
if (err) { | ||
// If this garbage collection diagnostic query encounters a fatal error, | ||
// emit a warning and then don't do anything else for now. | ||
self.warn(err); | ||
return; | ||
} | ||
_cache.model.count({ | ||
// --• | ||
// If there aren't enough expired cache entries for this hash to warrant a wipe, just bail. | ||
if (numOldCacheEntries <= _cache.maxOldEntriesBuffer) { | ||
return; | ||
} | ||
// --• | ||
// Otherwise, there are enough expired cache records for this exact set of argins | ||
// to warrant a wipe. So destroy all expired cache records with this hash. | ||
Cache.destroy({ | ||
where: { | ||
@@ -291,157 +401,166 @@ createdAt: { | ||
hash: hash | ||
} | ||
}).exec(function (err, numOldCacheEntries){ | ||
}, | ||
sort: 'createdAt DESC', | ||
skip: _cache.maxOldEntriesBuffer | ||
}).exec(function (err, oldCacheEntries) { | ||
if (err) { | ||
// If this garbage collection diagnostic query encounters a fatal error, | ||
// emit a warning and then don't do anything else for now. | ||
// If garbage collection encounters a fatal error, emit a warning | ||
// and then don't do anything else for now. | ||
self.warn(err); | ||
return; | ||
} | ||
if (numOldCacheEntries > _cache.maxOldEntriesBuffer) { | ||
// console.log('gc();'); | ||
// --• | ||
// Sucessfully wiped all expired cache records for this exact set of argins! | ||
_cache.model.destroy({ | ||
where: { | ||
createdAt: { | ||
'<=': _cache.expirationDate | ||
}, | ||
hash: hash | ||
}, | ||
sort: 'createdAt DESC', | ||
skip: _cache.maxOldEntriesBuffer | ||
}).exec(function (err, oldCacheEntries) { | ||
if (err) { | ||
// If garbage collection encounters a fatal error, emit a warning | ||
// and then don't do anything else for now. | ||
self.warn(err); | ||
} | ||
});//</.destroy() :: destroying expired cache records for this exact set of argins> | ||
});//</.count() :: counting expired cache records for this exact set of argins (to see if it's worth it to wipe them)> | ||
}//</if `_cache` is truthy, then we just started destroying expired cache entries> | ||
// _∏_ | ||
// Garbage collection was successful. | ||
// console.log('-gc success-'); | ||
}); | ||
} | ||
}); | ||
} | ||
// Before proceeding, ensure error exit is still configured w/ a callback. | ||
// If it is not, then get crazy and **throw** BEFORE calling the machine's `fn`. | ||
// | ||
// This is just a failsafe-- better to potentially terminate the process than | ||
// open up the possibility of silently swallowing errors later. | ||
if (!self._configuredExits.error){ | ||
throw new Error('Consistency violation: Cannot execute machine (`'+self.identity+'`) without providing any catchall error handling (e.g. an `error` callback).'); | ||
} | ||
// Before proceeding, ensure error exit is configured w/ | ||
// a callback. If it is not, then get crazy and **throw** BEFORE | ||
// calling the machine's `fn`. | ||
// | ||
// TODO: can probably remove this-- it's just here as a failsafe | ||
if (!self._configuredExits.error){ | ||
// console.log('machine:'+self.identity+' => NO ERROR EXIT CONFIGURED!'); | ||
// Fill in anonymous forwarding callbacks for any unhandled exits (ignoring the default exit) | ||
// and have them redirect to the `error` (i.e. catchall) exit | ||
_.each(_.keys(self.exits), function (exitCodeName) { | ||
// If it does not, throw. | ||
throw new Error('machine:'+self.identity+' => Invalid usage- an `error` callback must be provided. The machine was not executed.'); | ||
// Skip default exit and error exit (they're already accounted for.) | ||
if (exitCodeName === 'success' || exitCodeName === 'error') { | ||
return; | ||
} | ||
// console.log('machine:'+self.identity+' has error exit.'); | ||
// If this exit is handled then we're good. | ||
if (self._configuredExits[exitCodeName]) { | ||
return; | ||
} | ||
// Fill in anonymous forwarding callbacks for any unhandled exits (ignoring the default exit) | ||
// and have them redirect to the `error` (i.e. catchall) exit | ||
_.each(_.keys(self.exits), function (exitName) { | ||
// --• | ||
// Otherwise, the exit is unhandled. | ||
Debug('built fwding callback for exit "%s", where there is no implemented callback', exitCodeName); | ||
// Skip default exit and error exit | ||
if ((exitName === 'success') || (exitName === 'error')) { | ||
return; | ||
} | ||
// Build a callback function for this exit. | ||
// When/if it is run, this dynamically-generated callback will: | ||
// • generate an Error instance with a useful message | ||
// • trigger the callback configured for the `error` exit (and pass in its new Error as the first argument) | ||
self._configuredExits[exitCodeName] = function __triggeredMiscExit(_resultPassedInByMachineFn){ | ||
// If exit is unhandled, handle it with a function which triggers the `error` exit. | ||
// Generates an Error instance with a useful message and passes it as the first argument. | ||
if (!self._configuredExits[exitName]) { | ||
Debug('built fwding callback for exit "%s", where there is no implemented callback', exitName); | ||
self._configuredExits[exitName] = function forwardToErrorExit (_resultPassedInByMachineFn){ | ||
// Build an error instance. | ||
var errMsg = '`'+self.identity+'` triggered its `'+exitName+'` exit'; | ||
// Build an error instance | ||
var _err = new Error(util.format('`%s` triggered its `%s` exit', self.identity, exitName) + ((self.exits[exitName] && self.exits[exitName].description)?': '+self.exits[exitName].description:'') + ''); | ||
_err.code = exitName; | ||
_err.exit = exitName; | ||
// Use the description, if one was provided. | ||
var exitDef = self.exits[exitName]; | ||
if (!_.isObject(exitDef)) { throw new Error('Consistency violation: Live machine instance ('+self.identity+') has become corrupted! One of its exits (`'+exitName+'`) has gone missing _while the machine was being run_!'); } | ||
if (exitDef.description) { | ||
errMsg += ': '+self.exits[exitName].description; | ||
} | ||
// If a result was passed in, it will be stuff it in the generated Error instance | ||
// as the `output` property. | ||
if (!_.isUndefined(_resultPassedInByMachineFn)) { | ||
_err.output = _resultPassedInByMachineFn; | ||
} | ||
// Construct the error instance, then add the `exit` property. | ||
// (also set `code` for compatibility) | ||
var err_forwarding = new Error(errMsg); | ||
err_forwarding.exit = exitName; | ||
err_forwarding.code = exitName; | ||
// Trigger configured error callback on `_configuredExits` - (which is already a switchback... | ||
// ...so this should work even if no error callback was explicitly configured... | ||
// ...but in case it doesn't, we already threw above if no error exit exists) | ||
// - using our new Error instance as the argument. | ||
self._configuredExits.error(_err); | ||
}; | ||
// If a result was passed in, it will be stuff it in the generated Error instance | ||
// as the `output` property. | ||
if (!_.isUndefined(_resultPassedInByMachineFn)) { | ||
err_forwarding.output = _resultPassedInByMachineFn; | ||
} | ||
}); | ||
// Trigger configured error callback on `_configuredExits` - (which is already a switchback... | ||
// ...so this should work even if no error callback was explicitly configured... | ||
// ...but in case it doesn't, we already threw above if no error exit exists) | ||
// - using our new Error instance as the argument. | ||
self._configuredExits.error(err_forwarding); | ||
// Intercept the exits to implement exit type coercion, some logging functionality, | ||
// ensure at least one tick has elapsed (if relevant), etc. | ||
var interceptedExits = interceptExitCallbacks(self._configuredExits, _cache, hash, self); | ||
};//</built dynamic callback that forwards to `error`> | ||
// Now it's time to run the machine fn. | ||
try { | ||
// We'll create the ***implementor*** switchback | ||
// (fourth argument (`true`) tells the switchback to run synchronously) | ||
var implementorSwitchback = switchback(interceptedExits, undefined, undefined, true); | ||
});//</_.each() :: exit definition> | ||
// Before calling function, set up a `setTimeout` function that will fire | ||
// when the runtime duration exceeds the configured `timeout` property. | ||
// If `timeout` is falsey or <0, then we ignore it. | ||
if (self._doTrackDuration && self.timeout && self.timeout > 0){ | ||
if (self._timeoutAlarm) { | ||
throw new Error('Unexpected error occurred: `_timeoutAlarm` should never already exist on a machine instance before it is run. Perhaps you called `.exec()` more than once? If so, please fix and try again.'); | ||
} | ||
self._timeoutAlarm = setTimeout(function (){ | ||
if (self._exited) { | ||
throw new Error('Unexpected error occurred: timeout alarm should never be triggered when `_exited` is set. Perhaps you called `.exec()` more than once? If so, please fix and try again.'); | ||
} | ||
self._exited = 'error'; | ||
var err = new Error( util.format( | ||
'This machine took too long to execute (timeout of %dms exceeded.) '+ | ||
'There is probably an issue in the machine\'s implementation (might have forgotten to call `exits.success()`, etc.) '+ | ||
'If you are the implementor of this machine, and you\'re sure there are no problems, you can configure '+ | ||
'the maximum expected number of miliseconds for this machine using `timeout` (a top-level property in '+ | ||
'your machine definition). To disable this protection, set `timeout` to 0.', | ||
(self.timeout) | ||
) ); | ||
err.code = 'E_TIMEOUT'; | ||
// Intercept our configured exit callbacks in order to implement type coercion of runtime output. | ||
// This also takes care of some logging functionality, and if relevant, ensures at least one tick | ||
// has elapsed. Etcetera. | ||
var interceptedExitCbs = interceptExitCallbacks(self._configuredExits, _cache, hash, self); | ||
// Trigger callback | ||
implementorSwitchback.error(err); | ||
// Now it's time to run the machine fn. | ||
// > Use a try/catch to protect against any unexpected errors. | ||
try { | ||
// Then immediately set the `_timedOut` flag so when/if `fn` calls its exits, | ||
// we won't trigger the relevant callback (since we've already triggered `error`). | ||
self._timedOut = true; | ||
// We'll create the ***implementor*** switchback. | ||
// (fourth argument (`true`) tells the switchback to run synchronously) | ||
var implementorSwitchback = switchback(interceptedExitCbs, undefined, undefined, true); | ||
}, self.timeout); | ||
} | ||
// Before calling function, set up a `setTimeout` function that will fire | ||
// when the runtime duration exceeds the configured `timeout` property. | ||
// If `timeout` is falsey or <0, then we ignore it. | ||
if (self._doTrackDuration && self.timeout && self.timeout > 0){ | ||
// For sanity, do one last assertion that `fn` is valid. | ||
if (!_.isFunction(self.fn)) { | ||
throw new Error( | ||
'This machine ('+self.identity+') is corrupted! Its `fn` property is not a function '+ | ||
'(instead it\'s a '+rttc.getDisplayType(self.fn)+')' | ||
); | ||
} | ||
if (self._timeoutAlarm) { throw new Error('Consistency violation: `_timeoutAlarm` should never already exist on a machine instance before it is run. Perhaps you called `.exec()` more than once? If so, please fix and try again.'); } | ||
// Then call the machine's `fn`. | ||
self.fn.apply(self._configuredEnvironment, [self._configuredInputs, implementorSwitchback, self._configuredEnvironment]); | ||
return; | ||
self._timeoutAlarm = setTimeout(function __machineTimedOut(){ | ||
// Assert that our `_exited` spinlock has not already been set. | ||
// (better to terminate the process than trigger a callback twice) | ||
if (self._exited) { throw new Error('The timeout alarm was triggered when `_exited` was already set. Perhaps you called `.exec()` more than once? If so, please fix and try again.'); } | ||
self._exited = 'error'; | ||
var err = new Error( | ||
'This machine took too long to execute (timeout of '+self.timeout+'ms exceeded.) '+ | ||
'There is probably an issue in the machine\'s implementation (might have forgotten to call `exits.success()`, etc.) '+ | ||
'If you are the implementor of this machine, and you\'re sure there are no problems, you can configure '+ | ||
'the maximum expected number of miliseconds for this machine using `timeout` (a top-level property in '+ | ||
'your machine definition). To disable this protection, set `timeout` to 0.'); | ||
err.code = 'E_TIMEOUT'; | ||
// Trigger callback | ||
implementorSwitchback.error(err); | ||
// Then immediately set the `_timedOut` flag so when/if `fn` calls its exits, | ||
// we won't trigger the relevant callback (since we've already triggered `error`). | ||
self._timedOut = true; | ||
}, self.timeout);//</set timeout alarm> | ||
// _∏_ | ||
}//</if not tracking duration, or `timeout` is not set, or is less than zero for some reason> | ||
// >- | ||
// For sanity, do one last assertion to make sure `fn` is valid. | ||
if (!_.isFunction(self.fn)) { | ||
throw new Error(''+ | ||
'Consistency violation: Live machine instance ('+self.identity+') has become corrupted!\n'+ | ||
'Its `fn` property is no longer a function-- instead it\'s a '+rttc.getDisplayType(self.fn)+':\n'+ | ||
util.inspect(self.fn, {depth: null})+ | ||
''); | ||
} | ||
catch(e) { | ||
// Here we re-create the ***userland*** switchback and call it with the error that occurred. | ||
// (fourth argument (`true`) tells switchback to run synchronously) | ||
// | ||
// Note that this could probably be removed eventually, since at this point `interceptedExits` | ||
// should actually already be a switchback. | ||
return switchback(interceptedExits, undefined, undefined, true)(e); | ||
} | ||
})(); | ||
}); | ||
// --• | ||
// Then call the machine's `fn`. | ||
self.fn.apply(self._configuredEnvironment, [self._configuredInputs, implementorSwitchback, self._configuredEnvironment]); | ||
} catch(e) { | ||
// Here we re-create the ***userland*** switchback and call it with the error that occurred. | ||
// (fourth argument (`true`) tells switchback to run synchronously) | ||
// | ||
// Note that this could probably be removed eventually, since at this point `interceptedExitCbs` | ||
// should actually already be a switchback. | ||
return switchback(interceptedExitCbs, undefined, undefined, true)(e); | ||
}//</catch> | ||
});//</doing cache lookup, if relevant, then continuing on to do more stuff ^^> | ||
// _∏_ | ||
return this; | ||
}; | ||
{ | ||
"name": "machine", | ||
"version": "13.0.0-2", | ||
"version": "13.0.0-3", | ||
"description": "Configure and execute machines", | ||
@@ -36,3 +36,2 @@ "main": "index.js", | ||
"lodash": "3.10.1", | ||
"object-hash": "0.3.0", | ||
"rttc": "^9.8.1", | ||
@@ -39,0 +38,0 @@ "switchback": "2.0.0" |
131349
6
2679
- Removedobject-hash@0.3.0
- Removedobject-hash@0.3.0(transitive)