├── .gitattributes ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .editorconfig ├── history.md ├── SECURITY.md ├── LICENSE ├── CONTRIBUTING.md ├── test ├── chain.js └── collector.js ├── package.json ├── README.md ├── .eslintrc.json └── do.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: tshemsedinov 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "overrides": [ 5 | { 6 | "files": ["**/.*rc", "**/*.json"], 7 | "options": { "parser": "json" } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.js,*.mjs,*.ts,*.json,*.yml}] 11 | indent_size = 2 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | - [ ] tests and linter show no problems (`npm t`) 8 | - [ ] tests are added/updated for bug fixes and new features 9 | - [ ] code is properly formatted (`npm run fmt`) 10 | - [ ] description of changes is added in CHANGELOG.md 11 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | 3 | - Revert fix introduced in 0.2.0, instead provide documentation and a way how to avoid such issues. Using nextTick causes loosing stack traces and is not the right way to fix this. 4 | 5 | ## 0.3.0 6 | 7 | - Implement Do#complete 8 | - Make error/success callbacks optional if complete is used 9 | 10 | ## 0.2.0 11 | 12 | - Fix sync callbacks issue, more info in Do#done api description. 13 | 14 | ## 0.1.0 15 | 16 | - Ensured success callback can be triggered only once. 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Please don't open an issue to ask questions 4 | --- 5 | 6 | Issues on GitHub are intended to be related to problems and feature requests 7 | so we recommend not using this medium to ask them here grin. Thanks for 8 | understanding! 9 | 10 | If you have a question, please check out our support groups and channels for 11 | developers community: 12 | 13 | Telegram: 14 | 15 | - Channel for Metarhia community: https://t.me/metarhia 16 | - Group for Metarhia technology stack community: https://t.me/metaserverless 17 | - Group for NodeUA community: https://t.me/nodeua 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: usage example or test. 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected. 17 | 18 | **Screenshots** 19 | If applicable, add screenshots to help explain your problem. 20 | 21 | **Desktop (please complete the following information):** 22 | 23 | - OS: [e.g. Fedora 30 64-bit] 24 | - Node.js version [e.g. 14.15.1] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.0.x | :x: | 8 | | 0.1.x | :x: | 9 | | 0.2.x | :x: | 10 | | 0.3.x | :x: | 11 | | 0.4.x | :x: | 12 | | 0.5.x | :x: | 13 | | 0.6.x | :x: | 14 | | 0.7.x | :white_check_mark: | 15 | 16 | ## Reporting a Vulnerability 17 | 18 | If you believe you have found a security vulnerability, let us know by sending 19 | email to [timur.shemsedinov@gmail.com](mailto:timur.shemsedinov@gmail.com) 20 | We will investigate that and do our best to quickly fix the problem. 21 | 22 | Please don't open an issue to or discuss this security vulnerability in a public 23 | place. Thanks for understanding! 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing CI 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: 11 | - 16 12 | - 18 13 | - 20 14 | - 21 15 | os: 16 | - ubuntu-latest 17 | - windows-latest 18 | - macos-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node }} 25 | - uses: actions/cache@v2 26 | with: 27 | path: ~/.npm 28 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 29 | restore-keys: | 30 | ${{ runner.os }}-node- 31 | - run: npm ci 32 | - run: npm test 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2023 do contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | - [Issues](#issues) 4 | - [Pull Requests](#pull-requests) 5 | 6 | ## Issues 7 | 8 | There are two reasons to open an issue: 9 | 10 | - Bug report 11 | - Feature request 12 | 13 | For bug reports please describe the bug with a clear and concise description, 14 | steps to reproduce the behavior (usage example or test), expected behavior, 15 | provide OS and Node.js version, you can upload screenshots and any additional 16 | context for better understanding. 17 | 18 | Please don't open an issue to ask questions. 19 | 20 | Issues on GitHub are intended to be related to problems and feature requests 21 | so we recommend not using this medium to ask them here grin. Thanks for 22 | understanding! 23 | 24 | If you have a question, please check out our support groups and channels for 25 | developers community: 26 | 27 | Telegram: 28 | 29 | - Channel for Metarhia community: https://t.me/metarhia 30 | - Group for Metarhia technology stack community: https://t.me/metaserverless 31 | - Group for NodeUA community: https://t.me/nodeua 32 | 33 | ## Pull Requests 34 | 35 | Before open pull request please follow checklist: 36 | 37 | - [ ] tests and linter show no problems (`npm t`) 38 | - [ ] tests are added/updated for bug fixes and new features 39 | - [ ] code is properly formatted (`npm run fmt`) 40 | - [ ] description of changes is added in CHANGELOG.md 41 | -------------------------------------------------------------------------------- /test/chain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chain = require('..'); 4 | const metatests = require('metatests'); 5 | 6 | const wrapAsync = ( 7 | // Emulate Asynchronous calls 8 | callback // function 9 | ) => { 10 | setTimeout(callback, Math.floor(Math.random() * 1000)); 11 | }; 12 | 13 | metatests.test('simple chain/do', (test) => { 14 | const readConfig = (name, callback) => { 15 | test.strictSame(name, 'myConfig'); 16 | wrapAsync(() => { 17 | callback(null, { name }); 18 | }); 19 | }; 20 | 21 | const selectFromDb = (query, callback) => { 22 | test.strictSame(query, 'select * from cities'); 23 | wrapAsync(() => { 24 | callback(null, [{ name: 'Kiev' }, { name: 'Roma' }]); 25 | }); 26 | }; 27 | 28 | const getHttpPage = (url, callback) => { 29 | test.strictSame(url, 'http://kpi.ua'); 30 | wrapAsync(() => { 31 | callback(null, 'Some archaic web here'); 32 | }); 33 | }; 34 | 35 | const readFile = (path, callback) => { 36 | test.strictSame(path, 'README.md'); 37 | wrapAsync(() => { 38 | callback(null, 'file content'); 39 | }); 40 | }; 41 | 42 | const c1 = chain 43 | .do(readConfig, 'myConfig') 44 | .do(selectFromDb, 'select * from cities') 45 | .do(getHttpPage, 'http://kpi.ua') 46 | .do(readFile, 'README.md'); 47 | 48 | c1((err, result) => { 49 | test.strictSame(err, null); 50 | test.strictSame(result, 'file content'); 51 | test.end(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "do", 3 | "version": "0.7.0", 4 | "description": "The simplest way to manage asynchronicity", 5 | "license": "MIT", 6 | "contributors": [ 7 | "Timur Shemsedinov ", 8 | "Oleg Slobodskoi ", 9 | "Timoshenko Ivan " 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/metarhia/do.git" 14 | }, 15 | "homepage": "https://metarhia.com", 16 | "keywords": [ 17 | "metasync", 18 | "callback", 19 | "callbacks", 20 | "promise", 21 | "sync", 22 | "async", 23 | "chain", 24 | "asyncronous", 25 | "parallel", 26 | "sequential", 27 | "metarhia", 28 | "step", 29 | "control", 30 | "flow", 31 | "collector", 32 | "errback", 33 | "err-first", 34 | "error-first", 35 | "callback-last", 36 | "datacollector", 37 | "keycollector", 38 | "composition" 39 | ], 40 | "engines": { 41 | "node": ">=8.0.0" 42 | }, 43 | "main": "do.js", 44 | "files": [], 45 | "readmeFilename": "README.md", 46 | "scripts": { 47 | "test": "npm run lint && metatests test", 48 | "lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\" \".*rc\" \"**/*.yml\"", 49 | "fmt": "prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\" \".*rc\" \"**/*.yml\"" 50 | }, 51 | "devDependencies": { 52 | "eslint": "^8.53.0", 53 | "eslint-config-prettier": "^9.0.0", 54 | "eslint-plugin-prettier": "^5.0.1", 55 | "metatests": "^0.8.2", 56 | "prettier": "^3.0.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## "do" is the simplest way to manage asynchronicity 2 | 3 | [![CI Status](https://github.com/metarhia/do/workflows/Testing%20CI/badge.svg)](https://github.com/metarhia/do/actions?query=workflow%3A%22Testing+CI%22+branch%3Amaster) 4 | [![NPM Version](https://badge.fury.io/js/do.svg)](https://badge.fury.io/js/do) 5 | [![NPM Downloads/Month](https://img.shields.io/npm/dm/do.svg)](https://www.npmjs.com/package/do) 6 | [![NPM Downloads](https://img.shields.io/npm/dt/do.svg)](https://www.npmjs.com/package/do) 7 | 8 | If you don't want to use all the async/chain libraries but just want a reliable way to know when the function is done - this is for you. 9 | 10 | ## Installation 11 | 12 | `npm i do` 13 | 14 | ## Usage 15 | 16 | Series async execution 17 | 18 | ```js 19 | const chain = require('do'); 20 | 21 | const c1 = chain 22 | .do(readConfig, 'myConfig') 23 | .do(selectFromDb, 'select * from cities') 24 | .do(getHttpPage, 'http://kpi.ua') 25 | .do(readFile, 'README.md'); 26 | 27 | c1((err, result) => { 28 | console.log('done'); 29 | if (err) console.log(err); 30 | else console.dir({ result }); 31 | }); 32 | ``` 33 | 34 | Data collector 35 | 36 | ```js 37 | const chain = require('do'); 38 | const fs = require('fs'); 39 | 40 | const dc = chain.do(6); 41 | 42 | dc('user', null, { name: 'Marcus Aurelius' }); 43 | fs.readFile('HISTORY.md', (err, data) => dc.collect('history', err, data)); 44 | fs.readFile('README.md', dc.callback('readme')); 45 | fs.readFile('README.md', dc('readme')); 46 | dc.take('readme', fs.readFile, 'README.md'); 47 | setTimeout(() => dc.pick('timer', { date: new Date() }), 1000); 48 | ``` 49 | 50 | ## Run tests 51 | 52 | `npm test` 53 | 54 | ## License & Contributors 55 | 56 | Copyright (c) 2013-2023 do contributors. 57 | See github for full [contributors list](https://github.com/metarhia/do/graphs/contributors). 58 | Do is [MIT licensed](./LICENSE). 59 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | }, 11 | "globals": { 12 | "BigInt": true 13 | }, 14 | "rules": { 15 | "indent": ["error", 2], 16 | "linebreak-style": ["error", "unix"], 17 | "quotes": ["error", "single"], 18 | "semi": ["error", "always"], 19 | "no-loop-func": ["error"], 20 | "block-spacing": ["error", "always"], 21 | "camelcase": ["error"], 22 | "eqeqeq": ["error", "always"], 23 | "strict": ["error", "global"], 24 | "brace-style": [ 25 | "error", 26 | "1tbs", 27 | { 28 | "allowSingleLine": true 29 | } 30 | ], 31 | "comma-style": ["error", "last"], 32 | "comma-spacing": [ 33 | "error", 34 | { 35 | "before": false, 36 | "after": true 37 | } 38 | ], 39 | "eol-last": ["error"], 40 | "func-call-spacing": ["error", "never"], 41 | "key-spacing": [ 42 | "error", 43 | { 44 | "beforeColon": false, 45 | "afterColon": true, 46 | "mode": "minimum" 47 | } 48 | ], 49 | "keyword-spacing": [ 50 | "error", 51 | { 52 | "before": true, 53 | "after": true, 54 | "overrides": { 55 | "function": { 56 | "after": false 57 | } 58 | } 59 | } 60 | ], 61 | "max-len": [ 62 | "error", 63 | { 64 | "code": 80, 65 | "ignoreUrls": true 66 | } 67 | ], 68 | "max-nested-callbacks": [ 69 | "error", 70 | { 71 | "max": 7 72 | } 73 | ], 74 | "new-cap": [ 75 | "error", 76 | { 77 | "newIsCap": true, 78 | "capIsNew": false, 79 | "properties": true 80 | } 81 | ], 82 | "new-parens": ["error"], 83 | "no-lonely-if": ["error"], 84 | "no-trailing-spaces": ["error"], 85 | "no-unneeded-ternary": ["error"], 86 | "no-whitespace-before-property": ["error"], 87 | "object-curly-spacing": ["error", "always"], 88 | "operator-assignment": ["error", "always"], 89 | "operator-linebreak": ["error", "after"], 90 | "semi-spacing": [ 91 | "error", 92 | { 93 | "before": false, 94 | "after": true 95 | } 96 | ], 97 | "space-before-blocks": ["error", "always"], 98 | "space-before-function-paren": [ 99 | "error", 100 | { 101 | "anonymous": "always", 102 | "named": "never", 103 | "asyncArrow": "always" 104 | } 105 | ], 106 | "space-in-parens": ["error", "never"], 107 | "space-infix-ops": ["error"], 108 | "space-unary-ops": [ 109 | "error", 110 | { 111 | "words": true, 112 | "nonwords": false, 113 | "overrides": { 114 | "typeof": false 115 | } 116 | } 117 | ], 118 | "no-unreachable": ["error"], 119 | "no-global-assign": ["error"], 120 | "no-self-compare": ["error"], 121 | "no-unmodified-loop-condition": ["error"], 122 | "no-constant-condition": [ 123 | "error", 124 | { 125 | "checkLoops": false 126 | } 127 | ], 128 | "no-console": ["off"], 129 | "no-useless-concat": ["error"], 130 | "no-useless-escape": ["error"], 131 | "no-shadow-restricted-names": ["error"], 132 | "no-use-before-define": [ 133 | "error", 134 | { 135 | "functions": false 136 | } 137 | ], 138 | "arrow-parens": ["error", "always"], 139 | "arrow-body-style": ["error", "as-needed"], 140 | "arrow-spacing": ["error"], 141 | "no-confusing-arrow": [ 142 | "error", 143 | { 144 | "allowParens": true 145 | } 146 | ], 147 | "no-useless-computed-key": ["error"], 148 | "no-useless-rename": ["error"], 149 | "no-var": ["error"], 150 | "object-shorthand": ["error", "always"], 151 | "prefer-arrow-callback": ["error"], 152 | "prefer-const": ["error"], 153 | "prefer-numeric-literals": ["error"], 154 | "prefer-rest-params": ["error"], 155 | "prefer-spread": ["error"], 156 | "rest-spread-spacing": ["error", "never"], 157 | "template-curly-spacing": ["error", "never"], 158 | "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }] 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /do.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Do() {} 4 | 5 | const chain = function (fn, ...args) { 6 | const current = (done) => { 7 | if (done) current.done = done; 8 | if (current.prev) { 9 | current.prev.next = current; 10 | current.prev(); 11 | } else { 12 | current.forward(); 13 | } 14 | return current; 15 | }; 16 | 17 | const prev = this instanceof Do ? this : null; 18 | const fields = { prev, fn, args, done: null }; 19 | 20 | Object.setPrototypeOf(current, Do.prototype); 21 | return Object.assign(current, fields); 22 | }; 23 | 24 | Do.prototype.do = function (fn, ...args) { 25 | return chain.call(this, fn, ...args); 26 | }; 27 | 28 | Do.prototype.forward = function () { 29 | if (this.fn) 30 | this.fn(...this.args, (err, data) => { 31 | const next = this.next; 32 | if (next) { 33 | if (next.fn) next.forward(); 34 | } else if (this.done) { 35 | this.done(err, data); 36 | } 37 | }); 38 | }; 39 | 40 | function Collector() {} 41 | 42 | Collector.prototype.collect = function (key, err, value) { 43 | if (this.finished) return this; 44 | if (err) { 45 | this.finalize(err, this.data); 46 | return this; 47 | } 48 | if (this.expectKeys && !this.expectKeys.has(key)) { 49 | if (this.unique) { 50 | const err = new Error('Unexpected key: ' + key); 51 | this.finalize(err, this.data); 52 | return this; 53 | } 54 | } else if (!this.keys.has(key)) { 55 | this.count++; 56 | } 57 | this.data[key] = value; 58 | this.keys.add(key); 59 | if (this.expected === this.count) { 60 | this.finalize(null, this.data); 61 | } 62 | return this; 63 | }; 64 | 65 | Collector.prototype.pick = function (key, value) { 66 | this.collect(key, null, value); 67 | return this; 68 | }; 69 | 70 | Collector.prototype.fail = function (key, err) { 71 | this.collect(key, err); 72 | return this; 73 | }; 74 | 75 | Collector.prototype.take = function (key, fn, ...args) { 76 | fn(...args, (err, data) => { 77 | this.collect(key, err, data); 78 | }); 79 | return this; 80 | }; 81 | 82 | Collector.prototype.callback = function (key) { 83 | return (...args) => this(key, ...args); 84 | }; 85 | 86 | Collector.prototype.timeout = function (msec) { 87 | if (this.timer) { 88 | clearTimeout(this.timer); 89 | this.timer = null; 90 | } 91 | if (msec > 0) { 92 | this.timer = setTimeout(() => { 93 | const err = new Error('Collector timed out'); 94 | this.finalize(err, this.data); 95 | }, msec); 96 | } 97 | return this; 98 | }; 99 | 100 | Collector.prototype.done = function (callback) { 101 | this.finish = callback; 102 | return this; 103 | }; 104 | 105 | Collector.prototype.finalize = function (err, data) { 106 | if (this.finished) return this; 107 | if (this.finish) { 108 | if (this.timer) { 109 | clearTimeout(this.timer); 110 | this.timer = null; 111 | } 112 | this.finished = true; 113 | if (this.finish) this.finish(err, data); 114 | } 115 | return this; 116 | }; 117 | 118 | Collector.prototype.distinct = function (value = true) { 119 | this.unique = value; 120 | return this; 121 | }; 122 | 123 | Collector.prototype.cancel = function (err) { 124 | err = err || new Error('Collector cancelled'); 125 | this.finalize(err, this.data); 126 | return this; 127 | }; 128 | 129 | Collector.prototype.then = function (fulfill, reject) { 130 | this.finish = (err, result) => { 131 | if (err) reject(err); 132 | else fulfill(result); 133 | }; 134 | return this; 135 | }; 136 | 137 | // Collector instance constructor 138 | // expected or array of string, 139 | // Returns: Collector 140 | const collect = (expected) => { 141 | const expectKeys = Array.isArray(expected) ? new Set(expected) : null; 142 | const fields = { 143 | expectKeys, 144 | expected: expectKeys ? expectKeys.size : expected, 145 | keys: new Set(), 146 | count: 0, 147 | timer: null, 148 | finish: null, 149 | unique: false, 150 | finished: false, 151 | data: {}, 152 | }; 153 | const collector = (...args) => { 154 | if (args.length === 1) return collector.callback(args[0]); 155 | if (args.length === 2) { 156 | if (args[1] instanceof Error) return collector.fail(...args); 157 | else return collector.pick(...args); 158 | } 159 | if (typeof args[1] === 'function') return collector.take(...args); 160 | return collector.collect(...args); 161 | }; 162 | Object.setPrototypeOf(collector, Collector.prototype); 163 | return Object.assign(collector, fields); 164 | }; 165 | 166 | const ex = (...args) => 167 | (typeof args[0] === 'function' ? chain : collect)(...args); 168 | 169 | ex.do = ex; 170 | module.exports = ex; 171 | -------------------------------------------------------------------------------- /test/collector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const collect = require('..'); 4 | const metatests = require('metatests'); 5 | 6 | metatests.test('data collector functor', (test) => { 7 | const expectedResult = { 8 | key1: 1, 9 | key2: 2, 10 | key3: 3, 11 | }; 12 | 13 | const dc = collect(3) 14 | .done((err, result) => { 15 | test.error(err); 16 | test.strictSame(result, expectedResult); 17 | test.end(); 18 | }) 19 | .timeout(1000); 20 | 21 | dc('key1', null, 1); 22 | dc('key2', null, 2); 23 | dc('key3', null, 3); 24 | }); 25 | 26 | metatests.test('data collector method', (test) => { 27 | const expectedResult = { 28 | key1: 1, 29 | key2: 2, 30 | key3: 3, 31 | }; 32 | 33 | const dc = collect(3) 34 | .done((err, result) => { 35 | test.error(err); 36 | test.strictSame(result, expectedResult); 37 | test.end(); 38 | }) 39 | .timeout(1000); 40 | 41 | dc.collect('key1', null, 1); 42 | dc.collect('key2', null, 2); 43 | dc.collect('key3', null, 3); 44 | }); 45 | 46 | metatests.test('data collector', (test) => { 47 | const expectedResult = { foo: 1, bar: 2 }; 48 | 49 | const dc = collect(['foo', 'bar', 'foo']) 50 | .done((err, result) => { 51 | test.error(err); 52 | test.strictSame(result, expectedResult); 53 | test.end(); 54 | }) 55 | .timeout(1000); 56 | 57 | dc.pick('foo', 1); 58 | dc.pick('bar', 2); 59 | }); 60 | 61 | metatests.test('data collector with shorthand pick', (test) => { 62 | const expectedResult = { foo: 1, bar: 2 }; 63 | 64 | const dc = collect(['foo', 'bar', 'foo']) 65 | .done((err, result) => { 66 | test.error(err); 67 | test.strictSame(result, expectedResult); 68 | test.end(); 69 | }) 70 | .timeout(1000); 71 | 72 | dc('foo', 1); 73 | dc('bar', 2); 74 | }); 75 | 76 | metatests.test('data collector', (test) => { 77 | const expectedResult = { 78 | key1: 1, 79 | key2: 2, 80 | key3: 3, 81 | }; 82 | 83 | const kc = collect(['key1', 'key2', 'key3']) 84 | .done((err, result) => { 85 | test.error(err); 86 | test.strictSame(result, expectedResult); 87 | test.end(); 88 | }) 89 | .timeout(); 90 | 91 | kc.collect('key1', null, 1); 92 | kc.collect('key2', null, 2); 93 | kc.collect('key3', null, 3); 94 | }); 95 | 96 | metatests.test('distinct data collector', (test) => { 97 | const expectedResult = { 98 | key1: 2, 99 | key2: 2, 100 | key3: 3, 101 | }; 102 | 103 | const dc = collect(3) 104 | .distinct() 105 | .done((err, result) => { 106 | test.error(err); 107 | test.strictSame(result, expectedResult); 108 | test.end(); 109 | }); 110 | 111 | dc.pick('key1', 1); 112 | dc.pick('key1', 2); 113 | dc.pick('key2', 2); 114 | dc.pick('key3', 3); 115 | }); 116 | 117 | metatests.test('distinct key collector', (test) => { 118 | const expectedResult = { 119 | key1: 2, 120 | key2: 2, 121 | key3: 3, 122 | }; 123 | 124 | const kc = collect(['key1', 'key2', 'key3']) 125 | .distinct() 126 | .done((err, result) => { 127 | test.error(err); 128 | test.strictSame(result, expectedResult); 129 | test.end(); 130 | }); 131 | 132 | kc.pick('key1', 1); 133 | kc.pick('key1', 2); 134 | kc.pick('key2', 2); 135 | kc.pick('key3', 3); 136 | }); 137 | 138 | metatests.test('data collector with repeated keys', (test) => { 139 | const dc = collect(3) 140 | .timeout(100) 141 | .done((err) => { 142 | test.assert(err); 143 | test.end(); 144 | }); 145 | 146 | dc.collect('key1', null, 1); 147 | dc.collect('key1', null, 2); 148 | dc.collect('key2', null, 2); 149 | }); 150 | 151 | metatests.test('key collector with repeated keys', (test) => { 152 | const kc = collect(['key1', 'key2', 'key3']) 153 | .timeout(100) 154 | .done((err) => { 155 | test.assert(err); 156 | test.end(); 157 | }); 158 | 159 | kc.collect('key1', null, 1); 160 | kc.collect('key1', null, 2); 161 | kc.collect('key2', null, 2); 162 | }); 163 | 164 | metatests.test('collect with error', (test) => { 165 | const testErr = new Error('Test error'); 166 | const col = collect(1); 167 | col.done((err, res) => { 168 | test.strictSame(err, testErr); 169 | test.strictSame(res, {}); 170 | test.end(); 171 | }); 172 | col.fail('someKey', testErr); 173 | }); 174 | 175 | metatests.test('collect with error (shorthand fail)', (test) => { 176 | const testErr = new Error('Test error'); 177 | const col = collect(1); 178 | col.done((err, res) => { 179 | test.strictSame(err, testErr); 180 | test.strictSame(res, {}); 181 | test.end(); 182 | }); 183 | col('someKey', testErr); 184 | }); 185 | 186 | metatests.test('collect method calling after it is done', (test) => { 187 | const col = collect(1); 188 | col.done((err, res) => { 189 | test.error(err); 190 | test.strictSame(res, { someKey: 'someVal' }); 191 | test.end(); 192 | }); 193 | col.pick('someKey', 'someVal'); 194 | col.pick('someKey2', 'someVal2'); 195 | }); 196 | 197 | metatests.test('keys collector receives wrong key', (test) => { 198 | const col = collect(['rightKey']); 199 | col.done((err, res) => { 200 | test.error(err); 201 | test.strictSame(res, { wrongKey: 'someVal', rightKey: 'someVal' }); 202 | test.end(); 203 | }); 204 | col.pick('wrongKey', 'someVal'); 205 | col.pick('rightKey', 'someVal'); 206 | }); 207 | 208 | metatests.test('distinct keys collector receives wrong key', (test) => { 209 | const col = collect(['rightKey']).distinct(); 210 | col.done((err) => { 211 | test.assert(err); 212 | test.end(); 213 | }); 214 | col.pick('wrongKey', 'someVal'); 215 | col.pick('rightKey', 'someVal'); 216 | }); 217 | 218 | metatests.test('collect with take', (test) => { 219 | const col = collect(1); 220 | col.done((err, res) => { 221 | test.error(err); 222 | test.strictSame(res, { someKey: 'someVal' }); 223 | test.end(); 224 | }); 225 | const af = (x, callback) => callback(null, x); 226 | col.take('someKey', af, 'someVal'); 227 | }); 228 | 229 | metatests.test('collect with shorthand take', (test) => { 230 | const col = collect(1); 231 | col.done((err, res) => { 232 | test.error(err); 233 | test.strictSame(res, { someKey: 'someVal' }); 234 | test.end(); 235 | }); 236 | const af = (x, callback) => callback(null, x); 237 | col('someKey', af, 'someVal'); 238 | }); 239 | 240 | metatests.test('collect generate callback', (test) => { 241 | const col = collect(1); 242 | col.done((err, res) => { 243 | test.error(err); 244 | test.strictSame(res, { someKey: 'someVal' }); 245 | test.end(); 246 | }); 247 | const af = (x, callback) => callback(null, x); 248 | af('someVal', col.callback('someKey')); 249 | }); 250 | 251 | metatests.test('collect generate callback shorthand', (test) => { 252 | const col = collect(1); 253 | col.done((err, res) => { 254 | test.error(err); 255 | test.strictSame(res, { someKey: 'someVal' }); 256 | test.end(); 257 | }); 258 | const af = (x, callback) => callback(null, x); 259 | af('someVal', col('someKey')); 260 | }); 261 | 262 | metatests.test('collect with timeout error', (test) => { 263 | const timeoutErr = new Error('Collector timed out'); 264 | const col = collect(1) 265 | .done((err, res) => { 266 | test.strictSame(err, timeoutErr); 267 | test.strictSame(res, {}); 268 | test.end(); 269 | }) 270 | .timeout(1); 271 | const af = (x, callback) => setTimeout(() => callback(null, x), 2); 272 | col.take('someKey', af, 'someVal'); 273 | }); 274 | 275 | metatests.test('collect with take calls bigger than expected', (test) => { 276 | const col = collect(1).done((err, res) => { 277 | test.error(err); 278 | test.strictSame(Object.keys(res).length, 1); 279 | test.end(); 280 | }); 281 | const af = (x, callback) => setTimeout(() => callback(null, x), 1); 282 | col.take('someKey', af, 'someVal'); 283 | col.take('someKey2', af, 'someVal2'); 284 | }); 285 | 286 | metatests.test('cancel data collector', (test) => { 287 | const dc = collect(3).done((err) => { 288 | test.assert(err); 289 | test.end(); 290 | }); 291 | 292 | dc.pick('key', 'value'); 293 | dc.cancel(); 294 | }); 295 | 296 | metatests.test('cancel key collector', (test) => { 297 | const dc = collect(['uno', 'due']).done((err) => { 298 | test.assert(err); 299 | test.end(); 300 | }); 301 | 302 | dc.pick('key', 'value'); 303 | dc.cancel(); 304 | }); 305 | 306 | metatests.test('collect then success', (test) => { 307 | const col = collect(1).then( 308 | (result) => { 309 | test.assert(result); 310 | test.end(); 311 | }, 312 | (err) => { 313 | test.error(err); 314 | test.end(); 315 | } 316 | ); 317 | col.pick('Key', 'value'); 318 | }); 319 | 320 | metatests.test('collect then fail', (test) => { 321 | collect(5) 322 | .timeout(10) 323 | .then( 324 | (result) => { 325 | test.error(result); 326 | test.end(); 327 | }, 328 | (err) => { 329 | test.assert(err); 330 | test.end(); 331 | } 332 | ); 333 | }); 334 | --------------------------------------------------------------------------------