help-build-machine.js 115 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061
  1. /**
  2. * Module dependencies
  3. */
  4. var util = require('util');
  5. var _ = require('@sailshq/lodash');
  6. var flaverr = require('flaverr');
  7. var parley = require('parley');
  8. var rttc = require('rttc');
  9. var getIsProductionWithoutDebug = require('./get-is-production-without-debug');
  10. var hashCustomUsageOpts = require('./hash-custom-usage-opts');
  11. var normalizeMachineDef = require('./normalize-machine-def');
  12. var normalizeArgins = require('./normalize-argins');
  13. var GENERIC_HELP_SUFFIX = require('./GENERIC_HELP_SUFFIX.string');
  14. var getMethodName = require('../get-method-name');
  15. /**
  16. * .helpBuildMachine()
  17. *
  18. * Build a callable (aka "wet machine") function using the specified definition
  19. * and custom usage options.
  20. *
  21. * ·····································································································
  22. * @param {Dictionary} opts
  23. * · · · · · · · · · · · · · · · · · · · · · ·
  24. * Implementor options:
  25. *
  26. * @property {Dictionary} def (A Node-Machine definition -- see https://node-machine.org/spec)
  27. *
  28. * · · · · · · · · · · · · · · · · · · · · · ·
  29. * Custom (usage / higher-order runner) options:
  30. *
  31. * @property {String?} arginStyle ("named" or "serial")
  32. * @property {String?} execStyle ("natural", "deferred", or "immediate")
  33. *
  34. * @property {String?} arginValidationTactic ("coerceAndCloneOrError" or "error" or "doNotCheck")
  35. * @property {String?} resultValidationTactic ("forceCoerceAndClone" or "coerceAndCloneOrError" or "error" or "doNotCheck")
  36. * @property {String?} extraArginsTactic ("warn" or "warnAndOmit" or "error" or "doNotCheck")
  37. * @property {String?} extraCallbacksTactic ("warnAndOmit" or "error" or "doNotCheck")
  38. *
  39. * @property {Function?} finalAfterExec (a lifecycle callback-- specifically the final after-`.exec()` handler function, as defined by parley)
  40. * @property {Dictionary?} defaultMeta (an optional dictionary of defaults to automatically include as if `.meta()` was used, but every time the machine function is executed. `.meta()` can still be called in userland, and if so, the provided metadata is treated as overrides and `_.extend()`-ed on top of a shallow copy of the baked-in `defaultMeta`. Note that despite this option, in userland, subsequent calls to `.meta()` after the first STILL COMPLETELY IGNORE PRIOR USERLAND `.meta()` CALLS! For example, if you build a machine with a `defaultMeta` of `{foo: 123}`, then in userland code execute it with `.meta({foo: 456})`, in the machine's implementation, `this.foo` will be `456`. But if you execute the machine with `.meta({foo: 456}).meta({bar: 999})`, then in the machine's implementation, `this.bar` will be `999` but `this.foo` will be `undefined`!)
  41. * @property {Dictionary?} defaultArgins (an optional dictionary of defaults to automatically include as they were passed in as argins, but every time the machine function is executed. These take precedent over `defaultsTo`. If any of them don't apply to this machine (i.e. no matching input), they will be ignored.)
  42. *
  43. * @property {String?} implementationSniffingTactic ("analog" or "analogOrClassical")
  44. * ·····································································································
  45. * @param {Error?} btOmen
  46. * Our build-time omen, if relevant.
  47. * (For a more complete explanation of what an "omen" is,
  48. * see the relevant comments in `build.js`.)
  49. * ·····································································································
  50. * @returns {Function} a callable (aka "wet") machine function, potentially with a custom usage style.
  51. */
  52. module.exports = function helpBuildMachine(opts, btOmen) {
  53. // Assert valid usage of this method.
  54. // > (in production, we skip this stuff to save cycles. We can get away w/ that
  55. // > because we only include this here to provide better error msgs in development
  56. // > anyway)
  57. if (!getIsProductionWithoutDebug()) {
  58. if (!_.isObject(opts) || _.isArray(opts) || _.isFunction(opts)) {
  59. throw new Error('opts must be a dictionary (i.e. plain JavaScript object like `{}`). But instead, got:\n'+util.inspect(opts, {depth: 5}));
  60. }
  61. // We only know how to build "dry" Node-Machine definitions. But we also use duck-typing
  62. // to check for other common things which are mistakenly passed in so we can handle them
  63. // with explicit error messages:
  64. // • already-instantiated ("wet") machine instances
  65. // • naked functions
  66. if (
  67. _.isFunction(opts.def) && (
  68. (opts.def.getDef&&opts.def.customize) || // machine@latest
  69. opts.def.isWetMachine || // machine@circa v12.x-14.x
  70. opts.def.name==='_callableMachineWrapper'// machine@ earlier versions
  71. )
  72. ) {
  73. throw new Error(
  74. 'Cannot build because building from a pre-existing Callable is no longer supported-- '+
  75. 'please pass in the dry definition instead.\n'+
  76. ' [?] Check out https://sailsjs.com/support for further assistance.'
  77. );
  78. } else if (_.isFunction(opts.def)) {
  79. throw new Error(
  80. 'Cannot build from a raw function.\n'+
  81. 'Instead, please use a well-formed definition like:\n'+
  82. ' {\n'+
  83. ' fn: async function (inputs, exits) {\n'+
  84. ' return exits.success();\n'+
  85. ' }\n'+
  86. ' }\n'+
  87. ' [?] Check out https://sailsjs.com/support for further assistance.'
  88. );
  89. } else if (!_.isObject(opts.def) || _.isArray(opts.def)) {
  90. throw new Error('Cannot build because the definition is invalid. Expected a dictionary (i.e. plain JavaScript object like `{}`), but instead, got:\n'+util.inspect(opts.def, {depth: 5}));
  91. }
  92. if (opts.arginStyle !== undefined && !_.contains(['named','serial'], opts.arginStyle)) {
  93. throw new Error('If specified, `arginStyle` must be either "named" or "serial". But instead, got:\n'+util.inspect(opts.arginStyle, {depth: 5}));
  94. }
  95. if (opts.execStyle !== undefined && !_.contains(['natural','deferred','immediate'], opts.execStyle)) {
  96. throw new Error('If specified, `execStyle` must be either "natural", "deferred", or "immediate". But instead, got:\n'+util.inspect(opts.execStyle, {depth: 5}));
  97. }
  98. if (opts.arginValidationTactic !== undefined && !_.contains(['coerceAndCloneOrError','error','doNotCheck'], opts.arginValidationTactic)) {
  99. throw new Error('If specified, `arginValidationTactic` must be either "coerceAndCloneOrError", "error", or "doNotCheck". But instead, got:\n'+util.inspect(opts.arginValidationTactic, {depth: 5}));
  100. }
  101. if (opts.resultValidationTactic !== undefined && !_.contains(['forceCoerceAndClone','coerceAndCloneOrError','error','doNotCheck'], opts.resultValidationTactic)) {
  102. throw new Error('If specified, `resultValidationTactic` must be either "forceCoerceAndClone", "coerceAndCloneOrError", "error", or "doNotCheck". But instead, got:\n'+util.inspect(opts.resultValidationTactic, {depth: 5}));
  103. }
  104. if (opts.extraArginsTactic !== undefined && !_.contains(['warn', 'warnAndOmit','error','doNotCheck'], opts.extraArginsTactic)) {
  105. throw new Error('If specified, `extraArginsTactic` must be either "warn", "warnAndOmit", "error", or "doNotCheck". But instead, got:\n'+util.inspect(opts.extraArginsTactic, {depth: 5}));
  106. }
  107. if (opts.extraCallbacks !== undefined && !_.contains(['warnAndOmit', 'error','doNotCheck'], opts.extraCallbacks)) {
  108. throw new Error('If specified, `extraCallbacks` must be either "warnAndOmit", "error", or "doNotCheck" (i.e. silently omit). But instead, got:\n'+util.inspect(opts.extraCallbacks, {depth: 5}));
  109. }
  110. if (opts.implementationSniffingTactic !== undefined && !_.contains(['analog', 'analogOrClassical'], opts.implementationSniffingTactic)) {
  111. throw new Error('If specified, `implementationSniffingTactic` must be either "analog" or "analogOrClassical". But instead, got:\n'+util.inspect(opts.extraCallbacks, {depth: 5}));
  112. }
  113. var VALID_OPTIONS = [
  114. 'def',
  115. // Sweeping usage style preferences:
  116. 'arginStyle',
  117. 'execStyle',
  118. // More advanced / fine-grained / intricate / lower-level / performance-oriented preferences:
  119. // > These options allow userland code to circumvent default conventions of the runner.
  120. // > Use at your own risk! These settings potentially allow you to set up circumstances that
  121. // > will make your use case dependent on an interplay of guarantees and expectations between
  122. // > the machine implementation and your userland code. In other words, if you change these,
  123. // > I sure hope you know what you're doing!
  124. 'arginValidationTactic',
  125. 'resultValidationTactic',
  126. 'extraArginsTactic',
  127. 'extraCallbacksTactic',
  128. // Options for higher-level runners:
  129. 'finalAfterExec',
  130. 'defaultMeta',
  131. 'defaultArgins',
  132. // Options for implementors:
  133. 'implementationSniffingTactic'
  134. ];
  135. var extraneousOpts = _.difference(_.keys(opts), VALID_OPTIONS);
  136. if (extraneousOpts.length > 0) {
  137. throw flaverr({
  138. name:
  139. 'UsageError',
  140. message:
  141. 'Sorry, cannot build function due to unrecognized option(s): '+util.inspect(extraneousOpts, {depth: 5})+'\n'+
  142. 'Valid options are: \n'+
  143. ' • '+VALID_OPTIONS.join('\n • ')+'\n'+
  144. '\n'+
  145. GENERIC_HELP_SUFFIX
  146. }, btOmen);
  147. }
  148. }//fi (skip usage checks in non-debug production env for performance reasons)
  149. // Get easy access to the machine definition.
  150. var nmDef = opts.def;
  151. // Normalize + validate + set implicit defaults for implementation-related
  152. // build options from userland:
  153. var implementationSniffingTactic = opts.implementationSniffingTactic || 'analog';
  154. // Verify correctness of node-machine definition.
  155. // > Note that we modify the "dry" definition in place.
  156. var implProblems = normalizeMachineDef(nmDef, implementationSniffingTactic);
  157. if (implProblems.length > 0) {
  158. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  159. // NOTE: In previous versions, this was:
  160. // ```
  161. // code: 'E_MACHINE_DEFINITION_INVALID',
  162. // ```
  163. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  164. throw flaverr({
  165. name: 'ImplementationError',
  166. code: 'E_INVALID_DEFINITION',
  167. problems: implProblems,
  168. message:
  169. 'Sorry, could not interpret '+(nmDef.identity?'"'+nmDef.identity+'"':'this function')+' because '+
  170. 'its underlying implementation has '+(implProblems.length===1?'a problem':implProblems.length+' problems')+':\n'+
  171. '------------------------------------------------------\n'+
  172. '• '+implProblems.join('\n• ')+'\n'+
  173. '------------------------------------------------------\n'+
  174. '\n'+
  175. (nmDef.identity?
  176. 'If you are the maintainer of "'+nmDef.identity+'", then you can change '+
  177. 'its implementation to solve the problem'+(implProblems.length===1?'':'s')+' above. '+
  178. 'Otherwise, please file a bug report with the maintainer, or fork your own copy and '+
  179. 'fix that.'
  180. :
  181. 'Please resolve the problem'+(implProblems.length===1?'':'s')+' above and try again.'
  182. )+'\n'+
  183. GENERIC_HELP_SUFFIX
  184. }, btOmen);
  185. }//•
  186. // This variable is used as a way to easily access the effective identity
  187. // of this machine (e.g. in order to compute the method name for use in
  188. // error messages)
  189. var identity = nmDef.identity;
  190. // Normalize + validate + set implicit defaults for other userland build options:
  191. var arginStyle = opts.arginStyle || 'named';
  192. var execStyle = opts.execStyle || 'deferred';
  193. var extraArginsTactic = opts.extraArginsTactic || 'warnAndOmit';
  194. var extraCallbacksTactic = opts.extraCallbacksTactic || 'warnAndOmit';
  195. var arginValidationTactic = opts.arginValidationTactic || 'coerceAndCloneOrError';
  196. var resultValidationTactic = opts.resultValidationTactic || 'doNotCheck';
  197. var finalAfterExec = opts.finalAfterExec;
  198. var defaultMeta = opts.defaultMeta;
  199. var defaultArgins = opts.defaultArgins;
  200. if (arginStyle === 'serial') {
  201. // This is a sane covention to allow any machine to be called with serial usage,
  202. // regardless of how many inputs it has and how they're set up, and even if it
  203. // doesn't define an `args` array to indicate its preference for ordering.
  204. // (This just respects key order in `inputs`.)
  205. if (nmDef.args === undefined) {
  206. nmDef.args = _.reduce(nmDef.inputs, function(args, inputDef, inputCodeName){
  207. args.push(inputCodeName);
  208. return args;
  209. }, []);
  210. }
  211. // If specified, check declared `args` to be sure each entry is valid;
  212. // that is, either the code name of a declared input or recognized
  213. // special notation.
  214. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  215. // FUTURE: Pull this into normalize-machine-def.
  216. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  217. if (nmDef.args !== undefined) {
  218. if (!_.isArray(nmDef.args)) {
  219. throw flaverr({
  220. name:
  221. 'ImplementationError',
  222. message:
  223. 'Sorry, cannot build function (`'+getMethodName(nmDef.identity)+'`) because\n'+
  224. 'the underlying implementation\'s `args` is not an array. Instead, got:\n'+
  225. '```\n'+
  226. util.inspect(nmDef.args, {depth:5})+'\n'+
  227. '```\n'+
  228. GENERIC_HELP_SUFFIX
  229. }, btOmen);
  230. }
  231. _.each(nmDef.args, function(arg, i) {
  232. if (arg === '{...}' || arg === '{…}' || arg === '...' || arg === '…' || arg === '*' || arg === '{}') {
  233. throw flaverr({
  234. name:
  235. 'ImplementationError',
  236. message:
  237. 'Sorry, cannot build function (`'+getMethodName(nmDef.identity)+'`) because\n'+
  238. 'the underlying implementation\'s `args` array contains invalid syntax.\n'+
  239. '> Did you mean to use `\'{*}\'` instead?\n'+
  240. GENERIC_HELP_SUFFIX
  241. }, btOmen);
  242. } else if (arg === '{*}') {
  243. if (i !== nmDef.args.length-1){
  244. throw flaverr({
  245. name:
  246. 'ImplementationError',
  247. message:
  248. 'Sorry, cannot build function (`'+getMethodName(nmDef.identity)+'`) because\n'+
  249. 'the underlying implementation\'s `args` array is invalid. If the special\n'+
  250. '`\'{*}\'` notation is used, it must be the very last item in `args`.\n'+
  251. GENERIC_HELP_SUFFIX
  252. }, btOmen);
  253. } else {
  254. // Otherwise it's ok. This is a valid symbol.
  255. }
  256. } else if (!_.isString(arg) || !_.contains(_.keys(nmDef.inputs), arg)) {
  257. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  258. // FUTURE: allow declaring variadic usage (`[[]]`) and spread usage (`etc[]`)
  259. // (See other "FUTURE" notes below for links w/ more info)
  260. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  261. throw flaverr({
  262. name:
  263. 'ImplementationError',
  264. message:
  265. 'Sorry, cannot build function (`'+getMethodName(nmDef.identity)+'`) because\n'+
  266. 'the underlying implementation\'s `args` array contains invalid syntax: `'+util.inspect(arg, {depth:5})+'`\n'+
  267. 'All entries in `args` should be either recognized special notation or the name of a declared input.\n'+
  268. GENERIC_HELP_SUFFIX
  269. }, btOmen);
  270. } else {
  271. // Otherwise it's ok. It's a recognized input code name.
  272. }
  273. });//∞
  274. }
  275. }//fi
  276. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  277. // FUTURE: (maybe) Since timeouts, spinlocks, catching, etc don't work unless using the
  278. // Deferred usage pattern, then log a warning if an explicit callback was passed in at
  279. // runtime for a machine implementation that declares a `timeout`.
  280. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  281. // Return our callable ("wet") machine (which is just a function that runs `fn`,
  282. // really, plus some extra properties that we add below.)
  283. var wetMachine = function runFn(_argins, _explicitCbMaybe, _metadata){
  284. var argins;
  285. var explicitCbMaybe;
  286. var metadata;
  287. // Potentially build a runtime "omen".
  288. // (see comments in `build.js` for explanation/reminder of what an "omen" is.)
  289. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  290. // FUTURE: Provide a way to pass in a custom runtime omen (prbly in metadata).
  291. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  292. var rtOmen = flaverr.omen(runFn);
  293. // In addition to the omen (which won't necessarily have been created -- e.g. in production),
  294. // we also define a traceRef instance. We use this below to ensure that a particular runtime
  295. // error comes from the same "wet machine" invocation we expected it to come from.
  296. var privateTraceRef = {};
  297. // console.log(identity,'• arginStyle', arginStyle, ' • execStyle', execStyle);
  298. // Now determine how to parse our arguments.
  299. switch (arginStyle) {
  300. case 'named':
  301. // If "named", then `argins`, if any, are defined as a dictionary and passed in as the first argument.
  302. // Tolerate a few alternative usages:
  303. // • `runFn(function(err, result){...})`
  304. // • `runFn(function(err, result){...}, {...})`
  305. if (_.isFunction(_argins) && _.isUndefined(_explicitCbMaybe)) {
  306. metadata = _explicitCbMaybe;
  307. explicitCbMaybe = _argins;
  308. }//fi
  309. if (_argins !== undefined) {
  310. argins = _argins;
  311. }
  312. else {
  313. argins = {};
  314. }//fl
  315. if (_explicitCbMaybe !== undefined) {
  316. explicitCbMaybe = _explicitCbMaybe;
  317. }//fi
  318. if (_metadata !== undefined) {
  319. // (fka habitat vars)
  320. metadata = _metadata;
  321. }
  322. else {
  323. metadata = {};
  324. }//fi
  325. // Check usage.
  326. if (!_.isObject(argins) || _.isFunction(argins) || _.isArray(argins)) {
  327. throw flaverr({
  328. name:
  329. 'UsageError',
  330. message:
  331. 'Sorry, this function doesn\'t know how to handle usage like that.\n'+
  332. 'If provided, the 1st argument should be a dictionary like `{...}`\n'+
  333. 'consisting of input values (aka "argins") to pass through to the fn.\n'+
  334. GENERIC_HELP_SUFFIX
  335. }, rtOmen);
  336. }
  337. if (!_.isUndefined(explicitCbMaybe) && !_.isFunction(explicitCbMaybe)) {
  338. if (!_.isArray(explicitCbMaybe) && _.isObject(explicitCbMaybe)) {
  339. throw flaverr({
  340. name:
  341. 'UsageError',
  342. message:
  343. 'Sorry, this function doesn\'t know how to handle {...} callbacks.\n'+
  344. 'If provided, the 2nd argument should be a function like `function(err,result){...}`\n'+
  345. '| If you passed in {...} on purpose as a "switchback" (dictionary of callbacks),\n'+
  346. '| please be aware that, as of machine v15, you can no longer pass in a switchback\n'+
  347. '| as the 2nd argument. And you can\'t pass a switchback in to .exec() anymore either.\n'+
  348. '| Instead, you\'ll need to explicitly call .switch().\n'+
  349. GENERIC_HELP_SUFFIX
  350. }, rtOmen);
  351. }
  352. else {
  353. throw flaverr({
  354. name:
  355. 'UsageError',
  356. message:
  357. 'Sorry, this function doesn\'t know how to handle usage like that.\n'+
  358. 'If provided, the 2nd argument should be a function like `function(err,result){...}`\n'+
  359. 'that will be triggered as a callback after this fn is finished.\n'+
  360. GENERIC_HELP_SUFFIX
  361. }, rtOmen);
  362. }
  363. }//•
  364. break;
  365. case 'serial':
  366. // Parse argins
  367. argins = _.reduce(arguments, function(argins, argin, i){
  368. if (!nmDef.args) {
  369. throw new Error('Consistency violation: `args` is not defined in the implementation, and for some unknown reason, a suitable ordering could not be automatically inferred from `inputs`.');
  370. }
  371. if (!(nmDef.args[i])) {
  372. throw new Error('Invalid usage with serial arguments: Received unexpected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument.');
  373. }
  374. // Interpret special notation.
  375. // > Remember, if we made it to this point, we know it's valid b/c it's already been checked.
  376. if (nmDef.args[i] === '{*}') {
  377. if (argin !== undefined && (!_.isObject(argin) || _.isArray(argin) || _.isFunction(argin))) {
  378. throw new Error('Invalid usage with serial arguments: If provided, expected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument to be a dictionary (plain JavaScript object, like `{}`). But instead, got: '+util.inspect(argin, {depth:5})+'');
  379. } else if (argin !== undefined && _.intersection(_.keys(argins), _.keys(argin)).length > 0) {
  380. throw new Error('Invalid usage with serial arguments: If provided, expected '+(i===0?'first':i===1?'second':i===2?'third':(i+1)+'th')+' argument to have keys which DO NOT overlap with other already-configured argins! But in reality, it contained conflicting keys: '+_.intersection(_.keys(argins), _.keys(argin))+'');
  381. }
  382. _.extend(argins, argin);
  383. } else {
  384. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  385. // Note: For design considerations & historical context, see:
  386. // • https://github.com/node-machine/machine/commit/fa3829fa637a267793be4a7fb573e008581c4656
  387. // • https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facec
  388. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  389. // FUTURE: Support declaring variadic usage
  390. // https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facecR58
  391. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  392. // FUTURE: Support declaring spread arguments
  393. // https://github.com/node-machine/spec/pull/2/files#diff-eba3c42d87dad8fb42b4080df85facecR66
  394. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  395. // Otherwise interpret this as the code name of an input
  396. argins[nmDef.args[i]] = argin;
  397. }
  398. return argins;
  399. }, {});//= (∞)
  400. break;
  401. default:
  402. throw flaverr({
  403. name:'UsageError',
  404. message: 'Unrecognized arginStyle: "'+arginStyle+'"'
  405. }, btOmen);
  406. }
  407. // Build and return an appropriate deferred object.
  408. // (Or possibly just start executing the machine immediately, depending on usage)
  409. var deferredObj = parley(
  410. // ███████╗██╗ ██╗███████╗ ██████╗██╗ ██╗████████╗██╗ ██████╗ ███╗ ██╗███████╗██████╗
  411. // ██╔════╝╚██╗██╔╝██╔════╝██╔════╝██║ ██║╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝██╔══██╗
  412. // █████╗ ╚███╔╝ █████╗ ██║ ██║ ██║ ██║ ██║██║ ██║██╔██╗ ██║█████╗ ██████╔╝
  413. // ██╔══╝ ██╔██╗ ██╔══╝ ██║ ██║ ██║ ██║ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══██╗
  414. // ███████╗██╔╝ ██╗███████╗╚██████╗╚██████╔╝ ██║ ██║╚██████╔╝██║ ╚████║███████╗██║ ██║
  415. // ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
  416. //
  417. function (done){
  418. // Now actually run the machine, in whatever way is appropriate based on its implementation type.
  419. switch (nmDef.implementationType) {
  420. case 'composite':
  421. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  422. // See https://www.youtube.com/watch?v=1X-Q-HUS4mg&list=PLIQKJlrxhPyIDGAZ6CastNOmSSQkXSx2E&index=1
  423. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  424. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute logic implemented in this way (`implementationType: \'composite\'`). Could you transpile it to the common tongue (JavaScript) first, and then change the declared `implementationType`?\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  425. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  426. // FUTURE: for all `string:*` cases: make everything build properly even though `fn` doesn't exist
  427. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  428. case 'string:js':
  429. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute a code string (`implementationType: \'string:js\'`). Could you use `eval()` on it first to get a hydrated function, and then change the declared `implementationType` to `classical` or `analog`?\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  430. case 'string:c':
  431. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in C (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in C, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  432. case 'string:c++':
  433. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in C++ (`implementationType: \''+nmDef.implementationType+'++\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in C++, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  434. case 'string:c#':
  435. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in C# (`implementationType: \''+nmDef.implementationType+'#\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in C#, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  436. case 'string:rust':
  437. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Rust (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Rust, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  438. case 'string:php':
  439. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in PHP (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in PHP, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  440. case 'string:python':
  441. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Python (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Python, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  442. case 'string:ruby':
  443. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Ruby (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Ruby, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  444. case 'string:erlang':
  445. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Erlang (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Erlang, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  446. case 'string:java':
  447. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Java (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Java, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  448. case 'string:groovy':
  449. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Groovy (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Groovy, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  450. case 'string:scala':
  451. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in Scala (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in Scala, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  452. case 'string:typescript':
  453. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in TypeScript (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in TypeScript, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  454. case 'string:coffeescript':
  455. return done(flaverr({name:'UsageError', message: 'This runner doesn\'t know how to interpret and execute code strings of logic implemented in TypeScript (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile it to the common tongue (JavaScript) first, then change the declared `implementationType` to `string:js`? Otherwise, try a runner written in TypeScript, or rewrite this logic in JavaScript.\n'+GENERIC_HELP_SUFFIX}, rtOmen));
  456. case 'abstract':
  457. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  458. // FUTURE: make everything build properly even though `fn` won't exist
  459. // i.e. - still do the argin checks etc, and then have the machine PRETEND to execute,
  460. // but exit with a descriptive + parseable "Not implemented yet" kind of an error.
  461. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  462. return done(flaverr({name:'UsageError', message: 'The experimental `abstract` implementation type is not yet supported. See https://sailsjs.com/support for help.'}, rtOmen));
  463. case 'classical':
  464. case 'analog':
  465. // If build-time options indicate that we're free to perform cloning of argins,
  466. // then do an initial, one-time shallow clone. In a moment when we begin
  467. // validating/coercing argins, this will allow us to safely make changes
  468. // without damaging the argins dictionary passed in from userland (which, although
  469. // not recommended, still might potentially be in use elsewhere).
  470. if (arginValidationTactic === 'coerceAndCloneOrError') {
  471. argins = _.extend({}, argins);
  472. }
  473. // Validate argins vs. the declared input definitions, honoring type safety, potentially performing
  474. // light coercion, applying defaultsTo, ensuring required argins are present for all required inputs,
  475. // and preventing unexpected extraneous argins.
  476. // > • In many cases, this will modify `argins` in-place, as a direct reference.
  477. // > • Also note that this behavior is somewhat configurable via build-time options.
  478. var validationProblems = normalizeArgins(argins, arginValidationTactic, extraArginsTactic, defaultArgins, nmDef, rtOmen);
  479. if (validationProblems.length > 0) {
  480. return done(flaverr({
  481. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  482. // NOTE: In previous versions, this was:
  483. // ```
  484. // code: 'E_MACHINE_RUNTIME_VALIDATION',
  485. // ```
  486. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  487. name:
  488. 'UsageError',
  489. code:
  490. 'E_INVALID_ARGINS',
  491. problems:
  492. validationProblems,
  493. message:
  494. 'Could not run '+getMethodName(nmDef.identity)+'() because '+
  495. 'of '+(validationProblems.length===1?'a problem':validationProblems.length+' problems')+':\n'+
  496. '------------------------------------------------------\n'+
  497. '• '+validationProblems.join('\n• ')+'\n'+
  498. '------------------------------------------------------\n'+
  499. '\n'+
  500. 'Please adjust your usage and try again.\n'+
  501. // ' the maintainer of "'+getMethodName(nmDef.identity)+'", then you can change '+
  502. // 'If you are the maintainer of "'+getMethodName(nmDef.identity)+'", then you can change '+
  503. // 'its implementation to solve the problem'+(validationProblems.length===1?'':'s')+' above. '+
  504. // 'Otherwise, please file a bug report with the maintainer, or fork your own copy and '+
  505. // 'fix that.'
  506. // +'\n'+
  507. // 'Could not run `'+getMethodName(identity)+'` due to '+validationProblems.length+' '+
  508. // 'usage problem'+(validationProblems.length===1?'':'s')+':\n'+
  509. // (()=>{
  510. // var bulletPrefixedProblems = _.map(validationProblems, function (vProblem){ return ' • '+vProblem.details; });
  511. // var prettyPrintedValidationProblemsStr = bulletPrefixedProblems.join('\n');
  512. // return prettyPrintedValidationProblemsStr;
  513. // })(),
  514. GENERIC_HELP_SUFFIX,
  515. }, rtOmen));
  516. }//•
  517. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  518. // FUTURE: (maybe) Build callable forms of lambda inversions (aka submachines)??
  519. // But really, maybe consider ripping out support for this in the interest of simplicity.
  520. // Only a few machines really need to use it, and it's easy to make it work. The only thing
  521. // is that we would then lose the nice, consistent handling of edge cases provided by the machine
  522. // runner. But the performance benefits and simplification are probably worth it.
  523. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  524. // Build & return exit handler callbacks for use by the machine's `fn`.
  525. // > Note: The only reason this is a self-calling function is to keep private variables insulated.
  526. var implSideExitHandlerCbs = (function _gettingHandlerCbs(){
  527. var handlerCbs = function() {
  528. throw flaverr({
  529. name: 'CompatibilityError',
  530. message: 'Implementor-land switchbacks are no longer supported by default in the machine runner. Instead of `exits()`, please call `exits.success()` or `exits.error()` from within your machine `fn`. (For help, visit https://sailsjs.com/support)'
  531. }, rtOmen);
  532. };
  533. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  534. //
  535. // USAGE: Node-style callback, or with a promise, or with async...await
  536. // (note that, when leveraging switchbacks, there are slightly different rules)
  537. //
  538. // _______________________________________________________________________________________________________________________________________________________________________________________________________________________________________
  539. // || exit => | 'error' (explicit) | 'error' (throw) | 'error' (timeout) | 'error' (bad argins) | misc. exit | misc. exit | success | success |
  540. // \/ output | `exits.error()` | `throw new Error()` | | | (NOT expecting output) | (EXPECTING output) | (NOT expecting output) | (EXPECTING output) |
  541. // ______________________|_________________________|_________________________|_________________________|_________________________|_________________________|_________________________|_________________________|_________________________|
  542. // Error instance | pass straight through | pass straight through | N/A - always an Error | N/A - always an Error | pass straight through | coerce | pass straight through | coerce |
  543. // | | (handled by parley) | (handled by parley) | | | | | |
  544. // ----------------------| -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- |
  545. // String data | new Error w/ str as msg | new Error w/ str as msg | N/A - always an Error | N/A - always an Error | new Error w/ str as msg | coerce | pass straight through | coerce |
  546. // | | | (handled by parley) | | | | | |
  547. // ----------------------| -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- |
  548. // Other non-Error data | new Error + wrap output | new Error + wrap output | N/A - always an Error | N/A - always an Error | new Error + wrap output | coerce | pass straight through | coerce |
  549. // | | | (handled by parley) | | | | | |
  550. // ----------------------| -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- | -- -- -- -- -- -- |
  551. // No output (undefined) | new Error, generic | new Error, generic | N/A - always an Error | N/A - always an Error | new Error, w/ descrptn. | coerce | pass straight through | coerce |
  552. // | | | (handled by parley) | | | | | |
  553. // _______________________________________________________________________________________________________________________________________________________________________________________________________________________________________
  554. //
  555. /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  556. // Set up support for exit forwarding in implementor-side exit handler callbacks:
  557. // > Note: We use another self-calling function in order to share future logic-- it's just for convenience/deduplication.
  558. // > The important thing is that we call `done` herein.
  559. (function _attachingHandlerCbs(proceed){
  560. // * * * Implementation of exits.error()... * * *
  561. handlerCbs.error = function(rawOutput){
  562. // Ensure that the catchall error exit (`error`) always comes back with an Error instance
  563. // (i.e. so node callback expectations are fulfilled)
  564. var err;
  565. if (undefined === rawOutput) {
  566. err = flaverr({
  567. name: 'Error',
  568. message: 'Internal error occurred while running `'+getMethodName(identity)+'`.',
  569. raw: flaverr({ name: 'ImplementationError' }, new Error('Called generic exits.error() callback, but without passing in an Error instance to explain "why".'), handlerCbs.error)
  570. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  571. // FUTURE: Consider doing something like this ^^ for a few of the other Errors below!
  572. // (Specifically, the ones that came from a deliberate invocation of `exits.error()`
  573. // since, in those cases, the trace is easily lost. However, note the tactic of calling
  574. // exits.error() deliberately is growing increasingly rare thanks to `await`, so going
  575. // further than this may not really be worth it.)
  576. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  577. }, rtOmen);
  578. }
  579. else {
  580. // Check to see if this is an Error (or close enough), and if it's not 100% canonical,
  581. // then grab the proper Error instance from it.
  582. var rawOutputParsedAsErr = flaverr.parseError(rawOutput);
  583. if (rawOutputParsedAsErr) {
  584. // Check the Error instance to see if it indicates any special meaning imposed by
  585. // the machine runner, whether that be an `Exception` or argin validation error.
  586. //
  587. // In any of these cases, strip that information off and preserve it instead. This allows
  588. // us to correctly treat this as an internal error.
  589. if (_.isError(rawOutputParsedAsErr) && (rawOutputParsedAsErr.name === 'Exception' || (rawOutputParsedAsErr.name === 'UsageError' && rawOutputParsedAsErr.code === 'E_INVALID_ARGINS'))) {
  590. if (rawOutputParsedAsErr.name === 'Exception' && rawOutputParsedAsErr.traceRef === privateTraceRef) { throw new Error('Consistency violation: Somehow received exit Exception for this invocation, but in the internal handler for the error exit! This should never happen! (There is probably a bug in this version of the runner-- please file a bug at https://sailsjs.com/bugs)'); }
  591. // IWMIH, it means this error is from some other, unrelated callable (wet machine) called
  592. // internally from within the implementation. So we can wrap it up in an external error
  593. // to ease negotiation in userland code (which should _never_ need to worry about tracerefs!)
  594. // > We'll also go ahead and remove our internal error's `traceRef`, now that it isn't needed anymore.
  595. delete rawOutputParsedAsErr.traceRef;
  596. // Note that these errors are NOT truly "wrapped" (i.e. with `flaverr.wrap()`)
  597. // They are designed to be useful both as-is in deeply nested
  598. // situations AND for lossless(ish) parsing by top-level runners like
  599. // `machine-as-action`. And since they need to handle both kinds of things
  600. // we have to be a bit more nuanced.
  601. err = flaverr({
  602. name: 'Error',
  603. code: 'E_INTERNAL_ERROR',
  604. message: 'Internal error occurred while running `'+getMethodName(identity)+'`. (See `.raw` for more information.)',
  605. // flaverr.getBareTrace(rawOutputParsedAsErr, 1).replace(/^\s+at\s*/, '')+'.\n'+
  606. raw: rawOutputParsedAsErr
  607. }, rtOmen);
  608. } else {
  609. err = rawOutputParsedAsErr;
  610. }
  611. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  612. // Note that we could check to see if this is an internal "E_FROM_WITHIN" error here--
  613. // i.e. which came from a nested invocations of a parley-wrapped function.
  614. // (see https://github.com/mikermcneil/parley/blob/df030c857ccddb52707be9d74dd1392c1b050d46/lib/private/Deferred.js#L216-L245)
  615. // And we could even attempt to improve the error output this way (like in whelk)--
  616. // but there are problems with that approach since doing so could mess up automatic
  617. // inspection and deduplication provided by parley's call stack management, since machines
  618. // are regularly called from inside one other. Thus we deliberately left this extra check out.
  619. // It is included here for though, commented out- just for posterity:
  620. // ```
  621. // …} else if (rawOutputParsedAsErr.name === 'Envelope' && rawOutputParsedAsErr.code === 'E_FROM_WITHIN') {
  622. // err = flaverr.unwrap('E_FROM_WITHIN', rawOutputParsedAsErr);
  623. // }…
  624. // ```
  625. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  626. } else {
  627. // Side note: In the past, string output had special handling (automatically converted into
  628. // an Error instance). But now, this causes a separate ImplementationError about how it
  629. // should be an Error instance, just like any other non-Error data would.
  630. err = flaverr({
  631. name: 'ImplementationError',
  632. message:
  633. 'Internal error occurred while running `'+getMethodName(identity)+'`. Got non-Error: '+util.inspect(rawOutput, {depth: 5})+'\n'+
  634. '\n'+
  635. 'If you are the maintainer of "'+getMethodName(identity)+'", then you can change '+
  636. 'its implementation to solve the problem (Most of the time, the solution is just to '+
  637. 'throw an actual Error instance instead. Alternatively, if the goal was to indicate '+
  638. 'a particular exception, you could throw any of the special, reserved "exit signals"-- '+
  639. 'e.g. the code name of any of your defined exits besides "error" or "success"). '+
  640. 'Otherwise, please file a bug report with the maintainer, or fork your own copy and '+
  641. 'fix that.\n'+
  642. GENERIC_HELP_SUFFIX,
  643. raw: rawOutput
  644. }, rtOmen);
  645. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  646. // FUTURE: add in additional verbiage explaining/clarifying why there should always
  647. // be an Error instance sent back -- not some other value like this.
  648. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  649. }
  650. }//fi
  651. return proceed(err);
  652. };//ƒ
  653. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  654. // FUTURE: MAYBE implement runtime refinement based on `like` and
  655. // `itemOf` (but should consider performance here, especially since
  656. // output coercion is off by default now...)
  657. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  658. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  659. // FUTURE: MAYBE Implement runtime refinement based on `getExample`
  660. // (but same as above -- might not make sense anymore...)
  661. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  662. // * * * Implementation of exits.success()... * * *
  663. handlerCbs.success = function(rawOutput){
  664. try {
  665. // If configured to do so, ensure valid result (vs. expected output type for this exit)
  666. var result;
  667. if (resultValidationTactic === 'doNotCheck' || !nmDef.exits.success.outputType) {
  668. result = rawOutput;
  669. }
  670. else if (resultValidationTactic === 'forceCoerceAndClone') {
  671. try {
  672. result = rttc.coerce(nmDef.exits.success.outputType, rawOutput);
  673. } catch (err) {
  674. throw flaverr({
  675. name: 'Error',
  676. message: 'Unexpected error when attempting to coerce the result from successfully running `'+getMethodName(identity)+'`. Internal error details: '+util.inspect(err, {depth: 5}),
  677. raw: err,
  678. outputThatCouldNotBeCoerced: rawOutput,
  679. }, rtOmen);
  680. }
  681. }
  682. else if (resultValidationTactic === 'coerceAndCloneOrError' || resultValidationTactic === 'error') {
  683. try {
  684. if (resultValidationTactic === 'coerceAndCloneOrError') {
  685. result = rttc.validate(nmDef.exits.success.outputType, rawOutput);
  686. }
  687. else {
  688. rttc.validateStrict(nmDef.exits.success.outputType, rawOutput);
  689. result = rawOutput;
  690. }
  691. } catch (err) {
  692. switch (err.code) {
  693. case 'E_INVALID':
  694. throw flaverr({
  695. name: 'ImplementationError',
  696. code: 'E_INVALID_RESULT_DATA',
  697. raw: rawOutput,
  698. message: 'Successfully ran "'+getMethodName(identity)+'"… but with an unexpected result.\n'+
  699. '------------------------------------------------------\n'+
  700. ' • '+rttc.getInvalidityMessage(nmDef.exits.success.outputType, rawOutput, err, 'result data')+'\n'+
  701. '------------------------------------------------------\n'+
  702. '\n'+
  703. 'If you are the maintainer of "'+getMethodName(identity)+'", then you can change '+
  704. 'its implementation accordingly (either send back the proper expected data in your '+
  705. 'call(s) to `exits.success(…)`, or update the "success" exit\'s `outputType` to loosen '+
  706. 'up its data type expectation. Otherwise, please file a bug report with the maintainer, '+
  707. 'or fork your own copy and fix that.\n'+
  708. GENERIC_HELP_SUFFIX
  709. }, rtOmen);
  710. default:
  711. throw err;
  712. }
  713. }
  714. }//fi
  715. } catch (err) { return proceed(err); }
  716. return proceed(undefined, result);
  717. };//ƒ
  718. // * * * Implementation of each of the other misc. exits (`exits.*()`)... * * *
  719. _.each(_.difference(_.keys(nmDef.exits), ['error','success']), function (miscExitCodeName){
  720. handlerCbs[miscExitCodeName] = function (rawOutput){
  721. // If configured to do so, ensure valid result (vs. expected output type for this exit)
  722. var result;
  723. try {
  724. if (resultValidationTactic === 'doNotCheck' || !nmDef.exits[miscExitCodeName].outputType) {
  725. result = rawOutput;
  726. }
  727. else if (resultValidationTactic === 'forceCoerceAndClone') {
  728. try {
  729. result = rttc.coerce(nmDef.exits[miscExitCodeName].outputType, rawOutput);
  730. } catch (err) {
  731. throw flaverr({
  732. name: 'Error',
  733. message: 'After running "'+getMethodName(identity)+'", which triggered its "'+miscExitCodeName+'" exception, an unexpected error was encountered while attempting to coerce the result data that was returned. Internal error details: '+util.inspect(err, {depth: 5}),
  734. raw: err,
  735. outputThatCouldNotBeCoerced: rawOutput
  736. }, rtOmen);
  737. }
  738. }
  739. else if (resultValidationTactic === 'coerceAndCloneOrError' || resultValidationTactic === 'error') {
  740. try {
  741. if (resultValidationTactic === 'coerceAndCloneOrError') {
  742. result = rttc.validate(nmDef.exits[miscExitCodeName].outputType, rawOutput);
  743. }
  744. else {
  745. rttc.validateStrict(nmDef.exits[miscExitCodeName].outputType, rawOutput);
  746. result = rawOutput;
  747. }
  748. } catch (err) {
  749. switch (err.code) {
  750. case 'E_INVALID':
  751. throw flaverr({
  752. name: 'ImplementationError',
  753. code: 'E_INVALID_RESULT_DATA',
  754. raw: rawOutput,
  755. message: 'Ran "'+getMethodName(identity)+'", which triggered its "'+miscExitCodeName+'" exception… but with invalid output.\n'+
  756. '------------------------------------------------------\n'+
  757. ' • '+rttc.getInvalidityMessage(nmDef.exits[miscExitCodeName].outputType, rawOutput, err, 'result data')+'\n'+
  758. '------------------------------------------------------\n'+
  759. '\n'+
  760. 'If you are the maintainer of "'+getMethodName(identity)+'", then you can change '+
  761. 'its implementation accordingly (either send back the proper expected data in your '+
  762. 'call(s) to `exits.'+miscExitCodeName+'(…)`, or update the "'+miscExitCodeName+'" exit\'s '+
  763. '`outputType` to loosen up its data type expectation. Otherwise, please file a bug report '+
  764. 'with the maintainer, or fork your own copy and fix that.\n'+
  765. GENERIC_HELP_SUFFIX
  766. }, rtOmen);
  767. default:
  768. throw err;
  769. }
  770. }
  771. }//fi
  772. } catch (err) { return proceed(err); }
  773. // IWMIH, the output is fine.
  774. //
  775. // So now build our Error instance for our "exception" (fka "forwarding error").
  776. var err = flaverr(
  777. (function _gettingExceptionPropsForFlaverr(){
  778. var props = {
  779. name: 'Exception',
  780. code: miscExitCodeName,
  781. exit: miscExitCodeName,//« (just an alias for `code`, for clarity and compatibility)
  782. traceRef: privateTraceRef,
  783. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  784. // ^^FUTURE: Consider setting traceRef as a non-enumerable property so that it
  785. // is excluded from logs. This would potentially slow things down, so it
  786. // would only be enabled in development, or in production with DEBUG truthy.
  787. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  788. message: (function _gettingErrMsg(){
  789. if (!_.isObject(nmDef.exits[miscExitCodeName])) { throw new Error('Consistency violation: '+getMethodName(identity)+' has become corrupted! One of its exits (`'+miscExitCodeName+'`) has gone missing since build-time!'); }
  790. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  791. // NOTE: For many, many more permutations of this, as well as notes &
  792. // thinking around it, see:
  793. // • https://github.com/node-machine/machine/commit/f93e5228138c741c2ccb160072aa1e3ad6f17ed3
  794. // • https://github.com/node-machine/machine/commit/a00a14afc17fe2080ca6f84693181063e6b6811d
  795. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  796. var errMsg = '`'+getMethodName(identity)+'` failed ("'+miscExitCodeName+'").';
  797. if (nmDef.exits[miscExitCodeName].description) {
  798. errMsg += ' '+nmDef.exits[miscExitCodeName].description+'';
  799. }
  800. if (rawOutput !== undefined) {
  801. // Determine prefix.
  802. var rawOutputParsedAsError = flaverr.parseError(rawOutput);
  803. if (rawOutputParsedAsError) {
  804. errMsg += ' (Also got an additional error -- see `.raw`).';
  805. } else if (nmDef.exits[miscExitCodeName].outputFriendlyName) {
  806. errMsg += '\n'+nmDef.exits[miscExitCodeName].outputFriendlyName+':';
  807. } else {
  808. errMsg += '\nAdditional data:';
  809. }
  810. if (!rawOutputParsedAsError) {
  811. errMsg += '\n\n'+util.inspect(rawOutput, {depth: 5});
  812. }
  813. }//fi
  814. return errMsg;
  815. })(),//†
  816. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  817. // FUTURE: In the past, we also exposed:
  818. // ```
  819. // output: rawOutput,
  820. // ```
  821. // (Nowadays, you can get this info by accessing `.raw`)
  822. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  823. };
  824. // Attach `.raw` if there is a non-undefined result to include.
  825. if (result !== undefined) {
  826. props.raw = result;
  827. }
  828. return props;
  829. })(),//†
  830. rtOmen
  831. );
  832. return proceed(err);
  833. };//ƒ
  834. });//∞ </ each misc. exit from nmDef >
  835. })(function (err, result){
  836. // Then trigger our callback with the appropriate arguments.
  837. if (err) { return done(err); }
  838. if (_.isUndefined(result)) { return done(); }
  839. return done(undefined, result);
  840. });//_∏_ (†)
  841. return handlerCbs;
  842. })();//† (implSideExitHandlerCbs:= {…:ƒ})
  843. // Run `fn`.
  844. //
  845. // > Notes:
  846. // > (1) When running our fn, we apply a special `this` context using
  847. // > the provided meta keys (aka habitat vars)
  848. // >
  849. // > (2) If the `fn` is an ES8 async function, then we also attach a handler
  850. // > to `.catch()` its return value (which will be a promise) in order to
  851. // > react to unhandled promise rejections in the same way that parley
  852. // > automatically handles any uncaught exceptions that it throws synchronously.
  853. // > (https://trello.com/c/UdK9ooJ3/108-es7-async-await-in-core-sniff-request-handler-function-to-see-if-it-s-an-async-function-if-so-then-grab-the-return-value-from-th)
  854. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  855. // FUTURE: benchmark this `constructor.name` check and, if tangible enough,
  856. // provide some mechanism for passing in this information so that it can be
  857. // predetermined (e.g. at build-time).
  858. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  859. // ªIn the event of an uncaught error being thrown from the implementation, where
  860. // the value thrown happens to represent a particular way to exit, rather than a
  861. // normal error, this `specialExitSignal` variable will be used to hold a signal
  862. // string. i.e. If a string is thrown from the implementation, then we look to
  863. // see if it matches the name of an exit. If so, then we consider this equivalent
  864. // to `exits.theExitName()`. (This is a convenience to allow for more
  865. // flexible flow control in the fn, and to allow implementors to avoid
  866. // unnecessary code just to escape iteratees from loops, recursion, etc.)
  867. //
  868. // 2 examples:
  869. //
  870. // ```
  871. // throw 'emailAlreadyInUse';
  872. // ```
  873. //
  874. // ```
  875. // throw { emailAlreadyInUse: 'irl@example.com' };
  876. // ```
  877. //
  878. // --
  879. // Note below that we also check to make sure the error isn't a special E_NON_ERROR
  880. // envelope thrown when invoking some other parley-wrapped function from
  881. // within the current machine's `fn`. If so, we check that out too.
  882. // (see https://github.com/mikermcneil/parley/commit/8f28d1d8f0960867e47fdac3e587db1f94f79fb8)
  883. var specialExitSignal;
  884. if (nmDef._fnIsAsyncFunction) {
  885. // Note that `_.bind()` works perfectly OK with ES8 async functions, at least
  886. // in platforms tested (such as Node 8.1.2). We still keep this as a separate
  887. // code path from below though, just to be 100% about what's going on.
  888. var boundES8AsyncFn = _.bind(nmDef.fn, metadata);
  889. var promise;
  890. try {
  891. if (nmDef.implementationType === 'classical') {
  892. promise = boundES8AsyncFn(argins);
  893. } else {
  894. promise = boundES8AsyncFn(argins, implSideExitHandlerCbs, metadata);
  895. }
  896. } catch (err) {
  897. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  898. // FUTURE: Determine whether we can get rid of this extra try/catch
  899. // (pretty sure we only need the 2 places, not 3).
  900. // An AsyncFunction should never just straight up throw-- it should
  901. // return a rejecting promise. (Obviously a non-AsyncFunction still
  902. // might throw-- but that's handled separately below anyhow.)
  903. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  904. // > See "ª" above for explanation of what's going on here w/ the special exit signal check.
  905. specialExitSignal = flaverr.unwrap('E_NON_ERROR', err);
  906. if (_.isString(specialExitSignal) && specialExitSignal !== 'error' && specialExitSignal !== 'success' && implSideExitHandlerCbs[specialExitSignal]) {
  907. return implSideExitHandlerCbs[specialExitSignal]();
  908. }
  909. else if (!_.isError(specialExitSignal) && !_.isArray(specialExitSignal) && !_.isFunction(specialExitSignal) && _.isObject(specialExitSignal) && _.keys(specialExitSignal).length === 1 && _.keys(specialExitSignal)[0] !== 'error' && _.keys(specialExitSignal)[0] !== 'success' && implSideExitHandlerCbs[_.keys(specialExitSignal)[0]]) {
  910. return implSideExitHandlerCbs[_.keys(specialExitSignal)[0]](specialExitSignal[_.keys(specialExitSignal)[0]]);
  911. }
  912. else {
  913. return implSideExitHandlerCbs.error(err);
  914. }
  915. }//</catch>
  916. if (nmDef.implementationType === 'classical') {
  917. // Instead of expecting exits.success() to be called, use the act of returning
  918. // as the success exit signal, and use the return value as the success output:
  919. promise = promise.then(function(result) {
  920. implSideExitHandlerCbs.success(result);
  921. });//œ
  922. }//fi
  923. // Also note that here, we don't write in the usual `return done(e)` style.
  924. // This is deliberate -- to provide a conspicuous reminder that we aren't
  925. // trying to get up to any funny business with the promise chain.
  926. promise.catch(function(err) {
  927. // > See "ª" above for explanation of what's going on here w/ the special exit signal check.
  928. specialExitSignal = flaverr.unwrap('E_NON_ERROR', err);
  929. if (_.isString(specialExitSignal) && specialExitSignal !== 'error' && specialExitSignal !== 'success' && implSideExitHandlerCbs[specialExitSignal]) {
  930. implSideExitHandlerCbs[specialExitSignal]();
  931. }
  932. else if (!_.isError(specialExitSignal) && !_.isArray(specialExitSignal) && !_.isFunction(specialExitSignal) && _.isObject(specialExitSignal) && _.keys(specialExitSignal).length === 1 && _.keys(specialExitSignal)[0] !== 'error' && _.keys(specialExitSignal)[0] !== 'success' && implSideExitHandlerCbs[_.keys(specialExitSignal)[0]]) {
  933. implSideExitHandlerCbs[_.keys(specialExitSignal)[0]](specialExitSignal[_.keys(specialExitSignal)[0]]);
  934. }
  935. else {
  936. implSideExitHandlerCbs.error(err);
  937. }
  938. });//æ
  939. } else {
  940. // When using implementationType: 'classical', things work a bit differently.
  941. // Instead of expecting exits.success() to be called, use the act of returning
  942. // as the success exit signal, and use the return value as the success output.
  943. var classicalResult;
  944. try {
  945. var boundFn = _.bind(nmDef.fn, metadata);
  946. if (nmDef.implementationType === 'classical') {
  947. classicalResult = boundFn(argins);
  948. } else {
  949. boundFn(argins, implSideExitHandlerCbs, metadata);
  950. }
  951. } catch (err) {
  952. // > See "ª" above for explanation of what's going on here w/ the special exit signal check.
  953. specialExitSignal = flaverr.unwrap('E_NON_ERROR', err);
  954. if (_.isString(specialExitSignal) && specialExitSignal !== 'error' && specialExitSignal !== 'success' && implSideExitHandlerCbs[specialExitSignal]) {
  955. return implSideExitHandlerCbs[specialExitSignal]();
  956. }
  957. else if (!_.isError(specialExitSignal) && !_.isArray(specialExitSignal) && !_.isFunction(specialExitSignal) && _.isObject(specialExitSignal) && _.keys(specialExitSignal).length === 1 && _.keys(specialExitSignal)[0] !== 'error' && _.keys(specialExitSignal)[0] !== 'success' && implSideExitHandlerCbs[_.keys(specialExitSignal)[0]]) {
  958. return implSideExitHandlerCbs[_.keys(specialExitSignal)[0]](specialExitSignal[_.keys(specialExitSignal)[0]]);
  959. }
  960. else {
  961. return implSideExitHandlerCbs.error(err);
  962. }
  963. }//</catch>
  964. if (nmDef.implementationType === 'classical') {
  965. implSideExitHandlerCbs.success(classicalResult);
  966. }
  967. }
  968. break;
  969. default:
  970. // Unrecognized code language:
  971. if (_.isString(nmDef.implementationType) && nmDef.implementationType.match(/^string\:.+$/)) {
  972. return done(flaverr({
  973. name:'UsageError',
  974. message: 'This runner doesn\'t know how to interpret and execute code strings implemented in this language (`implementationType: \''+nmDef.implementationType+'\'`). Could you transpile to another language like JavaScript first, then change the declared `implementationType` to `string:js`?\n'+GENERIC_HELP_SUFFIX
  975. }, rtOmen));
  976. }//•
  977. // Should never make it here:
  978. throw new Error('Consistency violation: Unrecognized implementation type in the middle of the machine runner -- but this should have been caught earlier!');
  979. }//</ switch(nmDef.implementationType) >
  980. },//ƒ </ handleExec function >
  981. // ███████╗██╗ ██╗██████╗ ██╗ ██╗ ██████╗██╗████████╗ ██████╗██████╗
  982. // ██╔════╝╚██╗██╔╝██╔══██╗██║ ██║██╔════╝██║╚══██╔══╝ ██╔════╝██╔══██╗
  983. // █████╗ ╚███╔╝ ██████╔╝██║ ██║██║ ██║ ██║ ██║ ██████╔╝
  984. // ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║██║ ██║ ██║ ██║ ██╔══██╗
  985. // ███████╗██╔╝ ██╗██║ ███████╗██║╚██████╗██║ ██║ ╚██████╗██████╔╝
  986. // ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝╚═════╝
  987. //
  988. // If provided, use the explicit callback.
  989. explicitCbMaybe || undefined,
  990. // ███████╗██╗ ██╗████████╗██████╗ █████╗
  991. // ██╔════╝╚██╗██╔╝╚══██╔══╝██╔══██╗██╔══██╗
  992. // █████╗ ╚███╔╝ ██║ ██████╔╝███████║
  993. // ██╔══╝ ██╔██╗ ██║ ██╔══██╗██╔══██║
  994. // ███████╗██╔╝ ██╗ ██║ ██║ ██║██║ ██║
  995. // ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝
  996. //
  997. // ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ███████╗
  998. // ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██╔════╝
  999. // ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║███████╗
  1000. // ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║╚════██║
  1001. // ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝███████║
  1002. // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
  1003. //
  1004. // Extra methods for the Deferred:
  1005. {
  1006. /**
  1007. * .retry()
  1008. *
  1009. * Attach an exponential backoff and retry strategy for this invocation.
  1010. *
  1011. * > To test on the REPL:
  1012. * > ```
  1013. * > require('./').build({identity:'asdf', timeout: 500, sideEffects: 'idempotent', inputs:{foo: {type:'number', required: true}},fn: function(inputs, exits){ var magicNo = inputs.foo+Math.random(); if (magicNo > 0.3) { setTimeout(()=>{ exits.success(magicNo); }, 1000); } else { exits.success(magicNo); } } })({foo: 0}).retry({name:'TimeoutError'}, [5, 10, 5000]).timeout(550).log()
  1014. * > ```
  1015. *
  1016. * @throws {Error} If it fails even after all retries
  1017. */
  1018. retry: function (negotiationRule, retryDelaySeries){
  1019. // Validate arguments
  1020. var isValidNegotationRule = (
  1021. (negotiationRule && _.isString(negotiationRule)) ||
  1022. _.isArray(negotiationRule) ||
  1023. (_.isObject(negotiationRule) && !_.isArray(negotiationRule) && !_.isFunction(negotiationRule))
  1024. );
  1025. if (negotiationRule !== undefined && !isValidNegotationRule) {
  1026. throw flaverr({
  1027. name:
  1028. 'UsageError',
  1029. message:
  1030. 'Invalid usage of `.retry()`. Invalid error negotiation rule: `'+util.inspect(negotiationRule,{depth:null})+'`.\n'+
  1031. ' [?] For advice or assistance, come visit https://sailsjs.com/support'
  1032. }, rtOmen);
  1033. }
  1034. if (retryDelaySeries === undefined) {
  1035. retryDelaySeries = [ 250, 500, 1000 ];
  1036. } else if (!_.isArray(retryDelaySeries) || retryDelaySeries.length < 1 || !_.all(retryDelaySeries, function(ms){ return _.isNumber(ms) && ms > 0 && Math.floor(ms) === ms; })) {
  1037. throw flaverr({
  1038. name:
  1039. 'UsageError',
  1040. message:
  1041. 'Invalid usage of `.retry()`. Provided retry delay series is invalid.\n'+
  1042. 'If specified, it should be an array of at least one positive integer (# ms).\n'+
  1043. ' [?] For advice or assistance, come visit https://sailsjs.com/support'
  1044. }, rtOmen);
  1045. }//•
  1046. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1047. // FUTURE: Consider bringing this back if we add support for `retry`
  1048. // as a implementor-land thing. (Otherwise, it's too annoying -- imagine
  1049. // a POST request that you know is idempotent, but can't otherwise
  1050. // communicate that to the runner! Could also have some kind of .idempotent()
  1051. // thing to provide a workaround, but that'd be a little over the top...)
  1052. // ```
  1053. // var def = wetMachine.getDef();
  1054. // if (def.sideEffects !== 'idempotent' && def.sideEffects !== 'cacheable') {
  1055. // throw flaverr({
  1056. // name:
  1057. // 'UsageError',
  1058. // message:
  1059. // 'Sorry, it might be unsafe to use `.retry()` with this function ("'+getMethodName(def.identity)+'"),\n'+
  1060. // 'because it does not declare itself idempotent or cacheable (e.g. `sideEffects: \'idempotent\'`).\n'+
  1061. // GENERIC_HELP_SUFFIX
  1062. // }, rtOmen);
  1063. // }
  1064. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1065. var callableWithNormalizedUsage = helpBuildMachine({
  1066. def: wetMachine.getDef(),
  1067. arginStyle: 'named',
  1068. execStyle: 'deferred'
  1069. });
  1070. // ^^Purposely omitting omen argument here (build should never fail--
  1071. // if it does, it's a bug in the runner, and we want to know where it
  1072. // came from)
  1073. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1074. // FUTURE: Maybe figure out a way to have the "tolerate" interruption
  1075. // bound as a result of this .retry() attached FIRST, before other
  1076. // tolerate/intercept interruptions.
  1077. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1078. var originalDeferred = this;
  1079. this.tolerate(negotiationRule, function $originalRetryHandler(originalErr){
  1080. var numRetriesSoFar = 1;
  1081. var errorsThatCausedRetries = [ originalErr ];
  1082. var originalInvocationInfo = originalDeferred.getInvocationInfo();
  1083. return new Promise(function(resolve, reject){
  1084. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1085. // FUTURE: consider an opt-in warning that logs before each
  1086. // retry (or better yet, a configurable notifier function)
  1087. // ```
  1088. // console.warn('Waiting '+retryDelaySeries[numRetriesSoFar - 1]+'ms before trying again... Here is the error: ', originalErr);
  1089. // ```
  1090. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1091. (function _recursively(done){
  1092. setTimeout(function (){
  1093. var π = callableWithNormalizedUsage(_.clone(argins), undefined, _.clone(metadata));
  1094. π.tolerate(negotiationRule, function(err) {
  1095. return new Promise(function(resolve, reject){
  1096. errorsThatCausedRetries.push(err);
  1097. if (numRetriesSoFar === retryDelaySeries.length) {
  1098. reject(flaverr({
  1099. name:
  1100. 'UsageError',
  1101. message:
  1102. 'Even after trying '+(numRetriesSoFar+1)+' times, this function ("'+getMethodName(nmDef.identity)+'"),\n'+
  1103. 'still did not return successfully. The original failure was as follows:\n'+
  1104. '--\n'+
  1105. originalErr.message+'\n'+
  1106. '--\n'+
  1107. '(Check out `.raw` for details about subsequent failures.)',
  1108. raw:
  1109. errorsThatCausedRetries.slice(1)
  1110. }, originalErr));
  1111. } else {
  1112. numRetriesSoFar++;
  1113. _recursively(function(err, result) {
  1114. if (err) { return reject(err); }
  1115. return resolve(result);
  1116. });
  1117. }//fi
  1118. });//•_∏_ </ new Promise() >
  1119. }, { thenable: true });
  1120. if (originalInvocationInfo.timeout) {
  1121. π.timeout(originalInvocationInfo.timeout);
  1122. }//fi
  1123. if (originalInvocationInfo.interruptions) {
  1124. // Bind tolerates/intercepts for all but the original retry
  1125. _.each(originalInvocationInfo.interruptions, function(interruption){
  1126. if (interruption.type === 'tolerate') {
  1127. if (interruption.handler !== $originalRetryHandler) {
  1128. π.tolerate(interruption.rule, interruption.handler, { thenable: interruption.thenable });
  1129. }//fi
  1130. // (ignore original retry so we don't recurse infinitely)
  1131. } else if (interruption.type === 'intercept') {
  1132. π.intercept(interruption.rule, interruption.handler, { thenable: interruption.thenable });
  1133. } else {
  1134. throw new Error('Consistency violation: Unrecognized interruption type (this should never happen)');
  1135. }//fi
  1136. });//∞
  1137. }//fi
  1138. π.exec(done);//_∏_ </ callableWithNormalizedUsage().exec() >
  1139. }, retryDelaySeries[numRetriesSoFar - 1]);// </ setTimeout() >
  1140. // ~∞%°
  1141. })(function afterwards(err, result) {
  1142. if (err) {
  1143. reject(err);
  1144. } else {
  1145. resolve(result);
  1146. }
  1147. });//_∏_
  1148. });//•_∏_ </ new Promise() >
  1149. }, { thenable: true });
  1150. return this;
  1151. },
  1152. /**
  1153. * .now()
  1154. *
  1155. * @returns {Ref} output from machine's success exit
  1156. * @throws {Error} If something goes wrong, or the machine's fn triggers a non-success exit.
  1157. */
  1158. now: function (){
  1159. // Check that the machine definition explicitly flagged itself as synchronous.
  1160. // > If `sync` was NOT set, then this is a usage error.
  1161. // > You can't run a machine synchronously unless it proudly declares itself as such.
  1162. if (!nmDef.sync) {
  1163. throw flaverr({
  1164. name:
  1165. 'UsageError',
  1166. message:
  1167. 'Sorry, this function ("'+getMethodName(nmDef.identity)+'") cannot be called synchronously,\n'+
  1168. 'because it does not declare support for synchronous usage (i.e. `sync: true`).\n'+
  1169. 'Rather than `.now()`, please use `await` or `.exec()`.\n'+
  1170. GENERIC_HELP_SUFFIX
  1171. }, rtOmen);
  1172. }//-•
  1173. // Call the underlying original `.now()` method from `parley`'s Deferred.
  1174. try {
  1175. return this.constructor.prototype.now.call(this);
  1176. } catch (err) {
  1177. // Note that we don't have to worry about a traceref here because parley
  1178. // automatically wraps E_NOT_SYNCHRONOUS errors from any internal calls
  1179. // in the implementation.
  1180. if (err.code === 'E_NOT_SYNCHRONOUS') {
  1181. throw flaverr({
  1182. name:
  1183. 'ImplementationError',
  1184. message:
  1185. 'Failed to call this function ("'+getMethodName(nmDef.identity)+'") synchronously,\n'+
  1186. 'because it is not actually synchronous. Instead, its implementation is asynchronous--\n'+
  1187. 'which is inconsistent with its declared interface (`sync: true`).\n'+
  1188. 'If you are the maintainer of "'+getMethodName(nmDef.identity)+'", then you can change its\n'+
  1189. 'implementation so that it\'s actually synchronous. Otherwise, please file a bug report\n'+
  1190. 'with the maintainer, or fork your own copy and fix that.\n'+
  1191. GENERIC_HELP_SUFFIX
  1192. }, rtOmen);
  1193. }
  1194. else {
  1195. throw err;
  1196. }
  1197. }
  1198. },
  1199. /**
  1200. * .meta()
  1201. */
  1202. meta: function (_metadata){
  1203. // Check usage.
  1204. if (!_.isObject(_metadata) || _.isArray(_metadata) || _.isFunction(_metadata)) {
  1205. throw flaverr({
  1206. name:
  1207. 'UsageError',
  1208. message:
  1209. 'When calling .meta(), please pass in a dictionary (plain JavaScript object\n'+
  1210. 'like `{}`). The keys from this dictionary will be available as properties of\n'+
  1211. '`this` from within the implementation (`fn`).\n'+
  1212. GENERIC_HELP_SUFFIX
  1213. }, rtOmen);
  1214. }//•
  1215. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1216. // FUTURE: Maybe prevent certain reserved meta keys from being used.
  1217. // (e.g. similar to what we check for in `parley` re method names)
  1218. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1219. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1220. // FUTURE: Maybe log warning when `.meta()` is called more than once?
  1221. // (since it can be confusing that the existing metadata gets replaced)
  1222. // (see also comments about defaultMeta for more info)
  1223. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1224. // Now set our private `metadata`, completely replacing anything that's already there.
  1225. // If configured to do so, extend a fresh, shallow-cloned copy of the `defaultMeta`
  1226. // with the newly provided dictionary. Otherwise just drop it in.
  1227. if (defaultMeta) {
  1228. metadata = _.extend({}, defaultMeta, _metadata);
  1229. } else {
  1230. metadata = _metadata;
  1231. }
  1232. return this;
  1233. },
  1234. /**
  1235. * .switch()
  1236. */
  1237. switch: function (userlandHandlers) {
  1238. // Check usage.
  1239. if (!_.isObject(userlandHandlers) || _.isArray(userlandHandlers) || _.isFunction(userlandHandlers)) {
  1240. throw flaverr({
  1241. name:
  1242. 'UsageError',
  1243. message:
  1244. 'Sorry, .switch() doesn\'t know how to handle usage like that.\n'+
  1245. 'You should pass in a dictionary like `{...}` consisting of at least two\n'+
  1246. 'handler functions: one for `error` and one for `success`. You can also\n'+
  1247. 'provide additional keys for any other exits you want to explicitly handle.\n'+
  1248. GENERIC_HELP_SUFFIX
  1249. }, rtOmen);
  1250. }//-•
  1251. // Before proceeding, ensure error exit is still configured w/ a callback.
  1252. // If it is not, then get crazy and **throw** BEFORE calling the machine's `fn`.
  1253. //
  1254. // This is just yet another failsafe-- better to potentially terminate the process than
  1255. // open up the possibility of silently swallowing errors later.
  1256. if (!userlandHandlers.error){
  1257. throw flaverr({
  1258. name:
  1259. 'UsageError',
  1260. message:
  1261. 'Invalid usage of .switch() -- missing `error` handler.\n'+
  1262. 'If you use .switch({...}), the provided dictionary (aka "switchback"), must\n'+
  1263. 'define an `error` key with a catchall callback function. Otherwise, there\n'+
  1264. 'would be no way to handle any unexpected or internal errors!\n'+
  1265. GENERIC_HELP_SUFFIX
  1266. }, rtOmen);
  1267. }//-•
  1268. // Same thing for the `success` handler -- except in this case, use the provided `error`
  1269. // handler, rather than throwing an uncatchable Error.
  1270. if (!userlandHandlers.success){
  1271. return userlandHandlers.error(flaverr({
  1272. name:
  1273. 'UsageError',
  1274. message:
  1275. 'Invalid usage of .switch() -- missing `success` handler.\n'+
  1276. 'If you use .switch({...}), the provided dictionary (aka "switchback"), must\n'+
  1277. 'define a `success` key with a callback function. If you do not care about\n'+
  1278. 'the success scenario, please provide a no-op callback, or use .exec() instead.\n'+
  1279. GENERIC_HELP_SUFFIX
  1280. }, rtOmen));
  1281. }//-•
  1282. // Unless configured otherwise, make sure that no extra callbacks were provided (i.e. for undeclared exits)
  1283. if (extraCallbacksTactic !== 'doNotCheck') {
  1284. var unrecognizedExitCodeNames = _.difference(_.keys(userlandHandlers), _.keys(nmDef.exits));
  1285. if (unrecognizedExitCodeNames.length > 0) {
  1286. var extraCallbacksErrorMsg =
  1287. 'Invalid usage of .switch() in call to `'+getMethodName(nmDef.identity)+'`.\n'+
  1288. unrecognizedExitCodeNames.length+' of the provided handler functions (`' + unrecognizedExitCodeNames.join(', ') +'`) '+
  1289. (unrecognizedExitCodeNames.length === 1?'does':'do')+' not match up with any recognized exit.\n'+
  1290. 'Please try again without the offending function'+(unrecognizedExitCodeNames.length === 1?'':'s')+', or check your usage and adjust accordingly.\n'+
  1291. GENERIC_HELP_SUFFIX;
  1292. if (extraCallbacksTactic === 'error') {
  1293. return userlandHandlers.error(flaverr({
  1294. name: 'UsageError',
  1295. message: extraCallbacksErrorMsg
  1296. }, rtOmen));
  1297. }//•
  1298. if (extraCallbacksTactic === 'warnAndOmit') {
  1299. // Include stack trace here, if rtOmen is truthy.
  1300. var potentialWarningSuffix = rtOmen ? '\n'+flaverr.getBareTrace(rtOmen, 3) : '';
  1301. console.warn(
  1302. '- - - - - - - - - - - - - - - - - - - - - - - -\n'+
  1303. 'WARNING: '+extraCallbacksErrorMsg+potentialWarningSuffix+'\n'+
  1304. '- - - - - - - - - - - - - - - - - - - - - - - -'
  1305. );
  1306. }//fi
  1307. }//fi
  1308. }//fi
  1309. this.exec(function (err, result){
  1310. if (err) {
  1311. if (err.name === 'Exception' && err.traceRef === privateTraceRef) {
  1312. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1313. // Note: In the past, we did this:
  1314. // ```
  1315. // if (err.name === 'Exception' && err === omen) {
  1316. // ```
  1317. //
  1318. // ...but that didn't work in prod! (Because the omen doesn't always exist.)
  1319. // So we changed it.
  1320. //
  1321. // --
  1322. //
  1323. // To explain why this `=== omen` check (or equivalent) is necessary, have a look at this example:
  1324. // ```
  1325. // var inner = require('./')({ exits: { foo: {description: 'Whoops' } }, fn: function(inputs, exits) { return exits.foo(987); } }); var outer = require('./')({ exits: { foo: { description: 'Not the same' }}, fn: function(inputs, exits) { inner({}, (err)=>{ if (err) { return exits.error(err); } return exits.success(); }); } })().switch({ error: (err)=>{ console.log('Got error:',err); }, foo: ()=>{ console.log('Should NEVER make it here. The `foo` exit of some other machine in the implementation has nothing to do with THIS `foo` exit!!'); }, success: ()=>{ console.log('Got success.'); }, });
  1326. // ```
  1327. // > Note: The same thing is true any time we might want to negotiate uncaught errors or unhandled
  1328. // > promise rejections thrown from inside `fn`, timeout errors, or RTTC validation errors.
  1329. // > It is important to remember that other machines used internally within `fn` could cause
  1330. // > similar-looking errors! We take some steps to handle this automatically elsewhere in this
  1331. // > file (see where we define the implementor-side `exits.error` callback)
  1332. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1333. // console.log('This represents an actual exit traversal');
  1334. if (!err.code) { throw new Error('Consistency violation: Recognized misc. exit exceptions from the machine runner should always have a `code` property, but this one does not! Here is the Error:'+util.inspect(err, {depth:null})); }
  1335. if (err.code === 'error' || err.code === 'success') { throw new Error('Consistency violation: Recognized misc. exit exceptions from the machine runner should NEVER have a `code` property set to `error` or `success`, but somehow, this one does! Here is the Error:'+util.inspect(err, {depth:null})); }
  1336. // Strip off the exception's `traceRef`, now that it isn't needed anymore.
  1337. delete err.traceRef;
  1338. // Sometimes userland won't provide a specific way to handle this exception:
  1339. if (!userlandHandlers[err.code]) {
  1340. return userlandHandlers.error(err);
  1341. }
  1342. // But other times, it will:
  1343. else {
  1344. return userlandHandlers[err.code](err.raw);
  1345. }
  1346. }//-•
  1347. // if (err.name === 'Exception') { console.log('DEBUG: This error must have come from some internal machine from within THIS machine\'s implementation (aka `fn`)!'); }
  1348. return userlandHandlers.error(err);
  1349. }//-•
  1350. userlandHandlers.success(result);
  1351. });//</ .exec() >
  1352. return;
  1353. },
  1354. // Compatibility:
  1355. // =====================================================================================================================
  1356. execSync: function (){
  1357. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1358. // FUTURE: Remove this deprecated method in favor of .now().
  1359. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1360. return this.now();
  1361. },
  1362. setEnv: function (_metadata) {
  1363. // Old implementation, for reference:
  1364. // https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/Machine.constructor.js#L365-L383
  1365. console.warn(
  1366. 'DEPRECATED AS OF MACHINE v15: Please use `.meta()` instead of `.setEnv()` in the future\n'+
  1367. '(adjusting it for you automatically this time)\n'
  1368. );
  1369. return this.meta(_metadata);
  1370. },
  1371. // FUTURE: Consider completely removing the rest of these compatibility-error-throwing
  1372. // niceties in production (to optimize performance of machine construction)
  1373. // =====================================================================================================================
  1374. demuxSync: function () {
  1375. throw flaverr({
  1376. name:
  1377. 'CompatibilityError',
  1378. message:
  1379. 'As of machine v15, the experimental `.demuxSync()` method is no longer supported.\n'+
  1380. 'Instead, please use something like this:\n'+
  1381. '\n'+
  1382. '```\n'+
  1383. ' var origResultFromDemuxSync = (()=>{\n'+
  1384. ' try {\n'+
  1385. ' thisMethod().now();\n'+
  1386. ' return true;\n'+
  1387. ' } catch (e) { return false; }\n'+
  1388. ' })();\n'+
  1389. '```\n'+
  1390. '\n'+
  1391. 'Here is a link to the original implementation, for reference:\n'+
  1392. 'https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/Machine.prototype.demuxSync.js'
  1393. }, rtOmen);
  1394. },
  1395. cache: function () {
  1396. throw flaverr({
  1397. name:
  1398. 'CompatibilityError',
  1399. message:
  1400. 'As of machine v15, built-in caching functionality (and thus the `.cache()` method) is no longer supported.\n'+
  1401. 'Instead, please use your own caching mechanism-- for example:\n'+
  1402. '\n'+
  1403. '```\n'+
  1404. ' global.MY_CACHE = global.MY_CACHE || {};\n'+
  1405. ' var result = MY_CACHE[\'foo\'];\n'+
  1406. ' if (result === undefined) {\n'+
  1407. ' result = await thisMethod({id:\'foo\'});\n'+
  1408. ' MY_CACHE[\'foo\'] = result;\n'+
  1409. ' }\n'+
  1410. '```\n'+
  1411. '\n'+
  1412. 'Here is a link to part of the original implementation, for reference:\n'+
  1413. 'https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/Machine.prototype.cache.js'
  1414. }, rtOmen);
  1415. },
  1416. },
  1417. // ████████╗██╗███╗ ███╗███████╗ ██████╗ ██╗ ██╗████████╗
  1418. // ╚══██╔══╝██║████╗ ████║██╔════╝██╔═══██╗██║ ██║╚══██╔══╝
  1419. // ██║ ██║██╔████╔██║█████╗ ██║ ██║██║ ██║ ██║
  1420. // ██║ ██║██║╚██╔╝██║██╔══╝ ██║ ██║██║ ██║ ██║
  1421. // ██║ ██║██║ ╚═╝ ██║███████╗╚██████╔╝╚██████╔╝ ██║
  1422. // ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
  1423. //
  1424. // If provided, use the timeout (max # of ms to wait for this machine to finish executing)
  1425. nmDef.timeout || undefined,
  1426. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1427. // FUTURE: Write tests that ensure all of the scenarios in c4e738e8016771bd55b78cba277e62a83d5c61fc are working.
  1428. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1429. // ██████╗ ███╗ ███╗███████╗███╗ ██╗
  1430. // ██╔═══██╗████╗ ████║██╔════╝████╗ ██║
  1431. // ██║ ██║██╔████╔██║█████╗ ██╔██╗ ██║
  1432. // ██║ ██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║
  1433. // ╚██████╔╝██║ ╚═╝ ██║███████╗██║ ╚████║
  1434. // ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝
  1435. //
  1436. // Pass in the omen, if we have one.
  1437. rtOmen || undefined,
  1438. // To allow higher-level tools to extend this runner (e.g. for improved error messages),
  1439. // we pass through a final interceptor function here, if one was provided, for changing
  1440. // the error/result on the way back out from .exec().
  1441. finalAfterExec || undefined
  1442. );//parley(…)
  1443. // If an explicit callback was supplied, then there won't be a deferred object.
  1444. // (so we should bail now)
  1445. if (!deferredObj) { return; }
  1446. // Finally, look at the "execStyle" and determine what to do next.
  1447. switch (execStyle) {
  1448. case 'natural':
  1449. if (nmDef.sync) {
  1450. return deferredObj.now();
  1451. }
  1452. else {
  1453. return deferredObj;
  1454. }
  1455. case 'deferred':
  1456. return deferredObj;
  1457. case 'immediate':
  1458. if (nmDef.sync) {
  1459. return deferredObj.now();
  1460. }
  1461. else {
  1462. return deferredObj.toPromise();
  1463. }
  1464. default:
  1465. throw flaverr({name:'UsageError'}, new Error('Unrecognized execStyle: "'+execStyle+'"'));
  1466. }
  1467. };//ƒ ( constructing callable )
  1468. // Now attach a few additional properties to the callable ("wet") machine function.
  1469. // (This is primarily for compatibility with existing tooling, and for easy access to the def.
  1470. // But it also enhances the UX of working with machinepacks by adding .inspect() etc. And it
  1471. // allows us to attach the .customize() function for providing custom usage options.)
  1472. // > Note that these properties should NEVER be changed after this point!
  1473. /**
  1474. * .getDef()
  1475. *
  1476. * Get the original, "dry" machine definition that was used to construct this "wet" machine.
  1477. * (Be careful: The returned definition should never be modified!)
  1478. *
  1479. * @returns {Ref} [the "dry" node machine definition]
  1480. */
  1481. Object.defineProperty(wetMachine, 'getDef', {
  1482. enumerable: false,
  1483. configurable: false,
  1484. writable: false,
  1485. value: function getDef(){
  1486. return nmDef;
  1487. }//ƒ
  1488. });
  1489. /**
  1490. * .toJSON()
  1491. *
  1492. * Note that, if this "dry" representation is ACTUALLY JSON-stringified afterwards,
  1493. * the stringification process will be lossy. Functions like `fn` or `custom` validators
  1494. * are not actually JSON serializable. (To overcome this, use an additional layer of
  1495. * serialization and deserialization such as rttc.dehydrate() and rttc.hydrate().)
  1496. *
  1497. * (Automatically invoked before JSON stringification when this is passed
  1498. * into `JSON.stringify()`)
  1499. */
  1500. Object.defineProperty(wetMachine, 'toJSON', {
  1501. enumerable: false,
  1502. configurable: false,
  1503. writable: false,
  1504. value: function toJSON(){
  1505. return wetMachine.getDef();
  1506. }//ƒ
  1507. });
  1508. /**
  1509. * .toString()
  1510. *
  1511. * (Automatically invoked before casting, string concatenation, etc.)
  1512. *
  1513. * This can be overridden.
  1514. */
  1515. Object.defineProperty(wetMachine, 'toString', {
  1516. enumerable: false,
  1517. configurable: false,
  1518. writable: true,
  1519. value: function toString(){
  1520. return '[Machine: '+nmDef.identity+']';
  1521. }//ƒ
  1522. });
  1523. /**
  1524. * .inspect()
  1525. *
  1526. * (Automatically invoked in Node.js environments when this is passed into `util.inspect()` or `console.log()`)
  1527. *
  1528. * This can be overridden.
  1529. */
  1530. Object.defineProperty(wetMachine, 'inspect', {
  1531. enumerable: false,
  1532. configurable: false,
  1533. writable: true,
  1534. value: function inspect(){
  1535. var hasInputs = (Object.keys(nmDef.inputs).length > 0);
  1536. var lengthOfLongestInputCodeName = (function(){
  1537. var longest = 0;
  1538. _.each(Object.keys(nmDef.inputs), function(inputCodeName){
  1539. if (inputCodeName.length > longest) {
  1540. longest = inputCodeName.length;
  1541. }
  1542. });
  1543. return longest;
  1544. })();
  1545. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1546. // FUTURE: Potentially show include like `result = ` in example usage, if the
  1547. // success exit has output.
  1548. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1549. return ''+
  1550. '-----------------------------------------\n'+
  1551. // OVERVIEW
  1552. // ===============
  1553. ' .'+getMethodName(nmDef.identity)+'()\n'+
  1554. // - - - - - - - - - - - - - - -
  1555. // The old way, for posterity:
  1556. // ' ['+nmDef.identity+']\n'+
  1557. // - - - - - - - - - - - - - - -
  1558. (
  1559. nmDef.description ? (' \n'+nmDef.description+'\n') : ''
  1560. )+
  1561. // USAGE
  1562. // ===============
  1563. (
  1564. execStyle === 'immediate' || execStyle === 'natural'?
  1565. (
  1566. '\n'+
  1567. ' Usage:\n'+
  1568. (
  1569. nmDef.sync?
  1570. ' '+getMethodName(nmDef.identity)+'('+(hasInputs?(arginStyle==='serial'?'…':'{…}'):'')+');\n'
  1571. :
  1572. ' await '+getMethodName(nmDef.identity)+'('+(hasInputs?(arginStyle==='serial'?'…':'{…}'):'')+');\n'
  1573. )+
  1574. '\n'
  1575. )
  1576. :
  1577. (
  1578. '\n'+
  1579. ' Usage:\n'+
  1580. (
  1581. nmDef.sync?
  1582. ' '+getMethodName(nmDef.identity)+'('+(hasInputs?(arginStyle==='serial'?'…':'{…}'):'')+').now();\n'
  1583. :
  1584. (
  1585. ' await '+getMethodName(nmDef.identity)+'('+(hasInputs?(arginStyle==='serial'?'…':'{…}'):'')+');\n'
  1586. )
  1587. )+
  1588. '\n'
  1589. )
  1590. )+
  1591. // INPUTS
  1592. // ===============
  1593. (
  1594. arginStyle==='serial'?
  1595. ' Arguments:'
  1596. :
  1597. ' Inputs:'
  1598. )+
  1599. (function _formattingInputDocs(){
  1600. var hasRequiredUnconfiguredSerialArgs;
  1601. var lastRequiredUnconfiguredArgIdx;
  1602. if (arginStyle === 'serial') {
  1603. var requiredInputCodeNames = _.reduce(nmDef.inputs, function(memo, inputDef, inputCodeName) {
  1604. if (inputDef.required) {
  1605. memo.push(inputCodeName);
  1606. }
  1607. return memo;
  1608. }, []);
  1609. _.each(nmDef.args||_.keys(nmDef.inputs), function(arg, i){
  1610. var isGloballyConfigured = defaultArgins? defaultArgins[arg] !== undefined : false;
  1611. var isRequired = _.contains(requiredInputCodeNames, arg);
  1612. if (isRequired && !isGloballyConfigured) {
  1613. hasRequiredUnconfiguredSerialArgs = true;
  1614. lastRequiredUnconfiguredArgIdx = i;
  1615. }
  1616. });//∞
  1617. }//fi
  1618. return _.reduce((
  1619. arginStyle==='serial' && nmDef.args?
  1620. nmDef.args
  1621. :
  1622. _.keys(nmDef.inputs)
  1623. ), function (memo, arg, i){
  1624. var MIN_PADDING = 2;
  1625. var pad = _.repeat(' ', Math.max(
  1626. MIN_PADDING,
  1627. (lengthOfLongestInputCodeName-arg.length)+MIN_PADDING
  1628. ));
  1629. var inputDef = nmDef.inputs[arg];
  1630. var prettySerialArgIdx = (i<9?' ':'')+(i+1)+'.';
  1631. var prefix = (
  1632. // Already globally configured:
  1633. defaultArgins && defaultArgins[arg] !== undefined?(
  1634. arginStyle==='serial'?
  1635. '✔ '+prettySerialArgIdx+' '
  1636. :
  1637. ' ✔ '
  1638. ):
  1639. // Required:
  1640. inputDef && inputDef.required?(
  1641. arginStyle==='serial'?
  1642. '* '+prettySerialArgIdx+' '
  1643. :
  1644. ' -*-'
  1645. ):
  1646. // Optional:
  1647. arginStyle==='serial'?(
  1648. hasRequiredUnconfiguredSerialArgs && i < lastRequiredUnconfiguredArgIdx?
  1649. '* '+prettySerialArgIdx+' '
  1650. // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1651. // FUTURE: Consider maybe disambiguating this case
  1652. // (although it does carry the risk of making it seem like
  1653. // the usage is variadic)
  1654. // ```
  1655. // '° '+prettySerialArgIdx+' '
  1656. // ```
  1657. // - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1658. :
  1659. ' '+prettySerialArgIdx+' '
  1660. )
  1661. :
  1662. ' - '
  1663. );//:= </prefix>
  1664. // Handle special "{*}" syntax:
  1665. if (arg === '{*}') {
  1666. memo += '\n '+prefix+' {…}'+pad+'(more opts)';
  1667. var miscInputCodeNames = _.difference(_.keys(nmDef.inputs), nmDef.args);
  1668. _.each(miscInputCodeNames, function(inputCodeName, i){
  1669. var isTheLastOne = (i === miscInputCodeNames.length-1);
  1670. memo += '\n '+(isTheLastOne?'└':'├')+'─ '+inputCodeName;
  1671. });
  1672. return memo;
  1673. }//•
  1674. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1675. // FUTURE: Support other `args` syntax
  1676. // (see other related "FUTURE" notes in `help-build-machine` for more info)
  1677. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1678. // Otherwise handle this like any other input:
  1679. if (arginStyle==='serial') {
  1680. memo += '\n '+prefix+' '+arg;
  1681. } else {
  1682. memo += '\n '+prefix+' '+arg;
  1683. }
  1684. if (inputDef.type) {
  1685. if (_.isString(inputDef.type)) {
  1686. memo += pad+'(type: '+inputDef.type+')';
  1687. }
  1688. else if (_.isEqual(['string'], inputDef.type)){
  1689. memo += pad+'(array of strings; e.g. `[\'a\',\'b\',\'c\']`)';
  1690. }
  1691. else if (_.isEqual(['number'], inputDef.type)){
  1692. memo += pad+'(array of numbers; e.g. `[1,2,3]`)';
  1693. }
  1694. else if (_.isArray(inputDef.type) && _.isObject(inputDef.type[0]) && !_.isArray(inputDef.type[0])){
  1695. memo += pad+'(array of dictionaries; e.g. `[{…}, {…}, {…}]`)';
  1696. }
  1697. else if (_.isArray(inputDef.type)){
  1698. memo += pad+'(an array; e.g. `[…]`)';
  1699. }
  1700. else if (_.isObject(inputDef.type)) {
  1701. memo += pad+'(a dictionary; e.g. `{…}`)';
  1702. }
  1703. else {
  1704. // This should never happen!
  1705. }
  1706. }
  1707. else if (inputDef.example !== undefined) {
  1708. var displayTypeInferredFromExample = rttc.inferDisplayType(inputDef.example);
  1709. // (^^FUTURE: can probably just use actual `type` here now that the nmDef gets normalized)
  1710. if (displayTypeInferredFromExample === 'string' || displayTypeInferredFromExample === 'number') {
  1711. memo += pad+'(e.g. '+util.inspect(inputDef.example, {depth: 5})+')';
  1712. }
  1713. else {
  1714. memo += pad+'(type: '+displayTypeInferredFromExample+')';
  1715. }
  1716. }
  1717. return memo;
  1718. }, '')||
  1719. ' (n/a)';
  1720. })()+'\n'+//†
  1721. '-----------------------------------------\n';
  1722. }//ƒ
  1723. });//…) ( defining property on callable )
  1724. // Set up a home for cached customizations.
  1725. // (See `customize()` below for more information.)
  1726. var cachedCustomizations = {};
  1727. /**
  1728. * .customize()
  1729. *
  1730. * Re-build a customized version of this machine on the fly, using the specified
  1731. * custom usage options.
  1732. * > If this exact customization has been used before for this machine,
  1733. * > the customized machine will be _cloned and cached_. This works much
  1734. * > like Node's core require() cache, and is designed to improve performance
  1735. * > by avoiding unnecessarily duplicating work on a per-call basis.
  1736. *
  1737. * @param {Dictionary} customUsageOpts
  1738. * @property {String?} arginStyle ("named" or "serial")
  1739. * @property {String?} execStyle ("deferred" or "immediate")
  1740. * … (For full reference of opts, see `buildWithCustomUsage()`)
  1741. *
  1742. * @returns {Ref} [a custom, spin-off duplicate of this set machine w/ custom usage]
  1743. */
  1744. Object.defineProperty(wetMachine, 'customize', {
  1745. enumerable: false,
  1746. configurable: false,
  1747. writable: true,
  1748. value: function customize(customUsageOpts){
  1749. if (!customUsageOpts || _.isEqual(customUsageOpts, {})) { throw new Error('Consistency violation: Cannot call `.customize()` without providing any custom usage options! Please specify at least one option such as "arginStyle" or "execStyle".'); }
  1750. if (!_.isObject(customUsageOpts) || _.isArray(customUsageOpts) || _.isFunction(customUsageOpts)) { throw new Error('Consistency violation: `.customize()` must be called with a dictionary of custom usage options.'); }
  1751. if (customUsageOpts.def !== undefined) { throw new Error('Consistency violation: Cannot specify `def` when calling `.customize()`! Instead, specify custom usage options like "arginStyle" or "execStyle".'); }
  1752. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1753. // FUTURE: Maybe support shallow-merging on top of existing custom usage
  1754. // opts as overrides, rather than requiring a complete replacement
  1755. // e.g.
  1756. // ```
  1757. // customUsageOpts = _.extend({
  1758. // arginStyle: arginstyle,
  1759. // execStyle: execStyle,
  1760. // extraArginsTactic: extraArginsTactic,
  1761. // //…
  1762. // //etc…
  1763. // //…
  1764. // }, customUsageOpts);
  1765. // ```
  1766. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1767. var hashed;
  1768. try {
  1769. hashed = hashCustomUsageOpts(customUsageOpts);
  1770. } catch (err) {
  1771. if (!flaverr.taste('E_UNHASHABLE', err)) {
  1772. throw err;
  1773. }
  1774. }//>-
  1775. // Use cached customization, if possible.
  1776. if (hashed && cachedCustomizations[hashed]) {
  1777. return cachedCustomizations[hashed];
  1778. }//-•
  1779. var customizedWetMachine = helpBuildMachine(_.extend({
  1780. def: wetMachine.getDef(),
  1781. }, customUsageOpts));
  1782. // Cache this customization in case `.customize()` gets called again.
  1783. if (hashed) {
  1784. cachedCustomizations[hashed] = customizedWetMachine;
  1785. }
  1786. return customizedWetMachine;
  1787. }//ƒ
  1788. });//…) ( defining property on callable )
  1789. /**
  1790. * .with()
  1791. *
  1792. * Pre-invoke this callable with the specified dictionary of named argins,
  1793. * regardless of the current `arginStyle` setting.
  1794. *
  1795. * > This is an "escape hatch" from `arginStyle: 'serial'`; useful for
  1796. * > situations where it's cleaner/easier/more obvious for userland code
  1797. * > to use named input parameters instead of serial arguments.
  1798. * > If this Callable's arginStyle is ALREADY 'serial', then the `with`
  1799. * > property is just a circular reference to the Callable itself, rather
  1800. * > than a separate customization.
  1801. *
  1802. * @type {Callable}
  1803. */
  1804. Object.defineProperty(wetMachine, 'with', {
  1805. enumerable: false,
  1806. configurable: false,
  1807. writable: false,
  1808. value: arginStyle === 'named' ? wetMachine : wetMachine.customize({
  1809. arginStyle: 'named',
  1810. execStyle: execStyle,
  1811. extraArginsTactic: extraArginsTactic,
  1812. extraCallbacksTactic: extraCallbacksTactic,
  1813. arginValidationTactic: arginValidationTactic,
  1814. resultValidationTactic: resultValidationTactic,
  1815. finalAfterExec: finalAfterExec,
  1816. defaultMeta: defaultMeta,
  1817. defaultArgins: defaultArgins,
  1818. // Note there is no reason to pass through `implementationSniffingTactic`
  1819. // here, since it would have already applied now (it applies at build-time.)
  1820. })
  1821. });
  1822. return wetMachine;
  1823. };