├── .npmignore ├── .editorconfig ├── .travis.yml ├── test ├── .eslintrc ├── utils │ ├── bench-sync.util.js │ └── bench.util.js ├── fixtures │ ├── private │ │ └── help-find.util.js │ ├── find-but-with-timeout.fixture.js │ ├── validate-but-with-9-custom-methods.fixture.js │ ├── find.fixture.js │ ├── find-but-with-final-after-exec-lc.fixture.js │ └── validate.fixture.js ├── sanity.test.js ├── behavior.test.js ├── baseline.benchmark.js └── practical.test.js ├── .gitignore ├── package.json ├── appveyor.yml ├── .eslintrc ├── .jshintrc ├── lib ├── parley.js └── private │ └── Deferred.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | ./.gitignore 3 | ./.jshintrc 4 | ./.editorconfig 5 | ./.travis.yml 6 | ./appveyor.yml 7 | ./example 8 | ./examples 9 | ./test 10 | ./tests 11 | ./.github 12 | 13 | node_modules 14 | npm-debug.log 15 | .node_history 16 | *.swo 17 | *.swp 18 | *.swn 19 | *.swm 20 | *.seed 21 | *.log 22 | *.out 23 | *.pid 24 | lib-cov 25 | .DS_STORE 26 | *# 27 | *\# 28 | .\#* 29 | *~ 30 | .idea 31 | .netbeans 32 | nbproject 33 | .tmp 34 | dump.rdb 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # ╔═╗╔╦╗╦╔╦╗╔═╗╦═╗┌─┐┌─┐┌┐┌┌─┐┬┌─┐ 2 | # ║╣ ║║║ ║ ║ ║╠╦╝│ │ ││││├┤ ││ ┬ 3 | # o╚═╝═╩╝╩ ╩ ╚═╝╩╚═└─┘└─┘┘└┘└ ┴└─┘ 4 | # 5 | # This file (`.editorconfig`) exists to help maintain consistent formatting 6 | # throughout this package, the Sails framework, and the Node-Machine project. 7 | # 8 | # To review what each of these options mean, see: 9 | # http://editorconfig.org/ 10 | root = true 11 | 12 | [*] 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 2 | # ╔╦╗╦═╗╔═╗╦ ╦╦╔═╗ ┬ ┬┌┬┐┬ # 3 | # ║ ╠╦╝╠═╣╚╗╔╝║╚═╗ └┬┘││││ # 4 | # o ╩ ╩╚═╩ ╩ ╚╝ ╩╚═╝o ┴ ┴ ┴┴─┘ # 5 | # # 6 | # This file configures Travis CI. # 7 | # (i.e. how we run the tests... mainly) # 8 | # # 9 | # https://docs.travis-ci.com/user/customizing-the-build # 10 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 11 | 12 | language: node_js 13 | 14 | node_js: 15 | - "6" 16 | - "8" 17 | - "10" 18 | - "node" 19 | 20 | branches: 21 | only: 22 | - master 23 | 24 | notifications: 25 | email: 26 | - ci@sailsjs.com 27 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ ┌─┐┬ ┬┌─┐┬─┐┬─┐┬┌┬┐┌─┐ 3 | // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ │ │└┐┌┘├┤ ├┬┘├┬┘│ ││├┤ 4 | // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ └─┘ └┘ └─┘┴└─┴└─┴─┴┘└─┘ 5 | // ┌─ ┌─┐┌─┐┬─┐ ┌─┐┬ ┬┌┬┐┌─┐┌┬┐┌─┐┌┬┐┌─┐┌┬┐ ┌┬┐┌─┐┌─┐┌┬┐┌─┐ ─┐ 6 | // │ ├┤ │ │├┬┘ ├─┤│ │ │ │ ││││├─┤ │ ├┤ ││ │ ├┤ └─┐ │ └─┐ │ 7 | // └─ └ └─┘┴└─ ┴ ┴└─┘ ┴ └─┘┴ ┴┴ ┴ ┴ └─┘─┴┘ ┴ └─┘└─┘ ┴ └─┘ ─┘ 8 | // > An .eslintrc configuration override for use with the tests in this directory. 9 | // 10 | // (See .eslintrc in the root directory of this package for more info.) 11 | 12 | "extends": [ 13 | "../.eslintrc" 14 | ], 15 | 16 | "env": { 17 | "mocha": true 18 | }, 19 | 20 | "globals": { 21 | // "assert": true, 22 | // "util": true 23 | // …plus any other convenience globals exposed in test suites 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/utils/bench-sync.util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var Benchmark = require('benchmark'); 7 | 8 | 9 | 10 | /** 11 | * benchSync() 12 | * 13 | * (see https://benchmarkjs.com/docs#Benchmark) 14 | * --------------------------- 15 | * @param {String} name 16 | * @param {Array} testFns [array of functions] 17 | */ 18 | 19 | module.exports = function benchSync (name, testFns) { 20 | 21 | var suite = new Benchmark.Suite({ name: name }); 22 | 23 | _.each(testFns, function (testFn, i) { 24 | suite = suite.add((testFn.name||name)+'#'+i, testFn); 25 | });// 26 | 27 | suite.on('cycle', function(event) { 28 | console.log(' •',String(event.target)); 29 | }) 30 | .on('complete', function() { 31 | // console.log('Fastest is ' + this.filter('fastest').map('name')); 32 | // console.log('Slowest is ' + this.filter('slowest').map('name')); 33 | }) 34 | .run(); 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ┌─┐┬┌┬┐╦╔═╗╔╗╔╔═╗╦═╗╔═╗ 2 | # │ ┬│ │ ║║ ╦║║║║ ║╠╦╝║╣ 3 | # o└─┘┴ ┴ ╩╚═╝╝╚╝╚═╝╩╚═╚═╝ 4 | # 5 | # This file (`.gitignore`) exists to signify to `git` that certain files 6 | # and/or directories should be ignored for the purposes of version control. 7 | # 8 | # This is primarily useful for excluding temporary files of all sorts; stuff 9 | # generated by IDEs, build scripts, automated tests, package managers, or even 10 | # end-users (e.g. file uploads). `.gitignore` files like this also do a nice job 11 | # at keeping sensitive credentials and personal data out of version control systems. 12 | # 13 | 14 | ############################ 15 | # sails / node.js / npm 16 | ############################ 17 | node_modules 18 | .tmp 19 | npm-debug.log 20 | package-lock.json 21 | package-lock.* 22 | .waterline 23 | .node_history 24 | 25 | ############################ 26 | # editor & OS files 27 | ############################ 28 | *.swo 29 | *.swp 30 | *.swn 31 | *.swm 32 | *.seed 33 | *.log 34 | *.out 35 | *.pid 36 | lib-cov 37 | .DS_STORE 38 | *# 39 | *\# 40 | .\#* 41 | *~ 42 | .idea 43 | .netbeans 44 | nbproject 45 | 46 | ############################ 47 | # misc 48 | ############################ 49 | dump.rdb 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parley", 3 | "version": "4.0.0", 4 | "description": "Practical, lightweight flow control for Node.js. Supports `await`, callbacks and promises.", 5 | "main": "lib/parley.js", 6 | "scripts": { 7 | "custom-tests": "mocha test/*.test.js", 8 | "lint": "eslint . --max-warnings=0 --ignore-pattern 'test/' && echo '✔ Your code looks good.'", 9 | "bench": "NODE_ENV=production mocha test/*.benchmark.js", 10 | "bench-win": "mocha test/*.benchmark.js", 11 | "test": "npm run lint && npm run custom-tests && npm run bench" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/mikermcneil/parley.git" 16 | }, 17 | "keywords": [ 18 | "flowcontrol", 19 | "await", 20 | "async/await", 21 | "async", 22 | "promise", 23 | "callback", 24 | "deferred" 25 | ], 26 | "author": "Mike McNeil", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@sailshq/lodash": "^3.10.2", 30 | "bluebird": "3.2.1", 31 | "flaverr": "^1.5.1" 32 | }, 33 | "devDependencies": { 34 | "benchmark": "2.1.2", 35 | "eslint": "4.19.1", 36 | "mocha": "6.1.3" 37 | }, 38 | "engines": { 39 | "node": ">=8" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/private/help-find.util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var flaverr = require('flaverr'); 7 | 8 | 9 | // This is used by find.fixture.js. 10 | // It was originally and part of a benchmarking experiment, and its extrapolation 11 | // was found to have a positive impact on performance. 12 | module.exports = function helpFind(unused, metadata, finalCb) { 13 | if (unused) { 14 | finalCb(new Error('Consistency violation: Unexpected internal error occurred before beginning with any business logic. Details: '+unused.stack)); 15 | return; 16 | }//-• 17 | 18 | // Now actually do stuff. 19 | 20 | 21 | // In this case, we'll just pretend, since this part doesn't matter. 22 | // (we just wait a few miliseconds, and then send back an array consisting 23 | // of one item: the `criteria` that was received.) 24 | setTimeout(function (){ 25 | var fakeResult = [ metadata.criteria ]; 26 | 27 | // Note that, as a way for our test cases to instrument the outcome, 28 | // we check `metadata.criteria` here, and if it happens to be `false` 29 | // or `null`, then we trigger an error instead. 30 | if (metadata.criteria === false) { 31 | return finalCb(flaverr('E_SOME_ERROR', new Error('Simulated failure (E_SOME_ERROR)'))); 32 | } 33 | if (_.isNull(metadata.criteria)) { 34 | return finalCb(new Error('Simulated failure (catchall / misc. error)')); 35 | } 36 | 37 | return finalCb(undefined, fakeResult); 38 | 39 | }, 25); 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # # # # # # # # # # # # # # # # # # # # # # # # # # 2 | # ╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╦╔═╗╦═╗ ┬ ┬┌┬┐┬ # 3 | # ╠═╣╠═╝╠═╝╚╗╔╝║╣ ╚╦╝║ ║╠╦╝ └┬┘││││ # 4 | # ╩ ╩╩ ╩ ╚╝ ╚═╝ ╩ ╚═╝╩╚═o ┴ ┴ ┴┴─┘ # 5 | # # 6 | # This file configures Appveyor CI. # 7 | # (i.e. how we run the tests on Windows) # 8 | # # 9 | # https://www.appveyor.com/docs/lang/nodejs-iojs/ # 10 | # # # # # # # # # # # # # # # # # # # # # # # # # # 11 | 12 | 13 | # Test against these versions of Node.js. 14 | environment: 15 | matrix: 16 | - nodejs_version: "6" 17 | - nodejs_version: "8" 18 | - nodejs_version: "10" 19 | 20 | # Install scripts. (runs after repo cloning) 21 | install: 22 | # Get the latest stable version of Node.js 23 | # (Not sure what this is for, it's just in Appveyor's example.) 24 | - ps: Install-Product node $env:nodejs_version 25 | # Install declared dependencies 26 | - npm install 27 | 28 | 29 | before_test: 30 | - cmd: set NODE_ENV=production 31 | 32 | # Post-install test scripts. 33 | test_script: 34 | # Output Node and NPM version info. 35 | # (Presumably just in case Appveyor decides to try any funny business? 36 | # But seriously, always good to audit this kind of stuff for debugging.) 37 | - node --version 38 | - npm --version 39 | # Run the actual tests. 40 | - npm run custom-tests && npm run bench-win 41 | 42 | 43 | # Don't actually build. 44 | # (Not sure what this is for, it's just in Appveyor's example. 45 | # I'm not sure what we're not building... but I'm OK with not 46 | # building it. I guess.) 47 | build: off 48 | -------------------------------------------------------------------------------- /test/fixtures/find-but-with-timeout.fixture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var flaverr = require('flaverr'); 7 | var parley = require('../../'); 8 | var helpFind = require('./private/help-find.util'); 9 | 10 | 11 | /** 12 | * find-but-with-timeout.fixture.js 13 | * 14 | * A simplified mock of Waterline's `find()` model method -- but with an extremely short timeout. 15 | * 16 | * > See `find.fixture.js` for more info. Many comments were removed from the code below 17 | * > to avoid unnecessary duplication and reduce the chances of things getting weird. 18 | * 19 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 20 | * @param {Dictionary?} criteria 21 | * @param {Function} explicitCb 22 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 23 | * @returns {Deferred} If no callback specified 24 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 25 | */ 26 | module.exports = function findButWithTimeout( /* variadic */ ){ 27 | 28 | var metadata = {}; 29 | 30 | var explicitCb; 31 | 32 | // Handle variadic usage: 33 | // =========================================================================== 34 | if (!_.isUndefined(arguments[0])) { 35 | if (_.isFunction(arguments[0])) { 36 | explicitCb = arguments[0]; 37 | } 38 | else { 39 | metadata.criteria = arguments[0]; 40 | } 41 | }//>- 42 | 43 | if (!_.isUndefined(arguments[1])) { 44 | explicitCb = arguments[1]; 45 | }//>- 46 | // =========================================================================== 47 | 48 | return parley(function (done){ 49 | helpFind(undefined, metadata, done); 50 | }, explicitCb, { 51 | where: function(clause) { 52 | metadata.criteria = metadata.criteria || {}; 53 | metadata.criteria.where = clause; 54 | return this; 55 | } 56 | }, 2); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /test/fixtures/validate-but-with-9-custom-methods.fixture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var parley = require('../../'); 6 | 7 | 8 | /** 9 | * validate-but-with-9-custom-methods.fixture.js 10 | * 11 | * A simplified mock of a hypothetical `validate()` model method, 12 | * just like the other fixture (see `validate.fixture.js`) but with 9 13 | * distinct custom methods available on the Deferred instance. 14 | * (This is primarily for use in benchmarks.) 15 | * 16 | * @param {Function} explicitCbMaybe 17 | * 18 | * @returns {Deferred} If no callback specified 19 | */ 20 | module.exports = function validateButWith9CustomMethods(explicitCbMaybe){ 21 | 22 | // This deferred may or may not actually need to get built. 23 | // 24 | // If an explicit callback was specified, then go ahead 25 | // and proceed to where the real action is at & return `undefined`. 26 | // Otherwise, no callback was specified explicitly, 27 | // so we'll build and return a Deferred instance instead. 28 | 29 | var deferred = parley(function (finalCb){ 30 | 31 | // Now actually do stuff. 32 | // ...except actually don't-- this is just pretend. 33 | 34 | // All done. 35 | return finalCb(); 36 | 37 | }, explicitCbMaybe, { 38 | a: function (beep, boop) { this._mathIsFun = (Math.random()+'hi0'); return this; }, 39 | b: function (baa, baaa, black, sheep) { this._mathIsFun = (Math.random()+'hi1'); return this; }, 40 | c: function (beep, boop) { this._mathIsFun = (Math.random()+'hi2'); return this; }, 41 | d: function (baa, baaa, black, sheep) { this._mathIsFun = (Math.random()+'hi3'); return this; }, 42 | e: function (beep, boop) { this._mathIsFun = (Math.random()+'hi5'); return this; }, 43 | f: function (baa, baaa, black, sheep) { this._mathIsFun = (Math.random()+'hi5'); return this; }, 44 | g: function (beep, boop) { this._mathIsFun = (Math.random()+'hi6'); return this; }, 45 | h: function (baa, baaa, black, sheep) { this._mathIsFun = (Math.random()+'hi7'); return this; }, 46 | i: function (beep, boop) { this._mathIsFun = (Math.random()+'hi8'); return this; }, 47 | }); 48 | 49 | return deferred; 50 | 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /test/sanity.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var _ = require('@sailshq/lodash'); 7 | var parley = require('../'); 8 | 9 | 10 | 11 | /** 12 | * sanity.test.js 13 | * 14 | * A test of parley's most basic usage. 15 | * 16 | * > This is really just a sanity check to make sure there are no 17 | * > unexpected problems in the simplest assumptions of this module. 18 | */ 19 | 20 | describe('parley()', function() { 21 | it('should throw', function(){ 22 | try { parley(); } 23 | catch (e) { return; } 24 | throw new Error('Should have thrown an Error'); 25 | }); 26 | }); 27 | 28 | 29 | describe('parley(handleExec)', function() { 30 | 31 | describe('with invalid handleExec', function (){ 32 | it('should throw', function(){ 33 | try { parley(123); } 34 | catch (e) { return; } 35 | throw new Error('Should have thrown an Error'); 36 | }); 37 | it('should throw', function(){ 38 | try { parley([123, 456]); } 39 | catch (e) { return; } 40 | throw new Error('Should have thrown an Error'); 41 | }); 42 | }); 43 | 44 | describe('with valid handleExec', function (){ 45 | var π; 46 | it('should not throw', function(){ 47 | π = parley(function (done){ return done(); }); 48 | }); 49 | it('should have returned an object of some sort', function(){ 50 | if (!_.isObject(π)) { throw new Error('Instead got: '+util.inspect(π,{depth:5})+''); } 51 | }); 52 | describe('deferred object (the "parley" itself)', function (){ 53 | it('should have an `.exec()` method', function(){ 54 | if (!_.isFunction(π.exec)) { throw new Error('Instead got: '+util.inspect(π.exec,{depth:5})+''); } 55 | }); 56 | it('should have a `.then()` method', function(){ 57 | if (!_.isFunction(π.then)) { throw new Error('Instead got: '+util.inspect(π.then,{depth:5})+''); } 58 | }); 59 | it('should have a `.catch()` method', function(){ 60 | if (!_.isFunction(π.catch)) { throw new Error('Instead got: '+util.inspect(π.catch,{depth:5})+''); } 61 | }); 62 | it('should have a `.toPromise()` method', function(){ 63 | if (!_.isFunction(π.toPromise)) { throw new Error('Instead got: '+util.inspect(π.toPromise,{depth:5})+''); } 64 | }); 65 | }); 66 | }); 67 | 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /test/fixtures/find.fixture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var flaverr = require('flaverr'); 7 | var parley = require('../../'); 8 | var helpFind = require('./private/help-find.util'); 9 | 10 | 11 | /** 12 | * find.fixture.js 13 | * 14 | * A simplified mock of Waterline's `find()` model method. 15 | * 16 | * @param {Dictionary?} criteria 17 | * @param {Function} explicitCb 18 | * 19 | * @returns {Deferred} If no callback specified 20 | */ 21 | module.exports = function find( /* variadic */ ){ 22 | 23 | var metadata = {}; 24 | 25 | var explicitCb; 26 | 27 | // Handle variadic usage: 28 | // =========================================================================== 29 | if (!_.isUndefined(arguments[0])) { 30 | if (_.isFunction(arguments[0])) { 31 | explicitCb = arguments[0]; 32 | } 33 | else { 34 | metadata.criteria = arguments[0]; 35 | } 36 | }//>- 37 | 38 | if (!_.isUndefined(arguments[1])) { 39 | explicitCb = arguments[1]; 40 | }//>- 41 | // =========================================================================== 42 | 43 | // This deferred may or may not actually need to get built. 44 | // (but in case it does, we define it out here so we can unambiguously 45 | // return it below) 46 | // 47 | // > If an explicit callback was specified, then go ahead 48 | // > and proceed to where the real action is at. 49 | // > Otherwise, no callback was specified explicitly, 50 | // > so we'll build and return a Deferred instead. 51 | var deferred = parley(function (deferredCb){ 52 | helpFind(undefined, metadata, deferredCb); 53 | }, explicitCb); 54 | 55 | 56 | // If we ended up building a Deferred above, we would have done so synchronously. 57 | // In other words, if there's going to be a Deferred, we have it here. 58 | // 59 | // So if we DON'T have a Deferred, then we know that we must have already went ahead 60 | // and performed our business logic. So we'll just return undefined. 61 | if (!deferred) { 62 | return; 63 | }//-• 64 | 65 | 66 | // IWMIH, then we know we have a Deferred. 67 | // (and thus we haven't actually done anything yet.) 68 | 69 | // At this point, we might opt to attach some methods to our Deferred. 70 | _.extend(deferred, { 71 | where: function(clause) { 72 | metadata.criteria = metadata.criteria || {}; 73 | metadata.criteria.where = clause; 74 | return deferred; 75 | } 76 | }); 77 | 78 | // When we're confident that our Deferred is ready for primetime, 79 | // we finish up by returning it. 80 | return deferred; 81 | 82 | }; 83 | -------------------------------------------------------------------------------- /test/fixtures/find-but-with-final-after-exec-lc.fixture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var flaverr = require('flaverr'); 7 | var parley = require('../../'); 8 | var helpFind = require('./private/help-find.util'); 9 | 10 | 11 | /** 12 | * find-but-with-final-after-exec-lc.fixture.js 13 | * 14 | * A simplified/fake mock of Waterline's `find()` model method -- 15 | * but using a lifecycle callback to do some weird stuff. 16 | * 17 | * > See `find.fixture.js` for more info. Many comments were removed from the code below 18 | * > to avoid unnecessary duplication and reduce the chances of things getting weird. 19 | * 20 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 21 | * @param {Dictionary?} criteria 22 | * @param {Function} explicitCb 23 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 | * @returns {Deferred} If no callback specified 25 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 26 | */ 27 | module.exports = function findButWithFinalAfterExecLC( /* variadic */ ){ 28 | 29 | var metadata = {}; 30 | 31 | var explicitCb; 32 | 33 | // Handle variadic usage: 34 | // =========================================================================== 35 | if (!_.isUndefined(arguments[0])) { 36 | if (_.isFunction(arguments[0])) { 37 | explicitCb = arguments[0]; 38 | } 39 | else { 40 | metadata.criteria = arguments[0]; 41 | } 42 | }//>- 43 | 44 | if (!_.isUndefined(arguments[1])) { 45 | explicitCb = arguments[1]; 46 | }//>- 47 | // =========================================================================== 48 | 49 | return parley(function (done){ 50 | helpFind(undefined, metadata, done); 51 | }, explicitCb, { 52 | where: function(clause) { 53 | metadata.criteria = metadata.criteria || {}; 54 | metadata.criteria.where = clause; 55 | return this; 56 | } 57 | }, undefined, undefined, function (err, result){ 58 | // console.log('* * * * running intercept'); 59 | if (err) { 60 | if (err.code !== 'E_SOME_ERROR') { 61 | err = flaverr('E_SOME_UNRECOGNIZED_ERROR', new Error(err.message)); 62 | // console.log('* * MUTATED ERROR!'); 63 | return err; 64 | }//-• 65 | return err; 66 | }//-• 67 | 68 | // Unless criteria is `true`, simulate a case where we'd want to change the result. 69 | // > Note that we could mutate result and return that, or just return the result. 70 | // > It shouldn't matter! Same thing for the error above. 71 | if (metadata.criteria !== true) { 72 | result.push({ fake: true }); 73 | // console.log('* * MUTATED OUTPUT!'); 74 | return result; 75 | }//-• 76 | return result; 77 | }); 78 | 79 | }; 80 | -------------------------------------------------------------------------------- /test/utils/bench.util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var Benchmark = require('benchmark'); 7 | 8 | 9 | 10 | /** 11 | * bench() 12 | * 13 | * (see https://benchmarkjs.com/docs#Benchmark) 14 | * --------------------------- 15 | * @param {String} name 16 | * @param {Array} testFns [array of functions] 17 | * @param {Function} done 18 | */ 19 | 20 | module.exports = function bench (name, testFns, done) { 21 | 22 | var suite = new Benchmark.Suite({ name: name }); 23 | 24 | _.each(testFns, function (testFn, i) { 25 | suite = suite.add((testFn.name||name)+'#'+i, { 26 | defer: true, 27 | fn: function(deferred){ 28 | 29 | var oneTickHasElapsed; 30 | setImmediate(function (){ 31 | oneTickHasElapsed = true; 32 | }); 33 | 34 | testFn(function (err) { 35 | if (err) { 36 | console.error('An error occured when attempting to run benchmark:\n',err); 37 | }//>- 38 | 39 | // Ensure one tick has elapsed before proceeding 40 | // (otherwise, benchmark doesn't work properly) 41 | if (oneTickHasElapsed) { 42 | deferred.resolve(); 43 | } 44 | else { 45 | setImmediate(function (){ 46 | deferred.resolve(); 47 | }); 48 | } 49 | 50 | }); 51 | } 52 | }); 53 | });// 54 | 55 | suite.on('cycle', function(event) { 56 | console.log(' •',String(event.target)); 57 | }) 58 | .on('complete', function() { 59 | // console.log('Fastest is ' + this.filter('fastest').map('name')); 60 | // console.log('Slowest is ' + this.filter('slowest').map('name')); 61 | return done(); 62 | }) 63 | .run({ async: true }); 64 | 65 | }; 66 | 67 | 68 | 69 | 70 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 71 | // For posterity, here's how to do it asynchronously: 72 | 73 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 74 | // module.exports = function bench (name, testFns, done) { 75 | // var suite = new Benchmark.Suite({ name: name }); 76 | // _.each(testFns, function (testFn, i) { 77 | // suite = suite.add(testFn.name+'#'+i, { 78 | // defer: true, 79 | // fn: function (deferred) { 80 | // testFn(function _afterRunningTestFn(err){ 81 | // setImmediate(function _afterEnsuringAsynchronous(){ 82 | // if (err) { 83 | // console.error('An error occured when attempting to benchmark this code:\n',err); 84 | // }//>- (resolve the deferred either way) 85 | // deferred.resolve(); 86 | // });// 87 | // });// 88 | // } 89 | // });// 90 | // });// 91 | 92 | // suite.on('cycle', function(event) { 93 | // console.log(' •',String(event.target)); 94 | // }) 95 | // .on('complete', function() { 96 | // console.log('Fastest is ' + this.filter('fastest').map('name')); 97 | // console.log('Slowest is ' + this.filter('slowest').map('name')); 98 | // return done(undefined, this); 99 | // }) 100 | // .run(); 101 | // }; 102 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 103 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ 3 | // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ 4 | // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ 5 | // A set of basic conventions for use within any arbitrary Node.js package -- 6 | // inside or outside the Sails framework. For the master copy of this file, 7 | // see the `.eslintrc` template file in the `sails-generate` package 8 | // (https://www.npmjs.com/package/sails-generate.) 9 | // Designed for ESLint v4. 10 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 11 | // For more information about any of the rules below, check out the relevant 12 | // reference page on eslint.org. For example, to get details on "no-sequences", 13 | // you would visit `http://eslint.org/docs/rules/no-sequences`. If you're unsure 14 | // or could use some advice, come by https://sailsjs.com/support. 15 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 16 | 17 | "env": { 18 | "node": true 19 | }, 20 | 21 | "parserOptions": { 22 | "ecmaVersion": 8 23 | }, 24 | 25 | "globals": { 26 | "Promise": true 27 | // ^^Available since Node v4 28 | }, 29 | 30 | "rules": { 31 | "block-scoped-var": ["error"], 32 | "callback-return": ["error", ["done", "proceed", "next", "onwards", "callback", "cb"]], 33 | "camelcase": ["warn", {"properties": "always"}], 34 | "comma-style": ["warn", "last"], 35 | "curly": ["warn"], 36 | "eqeqeq": ["error", "always"], 37 | "eol-last": ["warn"], 38 | "handle-callback-err": ["error"], 39 | "indent": ["warn", 2, { 40 | "SwitchCase": 1, 41 | "MemberExpression": "off", 42 | "FunctionDeclaration": {"body":1, "parameters": "off"}, 43 | "FunctionExpression": {"body":1, "parameters": "off"}, 44 | "CallExpression": {"arguments":"off"}, 45 | "ArrayExpression": 1, 46 | "ObjectExpression": 1, 47 | "ignoredNodes": ["ConditionalExpression"] 48 | }], 49 | "linebreak-style": ["error", "unix"], 50 | "no-dupe-keys": ["error"], 51 | "no-duplicate-case": ["error"], 52 | "no-extra-semi": ["warn"], 53 | "no-labels": ["error"], 54 | "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], 55 | "no-redeclare": ["warn"], 56 | "no-return-assign": ["error", "always"], 57 | "no-sequences": ["error"], 58 | "no-trailing-spaces": ["warn"], 59 | "no-undef": ["error"], 60 | "no-unexpected-multiline": ["warn"], 61 | "no-unreachable": ["warn"], 62 | "no-unused-vars": ["warn", {"caughtErrors":"all", "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)", "argsIgnorePattern": "^unused($|[A-Z].*$)", "varsIgnorePattern": "^unused($|[A-Z].*$)" }], 63 | "no-use-before-define": ["error", {"functions":false}], 64 | "one-var": ["warn", "never"], 65 | "prefer-arrow-callback": ["warn", {"allowNamedFunctions":false}], 66 | "quotes": ["warn", "single", {"avoidEscape":false, "allowTemplateLiterals":true}], 67 | "semi": ["warn", "always"], 68 | "semi-spacing": ["warn", {"before":false, "after":true}], 69 | "semi-style": ["warn", "last"] 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /test/fixtures/validate.fixture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('@sailshq/lodash'); 6 | var parley = require('../../'); 7 | 8 | 9 | /** 10 | * validate.fixture.js 11 | * 12 | * A simplified mock of a hypothetical `validate()` model method 13 | * that is actually synchronous. (This is primarily for use in benchmarks.) 14 | * 15 | * @param {Function} explicitCbMaybe 16 | * 17 | * @returns {Deferred} If no callback specified 18 | */ 19 | module.exports = function validate(explicitCbMaybe){ 20 | 21 | var metadata = {}; 22 | 23 | // This deferred may or may not actually need to get built. 24 | // (but in case it does, we define it out here so we can unambiguously 25 | // return it below) 26 | var deferred; 27 | 28 | 29 | // If an explicit callback was specified, then go ahead 30 | // and proceed to where the real action is at. 31 | // Otherwise, no callback was specified explicitly, 32 | // so we'll build and return a Deferred instead. 33 | deferred = parley(function (finalCb){ 34 | 35 | // Now actually do stuff. 36 | // ...except actually don't-- this is just pretend. 37 | 38 | // All done. 39 | return finalCb(); 40 | 41 | }, explicitCbMaybe); 42 | 43 | 44 | // If we ended up building a Deferred above, we would have done so synchronously. 45 | // In other words, if there's going to be a Deferred, we have it here. 46 | // 47 | // So if we DON'T have a Deferred, then we know that we must have already went ahead 48 | // and performed our business logic. So we'll just return undefined. 49 | if (!deferred) { 50 | return; 51 | }//-• 52 | 53 | 54 | // IWMIH, then we know we have a Deferred. 55 | // (and thus we haven't actually done anything yet.) 56 | 57 | // At this point, we might opt to attach some methods to our Deferred. 58 | // --(1)------------------------------------------------------- 59 | // --too slow: 60 | // --(e.g. 212k ops/sec) 61 | // deferred.meta = function (_meta){ 62 | // metadata.meta = _meta; 63 | // return deferred; 64 | // }; 65 | // --(2)------------------------------------------------------- 66 | // --perfectly fast, but doesn't do anything: 67 | // --(e.g. 373k ops/sec) 68 | // var theMeta = function (_meta){ 69 | // metadata.meta = _meta; 70 | // return deferred; 71 | // }; 72 | // --(3)------------------------------------------------------- 73 | // --somewhat better than the original!!... 74 | // --(e.g. 273k ops/sec) 75 | // --....but problematic, because it doesn't actually mutate 76 | // --the original deferred, which could cause inconsistencies. 77 | // deferred = _.extend({ 78 | // meta: function (_meta){ 79 | // metadata.meta = _meta; 80 | // return deferred; 81 | // } 82 | // }, deferred); 83 | // --(4)------------------------------------------------------- 84 | // --considerably better than the original!! 85 | // --(Even more than #3... plus it's totally valid!) 86 | // --(e.g. ~268k-292k ops/sec) 87 | _.extend(deferred, { 88 | meta: function (_meta){ 89 | metadata.meta = _meta; 90 | return deferred; 91 | }, 92 | // Uncomment these methods for testing performance: 93 | // (this function gets slower and slower the more you add dynamically like this) 94 | // ================================================================================================ 95 | // a: function (beep, boop) { console.log(Math.random()+'hi0'); return deferred; }, 96 | // b: function (baa, baaa, black, sheep) { console.log(Math.random()+'hi1'); return deferred; }, 97 | // c: function (beep, boop) { console.log(Math.random()+'hi2'); return deferred; }, 98 | // d: function (baa, baaa, black, sheep) { console.log(Math.random()+'hi3'); return deferred; }, 99 | // e: function (beep, boop) { console.log(Math.random()+'hi5'); return deferred; }, 100 | // f: function (baa, baaa, black, sheep) { console.log(Math.random()+'hi5'); return deferred; }, 101 | // g: function (beep, boop) { console.log(Math.random()+'hi6'); return deferred; }, 102 | // h: function (baa, baaa, black, sheep) { console.log(Math.random()+'hi7'); return deferred; }, 103 | // i: function (beep, boop) { console.log(Math.random()+'hi8'); return deferred; }, 104 | // j: function (baa, baaa, black, sheep) { console.log(Math.random()+'hi9'); return deferred; }, 105 | // k: function (beep, boop) { console.log(Math.random()+'hi10'); return deferred; }, 106 | // l: function (baa, baaa, black, sheep) { console.log(Math.random()+'hi11'); return deferred; }, 107 | // ================================================================================================ 108 | }); 109 | 110 | // When we're confident that our Deferred is ready for primetime, 111 | // we finish up by returning it. 112 | return deferred; 113 | 114 | }; 115 | 116 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // ┬┌─┐╦ ╦╦╔╗╔╔╦╗┬─┐┌─┐ 3 | // │└─┐╠═╣║║║║ ║ ├┬┘│ 4 | // o└┘└─┘╩ ╩╩╝╚╝ ╩ ┴└─└─┘ 5 | // 6 | // This file (`.jshintrc`) exists to help with consistency of code 7 | // throughout this package, and throughout Sails and the Node-Machine project. 8 | // 9 | // To review what each of these options mean, see: 10 | // http://jshint.com/docs/options 11 | // 12 | // (or: https://github.com/jshint/jshint/blob/master/examples/.jshintrc) 13 | 14 | 15 | 16 | ////////////////////////////////////////////////////////////////////// 17 | // NOT SUPPORTED IN SOME JSHINT VERSIONS SO LEAVING COMMENTED OUT: 18 | ////////////////////////////////////////////////////////////////////// 19 | // Prevent overwriting prototypes of native classes like `Array`. 20 | // (doing this is _never_ ok in any of our packages that are intended 21 | // to be used as dependencies of other developers' modules and apps) 22 | // "freeze": true, 23 | ////////////////////////////////////////////////////////////////////// 24 | 25 | 26 | ////////////////////////////////////////////////////////////////////// 27 | // EVERYTHING ELSE: 28 | ////////////////////////////////////////////////////////////////////// 29 | 30 | // Allow the use of `eval` and `new Function()` 31 | // (we sometimes actually need to use these things) 32 | "evil": true, 33 | 34 | // Tolerate funny-looking dashes in RegExp literals. 35 | // (see https://github.com/jshint/jshint/issues/159#issue-903547) 36 | "regexdash": true, 37 | 38 | // The potential runtime "Environments" (as defined by jshint) 39 | // that the _style_ of code written in this package should be 40 | // compatible with (not the code itself, of course). 41 | "browser": true, 42 | "node": true, 43 | "wsh": true, 44 | 45 | // Tolerate the use `[]` notation when dot notation would be possible. 46 | // (this is sometimes preferable for readability) 47 | "sub": true, 48 | 49 | // Do NOT suppress warnings about mixed tabs and spaces 50 | // (two spaces always, please; see `.editorconfig`) 51 | "smarttabs": false, 52 | 53 | // Suppress warnings about trailing whitespace 54 | // (this is already enforced by the .editorconfig, so no need to warn as well) 55 | "trailing": false, 56 | 57 | // Suppress warnings about the use of expressions where fn calls or assignments 58 | // are expected, and about using assignments where conditionals are expected. 59 | // (while generally a good idea, without this setting, JSHint needlessly lights up warnings 60 | // in existing, working code that really shouldn't be tampered with. Pandora's box and all.) 61 | "expr": true, 62 | "boss": true, 63 | 64 | // Do NOT suppress warnings about using functions inside loops 65 | // (in the general case, we should be using iteratee functions with `_.each()` 66 | // or `Array.prototype.forEach()` instead of `for` or `while` statements 67 | // anyway. This warning serves as a helpful reminder.) 68 | "loopfunc": false, 69 | 70 | // Suppress warnings about "weird constructions" 71 | // i.e. allow code like: 72 | // ``` 73 | // (new (function OneTimeUsePrototype () { } )) 74 | // ``` 75 | // 76 | // (sometimes order of operations in JavaScript can be scary. There is 77 | // nothing wrong with using an extra set of parantheses when the mood 78 | // strikes or you get "that special feeling".) 79 | "supernew": true, 80 | 81 | // Do NOT allow backwards, node-dependency-style commas. 82 | // (while this code style choice was used by the project in the past, 83 | // we have since standardized these practices to make code easier to 84 | // read, albeit a bit less exciting) 85 | "laxcomma": false, 86 | 87 | // Do NOT allow avant garde use of commas in conditional statements. 88 | // (this prevents accidentally writing code like: 89 | // ``` 90 | // if (!_.contains(['+ci', '-ci', '∆ci', '+ce', '-ce', '∆ce']), change.verb) {...} 91 | // ``` 92 | // See the problem in that code? Neither did we-- that's the problem!) 93 | "nocomma": true, 94 | 95 | // Strictly enforce the consistent use of single quotes. 96 | // (this is a convention that was established primarily to make it easier 97 | // to grep [or FIND+REPLACE in Sublime] particular string literals in 98 | // JavaScript [.js] files. Note that JSON [.json] files are, of course, 99 | // still written exclusively using double quotes around key names and 100 | // around string literals.) 101 | "quotmark": "single", 102 | 103 | // Do NOT suppress warnings about the use of `==null` comparisons. 104 | // (please be explicit-- use Lodash or `require('util')` and call 105 | // either `.isNull()` or `.isUndefined()`) 106 | "eqnull": false, 107 | 108 | // Strictly enforce the use of curly braces with `if`, `else`, and `switch` 109 | // as well as, much less commonly, `for` and `while` statements. 110 | // (this is just so that all of our code is consistent, and to avoid bugs) 111 | "curly": true, 112 | 113 | // Strictly enforce the use of `===` and `!==`. 114 | // (this is always a good idea. Check out "Truth, Equality, and JavaScript" 115 | // by Angus Croll [the author of "If Hemmingway Wrote JavaScript"] for more 116 | // explanation as to why.) 117 | "eqeqeq": true, 118 | 119 | // Allow initializing variables to `undefined`. 120 | // For more information, see: 121 | // • https://jslinterrors.com/it-is-not-necessary-to-initialize-a-to-undefined 122 | // • https://github.com/jshint/jshint/issues/1484 123 | // 124 | // (it is often very helpful to explicitly clarify the initial value of 125 | // a local variable-- especially for folks new to more advanced JavaScript 126 | // and who might not recognize the subtle, yet critically important differences between our seemingly 127 | // between `null` and `undefined`, and the impact on `typeof` checks) 128 | "-W080": true 129 | 130 | } 131 | -------------------------------------------------------------------------------- /test/behavior.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var assert = require('assert'); 7 | var _ = require('@sailshq/lodash'); 8 | var parley = require('../'); 9 | 10 | 11 | 12 | /** 13 | * behavior.test.js 14 | * 15 | * Tests verifying parley's behavior with both callback and promise usage. 16 | */ 17 | 18 | describe('behavior.test.js', function() { 19 | 20 | 21 | // ███████╗██╗ ██╗███████╗ ██████╗ ██╗██╗ 22 | // ██╔════╝╚██╗██╔╝██╔════╝██╔════╝██╔╝╚██╗ 23 | // █████╗ ╚███╔╝ █████╗ ██║ ██║ ██║ 24 | // ██╔══╝ ██╔██╗ ██╔══╝ ██║ ██║ ██║ 25 | // ██╗███████╗██╔╝ ██╗███████╗╚██████╗╚██╗██╔╝ 26 | // ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝╚═╝ 27 | // 28 | describe('.exec()', function() { 29 | describe('with proper usage', function() { 30 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 31 | it('should work', function(done){ 32 | deferred.exec(function(err) { 33 | if (err) { return done(err); } 34 | return done(); 35 | }); 36 | }); 37 | }); 38 | describe('when called more than once', function() { 39 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 40 | it('should ignore subsequent calls', function(done){ 41 | this.slow(300); 42 | 43 | // As a hack, override console.warn(). 44 | // (this is mainly to improve the experience of looking at test results, 45 | // but it also has the benefit of adding another check.) 46 | var origConsoleWarn = global.console.warn; 47 | var counter = 0; 48 | global.console.warn = function(){ 49 | counter++; 50 | }; 51 | 52 | deferred.exec(function (){ 53 | setTimeout(function (){ 54 | global.console.warn = origConsoleWarn; 55 | try { 56 | assert.equal(counter, 3); 57 | } catch(e) { return done(e); } 58 | return done(); 59 | }, 125); 60 | }); 61 | 62 | // The following .exec() calls will be ignored. 63 | // (Note that 3 extra warnings will be logged, though.) 64 | deferred.exec(function (){ 65 | return done(new Error('Should never make it here')); 66 | }); 67 | deferred.exec(function (){ 68 | return done(new Error('Should never make it here')); 69 | }); 70 | deferred.exec(function (){ 71 | return done(new Error('Should never make it here')); 72 | }); 73 | }); 74 | }); 75 | describe('with invalid callback', function() { 76 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 77 | it('should throw', function(){ 78 | try { deferred.exec(123); } 79 | catch (e) { return; } 80 | throw new Error('Should have thrown an Error'); 81 | }); 82 | }); 83 | describe('with no arguments', function() { 84 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 85 | it('should throw', function(){ 86 | try { deferred.exec(); } 87 | catch (e) { return; } 88 | throw new Error('Should have thrown an Error'); 89 | }); 90 | }); 91 | describe('with two arguments and a callback that throws an uncaught exception', function() { 92 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 93 | it('should run uncaught exception handler', function(done){ 94 | this.slow(300); 95 | 96 | deferred.exec(function (){ 97 | throw new Error('This is uncaught! Watch out!'); 98 | }, function(uncaughtError) { 99 | try { 100 | assert.equal(uncaughtError.message, 'This is uncaught! Watch out!'); 101 | } catch (e) { return done(e); } 102 | return done(); 103 | }); 104 | 105 | }); 106 | }); 107 | });// 108 | 109 | 110 | // ████████╗██╗ ██╗███████╗███╗ ██╗ ██╗██╗ 111 | // ╚══██╔══╝██║ ██║██╔════╝████╗ ██║██╔╝╚██╗ 112 | // ██║ ███████║█████╗ ██╔██╗ ██║██║ ██║ 113 | // ██║ ██╔══██║██╔══╝ ██║╚██╗██║██║ ██║ 114 | // ██╗██║ ██║ ██║███████╗██║ ╚████║╚██╗██╔╝ 115 | // ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═╝╚═╝ 116 | // 117 | describe('.then()', function() { 118 | describe('with proper usage', function() { 119 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 120 | it('should work', function(done){ 121 | deferred.then(function(result) { 122 | return done(); 123 | }).catch(function(err){ return done(err); }); 124 | }); 125 | }); 126 | describe('when called more than once', function() { 127 | var deferred; before(function(){ deferred = parley(function(done){ setTimeout(function (){ return done(undefined, 'hello!'); }, 12); }); }); 128 | it('should do the normal promise chaining thing', function(done){ 129 | this.slow(300); 130 | 131 | deferred.then(function (){ 132 | // do nothing 133 | }).catch(function(err){ return done(err); }); 134 | 135 | // The following .then() calls will all run in order. 136 | deferred.then(function (){ 137 | // do nothing 138 | }).catch(function(err){ return done(err); }); 139 | deferred.then(function (){ 140 | // do nothing 141 | }).catch(function(err){ return done(err); }); 142 | deferred.then(function (){ 143 | return done(); 144 | }).catch(function(err){ return done(err); }); 145 | }); 146 | }); 147 | });// 148 | 149 | 150 | // ██╗ ██╗ ██╗ ██████╗██╗ ██╗███████╗████████╗ ██████╗ ███╗ ███╗ 151 | // ██║ ██║ ██╔╝ ██╔════╝██║ ██║██╔════╝╚══██╔══╝██╔═══██╗████╗ ████║ 152 | // ██║ █╗ ██║ ██╔╝ ██║ ██║ ██║███████╗ ██║ ██║ ██║██╔████╔██║ 153 | // ██║███╗██║ ██╔╝ ██║ ██║ ██║╚════██║ ██║ ██║ ██║██║╚██╔╝██║ 154 | // ╚███╔███╔╝██╔╝ ╚██████╗╚██████╔╝███████║ ██║ ╚██████╔╝██║ ╚═╝ ██║ 155 | // ╚══╝╚══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ 156 | // 157 | // ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ███████╗ 158 | // ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██╔════╝ 159 | // ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║███████╗ 160 | // ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║╚════██║ 161 | // ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝███████║ 162 | // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ 163 | // 164 | describe('building and/or executing a deferred that uses one or more custom methods', function(){ 165 | 166 | // ┌┐┌┌─┐┬─┐┌┬┐┌─┐┬ ┌─┐┌─┐┌─┐┌─┐ 167 | // ││││ │├┬┘│││├─┤│ │ ├─┤└─┐├┤ 168 | // ┘└┘└─┘┴└─┴ ┴┴ ┴┴─┘ └─┘┴ ┴└─┘└─┘ 169 | describe('where everything is valid and normal', function(){ 170 | it('should work', function(){ 171 | var deferred = parley(function(done){ 172 | setTimeout(function (){ return done(undefined, 'hello!'); }, 12); 173 | }, undefined, { 174 | foo: function (){ return deferred; } 175 | }); 176 | });// 177 | });// 178 | 179 | // ┌─┐┬ ┬┌─┐┌┬┐┌─┐┌┬┐ ┌┬┐┌─┐┌┬┐┬ ┬┌─┐┌┬┐ ┌┐┌┌─┐┌┬┐┌─┐ 180 | // │ │ │└─┐ │ │ ││││ │││├┤ │ ├─┤│ │ ││ │││├─┤│││├┤ 181 | // └─┘└─┘└─┘ ┴ └─┘┴ ┴ ┴ ┴└─┘ ┴ ┴ ┴└─┘─┴┘ ┘└┘┴ ┴┴ ┴└─┘ 182 | // ╔═╗╔═╗╔╗╔╔═╗╦ ╦╔═╗╔╦╗╔═╗ ┬ ┬┬┌┬┐┬ ┬ ╦═╗╔═╗╔═╗╔═╗╦═╗╦ ╦╔═╗╔╦╗ ╔═╗╦═╗╔═╗╔═╗╔═╗╦═╗╔╦╗╦ ╦ 183 | // ║ ║ ║║║║╠╣ ║ ║║ ║ ╚═╗ ││││ │ ├─┤ ╠╦╝║╣ ╚═╗║╣ ╠╦╝╚╗╔╝║╣ ║║ ╠═╝╠╦╝║ ║╠═╝║╣ ╠╦╝ ║ ╚╦╝ 184 | // ╚═╝╚═╝╝╚╝╚ ╩═╝╩╚═╝ ╩ ╚═╝ └┴┘┴ ┴ ┴ ┴ ╩╚═╚═╝╚═╝╚═╝╩╚═ ╚╝ ╚═╝═╩╝ ╩ ╩╚═╚═╝╩ ╚═╝╩╚═ ╩ ╩ 185 | describe('but where one or more custom methods have names which conflict with a reserved property', function(){ 186 | 187 | describe('given `exec`', function(){ 188 | it('should throw', function(){ 189 | try { 190 | var deferred = parley(function(done){ throw new Error('Should never make it here.'); }, undefined, { 191 | foo: function (){ return deferred; }, 192 | exec: function (){ return deferred; } 193 | }); 194 | } catch (e) { return; } 195 | 196 | throw new Error('Should have thrown an Error'); 197 | });// 198 | });// 199 | 200 | describe('given `toPromise`', function(){ 201 | it('should throw', function(){ 202 | try { 203 | var deferred = parley(function(done){ throw new Error('Should never make it here.'); }, undefined, { 204 | foo: function (){ return deferred; }, 205 | toPromise: function (){ return deferred; } 206 | }); 207 | } catch (e) { return; } 208 | 209 | throw new Error('Should have thrown an Error'); 210 | });// 211 | });// 212 | 213 | describe('given `_hasBegunExecuting`', function(){ 214 | it('should throw', function(){ 215 | try { 216 | var deferred = parley(function(done){ throw new Error('Should never make it here.'); }, undefined, { 217 | foo: function (){ return deferred; }, 218 | _hasBegunExecuting: function (){ return deferred; } 219 | }); 220 | } catch (e) { return; } 221 | 222 | throw new Error('Should have thrown an Error'); 223 | });// 224 | });// 225 | 226 | describe('given `constructor`', function(){ 227 | it('should throw', function(){ 228 | try { 229 | var deferred = parley(function(done){ throw new Error('Should never make it here.'); }, undefined, { 230 | foo: function (){ return deferred; }, 231 | constructor: function (){ return deferred; } 232 | }); 233 | } catch (e) { return; } 234 | 235 | throw new Error('Should have thrown an Error'); 236 | });// 237 | });// 238 | 239 | describe('given `inspect`', function(){ 240 | it('should throw', function(){ 241 | try { 242 | var deferred = parley(function(done){ throw new Error('Should never make it here.'); }, undefined, { 243 | foo: function (){ return deferred; }, 244 | inspect: function (){ return deferred; } 245 | }); 246 | } catch (e) { return; } 247 | 248 | throw new Error('Should have thrown an Error'); 249 | });// 250 | });// 251 | 252 | 253 | });// 254 | 255 | 256 | });// 257 | 258 | }); 259 | 260 | 261 | // // A few additional, ad hoc tests: 262 | // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 263 | // // Success condition: 264 | // // ==================== 265 | // // .toPromise() 266 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(undefined, 'hello!'); }, 1000); } }); promise = π.toPromise(); promise.then(function(result){ console.log('done!', result); }).catch(function(err){ console.error('ERROR',err); }); 267 | // // .then() shortcut 268 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(undefined, 'hello!'); }, 1000); } }); π.then(function(result){ console.log('done!', result); }).catch(function(err){ console.error('ERROR',err); }); 269 | // // .exec() 270 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(undefined, 'hello!'); }, 1000); } }); π.exec(function(err, result){ if (err){ console.error('ERROR',err, result); return; } console.log('done!', err, result); }); 271 | 272 | // // Error condition: 273 | // // ==================== 274 | // // .toPromise() 275 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(new Error('uh oh'), 'should never get this!'); }, 1000); } }); promise = π.toPromise(); promise.then(function(result){ console.log('done!', result); }).catch(function(err){ console.error('ERROR',err); }); 276 | // // .then() shortcut 277 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(new Error('uh oh'), 'should never get this!'); }, 1000); } }); π.then(function(result){ console.log('done!', result); }).catch(function(err){ console.error('ERROR',err); }); 278 | // // .catch() shortcut 279 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(new Error('uh oh'), 'should never get this!'); }, 1000); } }); π.catch(function(err){ console.error('ERROR',err); }); 280 | // // .exec() 281 | // π = require('./')({ codeName: 'asdf', handleExec: function foo(done){ console.log('working...'); setTimeout(function (){ console.log('finishing...'); return done(new Error('uh oh'), 'should never get this!'); }, 1000); } }); π.exec(function(err, result){ if (err){ console.error('ERROR',err, result); return; } console.log('done!', err, result); }); 282 | // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 283 | -------------------------------------------------------------------------------- /lib/parley.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var _ = require('@sailshq/lodash'); 7 | var flaverr = require('flaverr'); 8 | var Deferred = require('./private/Deferred'); 9 | 10 | // Optimization: Pull process env check up here. 11 | var IS_DEBUG_OR_NON_PRODUCTION_ENV = ( 12 | process.env.NODE_ENV !== 'production' || 13 | process.env.DEBUG 14 | ); 15 | 16 | /** 17 | * parley() 18 | * 19 | * Build a deferred object that supports Node-style callbacks and promises. 20 | * > See README.md for more details. 21 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 22 | * @param {Function} handleExec 23 | * The `handleExec` function to call (either immediately or when the Deferred 24 | * is executed, depending on whether an explicit cb was provided) 25 | * 26 | * @param {Function?} explicitCbMaybe 27 | * An optional parameter that, if specified, is passed directly as the incoming 28 | * `done` argument to your "handleExec" handler function (i.e. _its_ callback). 29 | * Otherwise, if it is omitted, then handleExec receives an internally-generated 30 | * callback (from parley) as its `done` argument. When called, this implicit `done` 31 | * will appropriately dispatch with the deferred object. Finally, note that if an 32 | * explicit callback is provided, parley will return undefined instead of returning 33 | * a Deferred. 34 | * > The nice thing about this is that it allows implementor code that provides this 35 | * > feature to avoid manually duplicating the branching logic (i.e. the code that 36 | * > checks to see if an explicit cb was provided, and if not, returns a new Deferred) 37 | * 38 | * @param {Dictionary?} customMethods 39 | * An optional dictionary of custom functions that, if specified, will be used to extend 40 | * the Deferred object. It omitted, then only the default methods like `.exec()` will 41 | * exist. 42 | * > e.g. 43 | * > ``` 44 | * > { 45 | * > where: function (whereClause) { 46 | * > this._criteria = this._criteria || {}; 47 | * > this._criteria.where = whereClause; 48 | * > return this; 49 | * > }, 50 | * > foo: function(){...}, 51 | * > bar: function(){...}, 52 | * > ... 53 | * > } 54 | * 55 | * @param {Number?} timeout 56 | * Optional. If specified, timeouts will be enabled, and this number will indicate 57 | * the max # of milliseconds to let the `handleExec` logic run before giving up and 58 | * failing with a TimeoutError. (To disable timeouts, leave this undefined.) 59 | * 60 | * @param {Error?} omen 61 | * An optional omen to use for improving the stack trace, in the event of an error. 62 | * 63 | * @param {Function?} finalAfterExecLC 64 | * An optional, synchronous handler function for intercepting the arguments to .exec()'s 65 | * callback. Only applicable when using the Deferred-style usage-- including when using 66 | * promises via ES8's async/await or .then()/.catch(). Receives the `(err, result)` 67 | * function signature, where either `err` is an Error instance and `result` is undefined, 68 | * or `err` is undefined and `result` may or may not exist. In any case, if specified, 69 | * this handler function MUST not throw, and it MUST respect standard node-style callback 70 | * conventions insofar as how it decides a return value. For example, if it receives `err`, 71 | * it must return an Error instance. (ENFORCING THIS IS UP TO YOUR CODE!) 72 | * > NOTE: The purpose of this handler is to allow for changing the behavior of .exec() 73 | * > without necessarily calling it, or reinventing the wheel and creating our own version. 74 | * > For example, we might want to include nicer, more customized error messages. Or we 75 | * > might want to intercept built-in error scenarios that would be otherwise difficult to 76 | * > capture-- things like timeouts, double-invocation, and unhandled throwing. 77 | * 78 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 79 | * @returns {Deferred} 80 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 81 | * @throws {Error} If there are unexpected usage problems with how parley() itself is called 82 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 83 | */ 84 | 85 | module.exports = function parley(handleExec, explicitCbMaybe, customMethods, timeout, omen, finalAfterExecLC){ 86 | 87 | // A few (very carefully picked) sanity checks for implementors. 88 | // 89 | // > Note that we deliberately use `typeof` instead of _.isFunction() for performance. 90 | if (!handleExec) { 91 | throw new Error('Consistency violation: Must specify a first argument when calling parley() -- please provide a `handleExec` function or a dictionary of options'); 92 | } 93 | if (typeof handleExec !== 'function') { 94 | throw new Error('Consistency violation: First argument to parley() should be a function. But instead, got: '+util.inspect(handleExec, {depth:2})+''); 95 | } 96 | 97 | 98 | //========================================================================================== 99 | // ALL OTHER **IMPLEMENTOR** USAGE CHECKS WERE REMOVED FOR PERFORMANCE REASONS. 100 | // 101 | // > Check out this commit for more of the original code: 102 | // > https://github.com/mikermcneil/parley/commit/e7ec7e445e2a502b9fcb57bc746c7b9714d3cf16 103 | // > 104 | // > Or for another example of a simple check that would pack a 24% performance hit 105 | // > for building Deferreds, see: 106 | // > https://github.com/mikermcneil/parley/commit/7d475d0c2165b683d8d5af98a4d073875f14cbd3 107 | // > 108 | // > Also note we still do a few (very carefully picked) validations for things that could 109 | // > affect end users of parley-implementing functions -- i.e. code that calls .exec() twice, 110 | // > etc. That's all handled elsewhere (where the exec() method is defined.) 111 | //========================================================================================== 112 | 113 | 114 | // ██╗ ██╗ █████╗ ███╗ ██╗██████╗ ██╗ ███████╗ 115 | // ██║ ██║██╔══██╗████╗ ██║██╔══██╗██║ ██╔════╝ 116 | // ███████║███████║██╔██╗ ██║██║ ██║██║ █████╗ 117 | // ██╔══██║██╔══██║██║╚██╗██║██║ ██║██║ ██╔══╝ 118 | // ██║ ██║██║ ██║██║ ╚████║██████╔╝███████╗███████╗ 119 | // ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚══════╝ 120 | // 121 | // ███████╗██╗ ██╗██████╗ ██╗ ██╗ ██████╗██╗████████╗ ██████╗██████╗ 122 | // ██╔════╝╚██╗██╔╝██╔══██╗██║ ██║██╔════╝██║╚══██╔══╝ ██╔════╝██╔══██╗ 123 | // █████╗ ╚███╔╝ ██████╔╝██║ ██║██║ ██║ ██║ ██║ ██████╔╝ 124 | // ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║██║ ██║ ██║ ██║ ██╔══██╗ 125 | // ███████╗██╔╝ ██╗██║ ███████╗██║╚██████╗██║ ██║ ╚██████╗██████╔╝ 126 | // ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝╚═════╝ 127 | // 128 | // ╦╔═╗ ┌─┐┬─┐┌─┐┬ ┬┬┌┬┐┌─┐┌┬┐ 129 | // ║╠╣ ├─┘├┬┘│ │└┐┌┘│ ││├┤ ││ 130 | // ╩╚ ┴ ┴└─└─┘ └┘ ┴─┴┘└─┘─┴┘ 131 | // If explicitCb provided, run the handleExec logic, then call the explicit callback. 132 | // 133 | // > All of the additional checks from below (e.g. try/catch) are NOT performed 134 | // > in the situation where an explicit callback was provided. This is to allow 135 | // > for userland code to squeeze better performance out of particular method calls 136 | // > by simply passing through the callback directly. 137 | // > (As a bonus, it also avoids duplicating the code below in this file.) 138 | if (explicitCbMaybe) { 139 | 140 | handleExec(explicitCbMaybe); 141 | 142 | // ██████╗ ███████╗████████╗██╗ ██╗██████╗ ███╗ ██╗ 143 | // ██╔══██╗██╔════╝╚══██╔══╝██║ ██║██╔══██╗████╗ ██║ 144 | // ██████╔╝█████╗ ██║ ██║ ██║██████╔╝██╔██╗ ██║ 145 | // ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██║╚██╗██║ 146 | // ██║ ██║███████╗ ██║ ╚██████╔╝██║ ██║██║ ╚████║ 147 | // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ 148 | // 149 | // ██╗ ██╗███╗ ██╗██████╗ ███████╗███████╗██╗███╗ ██╗███████╗██████╗ 150 | // ██║ ██║████╗ ██║██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝██╔══██╗ 151 | // ██║ ██║██╔██╗ ██║██║ ██║█████╗ █████╗ ██║██╔██╗ ██║█████╗ ██║ ██║ 152 | // ██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██╔══╝ ██║██║╚██╗██║██╔══╝ ██║ ██║ 153 | // ╚██████╔╝██║ ╚████║██████╔╝███████╗██║ ██║██║ ╚████║███████╗██████╔╝ 154 | // ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═════╝ 155 | // 156 | return; 157 | 158 | }//-• 159 | 160 | // Otherwise, no explicit callback was provided- so we'll build & return a Deferred... 161 | 162 | 163 | // ██████╗ ████████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗███████╗███████╗ 164 | // ██╔═══██╗╚══██╔══╝██║ ██║██╔════╝██╔══██╗██║ ██║██║██╔════╝██╔════╝██╗ 165 | // ██║ ██║ ██║ ███████║█████╗ ██████╔╝██║ █╗ ██║██║███████╗█████╗ ╚═╝ 166 | // ██║ ██║ ██║ ██╔══██║██╔══╝ ██╔══██╗██║███╗██║██║╚════██║██╔══╝ ██╗ 167 | // ╚██████╔╝ ██║ ██║ ██║███████╗██║ ██║╚███╔███╔╝██║███████║███████╗╚═╝ 168 | // ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝╚══════╝╚══════╝ 169 | // 170 | // ██████╗ ██╗ ██╗██╗██╗ ██████╗ 171 | // ██╔══██╗██║ ██║██║██║ ██╔══██╗ 172 | // ██████╔╝██║ ██║██║██║ ██║ ██║ 173 | // ██╔══██╗██║ ██║██║██║ ██║ ██║ 174 | // ██████╔╝╚██████╔╝██║███████╗██████╔╝ 175 | // ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ 176 | // 177 | // ██████╗ ███████╗███████╗███████╗██████╗ ██████╗ ███████╗██████╗ 178 | // ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██╔══██╗ 179 | // ██║ ██║█████╗ █████╗ █████╗ ██████╔╝██████╔╝█████╗ ██║ ██║ 180 | // ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██║ ██║ 181 | // ██████╔╝███████╗██║ ███████╗██║ ██║██║ ██║███████╗██████╔╝ 182 | // ╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ 183 | // 184 | // Build deferred object. 185 | // 186 | // > For more info & benchmarks, see: 187 | // > https://github.com/mikermcneil/parley/commit/5996651c4b15c7850b5eb2e4dc038e8202414553#commitcomment-20256030 188 | // > 189 | // > And also `baseline.benchmark.js` in this repo. 190 | // > 191 | // > But then also see: 192 | // > https://github.com/mikermcneil/parley/commit/023dc9396bdfcd02290624ca23cb2d005037f398 193 | // > 194 | // > (Basically, it keeps going back and forth between this and closures, but after a lot 195 | // > of experimentation, the prototypal approach seems better for overall performance.) 196 | var π = new Deferred(handleExec); 197 | 198 | 199 | // If appropriate, start the 15 second .exec() countdown. 200 | var EXEC_COUNTDOWN_IN_SECONDS = 15; 201 | if (IS_DEBUG_OR_NON_PRODUCTION_ENV) { 202 | π._execCountdown = setTimeout(()=>{ 203 | // IWMIH, it means that this Deferred hasn't begun executing, 204 | // even after 15 seconds. This deserves a warning log. 205 | // > See https://trello.com/c/7QnQZ6aC for more background. 206 | console.warn( 207 | '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'+ 208 | 'WARNING: A function that was initially called over '+EXEC_COUNTDOWN_IN_SECONDS+' seconds\n'+ 209 | 'ago has still not actually been executed. Any chance the\n'+ 210 | 'source code is missing an "await"?\n'+ 211 | '\n'+ 212 | (π._omen?( 213 | 'To assist you in hunting this down, here is a stack trace:\n'+ 214 | '```\n'+ 215 | flaverr.getBareTrace(π._omen)+'\n'+ 216 | '```\n'+ 217 | '\n' 218 | ):'')+ 219 | // 'Please double-check this code is not missing an "await".\n'+ 220 | // '\n'+ 221 | ' [?] For more help, visit https://sailsjs.com/support\n'+ 222 | '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' 223 | ); 224 | }, EXEC_COUNTDOWN_IN_SECONDS*1000); 225 | }//fi 226 | 227 | 228 | // ┌─┐┌┬┐┌┬┐┌─┐┌─┐┬ ┬ ╔═╗╦ ╦╔═╗╔╦╗╔═╗╔╦╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗ 229 | // ├─┤ │ │ ├─┤│ ├─┤ ║ ║ ║╚═╗ ║ ║ ║║║║ ║║║║╣ ║ ╠═╣║ ║ ║║╚═╗ 230 | // ┴ ┴ ┴ ┴ ┴ ┴└─┘┴ ┴ ╚═╝╚═╝╚═╝ ╩ ╚═╝╩ ╩ ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝╚═╝ 231 | // ┌─ ┬┌─┐ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ─┐ 232 | // │─── │├┤ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ───│ 233 | // └─ ┴└ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ─┘ 234 | // If a dictionary of `customMethods` were provided, attach them dynamically. 235 | if (customMethods) { 236 | 237 | // Even with no contents, using an _.each() loop here would actually hurt the performance 238 | // of the "just_build" benchmark by 93% (~13x as slow). Granted, it was very fast to 239 | // begin with... but compare with this `for` loop which, with no contents, only hurts 240 | // the same benchmark's performance by 25% ~(1.3x as slow). 241 | var methodFn; 242 | for (var methodName in customMethods) { 243 | 244 | // We explicitly prevent overriding: 245 | if ( 246 | // • built-in methods: 247 | methodName === 'exec' || 248 | methodName === 'then' || 249 | methodName === 'catch' || 250 | methodName === 'toPromise' || 251 | methodName === 'intercept' || 252 | methodName === 'tolerate' || 253 | // (Note that we explicitly omit `.log()`, `.now()`, and `.timeout()` 254 | // so that they may be potentially overridden.) 255 | 256 | // • other special, private properties: 257 | methodName === '_execCountdown' || 258 | methodName === '_hasBegunExecuting' || 259 | methodName === '_hasFinishedExecuting' || 260 | methodName === '_hasStartedButNotFinishedAfterExecLC' || 261 | methodName === '_hasAlreadyWaitedAtLeastOneTick' || 262 | methodName === '_skipImplSpinlockWarning' || 263 | methodName === '_hasTimedOut' || 264 | methodName === '_handleExec' || 265 | methodName === '_promise' || 266 | methodName === '_timeout' || 267 | methodName === '_omen' || 268 | methodName === '_userlandAfterExecLCs' || 269 | methodName === '_finalAfterExecLC' || 270 | 271 | // • the standard JavaScript object flora: 272 | methodName === '__defineGetter__' || 273 | methodName === '__defineSetter__' || 274 | methodName === '__lookupGetter__' || 275 | methodName === '__lookupSetter__' || 276 | methodName === '__proto__' || 277 | methodName === 'constructor' || 278 | methodName === 'hasOwnProperty' || 279 | methodName === 'isPrototypeOf' || 280 | methodName === 'propertyIsEnumerable' || 281 | methodName === 'toLocaleString' || 282 | methodName === 'toString' || 283 | methodName === 'valueOf' || 284 | 285 | // • and things that are just a really bad idea: 286 | // (or at the very least, which shouldn't be defined this way) 287 | methodName === 'prototype' || 288 | methodName === 'toJSON' || 289 | methodName === 'inspect' 290 | ) { 291 | throw new Error('Cannot define custom method (`.'+methodName+'()`) because `'+methodName+'` is a reserved/built-in property.'); 292 | } 293 | methodFn = customMethods[methodName]; 294 | π[methodName] = methodFn; 295 | }// 296 | 297 | }//>- 298 | 299 | 300 | // ┌─┐┌┬┐┌┬┐┌─┐┌─┐┬ ┬ ╔╦╗╦╔╦╗╔═╗╔═╗╦ ╦╔╦╗ 301 | // ├─┤ │ │ ├─┤│ ├─┤ ║ ║║║║║╣ ║ ║║ ║ ║ 302 | // ┴ ┴ ┴ ┴ ┴ ┴└─┘┴ ┴ ╩ ╩╩ ╩╚═╝╚═╝╚═╝ ╩ 303 | // ┌─ ┬┌─┐ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ─┐ 304 | // │─── │├┤ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ───│ 305 | // └─ ┴└ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ─┘ 306 | if (timeout) { 307 | if (!_.isNumber(timeout)) { throw new Error('Consistency violation: If provided, `timeout` argument to parley should be a number (i.e. max # of milliseconds to wait before giving up). But instead, got: '+util.inspect(timeout, {depth:2})+''); } 308 | π._timeout = timeout; 309 | } 310 | 311 | // ┌─┐┌┬┐┌┬┐┌─┐┌─┐┬ ┬ ╔═╗╔╦╗╔═╗╔╗╔ 312 | // ├─┤ │ │ ├─┤│ ├─┤ ║ ║║║║║╣ ║║║ 313 | // ┴ ┴ ┴ ┴ ┴ ┴└─┘┴ ┴ ╚═╝╩ ╩╚═╝╝╚╝ 314 | // ┌─ ┬┌─┐ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ─┐ 315 | // │─── │├┤ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ───│ 316 | // └─ ┴└ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ─┘ 317 | if (omen) { 318 | if (!_.isError(omen)) { throw new Error('Consistency violation: If provided, `omen` argument to parley should be a pre-existing omen (i.e. an Error instance). But instead, got: '+util.inspect(omen, {depth:2})+''); } 319 | π._omen = omen; 320 | } 321 | 322 | // ┌─┐┌┬┐┌┬┐┌─┐┌─┐┬ ┬ ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔═╗╔╦╗╔═╗╔═╗╔╦╗╔═╗╦═╗╔═╗═╗ ╦╔═╗╔═╗ 323 | // ├─┤ │ │ ├─┤│ ├─┤ ║║║║ ║ ║╣ ╠╦╝║ ║╣ ╠═╝ ║ ╠═╣╠╣ ║ ║╣ ╠╦╝║╣ ╔╩╦╝║╣ ║ 324 | // ┴ ┴ ┴ ┴ ┴ ┴└─┘┴ ┴ ╩╝╚╝ ╩ ╚═╝╩╚═╚═╝╚═╝╩ ╩ ╩ ╩╚ ╩ ╚═╝╩╚═╚═╝╩ ╚═╚═╝╚═╝ 325 | // ┌─ ┬┌─┐ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ─┐ 326 | // │─── │├┤ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ───│ 327 | // └─ ┴└ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ─┘ 328 | if (finalAfterExecLC) { 329 | if (!_.isFunction(finalAfterExecLC)) { throw new Error('Consistency violation: If provided, `finalAfterExecLC` argument to parley should be a handler function (see parley README for more information or visit https://sailsjs.com/support for help). But instead of that function, got: '+util.inspect(finalAfterExecLC, {depth:5})+''); } 330 | π._finalAfterExecLC = finalAfterExecLC; 331 | } 332 | 333 | // ██████╗ ███████╗████████╗██╗ ██╗██████╗ ███╗ ██╗ 334 | // ██╔══██╗██╔════╝╚══██╔══╝██║ ██║██╔══██╗████╗ ██║ 335 | // ██████╔╝█████╗ ██║ ██║ ██║██████╔╝██╔██╗ ██║ 336 | // ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██╗██║╚██╗██║ 337 | // ██║ ██║███████╗ ██║ ╚██████╔╝██║ ██║██║ ╚████║ 338 | // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ 339 | // 340 | // ██████╗ ███████╗███████╗███████╗██████╗ ██████╗ ███████╗██████╗ 341 | // ██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██╔══██╗ 342 | // ██║ ██║█████╗ █████╗ █████╗ ██████╔╝██████╔╝█████╗ ██║ ██║ 343 | // ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██║ ██║ 344 | // ██████╔╝███████╗██║ ███████╗██║ ██║██║ ██║███████╗██████╔╝ 345 | // ╚═════╝ ╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ 346 | // 347 | // Return deferred object 348 | return π; 349 | 350 | };//ƒ 351 | 352 | 353 | 354 | /** 355 | * parley.callable() 356 | * 357 | * Build & return a "parley callable", a simple asynchronous function which, 358 | * every time it is invoked, returns a Deferred object. 359 | * > This is a shortcut for building simple functions when you don't need the 360 | * > full customizability of calling parley() to build a Deferred for you on the 361 | * > fly (e.g. b/c as an implementor, you don't care about having custom 362 | * > chainable methods) 363 | * 364 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 365 | * @param {AsyncFunction} gracefullyHandleExec 366 | * @param {Number?} timeout 367 | * @param {Error?} omen 368 | * 369 | * @returns {Function} 370 | */ 371 | module.exports.callable = (gracefullyHandleExec, timeout, omen)=>{ 372 | 373 | if (!_.isFunction(gracefullyHandleExec) || gracefullyHandleExec.constructor.name !== 'AsyncFunction') { 374 | throw new Error('parley.callable() expects an async function (e.g. `async ()=>{}`) to be provided as the first argument. Instead, got: '+util.inspect(gracefullyHandleExec, {depth:5})); 375 | }//• 376 | 377 | // Build our "callable" 378 | // 379 | // Note that this function is _deliberately_ NOT an arrow function, so that 380 | // we can use `this` and `arguments` to simulate gracefullyHandleExec being 381 | // called exactly as-is. (In most cases, this should not matter. But 382 | // packages are sometimes used in mysterious, eclectic ways.) 383 | // 384 | // Also note that we name this function so that we can reference it below 385 | // when we build an omen. 386 | let parleyCallableFn = function (/*…*/) { 387 | let parley = module.exports; 388 | let newCallableFnArguments = arguments; 389 | let newCallableFnCtx = this;//« should really never matter, we just do it this way for consistency 390 | omen = omen || flaverr.omen(parleyCallableFn); 391 | return parley((done)=>{ 392 | gracefullyHandleExec.apply(newCallableFnCtx, newCallableFnArguments) 393 | .then((resultMaybe)=>{ 394 | done(undefined, resultMaybe); 395 | }) 396 | .catch((err)=>{ 397 | done(err); 398 | }); 399 | }, undefined, undefined, timeout, omen, undefined); 400 | };//ƒ 401 | 402 | // Return our new "callable" 403 | return parleyCallableFn; 404 | 405 | };//ƒ 406 | -------------------------------------------------------------------------------- /test/baseline.benchmark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var parley = require('../'); 6 | var benchSync = require('./utils/bench-sync.util'); 7 | var bench = require('./utils/bench.util'); 8 | 9 | if (process.env.NODE_ENV !== 'production') { 10 | throw new Error('Benchmarks should be run with NODE_ENV=production!'); 11 | } 12 | 13 | 14 | /** 15 | * baseline.benchmark.js 16 | * 17 | * A performance benchmark for Deferred instantiation and execution. 18 | */ 19 | 20 | describe('baseline.benchmark.js', function() { 21 | 22 | // Set "timeout" and "slow" thresholds incredibly high 23 | // to avoid running into issues. 24 | this.slow(240000); 25 | this.timeout(240000); 26 | 27 | before(function(){ 28 | console.log( 29 | ' • • • • • • \n'+ 30 | ' • • o \n'+ 31 | ' • b e n c h m a r k s • \n'+ 32 | ' • (instantiation) ° \n'+ 33 | '------------------------------------'+ 34 | ''); 35 | }); 36 | 37 | // ╔═╗╦═╗ ╦╔╦╗╦ ╦╦═╗╔═╗╔═╗ 38 | // ╠╣ ║╔╩╦╝ ║ ║ ║╠╦╝║╣ ╚═╗ 39 | // ╚ ╩╩ ╚═ ╩ ╚═╝╩╚═╚═╝╚═╝ 40 | // These functions and data are used in both benchmarks below. 41 | // (It's ok to have them up here--they are never mutated by the benchmarks or parley itself) 42 | var find = require('./fixtures/find.fixture'); 43 | var validate = require('./fixtures/validate.fixture'); 44 | var validateButWith9CustomMethods = require('./fixtures/validate-but-with-9-custom-methods.fixture'); 45 | var NINE_CUSTOM_METHODS = { 46 | pretend1: function(x,y){ this._i = this._i || 0; this._i++; console.log(this._i, x, y); return this; }, 47 | pretend2: function(x,y){ this._j = this._j || 0; this._j++; console.log(this._j, x, y); return this; }, 48 | pretend3: function(x,y){ this._k = this._k || 0; this._k++; console.log(this._k, x, y); return this; }, 49 | pretend4: function(x,y){ this._i = this._i || 0; this._i++; console.log(this._i, x, y); return this; }, 50 | pretend5: function(x,y){ this._j = this._j || 0; this._j++; console.log(this._j, x, y); return this; }, 51 | pretend6: function(x,y){ this._k = this._k || 0; this._k++; console.log(this._k, x, y); return this; }, 52 | pretend7: function(x,y){ this._i = this._i || 0; this._i++; console.log(this._i, x, y); return this; }, 53 | pretend8: function(x,y){ this._j = this._j || 0; this._j++; console.log(this._j, x, y); return this; }, 54 | pretend9: function(x,y){ this._k = this._k || 0; this._k++; console.log(this._k, x, y); return this; }, 55 | }; 56 | 57 | 58 | 59 | // ╔═╗╔╗╔╔═╗╔═╗╔═╗╦ ╦╔═╗╔╦╗ 60 | // ╚═╗║║║╠═╣╠═╝╚═╗╠═╣║ ║ ║ 61 | // ╚═╝╝╚╝╩ ╩╩ ╚═╝╩ ╩╚═╝ ╩ 62 | // Just some one-off snapshots run on a laptop. 63 | // For historical reports, see the history of this file on GitHub. 64 | // 65 | // ================================================================================================================ 66 | // For the latest report... 67 | // ================================================================================================================ 68 | // 69 | // * * * * * * * * * * 70 | // * See README.md! * 71 | // * * * * * * * * * * 72 | // 73 | // ================================================================================================================ 74 | 75 | // ================================================================================================================ 76 | // Jan 15, 2017 (take 6) 77 | // ================================================================================================================ 78 | // After implementing auto-custom-method-attaching stuff: 79 | // 80 | // baseline.benchmark.js 81 | // • • • • • • 82 | // • • o 83 | // • b e n c h m a r k s • 84 | // • (instantiation) ° 85 | // ------------------------------------ 86 | // parley(handler) 87 | // • just_build#0 x 16,889,956 ops/sec ±2.42% (79 runs sampled) 88 | // ✓ should be performant enough (using benchSync()) 89 | // parley(handler).exec(cb) 90 | // • build_AND_exec#0 x 1,612,188 ops/sec ±2.92% (80 runs sampled) 91 | // ✓ should be performant enough (using benchSync()) 92 | // practical benchmark 93 | // • mock "find()"#0 x 34.82 ops/sec ±1.23% (74 runs sampled) 94 | // ✓ should be performant enough when calling fake "find" w/ .exec() (using bench()) 95 | // • mock "find()"#0 x 34.68 ops/sec ±1.14% (74 runs sampled) 96 | // ✓ should be performant enough when calling NAKED fake "find" (using bench()) 97 | // • mock "validate()"#0 x 621,578 ops/sec ±1.66% (85 runs sampled) 98 | // ✓ should be performant enough when calling fake "validate" w/ .exec() (using benchSync()) 99 | // • mock "validate()"#0 x 7,467,393 ops/sec ±4.01% (86 runs sampled) 100 | // ✓ should be performant enough when calling NAKED "validate" (using benchSync()) 101 | // ------------------------------------ 102 | // • • • • • • 103 | // • • o 104 | // • < / b e n c h m a r k s > • 105 | // • ° 106 | // o° 107 | // 108 | // ================================================================================================================ 109 | // Jan 15, 2017 (take 5) 110 | // ================================================================================================================ 111 | // baseline.benchmark.js 112 | // • • • • • • 113 | // • • o 114 | // • b e n c h m a r k s • 115 | // • (instantiation) ° 116 | // ------------------------------------ 117 | // parley(handler) 118 | // • just_build#0 x 18,016,705 ops/sec ±1.35% (86 runs sampled) 119 | // ✓ should be performant enough (using benchSync()) 120 | // parley(handler).exec(cb) 121 | // • build_AND_exec#0 x 1,724,116 ops/sec ±1.95% (86 runs sampled) 122 | // ✓ should be performant enough (using benchSync()) 123 | // practical benchmark 124 | // • mock "find()"#0 x 34.01 ops/sec ±1.00% (73 runs sampled) 125 | // ✓ should be performant enough when calling fake "find" w/ .exec() (using bench()) 126 | // • mock "find()"#0 x 34.35 ops/sec ±1.06% (74 runs sampled) 127 | // ✓ should be performant enough when calling NAKED fake "find" (using bench()) 128 | // • mock "validate()"#0 x 542,632 ops/sec ±2.00% (85 runs sampled) 129 | // ✓ should be performant enough when calling fake "validate" w/ .exec() (using benchSync()) 130 | // • mock "validate()"#0 x 8,333,857 ops/sec ±5.42% (83 runs sampled) 131 | // ✓ should be performant enough when calling NAKED "validate" (using benchSync()) 132 | // ------------------------------------ 133 | // • • • • • • 134 | // • • o 135 | // • < / b e n c h m a r k s > • 136 | // • ° 137 | // o° 138 | // ================================================================================================================ 139 | 140 | 141 | // ================================================================================================================ 142 | // Dec 20, 2016 (take 4): (after removing pretty-print, BEFORE switching to the constructor approach) 143 | // ================================================================================================================ 144 | // baseline.benchmark.js 145 | // • • • • • • 146 | // • • o 147 | // • b e n c h m a r k s • 148 | // • ° 149 | // ------------------------------------ 150 | // parley(handler) 151 | // • just_build#0 x 527,939 ops/sec ±1.45% (85 runs sampled) 152 | // ✓ should be performant enough (using benchSync()) 153 | // parley(handler).exec(cb) 154 | // • build_AND_exec#0 x 420,899 ops/sec ±1.61% (85 runs sampled) 155 | // ✓ should be performant enough (using benchSync()) 156 | // practical benchmark 157 | // • mock "find()"#0 x 34.33 ops/sec ±0.90% (73 runs sampled) 158 | // ✓ should be performant enough when calling fake "find" w/ .exec() (using bench()) 159 | // • mock "find()"#0 x 34.20 ops/sec ±0.95% (74 runs sampled) 160 | // ✓ should be performant enough when calling NAKED fake "find" (using bench()) 161 | // • mock "validate()"#0 x 173,206 ops/sec ±3.02% (78 runs sampled) 162 | // ✓ should be performant enough when calling fake "validate" w/ .exec() (using benchSync()) 163 | // • mock "validate()"#0 x 5,805,213 ops/sec ±4.04% (87 runs sampled) 164 | // ✓ should be performant enough when calling NAKED "validate" (using benchSync()) 165 | // ------------------------------------ 166 | // • • • • • • 167 | // • • o 168 | // • < / b e n c h m a r k s > • 169 | // • ° 170 | // o° 171 | // ================================================================================================================ 172 | 173 | 174 | // ╔═╗╔╗ ╔═╗╔═╗╦═╗╦ ╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ 175 | // ║ ║╠╩╗╚═╗║╣ ╠╦╝╚╗╔╝╠═╣ ║ ║║ ║║║║╚═╗ 176 | // ╚═╝╚═╝╚═╝╚═╝╩╚═ ╚╝ ╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ 177 | // 178 | // • Removing pretty-print caused a huge performance increase 179 | // (33x instead of 317x slower than naked usage) 180 | // 181 | // • The additional time added by calling .exec() (vs. just building) is really only 182 | // visible now, AFTER removing pretty-print. It's a difference of 100,000 ops/sec. 183 | // 184 | // • By itself, switching to a Deferred constructor doesn't really improve performance 185 | // by THAT much. In some cases, it actually makes it worse (e.g. consistent decrease 186 | // in ops/sec for the first 2 benchmarks: just_build, build_and_exec). BUT it does 187 | // ever-so-slightly increase performance for both mock "find" mock "validate". 188 | // The question is: "why?" My guess is that it has something to do w/ accessing 189 | // `this` being slower than closure scope, and thus outweighing the gains of faster 190 | // construction. But even then, that wouldn't explain "just_build" being slower, so 191 | // it's probably not that... 192 | // 193 | // • Reducing the number of `this`/`self` references did not seem to make any sort of 194 | // meaningful difference on performance. (see 1d8b6239de2cd84ac76ee015d099c3c5a7013989) 195 | // *******UPDATE*******: 196 | // Actually -- it might... after removing two unncesssary `this` assignments from the 197 | // CONSTRUCTOR itself, performance for "just_build" shot up to where it was for the 198 | // original closure approach (and possibly a bit higher). Still, this is negligible 199 | // at the moment, but it's probably an effect that is more pronounced when overall ops/sec 200 | // are orders of magnitude higher (e.g. in the millions instead of the hundreds of thousands.) 201 | // Even then-- this is still less important than one might expect! 202 | // 203 | // • Aside: using a standalone function declaration (rather than invoking a self-calling function) 204 | // increases performance, like you might expect. Whether it's enough to matter is probably 205 | // situational. In the case of the commit where this observation was added to the code base, 206 | // it made a difference of ~1,000,000 ops/sec for the "NAKED mock validate" benchmark, and a 207 | // difference of ~20,000 ops/sec for the "validate w/ .exec()" benchmark. Worth it...? 208 | // No. Inline function declarations are NEVER worth it. But in some cases it might be worthwhile 209 | // to pull out shared futures used by self-invoking functions and drop them into a separate module. 210 | // *******UPDATE*******: 211 | // Just verified that, by moving the inline function to a separate file, performance for the 212 | // "NAKED mock validate" went up by an ADDITIONAL 2,000,000 ops/sec, and by an ADDITIONAL 213 | // ~20,000 ops/sec for the "validate w/ .exec()" benchmark. So, in conclusion, the answer to the 214 | // question of "Worth it?" is a resounding YES -- but only for a hot code path like this. For 215 | // other bits of code, the advantages of keeping the logic inline and avoiding a separate, 216 | // weirdly-specific file, are well worth it. And as for INLINE named function declarations? 217 | // They're still never worth it. Not only do they clutter the local scope and create scoffable 218 | // confusion about flow control (plus all the resulting bug potential), they aren't even as fast 219 | // as pulling out the code into a separate file. (Presumably this is because V8 has to make sure 220 | // the inline function can access the closure scope.) 221 | // 222 | // • It is worth noting that, despite how exciting the previous notes about pulling out self-invoking 223 | // functions was, when attempted with the mock "find" fixture, the relevant benchmarks showed no 224 | // noticeable improvement (i.e. because they're doing something asynchronous.) 225 | // 226 | // • Swapping out non-standard variable names (e.g. π) did not have any noticeable effect. 227 | // 228 | // • When using the constructor+prototype approach, accessing `this` is slow. It's not THAT bad, 229 | // but it is definitely a thing. Note that it is somewhat worse if in the constructor-- and 230 | // also worse on assignment (this.foo = x) than on access (var x = this.foo). 231 | // 232 | // • When using the closure approach, adding new methods dynamically is slow. This doesn't seem 233 | // to be because defining new functions is slow, per se. Rather it seems to have to do with 234 | // mutating the object after it's already been created. As a middle ground, it seems that relying 235 | // on Lodash's built-in optimizations is the way to go. Simply changing from `deferred.meta = ...` 236 | // to `_.extend(deferred, { meta: ... })` split the difference as far as performance. It improved 237 | // the performance of the 'mock validate with .exec()' benchmark by ~50k-60k ops/sec; i.e. ~20%) 238 | // 239 | // • STILL BE CAREFUL when using the closure approach. Even with the _.extend() trick, performance 240 | // decreases as more and more methods are added, whether or not they're within the same `.extend()` 241 | // call. BUT: What's still unclear is if this is due to function construction, or something else. 242 | // In this case, in practice, tricks would need to be used to circumvent the need for closure scope 243 | // access (i.e. prbly .bind()). But the answer to the question can actualy be figured out regardless-- 244 | // by defining stub functions once per process. 245 | // *******UPDATE*******: 246 | // Well, the answer is that the function construction must have mattered somewhat, but even after 247 | // pulling the entire dictionary of methods out (and pretending they're static), the performance is 248 | // still lower than when _.extend() is used to attach only ONE method-- even when that one method is 249 | // defined inline. So, at the end of the day, we're just going to have to deal with the fact that, 250 | // if we add methods to the Deferred dynamically and construction-time, it's going to be slower and 251 | // slower for every additional method we add. 252 | // 253 | // • _.each() is slower than `for`, sometimes by a factor of 10. But this only matters in extreme 254 | // circumstances, where the logic being benchmarked is already very fast to begin with. So in 255 | // almost every case, it's still never worth using a `for` loop instead of `_.each()`. 256 | // 257 | // • See Spring-Autumn 2017 commit history of the parley repo in general for more insights. 258 | 259 | 260 | // ╔═╗╦ ╦╦╔╦╗╔═╗ 261 | // ╚═╗║ ║║ ║ ║╣ 262 | // ╚═╝╚═╝╩ ╩ ╚═╝ 263 | describe('parley(handler)', function(){ 264 | it('should be performant enough (using benchSync())', function (){ 265 | benchSync('parley(handler)', [ 266 | 267 | function just_build(){ 268 | var deferred = parley(function(handlerCb) { return handlerCb(); }); 269 | } 270 | 271 | ]);// 272 | }); 273 | }); 274 | 275 | 276 | describe('parley(handler).exec(cb)', function(){ 277 | it('should be performant enough (using benchSync())', function (){ 278 | benchSync('parley(handler).exec(cb)', [ 279 | 280 | function build_AND_exec(){ 281 | var deferred = parley(function(handlerCb) { return handlerCb(); }); 282 | deferred.exec(function (err) { 283 | if (err) { 284 | console.error('Unexpected error running benchmark:',err); 285 | }//>- 286 | // Note: Since the handler is blocking, we actually make 287 | // it in here within one tick of the event loop. 288 | }); 289 | } 290 | 291 | ]);// 292 | }); 293 | 294 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 295 | // For additional permutations using bench() +/- extra setImmediate() calls, 296 | // see the commit history of this file. As it turn out, the setImmediate() 297 | // calls just add weight and make it harder to judge the accuracy of results. 298 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 299 | 300 | });// 312 | }); 313 | });// 314 | 315 | 316 | describe('parley(handler, undefined, {...}).exec(cb) (w/ 9 custom methods)', function(){ 317 | it('should be performant enough (using benchSync())', function (){ 318 | benchSync('parley(handler, undefined, {...}).exec(cb)', [ 319 | 320 | function build_AND_exec_with_9_custom_methods(){ 321 | var deferred = parley(function(handlerCb) { return handlerCb(); }, undefined, NINE_CUSTOM_METHODS); 322 | deferred.exec(function (err) { 323 | if (err) { 324 | console.error('Unexpected error running benchmark:',err); 325 | }//>- 326 | // Note: Since the handler is blocking, we actually make 327 | // it in here within one tick of the event loop. 328 | }); 329 | } 330 | 331 | ]);// 332 | }); 333 | });// 334 | 335 | 336 | describe('practical benchmark', function(){ 337 | 338 | var DOES_CURRENT_NODE_VERSION_SUPPORT_AWAIT = process.version.match(/^v8\./); 339 | 340 | if (DOES_CURRENT_NODE_VERSION_SUPPORT_AWAIT) { 341 | it('should be performant enough when calling fake "find" w/ `await` (using bench())', function (done){ 342 | bench('mock "await find()"', [ 343 | eval( 344 | '(()=>{\n'+ 345 | ' return async function (next){\n'+ 346 | ' var result;\n'+ 347 | ' try {\n'+ 348 | ' result = await find({ where: {id:3, x:30} });\n'+ 349 | ' } catch (err) {\n'+ 350 | ' return next(err);\n'+ 351 | ' }\n'+ 352 | ' return next();\n'+ 353 | ' }\n'+ 354 | '})()\n' 355 | ) 356 | ], done); 357 | }); 358 | } 359 | 360 | it('should be performant enough when calling fake "find" w/ .exec() (using bench())', function (done){ 361 | bench('mock "find().exec()"', [ 362 | 363 | function (next){ 364 | find({ where: {id:3, x:30} }) 365 | .exec(function (err, result) { 366 | if (err) { return next(err); } 367 | return next(); 368 | }); 369 | } 370 | 371 | ], done); 372 | }); 373 | 374 | it('should be performant enough when calling NAKED fake "find" (using bench())', function (done){ 375 | bench('mock "find(..., explicitCb)"', [ 376 | 377 | function (next){ 378 | find({ where: {id:3, x:30} }, function (err, result) { 379 | if (err) { return next(err); } 380 | return next(); 381 | }); 382 | } 383 | 384 | ], done); 385 | }); 386 | 387 | it('should be performant enough when calling fake "validate" w/ .exec() (using benchSync())', function (){ 388 | benchSync('mock "validate().exec()"', [ 389 | 390 | function (){ 391 | validate() 392 | .exec(function (err) { 393 | if (err) { 394 | console.error('Unexpected error running benchmark:',err); 395 | }//>- 396 | // Note: Since the handler is blocking, we actually make 397 | // it in here within one tick of the event loop. 398 | }); 399 | } 400 | 401 | ]); 402 | }); 403 | 404 | it('should be performant enough when calling fake "validate" w/ .exec() + uncaught exception handler (using benchSync())', function (){ 405 | benchSync('mock "validate().exec()"', [ 406 | 407 | function (){ 408 | validate() 409 | .exec(function (err) { 410 | if (err) { 411 | console.error('Unexpected error running benchmark:',err); 412 | }//>- 413 | // Note: Since the handler is blocking, we actually make 414 | // it in here within one tick of the event loop. 415 | }, function (){ 416 | console.error('Consistency violation: This should never happen: Something is broken!'); 417 | throw new Error('Consistency violation: This should never happen: Something is broken!'); 418 | }); 419 | } 420 | 421 | ]); 422 | }); 423 | 424 | it('should be performant enough calling fake "validateButWith9CustomMethods" w/ .exec() (using benchSync())', function (){ 425 | benchSync('mock "validateButWith9CustomMethods().exec()"', [ 426 | 427 | function (){ 428 | validateButWith9CustomMethods() 429 | .exec(function (err) { 430 | if (err) { 431 | console.error('Unexpected error running benchmark:',err); 432 | }//>- 433 | // Note: Since the handler is blocking, we actually make 434 | // it in here within one tick of the event loop. 435 | }); 436 | } 437 | 438 | ]); 439 | }); 440 | 441 | it('should be performant enough when calling NAKED "validate" (using benchSync())', function (){ 442 | benchSync('mock "validate(..., explicitCb)"', [ 443 | 444 | function (){ 445 | validate(function (err) { 446 | if (err) { 447 | console.error('Unexpected error running benchmark:',err); 448 | }//>- 449 | // Note: Since the handler is blocking, we actually make 450 | // it in here within one tick of the event loop. 451 | }); 452 | } 453 | 454 | ]); 455 | }); 456 | });// 457 | 458 | 459 | after(function(){ 460 | console.log( 461 | '------------------------------------\n'+ 462 | ' • • • • • • \n'+ 463 | ' • • o \n'+ 464 | ' • < / b e n c h m a r k s > • \n'+ 465 | ' • ° \n'+ 466 | ' o° \n'+ 467 | ''); 468 | }); 469 | 470 | });// 471 | -------------------------------------------------------------------------------- /test/practical.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var assert = require('assert'); 6 | var _ = require('@sailshq/lodash'); 7 | var find = require('./fixtures/find.fixture'); 8 | var validateButWith9CustomMethods = require('./fixtures/validate-but-with-9-custom-methods.fixture'); 9 | var findButWithTimeout = require('./fixtures/find-but-with-timeout.fixture'); 10 | var findButWithFinalAfterExecLC = require('./fixtures/find-but-with-final-after-exec-lc.fixture'); 11 | 12 | 13 | /** 14 | * practical.test.js 15 | * 16 | * A simple test verifiying a couple of real-world use cases. 17 | */ 18 | 19 | describe('practical.test.js', function() { 20 | 21 | // ██████╗ ██████╗ ███████╗████████╗███████╗███╗ ██╗██████╗ 22 | // ██╔══██╗██╔══██╗██╔════╝╚══██╔══╝██╔════╝████╗ ██║██╔══██╗ 23 | // ██████╔╝██████╔╝█████╗ ██║ █████╗ ██╔██╗ ██║██║ ██║ 24 | // ██╔═══╝ ██╔══██╗██╔══╝ ██║ ██╔══╝ ██║╚██╗██║██║ ██║ 25 | // ██║ ██║ ██║███████╗ ██║ ███████╗██║ ╚████║██████╔╝ 26 | // ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ 27 | // 28 | // ███████╗██╗███╗ ██╗██████╗ ██╗██╗ 29 | // ██╔════╝██║████╗ ██║██╔══██╗██╔╝╚██╗ 30 | // █████╗ ██║██╔██╗ ██║██║ ██║██║ ██║ 31 | // ██╔══╝ ██║██║╚██╗██║██║ ██║██║ ██║ 32 | // ██╗██║ ██║██║ ╚████║██████╔╝╚██╗██╔╝ 33 | // ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═╝╚═╝ 34 | // 35 | describe('calling a simplified mock of Waterline\'s `find()` model method', function(){ 36 | 37 | describe('simulated success', function(){ 38 | 39 | it('should work with explicit callback', function(done){ 40 | find(function (err, result) { 41 | if (err) { return done(err); } 42 | try { 43 | assert.deepEqual(result, [undefined]); 44 | } catch (e) { return done(e); } 45 | return done(); 46 | }); 47 | }); 48 | it('should work with 1st arg + explicit callback', function(done){ 49 | find({ where: {id:3} }, function (err, result) { 50 | if (err) { return done(err); } 51 | try { 52 | assert.deepEqual(result, [{ where:{id:3} }]); 53 | } catch (e) { return done(e); } 54 | return done(); 55 | }); 56 | }); 57 | 58 | it('should work with .exec()', function(done){ 59 | find().exec(function (err, result) { 60 | if (err) { return done(err); } 61 | try { 62 | assert.deepEqual(result, [undefined]); 63 | } catch (e) { return done(e); } 64 | return done(); 65 | }); 66 | }); 67 | it('should work with 1st arg + .exec()', function(done){ 68 | find({ where: {id:3} }).exec(function (err, result) { 69 | if (err) { return done(err); } 70 | try { 71 | assert.deepEqual(result, [{ where:{id:3} }]); 72 | } catch (e) { return done(e); } 73 | return done(); 74 | }); 75 | }); 76 | it('should work with .where() + .exec()', function(done){ 77 | find() 78 | .where({id:4}) 79 | .exec(function (err, result) { 80 | if (err) { return done(err); } 81 | try { 82 | assert.deepEqual(result, [{ where:{id:4} }]); 83 | } catch (e) { return done(e); } 84 | return done(); 85 | }); 86 | }); 87 | it('should work with 1st arg + .where() + .exec()', function(done){ 88 | find({ where: {id:3, x:30} }) 89 | .where({id:4}) 90 | .exec(function (err, result) { 91 | if (err) { return done(err); } 92 | try { 93 | assert.deepEqual(result, [{ where:{id:4} }]); 94 | } catch (e) { return done(e); } 95 | return done(); 96 | }); 97 | }); 98 | 99 | });// 100 | 101 | });// 102 | 103 | 104 | // ██╗ ██╗███████╗██╗███╗ ██╗ ██████╗ ██████╗██╗ ██╗███████╗████████╗ ██████╗ ███╗ ███╗ 105 | // ██║ ██║██╔════╝██║████╗ ██║██╔════╝ ██╔════╝██║ ██║██╔════╝╚══██╔══╝██╔═══██╗████╗ ████║ 106 | // ██║ ██║███████╗██║██╔██╗ ██║██║ ███╗ ██║ ██║ ██║███████╗ ██║ ██║ ██║██╔████╔██║ 107 | // ██║ ██║╚════██║██║██║╚██╗██║██║ ██║ ██║ ██║ ██║╚════██║ ██║ ██║ ██║██║╚██╔╝██║ 108 | // ╚██████╔╝███████║██║██║ ╚████║╚██████╔╝ ╚██████╗╚██████╔╝███████║ ██║ ╚██████╔╝██║ ╚═╝ ██║ 109 | // ╚═════╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ 110 | // 111 | // ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ███████╗ 112 | // ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██╔════╝ 113 | // ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║███████╗ 114 | // ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║╚════██║ 115 | // ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝███████║ 116 | // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ 117 | // 118 | describe('calling something that takes advantage of parley\'s built-in support for custom methods', function(){ 119 | 120 | it('should work with explicit callback', function(done){ 121 | validateButWith9CustomMethods(function (err, result) { 122 | if (err) { return done(err); } 123 | try { 124 | assert.strictEqual(result, undefined); 125 | } catch (e) { return done(e); } 126 | return done(); 127 | }); 128 | }); 129 | 130 | it('should work with .exec()', function(done){ 131 | validateButWith9CustomMethods().exec(function (err, result) { 132 | if (err) { return done(err); } 133 | try { 134 | assert.strictEqual(result, undefined); 135 | } catch (e) { return done(e); } 136 | return done(); 137 | }); 138 | }); 139 | 140 | it('should work with .then()', function(done){ 141 | validateButWith9CustomMethods() 142 | .then(function (result) { 143 | try { 144 | assert.strictEqual(result, undefined); 145 | } catch (e) { return done(e); } 146 | return done(); 147 | }).catch(function(err) { return done(err); }); 148 | }); 149 | 150 | it('should work with .b() + .exec()', function(done){ 151 | validateButWith9CustomMethods() 152 | .b({id:4}) 153 | .exec(function (err, result) { 154 | if (err) { return done(err); } 155 | try { 156 | assert.strictEqual(result, undefined); 157 | } catch (e) { return done(e); } 158 | return done(); 159 | }); 160 | }); 161 | 162 | it('should work with .b() + .then()', function(done){ 163 | validateButWith9CustomMethods() 164 | .b({id:4}) 165 | .then(function (result) { 166 | try { 167 | assert.strictEqual(result, undefined); 168 | } catch (e) { return done(e); } 169 | return done(); 170 | }).catch(function(err) { return done(err); }); 171 | }); 172 | 173 | });// 174 | 175 | 176 | 177 | 178 | // ██╗ ██╗███████╗██╗███╗ ██╗ ██████╗ 179 | // ██║ ██║██╔════╝██║████╗ ██║██╔════╝ 180 | // ██║ ██║███████╗██║██╔██╗ ██║██║ ███╗ 181 | // ██║ ██║╚════██║██║██║╚██╗██║██║ ██║ 182 | // ╚██████╔╝███████║██║██║ ╚████║╚██████╔╝ 183 | // ╚═════╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ 184 | // 185 | // ████████╗██╗███╗ ███╗███████╗ ██████╗ ██╗ ██╗████████╗ 186 | // ╚══██╔══╝██║████╗ ████║██╔════╝██╔═══██╗██║ ██║╚══██╔══╝ 187 | // ██║ ██║██╔████╔██║█████╗ ██║ ██║██║ ██║ ██║ 188 | // ██║ ██║██║╚██╔╝██║██╔══╝ ██║ ██║██║ ██║ ██║ 189 | // ██║ ██║██║ ╚═╝ ██║███████╗╚██████╔╝╚██████╔╝ ██║ 190 | // ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ 191 | // 192 | describe('calling something that takes advantage of parley\'s built-in support for timeouts', function(){ 193 | 194 | describe('in cases where timeout is explicitly unsupported', function(){ 195 | 196 | it('should NOT RESPECT TIMEOUT WHEN given an explicit callback', function(done){ 197 | findButWithTimeout(function (err, result) { 198 | if (err) { return done(err); } 199 | try { 200 | assert.deepEqual(result, [undefined]); 201 | } catch (e) { return done(e); } 202 | return done(); 203 | }); 204 | }); 205 | it('should NOT RESPECT TIMEOUT WHEN given 1st arg + explicit callback', function(done){ 206 | findButWithTimeout({ where: {id:3} }, function (err, result) { 207 | if (err) { return done(err); } 208 | try { 209 | assert.deepEqual(result, [{ where:{id:3} }]); 210 | } catch (e) { return done(e); } 211 | return done(); 212 | }); 213 | }); 214 | 215 | }); 216 | 217 | 218 | describe('in cases where timeout is supposed to work', function(){ 219 | 220 | it('should time out properly given .exec()', function(done){ 221 | findButWithTimeout().exec(function (err, result) { 222 | try { 223 | assert.equal(err.name, 'TimeoutError'); 224 | } catch (e) { return done(e); } 225 | return done(); 226 | }); 227 | }); 228 | it('should time out properly given 1st arg + .exec()', function(done){ 229 | findButWithTimeout({ where: {id:3} }).exec(function (err, result) { 230 | try { 231 | assert.equal(err.name, 'TimeoutError'); 232 | } catch (e) { return done(e); } 233 | return done(); 234 | }); 235 | }); 236 | it('should time out properly given .where() + .exec()', function(done){ 237 | findButWithTimeout() 238 | .where({id:4}) 239 | .exec(function (err, result) { 240 | try { 241 | assert.equal(err.name, 'TimeoutError'); 242 | } catch (e) { return done(e); } 243 | return done(); 244 | }); 245 | }); 246 | it('should time out properly given 1st arg + .where() + .exec()', function(done){ 247 | findButWithTimeout({ where: {id:3, x:30} }) 248 | .where({id:4}) 249 | .exec(function (err) { 250 | try { 251 | assert.equal(err.name, 'TimeoutError'); 252 | } catch (e) { return done(e); } 253 | return done(); 254 | }); 255 | }); 256 | 257 | }); 258 | 259 | 260 | 261 | });// 262 | 263 | 264 | // ██╗ ██╗███████╗██╗███╗ ██╗ ██████╗ ██╗ ██████╗ 265 | // ██║ ██║██╔════╝██║████╗ ██║██╔════╝ ██║ ██╔════╝██╗ 266 | // ██║ ██║███████╗██║██╔██╗ ██║██║ ███╗ ██║ ██║ ╚═╝ 267 | // ██║ ██║╚════██║██║██║╚██╗██║██║ ██║ ██║ ██║ ██╗ 268 | // ╚██████╔╝███████║██║██║ ╚████║╚██████╔╝ ███████╗╚██████╗╚═╝ 269 | // ╚═════╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ 270 | // 271 | // ██╗███╗ ██╗████████╗███████╗██████╗ ██████╗███████╗██████╗ ████████╗ █████╗ ███████╗████████╗███████╗██████╗ ███████╗██╗ ██╗███████╗ ██████╗ 272 | // ██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗██╔════╝╚██╗██╔╝██╔════╝██╔════╝ 273 | // ██║██╔██╗ ██║ ██║ █████╗ ██████╔╝██║ █████╗ ██████╔╝ ██║ ███████║█████╗ ██║ █████╗ ██████╔╝█████╗ ╚███╔╝ █████╗ ██║ 274 | // ██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗██║ ██╔══╝ ██╔═══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔══╝ ██╔══██╗██╔══╝ ██╔██╗ ██╔══╝ ██║ 275 | // ██║██║ ╚████║ ██║ ███████╗██║ ██║╚██████╗███████╗██║ ██║ ██║ ██║██║ ██║ ███████╗██║ ██║███████╗██╔╝ ██╗███████╗╚██████╗ 276 | // ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ 277 | // 278 | describe('calling something that takes advantage of interceptAfterExec', function(){ 279 | 280 | describe('in cases where interceptAfterExec is explicitly unsupported given an explicit callback with success (i.e. where the LC might normally modify the result/error if we were using .exec())', function(){ 281 | 282 | it('should NOT RESPECT LC WHEN given explicit callback as first arg', function(done){ 283 | findButWithFinalAfterExecLC(function (err, result) { 284 | if (err) { return done(err); } 285 | try { 286 | assert.deepEqual(result, [undefined]); 287 | } catch (e) { return done(e); } 288 | return done(); 289 | }); 290 | }); 291 | it('should NOT RESPECT LC WHEN given 1st arg + explicit callback', function(done){ 292 | findButWithFinalAfterExecLC({ where: {id:3} }, function (err, result) { 293 | if (err) { return done(err); } 294 | try { 295 | assert.deepEqual(result, [{ where:{id:3} }]); 296 | } catch (e) { return done(e); } 297 | return done(); 298 | }); 299 | }); 300 | 301 | }); 302 | 303 | 304 | describe('in cases where this is supposed to work', function(){ 305 | 306 | it('should work normally given .exec() with an error, where the LC is a pass-through', function(done){ 307 | findButWithFinalAfterExecLC(false).exec(function (err) { 308 | try { 309 | assert(_.isError(err), 'Expecting `err` to be an Error instance! But instead got: '+err); 310 | assert.equal(err.code, 'E_SOME_ERROR', 'Expected error with a `code` of "E_SOME_ERROR". But instead, got an error with a different code (`'+err.code+'`). Here\'s the error: '+err); 311 | } catch (e) { return done(e); } 312 | return done(); 313 | }); 314 | }); 315 | it('should work normally given .exec() with success, where the LC is a pass-through', function(done){ 316 | findButWithFinalAfterExecLC(true).exec(function (err, result) { 317 | try { 318 | assert(!err, 'Got unexpected error in test: '+err); 319 | assert(_.isArray(result), 'Expecting result to be an array! But instead got: '+result); 320 | assert.equal(result.length, 1); 321 | assert.equal(result[0], true); 322 | } catch (e) { return done(e); } 323 | return done(); 324 | }); 325 | }); 326 | it('should properly apply changes from LC given .exec() with an error', function(done){ 327 | findButWithFinalAfterExecLC(null).exec(function (err) { 328 | try { 329 | assert(_.isError(err), 'Expecting `err` to be an Error instance! But instead got: '+err); 330 | assert.equal(err.code, 'E_SOME_UNRECOGNIZED_ERROR', 'Expected error with a `code` of "E_SOME_UNRECOGNIZED_ERROR". But instead, got an error with a different code (`'+err.code+'`). Here\'s the error: '+err); 331 | } catch (e) { return done(e); } 332 | return done(); 333 | }); 334 | }); 335 | it('should properly apply changes from LC given .exec() with success', function(done){ 336 | findButWithFinalAfterExecLC().exec(function (err, result) { 337 | try { 338 | assert(!err, 'Got unexpected error in test: '+err); 339 | assert(_.isArray(result), 'Expecting result to be an array! But instead got: '+result); 340 | assert.equal(result.length, 2); 341 | assert.equal(result[0], undefined); 342 | assert.deepEqual(result[1], { fake: true }); 343 | } catch (e) { return done(e); } 344 | return done(); 345 | }); 346 | }); 347 | 348 | }); 349 | 350 | 351 | });// 352 | 353 | 354 | 355 | 356 | // ██╗███╗ ██╗████████╗███████╗██████╗ ██████╗███████╗██████╗ ████████╗ ██╗██╗ 357 | // ██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔╝╚██╗ 358 | // ██║██╔██╗ ██║ ██║ █████╗ ██████╔╝██║ █████╗ ██████╔╝ ██║ ██║ ██║ 359 | // ██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗██║ ██╔══╝ ██╔═══╝ ██║ ██║ ██║ 360 | // ██╗██║██║ ╚████║ ██║ ███████╗██║ ██║╚██████╗███████╗██║ ██║ ╚██╗██╔╝ 361 | // ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═╝╚═╝ 362 | // 363 | describe('calling find().intercept()', function(){ 364 | 365 | it('should ignore `.intercept()` if find() returns successfully', function(done){ 366 | var didRunIntercept; 367 | find() 368 | .intercept('E_SOME_ERROR', function(err){ 369 | didRunIntercept = true; 370 | return err; 371 | }) 372 | .exec(function (err, result) { 373 | if (err) { return done(err); } 374 | try { 375 | assert.deepEqual(result, [undefined]); 376 | assert(!didRunIntercept); 377 | } catch (e) { return done(e); } 378 | return done(); 379 | }); 380 | }); 381 | 382 | it('should ignore `.intercept()` if find() throws an unrelated exception', function(done){ 383 | var didRunIntercept; 384 | find(false) 385 | .intercept('E_SOME_OTHER_ERROR_THAT_WONT_BE_THROWN', function(err){ 386 | didRunIntercept = true; 387 | return err; 388 | }) 389 | .exec(function (err) { 390 | try { 391 | assert(_.isError(err), 'Expecting `err` to be an Error instance! But instead got: '+err); 392 | assert.equal(err.code, 'E_SOME_ERROR', 'Expected error with a `code` of "E_SOME_ERROR". But instead, got an error with a different code (`'+err.code+'`). Here\'s the error: '+err); 393 | assert(!didRunIntercept); 394 | } catch (e) { return done(e); } 395 | return done(); 396 | }); 397 | }); 398 | 399 | it('should run `.intercept()` if find() throws a matching exception, and the final error thrown should be whatever .intercept() returned', function(done){ 400 | var didRunIntercept; 401 | find(false) 402 | .intercept('E_SOME_ERROR', function(err){ 403 | didRunIntercept = true; 404 | var newErr = new Error('Some new error (original err:'+err+')'); 405 | newErr.code = 'E_MASHED_POTATOES'; 406 | return newErr; 407 | }) 408 | .exec(function (err) { 409 | try { 410 | assert(_.isError(err), 'Expecting `err` to be an Error instance! But instead got: '+err); 411 | assert.equal(err.code, 'E_MASHED_POTATOES', 'Expected error with a `code` of "E_MASHED_POTATOES", because it should have been intercepted and rethrown automatically using the return value from .intercept(). But instead, got an error with a different code (`'+err.code+'`). Here\'s the error: '+err); 412 | assert(didRunIntercept); 413 | } catch (e) { return done(e); } 414 | return done(); 415 | }); 416 | }); 417 | 418 | it('should pass in the proper error as the first and only argument to the LC', function(done){ 419 | var argReceivedInLC; 420 | find(false) 421 | .intercept('E_SOME_ERROR', function(err){ 422 | argReceivedInLC = err; 423 | var newErr = new Error('Some new error (original err:'+err+')'); 424 | newErr.code = 'E_MASHED_POTATOES'; 425 | return newErr; 426 | }) 427 | .exec(function () { 428 | try { 429 | assert(_.isError(argReceivedInLC), 'Expecting arg received in LC to be an Error instance! But instead got: '+argReceivedInLC); 430 | assert.equal(argReceivedInLC.code, 'E_SOME_ERROR', 'Expected error with a `code` of "E_SOME_ERROR". But instead, got an error with a different code (`'+argReceivedInLC.code+'`). Here\'s the error: '+argReceivedInLC); 431 | } catch (e) { return done(e); } 432 | return done(); 433 | }); 434 | }); 435 | 436 | it('should still call the final after exec LC from implementorland, if one was configured (and it should call it last, with the modifications from this LC already taken into account)', function(done){ 437 | findButWithFinalAfterExecLC(false) 438 | .intercept(function(err){ 439 | return new Error('Some other error (interecepted from original error: '+err.stack+')'); 440 | }) 441 | .exec(function (err) { 442 | try { 443 | assert(_.isError(err), 'Expecting `err` to be an Error instance! But instead got: '+err); 444 | assert.equal(err.code, 'E_SOME_UNRECOGNIZED_ERROR', 'Expected error with a `code` of "E_SOME_UNRECOGNIZED_ERROR". But instead, got an error with a different code (`'+err.code+'`). Here\'s the error: '+err); 445 | } catch (e) { return done(e); } 446 | return done(); 447 | }); 448 | }); 449 | 450 | });// 451 | 452 | // ████████╗ ██████╗ ██╗ ███████╗██████╗ █████╗ ████████╗███████╗ ██╗██╗ 453 | // ╚══██╔══╝██╔═══██╗██║ ██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝██╔╝╚██╗ 454 | // ██║ ██║ ██║██║ █████╗ ██████╔╝███████║ ██║ █████╗ ██║ ██║ 455 | // ██║ ██║ ██║██║ ██╔══╝ ██╔══██╗██╔══██║ ██║ ██╔══╝ ██║ ██║ 456 | // ██╗██║ ╚██████╔╝███████╗███████╗██║ ██║██║ ██║ ██║ ███████╗╚██╗██╔╝ 457 | // ╚═╝╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝╚═╝ 458 | // 459 | describe('calling .find().tolerate()', function(){ 460 | 461 | it('should ignore `.tolerate()` if find() returns successfully', function(done){ 462 | var didRunTolerate; 463 | find() 464 | .tolerate('E_SOME_ERROR', function(){ 465 | didRunTolerate = true; 466 | }) 467 | .exec(function (err, result) { 468 | if (err) { return done(err); } 469 | try { 470 | assert.deepEqual(result, [undefined]); 471 | assert(!didRunTolerate); 472 | } catch (e) { return done(e); } 473 | return done(); 474 | }); 475 | }); 476 | 477 | it('should ignore `.tolerate()` if find() throws an unrelated exception', function(done){ 478 | var didRunTolerate; 479 | find(false) 480 | .tolerate('E_SOME_OTHER_ERROR_THAT_WONT_BE_THROWN', function(){ 481 | didRunTolerate = true; 482 | }) 483 | .exec(function (err) { 484 | try { 485 | assert(_.isError(err), 'Expecting `err` to be an Error instance! But instead got: '+err); 486 | assert.equal(err.code, 'E_SOME_ERROR', 'Expected error with a `code` of "E_SOME_ERROR". But instead, got an error with a different code (`'+err.code+'`). Here\'s the error: '+err); 487 | assert(!didRunTolerate); 488 | } catch (e) { return done(e); } 489 | return done(); 490 | }); 491 | }); 492 | 493 | it('should run `.tolerate()` if find() throws a matching exception, and the final result should be whatever .tolerate() returned', function(done){ 494 | var didRunTolerate; 495 | find(false) 496 | .tolerate('E_SOME_ERROR', function(){ 497 | didRunTolerate = true; 498 | return 'mm mmm mashed potatoes'; 499 | }) 500 | .exec(function (err, result) { 501 | if (err) { return done(err); } 502 | try { 503 | assert(didRunTolerate); 504 | assert.deepEqual(result, 'mm mmm mashed potatoes'); 505 | } catch (e) { return done(e); } 506 | return done(); 507 | }); 508 | }); 509 | 510 | it('should honor `.tolerate()` even if it doesn\'t have a LC -- assuming find() throws a matching exception. (In this case, the final result should be `undefined`.)', function(done){ 511 | find(false) 512 | .tolerate('E_SOME_ERROR') 513 | .exec(function (err, result) { 514 | if (err) { return done(err); } 515 | try { 516 | assert.deepEqual(result, undefined); 517 | } catch (e) { return done(e); } 518 | return done(); 519 | }); 520 | }); 521 | 522 | it('should run `.tolerate()` even if it is completely generic (i.e. not filtered by any rule at all) -- assuming find() throws any kind of error. (In this case, the final result should be whatever the `.tolerate()` LC returned.)', function(done){ 523 | var didRunTolerate; 524 | find(false) 525 | .tolerate(function(){ 526 | didRunTolerate = true; 527 | return 'mm mmm mashed potatoes'; 528 | }) 529 | .exec(function (err, result) { 530 | if (err) { return done(err); } 531 | try { 532 | assert(didRunTolerate); 533 | assert.deepEqual(result, 'mm mmm mashed potatoes'); 534 | } catch (e) { return done(e); } 535 | return done(); 536 | }); 537 | }); 538 | 539 | it('should pass in the proper error as the first and only argument to the LC', function(done){ 540 | var argReceivedInLC; 541 | find(false) 542 | .tolerate(function(err){ 543 | argReceivedInLC = err; 544 | }) 545 | .exec(function () { 546 | try { 547 | assert(_.isError(argReceivedInLC), 'Expecting arg received in LC to be an Error instance! But instead got: '+argReceivedInLC); 548 | assert.equal(argReceivedInLC.code, 'E_SOME_ERROR', 'Expected error with a `code` of "E_SOME_ERROR". But instead, got an error with a different code (`'+argReceivedInLC.code+'`). Here\'s the error: '+argReceivedInLC); 549 | } catch (e) { return done(e); } 550 | return done(); 551 | }); 552 | }); 553 | 554 | it('should still call the final after exec LC from implementorland, if one was configured (and it should call it last, with the modifications from this LC already taken into account)', function(done){ 555 | findButWithFinalAfterExecLC(false) 556 | .tolerate(function(){ return ['how many', 'mashed potatoes', 'will it take??!']; }) 557 | .exec(function (err, result) { 558 | if (err) { return done(err); } 559 | try { 560 | assert.deepEqual(result, ['how many', 'mashed potatoes', 'will it take??!', {fake: true}]); 561 | } catch (e) { return done(e); } 562 | return done(); 563 | }); 564 | }); 565 | 566 | });// 567 | 568 | }); 569 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | parley 2 | ========= 3 | 4 | Practical, lightweight flow control for Node.js, with support for `await`, deferred execution, traditional Node callbacks, and promise chaining. 5 | 6 | > Powered by [bluebird](http://bluebirdjs.com/) 7 | 8 | ## Usage 9 | 10 | These days, there are several different common ways that developers call functions in Node.js and JavaScript. Parley helps _your code_ support all three of the mainstream flow control paradigms. 11 | 12 | Parley helps you write functions that can be called like this: 13 | 14 | ```javascript 15 | var result = await doStuff(123); 16 | ``` 17 | 18 | Or like this: 19 | 20 | ```javascript 21 | doStuff(123) 22 | .exec((err, result)=>{ 23 | 24 | }); 25 | ``` 26 | 27 | Or even like this: 28 | 29 | ```javascript 30 | doStuff(123) 31 | .then((result)=>{ 32 | 33 | }) 34 | .catch((err)=>{ 35 | 36 | }); 37 | ``` 38 | 39 | > parley functions return a Deferred. You can also obtain a promise by calling [`.toPromise()`](#toPromise). 40 | 41 | On top of the basics, parley makes it simple to implement timeouts (userland or implementorland), advanced error negotiation, improved stack traces (through omens), and retries (e.g. exponential backoff). 42 | 43 | > For more information on usage, keep reading below. 44 | 45 | ## About 46 | 47 | Parley is brought to you by [the team behind Sails.js](https://sailsjs.com/about), and used internally by the [Sails framework](https://sailsjs.com), [Waterline ORM](http://waterlinejs.org), the [node-machine project](http://node-machine.org), and more. 48 | 49 | 50 | ## Compatibility 51 | 52 | Parley supports Node 8 and up, with backwards-compatibility for Node 6 and Node 4. (But note that `await` is not supported by Node versions < 7.9.) 53 | 54 | 55 | ## Benchmarks 56 | 57 | As of July 3, 2017: 58 | 59 | ``` 60 | baseline.benchmark.js 61 | • • • • • • 62 | • • o 63 | • b e n c h m a r k s • 64 | • (instantiation) ° 65 | ------------------------------------ 66 | parley(handler) 67 | • just_build#0 x 28,097,782 ops/sec ±1.42% (90 runs sampled) 68 | ✓ should be performant enough (using benchSync()) 69 | parley(handler).exec(cb) 70 | • build_AND_exec#0 x 3,185,038 ops/sec ±1.53% (93 runs sampled) 71 | ✓ should be performant enough (using benchSync()) 72 | parley(handler, undefined, {...}) (w/ 9 custom methods) 73 | • just_build_with_9_custom_methods#0 x 4,274,101 ops/sec ±1.38% (89 runs sampled) 74 | ✓ should be performant enough (using benchSync()) 75 | parley(handler, undefined, {...}).exec(cb) (w/ 9 custom methods) 76 | • build_AND_exec_with_9_custom_methods#0 x 1,822,064 ops/sec ±1.24% (88 runs sampled) 77 | ✓ should be performant enough (using benchSync()) 78 | practical benchmark 79 | • mock "find().exec()"#0 x 34.61 ops/sec ±0.99% (78 runs sampled) 80 | ✓ should be performant enough when calling fake "find" w/ .exec() (using bench()) 81 | • mock "find(..., explicitCb)"#0 x 35.04 ops/sec ±1.11% (79 runs sampled) 82 | ✓ should be performant enough when calling NAKED fake "find" (using bench()) 83 | • mock "validate().exec()"#0 x 1,463,995 ops/sec ±1.03% (89 runs sampled) 84 | ✓ should be performant enough when calling fake "validate" w/ .exec() (using benchSync()) 85 | • mock "validate().exec()"#0 x 1,240,289 ops/sec ±2.69% (94 runs sampled) 86 | ✓ should be performant enough when calling fake "validate" w/ .exec() + uncaught exception handler (using benchSync()) 87 | • mock "validateButWith9CustomMethods().exec()"#0 x 1,030,355 ops/sec ±2.26% (96 runs sampled) 88 | ✓ should be performant enough calling fake "validateButWith9CustomMethods" w/ .exec() (using benchSync()) 89 | • mock "validate(..., explicitCb)"#0 x 9,696,815 ops/sec ±2.76% (88 runs sampled) 90 | ✓ should be performant enough when calling NAKED "validate" (using benchSync()) 91 | ------------------------------------ 92 | • • • • • • 93 | • • o 94 | • < / b e n c h m a r k s > • 95 | • ° 96 | o° 97 | ``` 98 | 99 | 100 | _Originally, back in January 15, 2017:_ 101 | 102 | ``` 103 | parley(handler) 104 | • just_build#0 x 18,162,364 ops/sec ±0.98% (90 runs sampled) 105 | ✓ should be performant enough (using benchSync()) 106 | parley(handler).exec(cb) 107 | • build_AND_exec#0 x 1,804,891 ops/sec ±1.77% (84 runs sampled) 108 | ✓ should be performant enough (using benchSync()) 109 | parley(handler, undefined, {...}) (w/ 9 custom methods) 110 | • just_build_with_9_custom_methods#0 x 3,947,502 ops/sec ±1.62% (90 runs sampled) 111 | ✓ should be performant enough (using benchSync()) 112 | parley(handler, undefined, {...}).exec(cb) (w/ 9 custom methods) 113 | • build_AND_exec_with_9_custom_methods#0 x 1,259,925 ops/sec ±2.08% (76 runs sampled) 114 | ✓ should be performant enough (using benchSync()) 115 | practical benchmark 116 | • mock "find().exec()"#0 x 33.69 ops/sec ±0.98% (73 runs sampled) 117 | ✓ should be performant enough when calling fake "find" w/ .exec() (using bench()) 118 | • mock "find(..., explicitCb)"#0 x 33.93 ops/sec ±0.90% (73 runs sampled) 119 | ✓ should be performant enough when calling NAKED fake "find" (using bench()) 120 | • mock "validate().exec()"#0 x 789,446 ops/sec ±1.85% (92 runs sampled) 121 | ✓ should be performant enough when calling fake "validate" w/ .exec() (using benchSync()) 122 | • mock "validateButWith9CustomMethods().exec()"#0 x 686,544 ops/sec ±1.21% (90 runs sampled) 123 | ✓ should be performant enough calling fake "validateButWith9CustomMethods" w/ .exec() (using benchSync()) 124 | • mock "validate(..., explicitCb)"#0 x 10,157,027 ops/sec ±1.77% (87 runs sampled) 125 | ✓ should be performant enough when calling NAKED "validate" (using benchSync()) 126 | ``` 127 | 128 | 129 | ## Help 130 | 131 | If you have questions or are having trouble, click [here](http://sailsjs.com/support). 132 | 133 | If you're in a hurry to use a _parley-enabled API in practice_, it might help to check out a couple of real-world examples: 134 | • [.find()](https://github.com/balderdashy/sails-docs/blob/f4858b0d3c6bb80bc130060ecdd428e735ec111e/reference/waterline/models/find.md) _(in Sails.js / Waterline ORM)_ 135 | • [`.build()`](https://github.com/node-machine/machine/tree/c65d6430d72fa93c794f0f80344665028b94cb0c#callables) _(in `machine`)_ 136 | 137 | If you're interested in learning more about this approach to async flow control in general, or considering using parley to support `await`, promises, and traditional Node callbacks _for your own functions_, then keep reading-- there's a whole lot more for you below. 138 | 139 | ## Bugs   [![NPM version](https://badge.fury.io/js/parley.svg)](http://npmjs.com/package/parley) 140 | 141 | To report a bug, [click here](http://sailsjs.com/bugs). 142 | 143 | 144 | 145 | ## Overview 146 | 147 | This section offers a high-level look at how to use parley from both a userland and implementor perspective. You can also skip ahead to the [API reference below](#api-reference). 148 | 149 | 150 | ### Building a deferred object 151 | 152 | Use parley to build a **deferred object**. This provides access to `.exec()`, `.then()`, `.catch()`, and `.toPromise()`, but you can also attach any extra methods you'd like to add. (There are also a few extra methods like `.log()` provided automatically as syntactic sugar-- more on that below.) 153 | 154 | ```javascript 155 | var parley = require('parley'); 156 | 157 | var deferred = parley((done)=>{ 158 | setTimeout(()=>{ 159 | if (Math.random() > 0.5) { 160 | return done(new Error('whoops, unlucky I guess')); 161 | } 162 | if (Math.random() > 0.2) { 163 | return done(undefined, Math.floor(5*Math.random())); 164 | } 165 | return done(); 166 | }, 50); 167 | }); 168 | ``` 169 | 170 | > For a more complete version of the above example, [click here](https://gist.github.com/mikermcneil/621b55cfc54f133a1db30d7238ca52b1). 171 | 172 | 173 | ### Results 174 | 175 | To send back a result value from your handler, specify it as the second argument when invoking `done`. 176 | 177 | ```javascript 178 | return done(undefined, 'hello world'); 179 | ``` 180 | 181 | Depending on how userland code chooses to work with the deferred object, your result will be passed back to userland as either the return value, the second argument to the `.exec()` callback, or as the value resolved from the promise. 182 | 183 | ```javascript 184 | // Recommended approach (available in Node.js >= v7.9) 185 | var result = await yourFn(); 186 | ``` 187 | 188 | ```javascript 189 | // traditional Node-style callback 190 | .exec((err, result)=>{ 191 | // => undefined, 'hello world' 192 | }); 193 | 194 | // or legacy promise chaining 195 | .then((result)=>{ 196 | // => 'hello world' 197 | }); 198 | ``` 199 | 200 | 201 | ### Errors 202 | 203 | To send back an error from your handler, handle it in the conventional Node.js way. 204 | 205 | ```javascript 206 | return done(new Error('Oops')); 207 | ``` 208 | 209 | Depending on how userland code chooses to work with the deferred object, your error will be passed back to userland as either the first argument to the `.exec()` callback, or as the promise's rejection "reason". 210 | 211 | ```javascript 212 | // Recommended approach (available in Node.js >= v7.9) 213 | var result; 214 | try { 215 | result = await yourFn(); 216 | } catch (err) { 217 | // => [Error: oops] 218 | } 219 | ``` 220 | 221 | ```javascript 222 | // traditional Node-style callback 223 | .exec((err, result)=>{ 224 | // => [Error: oops], undefined 225 | }); 226 | 227 | // or legacy promise-chaining 228 | .catch((err)=>{ 229 | // => [Error: oops] 230 | }); 231 | ``` 232 | 233 | #### Custom exceptions 234 | 235 | Sometimes, there is one or more "exceptional" exit a function might take, which are fundamentally different than other generic errors that might occur-- for example, consider the difference between a "someone with that username already exists" exception and a bug resulting from a typo, missing dependency, etc. 236 | 237 | To make it possible for userland code to negotiate different exits from your function, give your error a `code` property. 238 | 239 | ```javascript 240 | var x = Math.random(); 241 | 242 | // Miscellaneous error (no code) 243 | if (x > 1) { 244 | return done(new Error('Consistency violation: This should never happen.')); 245 | } 246 | 247 | var flaverr = require('flaverr'); 248 | // Other recognized exceptions 249 | if (x > 0.6) { 250 | return done(flaverr('E_TOO_BIG', new Error('Oops: too big'))); 251 | } 252 | if (x < 0.4) { 253 | return done(flaverr('E_TOO_SMALL', new Error('Too small -- probably already in use!'))) 254 | } 255 | ``` 256 | 257 | #### Negotiating errors 258 | 259 | The aforementioned approach makes it easy to negotiate errors in userland. Whether the userland code is using `await`, a Node-style callback, or promise-chaining, the underlying approach is conceptually the same regardless. 260 | 261 | ```javascript 262 | // Recommended approach (available in Node.js >= v7.9) 263 | var result; 264 | try { 265 | result = await yourFn(); 266 | } catch (err) { 267 | switch(err.code) { 268 | case 'E_TOO_BIG': return res.status(400).json({ reason: 'Ooh, too bad! '+err.message }); 269 | case 'E_TOO_SMALL': return res.status(401).json({ reason: 'Please try again later. '+err.message }); 270 | default: 271 | console.error('Unexpected error:',err.stack); 272 | return res.sendStatus(500); 273 | } 274 | } 275 | 276 | // … 277 | ``` 278 | 279 | 280 | ```javascript 281 | // traditional Node-style callback 282 | .exec(function(err, result) { 283 | if (err) { 284 | switch(err.code) { 285 | case 'E_TOO_BIG': return res.status(400).json({ reason: 'Ooh, too bad! '+err.message }); 286 | case 'E_TOO_SMALL': return res.status(401).json({ reason: 'Please try again later. '+err.message }); 287 | default: 288 | console.error('Unexpected error:',err.stack); 289 | return res.sendStatus(500); 290 | } 291 | }//-• 292 | 293 | // ... 294 | }); 295 | ``` 296 | 297 | ```Javascript 298 | // or legacy promise-chaining 299 | .then(function (result) { 300 | // ... 301 | }) 302 | .catch({ code: 'E_TOO_BIG' }, function(err) { 303 | return res.status(400).json({ reason: 'Ooh, too bad! '+err.message }); 304 | }) 305 | .catch({ code: 'E_TOO_SMALL' }, function(err) { 306 | return res.status(401).json({ reason: 'Please try again later. '+err.message }); 307 | }) 308 | .catch(function(err) { 309 | console.error('Unexpected error:',err.stack); 310 | return res.sendStatus(500); 311 | }); 312 | ``` 313 | 314 | 315 | #### Handling uncaught exceptions 316 | 317 | For a long time, uncaught exceptions were the skeletons in JavaScript's closet. That's because, out of the box, when using asynchronous callbacks in Node.js, _if the code in your callback throws an uncaught error, the process **will crash!**_ 318 | 319 | For example, the following code would crash the process: 320 | 321 | ```javascript 322 | setTimeout(function (){ 323 | 324 | // Since this string can't be parsed as JSON, this will throw an error. 325 | // And since we aren't using try...catch, it will crash the process. 326 | JSON.parse('who0ps"thisis totally not valid js{}n'); 327 | 328 | //… 329 | 330 | }, 50); 331 | ``` 332 | 333 | This behavior leads to stability issues, wasted dev hours, security vulnerabilities, extreme susceptibility to denial-of-service attacks, weeping, crying, moaning, therapist appointments and much wailing and gnashing of teeth. 334 | 335 | **But if you're using Node >= v7.9, you're in luck. [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) solves _all_ of these problems.** 336 | 337 | > If you're new to Node, congratulations! You're getting started at _the best possible time_. It's never been faster, easier, and more secure to build apps with JavaScript. 338 | > 339 | > And for those of us that have been using Node.js for years, these are incredibly exciting times to be a Node.js developer. As a community, we've finally conquered one of Node's biggest challenges and it's often-quoted only remaining hurdle to adoption: "callback hell". The callbacks are dead. Long live `await`! 340 | 341 | #### What if I'm stuck with an old version of Node.js? 342 | 343 | Well, then buckle up. 344 | 345 | To protect against the problems mentioned above, you'll need to always be sure to use try...catch blocks around any logic 346 | that might throw in an asynchronous, Node-style callback. 347 | 348 | For example: 349 | 350 | ```javascript 351 | setTimeout(function (){ 352 | 353 | try { 354 | JSON.parse('who0ps"thisis totally not valid js{}n'); 355 | } catch (e) { return res.serverError(e); } 356 | 357 | //… 358 | 359 | }, 50); 360 | ``` 361 | 362 | Here are a few common use cases to watch out for: 363 | + basic JavaScript errors; e.g. syntax issues, or trying to use the dot (.) operator on `null`. 364 | + trying to JSON.parse() some data that is not a valid, parseable JSON string 365 | + trying to JSON.stringify() a circular object 366 | + RPS methods in Sails.js; e.g. `.publish()`, `.subscribe()`, `.unsubscribe()` 367 | + Waterline's `.validate()` model method 368 | + Node core's `assert()` 369 | + most synchronous methods from Node core (e.g. `fs.readFileSync()`) 370 | + any synchronous machine called with `.execSync()` 371 | + other synchronous functions from 3rd party libraries 372 | 373 | 374 | _Note that this is not an issue when using promises, since `.then()` automatically catches uncaught errors 375 | (although there are other considerations when using promises-- for instance, forgetting to use .catch() 376 | each time .then() is used is a common source of hard-to-debug issues, technical debt, and memory leaks.)_ 377 | 378 | 379 | > **EXPERIMENTAL:** As of parley 2.3.x, there is a new, experimental feature that allows you to 380 | > easily provide an extra layer of protection: an optional 2nd argument to `.exec()`. If specified, 381 | > this function will be used as an uncaught exception handler-- a simple fallback just in case something 382 | > happens to go wrong in your callback function. 383 | > 384 | > This allows you to safely write code like the following without crashing the server: 385 | > 386 | > ```javascript 387 | > User.create({ username: 'foo' }).exec(function (err, result) { 388 | > if (err) { 389 | > if (err.code === 'E_UNIQUE') { return res.badRequest('Username already in use.'); } 390 | > else { return res.serverError(err); } 391 | > } 392 | > 393 | > var thisWillNeverWork = JSON.parse('who0ps"thisis totally not valid js{}n'); 394 | > 395 | > return res.json(result); 396 | > 397 | > }, res.serverError); 398 | > ``` 399 | > 400 | > Of course, it's still best to be explicit about error handling whenever possible. 401 | > The extra layer of protection is just that-- it's here to help prevent issues 402 | > stemming from the myriad runtime edge cases it's almost impossible to anticipate 403 | > when building a production-ready web application. 404 | > 405 | > The best choice is to not use `.exec()` at all. Use `await`. 406 | 407 | 408 | #### Tolerating errors 409 | 410 | Sometimes, you just don't care. 411 | 412 | ```javascript 413 | var result = await sails.helpers.fs.readJson('./package.json') 414 | .tolerate('notFound', ()=>{ 415 | return { 416 | name: 'not-a-real-package', 417 | description: 'This is not a real package, and I don\'t care.' 418 | }; 419 | }); 420 | 421 | // Now `result` is either the contents of the real package.json file... or our fake stuff. 422 | ``` 423 | 424 | 425 | #### Catching and rethrowing errors 426 | 427 | But sometimes, you care a little _too_ much. 428 | 429 | ```javascript 430 | var result = await sails.helpers.fs.readJson('./package.json') 431 | .intercept('notFound', (err)=>{ 432 | return flaverr({ 433 | message: 'No package.json file could be found in the current working directory. And I care _very_ much.', 434 | code: 'E_WHERE_IS_MY_PACKAGE_JSON' 435 | }, err); 436 | }); 437 | 438 | // If the package.json file doesn't exist, we will have now thrown a much more useful error. 439 | ``` 440 | 441 | 442 | 443 | ### Flow control 444 | 445 | If you're using Node >= v7.9, you're in luck. With `await`, flow control works just like it does in any other language (with one exception: parallel processing/races. More on that below.) 446 | 447 | 448 | ##### What if I'm stuck with an old version of Node.js? 449 | 450 | Sorry to hear that... Once again, `await` solves all of these problems. It's the biggest boon to JavaScript development since Node.js was released. You'll wish you could use it. 451 | 452 | But don't worry- this section is for you. Since Node.js is asynchronous, when using Node < v7.9, seemingly-tricky flow control problems often arise in practical, userland code. Fortunately, they're solveable when equipped with the proper tools and strategies. 453 | 454 | > Most of the examples below use simple Node callbacks, but note that many similar affordances are available for promise-chaining -- for example, check out `.toPromise()` ([below](#toPromise)) and `Promise.all()` (in bluebird, or native in ES6, etc.). The concepts are more or less the same regardless. 455 | > 456 | > _Unless you and the rest of your team are experts with legacy promise-chaining and already have tight, consistently-applied and agreed-upon conventions for how to implement the use cases below, you're probably best off using Node callbacks. Of course, the **best and safest** strategy is to do neither, and instead to take advantage of `await`._ 457 | 458 | #### Async loops 459 | 460 | Using Node >= v7.9? You can just do a `for` loop. 461 | 462 | ```javascript 463 | var results = []; 464 | for (let letter of ['a','b','c','d','e','f','g','h','i','j','k','l']) { 465 | results.push(await doStuff(letter)); 466 | } 467 | return res.json(results); 468 | ``` 469 | 470 | But otherwise... 471 | 472 | 473 | ##### What if I'm stuck with an old version of Node.js? 474 | 475 | Loop over many asynchronous things, one at a time, using `async.eachSeries()`. 476 | 477 | > For this example, make sure you have access to the [`async` library](http://npmjs.com/package/async): 478 | > 479 | >```javascript 480 | >var async = require('async'); 481 | >``` 482 | 483 | ```javascript 484 | var results = []; 485 | async.eachSeries(['a','b','c','d','e','f','g','h','i','j','k','l'], function (letter, next) { 486 | doStuff(letter).exec(function (err, resultForThisLetter){ 487 | if (err) { return next(err); } 488 | results.push(resultForThisLetter) 489 | return next(); 490 | }); 491 | }, 492 | // ~∞%° 493 | function afterwards(err) { 494 | if (err) { 495 | console.error(err); 496 | return res.sendStatus(500); 497 | } 498 | return res.json(results); 499 | }); 500 | ``` 501 | 502 | #### Async "if" 503 | 504 | Using Node >= v7.9? You can just do an `if` statement. 505 | 506 | ```javascript 507 | var profileUser = await User.findOne({ id: req.param('id') }); 508 | if (!profileUser) { return res.notFound(); } 509 | 510 | var loggedInUser; 511 | if (req.session.userId) { 512 | loggedInUser = await User.findOne({ id: req.session.userId }); 513 | } 514 | 515 | return res.view('profile', { 516 | profile: _.omit(profileUser, ['password', 'email']), 517 | me: loggedInUser ? _.omit(loggedInUser, 'password') : {} 518 | }); 519 | ``` 520 | 521 | But otherwise... 522 | 523 | ##### What if I'm stuck with an old version of Node.js? 524 | 525 | Even simple detours and conditionals can sometimes be tricky when things get asynchronous. 526 | 527 | Fortunately, relatively concise and robust branching logic can be easily implemented using out-of-the-box JavaScript using this weird trick™. 528 | 529 | ```javascript 530 | User.findOne({ id: req.param('id') }) 531 | .exec(function(err, profileUser) { 532 | if (err) { return res.serverError(err); } 533 | if (!profileUser) { return res.notFound(); } 534 | 535 | // If the request came from a logged in user, 536 | // then fetch that user's record from the database. 537 | (function(proceed) { 538 | if (!req.session.userId) { 539 | return proceed(); 540 | } 541 | User.findOne({ id: req.session.userId }) 542 | .exec(function (err, loggedInUser) { 543 | if (err) { return proceed(err); } 544 | if (!loggedInUser) { return proceed(new Error('Logged-in user ('+req.session.userId+') is missing from the db!')); } 545 | return proceed(undefined, loggedInUser); 546 | }); 547 | 548 | // ~∞%° 549 | })(function afterwards(err, loggedInUser){ 550 | if (err) { return res.serverError(err); } 551 | 552 | return res.view('profile', { 553 | profile: _.omit(profileUser, ['password', 'email']), 554 | me: loggedInUser ? _.omit(loggedInUser, 'password') : {} 555 | }); 556 | 557 | }); 558 | }); 559 | ``` 560 | 561 | > [More background on using the if/then/finally pattern for asynchronous flow control](https://gist.github.com/mikermcneil/32391da94cbf212611933fabe88486e3) 562 | 563 | 564 | #### Async recursion 565 | 566 | Using Node >= v7.9? Recursion is never exactly "fun and easy" (IMO) but with `await`, you can do recursion just like you would with normal, synchronous code (like any other programming language). 567 | 568 | In the example below, we demonstrate that, but also take advantage of `.tolerate()` for error negotiation instead of using a try/catch block: 569 | 570 | ```javascript 571 | #!/usr/bin/env node 572 | 573 | var path = require('path'); 574 | 575 | // Starting from the current working directory, ascend upwards 576 | // looking for a package.json file. (Keep looking until we hit an error; if so, throw it.) 577 | // Otherwise, if there was no error, we found it! 578 | var nearestPJ = await (async function _recursively(thisDir){ 579 | var pathToCheck = path.resolve(thisDir, 'package.json'); 580 | return await sails.helpers.fs.exists(pathToCheck) 581 | .tolerate('doesNotExist', async (unusedErr)=>{ 582 | return await _recursively(path.dirname(thisDir)); 583 | }) || pathToCheck; 584 | })(process.cwd()); 585 | 586 | console.log('Found nearest package.json file at:',nearestPJ); 587 | ``` 588 | 589 | 590 | 591 | But otherwise... 592 | 593 | ##### What if I'm stuck with an old version of Node.js? 594 | 595 | Much like "if/then/finally" above, the secret to tidy asynchronous recursion is the (notorious) self-calling function. 596 | 597 | ```javascript 598 | #!/usr/bin/env node 599 | 600 | var path = require('path'); 601 | var fs = require('fs'); 602 | 603 | // Starting from the current working directory, ascend upwards 604 | // looking for a package.json file. (Keep looking until we hit an error.) 605 | (function _recursively(thisDir, done){ 606 | 607 | var pathToCheck = path.resolve(thisDir, './package.json'); 608 | fs.stat(pathToCheck, function(err) { 609 | if (err) { 610 | switch (err.code) { 611 | 612 | // Not found -- so keep going. 613 | case 'ENOENT': 614 | var oneLvlUp = path.dirname(thisDir); 615 | _recursively(oneLvlUp, function(err, nearestPJ) { 616 | if (err) { return done(err); } 617 | return done(undefined, nearestPJ); 618 | }); 619 | return; 620 | 621 | // Misc. error 622 | default: return done(err); 623 | } 624 | }//-• 625 | 626 | // Otherwise, found it! 627 | return done(undefined, pathToCheck); 628 | 629 | });// 630 | 631 | // ~∞%° 632 | })(process.cwd(), function afterwards(err, nearestPJ) { 633 | if (err) { 634 | console.error(err); 635 | return process.exit(1); 636 | } 637 | 638 | console.log('Found nearest package.json file at:',nearestPJ); 639 | 640 | }); 641 | ``` 642 | 643 | > [More examples and thoughts on asynchronous recursion](https://gist.github.com/mikermcneil/225198a46317050af1f772296f67e6ce) 644 | 645 | 646 | 647 | #### Parallel processing / "races" 648 | 649 | Sometimes, for performance reasons, it's convenient to do more than one thing at the same time. In many languages, this can be tricky. But in JavaScript (and thus, in Node.js), this kind of optimization is supported right out of the box. 650 | 651 | However, note that this _is_ one area where you can't just use `await`-- you'll need to use either callbacks or promise chaining. 652 | 653 | > **When should I optimize my code with parallel processing?** 654 | > 655 | > It's never worth optimizing until you've hit an _actual_ bottleneck, usually as far as per-user latency (or more rarely, as far as overall scalability). It's never worth inheriting the complexity of parallel processing until you're 100% sure your performance issue is related to "having to wait for one thing to finish before the next thing can start". If you're having performance issues for other reasons (e.g. slow SQL queries, slow 3rd party APIs, or a lack of indexes in your Mongo database), this won't help you at all, and like most forms of premature optimization, it'll just make your app more bug-prone, more complicated to understand, and harder to optimize in the future if _real_ performance issues arise. 656 | > 657 | > That said, if you actually need the performance boost from parallel processing, you're in luck: when Node.js puts its mind to it, the engine can be incredibly fast. 658 | 659 | To manage "races" between deferred objects while still performing tasks simultaneously, you can use `async.each()` -- for example, here's the `async.eachSeries()` code from above again, but optimized to run on groups of letters simultaneously, while still processing letters within those groups in sequential order: 660 | 661 | ```javascript 662 | var results = []; 663 | async.each(['abc','def','ghi','jkl'], function (group, next) { 664 | 665 | var theseLetters = group.split(''); 666 | var resultsForThisGroup = []; 667 | async.eachSeries(theseLetters, function (letter, next) { 668 | doStuff(letter).exec(function (err, resultForThisLetter){ 669 | if (err) { return next(err); } 670 | resultsForThisGroup.push(resultForThisLetter) 671 | return next(); 672 | }); 673 | },// ~∞%° 674 | function (err) { 675 | if (err) { return next(err); } 676 | 677 | resultsForThisGroup.forEach(function(letter){ 678 | results.push(letter); 679 | }); 680 | 681 | return next(); 682 | }); 683 | 684 | },// ~∞%° 685 | function afterwards(err) { 686 | if (err) { 687 | console.error(err); 688 | return res.sendStatus(500); 689 | } 690 | return res.json(results); 691 | }); 692 | ``` 693 | 694 | > [More background on asynchronous vs. synchronous flow control in general](https://gist.github.com/mikermcneil/755a2ae7cc62d9a59656ab3ba9076cc1) 695 | 696 | 697 | 698 | 699 | ## API reference 700 | 701 | ### Implementor interface 702 | 703 | #### parley() 704 | 705 | Build and return a deferred object. 706 | 707 | As its first argument, expects a function (often called the handler, or more specifically "handleExec") that will run whenever userland code executes the deferred object (e.g. with `await`, `.exec()`, or `.then()`). 708 | 709 | ```javascript 710 | var deferred = parley(function (done) { 711 | // • If something goes wrong, call `done(new Error('something went wrong'))` 712 | // • If everything worked out, and you want to send a result back, call `done(undefined, result);` 713 | // • Otherwise, if everything worked out but no result is necessary, simply call: 714 | return done(); 715 | }); 716 | ``` 717 | 718 | This first argument is mandatory-- it defines what your implementation _actually does_ when the `await` or `.exec()` is triggered. 719 | 720 | ##### Optional callback 721 | There is also an optional second argument you can use: another function that, if provided, will cause your handler (the first arg) to run _immediately_. 722 | 723 | This provides a simple, optimized shortcut for exposing an optional callback to your users. 724 | 725 | > Why bother? Well, for one thing, it's stylistically a good idea to give users a way to call your handler with as little sugar on top as possible. More rarely, for very performance-sensitive applications, direct callback usage does provide a mild performance benefit. 726 | 727 | ```javascript 728 | var deferred = parley(function (done){ 729 | // ... 730 | }, optionalCbFromUserland); 731 | 732 | // Note: if an optional cb was provided from userland, then parley **will not return anything**. 733 | // In other words: 734 | if (optionalCbFromUserland) { 735 | assert(deferred === undefined); 736 | } 737 | ``` 738 | 739 | ##### Custom methods 740 | The safest way to attach custom methods is by using parley's optional 3rd argument. The usual approach is for these custom methods to be chainable (i.e. return `this`). 741 | 742 | ```javascript 743 | var privateMetadata = {}; 744 | 745 | var deferred = parley(function (done){ 746 | // ... 747 | }, optionalCbFromUserland, { 748 | someCustomMethod: function(a,b,c){ 749 | privateMetadata = privateMetadata || {}; 750 | privateMetadata.foo = privateMetadata.foo || 1; 751 | privateMetadata.foo++; 752 | return deferred; 753 | } 754 | }); 755 | ``` 756 | 757 | > Don't use this approach to define non-functions or overrides with special meaning (e.g. `inspect`, `toString`, or `toJSON`). 758 | > To do that, just set the property directly-- for example: 759 | > ```javascript 760 | > deferred.inspect = function(){ return '[My cool deferred!]'; }; 761 | > ``` 762 | 763 | 764 | #### Stack traces 765 | 766 | When building asynchronous functions, you're likely to encounter issues with unhelpful stack traces. There's no _easy_ solution to this problem per se, but over the years, our team has developed a decent approach to solving this problem. It involves using temporary Error instances known colloquially as "omens": 767 | 768 | ```javascript 769 | const flaverr = require('flaverr'); 770 | 771 | // Take a snapshot of the stack trace BEFORE doing anything asynchronous. 772 | var omen = flaverr.omen(); 773 | 774 | var deferred = parley((done)=>{ 775 | 776 | // Wait for up to 8 seconds. 777 | var msToWait = 8 * Math.floor(Math.random()*1000); 778 | setTimeout(()=>{ 779 | if (Math.random() > 0.5) { 780 | // Use our "omen" (stack trace snapshot) to "flavor" our actual error. 781 | return done(flaverr({ 782 | code: 'E_LUCK_RAN_OUT', 783 | message: 'Too bad, your luck ran out!' 784 | }, omen)); 785 | } 786 | else { 787 | return done(); 788 | } 789 | }, msToWait); 790 | 791 | }, optionalCbFromUserland, { 792 | someCustomMethod: (a,b,c)=>{ 793 | privateMetadata = privateMetadata || {}; 794 | privateMetadata.foo = privateMetadata.foo || 1; 795 | privateMetadata.foo++; 796 | return deferred; 797 | } 798 | }, undefined, omen); 799 | ``` 800 | 801 | Now, when your function gets called, if there's an error, the developer who wrote the relevant code will get an excellent stack trace. 802 | 803 | 804 | #### Timeouts 805 | 806 | Sometimes, it's helpful to automatically time out if execution takes too long. In parley, Sails, Waterline, and the node-machine project, this is supported right out of the box. 807 | 808 | For instance, in the code from the previous example above, the execution of our little function might take anywhere from one or two milliseconds all the way up to 8 entire seconds. But what if we wanted it to time out after only 2 seconds? For that, we can use the fourth argument to parley: the timeout. 809 | 810 | This should be a number, expressed in milliseconds: 811 | 812 | ```javascript 813 | // … same code as the example above … 814 | 815 | // This time, with a timeout of 2 seconds (2000 milliseconds): 816 | var deferred = parley((done)=>{ 817 | // … same code as the example above … 818 | }, optionalCbFromUserland, { 819 | … custom methods here … 820 | }, 2000); 821 | ``` 822 | 823 | If the timeout is exceeded, an error is triggered, and any subsequent calls to `done` from your provided custom implementation are ignored. 824 | 825 | #### Improving stack traces of built-in errors 826 | 827 | For important modules that impact many developers (or for authors that really care about the sanity of their users, and who want to make it easier to debug their code), it is sometimes useful to go so far as improving the stack trace of _even parley's built-in errors_ such as timeouts. For this, simply use the 5th argument: the "omen". 828 | 829 | To stick with our running example: 830 | 831 | ```javascript 832 | // … same code as the example above … 833 | 834 | // Take a snapshot of the stack trace BEFORE doing anything asynchronous. 835 | var omen = new Error('omen'); 836 | 837 | var deferred = parley((done)=>{ 838 | // … same code as the example above … 839 | }, optionalCbFromUserland, { 840 | … custom methods here … 841 | }, 2000, omen); 842 | ``` 843 | 844 | 845 | 846 | 847 | ### Userland interface 848 | 849 | The deferred object returned by `parley()` exposes a few different methods. 850 | 851 | #### .exec() 852 | 853 | ```javascript 854 | parley(function(done){ return done(undefined, 1+1); }) 855 | .exec(function (err, result) { 856 | // => undefined, 2 857 | }); 858 | ``` 859 | 860 | ```javascript 861 | parley(function(done){ return done(new Error('whoops'), 1+1); }) 862 | .exec(function (err, result) { 863 | // => [Error: whoops], undefined 864 | }); 865 | ``` 866 | 867 | #### .then() 868 | 869 | ```javascript 870 | parley(function(done){ return done(undefined, 1+1); }) 871 | .then(function (result) { 872 | // => 2 873 | }); 874 | ``` 875 | 876 | #### .catch() 877 | 878 | ```javascript 879 | parley(function(done){ return done(new Error('whoops'), 1+1); }) 880 | .catch(function (err) { 881 | // => [Error: whoops] 882 | }); 883 | ``` 884 | 885 | #### .toPromise() 886 | 887 | ```javascript 888 | var promise1 = parley(function(done){ return done(undefined, 1+1); }).toPromise(); 889 | var promise2 = parley(function(done){ setTimeout(function(){ return done(); }, 10); }).toPromise(); 890 | 891 | Promise.all([ 892 | promise1, 893 | promise2 894 | ]) 895 | .then(function(result){ 896 | // => [2, undefined] 897 | }).catch(function (err) { 898 | 899 | }); 900 | ``` 901 | 902 | 903 | #### Other methods 904 | 905 | Implementors may also choose to attach other methods to the deferred object (e.g. `.where()`). See "Custom methods" above for more information. 906 | 907 | 908 | ## Contributing   [![Master Branch Build Status](https://travis-ci.org/mikermcneil/parley.svg?branch=master)](https://travis-ci.org/mikermcneil/parley)   [![Master Branch Build Status (Windows)](https://ci.appveyor.com/api/projects/status/tdu70ax32iymvyq3?svg=true)](https://ci.appveyor.com/project/mikermcneil/parley) 909 | 910 | Please observe the guidelines and conventions laid out in the [Sails project contribution guide](http://sailsjs.com/documentation/contributing) when opening issues or submitting pull requests. 911 | 912 | [![NPM](https://nodei.co/npm/parley.png?downloads=true)](http://npmjs.com/package/parley) 913 | 914 | ## Pronunciation 915 | 916 | [`/ˈpärlē/`](http://www.macmillandictionary.com/us/pronunciation/american/parley) 917 | 918 | > _Rather than picking barley and getting snarly, she decided to `npm install parley` and listen to some Bob Marley._ 919 | 920 | ## License 921 | 922 | This package, like the [Sails framework](http://sailsjs.com), is free and open-source under the [MIT License](http://sailsjs.com/license). 923 | 924 | 925 | -------------------------------------------------------------------------------- /lib/private/Deferred.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'); 6 | var _ = require('@sailshq/lodash'); 7 | var bluebird = require('bluebird'); 8 | var flaverr = require('flaverr'); 9 | 10 | // Optimization: Pull process env check up here. 11 | var IS_DEBUG_OR_NON_PRODUCTION_ENV = ( 12 | process.env.NODE_ENV !== 'production' || 13 | process.env.DEBUG 14 | ); 15 | 16 | 17 | /** 18 | * Deferred (constructor) 19 | * 20 | * The Deferred constructor is exported by this module (see bottom of file). 21 | * 22 | * ``` 23 | * var deferred = new Deferred(function(done){ 24 | * // ... 25 | * return done(); 26 | * }); 27 | * ``` 28 | * 29 | * > Why use a constructor instead of just creating this object dynamically? 30 | * > This is purely for performance, since this is a rather hot code path. 31 | * > For details, see "OBSERVATIONS" in `tests/baseline.benchmark.js`. 32 | * 33 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 34 | * @param {Function} handleExec [your function that should be run] 35 | * @param {Function} done [Node-style callback that your function should invoke when finished] 36 | * @param {Error?} err 37 | * @param {Ref?} resultMaybe 38 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 39 | * @constructs {Deferred} 40 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 41 | * @api private 42 | * PLEASE DO NOT REQUIRE AND USE THIS CONSTRUCTOR EXCEPT WITHIN THIS PACKAGE! 43 | * Instead, use `require('parley')` with the documented usage. If you're 44 | * trying to do this because you want to do an instanceof check, e.g. in 45 | * a test, consider checking something like `_.isFunction(foo.exec)` and/or 46 | * `foo.constructor.name === 'Deferred'`. 47 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 48 | */ 49 | function Deferred(handleExec){ 50 | // assert(_.isFunction(handleExec), 'Should always provide a function as the 1st argument when constructing a `Deferred`'); 51 | // assert.strictEqual(arguments.length, 1, 'Should always provide exactly 1 argument when constructing a `Deferred`'); 52 | 53 | // Attach the provided `handleExec`function so it can be called from inside the 54 | // `.exec()` instance method below. 55 | this._handleExec = handleExec; 56 | 57 | // Notes about our instance variables: 58 | // 59 | // • We'll track the provided `handleExec` function as `this._handleExec`, 60 | // so that we have a way to call it when the time comes. (See above.) 61 | // 62 | // • We'll use `this._hasBegunExecuting` below to track whether we have 63 | // begun executing this Deferred yet. This is mainly for spinlocks. 64 | // 65 | // • We'll use `this._execCountdown` for our .exec() warning countdown. 66 | // (This is the logic that helps you remember to type `await`.) 67 | // 68 | // • We'll use another instance variable below, `this._hasFinishedExecuting`, 69 | // to track whether this Deferred has _finished_ yet. This is mainly to 70 | // improve error & warning messages. 71 | 72 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 73 | // FUTURE: Investigate the performance impact of using Object.defineProperty() 74 | // for this and all other built-in instance variables (e.g. the spinlock-related 75 | // flags.) Also maybe for things like `toString()` and `inspect()`. 76 | // 77 | // This is really just as a nicety. The reason it was so slow before was likely 78 | // due to the fact that it got run on every invocation, whereas now (at least in 79 | // some cases, like for `inspect()`) it could be implemented so that it runs exactly 80 | // once per process. 81 | // 82 | // Why bother? Just because it makes Deferred instances nicer to look at. 83 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 84 | 85 | } 86 | 87 | 88 | /** 89 | * .exec() 90 | * 91 | * @param {Function} _cb 92 | * The Node-style callback to invoke when the parley-wrapped implementation is finished. 93 | * 94 | * @param {Function} handleUncaughtException 95 | * If specified, this function will be used as a handler for uncaught exceptions 96 | * thrown from within `_cb`. **But REMEMBER: this will not handle uncaught exceptions 97 | * from any OTHER asynchronous callbacks which might happen to be used within `_cb`.** 98 | * (It's the same sort of function you might pass into `.catch()`.) 99 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 100 | * ^^FUTURE: deprecate, then remove support for this 2nd arg 101 | * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 102 | * 103 | * ------------------------------------------------------------------------------------------ 104 | * Example usage: 105 | * 106 | * ``` 107 | * User.create({ username: 'foo' }).exec(function (err, result) { 108 | * if (err) { 109 | * if (err.code === 'E_UNIQUE') { return res.badRequest('Username already in use.'); } 110 | * else { return res.serverError(err); } 111 | * } 112 | * 113 | * return res.ok(); 114 | * 115 | * }, res.serverError); 116 | * ``` 117 | */ 118 | Deferred.prototype.exec = function(_cb, handleUncaughtException){ 119 | 120 | // Since thar be closure scope below, a hazard for young `this`s, we define `self`. 121 | var self = this; 122 | 123 | 124 | if (_cb === undefined) { 125 | throw flaverr({ 126 | name: 127 | 'UsageError', 128 | message: 129 | 'No callback supplied. Please provide a callback function when calling .exec().\n'+ 130 | ' [?] See https://sailsjs.com/support for help.' 131 | }, self._omen); 132 | }//-• 133 | 134 | if (!_.isFunction(_cb)) { 135 | if (!_.isArray(_cb) && _.isObject(_cb)) { 136 | throw flaverr({ 137 | name: 138 | 'UsageError', 139 | message: 140 | 'Sorry, `.exec()` doesn\'t know how to handle {...} callbacks.\n'+ 141 | 'Please provide a callback function when calling .exec().\n'+ 142 | '| If you passed in {...} on purpose as a "switchback" (dictionary of callbacks),\n'+ 143 | '| then try calling .switch() intead of .exec().\n'+ 144 | ' [?] See https://sailsjs.com/support for more help.' 145 | }, self._omen); 146 | } 147 | else { 148 | throw flaverr({ 149 | name: 150 | 'UsageError', 151 | message: 152 | 'Sorry, `.exec()` doesn\'t know how to handle a callback like that:\n'+ 153 | util.inspect(_cb, {depth: 1})+'\n'+ 154 | '\n'+ 155 | 'Instead, please provide a callback function when calling .exec().\n'+ 156 | ' [?] See https://sailsjs.com/support for help.' 157 | }, self._omen); 158 | } 159 | }//-• 160 | 161 | // Check usage of 2nd argument to .exec(). 162 | if (handleUncaughtException !== undefined) { 163 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 164 | // FUTURE: Consider adding a deprecation notice to this behavior, and then remove support 165 | // altogether. (This isn't really as necessary anymore now that ES8 async/await is widely 166 | // available on the server, and should be available in the browser... soonish?) 167 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 168 | if (!_.isFunction(handleUncaughtException)) { 169 | throw flaverr({ 170 | message: 171 | 'Sorry, `.exec()` doesn\'t know how to handle an uncaught exception handler like that:\n'+ 172 | util.inspect(handleUncaughtException, {depth: 1})+'\n'+ 173 | 'If provided, the 2nd argument to .exec() should be a function like `function(err){...}`\n'+ 174 | '(This function will be used as a failsafe in case the callback throws an uncaught error.)\n'+ 175 | ' [?] See https://sailsjs.com/support for help.' 176 | }, self._omen); 177 | }//• 178 | 179 | // Impose arbitrary restriction against an unsupported edge case. 180 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 181 | // FUTURE: maybe remove this restriction. But also... might not even be relevant, since 182 | // we'll probably get rid of support for this usage (see above) 183 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 184 | if (self._finalAfterExecLC){ 185 | throw new Error('Consistency violation: Currently, the 2nd argument to .exec() may not be used, since this Deferred was built with a custom `finalAfterExecLC` handler. Please avoid using the 2nd argument to .exec() and use ES8 async/await instead, if possible.'); 186 | }//• 187 | 188 | }//fi 189 | 190 | 191 | // Userland spinlock 192 | if (self._hasBegunExecuting) { 193 | console.warn( 194 | '\n'+ 195 | 'That\'s odd... It looks like this Deferred '+ 196 | 'has already '+(self._hasTimedOut?'timed out':self._hasFinishedExecuting?'finished executing':'begun executing')+'.\n'+ 197 | 'But attempting to execute a Deferred more than once tends to cause\n'+ 198 | 'unexpected race conditions and other bugs! So to be safe, rather than\n'+ 199 | 'executing it twice, the additional attempt was ignored automatically, and\n'+ 200 | 'this warning was logged instead. [?] See https://sailsjs.com/support for help.\n'+ 201 | (self._omen?( 202 | '\n'+ 203 | 'Stack trace:\n'+ 204 | '```\n'+ 205 | flaverr.getBareTrace(self._omen)+'\n'+ 206 | '```\n' 207 | ):'') 208 | ); 209 | return; 210 | }//-• 211 | self._hasBegunExecuting = true; 212 | 213 | // Clear countdown, if appropriate. 214 | if (IS_DEBUG_OR_NON_PRODUCTION_ENV) { 215 | clearTimeout(this._execCountdown); 216 | } 217 | 218 | // Before proceeding to execute the function, set up a `setTimeout` that will fire 219 | // when the runtime duration exceeds the configured timeout. 220 | // > If configured timeout is falsey or <0, then we ignore it. 221 | // > Also note that we include a worst-case-scenario spinlock here (but it should never fire!) 222 | var timeoutAlarm; 223 | if (self._timeout && self._timeout > 0) { 224 | timeoutAlarm = setTimeout(()=>{ 225 | if (self._hasFinishedExecuting) { 226 | console.warn( 227 | 'WARNING: Consistency violation: Trying to trigger timeout, but execution has\n'+ 228 | 'already finished! This should not be possible, and the fact that you\'re seeing\n'+ 229 | 'this message indicates that there is probably a bug somewhere in the tools -- or\n'+ 230 | 'possibly that this Deferred instance has been mutated by userland code.\n'+ 231 | 'Please report this at https://sailsjs.com/bugs or https://sailsjs.com/support.\n'+ 232 | '(silently ignoring it this time...)\n'+ 233 | (self._omen?( 234 | '\n'+ 235 | 'Stack trace:\n'+ 236 | '```\n'+ 237 | flaverr.getBareTrace(self._omen)+'\n'+ 238 | '```\n' 239 | ):'') 240 | ); 241 | return; 242 | }//• 243 | if (self._hasTimedOut) { 244 | console.warn( 245 | 'WARNING: Consistency violation: Trying to trigger timeout again after it has already\n'+ 246 | 'been triggered! This should not be possible, and the fact that you\'re seeing\n'+ 247 | 'this message indicates that there is probably a bug somewhere in the tools -- or\n'+ 248 | 'possibly that this Deferred instance has been mutated by userland code.\n'+ 249 | 'Please report this at https://sailsjs.com/bugs or https://sailsjs.com/support.\n'+ 250 | '(silently ignoring it this time...)\n'+ 251 | (self._omen?( 252 | '\n'+ 253 | 'Stack trace:\n'+ 254 | '```\n'+ 255 | flaverr.getBareTrace(self._omen)+'\n'+ 256 | '```\n' 257 | ):'') 258 | ); 259 | return; 260 | }//• 261 | self._hasTimedOut = true; 262 | var err = flaverr({ 263 | name: 264 | 'TimeoutError', 265 | traceRef: 266 | self, 267 | message: 268 | 'Took too long to finish executing (timeout of '+self._timeout+'ms exceeded.)\n'+ 269 | 'There is probably an issue in the implementation (might have forgotten to call `exits.success()`, etc.)\n'+ 270 | 'If you are the implementor of the relevant logic, and you\'re sure there are no problems, then you\'ll\n'+ 271 | 'want to (re)configure the timeout (maximum number of milliseconds to wait for the asynchronous logic to\n'+ 272 | 'finish). Otherwise, you can set the timeout to 0 to disable it.\n'+ 273 | ' [?] See https://sailsjs.com/support for help.', 274 | }, self._omen); 275 | return proceedToInterceptsAndChecks(err, undefined, undefined, self, _cb, handleUncaughtException); 276 | 277 | }, self._timeout);// _∏_ (invoking `setTimeout()`) 278 | }//>- 279 | 280 | 281 | // Trigger the executioner function. 282 | // > Note that we always wrap the executioner in a `try` block to prevent common issues from 283 | // > uncaught exceptions, at least within the tick. 284 | try { 285 | 286 | self._handleExec(function( /*…*/ ){ 287 | // > Note that we don't use .slice() on the `arguments` keyword -- this is for perf. 288 | // > (see https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#what-is-safe-arguments-usage) 289 | var errCbArg; 290 | var resultCbArg; 291 | var extraCbArgs; 292 | if (arguments.length > 2) { 293 | errCbArg = arguments[0]; 294 | resultCbArg = arguments[1]; 295 | extraCbArgs = Array.prototype.slice.call(arguments, 2); 296 | } else if (arguments.length > 1) { 297 | errCbArg = arguments[0]; 298 | resultCbArg = arguments[1]; 299 | } else if (arguments.length > 0) { 300 | errCbArg = arguments[0]; 301 | } 302 | 303 | proceedToAfterExecSpinlocks(errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException, timeoutAlarm); 304 | });//_∏_ 305 | 306 | } catch (e) {// Handle errors thrown synchronously by the `_handleExec` implementation: 307 | 308 | // Check to make sure this error isn't a special "escape hatch" from 309 | // the edge case where an error was thrown from within the userland callback 310 | // provided to .exec() -- specifically in the case where the handleExec logic 311 | // is synchronous (i.e. non-blocking- triggering its `done` within 1 tick.) 312 | if (e && e.name === 'Envelope' && e.code === 'E_ESCAPE_HATCH' && e.traceRef === self) { 313 | throw e.raw; 314 | }//• 315 | 316 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 317 | // NOTE: The following code is ALMOST exactly the same as the code above. 318 | // It's duplicated in place rather than extrapolating purely for performance, 319 | // and since the error messages vary a bit: 320 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 321 | if (self._hasFinishedExecuting) { 322 | console.warn( 323 | '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'+ 324 | 'WARNING: Something seems to be wrong with this function.\n'+ 325 | 'It threw an unhandled error during the first tick of its\n'+ 326 | 'execution... but before doing so, it somehow managed to\n'+ 327 | 'have already resolved/rejected once.\n'+ 328 | '(silently ignoring this...)\n'+ 329 | (self._omen?( 330 | '\n'+ 331 | 'To assist you in hunting this down, here is a stack trace:\n'+ 332 | '```\n'+ 333 | flaverr.getBareTrace(self._omen)+'\n'+ 334 | '```\n'+ 335 | '\n' 336 | ):'')+ 337 | ' [?] For more help, visit https://sailsjs.com/support\n'+ 338 | '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' 339 | ); 340 | return; 341 | }//-• 342 | 343 | if (self._hasTimedOut) { 344 | console.warn( 345 | 'WARNING: Consistency violation: This function threw an unhandled error during the\n'+ 346 | 'first tick of its execution... but before doing so, it somehow managed to\n'+ 347 | 'have already triggered the timeout. This should not be possible, and the fact that\n'+ 348 | 'you\'re seeing this message indicates that there is probably a bug somewhere in the\n'+ 349 | 'tools -- or possibly that this Deferred instance has been mutated by userland code.\n'+ 350 | 'Please report this at https://sailsjs.com/bugs or https://sailsjs.com/support.\n'+ 351 | '(silently ignoring it this time...)\n'+ 352 | (self._omen?( 353 | '\n'+ 354 | 'To assist you in hunting this down, here is a stack trace:\n'+ 355 | '```\n'+ 356 | flaverr.getBareTrace(self._omen)+'\n'+ 357 | '```\n'+ 358 | '\n' 359 | ):'') 360 | ); 361 | self._hasFinishedExecuting = true; 362 | return; 363 | }//-• 364 | 365 | if (timeoutAlarm) { 366 | clearTimeout(timeoutAlarm); 367 | }//>- 368 | 369 | // Ensure we end up with an Error instance. 370 | var err = flaverr.parseOrBuildError(e, self._omen); 371 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 372 | // ^ FUTURE: Better error message for non-Errors? 373 | // (See impl of parseOrBuildError() in flaverr for more context.) 374 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 375 | 376 | self._hasFinishedExecuting = true; 377 | 378 | return proceedToInterceptsAndChecks(err, undefined, undefined, self, _cb, handleUncaughtException); 379 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 380 | // FUTURE: Consider using the more detailed explanation for ALL 4 (!!) of the cases 381 | // handled by `.parseOrBuildError()` -- and use omens for them too...even the Error instances! 382 | // > see other "FUTURE" block above to read about the other stuff we'd want to do first-- 383 | // > and also note that we'd almost certainly want to leave out the bit about "available 384 | // > keys on `self`" bit (that's really just leftover from debugging) 385 | // ``` 386 | // return proceedToInterceptsAndChecks(flaverr({ 387 | // message: 388 | // 'Unexpected error was thrown while executing '+ 389 | // 'this Deferred:\n'+ 390 | // '```\n'+ 391 | // flaverr.getBareTrace(self._omen)+'\n'+ 392 | // '```\n'+ 393 | // 'Also, here are the available keys on `self` at this point:\n'+ 394 | // '```\n'+ 395 | // _.keys(self)+'\n'+ 396 | // '```' 397 | // }, self._omen), undefined, undefined, self, _cb, handleUncaughtException); 398 | // ``` 399 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 400 | 401 | }// 402 | 403 | // Use `self._hasAlreadyWaitedAtLeastOneTick` to track whether or not this 404 | // Deferred's logic is holistically asynchronous. 405 | if (self._hasFinishedExecuting && !self._hasStartedButNotFinishedAfterExecLC) { 406 | // IWMIH then we've already finished running `handleExec`, and we know 407 | // it must have been composed purely of blocking (i.e. synchronous) logic. 408 | // We also know there isn't an actively-running asynchronous afterExec LC 409 | // gumming up the works. 410 | self._hasAlreadyWaitedAtLeastOneTick = false; 411 | } 412 | else { 413 | // Otherwise, IWMIH we know that the callback hasn't been called yet-- meaning 414 | // that we're likely dealing with some non-blocking (i.e. asynchronous) logic. 415 | // (Or possibly a bug where the callback isn't getting called -_-) 416 | // OR possibly a long-running, asynchronous afterExec LC that hasn't finished 417 | // yet. 418 | self._hasAlreadyWaitedAtLeastOneTick = true; 419 | } 420 | 421 | }; 422 | 423 | 424 | /** 425 | * .then() 426 | * 427 | * For usage, see: 428 | * http://bluebirdjs.com/docs/api/then.html 429 | */ 430 | Deferred.prototype.then = function (){ 431 | var promise = this.toPromise(); 432 | // > Note that we don't use .slice() -- this is for perf. 433 | // > (see https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#what-is-safe-arguments-usage) 434 | return promise.then.apply(promise, arguments); 435 | }; 436 | 437 | 438 | /** 439 | * .catch() 440 | * 441 | * For usage, see: 442 | * http://bluebirdjs.com/docs/api/catch.html 443 | */ 444 | Deferred.prototype.catch = function (){ 445 | var promise = this.toPromise(); 446 | return promise.catch.apply(promise, arguments); 447 | 448 | // Note: While the following is all very nice, we would need to change `Promise.prototype` to 449 | // have it be universally useful. So to avoid that can of worms, leaving this commented out, 450 | // and the above commented _in_. (See the "FUTURE" note just below here for more thoughts about 451 | // a better solution that would allow for mixing Deferred syntax for error handling with standard 452 | // ES8 async/await) 453 | // ================================================================================================ 454 | // Deferred.prototype.catch = function (_handlerOrEligibilityFilter, _handlerMaybe){ 455 | // // Start running the logic and get a promise. 456 | // var promise = this.toPromise(); 457 | // 458 | // // If first argument is string, then we'll assume that the 2nd argument must be a function, 459 | // // and that the intent was to understand this as: 460 | // // ``` 461 | // // .catch({ code: theFirstArgument }, handler); 462 | // // ``` 463 | // // > Note that this behavior is an **EXTENSION OF BLUEBIRD!** 464 | // // > Thus it is not supported outside of parley, and it is a deliberate divergence 465 | // // > in order to more closely match the one-step-simpler interface exposed by tools 466 | // // > like `flaverr`. 467 | // if (_.isString(_handlerOrEligibilityFilter)) { 468 | // if (!_handlerMaybe) { 469 | // throw flaverr({ 470 | // name: 471 | // 'UsageError', 472 | // message: 473 | // 'Invalid usage of `.catch()`. If the first argument to .catch() is NOT a function,\n'+ 474 | // 'then a handler function should always be passed in as the second argument.\n'+ 475 | // 'See https://sailsjs.com/support for help.' 476 | // }, this._omen); 477 | // } 478 | // return promise.catch.apply(promise, [{ code: _handlerOrEligibilityFilter }, _handlerMaybe]); 479 | // } 480 | // // Otherwise, if a second argument of some kind was supplied, we'll assume it's 481 | // // our handler function, and that the first argument must be a valid eligibility 482 | // // filter. 483 | // else if (_handlerMaybe) { 484 | // return promise.catch.apply(promise, [_handlerOrEligibilityFilter, _handlerMaybe]); 485 | // } 486 | // // Otherwise, since there's no second argument, there must not be an eligibility filter. 487 | // // So we'll treat the first argument as our handler. 488 | // else { 489 | // return promise.catch.apply(promise, [_handlerOrEligibilityFilter]); 490 | // } 491 | // ================================================================================================ 492 | 493 | }; 494 | 495 | 496 | /** 497 | * .intercept() 498 | * 499 | * Bind a special after-exec lifecycle callback that is useful for easily catching+rethrowing 500 | * errors, without the risk of unintentionally swallowing unrelated bugs by using other 501 | * mechanisms (e.g. try/catch blocks) 502 | * 503 | * > See `bindUserlandAfterExecLC` utility for details on how this works. 504 | * 505 | * Also note that .intercept() gets one extra special shorthand notation: 506 | * If desired, you can specific a non-empty string instead of a function as your "specific handler". 507 | * In this case, a handler will be constructed automatically: simply a function that returns 508 | * the specified string. This is particularly useful for situations such as intercepting 509 | * an exception and throwing a different special exit signal for an enclosing action (for example, 510 | * within one of Sails's helpers, shell scripts, or actions2-style actions, or in the 511 | * implementation of a machinepack.) 512 | */ 513 | Deferred.prototype.intercept = function(negotiationRuleOrWildcardHandler, specificHandler, _lcOpts){ 514 | 515 | if (_.isString(specificHandler)) { 516 | var originalString = specificHandler; 517 | specificHandler = function(){ 518 | return originalString; 519 | };//ƒ 520 | } 521 | 522 | bindUserlandAfterExecLC('intercept', negotiationRuleOrWildcardHandler, specificHandler, this, _lcOpts); 523 | return this; 524 | }; 525 | 526 | 527 | /** 528 | * .tolerate() 529 | * 530 | * Bind a special after-exec lifecycle callback that is useful for gracefully handling 531 | * particular errors without accidentally swallowing unrelated bugs by using other mechanisms 532 | * (e.g. try/catch blocks). 533 | * 534 | * > See `bindUserlandAfterExecLC` utility for details on how this works. 535 | */ 536 | Deferred.prototype.tolerate = function(negotiationRuleOrWildcardHandler, specificHandler, _lcOpts){ 537 | bindUserlandAfterExecLC('tolerate', negotiationRuleOrWildcardHandler, specificHandler, this, _lcOpts); 538 | return this; 539 | }; 540 | 541 | 542 | /** 543 | * .toPromise() 544 | * 545 | * Begin executing this Deferred and return a promise. 546 | * 547 | * > See also: 548 | * > http://bluebirdjs.com/docs/api/promisify.html 549 | * 550 | * @returns {Promise} 551 | */ 552 | Deferred.prototype.toPromise = function (){ 553 | 554 | // Use cached promise, if one has already been created. 555 | // 556 | // > This prevents extraneous invocation in `.then()` chains. 557 | // > (& potentially improves perf of `.catch()`) 558 | if (this._promise) { 559 | return this._promise; 560 | }//-• 561 | 562 | // Build a function that, when called, will begin executing the underlying 563 | // logic and also return a promise. 564 | var getPromise = bluebird.promisify(this.exec, { context: this }); 565 | // ^^The `{context: this}` thing is equivalent to `bluebird.promisify(this.exec).bind(this)`, 566 | // > more or less. The reason we have to use `.bind()` here is so that `.exec()` 567 | // > gets called w/ the appropriate context. (If we were using closures instead 568 | // > of a prototypal thing, we wouldn't have to do this.) 569 | 570 | // Make a promise, and cache it as `this._promise` so that it can be 571 | // reused (e.g. in case there are multiple calls to `.then()` and `.catch()`) 572 | this._promise = getPromise(); 573 | return this._promise; 574 | 575 | }; 576 | 577 | 578 | /** 579 | * .now() 580 | * 581 | * Execute the Deferred and get an immediate result. 582 | * (Only works for synchronous logic.) 583 | * 584 | * @returns {Ref} result from invoking the `handleExec` function 585 | * @throws {Error} If something goes wrong with the `handleExec` logic 586 | */ 587 | Deferred.prototype.now = function () { 588 | 589 | var isFinishedExecuting; 590 | var immediateResult; 591 | var immediateError; 592 | this.exec((err, result)=>{ 593 | isFinishedExecuting = true; 594 | immediateError = err; 595 | immediateResult = result; 596 | });//_∏_ 597 | 598 | if (!isFinishedExecuting) { 599 | 600 | this._skipImplSpinlockWarning = true; 601 | // ^^This indicates that this Deferred should now silently ignore any 602 | // extra attempts to trigger its callbacks- whether that's from within 603 | // the custom implementation or from something more built-in (e.g. the 604 | // timeout) -- i.e. because we've already sent back a proper error, 605 | // and the extra warnings just muddy the waters, so to speak- making 606 | // it harder to tell what the actual problem was. 607 | 608 | throw flaverr({ 609 | name: 610 | 'UsageError', 611 | code: 612 | 'E_NOT_SYNCHRONOUS', 613 | message: 614 | 'Failed to call this function synchronously with `.now()` because\n'+ 615 | 'it is not actually synchronous. Rather than `.now()`, please use '+ 616 | '`await` or `.exec()`, `.then()`, etc.', 617 | }, this._omen); 618 | 619 | }//• 620 | 621 | if (immediateError) { 622 | throw immediateError; 623 | }//• 624 | 625 | return immediateResult; 626 | }; 627 | 628 | 629 | /** 630 | * .log() 631 | * 632 | * Log the output from this asynchronous logic, fully-expanded. 633 | * (Uses `util.inspect(..., {depth: null})`.) 634 | * 635 | * Note: This is for debugging / for exploration purposes, especially 636 | * for use on the Node.js/Sails.js REPL or in the browser console. 637 | * If there is an error, it will simply be logged to stderr. 638 | */ 639 | Deferred.prototype.log = function (){ 640 | 641 | if (!IS_DEBUG_OR_NON_PRODUCTION_ENV) { 642 | console.warn('* * * * * * * * * * * * * * * * * * * * * * * * * *'); 643 | console.warn('Warning: Production environment detected...'); 644 | console.warn('Please reconsider using using .log() in production.'); 645 | console.warn(' [?] If you\'re unsure, see https://sailsjs.com/support'); 646 | console.warn('* * * * * * * * * * * * * * * * * * * * * * * * * *'); 647 | } 648 | else { 649 | console.log('Running with `.log()`...'); 650 | } 651 | 652 | this.exec((err, result)=>{ 653 | if (err) { 654 | console.error(); 655 | console.error('- - - - - - - - - - - - - - - - - - - - - - - -'); 656 | console.error('An error occurred:'); 657 | console.error(); 658 | console.error(err); 659 | console.error('- - - - - - - - - - - - - - - - - - - - - - - -'); 660 | console.error(); 661 | return; 662 | }//-• 663 | 664 | console.log(); 665 | if (result === undefined) { 666 | console.log('- - - - - - - - - - - - - - - - - - - - - - - -'); 667 | console.log('Finished successfully.'); 668 | console.log(); 669 | console.log('(There was no result.)'); 670 | console.log('- - - - - - - - - - - - - - - - - - - - - - - -'); 671 | } 672 | else { 673 | console.log('- - - - - - - - - - - - - - - - - - - - - - - -'); 674 | console.log('Finished successfully.'); 675 | console.log(); 676 | console.log('Result:'); 677 | console.log(); 678 | console.log(util.inspect(result, {depth: null})); 679 | console.log('- - - - - - - - - - - - - - - - - - - - - - - -'); 680 | } 681 | console.log(); 682 | 683 | }); 684 | 685 | }; 686 | 687 | 688 | /** 689 | * .timeout() 690 | * 691 | * Set a timeout for this invocation (i.e. from userland). 692 | * 693 | * > Note: This works by reusing/overriding the implementor-land `_timeout` property. 694 | * 695 | * @param {Number} ms [number of milliseconds to wait for execution to finish before considering this timed out] 696 | * @throws {Error} If already begun executing 697 | */ 698 | Deferred.prototype.timeout = function (ms){ 699 | if (!_.isNumber(ms)) { 700 | throw flaverr({ 701 | name: 702 | 'UsageError', 703 | message: 704 | 'Invalid usage for `.timeout()`. Please provide a number of milliseconds\n'+ 705 | 'as the first argument, or use 0 to eliminate the timeout for this invocation.' 706 | }, this._omen); 707 | } 708 | 709 | if (this._hasBegunExecuting) { 710 | throw flaverr({ 711 | name: 712 | 'UsageError', 713 | message: 714 | 'Could not attach max milliseconds with `.timeout()` because this invocation\n'+ 715 | 'has already '+(this._hasTimedOut?'timed out':this._hasFinishedExecuting?'finished executing':'begun executing')+'.' 716 | }, this._omen); 717 | } 718 | 719 | this._timeout = ms; 720 | 721 | return this; 722 | }; 723 | 724 | 725 | // Attach `inspect`, `toString`, and `toJSON` functions 726 | // (This is mainly to hide the `_omen` property, which is pretty scary-looking) 727 | Deferred.prototype.toJSON = function (){ return null; }; 728 | Deferred.prototype.toString = function (){ return '[Deferred]'; }; 729 | Deferred.prototype.inspect = function (){ return '[Deferred]'; };// «« TODO: also include the symbol equivalent for Node >=10 730 | 731 | 732 | /** 733 | * .getInvocationInfo() 734 | * 735 | * Return information about this Deferred. 736 | * 737 | * > Note: This function is designed for use in higher-level tools. 738 | * > It provides a public API for accessing Deferred state such as `_timeout`. 739 | * 740 | * @returns {Dictionary} 741 | * @property {Number?} timeout 742 | * @property {Array?} @of {Dictionary} interruptions 743 | * @property {String} type 744 | * @property {String|Dictionary|Array} rule 745 | * @property {String|Function?} handler 746 | * @property {Boolean} isThenable 747 | */ 748 | Deferred.prototype.getInvocationInfo = function (){ 749 | return { 750 | timeout: this._timeout, 751 | interruptions: this._userlandAfterExecLCs 752 | }; 753 | }; 754 | 755 | 756 | // Finally, export the Deferred constructor. 757 | // (we could have done this earlier-- we just do it down here for consistency) 758 | module.exports = Deferred; 759 | 760 | 761 | 762 | 763 | 764 | ////////////////////////////////////////////////////////////////////////////////////////// 765 | // ██████╗ ██████╗ ████████╗██╗███╗ ███╗██╗███████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗███████╗ 766 | // ██╔═══██╗██╔══██╗╚══██╔══╝██║████╗ ████║██║╚══███╔╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝██╗ 767 | // ██║ ██║██████╔╝ ██║ ██║██╔████╔██║██║ ███╔╝ ███████║ ██║ ██║██║ ██║██╔██╗ ██║███████╗╚═╝ 768 | // ██║ ██║██╔═══╝ ██║ ██║██║╚██╔╝██║██║ ███╔╝ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║██╗ 769 | // ╚██████╔╝██║ ██║ ██║██║ ╚═╝ ██║██║███████╗██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║███████║╚═╝ 770 | // ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ 771 | // 772 | // Our callback (`_cb`) is intercepted by a couple of other functions. This is 773 | // just a slightly-more-efficient alternative to a series of self-calling functions. 774 | // We only do this to afford better performance in the general case. 775 | // 776 | // > (Normally, this sort of micro-optimization wouldn't matter, but this is an extradordinarily 777 | // > hot code path. Note that if we can prove self-calling functions are just as good, or even 778 | // > good enough, it would be preferable to use them instead (not only for consistency, but 779 | // > certainly for clarity as well).) 780 | // 781 | // To begin with, no matter what, intercept `_cb` by wrapping it in another function 782 | // (a new one that we'll call `cb`) which adds some additional checks. 783 | ////////////////////////////////////////////////////////////////////////////////////////// 784 | 785 | /** 786 | * Used exclusively by `Deferred.prototype.exec()`, this function is an optimization. 787 | * It would be much better to use an IIFE instead of defining this function, but we're 788 | * dealing with a very hot code path, so the performance gain is worth it. 789 | * That said, this optimization should never be applied in normal userland code! 790 | */ 791 | function proceedToAfterExecSpinlocks (errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException, timeoutAlarm) { 792 | 793 | // Implementorland spinlock 794 | if (self._hasFinishedExecuting && !self._skipImplSpinlockWarning) { 795 | console.warn( 796 | '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'+ 797 | 'WARNING: Something seems to be wrong with this function.\n'+ 798 | 'It is trying to signal that it has finished AGAIN, after\n'+ 799 | 'already resolving/rejecting once.\n'+ 800 | '(silently ignoring this...)\n'+ 801 | (self._omen?( 802 | '\n'+ 803 | 'To assist you in hunting this down, here is a stack trace:\n'+ 804 | '```\n'+ 805 | flaverr.getBareTrace(self._omen)+'\n'+ 806 | '```\n'+ 807 | 'Here is the original error:\n'+ 808 | '```\n'+ 809 | (errCbArg ? errCbArg : self._omen.raw.stack) + 810 | '```\n'+ 811 | '\n' 812 | ):'')+ 813 | ' [?] For more help, visit https://sailsjs.com/support\n'+ 814 | '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' 815 | ); 816 | return; 817 | }//• 818 | 819 | // If the deferred has already timed out, then there's no need to warn 820 | // (This was _bound_ to happen beings as how we timed out.) 821 | // 822 | // > Note that we still set a flag to track that this happened. This is to make sure 823 | // > this if/then statement can't possibly be true more than once (because that would 824 | // > definitely still be unexpected-- and really hard to anticipate / debug if it were 825 | // > to happen to you) 826 | if (self._hasTimedOut) { 827 | self._hasFinishedExecuting = true; 828 | return; 829 | }//• 830 | 831 | // Clear timeout, if relevant. 832 | if (timeoutAlarm) { 833 | clearTimeout(timeoutAlarm); 834 | } 835 | 836 | if (errCbArg) { 837 | // Ensure we're dealing w/ an Error instance. 838 | errCbArg = flaverr.parseOrBuildError(errCbArg, self._omen); 839 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 840 | // ^ FUTURE: Better error message for non-Errors? 841 | // (See impl of parseOrBuildError() in flaverr for more context.) 842 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 843 | }//fi 844 | self._hasFinishedExecuting = true; 845 | return proceedToInterceptsAndChecks(errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 846 | }//ƒ 847 | 848 | 849 | 850 | /** 851 | * Used exclusively by `Deferred.prototype.exec()` and `proceedToAfterExecSpinlocks`, this function is an optimization. 852 | * It would be much better to use an IIFE instead of defining this function, but we're 853 | * dealing with a very hot code path, so the performance gain is worth it. 854 | * That said, this optimization should never be applied in normal userland code! 855 | */ 856 | function proceedToInterceptsAndChecks (errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException) { 857 | 858 | // ┬ ┬┬─┐┌─┐┌─┐ ┌─┐┌─┐┌┬┐┌─┐┌┐┌┌┬┐┬┌─┐┬ ┬ ┬ ┬ ┌─┐┌─┐┌┐┌┌─┐┬ ┬┌─┐┬┌┐┌┌─┐ ┌─┐┬─┐┬─┐┌─┐┬─┐┌─┐ 859 | // │││├┬┘├─┤├─┘ ├─┘│ │ │ ├┤ │││ │ │├─┤│ │ └┬┘───│ │ ││││├┤ │ │└─┐│││││ ┬ ├┤ ├┬┘├┬┘│ │├┬┘└─┐ 860 | // └┴┘┴└─┴ ┴┴ ┴ └─┘ ┴ └─┘┘└┘ ┴ ┴┴ ┴┴─┘┴─┘┴ └─┘└─┘┘└┘└ └─┘└─┘┴┘└┘└─┘ └─┘┴└─┴└─└─┘┴└─└─┘ 861 | // ┌─┐┬─┐┌─┐┌┬┐ ┌─┐┌┬┐┬ ┬┌─┐┬─┐ ┌┬┐┌─┐┌─┐┌─┐┬─┐┬─┐┌─┐┌┬┐ ┬┌┐┌┬ ┬┌─┐┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐ 862 | // ├┤ ├┬┘│ ││││ │ │ │ ├─┤├┤ ├┬┘ ││├┤ ├┤ ├┤ ├┬┘├┬┘├┤ ││ ││││└┐┌┘│ ││ ├─┤ │ ││ ││││└─┐ 863 | // └ ┴└─└─┘┴ ┴ └─┘ ┴ ┴ ┴└─┘┴└─ ─┴┘└─┘└ └─┘┴└─┴└─└─┘─┴┘ ┴┘└┘ └┘ └─┘└─┘┴ ┴ ┴ ┴└─┘┘└┘└─┘ 864 | // ┬ ┬┬┌┬┐┬ ┬┬┌┐┌ ┌┬┐┬ ┬┌─┐ ┬ ┬┌─┐┌┐┌┌┬┐┬ ┌─┐┌─┐─┐ ┬┌─┐┌─┐ ┌─┐┬ ┬┌┐┌┌─┐┌┬┐┬┌─┐┌┐┌ 865 | // ││││ │ ├─┤││││ │ ├─┤├┤ ├─┤├─┤│││ │││ ├┤ ├┤ ┌┴┬┘├┤ │ ├┤ │ │││││ │ ││ ││││ 866 | // └┴┘┴ ┴ ┴ ┴┴┘└┘ ┴ ┴ ┴└─┘ ┴ ┴┴ ┴┘└┘─┴┘┴─┘└─┘└─┘┴ └─└─┘└─┘ └ └─┘┘└┘└─┘ ┴ ┴└─┘┘└┘ 867 | if (errCbArg) { 868 | 869 | var doWrap; 870 | 871 | // If we see E_NOT_SYNCHRONOUS, it should ALWAYS be wrapped. 872 | // (The only time it would ever come from THIS Deferred is if we called .now() -- 873 | // and the code that checks that is not even part of .exec()) 874 | if (_.isObject(errCbArg) && errCbArg.code === 'E_NOT_SYNCHRONOUS') { 875 | doWrap = true; 876 | } 877 | // If we see a TimeoutError from a Deferred **OTHER** than this one, 878 | // then wrap it. 879 | else if (_.isObject(errCbArg) && errCbArg.name === 'TimeoutError' && errCbArg.traceRef !== self) { 880 | doWrap = true; 881 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 882 | // Note: An easy way to test this is to run something like the following in the Node REPL: 883 | // ```` 884 | // require('machine')({identity: 'outside', fn: (inputs, exits)=>{ require('machine')({identity: 'inside',timeout: 2000, exits: {notFound:{}}, fn: (inputs, exits)=>{ /*deliberately never exits...*/ }})().exec((err)=>{ if (err){return exits.error(err);} return exits.success(); }); }})() 885 | // ```` 886 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 887 | } 888 | 889 | // If instructed to do so, perform the wrapping. 890 | if (doWrap) { 891 | errCbArg = flaverr.wrap({ 892 | code: 893 | 'E_FROM_WITHIN', 894 | message: 895 | 'Some logic inside this function\'s implementation encountered an error.\n'+ 896 | ' [?] See `.raw` for more details, or visit https://sailsjs.com/support for help.', 897 | }, errCbArg, self._omen); 898 | }//fi 899 | 900 | }//fi 901 | 902 | 903 | // ┬ ┬┌─┐┌─┐┬─┐┬ ┌─┐┌┐┌┌┬┐ ┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐─┐ ┬┌─┐┌─┐ ┬ ┌─┐┌─┐ 904 | // │ │└─┐├┤ ├┬┘│ ├─┤│││ ││ ├─┤├┤ │ ├┤ ├┬┘├┤ ┌┴┬┘├┤ │ │ │ └─┐ 905 | // └─┘└─┘└─┘┴└─┴─┘┴ ┴┘└┘─┴┘ ┴ ┴└ ┴ └─┘┴└─└─┘┴ └─└─┘└─┘ ┴─┘└─┘└─┘ 906 | // If this Deferred was configured with after-exec lifecycle callbacks from 907 | // userland via .intercept() or .tolerate(), then call those 908 | // lifecycle callbacks now, if appropriate, picking up the potentially-changed 909 | // (even potentially-reconstructed!) error or result. 910 | // 911 | // > Note that this is only relevant if there was an error of some kind. 912 | if (!(self._userlandAfterExecLCs && errCbArg)) { 913 | return proceedToFinalAfterExecLC(errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 914 | } else {//• 915 | 916 | // Now before proceeding further, check for a match (if there are any configured). 917 | // Unless we have a match, go ahead and bail. 918 | // > NOTE: We only ever run one of these handlers for any given response! 919 | var matchingUserlandLC; 920 | for (var i = 0; i < self._userlandAfterExecLCs.length; i++) { 921 | var lcDef = self._userlandAfterExecLCs[i]; 922 | if (lcDef.rule === undefined) { 923 | matchingUserlandLC = lcDef; 924 | break; 925 | } else if (flaverr.taste(lcDef.rule, errCbArg)) { 926 | matchingUserlandLC = lcDef; 927 | break; 928 | } 929 | }//∞ 930 | if (!matchingUserlandLC) { 931 | return proceedToFinalAfterExecLC(errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 932 | }//• 933 | 934 | (function(proceed){ 935 | 936 | // Get reasonable default for handler, if no explicit handler function was configured. 937 | if (matchingUserlandLC.handler === undefined && matchingUserlandLC.type === 'tolerate') { 938 | matchingUserlandLC.handler = function(){ return; };//ƒ 939 | } 940 | else if (matchingUserlandLC.handler === undefined && matchingUserlandLC.type === 'intercept') { 941 | matchingUserlandLC.handler = function(err){ return err; };//ƒ 942 | } 943 | 944 | // Run userland LC. 945 | self._hasStartedButNotFinishedAfterExecLC = true; 946 | if (!matchingUserlandLC.isThenable) { 947 | var resultFromHandler; 948 | try { 949 | resultFromHandler = matchingUserlandLC.handler(errCbArg); 950 | } catch (err) { return proceed(err); } 951 | return proceed(undefined, resultFromHandler); 952 | } else { 953 | var lcPromise; 954 | try { 955 | lcPromise = matchingUserlandLC.handler(errCbArg); 956 | } catch (err) { return proceed(err); } 957 | lcPromise.then((resultFromHandler)=>{ 958 | proceed(undefined, resultFromHandler); 959 | }).catch((err)=>{ 960 | proceed(err); 961 | });//_∏_ 962 | } 963 | })((err, resultFromHandler)=>{ 964 | 965 | // Clear spinlock. 966 | self._hasStartedButNotFinishedAfterExecLC = false; 967 | 968 | if (err) { 969 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 970 | // FUTURE: (Maybe) Specifically for `.tolerate()`, allow throwing special exit signals 971 | // from within the handler. 972 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 973 | 974 | // If this is an .intercept() handler, then it's possible the handler threw on purpose, 975 | // perhaps because it was attempting to send a special signal to its caller (e.g. the 976 | // implementation of an action/helper/etc) where it presumably has a special meaning. 977 | // So in this case, we customize the error message to reflect that possibility and to 978 | // suggest an appropriate resolution. 979 | if (matchingUserlandLC.type === 'intercept') { 980 | return proceedToFinalAfterExecLC(flaverr({ 981 | name: 982 | 'UsageError', 983 | message: 984 | 'Caught unexpected error in `.intercept()` handler, which should not throw:\n'+ 985 | flaverr.parseOrBuildError(err).message+'\n'+ 986 | 'If this was intentional, i.e. to communicate a signal to the caller, then\n'+ 987 | 'please just return the new or modified error you would like to use instead.\n'+ 988 | 'The value returned to `.intercept()` will be used as the new Error.\n'+ 989 | ' [?] See https://sailsjs.com/support for help.', 990 | raw: 991 | err 992 | }, self._omen), resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 993 | } 994 | else { 995 | // Otherwise, we'll allow this error through unscathed and treat it just like 996 | // any unexpected error that might have been thrown by the implementation. 997 | return proceedToFinalAfterExecLC(err, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 998 | } 999 | }//• 1000 | 1001 | // Now swallow or swap out the error, if instructed to do so. 1002 | 1003 | // Swallow: 1004 | // > i.e. if a matching `.tolerate()` was encountered, then consider 1005 | // > this successful no matter what, and use the value returned by the 1006 | // > LC as the new result. 1007 | if (matchingUserlandLC.type === 'tolerate') { 1008 | errCbArg = undefined; 1009 | resultCbArg = resultFromHandler; 1010 | } 1011 | // Swap: 1012 | // 1013 | // > i.e. if a matching `.intercept()` was encountered, then consider 1014 | // > whatever the intercept handler returned to be our new Error. 1015 | else if (matchingUserlandLC.type === 'intercept') { 1016 | 1017 | // If the handler returned `undefined`, then fail with an error. 1018 | // (this shouldn't happen, an indicates invalid usage) 1019 | if (resultFromHandler === undefined) { 1020 | return proceedToFinalAfterExecLC(flaverr({ 1021 | name: 1022 | 'UsageError', 1023 | message: 1024 | '`.intercept()` handler returned `undefined`, but this should never happen.\n'+ 1025 | 'Regardless, here is a summary of the original underlying error:\n'+ 1026 | flaverr.parseOrBuildError(errCbArg).message+'\n'+ 1027 | ' [?] See https://sailsjs.com/support for help.', 1028 | raw: 1029 | errCbArg 1030 | }, self._omen), resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 1031 | }//• 1032 | 1033 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1034 | // Normally, these errors must ALWAYS be Error instances already. 1035 | // But for this special case, where the original Error value 1036 | // is being overridden through the use of `.intercept()`, we'd 1037 | // LIKE to make a special exception to the rule (no pun intended.) 1038 | // 1039 | // There's only one problem: Because of bluebird's "maybeWrapAsError" 1040 | // implementation, we can't send certain non-Errors through to it 1041 | // (specifically primitives) because they get autowrapped. 1042 | // 1043 | // > Here's the relevant bit of code: 1044 | // > https://github.com/petkaantonov/bluebird/blob/e8d8525a0517280d11d6c77ae6b61df86419232b/src/promisify.js#L182-L184 1045 | // 1046 | // Again, most of the time, this would be fine. But while bluebird's 1047 | // looking out for us here is admirable, there are some situations. 1048 | // where this is not welcome -- such as when trying to throw a string. 1049 | // 1050 | // > Why throw a string? 1051 | // > This is useful for throwing special signals-- e.g. from the inside 1052 | // > of an actions2 action or a helper in Sails, a machine's fn in a 1053 | // > machinepack, or from a commandline script. 1054 | // 1055 | // So anyway, to work around this, we have to come up with a consistent 1056 | // way of wrapping up non-Errors to look like Errors. That's what we 1057 | // do next. 1058 | // 1059 | // ** Note that we also do this in a couple of other places in parley. ** 1060 | // ** (look for `flaverr.parseOrBuildError()` calls) ** 1061 | // 1062 | // > (If ever we find ourselves wanting to revert this approach, the old 1063 | // > code that used to check for non-Errors was removed in parley@376208fd1c0ab70e7a6b9c4ecfa563ec0d77a3a8. 1064 | // > But... as mentioned above-- there are some good reasons to keep things 1065 | // > the new way that they are now.) 1066 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1067 | 1068 | var interceptError = flaverr.parseOrBuildError(resultFromHandler, self._omen); 1069 | errCbArg = interceptError; 1070 | 1071 | }//fi 1072 | 1073 | return proceedToFinalAfterExecLC(errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException); 1074 | 1075 | });//_∏_ (†) 1076 | }//fi 1077 | }//ƒ 1078 | 1079 | 1080 | /** 1081 | * Used exclusively by `proceedToInterceptsAndChecks()`, this function is an optimization. 1082 | * It would be much better to use an IIFE instead of defining this function, but we're 1083 | * dealing with a very hot code path, so the performance gain is worth it. 1084 | * That said, this optimization should never be applied in normal userland code! 1085 | */ 1086 | function proceedToFinalAfterExecLC(errCbArg, resultCbArg, extraCbArgs, self, _cb, handleUncaughtException) { 1087 | 1088 | // ╔═╗╦╔╗╔╔═╗╦ ┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐─┐ ┬┌─┐┌─┐ ┬ ┌─┐ 1089 | // ╠╣ ║║║║╠═╣║ ├─┤├┤ │ ├┤ ├┬┘├┤ ┌┴┬┘├┤ │ │ │ 1090 | // ╚ ╩╝╚╝╩ ╩╩═╝ ┴ ┴└ ┴ └─┘┴└─└─┘┴ └─└─┘└─┘ ┴─┘└─┘ 1091 | // ┌─┐┬─┐┌─┐┌┬┐ ┬┌┬┐┌─┐┬ ┌─┐┌┬┐┌─┐┌┐┌┌┬┐┌─┐┬─┐┬ ┌─┐┌┐┌┌┬┐ 1092 | // ├┤ ├┬┘│ ││││ ││││├─┘│ ├┤ │││├┤ │││ │ │ │├┬┘│ ├─┤│││ ││ 1093 | // └ ┴└─└─┘┴ ┴ ┴┴ ┴┴ ┴─┘└─┘┴ ┴└─┘┘└┘ ┴ └─┘┴└─┴─┘┴ ┴┘└┘─┴┘ 1094 | // If this Deferred was built with an `finalAfterExecLC` lifecycle callback, 1095 | // then intercept our normal flow to call that lifecycle callback, picking up 1096 | // the potentially-changed (even potentially-reconstructed!) error or result. 1097 | if (self._finalAfterExecLC) { 1098 | if (errCbArg) { 1099 | errCbArg = self._finalAfterExecLC(errCbArg); 1100 | } 1101 | else { 1102 | resultCbArg = self._finalAfterExecLC(undefined, resultCbArg); 1103 | } 1104 | }//fi 1105 | 1106 | 1107 | // ┌┐┌┌─┐┬ ┬ ┌─┐┌─┐┌┬┐┬ ┬┌─┐┬ ┬ ┬ ┬ ┌┬┐┬─┐┬┌─┐┌─┐┌─┐┬─┐ ┌─┐┌┐ ┌─┐┌┐┌┌─┐┬ ┬┬─┐┬┌┐┌┌─┐ 1108 | // ││││ ││││ ├─┤│ │ │ │├─┤│ │ └┬┘ │ ├┬┘││ ┬│ ┬├┤ ├┬┘ │ ├┴┐ ├┤ │││└─┐│ │├┬┘│││││ ┬ 1109 | // ┘└┘└─┘└┴┘ ┴ ┴└─┘ ┴ └─┘┴ ┴┴─┘┴─┘┴ ┴ ┴└─┴└─┘└─┘└─┘┴└─ └─┘└─┘┘ └─┘┘└┘└─┘└─┘┴└─┴┘└┘└─┘ 1110 | // ┌┐┌┌─┐ ┌─┐┌─┐┌─┐┬┌┬┐┌─┐┌┐┌┌┬┐┌─┐┬ ┌─┐┬ ┬┌─┐┬ ┬ ┌─┐┬ ┬┬┌┐┌┌─┐ ┌─┐┌─┐ ┌─┐┬─┐┬─┐┌─┐┬─┐┌─┐ 1111 | // ││││ │ ├─┤│ │ │ ││├┤ │││ │ ├─┤│ └─┐│││├─┤│ │ │ │││││││││ ┬ │ │├┤ ├┤ ├┬┘├┬┘│ │├┬┘└─┐ 1112 | // ┘└┘└─┘ ┴ ┴└─┘└─┘┴─┴┘└─┘┘└┘ ┴ ┴ ┴┴─┘ └─┘└┴┘┴ ┴┴─┘┴─┘└─┘└┴┘┴┘└┘└─┘ └─┘└ └─┘┴└─┴└─└─┘┴└─└─┘ 1113 | // ┌─┐┬─┐┌─┐┌┬┐ ┌─┐┌─┐┬ ┬ ┌┐ ┌─┐┌─┐┬┌─┌─┐ 1114 | // ├┤ ├┬┘│ ││││ │ ├─┤│ │ ├┴┐├─┤│ ├┴┐└─┐ 1115 | // └ ┴└─└─┘┴ ┴ └─┘┴ ┴┴─┘┴─┘└─┘┴ ┴└─┘┴ ┴└─┘ 1116 | // If there are any extra arguments, send them back too. 1117 | // (This is unconventional, but permitted to allow for extra metadata, 1118 | // which is sometimes handy when you want to expose advanced usage.) 1119 | // Otherwise, if there's no result, just call the callback w/ no args. 1120 | // (This just makes for better log output, etc.) 1121 | // More on that below! 1122 | 1123 | // If this callback is being called after at least one tick has elapsed... 1124 | if (self._hasAlreadyWaitedAtLeastOneTick) { 1125 | 1126 | // If 2nd argument (handleUncaughtException) was provided to .exec(), then run that 1127 | // instead of throwing. This protects against unexpected, uncaught exceptions in 1128 | // asynchronous callbacks. 1129 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1130 | // FUTURE: Probably deprecate this, then remove support (see above). 1131 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1132 | if (handleUncaughtException) { 1133 | try { 1134 | if (extraCbArgs) { 1135 | return _cb.apply(undefined, [errCbArg, resultCbArg].concat(extraCbArgs)); 1136 | } else if (errCbArg !== undefined) { 1137 | return _cb(errCbArg); 1138 | } else if (resultCbArg !== undefined) { 1139 | return _cb(undefined, resultCbArg); 1140 | } else { 1141 | return _cb(); 1142 | } 1143 | } catch (unexpectedErrorFromCallback) { 1144 | return handleUncaughtException(unexpectedErrorFromCallback); 1145 | } 1146 | }//• 1147 | 1148 | // Otherwise, just trigger the callback as-is. 1149 | // (If it throws, it will crash the process!) 1150 | if (extraCbArgs) { 1151 | return _cb.apply(undefined, [errCbArg, resultCbArg].concat(extraCbArgs)); 1152 | } else if (errCbArg !== undefined) { 1153 | return _cb(errCbArg); 1154 | } else if (resultCbArg !== undefined) { 1155 | return _cb(undefined, resultCbArg); 1156 | } else { 1157 | return _cb(); 1158 | } 1159 | 1160 | }//• 1161 | //‡ 1162 | // Otherwise, our logic is synchronous (i.e. <1 async tick has elapsed at the time it's being 1163 | // called). So wrap the `_cb` from userland in a try/catch. If an unhandled error of any kind 1164 | // is thrown from the userland cb, our wrapper uses a special Envelope to bust out of the `try` 1165 | // block, ensuring that the unhandled exception is thrown up to userland. 1166 | // 1167 | // > NOTE: 1168 | // > Without this extra code here, we'd end up with the old behavior: outputting a puzzling error 1169 | // > message -- e.g. about something unexpected things happening in the Deferred, or a warning 1170 | // > about triggering the callback twice (when actually, the issue is that something went wrong 1171 | // > in the callback-- and that the Deferred logic happened to be synchronous, so it wasn't able 1172 | // > to escape parley's internal `try` block.) 1173 | // > 1174 | // > Some relevant links for reference: 1175 | // > • https://github.com/node-machine/machine/blob/7fdcf8a869605d0951909725061379cd27bd7f0d/lib/private/intercept-exit-callbacks.js#L186-L238 1176 | // > • https://github.com/node-machine/machine/search?utf8=%E2%9C%93&q=_hasFnYieldedYet&type= 1177 | // > • https://github.com/node-machine/machine/search?utf8=%E2%9C%93&q=_runningSynchronously&type= 1178 | else { 1179 | try { 1180 | if (extraCbArgs) { 1181 | return _cb.apply(undefined, [errCbArg, resultCbArg].concat(extraCbArgs)); 1182 | } else if (errCbArg !== undefined) { 1183 | return _cb(errCbArg); 1184 | } else if (resultCbArg !== undefined) { 1185 | return _cb(undefined, resultCbArg); 1186 | } else { 1187 | return _cb(); 1188 | } 1189 | } catch (unexpectedErrorFromCallback) { 1190 | throw flaverr.wrap({ 1191 | code: 'E_ESCAPE_HATCH', 1192 | traceRef: self 1193 | }, unexpectedErrorFromCallback, self._omen); 1194 | } 1195 | }//• 1196 | 1197 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1198 | // FUTURE: Additionally, this additional layer of wrapping could take care of improving 1199 | // stack traces, even in the case where an Error comes up from inside the implementation. 1200 | // If done carefully, this can be done in a way that protects characteristics of the 1201 | // internal Error (e.g. its "code", etc.), while also providing a better stack trace. 1202 | // 1203 | // For example, something like this: 1204 | // ``` 1205 | // var relevantPropNames = _.difference( 1206 | // _.union( 1207 | // ['name', 'message'], 1208 | // Object.getOwnPropertyNames(underlyingError) 1209 | // ), 1210 | // ['stack'] 1211 | // ); 1212 | // var errTemplate = _.pick(underlyingError, relevantPropNames); 1213 | // errTemplate.raw = underlyingError;//<< could override stuff-- that's ok (see below). 1214 | // var newError = flaverr(errTemplate, omen); 1215 | // ``` 1216 | // > Note that, above, we also kept the original error (and thus _its_ trace) and 1217 | // > attached that as a separate property. If the original error already has "raw", 1218 | // > that's ok. This is one thing that it makes sense for us to mutate-- and any 1219 | // > attempt to do otherwise would probably be more confusing (you can imagine a while 1220 | // > loop where we add underscores in front of the string "raw", and use that as a keyname. 1221 | // > But again, that ends up being more confusing from a userland perspective.) 1222 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1223 | 1224 | }//ƒ 1225 | 1226 | 1227 | /** 1228 | * bindUserlandAfterExecLC() 1229 | * 1230 | * Used exclusively by `Deferred.prototype.intercept()` and `Deferred.prototype.tolerate()`, 1231 | * this function is an optimization. 1232 | * It would be much better to use an IIFE instead of defining this function, but we're 1233 | * dealing with a very hot code path, so the performance gain is worth it. 1234 | * That said, this optimization should never be applied in normal userland code! 1235 | * 1236 | * @param {String} lcType 1237 | * @param {String|Dictionary|Function} negotiationRuleOrWildcardHandler 1238 | * @param {Function?} specificHandler 1239 | * @param {Deferred} deferred 1240 | * @param {Dictionary?} _lcOpts 1241 | * 1242 | * 1243 | * > The lifecycle callback attached here will run *before* this Deferred's 1244 | * > `_finalAfterExecLC` function (if it has one configured from implementorland.) 1245 | * > 1246 | * > Historical notes: 1247 | * > https://gist.github.com/mikermcneil/c1bc2d57f5bedae810295e5ed8c5f935 1248 | */ 1249 | function bindUserlandAfterExecLC(lcType, negotiationRuleOrWildcardHandler, specificHandler, deferred, _lcOpts){ 1250 | 1251 | // Handle variadic usage. 1252 | var handler; 1253 | var negotiationRule; 1254 | if (_.isFunction(negotiationRuleOrWildcardHandler) && specificHandler === undefined) { 1255 | handler = negotiationRuleOrWildcardHandler; 1256 | } 1257 | else { 1258 | negotiationRule = negotiationRuleOrWildcardHandler; 1259 | handler = specificHandler; 1260 | } 1261 | 1262 | // Validate arguments. 1263 | if (handler !== undefined && !_.isFunction(handler)) { 1264 | throw flaverr({ 1265 | name: 1266 | 'UsageError', 1267 | message: 1268 | 'Invalid usage of `.'+lcType+'()`. Provided handler function is invalid.\n'+ 1269 | ' [?] For advice or assistance, come visit https://sailsjs.com/support' 1270 | }, deferred._omen); 1271 | }//• 1272 | 1273 | if (handler === undefined && lcType === 'intercept') { 1274 | throw flaverr({ 1275 | name: 1276 | 'UsageError', 1277 | message: 1278 | 'Invalid usage of `.intercept()`. No handler function provided.\n'+ 1279 | ' [?] See https://sailsjs.com/support for help.' 1280 | }, deferred._omen); 1281 | }//• 1282 | 1283 | if (handler === undefined && negotiationRule === undefined && lcType === 'tolerate') { 1284 | throw flaverr({ 1285 | name: 1286 | 'UsageError', 1287 | message: 1288 | 'Invalid usage of `.tolerate()`. No handler function was provided, and no\n'+ 1289 | 'negotiation rule was provided either. It would be unsafe to continue.\n'+ 1290 | 'It is never a good idea to tolerate *ALL* errors a function might\n'+ 1291 | 'encounter, because doing so would make it easy to accidentally swallow\n'+ 1292 | 'real problems or bugs. So instead, please provide some way of narrowing\n'+ 1293 | 'down the errors which you\'d like to tolerate, like `.tolerate(\'E_FOOBAR\')`.\n'+ 1294 | ' [?] See https://sailsjs.com/support for help.' 1295 | }, deferred._omen); 1296 | }//• 1297 | 1298 | 1299 | if (negotiationRule !== undefined) { 1300 | 1301 | if (_.isString(negotiationRule) && negotiationRule) { 1302 | // Ok, we'll assume it's fine. 1303 | } 1304 | else if (_.isArray(negotiationRule)) { 1305 | // you can bind multiple LCs at the same time 1306 | // (array rules are automatically split into sub-rules) 1307 | } 1308 | else if (_.isObject(negotiationRule) && !_.isArray(negotiationRule) && !_.isFunction(negotiationRule)) { 1309 | // flaverr/bluebird/lodash-style dictionary negotiation rules are now supported. 1310 | } 1311 | else { 1312 | throw flaverr({ 1313 | name: 1314 | 'UsageError', 1315 | message: 1316 | 'Invalid usage of `.'+lcType+'()`. Invalid error negotiation rule: `'+util.inspect(negotiationRule,{depth:null})+'`.\n'+ 1317 | ' [?] For advice or assistance, come visit https://sailsjs.com/support' 1318 | }, deferred._omen); 1319 | } 1320 | 1321 | }//fi 1322 | 1323 | // Determine whether the handler function is a "thenable". 1324 | // (The `_lcOpts` approach allows backwards compat. for older versions of Node.) 1325 | var isThenable = ( 1326 | (handler && handler.constructor.name === 'AsyncFunction') || 1327 | (_.isObject(_lcOpts) && _lcOpts.thenable) 1328 | ); 1329 | 1330 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1331 | // FUTURE: MAYBE add a best-effort check to make sure there is no pre-existing 1332 | // after exec LC rule that matches this one (i.e. already previously registered 1333 | // using .tolerate() or .intercept()) 1334 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1335 | 1336 | if (!deferred._userlandAfterExecLCs) { 1337 | deferred._userlandAfterExecLCs = []; 1338 | }//fi 1339 | 1340 | if (_.isArray(negotiationRule)) { 1341 | for (var i=0; i