├── .cspell.json ├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── index.ts └── lib │ ├── D4C.spec.ts │ ├── D4C.ts │ ├── Queue.spec.ts │ └── Queue.ts ├── tsconfig.json ├── tsconfig.module.json └── yarn.lock /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", 4 | "language": "en", 5 | "words": [ 6 | "bitjson", 7 | "bitauth", 8 | "cimg", 9 | "circleci", 10 | "codecov", 11 | "commitlint", 12 | "dependabot", 13 | "editorconfig", 14 | "esnext", 15 | "execa", 16 | "exponentiate", 17 | "globby", 18 | "libauth", 19 | "mkdir", 20 | "prettierignore", 21 | "sandboxed", 22 | "transpiled", 23 | "typedoc", 24 | "untracked" 25 | ], 26 | "flagWords": [], 27 | "ignorePaths": [ 28 | "package.json", 29 | "package-lock.json", 30 | "yarn.lock", 31 | "tsconfig.json", 32 | "node_modules/**" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "project": "./tsconfig.json" }, 5 | "env": { "es6": true }, 6 | "ignorePatterns": ["node_modules", "build", "coverage"], 7 | "plugins": ["import", "eslint-comments", "functional"], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:eslint-comments/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/typescript", 13 | "plugin:functional/lite", 14 | "prettier", 15 | "prettier/@typescript-eslint" 16 | ], 17 | "globals": { "BigInt": true, "console": true, "WebAssembly": true }, 18 | "rules": { 19 | "functional/prefer-readonly-type":0, 20 | "functional/no-return-void":0, 21 | "functional/no-class":0, 22 | "functional/no-this-expression":0, 23 | "functional/no-throw-statement":0, 24 | "functional/immutable-data":0, 25 | "functional/functional-parameters":0, 26 | "functional/no-let":0, 27 | "@typescript-eslint/no-explicit-any":0, 28 | "@typescript-eslint/no-unused-vars":0, 29 | "prefer-spread":0, 30 | "eslint-comments/no-unlimited-disable": 0, 31 | "@typescript-eslint/explicit-module-boundary-types": "off", 32 | "eslint-comments/disable-enable-pair": [ 33 | "error", 34 | { "allowWholeFile": true } 35 | ], 36 | "eslint-comments/no-unused-disable": "warn", 37 | "import/order": [ 38 | "error", 39 | { "newlines-between": "always", "alphabetize": { "order": "asc" } } 40 | ], 41 | "sort-imports": [ 42 | "error", 43 | { "ignoreDeclarationSort": true, "ignoreCase": true } 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | - **What is the current behavior?** (You can also link to an open issue here) 4 | 5 | - **What is the new behavior (if this is a feature change)?** 6 | 7 | - **Other information**: 8 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [12.x, 14.x, 15.x, 16.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 16 | 17 | steps: 18 | # - uses: actions/checkout@v2 19 | # - name: Use Node.js ${{ matrix.node-version }} 20 | # uses: actions/setup-node@v2 21 | # with: 22 | # node-version: ${{ matrix.node-version }} 23 | # - run: npm ci 24 | # - run: npm run build --if-present 25 | # - run: npm test 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: Install dependencies 32 | run: yarn --frozen-lockfile 33 | - name: Run tests 34 | run: yarn test 35 | - name: Run coverage 36 | # env: 37 | # REPO_TOKEN: ${{ secrets.REPO_TOKEN }} 38 | run: yarn cov:lcov 39 | - name: Publish to coveralls.io 40 | uses: coverallsapp/github-action@v1.1.2 41 | with: 42 | github-token: ${{ github.token }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .nyc_output 3 | build 4 | node_modules 5 | test 6 | src/**.js 7 | coverage 8 | *.log 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "eamodio.gitlens", 6 | "streetsidesoftware.code-spell-checker", 7 | "editorconfig.editorconfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // To debug, make sure a *.spec.ts file is active in the editor, then run a configuration 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Active Spec", 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 10 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 11 | "port": 9229, 12 | "outputCapture": "std", 13 | "skipFiles": ["/**/*.js"], 14 | "preLaunchTask": "npm: build" 15 | // "smartStep": true 16 | }, 17 | { 18 | // Use this one if you're already running `yarn watch` 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug Active Spec (no build)", 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 23 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 24 | "port": 9229, 25 | "outputCapture": "std", 26 | "skipFiles": ["/**/*.js"] 27 | // "smartStep": true 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.userWords": [], // only use words from .cspell.json 3 | "cSpell.enabled": true, 4 | "editor.formatOnSave": true, 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.enablePromptUseWorkspaceTsdk": true, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "[typescript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | Those versions which only include documentation change might not be included here. 6 | 7 | ### [1.7.0](https://github.com/grimmer0125/d4c-queue/compare/v1.6.9...v1.7.0) (2021-11-15) 8 | 9 | Add option dropWhenReachLimit for better throtting effect. 10 | 11 | ### [1.6.9](https://github.com/grimmer0125/d4c-queue/compare/v1.6.5...v1.6.9) (2021-10-03) 12 | 13 | Update README. Fix potential security vulnerabilities in dependencies and gh-pages publishing. 14 | 15 | ### [1.6.5](https://github.com/grimmer0125/d4c-queue/compare/v1.6.4...v1.6.5) (2021-07-13) 16 | 17 | Update README and fix potential security vulnerabilities in dependencies. 18 | 19 | ### [1.6.4](https://github.com/grimmer0125/d4c-queue/compare/v1.6.0...v1.6.4) (2021-06-14) 20 | 21 | Update README and fix potential security vulnerabilities in dependencies. 22 | 23 | ### [1.6.0](https://github.com/grimmer0125/d4c-queue/compare/v1.5.10...v1.6.0) (2021-05-07) 24 | 25 | ⭐ Decorator concurrency mode ⭐ is added. 26 | 27 | ```ts 28 | @QConcurrency([ 29 | { limit: 100, isStatic: true }, 30 | { limit: 50, tag: '2' }, 31 | ]) 32 | class TestController { 33 | @concurrent 34 | static async fetchData(url: string) {} 35 | ``` 36 | 37 | #### ⚠ BREAKING CHANGES 38 | 39 | ```ts 40 | // orig: only setup one queue, omitting tag will apply default queue and new tag queue 41 | d4c = new D4C({ concurrency: { limit: 100 } }) 42 | d4c.setConcurrency({ limit: 10 }) 43 | 44 | // new. to setup multiple queue, omitting tag will only for deafult queue and not apply on new tag queue 45 | d4c = new D4C([{ concurrency: { limit: 100 } }]) 46 | d4c.setConcurrency([{ limit: 10 }]) 47 | ``` 48 | 49 | ### [1.5.10](https://github.com/grimmer0125/d4c-queue/compare/v1.5.9...v1.5.10) (2021-05-07) 50 | 51 | #### ⚠ BREAKING CHANGES 52 | 53 | ```ts 54 | /** orig */ 55 | d4c.setQueue({ concurrency: 10 }) 56 | 57 | /** new, rename parameter */ 58 | d4c.setConcurrency({ limit: 10 }) 59 | ``` 60 | 61 | - Allow D4C constructor can setup tag queue concurrency limit. `const d4c = new D4C({ limit: 100, tag: '2' });` 62 | - Improve README. 63 | 64 | ### [1.5.9](https://github.com/grimmer0125/d4c-queue/compare/v1.5.4...v1.5.9) (2021-05-07) 65 | 66 | Add TypeDoc site and refactor code and improve some test. 67 | 68 | ### [1.5.4](https://github.com/grimmer0125/d4c-queue/compare/v1.5.0...v1.5.4) (2021-05-07) 69 | 70 | #### ⚠ BREAKING CHANGES 71 | 72 | ```ts 73 | const d4c = new D4C(100) 74 | ``` 75 | 76 | To 77 | 78 | ```ts 79 | const d4c = new D4C({ concurrency: 100 }) 80 | ``` 81 | 82 | ### [1.5.0](https://github.com/grimmer0125/d4c-queue/compare/v1.4.5...v1.5.0) (2021-05-07) 83 | 84 | ⭐ New API ⭐ and minor bug fixes 85 | 86 | Add concurrency mode support for D4C instance usage. Previous it only supports synchronization mode (concurrency = 1). 87 | 88 | ```ts 89 | const d4c = new D4C(100) 90 | d4c.setQueue({ concurrency: 10 }) // change default concurrency from 1 to 10 91 | d4c.setQueue({ concurrency: 10, tag: 'queue2' }) // concurrency for queue2 92 | ``` 93 | 94 | ### [1.4.2](https://github.com/grimmer0125/d4c-queue/compare/v1.4.1...v1.4.2) (2021-05-06) 95 | 96 | Remove reflect-metadata 97 | 98 | ### [1.4.1](https://github.com/grimmer0125/d4c-queue/compare/v1.4.0...v1.4.1) (2021-05-05) 99 | 100 | Fix security vulnerabilities 101 | 102 | ### [1.4.0](https://github.com/grimmer0125/d4c-queue/compare/v1.3.12...v1.4.0) (2021-05-05) 103 | 104 | #### ⚠ BREAKING CHANGES 105 | 106 | Remove `@injectQ` decorator. Dynamically inject queues only when applying `@synchronized`. 107 | 108 | ### [1.3.12](https://github.com/grimmer0125/d4c-queue/compare/v1.3.11...v1.3.12) (2021-05-04) 109 | 110 | Add changelog link in README 111 | 112 | ### [1.3.11](https://github.com/grimmer0125/d4c-queue/compare/v1.3.10...v1.3.11) (2021-05-04) 113 | 114 | Fix internal queue bug 115 | 116 | ### [1.3.10](https://github.com/grimmer0125/d4c-queue/compare/v1.3.6...v1.3.10) (2021-05-04) 117 | 118 | Implement a FIFO queue instead of using third party library, denque 119 | 120 | ### [1.3.6](https://github.com/grimmer0125/d4c-queue/compare/v1.3.4...v1.3.6) (2021-05-03) 121 | 122 | Improve tests to 100% coverage 123 | 124 | ### [1.3.4](https://github.com/grimmer0125/d4c-queue/compare/v1.3.0...v1.3.4) (2021-05-02) 125 | 126 | Improve documentation and tests, and fix a bug about empty arguments in d4c.apply 127 | 128 | ### [1.3.0](https://github.com/grimmer0125/d4c-queue/compare/v1.2.6...v1.3.0) (2021-05-02) 129 | 130 | #### ⚠ BREAKING CHANGES 131 | 132 | - Improve queue system. Each instance/class is isolated with the others. 133 | - API breaking change. No more global usage. no more ~~dApply, dWrap~~, and add needed `@injectQ`. 134 | - back to es6 for main build. 135 | 136 | original: 137 | 138 | ```ts 139 | import { D4C, dApply, dWrap, synchronized } from 'd4c-queue' 140 | 141 | /** global usage*/ 142 | const asyncFunResult = await dWrap(asyncFun, { tag: 'queue1' })( 143 | 'asyncFun_arg1', 144 | 'asyncFun_arg2' 145 | ) 146 | 147 | /** instance usage */ 148 | const d4c = new D4C() 149 | d4c.apply(async) 150 | 151 | /** decorator usage */ 152 | class ServiceAdapter { 153 | @synchronized 154 | async connect() {} 155 | } 156 | ``` 157 | 158 | becomes 159 | 160 | ```ts 161 | import { D4C, injectQ, synchronized } from 'd4c-queue' 162 | 163 | /** instance usage */ 164 | d4c.apply(syncFun, { args: ['syncFun_arg1'] }) 165 | 166 | /** decorator usage */ 167 | @injectQ 168 | class ServiceAdapter { 169 | @synchronized 170 | async connect() {} 171 | } 172 | ``` 173 | 174 | ### [1.2.6](https://github.com/grimmer0125/d4c-queue/compare/v1.2.3...v1.2.6) (2021-05-01) 175 | 176 | - Inject default tag, and fix decorator defaultTag not take effect bug 177 | 178 | - Also fix a bug that class decorator defaultTag will be overwritten by 179 | some default tag. 180 | 181 | ### [1.2.3](https://github.com/grimmer0125/d4c-queue/compare/v1.2.2...v1.2.3) (2021-04-29) 182 | 183 | Try to build es5 package for main build 184 | 185 | ### [1.2.2](https://github.com/grimmer0125/d4c-queue/compare/v1.2.0...v1.2.2) (2021-04-29) 186 | 187 | Fix exporting module 188 | 189 | ### [1.2.0](https://github.com/grimmer0125/d4c-queue/compare/v1.1.5...v1.2.0) (2021-04-29) 190 | 191 | #### ⚠ BREAKING CHANGES 192 | 193 | Refactor API and instance method renaming 194 | 195 | Change static method to public function: 196 | 197 | ```ts 198 | D4C.apply -> dApply 199 | D4C.wrap -> dWrap 200 | D4C.synchronized -> synchronized 201 | D4C.register -> defaultTag 202 | ``` 203 | 204 | Change instance method naming: 205 | 206 | ```ts 207 | iapply -> apply 208 | iwrap -> wrap 209 | ``` 210 | 211 | ### [1.1.5](https://github.com/grimmer0125/d4c-queue/compare/v1.1.4...v1.1.5) (2021-04-28) 212 | 213 | Fix the bug about optional tag in @D4C.synchronized 214 | 215 | ### [1.1.4](https://github.com/grimmer0125/d4c-queue/compare/v1.1.0...v1.1.4) (2021-04-28) 216 | 217 | Add optional tag in @D4C.synchronized and fix 1.1.0 API change bug 218 | 219 | ### [1.1.0](https://github.com/grimmer0125/d4c-queue/compare/v1.0.0...v1.1.0) (2021-04-28) 220 | 221 | #### ⚠ BREAKING CHANGES 222 | 223 | Change API to let method decorator receive an option object, like global/instance usage. 224 | Also rename parameter, `nonBlockCurr` to `noBlockCurr`. 225 | 226 | ### 1.0.0 (2021-04-28) 227 | 228 | First release to https://www.npmjs.com/package/d4c-queue/v/1.0.0 229 | 230 | Features 231 | 232 | - global usage 233 | 234 | ```ts 235 | D4C.wrap(asyncFun, { tag: 'queue1' })('asyncFun_arg1', 'asyncFun_arg2') 236 | ``` 237 | 238 | - instance usage 239 | 240 | ```ts 241 | const d4c = new D4C() 242 | d4c.iwrap(asyncFun, { tag: 'queue1' })('asyncFun_arg1', 'asyncFun_arg2') 243 | ``` 244 | 245 | - decorator usage. 246 | 247 | ```ts 248 | @D4C.register(Symbol('jojo')) 249 | class ServiceAdapter { 250 | @D4C.synchronized 251 | client_send_message() {} 252 | } 253 | ``` 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Grimmer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # D4C Queue 2 | 3 | [![npm version](https://img.shields.io/npm/v/d4c-queue.svg)](https://www.npmjs.com/package/d4c-queue) ![example workflow](https://github.com/grimmer0125/d4c-queue/actions/workflows/node.js.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/grimmer0125/d4c-queue/badge.svg)](https://coveralls.io/github/grimmer0125/d4c-queue) 4 | 5 | Wrap an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)/[promise-returning](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)/`sync` function as a queue-ready async function, which is enqueued while being called. This is convenient to reuse it. In synchronization mode, task queues execute original functions sequentially by default (equivalently `concurrency limit = 1`). In concurrency mode, it allows changing concurrency limit to have concurrent tasks executed. It also supports `@synchronized`/`@concurrent` [decorator](https://www.typescriptlang.org/docs/handbook/decorators.html) on instance or static methods. Passing arguments and using `await` to get return values are also supported. 6 | 7 | ## Features 8 | 9 | 1. Two usages 10 | 1. D4C instance: synchronization mode & concurrency mode. 11 | 2. Class, instance, and static method decorators on classes: synchronization mode & concurrency mode. 12 | 2. Wrap a function to a new queue-ready async function. It is convenient to re-use this function. Also, it is able to pass arguments and get return value for each task function. 13 | 3. Support `async function`, a `promise-returning` function, and a `sync` function. 14 | 4. Sub queues system (via tags). 15 | 5. Support Browser and Node.js. 16 | 6. Fully Written in TypeScript and its `.d.ts` typing is out of box. JavaScript is supported, too. 17 | 7. This library implements a FIFO task queue for O(1) speed. Using built-in JavaScript array will have O(n) issue. 18 | 8. Well tested. 19 | 9. Optional parameter, `inheritPreErr`. If current task is waiting for previous tasks, set it as `true` to inherit the error of the previous task and the task will not be executed and throw a custom error `new PreviousError(task.preError.message ?? task.preError)`. If this parameter is omitted or set as `false`, the task will continue whether previous tasks happen errors or not. 20 | 10. Optional parameter, `noBlockCurr`. Set it as `true` to forcibly execute the current task in the another (microtask) execution of the event loop. This is useful if you pass a sync function as the first task but do not want it to block the current event loop. 21 | 11. Optional parameter, `dropWhenReachLimit`. Set it as `true`, then this function call will be dropped when the system detects the queue concurrency limit is reached. It is like a kind of throttle mechanism but not time interval based. The dropped function call will not be really executed and will throw a execption whose message is `QueueIsFull` and you need to catch it. 22 | 23 | ## Installation 24 | 25 | This package includes two builds. 26 | 27 | - ES6 build (ES2015) with CommonJS module for `main` build in package.json. 28 | - ES6 build (ES2015) with ES6 module for `module` build. Some tools will follow the `module` field in `package.json`, like Rollup, Webpack, or Parcel. 29 | 30 | Either `npm install d4c-queue` or `yarn add d4c-queue`. Then import this package. 31 | 32 | **ES6 import** 33 | 34 | ```typescript 35 | import { D4C, synchronized, QConcurrency, concurrent } from 'd4c-queue' 36 | ``` 37 | 38 | **CommonJS** 39 | 40 | ```typescript 41 | const { D4C, synchronized, QConcurrency, concurrent } = require('d4c-queue') 42 | ``` 43 | 44 | It is possible to use the `module` build with CommonJS require syntax in TypeScript or other build tools. 45 | 46 | ### Extra optional steps if you want to use decorators from this library 47 | 48 | Keep in mind that `decorators` are JavaScript proposals and may vary in the future. 49 | 50 | #### TypeScript users 51 | 52 | Modify your tsconfig.json to include the following settings 53 | 54 | ```json 55 | { 56 | "experimentalDecorators": true 57 | } 58 | ``` 59 | 60 | #### JavaScript users 61 | 62 | You can use Babel to support decorators, install `@babel/plugin-proposal-decorators`. 63 | 64 | For the users using **Create React App** JavaScript version, you can either use `eject` or [CRACO](https://github.com/gsoft-inc/craco) to customize your babel setting. Using create React App TypeScript Version just needs to modify `tsconfig.json.` 65 | 66 | See [babel.config.json](#babelconfigjson) in [Appendix](#Appendix). 67 | 68 | See [CRACO Setting](#craco-setting) in [Appendix](#Appendix). 69 | 70 | ## Usage example 71 | 72 | Keep in mind that a function will not be enqueued into a task queue even it becomes a new function after wrapping. A task will be enqueued only when it is executed. 73 | 74 | ### Designed queue system 75 | 76 | Each queue is isolated with the others. 77 | 78 | - Two instance of your decorated class will have two individual queue system. 79 | - The default queue in instance method queues is something like `@synchronized(self)` in other languages. 80 | - Each D4C instance will have its own queue system. 81 | 82 | ``` 83 | D4C queues (decorator) injected into your class: 84 | - instance method queues (per instance): 85 | - default queue 86 | - tag1 queue 87 | - tag2 queue 88 | - static method queues 89 | - default queue 90 | - tag1 queue 91 | - tag2 queue 92 | D4C instance queues (per D4C object): 93 | - default queue 94 | - tag1 queue 95 | - tag2 queue 96 | ``` 97 | 98 | ### D4C instance usage 99 | 100 | #### Synchronization mode 101 | 102 | ```typescript 103 | const d4c = new D4C() 104 | 105 | /** 106 | * in some execution of event loop 107 | * you can choose to await the result or not. 108 | */ 109 | const asyncFunResult = await d4c.wrap(asyncFun)( 110 | 'asyncFun_arg1', 111 | 'asyncFun_arg2' 112 | ) 113 | /** 114 | * in another execution of event loop. Either async or 115 | * sync function is ok. E.g., pass a sync function, 116 | * it will wait for asyncFun's finishing, then use await to get 117 | * the new wrapped async function's result. 118 | */ 119 | const syncFunFunResult = await d4c.wrap(syncFun)('syncFun_arg1') 120 | ``` 121 | 122 | Alternatively, you can use below 123 | 124 | ```typescript 125 | d4c.apply(syncFun, { args: ['syncFun_arg1'] }) 126 | ``` 127 | 128 | #### Concurrency mode 129 | 130 | Is it useful for rate-limiting or throttling tasks. For example, setup some concurrency limit to avoid send GitHub GraphQL API requests too fast, since it has rate limits control. 131 | 132 | Default concurrency limit of D4C instance is `1` in this library. 133 | 134 | Usage: 135 | 136 | ```ts 137 | /** change concurrency limit applied on default queues */ 138 | const d4c = new D4C([{ concurrency: { limit: 100 } }]) 139 | 140 | /** setup concurrency for specific queue: "2" */ 141 | const d4c = new D4C([{ concurrency: { limit: 100, tag: '2' } }]) 142 | ``` 143 | 144 | You can adjust concurrency via `setConcurrency`. 145 | 146 | ```ts 147 | const d4c = new D4C() 148 | /** change concurrency limit on default queue*/ 149 | d4c.setConcurrency([{ limit: 10 }]) 150 | 151 | /** change concurrency limit for queue2 */ 152 | d4c.setConcurrency([{ limit: 10, tag: 'queue2' }]) 153 | ``` 154 | 155 | When this async task function is called and the system detects the concurrency limit is reached, this tasks will not be really executed and will be enqueued. If you want to drop this task function call, you can set `dropWhenReachLimit` option when wrapping/applying the task function. e.g. 156 | 157 | ```ts 158 | const fn1 = d4c.wrap(taskFun, { dropWhenReachLimit: true }) 159 | 160 | try { 161 | await fn1() 162 | } catch (err) { 163 | // when the concurrency limit is reached at this moment. 164 | // err.message is QueueIsFull 165 | console.log({ err }) 166 | } 167 | ``` 168 | 169 | ### Decorators usage 170 | 171 | #### Synchronization mode 172 | 173 | ```typescript 174 | class ServiceAdapter { 175 | @synchronized 176 | async connect() {} 177 | 178 | @synchronized 179 | async client_send_message_wait_connect(msg: string) { 180 | // ... 181 | } 182 | 183 | //** parameters are optional */ 184 | @synchronized({ tag: 'world', inheritPreErr: true, noBlockCurr: true }) 185 | static async staticMethod(text: string) { 186 | return text 187 | } 188 | } 189 | ``` 190 | 191 | #### Concurrency mode 192 | 193 | `isStatic` is to specify this queue setting is for static method and default is false. omitting tag refers default queue. 194 | 195 | ```ts 196 | /** if omitting @QConcurrency, @concurrent will use its 197 | * default concurrency Infinity*/ 198 | @QConcurrency([ 199 | { limit: 100, isStatic: true }, 200 | { limit: 50, tag: '2' }, 201 | ]) 202 | class TestController { 203 | @concurrent 204 | static async fetchData(url: string) {} 205 | 206 | @concurrent({ tag: '2', dropWhenReachLimit: true }) 207 | async fetchData2(url: string) {} 208 | 209 | /** You can still use @synchronized, as long as 210 | * they are different queues*/ 211 | @synchronized({tag:'3') 212 | async connect() {} 213 | } 214 | ``` 215 | 216 | #### Arrow function property 217 | 218 | Using decorators on `arrow function property` does not work since some limitation. If you need the effect of arrow function, you can bind by yourself (e.g. `this.handleChange = this.handleChange.bind(this);`) or consider [autobind-decorator](https://www.npmjs.com/package/autobind-decorator) 219 | 220 | ```typescript 221 | @autobind 222 | @synchronized // should be the second line 223 | client_send_message_wait_connect(msg: string) { 224 | // ... 225 | } 226 | ``` 227 | 228 | Using D4C instance on `arrow function property` works well. 229 | 230 | ```ts 231 | class TestController { 232 | // alternative way 233 | // @autobind 234 | // bindMethodByArrowPropertyOrAutobind(){ 235 | // } 236 | 237 | bindMethodByArrowPropertyOrAutobind = async () => { 238 | /** access some property in this. accessible after wrapping*/ 239 | } 240 | } 241 | const d4c = new D4C() 242 | const res = await d4c.apply(testController.bindMethodByArrowPropertyOrAutobind) 243 | ``` 244 | 245 | ## Motivation and more detailed user scenarios 246 | 247 | ### Causality 248 | 249 | Sometimes a task function is better to be executed after the previous task function is finished. For example, assume you are writing a adapter to use a network client library to connect to a service, either in a React frontend or a Node.js backend program, and you do not want to block current event loop (e.g. using a UI indicator to wait) for this case, so `connect` is called first, later `send_message` is triggered in another UI event. In the adapter code, usually a flag can be used and do something like 250 | 251 | ```typescript 252 | send_message(msg: string) { 253 | if (this.connectingStatus === 'Connected') { 254 | // send message 255 | } else if (this.connectingStatus === 'Connecting') { 256 | // Um...how to wait for connecting successfully? 257 | } else (this.connectingStatus === 'Disconnected') { 258 | // try to re-connect 259 | } 260 | } 261 | ``` 262 | 263 | `Connecting` status is more ambiguous then `Disconnected` status. Now you can use a task queue to solve them. E.g., 264 | 265 | ```typescript 266 | class ServiceAdapter { 267 | async send_message(msg: string) { 268 | if (this.connectingStatus === 'Connected') { 269 | /** send message */ 270 | await client_send_message_without_wait_connect(msg) 271 | } else if (this.connectingStatus === 'Connecting') { 272 | /** send message */ 273 | await client_send_message_wait_connect(msg) 274 | } else { 275 | //.. 276 | } 277 | } 278 | 279 | @synchronized 280 | async connect() { 281 | // ... 282 | } 283 | 284 | @synchronized 285 | async client_send_message_wait_connect(msg: string) { 286 | // ... 287 | } 288 | 289 | async client_send_message_without_wait_connect(msg: string) { 290 | // ... 291 | } 292 | } 293 | ``` 294 | 295 | #### Another case: use D4C instance to guarantee the execution order 296 | 297 | The code snippet is from [embedded-pydicom-react-viewer](https://github.com/grimmer0125/embedded-pydicom-react-viewer). Some function only can be executed after init function is finished. 298 | 299 | ```typescript 300 | const d4c = new D4C() 301 | export const initPyodide = d4c.wrap(async () => { 302 | /** init Pyodide*/ 303 | }) 304 | 305 | /** without d4c-queue, it will throw exception while being called 306 | * before 'initPyodide' is finished */ 307 | export const parseByPython = d4c.wrap(async (buffer: ArrayBuffer) => { 308 | /** execute python code in browser */ 309 | }) 310 | ``` 311 | 312 | ### Race condition 313 | 314 | Concurrency may make race condition. And we usually use a synchronization mechanism (e.g. mutex) to solve it. A task queue can achieve this. 315 | 316 | It is similar to causality. Sometimes two function which access same data within and will result race condition if they are executed concurrently. Although JavaScript is single thread (except Node.js Worker threads, Web Workers and JS runtime), the intrinsic property of event loop may result in some unexpected race condition, e.g. 317 | 318 | ```typescript 319 | const func1 = async () => { 320 | console.log('func1 start, execution1 in event loop') 321 | await func3() 322 | console.log('func1 end, should not be same event loop execution1') 323 | } 324 | 325 | const func2 = async () => { 326 | console.log('func2') 327 | } 328 | 329 | async function testRaceCondition() { 330 | func1() // if add await will result in no race condition 331 | func2() 332 | } 333 | testRaceCondition() 334 | ``` 335 | 336 | `func2` will be executed when `func1` is not finished. 337 | 338 | #### Real world cases 339 | 340 | In backend, the practical example is to compare `Async/await` in [Express](https://expressjs.com/) framework and [Apollo](https://www.apollographql.com/docs/apollo-server/)/[NestJS](https://nestjs.com/) frameworks. [NestJS' GraphQL part](https://docs.nestjs.com/graphql/quick-start) is using Apollo and they have a different implementation than ExpressJS. [NestJS' Restful part](https://docs.nestjs.com/controllers) is the same as ExpressJS. 341 | 342 | No race condition on two API call in `Express`, any API will be executed one by one. After async handler callback function is finished, another starts to be executed. 343 | 344 | ```typescript 345 | /** Express case */ 346 | app.post('/testing', async (req, res) => { 347 | // Do something here 348 | }) 349 | ``` 350 | 351 | However, race condition may happen on two API call in `Apollo`/`NestJS`. 352 | 353 | ```typescript 354 | /** Apollo server case */ 355 | const resolvers = { 356 | Mutation: { 357 | orderBook: async (_, { email, book }, { dataSources }) => {}, 358 | }, 359 | Query: { 360 | books: async () => books, 361 | }, 362 | } 363 | ``` 364 | 365 | Two Apollo GraphQL queries/mutations may be executed concurrently, not like Express. This has advantage and disadvantage. If you need to worry about the possible race condition, you can consider this `d4c-queue` library, or `Database transaction` or [async-mutex](https://www.npmjs.com/package/async-mutex). You do not need to apply `d4c-queue` library on top API endpoint always, just apply on the place you worry about. 366 | 367 | #### NestJS GraphQL synchronized resolver example with this d4c-queue 368 | 369 | The below shows how to make `hello query` become `synchronized`. Keep in mind that `@synchronized` should be below `@Query`. 370 | 371 | ```typescript 372 | import { Query } from '@nestjs/graphql' 373 | import { synchronized } from 'd4c-queue' 374 | 375 | function delay() { 376 | return new Promise(function (resolve, reject) { 377 | setTimeout(function () { 378 | resolve('world') 379 | }, 10 * 1000) 380 | }) 381 | } 382 | 383 | export class TestsResolver { 384 | @Query((returns) => String) 385 | /** without @synchronized, two resolver may print 1/2 1/2 2/2 2/2 386 | * with @synchronized, it prints: 1/2 2/2 2/2 2/2 387 | */ 388 | @synchronized 389 | async hello() { 390 | console.log('hello graphql resolver part: 1/2') 391 | const resp = await delay() 392 | console.log('hello graphql resolver part: 2/2') 393 | return resp 394 | } 395 | } 396 | ``` 397 | 398 | ### Convenience 399 | 400 | To use async functions, sometimes we just `await async_func1()` to wait for its finishing then start to call `async_func2`. But if we also do not want to use `await` to block current event loop? The workaround way is to make another wrapper function manually to detach, like below 401 | 402 | ```typescript 403 | async wrap_function() { 404 | await async_func1() 405 | await async_func2() 406 | } 407 | 408 | current_function() { 409 | // just call 410 | wrap_function() 411 | 412 | // continue current following code 413 | // .. 414 | } 415 | ``` 416 | 417 | Use this library can easily achieve, becomes 418 | 419 | ```typescript 420 | current_function() { 421 | const d4c = new D4C(); 422 | d4c.apply(async_func1); 423 | d4c.apply(async_func2); 424 | } 425 | ``` 426 | 427 | ### Throttle case: avoid more and more delaying UI updating events as time grow 428 | 429 | Besides rate-limit cases (e.g. server side limit), another case is you trigger mouse move too often, and these events will cause some function calls (either calculation or API calls) and UI wait for these results to update in `await` way. It will not happen permanent UI dealy situation if these all happen in the same UI main thread since the funciton calls will avoid mouse move events been produced. But if these function calls are happend in another system (async http request) or calculation on web works (another thread), it may result in UI thread triggering too faster than the calcultion/consuming capability, which means it is over the performance bottlenect and you will see the UI updating delayed and delayed. The solution is to use `throttle` to limit the mouse event producing. This is why `dropWhenReachLimit` is introduced. 430 | 431 | ## API 432 | 433 | The parameters in the below signatures are optional. `inheritPreErr` and `noBlockCurr` are false by default. `tag` can overwrite the default tag and **specify different queue** for this method or function. 434 | 435 | You can check the generated [TypeDoc site](https://grimmer.io/d4c-queue/modules/_lib_d4c_.html). 436 | 437 | ### Decorators: 438 | 439 | - @QConcurrency 440 | 441 | setup a array of queue settings 442 | 443 | ```ts 444 | // use with @concurrent 445 | function QConcurrency( 446 | queuesParam: Array<{ 447 | limit: number 448 | tag?: string | symbol 449 | isStatic?: boolean 450 | }> 451 | ) {} 452 | 453 | // example: 454 | @QConcurrency([ 455 | { limit: 100, isStatic: true }, 456 | { limit: 50, tag: '2' }, 457 | ]) 458 | class TestController {} 459 | ``` 460 | 461 | - @synchronized & @concurrent 462 | 463 | ```typescript 464 | function synchronized(option?: { 465 | inheritPreErr?: boolean 466 | noBlockCurr?: boolean 467 | tag?: string | symbol 468 | }) {} 469 | 470 | /** default concurrency limit is Infinity, // use with @QConcurrency */ 471 | function concurrent(option?: { 472 | tag?: string | symbol 473 | inheritPreErr?: boolean 474 | noBlockCurr?: boolean 475 | dropWhenReachLimit?: boolean 476 | }) {} 477 | ``` 478 | 479 | Example: 480 | 481 | ```typescript 482 | @synchronized 483 | @synchronized() 484 | @synchronized({ tag: "world", inheritPreErr: true }) 485 | @synchronized({ inheritPreErr: true, noBlockCurr: true }) 486 | 487 | @concurrent 488 | @concurrent() 489 | @concurrent({ tag: "world", inheritPreErr: true }) 490 | @concurrent({ inheritPreErr: true, noBlockCurr: true, dropWhenReachLimit: true }) 491 | 492 | ``` 493 | 494 | See [decorators-usage](#decorators-usage) 495 | 496 | ### D4C instance usage 497 | 498 | Make a instance first, there is a default tag so using `tag` parameter to specify some queue is optional. 499 | 500 | - constructor 501 | 502 | ```ts 503 | constructor(queuesParam?: Array<{ tag?: string | symbol, limit?: number }>) { 504 | ``` 505 | 506 | usage: 507 | 508 | ```typescript 509 | /** default concurrency is 1*/ 510 | const d4c = new D4C() 511 | 512 | /** concurrency limit 500 applied on default queues */ 513 | const d4c = new D4C([{ concurrency: { limit: 500 } }]) 514 | 515 | /** setup concurrency for specific queue: "2" */ 516 | const d4c = new D4C([{ concurrency: { limit: 100, tag: '2' } }]) 517 | ``` 518 | 519 | - setConcurrency 520 | 521 | ```ts 522 | d4c.setConcurrency([{ limit: 10 }]) 523 | 524 | d4c.setConcurrency([{ limit: 10, tag: 'queue2' }]) 525 | ``` 526 | 527 | - wrap 528 | 529 | ```typescript 530 | public wrap( 531 | func: T, 532 | option?: { 533 | tag?: string | symbol; 534 | inheritPreErr?: boolean; 535 | noBlockCurr?: boolean; 536 | dropWhenReachLimit?: boolean; 537 | } 538 | ) 539 | ``` 540 | 541 | If original func is a async function, `wrap` will return `a async function` whose parameters and returned value's type (a.k.a. `Promise`) and value are same as original func. 542 | 543 | If original func is a sync function, `wrap` will return `a async function` whose parameters are the same as the original function, and returned value's promise generic type is the same as original func. Which means it becomes a awaitable async function, besides queueing. 544 | 545 | - apply 546 | 547 | ```typescript 548 | public apply( 549 | func: T, 550 | option?: { 551 | tag?: string | symbol; 552 | inheritPreErr?: boolean; 553 | noBlockCurr?: boolean; 554 | dropWhenReachLimit?: boolean; 555 | args?: Parameters; 556 | } 557 | ) 558 | ``` 559 | 560 | Almost the same as `wrap` but just directly executing the original function call, e.g. 561 | 562 | ```typescript 563 | const newFunc = d4c.wrap(asyncFun, { tag: "queue1" }) 564 | newFunc("asyncFun_arg1", "asyncFun_arg2");) 565 | ``` 566 | 567 | becomes 568 | 569 | ```typescript 570 | d4c.apply(asyncFun, { args: ['asyncFun_arg1'], tag: 'queue1' }) 571 | ``` 572 | 573 | ## Changelog 574 | 575 | Check [here](https://github.com/grimmer0125/d4c-queue/blob/master/CHANGELOG.md) 576 | 577 | ## Appendix 578 | 579 | I use `babel-node index.js` with the following setting to test. 580 | 581 | ### babel.config.json 582 | 583 | ```json 584 | { 585 | "presets": ["@babel/preset-env"], 586 | "plugins": [ 587 | [ 588 | "@babel/plugin-proposal-decorators", 589 | { 590 | "legacy": true 591 | } 592 | ] 593 | ] 594 | } 595 | ``` 596 | 597 | ### CRACO setting 598 | 599 | Follow its site, [CRACO](https://github.com/gsoft-inc/craco). 600 | 601 | 1. `yarn add @craco/craco` 602 | 2. Replace `react-scripts` with `craco` in `package.json` 603 | 3. `yarn add @babel/preset-env @babel/plugin-proposal-decorators` 604 | 4. Touch `craco.config.js` and modify its content as the following 605 | 5. Then just `yarn start`. 606 | 607 | `craco.config.js` (roughly same as `babel.config.json`): 608 | 609 | ```javascript 610 | module.exports = { 611 | babel: { 612 | presets: [['@babel/preset-env']], 613 | plugins: [['@babel/plugin-proposal-decorators', { legacy: true }]], 614 | loaderOptions: {}, 615 | loaderOptions: (babelLoaderOptions, { env, paths }) => { 616 | return babelLoaderOptions 617 | }, 618 | }, 619 | } 620 | ``` 621 | 622 | ### Angular service example 623 | 624 | ```typescript 625 | import { Injectable } from '@angular/core' 626 | import { QConcurrency, concurrent } from 'd4c-queue' 627 | 628 | // can be placed below @Injectable, too 629 | @QConcurrency([{ limit: 1 }]) 630 | @Injectable({ 631 | providedIn: 'root', 632 | }) 633 | export class HeroService { 634 | @concurrent 635 | async task1() { 636 | await wait(5 * 1000) 637 | } 638 | 639 | @concurrent 640 | async task2() { 641 | await wait(1 * 1000) 642 | } 643 | } 644 | ``` 645 | 646 | ### Use latest GitHub code of this library 647 | 648 | 1. git clone this repo 649 | 2. in cloned project folder, `yarn link` 650 | 3. `yarn test` or `yarn build` 651 | 4. in your project, `yarn link d4c-queue`. Do above ES6/CommonJS import to start to use. 652 | 5. in your project, `yarn unlink d4c-queue` to uninstall. 653 | 654 | The development environment of this library is Node.js v15.14.0 & Visual Studio Code. TypeScript 4.2.3 is also used and will be automatically installed in node_modules. [typescript-starter](https://github.com/bitjson/typescript-starter) is used to generate two builds, `main` and `module` via its setting. Some example code is in [tests](https://github.com/grimmer0125/d4c-queue/blob/master/src/lib/D4C.spec.ts). 655 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d4c-queue", 3 | "version": "1.7.1", 4 | "description": "A task queue executes tasks sequentially or concurrently. Wrap an async/promise-returning/sync function as a queue-ready async function for easy reusing. Support passing arguments/getting return value, @synchronized/@concurrent decorator, Node.js/Browser.", 5 | "main": "build/main/index.js", 6 | "typings": "build/main/index.d.ts", 7 | "module": "build/module/index.js", 8 | "repository": "https://github.com/grimmer0125/d4c-queue", 9 | "homepage": "https://grimmer0125.github.io/d4c-queue", 10 | "license": "MIT", 11 | "keywords": [ 12 | "task queue", 13 | "async", 14 | "promise", 15 | "await", 16 | "sequential", 17 | "synchronized", 18 | "decorator", 19 | "lock", 20 | "concurrency", 21 | "throttle", 22 | "typescript", 23 | "javascript", 24 | "node", 25 | "browser", 26 | "rate-limit", 27 | "apollo", 28 | "nestjs", 29 | "asynchronous", 30 | "worker", 31 | "async function", 32 | "concurrent", 33 | "synchronous", 34 | "job", 35 | "sync", 36 | "mutex", 37 | "synchronization", 38 | "queue", 39 | "fifo", 40 | "rate", 41 | "ratelimit", 42 | "limit", 43 | "limited", 44 | "throat", 45 | "task", 46 | "arrow function", 47 | "task-queue", 48 | "tasks", 49 | "task-runner", 50 | "microtask", 51 | "angular" 52 | ], 53 | "scripts": { 54 | "build": "run-p build:*", 55 | "build:main": "tsc -p tsconfig.json", 56 | "build:module": "tsc -p tsconfig.module.json", 57 | "fix": "run-s fix:*", 58 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 59 | "fix:lint": "eslint src --ext .ts --fix", 60 | "test": "run-s build test:*", 61 | "test:lint": "eslint src --ext .ts", 62 | "checkspelling": "cspell \"{README.md,.github/*.md,src/**/*.ts}\"", 63 | "test:unit": "nyc --silent ava", 64 | "check-cli": "run-s test diff-integration-tests check-integration-tests", 65 | "check-integration-tests": "run-s check-integration-test:*", 66 | "diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'", 67 | "watch:build": "tsc -p tsconfig.json -w", 68 | "watch:test": "nyc --silent ava --watch", 69 | "cov": "run-s build test:unit cov:html cov:lcov && open-cli coverage/index.html", 70 | "cov:html": "nyc report --reporter=html", 71 | "cov:lcov": "nyc report --reporter=lcov", 72 | "cov:send": "run-s cov:lcov && codecov", 73 | "cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100", 74 | "doc": "run-s doc:html && open-cli build/docs/index.html", 75 | "doc:html": "typedoc src/ --exclude **/*.spec.ts --target ES6 --exclude src/lib/Queue.ts --excludePrivate --excludeNotExported --out build/docs", 76 | "doc:json": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --json build/docs/typedoc.json", 77 | "doc:publish": "touch build/docs/.nojekyll && gh-pages -m \"[ci skip] Updates\" -d build/docs -t -r https://github.com/grimmer0125/d4c-queue.git", 78 | "version": "standard-version", 79 | "reset-hard": "git clean -dfx && git reset --hard && yarn", 80 | "prepare-release": "run-s reset-hard test cov:check doc:html version doc:publish" 81 | }, 82 | "engines": { 83 | "node": ">=10" 84 | }, 85 | "dependencies": {}, 86 | "devDependencies": { 87 | "@ava/typescript": "^1.1.1", 88 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 89 | "@types/node": "^14.14.41", 90 | "@typescript-eslint/eslint-plugin": "^4.0.1", 91 | "@typescript-eslint/parser": "^4.0.1", 92 | "autobind-decorator": "^2.4.0", 93 | "ava": "^3.12.1", 94 | "codecov": "^3.5.0", 95 | "cspell": "^4.1.0", 96 | "cz-conventional-changelog": "^3.3.0", 97 | "eslint": "^7.8.0", 98 | "eslint-config-prettier": "^6.11.0", 99 | "eslint-plugin-eslint-comments": "^3.2.0", 100 | "eslint-plugin-functional": "^3.0.2", 101 | "eslint-plugin-import": "^2.22.0", 102 | "gh-pages": "^3.1.0", 103 | "npm-run-all": "^4.1.5", 104 | "nyc": "^15.1.0", 105 | "open-cli": "^6.0.1", 106 | "prettier": "^2.1.1", 107 | "standard-version": "^9.0.0", 108 | "touch": "^3.1.0", 109 | "ts-node": "^9.0.0", 110 | "typedoc": "^0.19.0", 111 | "typescript": "4.2.3" 112 | }, 113 | "resolutions": { 114 | "merge": "^2.1.1", 115 | "trim-newlines": "^3.0.1", 116 | "normalize-url": "^4.5.1", 117 | "browserslist": "^4.16.5", 118 | "set-value": "^4.0.1", 119 | "path-parse": "^1.0.7", 120 | "ansi-regex": "^5.0.1" 121 | }, 122 | "files": [ 123 | "build/main", 124 | "build/module", 125 | "!**/*.spec.*", 126 | "!**/*.json", 127 | "CHANGELOG.md", 128 | "LICENSE", 129 | "README.md" 130 | ], 131 | "ava": { 132 | "failFast": true, 133 | "timeout": "60s", 134 | "typescript": { 135 | "rewritePaths": { 136 | "src/": "build/main/" 137 | } 138 | }, 139 | "files": [ 140 | "!build/module/**" 141 | ] 142 | }, 143 | "config": { 144 | "commitizen": { 145 | "path": "cz-conventional-changelog" 146 | } 147 | }, 148 | "nyc": { 149 | "extends": "@istanbuljs/nyc-config-typescript", 150 | "exclude": [ 151 | "**/*.spec.js" 152 | ] 153 | } 154 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { D4C, synchronized, QConcurrency, concurrent } from './lib/D4C'; 3 | -------------------------------------------------------------------------------- /src/lib/D4C.spec.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator' 2 | import test from 'ava' 3 | 4 | import { concurrent, D4C, ErrMsg, QConcurrency, synchronized } from './D4C' 5 | 6 | const fixture = ['hello'] 7 | const fixture2 = 'world' 8 | const queueTag = 'queue1' 9 | 10 | const funcAsync = async ( 11 | input?: string[], 12 | input2?: string 13 | ): Promise => { 14 | return input[0] + input2 15 | } 16 | 17 | const funcSync = (input: string[], input2: string): string => { 18 | return input[0] + input2 19 | } 20 | 21 | const funcPromise = (input: string[], input2: string): Promise => { 22 | return Promise.resolve(input[0] + input2) 23 | } 24 | 25 | const timeout = (seconds: number, target?: { str: string }) => { 26 | return new Promise((resolve, _) => 27 | setTimeout(() => { 28 | if (target?.str != undefined && target?.str != null) { 29 | target.str += seconds 30 | } 31 | resolve() 32 | }, seconds * 100) 33 | ) 34 | } 35 | 36 | const timeoutError = (seconds: number, result: string | Error) => { 37 | return new Promise((_, reject) => 38 | setTimeout(() => { 39 | reject(result) 40 | }, seconds * 100) 41 | ) 42 | } 43 | 44 | const immediateFun = (seconds: number, target: { str: string }) => { 45 | target.str += seconds 46 | } 47 | 48 | const immediateFunPromise = (seconds: number, target: { str: string }) => { 49 | target.str += seconds 50 | return Promise.resolve() 51 | } 52 | 53 | test('Class usage: test concurrency', async (t) => { 54 | @QConcurrency([ 55 | { limit: 100, isStatic: true }, 56 | { limit: 100, isStatic: false }, 57 | { limit: 1, isStatic: true, tag: '2' }, 58 | { limit: 1, tag: '2' }, 59 | ]) 60 | class TestController { 61 | @concurrent 62 | static async staticTimeout(seconds: number, obj: { str: string }) { 63 | await timeout(seconds, obj) 64 | } 65 | 66 | @concurrent 67 | async instanceTimeout(seconds: number, obj: { str: string }) { 68 | await timeout(seconds, obj) 69 | } 70 | 71 | @concurrent({ tag: '2' }) 72 | static async staticTimeout2(seconds: number, obj: { str: string }) { 73 | await timeout(seconds, obj) 74 | } 75 | 76 | @concurrent({ tag: '2' }) 77 | async instanceTimeout2(seconds: number, obj: { str: string }) { 78 | await timeout(seconds, obj) 79 | } 80 | 81 | @synchronized({ tag: '3' }) 82 | async random(seconds: number, obj: { str: string }) { 83 | await timeout(seconds, obj) 84 | } 85 | } 86 | // test: concurrency on static method, and use default tag 87 | let test = { str: '' } 88 | await Promise.all([ 89 | TestController.staticTimeout(0.5, test), 90 | TestController.staticTimeout(0.1, test), 91 | ]) 92 | t.is(test.str, '0.10.5') 93 | 94 | // test: concurrency on static method, 95 | test = { str: '' } 96 | await Promise.all([ 97 | TestController.staticTimeout2(0.5, test), 98 | TestController.staticTimeout2(0.1, test), 99 | ]) 100 | t.is(test.str, '0.50.1') 101 | 102 | // test: concurrency on instance method, and use default tag 103 | const testController = new TestController() 104 | test = { str: '' } 105 | await Promise.all([ 106 | testController.instanceTimeout(0.5, test), 107 | testController.instanceTimeout(0.1, test), 108 | ]) 109 | t.is(test.str, '0.10.5') 110 | 111 | // test: concurrency on instance method 112 | test = { str: '' } 113 | await Promise.all([ 114 | testController.instanceTimeout2(0.5, test), 115 | testController.instanceTimeout2(0.1, test), 116 | ]) 117 | t.is(test.str, '0.50.1') 118 | 119 | // test: no use class decorator so @concurrent will use default @concurrency Infinity 120 | class TestController2 { 121 | @concurrent 122 | static async staticTimeout(seconds: number, obj: { str: string }) { 123 | await timeout(seconds, obj) 124 | } 125 | } 126 | test = { str: '' } 127 | await Promise.all([ 128 | TestController2.staticTimeout(0.5, test), 129 | TestController2.staticTimeout(0.1, test), 130 | ]) 131 | t.is(test.str, '0.10.5') 132 | 133 | @QConcurrency([ 134 | { limit: 1, isStatic: true }, 135 | { limit: 1, isStatic: false }, 136 | { limit: 1, isStatic: true, tag: '2' }, 137 | { limit: 1, isStatic: false, tag: '2' }, 138 | ]) 139 | class TestController3 { 140 | static async staticTimeoutNoDecorator( 141 | seconds: number, 142 | obj: { str: string } 143 | ) { 144 | await timeout(seconds, obj) 145 | } 146 | 147 | @concurrent 148 | static async staticTimeout(seconds: number, obj: { str: string }) { 149 | await timeout(seconds, obj) 150 | } 151 | 152 | @concurrent 153 | async instanceTimeout(seconds: number, obj: { str: string }) { 154 | await timeout(seconds, obj) 155 | } 156 | 157 | @concurrent({ tag: '2' }) 158 | static async staticTimeout2(seconds: number, obj: { str: string }) { 159 | await timeout(seconds, obj) 160 | } 161 | 162 | @concurrent({ tag: '2' }) 163 | async instanceTimeout2(seconds: number, obj: { str: string }) { 164 | await timeout(seconds, obj) 165 | } 166 | } 167 | 168 | // Test: applying @classConcurrency but testing method not decorated will not be affected 169 | test = { str: '' } 170 | await Promise.all([ 171 | TestController3.staticTimeoutNoDecorator(0.5, test), 172 | TestController3.staticTimeoutNoDecorator(0.1, test), 173 | ]) 174 | t.is(test.str, '0.10.5') 175 | 176 | // Test: different queues are isolated with each other 177 | const testController3 = new TestController3() 178 | test = { str: '' } 179 | await Promise.all([ 180 | TestController.staticTimeout(0.5, test), 181 | testController.instanceTimeout(0.4, test), 182 | TestController.staticTimeout2(0.3, test), 183 | testController.instanceTimeout2(0.2, test), 184 | ]) 185 | t.is(test.str, '0.20.30.40.5') 186 | 187 | /** @classConcurrency tries to setup default queue's concurrency but is conflicted with synchronized (implicitly concurrency =1) */ 188 | let error = null 189 | try { 190 | @QConcurrency([{ limit: 1, isStatic: true }]) 191 | class TestController4 { 192 | static async staticTimeoutNoDecorator( 193 | seconds: number, 194 | obj: { str: string } 195 | ) { 196 | await timeout(seconds, obj) 197 | } 198 | 199 | @synchronized 200 | static async staticTimeout(seconds: number, obj: { str: string }) { 201 | await timeout(seconds, obj) 202 | } 203 | } 204 | } catch (err) { 205 | error = err 206 | } 207 | t.is(error.message, ErrMsg.ClassAndMethodDecoratorsIncompatible) 208 | 209 | /** for test coverage */ 210 | error = null 211 | try { 212 | @QConcurrency([{ limit: 1, isStatic: true, tag: '2' }]) 213 | class TestController5 { 214 | static async staticTimeoutNoDecorator( 215 | seconds: number, 216 | obj: { str: string } 217 | ) { 218 | await timeout(seconds, obj) 219 | } 220 | 221 | @synchronized 222 | static async staticTimeout(seconds: number, obj: { str: string }) { 223 | await timeout(seconds, obj) 224 | } 225 | } 226 | } catch (err) { 227 | error = err 228 | } 229 | t.is(error, null) 230 | 231 | /** for test coverage */ 232 | error = null 233 | try { 234 | @QConcurrency([{ limit: 1, isStatic: true, tag: '2' }]) 235 | class TestController6 { 236 | @concurrent 237 | static async staticTimeout(seconds: number, obj: { str: string }) { 238 | await timeout(seconds, obj) 239 | } 240 | } 241 | } catch (err) { 242 | error = err 243 | } 244 | t.is(error, null) 245 | 246 | /** for test coverage */ 247 | error = null 248 | try { 249 | @QConcurrency([{ limit: 1, tag: '2' }]) 250 | class TestController7 { 251 | @synchronized 252 | async staticTimeout(seconds: number, obj: { str: string }) { 253 | await timeout(seconds, obj) 254 | } 255 | } 256 | } catch (err) { 257 | error = err 258 | } 259 | t.is(error, null) 260 | 261 | /** for test coverage */ 262 | error = null 263 | try { 264 | @QConcurrency([ 265 | { limit: 1, tag: '2' }, 266 | { limit: 1, tag: '3', isStatic: true }, 267 | ]) 268 | class TestController8 { 269 | async staticTimeout(seconds: number, obj: { str: string }) { 270 | await timeout(seconds, obj) 271 | } 272 | } 273 | } catch (err) { 274 | error = err 275 | } 276 | t.is(error, null) 277 | 278 | /** for the same tag queue, @concurrent & @synchronized can not be applied both */ 279 | error = null 280 | try { 281 | @QConcurrency([{ limit: 1, isStatic: true }]) 282 | class TestController9 { 283 | static async staticTimeoutNoDecorator( 284 | seconds: number, 285 | obj: { str: string } 286 | ) { 287 | await timeout(seconds, obj) 288 | } 289 | 290 | @synchronized 291 | static async staticTimeout(seconds: number, obj: { str: string }) { 292 | await timeout(seconds, obj) 293 | } 294 | 295 | @concurrent 296 | static async staticTimeout2(seconds: number, obj: { str: string }) { 297 | await timeout(seconds, obj) 298 | } 299 | } 300 | } catch (err) { 301 | error = err 302 | } 303 | t.is(error.message, ErrMsg.TwoDecoratorsIncompatible) 304 | 305 | /** @classConcurrency' parameters should be an array */ 306 | error = null 307 | try { 308 | @QConcurrency({ limit: 1, tag: '2' } as any) 309 | class TestController10 { 310 | async staticTimeout(seconds: number, obj: { str: string }) { 311 | await timeout(seconds, obj) 312 | } 313 | } 314 | } catch (err) { 315 | error = err 316 | } 317 | t.is(error.message, ErrMsg.InvalidClassDecoratorParameter) 318 | 319 | /** limit should be a number */ 320 | error = null 321 | try { 322 | @QConcurrency([null, { limit: '3' }] as any) 323 | class TestController11 { 324 | async staticTimeout(seconds: number, obj: { str: string }) { 325 | await timeout(seconds, obj) 326 | } 327 | } 328 | } catch (err) { 329 | error = err 330 | } 331 | t.is(error.message, ErrMsg.InvalidClassDecoratorParameter) 332 | }) 333 | 334 | test('Instance usage: test concurrency', async (t) => { 335 | /** Fixme: dummy cases for code coverage */ 336 | let d4c = new D4C([null] as any) 337 | d4c = new D4C([{}] as any) 338 | d4c.setConcurrency(null) 339 | 340 | /** default queue: concurrency 100 test */ 341 | let test = { str: '' } 342 | d4c = new D4C([{ concurrency: { limit: 100 } }]) 343 | let fn1 = d4c.wrap(timeout) 344 | let fn2 = d4c.wrap(immediateFun) 345 | let fn3 = d4c.wrap(immediateFunPromise) 346 | await Promise.all([ 347 | fn1(2, test), 348 | fn2(1, test), 349 | fn1(0.5, test), 350 | fn3(0.2, test), 351 | fn1(0.05, test), 352 | ]) 353 | t.is(test.str, '10.20.050.52') 354 | 355 | /** tag queue: concurrency 100 test */ 356 | test = { str: '' } 357 | d4c = new D4C([{ concurrency: { limit: 100, tag: '2' } }]) 358 | fn1 = d4c.wrap(timeout, { tag: '2' }) 359 | fn2 = d4c.wrap(immediateFun, { tag: '2' }) 360 | fn3 = d4c.wrap(immediateFunPromise, { tag: '2' }) 361 | await Promise.all([ 362 | fn1(2, test), 363 | fn2(1, test), 364 | fn1(0.5, test), 365 | fn3(0.2, test), 366 | fn1(0.05, test), 367 | ]) 368 | t.is(test.str, '10.20.050.52') 369 | 370 | /** default queue: use setConcurrency to change concurrency on default to 100 */ 371 | test = { str: '' } 372 | d4c = new D4C() 373 | d4c.setConcurrency([{ limit: 100 }]) 374 | fn1 = d4c.wrap(timeout) 375 | fn2 = d4c.wrap(immediateFun) 376 | fn3 = d4c.wrap(immediateFunPromise) 377 | await Promise.all([ 378 | fn1(2, test), 379 | fn2(1, test), 380 | fn1(0.5, test), 381 | fn3(0.2, test), 382 | fn1(0.05, test), 383 | ]) 384 | t.is(test.str, '10.20.050.52') 385 | 386 | /** new tag with setConcurrency to set concurrency 1 */ 387 | test = { str: '' } 388 | d4c.setConcurrency([{ limit: 1, tag: '2' }]) 389 | fn1 = d4c.wrap(timeout, { tag: '2' }) 390 | fn2 = d4c.wrap(immediateFun, { tag: '2' }) 391 | fn3 = d4c.wrap(immediateFunPromise, { tag: '2' }) 392 | await Promise.all([ 393 | fn1(2, test), 394 | fn2(1, test), 395 | fn1(0.5, test), 396 | fn3(0.2, test), 397 | fn1(0.05, test), 398 | ]) 399 | t.is(test.str, '210.50.20.05') 400 | 401 | /** default queue: use setConcurrency to set it back to 1 */ 402 | test = { str: '' } 403 | d4c.setConcurrency([{ limit: 1 }]) 404 | fn1 = d4c.wrap(timeout) 405 | fn2 = d4c.wrap(immediateFun) 406 | fn3 = d4c.wrap(immediateFunPromise) 407 | await Promise.all([ 408 | fn1(2, test), 409 | fn2(1, test), 410 | fn1(0.5, test), 411 | fn3(0.2, test), 412 | fn1(0.05, test), 413 | ]) 414 | t.is(test.str, '210.50.20.05') 415 | 416 | let error = null 417 | try { 418 | d4c.setConcurrency([undefined] as any) 419 | } catch (err) { 420 | error = err 421 | } 422 | t.is(error.message, ErrMsg.InvalidQueueConcurrency) 423 | 424 | error = null 425 | try { 426 | d4c.setConcurrency([{ limit: -100 }]) 427 | } catch (err) { 428 | error = err 429 | } 430 | t.is(error.message, ErrMsg.InvalidQueueConcurrency) 431 | 432 | error = null 433 | try { 434 | d4c.setConcurrency([{ tag: true, limit: 100 }] as any) 435 | } catch (err) { 436 | error = err 437 | } 438 | t.is(error.message, ErrMsg.InvalidQueueTag) 439 | 440 | error = null 441 | try { 442 | d4c.setConcurrency([{ tag: true }] as any) 443 | } catch (err) { 444 | error = err 445 | } 446 | t.is(error.message, ErrMsg.InvalidQueueConcurrency) 447 | 448 | error = null 449 | try { 450 | d4c.setConcurrency([{ tag: true, limit: '100' }] as any) 451 | } catch (err) { 452 | error = err 453 | } 454 | t.is(error.message, ErrMsg.InvalidQueueConcurrency) 455 | 456 | error = null 457 | try { 458 | d4c = new D4C([{ concurrency: { limit: '11' } }] as any) 459 | } catch (err) { 460 | error = err 461 | } 462 | t.is(error.message, ErrMsg.InvalidQueueConcurrency) 463 | }) 464 | 465 | test('Instance usage: pass a class arrow function property', async (t) => { 466 | class TestController { 467 | greeting: string 468 | constructor(message: string) { 469 | this.greeting = message 470 | } 471 | 472 | greet = async (text: string) => { 473 | const str = 'Hello, ' + text + this.greeting 474 | return str 475 | } 476 | } 477 | const d4c = new D4C() 478 | const test = new TestController('!!') 479 | const newFunc = d4c.wrap(test.greet, { tag: queueTag }) 480 | const job = newFunc(fixture2) 481 | const resp = await job 482 | t.is(resp, 'Hello, world!!') 483 | 484 | /** wrap_exec part */ 485 | const resp2 = await d4c.apply(test.greet, { 486 | tag: queueTag, 487 | args: [fixture2], 488 | }) 489 | t.is(resp2, 'Hello, world!!') 490 | }) 491 | 492 | test('Decorator usage', async (t) => { 493 | class TestController { 494 | greeting: string 495 | constructor(message: string) { 496 | this.greeting = message 497 | this.testManualBind = this.testManualBind.bind(this) 498 | } 499 | 500 | @synchronized 501 | greet(text: string) { 502 | const str = 'Hello, ' + text + this.greeting 503 | return str 504 | } 505 | 506 | @synchronized() 507 | async testManualBind(text: string) { 508 | const str = 'Hello, ' + text + this.greeting 509 | return str 510 | } 511 | 512 | @autobind 513 | @synchronized() 514 | async testAutoBind(text: string) { 515 | const str = 'Hello, ' + text + this.greeting 516 | return str 517 | } 518 | 519 | @synchronized({}) 520 | static async staticMethod(text: string) { 521 | return text 522 | } 523 | 524 | @synchronized 525 | static async timeout(seconds: number, obj: { str: string }) { 526 | await timeout(seconds, obj) 527 | } 528 | 529 | @synchronized({ tag: '2' }) 530 | static async timeoutAnotherQueue(seconds: number, obj: { str: string }) { 531 | await timeout(seconds, obj) 532 | } 533 | 534 | @synchronized({ inheritPreErr: true }) 535 | async instanceTimeout(seconds: number, obj: { str: string }) { 536 | await timeout(seconds, obj) 537 | } 538 | 539 | @synchronized({}) 540 | async instanceTimeoutError(seconds: number, obj) { 541 | await timeoutError(seconds, obj) 542 | } 543 | 544 | @synchronized({ noBlockCurr: true }) 545 | testNoBlockCurr(seconds: number, obj: { str: string }) { 546 | obj.str += seconds 547 | } 548 | 549 | @autobind 550 | autobindMethodNoQueue(text: string) { 551 | const str = 'Hello, ' + text + this.greeting 552 | return str 553 | } 554 | } 555 | 556 | // /** instance method */ 557 | const testController = new TestController('!!') 558 | t.is(await testController.greet(fixture2), 'Hello, world!!') 559 | 560 | /** test if this lib working with manual bind */ 561 | const testManualBind = testController.testManualBind 562 | t.is(await testManualBind(fixture2), 'Hello, world!!') 563 | 564 | /** test if this lib working with auto bind */ 565 | const testAutoBind = testController.testAutoBind 566 | t.is(await testAutoBind(fixture2), 'Hello, world!!') 567 | 568 | /** static method part */ 569 | t.is(await TestController.staticMethod(fixture2), 'world') 570 | 571 | /** Test if they are really executed one by one */ 572 | let test = { str: '' } 573 | await Promise.all([ 574 | TestController.timeout(0.5, test), 575 | TestController.timeout(0.1, test), 576 | ]) 577 | t.is(test.str, '0.50.1') 578 | 579 | /** Test if these are really use different queues */ 580 | test = { str: '' } 581 | await Promise.all([ 582 | TestController.timeout(0.5, test), 583 | TestController.timeoutAnotherQueue(0.1, test), 584 | ]) 585 | t.is(test.str, '0.10.5') 586 | 587 | //** Static and Instance method should have different queues */ 588 | test = { str: '' } 589 | await Promise.all([ 590 | TestController.timeout(0.5, test), 591 | testController.instanceTimeout(0.1, test), 592 | ]) 593 | t.is(test.str, '0.10.5') 594 | 595 | //** Two instances should have different queues */ 596 | const testController2 = new TestController('!!') 597 | test = { str: '' } 598 | await Promise.all([ 599 | testController2.instanceTimeout(0.5, test), 600 | testController.instanceTimeout(0.1, test), 601 | ]) 602 | t.is(test.str, '0.10.5') 603 | 604 | /** Class instance and D4C instance should have different queues */ 605 | test = { str: '' } 606 | const fn = new D4C().wrap(timeout) 607 | await Promise.all([fn(0.5, test), testController.instanceTimeout(0.1, test)]) 608 | t.is(test.str, '0.10.5') 609 | 610 | /** composite case: D4C instance on no autobind decorated method */ 611 | let error = null 612 | try { 613 | const d4c = new D4C() 614 | const newFunc = d4c.wrap(testController.greet) 615 | const resp = await newFunc('') 616 | } catch (err) { 617 | error = err 618 | } 619 | t.is(error.message, ErrMsg.MissingThisDueBindIssue) 620 | 621 | /** composite case: D4C instance on autobind decorated method */ 622 | const d4c = new D4C() 623 | const result = await d4c.apply(testController.testAutoBind, { 624 | args: ['world'], 625 | }) 626 | t.is(result, 'Hello, world!!') 627 | 628 | /** composite case: D4C instance on autobind non-decorated method */ 629 | t.is( 630 | await d4c.apply(testController.autobindMethodNoQueue), 631 | 'Hello, undefined!!' 632 | ) 633 | 634 | /** Two class should not affect each other */ 635 | class TestController2 { 636 | greeting: string 637 | constructor(message: string) { 638 | this.greeting = message 639 | } 640 | 641 | @synchronized({}) 642 | static async timeout(seconds: number, obj: { str: string }) { 643 | await timeout(seconds, obj) 644 | } 645 | } 646 | test = { str: '' } 647 | await Promise.all([ 648 | TestController.timeout(0.5, test), 649 | TestController2.timeout(0.1, test), 650 | ]) 651 | t.is(test.str, '0.10.5') 652 | 653 | /** test invalid decorator */ 654 | error = null 655 | try { 656 | class TestController4 { 657 | @synchronized({ tag: true } as any) 658 | static async greet(text: string) { 659 | return text 660 | } 661 | } 662 | await TestController4.greet(fixture2), 'world' 663 | } catch (err) { 664 | error = err 665 | } 666 | t.is(error.message, ErrMsg.InvalidDecoratorOption) 667 | 668 | /** test if option inheritPreErr works on decorator */ 669 | ;(async () => { 670 | try { 671 | await testController.instanceTimeoutError(1, 'some_error') 672 | } catch (err) { 673 | // console.log(" err by purpose") 674 | } 675 | })() 676 | 677 | error = null 678 | try { 679 | await testController.instanceTimeout(0.1, { str: '' }) 680 | } catch (err) { 681 | error = err 682 | } 683 | t.is(error.message, 'some_error') 684 | 685 | /** test if option noBlockCurr works on decorator */ 686 | test = { str: '' } 687 | const job = testController.testNoBlockCurr(2, test) 688 | test.str = test.str + '1' 689 | await job 690 | t.is(test.str, '12') 691 | }) 692 | 693 | test('Instance usage: funcAsync, symbol tag', async (t) => { 694 | const d4c = new D4C() 695 | const job = d4c.wrap(funcAsync, { tag: Symbol('123') })(fixture, fixture2) 696 | t.is(await job, 'helloworld') 697 | }) 698 | 699 | test('Instance usage: funcAsync, a invalid null tag case', async (t) => { 700 | const d4c = new D4C() 701 | let error 702 | try { 703 | await d4c.wrap(funcAsync, { tag: null }) 704 | } catch (err) { 705 | error = err 706 | } 707 | t.is(error.message, ErrMsg.InstanceInvalidTag) 708 | }) 709 | 710 | test('Instance usage: async function', async (t) => { 711 | const d4c = new D4C() 712 | const newFunc = d4c.wrap(funcAsync, { tag: queueTag }) 713 | const job = newFunc(fixture, fixture2) 714 | const resp = await job 715 | t.is(resp, 'helloworld') 716 | }) 717 | 718 | test('Instance usage: non-async function', async (t) => { 719 | const d4c = new D4C() 720 | const newFunc = d4c.wrap(funcSync, { tag: queueTag }) 721 | const job = newFunc(fixture, fixture2) 722 | const resp = await job 723 | t.is(resp, 'helloworld') 724 | }) 725 | 726 | test('Instance usage: promise-returning function', async (t) => { 727 | const d4c = new D4C() 728 | const job = d4c.wrap(funcPromise, { tag: queueTag })(fixture, fixture2) 729 | const resp = await job 730 | t.is(resp, 'helloworld') 731 | }) 732 | 733 | test('Instance usage: apply a funcAsync', async (t) => { 734 | const d4c = new D4C() 735 | 736 | /** apply */ 737 | const resp = await d4c.apply(funcAsync, { 738 | tag: queueTag, 739 | args: [['33', '44'], '5'], 740 | }) 741 | t.is(resp, '335') 742 | }) 743 | 744 | test('Instance usage: test if queue really work, execute one by one', async (t) => { 745 | let test = { str: '' } 746 | await Promise.all([ 747 | timeout(2, test), 748 | immediateFun(1, test), 749 | timeout(0.5, test), 750 | immediateFunPromise(0.2, test), 751 | timeout(0.05, test), 752 | ]) 753 | t.is(test.str, '10.20.050.52') // 1, 0.2, 0.05, 0.5, 2 754 | 755 | test = { str: '' } 756 | const d4c = new D4C() 757 | const fn1 = d4c.wrap(timeout) 758 | const fn2 = d4c.wrap(immediateFun) 759 | const fn3 = d4c.wrap(immediateFunPromise) 760 | await Promise.all([ 761 | fn1(2, test), 762 | fn2(1, test), 763 | fn1(0.5, test), 764 | fn3(0.2, test), 765 | fn1(0.05, test), 766 | ]) 767 | t.is(test.str, '210.50.20.05') 768 | 769 | test = { str: '' } 770 | const d4c2 = new D4C() 771 | const fn11 = d4c2.wrap(timeout, { tag: '1' }) 772 | const fn21 = d4c2.wrap(immediateFun, { tag: '2' }) 773 | const fn31 = d4c2.wrap(immediateFunPromise, { tag: '3' }) 774 | await Promise.all([ 775 | fn11(2, test), 776 | fn21(1, test), 777 | fn11(0.5, test), 778 | fn31(0.2, test), 779 | fn11(0.05, test), 780 | ]) 781 | t.is(test.str, '10.220.50.05') // 1, 0.2, 2, 0.5, 0.05 782 | 783 | test = { str: '' } 784 | const fn12 = new D4C().wrap(timeout) 785 | const fn22 = new D4C().wrap(immediateFun) 786 | const fn32 = new D4C().wrap(immediateFunPromise) 787 | await Promise.all([ 788 | fn12(2, test), 789 | fn22(1, test), 790 | fn12(0.5, test), 791 | fn32(0.2, test), 792 | fn12(0.05, test), 793 | ]) 794 | t.is(test.str, '10.220.50.05') 795 | }) 796 | 797 | test('Instance usage: option noBlockCurr enable, with two non-async function ', async (t) => { 798 | const d4c = new D4C() 799 | let testStr = '' 800 | testStr += '1' 801 | const newFunc = d4c.wrap( 802 | () => { 803 | testStr += 'inFuncSyn' 804 | }, 805 | { tag: queueTag, noBlockCurr: true } 806 | ) 807 | testStr += '2' 808 | const job = newFunc() 809 | testStr += '3' 810 | 811 | const newFunc2 = d4c.wrap( 812 | () => { 813 | testStr += 'inFuncSyn2' 814 | }, 815 | { tag: queueTag } 816 | ) 817 | await Promise.all([job, newFunc2()]) 818 | testStr += '4' 819 | t.is(testStr, '123inFuncSyninFuncSyn24') 820 | }) 821 | 822 | test("Instance usage: option inheritPreErr enable: task2 inherit task1's error in object queue", async (t) => { 823 | const d4c = new D4C() 824 | 825 | const fun2 = async () => { 826 | console.log('dummy fun2') 827 | } 828 | 829 | const fun1ErrorProducer = async () => { 830 | try { 831 | await d4c.wrap(timeoutError)(1, new Error('some_error')) 832 | } catch (_) { 833 | // console.log(" err by purpose") 834 | } 835 | } 836 | 837 | fun1ErrorProducer() 838 | 839 | let error 840 | try { 841 | await d4c.wrap(fun2, { inheritPreErr: true })() 842 | } catch (err) { 843 | error = err 844 | } 845 | 846 | t.is(error.message, 'some_error') 847 | }) 848 | 849 | test('Instance usage: test option dropWhenReachLimit', async (t) => { 850 | const d4c = new D4C([{ concurrency: { limit: 2 } }]) 851 | const fn1 = d4c.wrap(timeout, { dropWhenReachLimit: true }) 852 | 853 | let error = null 854 | try { 855 | await fn1(3) 856 | await Promise.all([fn1(3), fn1(3), fn1(3)]) 857 | } catch (err) { 858 | error = err 859 | } 860 | t.is(error.message, ErrMsg.QueueIsFull) 861 | }) 862 | -------------------------------------------------------------------------------- /src/lib/D4C.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from './Queue' 2 | 3 | type Task = { 4 | unlock: (value?: unknown) => void 5 | preError?: Error 6 | inheritPreErr?: boolean 7 | } 8 | 9 | type TaskQueue = { 10 | queue: Queue 11 | /** isRunning will be removed since runningTask can cover its effect */ 12 | isRunning: boolean 13 | concurrency: number 14 | runningTask: number 15 | } 16 | 17 | type UnwrapPromise = T extends Promise 18 | ? U 19 | : T extends (...args: any) => Promise 20 | ? U 21 | : T extends (...args: any) => infer U 22 | ? U 23 | : T 24 | 25 | type QueueTag = string | symbol 26 | type TaskQueuesType = Map 27 | type IAnyFn = (...args: any[]) => Promise | any 28 | type MethodDecoratorType = ( 29 | target: any, 30 | propertyKey: string, 31 | descriptor: PropertyDescriptor 32 | ) => void 33 | 34 | export enum ErrMsg { 35 | InstanceInvalidTag = 'instanceInvalidTag: it should be string/symbol/undefined', 36 | InvalidDecoratorOption = 'not valid option when using decorators', 37 | InvalidQueueConcurrency = 'invalidQueueConcurrency', 38 | InvalidQueueTag = 'invalidQueueTag', 39 | InvalidClassDecoratorParameter = 'invalidClassDecoratorParameter', 40 | TwoDecoratorsIncompatible = 'TwoDecoratorsInCompatible', 41 | ClassAndMethodDecoratorsIncompatible = 'ClassAndMethodDecoratorsIncompatible', 42 | MissingThisDueBindIssue = 'missingThisDueBindIssue', 43 | QueueIsFull = 'QueueIsFull', 44 | } 45 | 46 | const queueSymbol = Symbol('d4cQueues') // subQueue system 47 | const concurrentSymbol = Symbol('concurrent') // record the concurrency of each instance method decorator's tag 48 | const isConcurrentSymbol = Symbol('isConcurrent') // record isConcurrent of each instance method decorator's tag 49 | 50 | const defaultTag = Symbol('D4C') 51 | 52 | const DEFAULT_CONCURRENCY = 1 53 | 54 | export class PreviousTaskError extends Error { 55 | constructor(message) { 56 | super(message) 57 | this.name = 'PreviousError' 58 | } 59 | } 60 | 61 | function checkIfClassConcurrencyApplyOnSynchronizedMethod( 62 | target, 63 | usedTag: string | symbol 64 | ) { 65 | // true means isConcurrent, false means sync, undefined means no static method decorator on this tag 66 | if (target[isConcurrentSymbol][usedTag] === undefined) { 67 | return 68 | } else if (target[isConcurrentSymbol][usedTag] === false) { 69 | throw new Error(ErrMsg.ClassAndMethodDecoratorsIncompatible) 70 | } 71 | } 72 | 73 | /** 74 | * Class decorator to setup concurrency for queues 75 | * @param queuesParam a array of each queue parameter 76 | */ 77 | export function QConcurrency( 78 | queuesParam: Array<{ 79 | limit: number 80 | tag?: string | symbol 81 | isStatic?: boolean 82 | }> 83 | ): ClassDecorator { 84 | if (!Array.isArray(queuesParam)) { 85 | throw new Error(ErrMsg.InvalidClassDecoratorParameter) 86 | } 87 | 88 | /** target is constructor */ 89 | return (target) => { 90 | queuesParam.forEach((queueParam) => { 91 | if (!queueParam) { 92 | return 93 | } 94 | const { tag, limit, isStatic } = queueParam 95 | if ( 96 | !checkTag(tag) || 97 | typeof limit !== 'number' || 98 | (isStatic !== undefined && typeof isStatic !== 'boolean') 99 | ) { 100 | throw new Error(ErrMsg.InvalidClassDecoratorParameter) 101 | } 102 | 103 | const usedTag = tag ?? defaultTag 104 | 105 | /** TODO: refactor below as they are use similar code */ 106 | if (isStatic) { 107 | // check if at least one static method is using @synchronized/@concurrent 108 | if (!target[queueSymbol]) { 109 | return 110 | } 111 | 112 | checkIfClassConcurrencyApplyOnSynchronizedMethod(target, usedTag) 113 | 114 | /** inject concurrency info for each tag in static method case */ 115 | if (target[concurrentSymbol]?.[usedTag]) { 116 | target[concurrentSymbol][usedTag] = limit 117 | } 118 | } else { 119 | // check if at least one instance method is using @synchronized/@concurrent 120 | if (target.prototype[queueSymbol] !== null) { 121 | return 122 | } 123 | 124 | checkIfClassConcurrencyApplyOnSynchronizedMethod( 125 | target.prototype, 126 | usedTag 127 | ) 128 | 129 | /** inject concurrency info for each tag in instance method case */ 130 | if (target.prototype[concurrentSymbol]?.[usedTag]) { 131 | target.prototype[concurrentSymbol][usedTag] = limit 132 | } 133 | } 134 | }) 135 | } 136 | } 137 | 138 | function checkTag(tag) { 139 | if (tag === undefined || typeof tag === 'string' || typeof tag === 'symbol') { 140 | return true 141 | } 142 | 143 | return false 144 | } 145 | 146 | function checkIfTwoDecoratorsHaveSameConcurrentValue( 147 | target, 148 | tag: QueueTag, 149 | isConcurrent: boolean 150 | ) { 151 | // init 152 | if (!target[isConcurrentSymbol]) { 153 | target[isConcurrentSymbol] = {} 154 | } 155 | 156 | // check if two decorators for same queue have same isConcurrency value 157 | if (target[isConcurrentSymbol][tag] === undefined) { 158 | target[isConcurrentSymbol][tag] = isConcurrent 159 | } else if (target[isConcurrentSymbol][tag] !== isConcurrent) { 160 | throw new Error(ErrMsg.TwoDecoratorsIncompatible) 161 | } 162 | 163 | /** set default concurrency is infinity for @concurrent on instance/static methods*/ 164 | if (isConcurrent) { 165 | if (!target[concurrentSymbol]) { 166 | target[concurrentSymbol] = {} 167 | } 168 | target[concurrentSymbol][tag] = Infinity 169 | } 170 | } 171 | 172 | function injectQueue( 173 | constructorOrPrototype, 174 | tag: string | symbol, 175 | isConcurrent: boolean 176 | ) { 177 | if (constructorOrPrototype.prototype) { 178 | // constructor, means static method 179 | if (!constructorOrPrototype[queueSymbol]) { 180 | constructorOrPrototype[queueSymbol] = new Map< 181 | string | symbol, 182 | TaskQueue 183 | >() 184 | } 185 | } else { 186 | // prototype, means instance method 187 | if (constructorOrPrototype[queueSymbol] !== null) { 188 | constructorOrPrototype[queueSymbol] = null 189 | } 190 | } 191 | 192 | checkIfTwoDecoratorsHaveSameConcurrentValue( 193 | constructorOrPrototype, 194 | tag, 195 | isConcurrent 196 | ) 197 | } 198 | 199 | /** if class has a static member call inheritPreErr, even no using parentheses, 200 | * targetOrOption will have targetOrOption property but its type is function */ 201 | function checkIfDecoratorOptionObject(obj: any): boolean { 202 | /** still count valid argument, e.g. @synchronized(null) */ 203 | if (obj === undefined || obj === null) { 204 | return true 205 | } 206 | 207 | /** 208 | * hasOwnProperty should be false since it is a literal object 209 | */ 210 | if ( 211 | typeof obj === 'object' && 212 | //eslint-disable-next-line 213 | !obj.hasOwnProperty('constructor') && 214 | (typeof obj.inheritPreErr === 'boolean' || 215 | obj.inheritPreErr === undefined) && 216 | (typeof obj.noBlockCurr === 'boolean' || obj.noBlockCurr === undefined) && 217 | (typeof obj.dropWhenReachLimit === 'boolean' || 218 | obj.dropWhenReachLimit === undefined) && 219 | checkTag(obj.tag) 220 | ) { 221 | return true 222 | } 223 | return false 224 | } 225 | 226 | /** 227 | * Static and instance method decorator. Default concurrency = Infinity. 228 | * usage example: 229 | * ```typescript 230 | * @concurrent 231 | * async fetchData() {} 232 | * // or 233 | * @concurrent({ tag: 'world', inheritPreErr: true, noBlockCurr: true }) 234 | * static async fetchData(url: string) {} 235 | * ``` 236 | * */ 237 | export function concurrent( 238 | target: any, 239 | propertyKey: string, 240 | descriptor: PropertyDescriptor 241 | ): void 242 | export function concurrent(option?: { 243 | tag?: string | symbol 244 | inheritPreErr?: boolean 245 | noBlockCurr?: boolean 246 | dropWhenReachLimit?: boolean 247 | }): MethodDecoratorType 248 | export function concurrent( 249 | targetOrOption?: any, 250 | propertyKey?: string, 251 | descriptor?: PropertyDescriptor 252 | ): void | MethodDecoratorType { 253 | return _methodDecorator(targetOrOption, propertyKey, descriptor, true) 254 | } 255 | 256 | /** 257 | * Static and instance method decorator. Default concurrency = 1 for lock. 258 | * usage example: 259 | * ```typescript 260 | * @synchronize 261 | * async connect() {} 262 | * // or 263 | * @synchronized({ tag: 'world', inheritPreErr: true, noBlockCurr: true }) 264 | * static async staticMethod(text: string) {} 265 | * ``` 266 | * */ 267 | export function synchronized( 268 | target: any, 269 | propertyKey: string, 270 | descriptor: PropertyDescriptor 271 | ): void 272 | export function synchronized(option?: { 273 | tag?: string | symbol 274 | inheritPreErr?: boolean 275 | noBlockCurr?: boolean 276 | }): MethodDecoratorType 277 | export function synchronized( 278 | targetOrOption?: any, 279 | propertyKey?: string, 280 | descriptor?: PropertyDescriptor 281 | ): void | MethodDecoratorType { 282 | return _methodDecorator(targetOrOption, propertyKey, descriptor, false) 283 | } 284 | 285 | function _methodDecorator( 286 | targetOrOption: any, 287 | propertyKey: string, 288 | descriptor: PropertyDescriptor, 289 | isConcurrent: boolean 290 | ): void | MethodDecoratorType { 291 | if (checkIfDecoratorOptionObject(targetOrOption)) { 292 | /** parentheses case containing option (=targetOrOption) */ 293 | return function ( 294 | target: any, 295 | propertyKey: string, 296 | descriptor: PropertyDescriptor 297 | ) { 298 | injectQueue(target, targetOrOption?.tag ?? defaultTag, isConcurrent) 299 | 300 | const originalMethod = descriptor.value 301 | const newFunc = _q(null, originalMethod, targetOrOption) 302 | descriptor.value = newFunc 303 | } 304 | } else { 305 | /** no parentheses case */ 306 | const type = typeof targetOrOption 307 | 308 | /** 309 | * static method decorator case: target type is constructor function. use target.prototype 310 | * method decorator case: target is a prototype object, not literally object. use target 311 | */ 312 | if ( 313 | (type === 'function' || targetOrOption.hasOwnProperty('constructor')) && // eslint-disable-line 314 | typeof propertyKey === 'string' && 315 | typeof descriptor === 'object' && 316 | typeof descriptor.value === 'function' 317 | ) { 318 | injectQueue(targetOrOption, defaultTag, isConcurrent) 319 | 320 | const originalMethod = descriptor.value 321 | const newFunc = _q(null, originalMethod, {}) 322 | descriptor.value = newFunc 323 | } else { 324 | throw new Error(ErrMsg.InvalidDecoratorOption) 325 | } 326 | } 327 | } 328 | 329 | function _q( 330 | d4cObj: { queues: TaskQueuesType; defaultConcurrency: number }, 331 | func: T, 332 | option?: { 333 | tag?: QueueTag 334 | inheritPreErr?: boolean 335 | noBlockCurr?: boolean 336 | dropWhenReachLimit?: boolean 337 | } 338 | ): (...args: Parameters) => Promise> { 339 | return async function (...args: any[]): Promise { 340 | /** Detect tag */ 341 | let tag: QueueTag 342 | if (option?.tag !== undefined) { 343 | tag = option.tag 344 | } else { 345 | tag = defaultTag 346 | } 347 | 348 | let decoratorConcurrencyLimit: number 349 | 350 | /** Assign queues */ 351 | let taskQueue: TaskQueue 352 | let currTaskQueues: TaskQueuesType 353 | if (d4cObj) { 354 | /** D4C instance case */ 355 | currTaskQueues = d4cObj.queues 356 | } else if (this && (this[queueSymbol] || this[queueSymbol] === null)) { 357 | if (this[queueSymbol] === null) { 358 | /** instance method decorator first time case, using injected queues in user defined objects*/ 359 | this[queueSymbol] = new Map() 360 | } 361 | 362 | currTaskQueues = this[queueSymbol] 363 | decoratorConcurrencyLimit = this[concurrentSymbol]?.[tag] 364 | } else { 365 | throw new Error(ErrMsg.MissingThisDueBindIssue) 366 | } 367 | 368 | /** Get sub-queue */ 369 | taskQueue = currTaskQueues.get(tag) 370 | if (!taskQueue) { 371 | taskQueue = { 372 | queue: new Queue(), 373 | isRunning: false, 374 | runningTask: 0, 375 | /** D4C instance usage ?? (Decorator usage - specified limit ?? decorator - unspecified case) */ 376 | concurrency: 377 | d4cObj?.defaultConcurrency ?? 378 | decoratorConcurrencyLimit ?? 379 | DEFAULT_CONCURRENCY, 380 | } 381 | currTaskQueues.set(tag, taskQueue) 382 | } 383 | 384 | /** Detect if the queue is running or not, use promise to wait it if it is running */ 385 | let result 386 | let err: Error 387 | let task: Task 388 | if (taskQueue.runningTask === taskQueue.concurrency) { 389 | if (!option?.dropWhenReachLimit) { 390 | const promise = new Promise(function (resolve) { 391 | task = { 392 | unlock: resolve, 393 | preError: null, 394 | inheritPreErr: option?.inheritPreErr, 395 | } 396 | }) 397 | taskQueue.queue.push(task) 398 | await promise 399 | taskQueue.runningTask += 1 400 | } else { 401 | // drop this time, throttle mechanism 402 | throw new Error(ErrMsg.QueueIsFull) 403 | } 404 | } else if (option?.noBlockCurr) { 405 | taskQueue.runningTask += 1 406 | await Promise.resolve() 407 | } else { 408 | taskQueue.runningTask += 1 409 | } 410 | 411 | /** Run the task */ 412 | if (task?.preError) { 413 | err = new PreviousTaskError(task.preError.message ?? task.preError) 414 | } else { 415 | try { 416 | /** this will be constructor function for static method case */ 417 | const value = func.apply(this, args) 418 | 419 | /** Detect if it is a async/promise function or not */ 420 | if (value && typeof value.then === 'function') { 421 | result = await value 422 | } else { 423 | result = value 424 | } 425 | } catch (error) { 426 | err = error 427 | } 428 | } 429 | taskQueue.runningTask -= 1 430 | 431 | /** After the task is executed, check the following tasks */ 432 | if (taskQueue.queue.length > 0) { 433 | const nextTask: Task = taskQueue.queue.shift() 434 | /** Pass error to next task */ 435 | if (err && nextTask.inheritPreErr) { 436 | nextTask.preError = err 437 | } 438 | nextTask.unlock() 439 | } 440 | 441 | if (err) { 442 | throw err 443 | } 444 | 445 | return result 446 | } as (...args: Parameters) => Promise> 447 | } 448 | 449 | export class D4C { 450 | private queues: TaskQueuesType 451 | 452 | private defaultConcurrency = DEFAULT_CONCURRENCY 453 | 454 | /** 455 | * Default concurrency is 1. Omitting tag means it is for default queue. 456 | * If you specify concurrency limit for some tag queue, 457 | * this instance will not use that tag queue by default. 458 | */ 459 | constructor( 460 | queuesParam?: Array<{ 461 | concurrency: { tag?: string | symbol; limit?: number } 462 | }> 463 | ) { 464 | this.queues = new Map() 465 | if (Array.isArray(queuesParam)) { 466 | queuesParam.forEach((option) => { 467 | if (option?.concurrency?.limit > 0) { 468 | this._setConcurrency(option.concurrency) 469 | } 470 | }) 471 | } 472 | } 473 | 474 | /** 475 | * @param option tag is optional for specific queue. omitting is for default queue 476 | * @param option.limit is limit of concurrency and should be >= 1 477 | */ 478 | setConcurrency( 479 | queuesParam: Array<{ 480 | tag?: string | symbol 481 | limit: number 482 | }> 483 | ) { 484 | if (Array.isArray(queuesParam)) { 485 | queuesParam.forEach((option) => { 486 | this._setConcurrency(option) 487 | }) 488 | } 489 | } 490 | 491 | private _setConcurrency(concurrency?: { 492 | tag?: string | symbol 493 | limit?: number 494 | }) { 495 | if ( 496 | concurrency?.limit === undefined || 497 | typeof concurrency.limit !== 'number' 498 | ) { 499 | throw new Error(ErrMsg.InvalidQueueConcurrency) 500 | } 501 | 502 | const { tag, limit } = concurrency 503 | if (limit < 1) { 504 | throw new Error(ErrMsg.InvalidQueueConcurrency) 505 | } 506 | if (!checkTag(tag)) { 507 | throw new Error(ErrMsg.InvalidQueueTag) 508 | } 509 | 510 | // TODO: refactor this, _q has similar code */ 511 | let usedTag: string | symbol 512 | if (tag !== undefined) { 513 | usedTag = tag 514 | } else { 515 | usedTag = defaultTag 516 | } 517 | 518 | // TODO: refactor, other places have similar code 519 | let taskQueue = this.queues.get(usedTag) 520 | if (!taskQueue) { 521 | taskQueue = { 522 | queue: new Queue(), 523 | isRunning: false, 524 | runningTask: 0, 525 | concurrency: limit, 526 | } 527 | } else { 528 | taskQueue.concurrency = limit 529 | } 530 | 531 | this.queues.set(usedTag, taskQueue) 532 | } 533 | 534 | /** It wraps original function for queue ready and executes it*/ 535 | apply( 536 | func: T, 537 | option?: { 538 | tag?: string | symbol 539 | inheritPreErr?: boolean 540 | noBlockCurr?: boolean 541 | dropWhenReachLimit?: boolean 542 | args?: Parameters 543 | } 544 | ): Promise> { 545 | const resp = this.wrap(func, option).apply(null, option?.args) 546 | return resp 547 | } 548 | 549 | /** It wraps original function for queue ready */ 550 | wrap( 551 | func: T, 552 | option?: { 553 | tag?: string | symbol 554 | inheritPreErr?: boolean 555 | noBlockCurr?: boolean 556 | dropWhenReachLimit?: boolean 557 | } 558 | ): (...args: Parameters) => Promise> { 559 | if (!option || checkTag(option.tag)) { 560 | return _q( 561 | { 562 | queues: this.queues, 563 | defaultConcurrency: this.defaultConcurrency, 564 | }, 565 | func, 566 | option 567 | ) 568 | } 569 | throw new Error(ErrMsg.InstanceInvalidTag) 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /src/lib/Queue.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { Queue } from "./Queue" 4 | test("Test FIFO queue", (t) => { 5 | const queue = new Queue(); 6 | queue.push(1); 7 | queue.push(2); 8 | t.is(queue.shift(), 1); 9 | t.is(queue.shift(), 2); 10 | t.is(queue.shift(), undefined); 11 | t.is(queue.length, 0) 12 | queue.push(3); 13 | t.is(queue.shift(), 3); 14 | 15 | const strQueue = new Queue(); 16 | strQueue.push("1"); 17 | strQueue.push("2"); 18 | t.is(strQueue.shift(), "1"); 19 | t.is(strQueue.shift(), "2"); 20 | t.is(strQueue.shift(), undefined); 21 | }); 22 | -------------------------------------------------------------------------------- /src/lib/Queue.ts: -------------------------------------------------------------------------------- 1 | class Node { 2 | public next: Node | null = null; 3 | public prev: Node | null = null; 4 | constructor(public data: T) { 5 | } 6 | } 7 | 8 | export class Queue { 9 | private head: Node | null = null; 10 | private tail: Node | null = null; 11 | public length = 0; 12 | 13 | public push(data: T) { 14 | const node = new Node(data); 15 | if (this.tail) { 16 | this.tail.next = node; 17 | node.prev = this.tail; 18 | this.tail = node; 19 | } else { 20 | this.head = node; 21 | this.tail = node; 22 | } 23 | this.length += 1; 24 | return 25 | } 26 | 27 | public shift() { 28 | if (this.length > 0) { 29 | this.length -= 1; 30 | const node = this.head; 31 | if (node.next) { 32 | this.head = node.next; 33 | this.head.prev = null; 34 | } else { 35 | this.head = null; 36 | this.tail = null; 37 | } 38 | return node.data; 39 | } 40 | return undefined; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es6", 5 | "outDir": "build/main", 6 | "rootDir": "src", 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "declaration": true, 10 | "inlineSourceMap": true, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 13 | 14 | // "strict": true /* Enable all strict type-checking options. */, 15 | 16 | /* Strict Type-Checking Options */ 17 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 18 | // "strictNullChecks": true /* Enable strict null checks. */, 19 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 20 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 21 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 22 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 23 | 24 | /* Additional Checks */ 25 | // "noUnusedLocals": true /* Report errors on unused locals. */, 26 | // "noUnusedParameters": true /* Report errors on unused parameters. */, 27 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 28 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 29 | 30 | /* Debugging Options */ 31 | "traceResolution": false /* Report module resolution log messages. */, 32 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 33 | "listFiles": false /* Print names of files part of the compilation. */, 34 | "pretty": true /* Stylize errors and messages using color and context. */, 35 | 36 | /* Experimental Options */ 37 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 38 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 39 | 40 | "lib": ["es6"], 41 | "types": ["node"], 42 | "typeRoots": ["node_modules/@types", "src/types"] 43 | }, 44 | "include": ["src/**/*.ts"], 45 | "exclude": ["node_modules/**"], 46 | "compileOnSave": false 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "outDir": "build/module", 6 | "module": "es6" 7 | }, 8 | "exclude": [ 9 | "node_modules/**" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------