├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── jest.config.js ├── .editorconfig ├── .github └── workflows │ ├── build-and-test.yml │ └── publish-to-npm.yml ├── package.json ├── LICENSE ├── README.md ├── qew.ts ├── tsconfig.json └── qew.test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .rts2_cache_* 3 | dist 4 | .yarn 5 | .pnp.* 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: borales/actions-yarn@v3.0.0 14 | with: 15 | cmd: install 16 | - uses: borales/actions-yarn@v3.0.0 17 | with: 18 | cmd: build 19 | - uses: borales/actions-yarn@v3.0.0 20 | with: 21 | cmd: test 22 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 16 15 | registry-url: https://registry.npmjs.org/ 16 | - run: yarn install 17 | - run: yarn build 18 | - run: yarn test 19 | - run: yarn npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qew", 3 | "version": "0.11.2", 4 | "description": "Queue asynchronous functions", 5 | "source": "qew.ts", 6 | "main": "dist/qew.js", 7 | "module": "dist/qew.mjs", 8 | "unpkg": "dist/qew.umd.js", 9 | "types": "dist/qew.d.ts", 10 | "scripts": { 11 | "start": "microbundle watch", 12 | "build": "microbundle", 13 | "watch": "tsc -w", 14 | "prepublish": "yarn build", 15 | "test": "jest" 16 | }, 17 | "keywords": [ 18 | "queue", 19 | "async", 20 | "throttle", 21 | "promise" 22 | ], 23 | "author": { 24 | "name": "Aron Adler", 25 | "email": "aron@adler.dev", 26 | "url": "https://adler.dev/" 27 | }, 28 | "license": "MIT", 29 | "repository": "github:Arrow7000/qew", 30 | "packageManager": "yarn@3.2.2", 31 | "devDependencies": { 32 | "@sinonjs/fake-timers": "^9.1.2", 33 | "@types/jest": "^28.1.6", 34 | "@types/node": "^18.6.1", 35 | "@types/sinonjs__fake-timers": "^8.1.2", 36 | "jest": "^28.1.3", 37 | "microbundle": "^0.15.0", 38 | "ts-jest": "^28.0.7", 39 | "typescript": "^4.7.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aron Adler 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 | # Qew 2 | 3 | A tiny library for queuing and throttling asynchronous functions. 4 | 5 | Perfect for managing resource-intensive processes and controlling access to rate-limited APIs. 6 | 7 | This project has 0 (zero) runtime dependencies 👌. 8 | 9 | ![NPM stats](https://nodei.co/npm/qew.png?downloads=true&downloadRank=true&stars=true) 10 | 11 | 1. [Installation](#installation) 12 | 1. [API](#api) 13 | 1. [`Qew.constructor`](#new-qewmaxconcurrency1-delayordelayfunc0) 14 | 1. [`Qew.push`](#qewpushasyncfunction) 15 | 1. [Use cases](#use-cases) 16 | 1. [Methods & type signatures](#methods-and-type-signatures) 17 | 1. [Contributing](#contributing) 18 | 19 | ## Installation 20 | 21 | Qew is available on npm. Install via 22 | 23 | ``` 24 | $ npm install qew 25 | ``` 26 | 27 | or 28 | 29 | ``` 30 | $ yarn add qew 31 | ``` 32 | 33 | ## API 34 | 35 | ### `new Qew(maxConcurrency=1, delayOrDelayFunc=0)` 36 | 37 | The constructor's type signature is 38 | 39 | ```typescript 40 | constructor Qew(maxConcurrency: number = 1, delay: number | (() => number) = 0): Qew; 41 | ``` 42 | 43 | #### Examples 44 | 45 | ```typescript 46 | const Qew = require("qew"); 47 | 48 | const qew = new Qew(); // maxConcurrency of 1, delay of 0ms 49 | const qew = new Qew(3); // maxConcurrency of 3, delay of 0ms 50 | const qew = new Qew(2, 250); // maxConcurrency of 2, delay of 250ms between end and start of functions 51 | ``` 52 | 53 | The delay parameter doesn't need to be a hardcoded number, you can also pass in a function that returns a number. 54 | 55 | ```typescript 56 | const getRandomDelay = () => Math.random() * 1000; 57 | 58 | const qew = new Qew(2, getRandomDelay); 59 | // maxConcurrency of 2, delay will be a new random value between 0ms and 1000ms 60 | ``` 61 | 62 | ### `Qew.push(asyncFunction)` 63 | 64 | Type signature 65 | 66 | ```typescript 67 | Qew.push(asyncFunc: () => Promise): Promise; 68 | ``` 69 | 70 | ##### Examples 71 | 72 | ```typescript 73 | // returns a promise, that runs when it is its turn in the queue. 74 | // will resolve or reject depending on asyncFunc's resolution 75 | const prom = qew.push(asyncFunc); 76 | 77 | prom 78 | .then(result => ...) 79 | .catch(error => ...); 80 | 81 | // `push`'s result can also be `await`ed in an async function like any promise 82 | const doStuff = async () => { 83 | const result = await qew.push(asyncFunc); 84 | // do something with `result` 85 | } 86 | 87 | ``` 88 | 89 | ## Use cases 90 | 91 | ### Accessing rate-limited APIs 92 | 93 | Basic example, queuing individual asynchronous functions. 94 | 95 | ```typescript 96 | const qew = new Qew(1, 100); // for API that has a rate limit of 10 reqs/sec 97 | 98 | qew.push(() => accessApi("a")).then(callback); 99 | qew.push(() => accessApi("b")).then(callback); 100 | qew.push(() => accessApi("c")).then(callback); 101 | 102 | function callback(err, result) { 103 | if (err) { 104 | return console.error(err); 105 | } 106 | console.log(result); 107 | } 108 | ``` 109 | 110 | ## Methods and type signatures 111 | 112 | ```typescript 113 | constructor Qew(maxConcurrent: number = 1, delay: number | (() => number) = 0): Qew; 114 | 115 | Qew.push(asyncFunc: () => Promise): Promise; 116 | ``` 117 | 118 | ## Contributing 119 | 120 | Contributions are welcome! Feel free to file an issue or submit a pull request. 121 | -------------------------------------------------------------------------------- /qew.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This returns a promise along with two functions to remotely ~detonate~ trigger either a resolution of rejection of the promise from elsewhere in the code. It's magic! 3 | */ 4 | function makeTriggerablePromise(): [ 5 | Promise, 6 | (inp: T) => void, 7 | (error: any) => void 8 | ] { 9 | let triggerResolveWith!: (inp: T) => void; 10 | let triggerRejectWith!: (error: any) => void; 11 | const promToReturn: Promise = new Promise((resolve, reject) => { 12 | const funcThatResolvesProm = (inp: T) => resolve(inp); 13 | triggerResolveWith = funcThatResolvesProm; 14 | triggerRejectWith = reject; 15 | }); 16 | return [promToReturn, triggerResolveWith, triggerRejectWith]; 17 | } 18 | 19 | export type Options = { debug?: boolean }; 20 | 21 | export class Qew { 22 | private queue: (() => void)[] = []; 23 | private executing = 0; 24 | private debug = false; 25 | 26 | /** 27 | * 28 | * @param maxConcurrent how many functions can be run simultaneously 29 | * @param delay how many ms to wait between when one function has resolved and 30 | * the next one is run 31 | * @param options the only option currently supported is `debug`, which if enabled prints debugging logs 32 | */ 33 | constructor( 34 | private maxConcurrent = 1, 35 | private delay: number | (() => number) = 0, 36 | options?: Options 37 | ) { 38 | if (maxConcurrent < 1) { 39 | throw new Error("maxConcurrent has to be 1 or higher"); 40 | } 41 | if (typeof delay === "number" && delay < 0) { 42 | throw new Error( 43 | "`delay` parameter should be either a non-negative number or a function that returns one" 44 | ); 45 | } 46 | 47 | this.debug = options?.debug ?? this.debug; 48 | 49 | this.debugLog( 50 | `Debugging for Qew enabled! This qew has a maxConcurrent of ${maxConcurrent} and a ${ 51 | typeof delay === "number" 52 | ? `delay of ${delay}ms` 53 | : "custom delay generator function" 54 | }` 55 | ); 56 | 57 | this.tryMove = this.tryMove.bind(this); 58 | this.push = this.push.bind(this); 59 | this.pushProm = this.pushProm.bind(this); 60 | this.debugLog = this.debugLog.bind(this); 61 | } 62 | 63 | private debugLog = (...args: Parameters) => 64 | this.debug ? console.log(...args) : undefined; 65 | 66 | /** 67 | * Push another async function onto the queue 68 | * @param asyncFunc the async function to push onto this queue 69 | * @returns a Promise that resolves with `asyncFunc`'s resolved value – 70 | * whenever `asyncFunc` has been run and resolved. Or the Promise will reject 71 | * if `asyncFunc`'s Promise rejects 72 | */ 73 | public push(asyncFunc: () => Promise) { 74 | const [prom, resolveProm, rejectProm] = makeTriggerablePromise(); 75 | 76 | const funcToRun = () => { 77 | asyncFunc() 78 | .then((result) => { 79 | resolveProm(result); 80 | this.executing = this.executing - 1; 81 | 82 | const delay = 83 | typeof this.delay === "function" ? this.delay() : this.delay; 84 | 85 | this.debugLog( 86 | `Function resolved! About to tryMove again after ${delay}ms. (At ${new Date()})` 87 | ); 88 | 89 | setTimeout(() => this.tryMove("promise completion"), delay); 90 | }) 91 | .catch(rejectProm); 92 | }; 93 | 94 | this.queue.push(funcToRun); 95 | 96 | this.tryMove("push"); 97 | 98 | return prom; 99 | } 100 | 101 | /** 102 | * @deprecated this is now only an alias for `Qew#push` 103 | */ 104 | public pushProm = this.push; 105 | 106 | private tryMove(triggeredBy: "push" | "promise completion") { 107 | this.debugLog( 108 | `Trying to move because of: ${triggeredBy} (at unix time ${new Date()})` 109 | ); 110 | 111 | if (this.executing < this.maxConcurrent) { 112 | const first = this.queue.shift(); 113 | this.debugLog( 114 | `Under execution limit! ${ 115 | first ? "Grabbed a function off the queue" : "Nothing on the queue" 116 | }` 117 | ); 118 | 119 | if (first) { 120 | this.executing = this.executing + 1; 121 | first(); 122 | } 123 | } else { 124 | this.debugLog( 125 | `Currently at execution limit of ${this.maxConcurrent} so stopped move attempt` 126 | ); 127 | } 128 | } 129 | } 130 | 131 | export default Qew; 132 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": [ 8 | "DOM", 9 | "ES2015", 10 | "ES2016", 11 | "ES2017", 12 | "ES2018", 13 | "ES2019", 14 | "ES2020", 15 | "ES2021", 16 | "ES2022" 17 | ] /* Specify library files to be included in the compilation. */, 18 | // "allowJs": true, /* Allow javascript files to be compiled. */ 19 | // "checkJs": true, /* Report errors in .js files. */ 20 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 21 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 22 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 23 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 24 | // "outFile": "./", /* Concatenate and emit output to single file. */ 25 | // "outDir": "./", /* Redirect output structure to the directory. */ 26 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 27 | // "composite": true, /* Enable project compilation */ 28 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 29 | // "removeComments": true, /* Do not emit comments to output. */ 30 | // "noEmit": true, /* Do not emit outputs. */ 31 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 32 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 33 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 34 | 35 | /* Strict Type-Checking Options */ 36 | "strict": true /* Enable all strict type-checking options. */, 37 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 38 | // "strictNullChecks": true, /* Enable strict null checks. */ 39 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 40 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 41 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 42 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 43 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 44 | 45 | /* Additional Checks */ 46 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 47 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 48 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 49 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 50 | 51 | /* Module Resolution Options */ 52 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 53 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 54 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 55 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 56 | // "typeRoots": [], /* List of folders to include type definitions from. */ 57 | // "types": [], /* Type declaration files to be included in compilation. */ 58 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 59 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 60 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 61 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 62 | 63 | /* Source Map Options */ 64 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 67 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 68 | 69 | /* Experimental Options */ 70 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 71 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 72 | 73 | /* Advanced Options */ 74 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /qew.test.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "@sinonjs/fake-timers"; 2 | import { Qew } from "./qew"; 3 | 4 | const callAfter = (ms: number, cb: () => void) => 5 | new Promise((res) => setTimeout(() => res(cb()), ms)); 6 | 7 | /** 8 | * Because Jest's fake timers are accursed and we shall never speak of them again. 9 | */ 10 | let clock: sinon.InstalledClock; 11 | 12 | const advanceClock = async (ms: number) => clock.tickAsync(ms); 13 | 14 | beforeAll(() => { 15 | clock = sinon.install(); 16 | }); 17 | 18 | beforeEach(() => { 19 | clock.reset(); 20 | }); 21 | 22 | afterAll(() => { 23 | clock.uninstall(); 24 | }); 25 | 26 | it("Test that qew can run a single function", async () => { 27 | const qew = new Qew(); 28 | 29 | const fn = jest.fn(() => 1); 30 | const oneSecFunc = () => callAfter(1000, fn); 31 | 32 | const prom = qew.push(oneSecFunc); 33 | 34 | expect(fn).toHaveBeenCalledTimes(0); 35 | 36 | await advanceClock(999); 37 | expect(fn).toHaveBeenCalledTimes(0); 38 | 39 | await advanceClock(1); 40 | expect(fn).toHaveBeenCalledTimes(1); 41 | 42 | await advanceClock(1000); 43 | expect(fn).toHaveBeenCalledTimes(1); 44 | 45 | expect(prom).resolves.toBe(1); 46 | }); 47 | 48 | it("Test singlethreaded qew with a delay", async () => { 49 | const qew = new Qew(1, 500); 50 | 51 | const fn1 = jest.fn(() => 1); 52 | const fn2 = jest.fn(() => 2); 53 | 54 | const prom1 = qew.push(() => callAfter(1000, fn1)); 55 | const prom2 = qew.push(() => callAfter(1000, fn2)); 56 | 57 | expect(fn1).toHaveBeenCalledTimes(0); 58 | expect(fn2).toHaveBeenCalledTimes(0); 59 | 60 | await advanceClock(999); 61 | expect(fn1).toHaveBeenCalledTimes(0); 62 | expect(fn2).toHaveBeenCalledTimes(0); 63 | 64 | await advanceClock(1); 65 | expect(fn1).toHaveBeenCalledTimes(1); 66 | expect(fn2).toHaveBeenCalledTimes(0); 67 | 68 | await advanceClock(500); 69 | expect(fn1).toHaveBeenCalledTimes(1); 70 | expect(fn2).toHaveBeenCalledTimes(0); 71 | 72 | await advanceClock(999); 73 | expect(fn1).toHaveBeenCalledTimes(1); 74 | expect(fn2).toHaveBeenCalledTimes(0); 75 | 76 | await advanceClock(1); 77 | expect(fn1).toHaveBeenCalledTimes(1); 78 | expect(fn2).toHaveBeenCalledTimes(1); 79 | 80 | await advanceClock(5000); 81 | expect(fn1).toHaveBeenCalledTimes(1); 82 | expect(fn2).toHaveBeenCalledTimes(1); 83 | 84 | expect(prom1).resolves.toBe(1); 85 | expect(prom2).resolves.toBe(2); 86 | }); 87 | 88 | it("Test qew with max concurrent tasks of 2 and no delay", async () => { 89 | const qew = new Qew(2); 90 | 91 | const fn1 = jest.fn(() => 1); 92 | const fn2 = jest.fn(() => 2); 93 | const fn3 = jest.fn(() => 3); 94 | const fn4 = jest.fn(() => 4); 95 | 96 | const prom1 = qew.push(() => callAfter(1000, fn1)); 97 | const prom2 = qew.push(() => callAfter(1000, fn2)); 98 | const prom3 = qew.push(() => callAfter(1000, fn3)); 99 | const prom4 = qew.push(() => callAfter(1000, fn4)); 100 | 101 | expect(fn1).toHaveBeenCalledTimes(0); 102 | expect(fn2).toHaveBeenCalledTimes(0); 103 | expect(fn3).toHaveBeenCalledTimes(0); 104 | expect(fn4).toHaveBeenCalledTimes(0); 105 | 106 | await advanceClock(999); 107 | expect(fn1).toHaveBeenCalledTimes(0); 108 | expect(fn2).toHaveBeenCalledTimes(0); 109 | expect(fn3).toHaveBeenCalledTimes(0); 110 | expect(fn4).toHaveBeenCalledTimes(0); 111 | 112 | await advanceClock(1); 113 | expect(fn1).toHaveBeenCalledTimes(1); 114 | expect(fn2).toHaveBeenCalledTimes(1); 115 | expect(fn3).toHaveBeenCalledTimes(0); 116 | expect(fn4).toHaveBeenCalledTimes(0); 117 | 118 | await advanceClock(999); 119 | expect(fn1).toHaveBeenCalledTimes(1); 120 | expect(fn2).toHaveBeenCalledTimes(1); 121 | expect(fn3).toHaveBeenCalledTimes(0); 122 | expect(fn4).toHaveBeenCalledTimes(0); 123 | 124 | // Hmm this is odd. 1ms should definitely be enough but maybe internally the promises need extra 1ms to resolve? Idk. 125 | await advanceClock(2); 126 | expect(fn1).toHaveBeenCalledTimes(1); 127 | expect(fn2).toHaveBeenCalledTimes(1); 128 | expect(fn3).toHaveBeenCalledTimes(1); 129 | expect(fn4).toHaveBeenCalledTimes(1); 130 | 131 | await advanceClock(5000); 132 | expect(fn1).toHaveBeenCalledTimes(1); 133 | expect(fn2).toHaveBeenCalledTimes(1); 134 | expect(fn3).toHaveBeenCalledTimes(1); 135 | expect(fn4).toHaveBeenCalledTimes(1); 136 | 137 | expect(prom1).resolves.toBe(1); 138 | expect(prom2).resolves.toBe(2); 139 | expect(prom3).resolves.toBe(3); 140 | expect(prom4).resolves.toBe(4); 141 | }); 142 | 143 | it("Test qew with max concurrent tasks of 2 and a delay", async () => { 144 | const qew = new Qew(2, 500); 145 | 146 | const fn1 = jest.fn(() => 1); 147 | const fn2 = jest.fn(() => 2); 148 | const fn3 = jest.fn(() => 3); 149 | const fn4 = jest.fn(() => 4); 150 | 151 | const prom1 = qew.push(() => callAfter(1000, fn1)); 152 | const prom2 = qew.push(() => callAfter(1000, fn2)); 153 | const prom3 = qew.push(() => callAfter(1000, fn3)); 154 | const prom4 = qew.push(() => callAfter(1000, fn4)); 155 | 156 | expect(fn1).toHaveBeenCalledTimes(0); 157 | expect(fn2).toHaveBeenCalledTimes(0); 158 | expect(fn3).toHaveBeenCalledTimes(0); 159 | expect(fn4).toHaveBeenCalledTimes(0); 160 | 161 | await advanceClock(999); 162 | expect(fn1).toHaveBeenCalledTimes(0); 163 | expect(fn2).toHaveBeenCalledTimes(0); 164 | expect(fn3).toHaveBeenCalledTimes(0); 165 | expect(fn4).toHaveBeenCalledTimes(0); 166 | 167 | await advanceClock(1); 168 | expect(fn1).toHaveBeenCalledTimes(1); 169 | expect(fn2).toHaveBeenCalledTimes(1); 170 | expect(fn3).toHaveBeenCalledTimes(0); 171 | expect(fn4).toHaveBeenCalledTimes(0); 172 | 173 | await advanceClock(1499); 174 | expect(fn1).toHaveBeenCalledTimes(1); 175 | expect(fn2).toHaveBeenCalledTimes(1); 176 | expect(fn3).toHaveBeenCalledTimes(0); 177 | expect(fn4).toHaveBeenCalledTimes(0); 178 | 179 | await advanceClock(1); 180 | expect(fn1).toHaveBeenCalledTimes(1); 181 | expect(fn2).toHaveBeenCalledTimes(1); 182 | expect(fn3).toHaveBeenCalledTimes(1); 183 | expect(fn4).toHaveBeenCalledTimes(1); 184 | 185 | await advanceClock(5000); 186 | expect(fn1).toHaveBeenCalledTimes(1); 187 | expect(fn2).toHaveBeenCalledTimes(1); 188 | expect(fn3).toHaveBeenCalledTimes(1); 189 | expect(fn4).toHaveBeenCalledTimes(1); 190 | 191 | expect(prom1).resolves.toBe(1); 192 | expect(prom2).resolves.toBe(2); 193 | expect(prom3).resolves.toBe(3); 194 | expect(prom4).resolves.toBe(4); 195 | }); 196 | 197 | it("Test qew with max concurrent tasks of 3, a delay and functions that run for different amounts of time", async () => { 198 | const qew = new Qew(3, 100); 199 | 200 | const fn1 = jest.fn(() => 1); 201 | const fn2 = jest.fn(() => 2); 202 | const fn3 = jest.fn(() => 3); 203 | const fn4 = jest.fn(() => 4); 204 | 205 | const prom1 = qew.push(() => callAfter(1000, fn1)); 206 | const prom2 = qew.push(() => callAfter(100, fn2)); 207 | const prom3 = qew.push(() => callAfter(800, fn3)); 208 | const prom4 = qew.push(() => callAfter(300, fn4)); 209 | 210 | expect(fn1).toHaveBeenCalledTimes(0); 211 | expect(fn2).toHaveBeenCalledTimes(0); 212 | expect(fn3).toHaveBeenCalledTimes(0); 213 | expect(fn4).toHaveBeenCalledTimes(0); 214 | 215 | await advanceClock(100); 216 | expect(fn1).toHaveBeenCalledTimes(0); 217 | expect(fn2).toHaveBeenCalledTimes(1); 218 | expect(fn3).toHaveBeenCalledTimes(0); 219 | expect(fn4).toHaveBeenCalledTimes(0); 220 | 221 | await advanceClock(400); 222 | expect(fn1).toHaveBeenCalledTimes(0); 223 | expect(fn2).toHaveBeenCalledTimes(1); 224 | expect(fn3).toHaveBeenCalledTimes(0); 225 | expect(fn4).toHaveBeenCalledTimes(1); 226 | 227 | await advanceClock(300); 228 | expect(fn1).toHaveBeenCalledTimes(0); 229 | expect(fn2).toHaveBeenCalledTimes(1); 230 | expect(fn3).toHaveBeenCalledTimes(1); 231 | expect(fn4).toHaveBeenCalledTimes(1); 232 | 233 | await advanceClock(200); 234 | expect(fn1).toHaveBeenCalledTimes(1); 235 | expect(fn2).toHaveBeenCalledTimes(1); 236 | expect(fn3).toHaveBeenCalledTimes(1); 237 | expect(fn4).toHaveBeenCalledTimes(1); 238 | 239 | await advanceClock(5000); 240 | expect(fn1).toHaveBeenCalledTimes(1); 241 | expect(fn2).toHaveBeenCalledTimes(1); 242 | expect(fn3).toHaveBeenCalledTimes(1); 243 | expect(fn4).toHaveBeenCalledTimes(1); 244 | 245 | expect(prom1).resolves.toBe(1); 246 | expect(prom2).resolves.toBe(2); 247 | expect(prom3).resolves.toBe(3); 248 | expect(prom4).resolves.toBe(4); 249 | }); 250 | --------------------------------------------------------------------------------