├── .gitignore ├── .mocharc.js ├── .github ├── workflows │ ├── pr-title.yml │ ├── unit-test.yml │ └── publish.js.yml.disabled └── dependabot.yml ├── tsconfig.json ├── eslint.config.mjs ├── lib ├── types.ts └── asyncbox.ts ├── package.json ├── README.md ├── LICENSE └── test └── asyncbox-specs.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | *.log 4 | package-lock.json* 5 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: ['tsx/cjs'], 3 | forbidOnly: Boolean(process.env.CI) 4 | }; 5 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits 2 | on: 3 | pull_request: 4 | types: [opened, edited, synchronize, reopened] 5 | 6 | jobs: 7 | lint: 8 | uses: appium/appium-workflows/.github/workflows/pr-title.yml@main 9 | with: 10 | config-preset: angular 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@appium/tsconfig/tsconfig.json", 4 | "compilerOptions": { 5 | "strict": false, 6 | "outDir": "build", 7 | "types": ["node", "mocha"], 8 | "checkJs": true 9 | }, 10 | "include": [ 11 | "lib", 12 | "test" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import appiumConfig from '@appium/eslint-config-appium-ts'; 2 | import {defineConfig, globalIgnores} from 'eslint/config'; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [appiumConfig], 7 | }, 8 | { 9 | files: ['test/**/*.{js,ts}'], 10 | rules: { 11 | 'func-names': 'off' 12 | } 13 | }, 14 | globalIgnores(['build']), 15 | ]); 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "chore" 11 | include: "scope" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "11:00" 17 | open-pull-requests-limit: 10 18 | commit-message: 19 | prefix: "chore" 20 | include: "scope" 21 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parameter provided to a progress callback 3 | */ 4 | export interface Progress { 5 | elapsedMs: number; 6 | timeLeft: number; 7 | progress: number; 8 | } 9 | 10 | /** 11 | * Progress callback for {@link longSleep} 12 | */ 13 | export type ProgressCallback = (progress: Progress) => void; 14 | 15 | /** 16 | * Options for {@link longSleep} 17 | */ 18 | export interface LongSleepOptions { 19 | thresholdMs?: number; 20 | intervalMs?: number; 21 | progressCb?: ProgressCallback | null; 22 | } 23 | 24 | /** 25 | * Options for {@link waitForCondition} 26 | */ 27 | export interface WaitForConditionOptions { 28 | waitMs?: number; 29 | intervalMs?: number; 30 | logger?: { 31 | debug: (...args: any[]) => void; 32 | }; 33 | error?: string | Error; 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths-ignore: 7 | - 'docs/**' 8 | - '*.md' 9 | push: 10 | branches: [ master ] 11 | paths-ignore: 12 | - 'docs/**' 13 | - '*.md' 14 | 15 | 16 | jobs: 17 | node_matrix: 18 | uses: appium/appium-workflows/.github/workflows/node-lts-matrix.yml@main 19 | 20 | test: 21 | needs: 22 | - node_matrix 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | node-version: ${{ fromJSON(needs.node_matrix.outputs.versions) }} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v6 30 | - uses: actions/setup-node@v6 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | check-latest: true 34 | - uses: SocketDev/action@v1 35 | with: 36 | mode: firewall-free 37 | - run: sfw npm install --no-package-lock 38 | name: Install dev dependencies 39 | - run: npm run lint 40 | name: Run linter 41 | - run: npm run test 42 | name: Run unit tests 43 | -------------------------------------------------------------------------------- /.github/workflows/publish.js.yml.disabled: -------------------------------------------------------------------------------- 1 | # Reenable this workflow as soon as the project is moved under the Appium org in npmjs 2 | 3 | name: Release 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: [ master ] 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | issues: write 14 | id-token: write # to enable use of OIDC for trusted publishing and npm provenance 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | - run: npm install --no-package-lock 26 | name: Install dependencies 27 | - run: npm run test 28 | name: Run NPM Test 29 | - run: | 30 | rm -rf package-lock.json node_modules 31 | # Remove dev dependencies from node_modules 32 | npm prune --omit=dev --no-package-lock 33 | name: Remove dev dependencies 34 | - run: npm shrinkwrap --omit=dev 35 | name: Create shrinkwrap 36 | - run: npm install --only=dev --no-package-lock 37 | name: Install dev dependencies for the release 38 | - run: npx semantic-release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | name: Release 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asyncbox", 3 | "description": "A collection of small async/await utilities", 4 | "tags": [ 5 | "async/await", 6 | "es7", 7 | "async" 8 | ], 9 | "version": "3.0.0", 10 | "author": "jlipps@gmail.com", 11 | "license": "Apache-2.0", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/jlipps/asyncbox.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/jlipps/asyncbox/issues" 18 | }, 19 | "engines": { 20 | "node": "^20.19.0 || ^22.12.0 || >=24.0.0", 21 | "npm": ">=10" 22 | }, 23 | "main": "./build/lib/asyncbox.js", 24 | "bin": {}, 25 | "directories": { 26 | "lib": "./lib" 27 | }, 28 | "files": [ 29 | "lib/**/*", 30 | "build/lib/**/*" 31 | ], 32 | "dependencies": { 33 | "bluebird": "^3.5.1", 34 | "lodash": "^4.17.4" 35 | }, 36 | "scripts": { 37 | "build": "tsc -b", 38 | "clean": "npm run build -- --clean", 39 | "rebuild": "npm run clean; npm run build", 40 | "dev": "npm run build -- --watch", 41 | "prepare": "npm run rebuild", 42 | "test": "mocha --exit --timeout 1m \"./test/**/*-specs.ts\"", 43 | "lint": "eslint .", 44 | "format": "prettier -w ./lib", 45 | "watch": "npm run dev" 46 | }, 47 | "prettier": { 48 | "bracketSpacing": false, 49 | "printWidth": 100, 50 | "singleQuote": true 51 | }, 52 | "devDependencies": { 53 | "@appium/eslint-config-appium-ts": "^2.0.5", 54 | "@appium/tsconfig": "^1.0.0", 55 | "@semantic-release/changelog": "^6.0.1", 56 | "@semantic-release/git": "^10.0.1", 57 | "@types/bluebird": "^3.5.37", 58 | "@types/lodash": "^4.14.189", 59 | "@types/mocha": "^10.0.10", 60 | "@types/node": "^24.10.1", 61 | "chai": "^6.2.1", 62 | "chai-as-promised": "^8.0.2", 63 | "conventional-changelog-conventionalcommits": "^9.0.0", 64 | "eslint": "^9.39.1", 65 | "prettier": "^3.0.0", 66 | "mocha": "^11.7.5", 67 | "semantic-release": "^25.0.2", 68 | "sinon": "^21.0.0", 69 | "ts-node": "^10.9.1", 70 | "tsx": "^4.21.0", 71 | "typescript": "^5.1.6" 72 | }, 73 | "types": "./build/lib/types.d.ts" 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | asyncbox 2 | ======== 3 | 4 | A collection of ES7 async/await utilities. Install via NPM: 5 | 6 | ``` 7 | npm install asyncbox 8 | ``` 9 | 10 | Then, behold! 11 | 12 | ### Sleep 13 | 14 | An async/await version of setTimeout 15 | 16 | ```js 17 | import { sleep } from 'asyncbox'; 18 | 19 | async function myFn () { 20 | // do some stuff 21 | await sleep(1000); // wait one second 22 | // do some other stuff 23 | }; 24 | ``` 25 | 26 | ### Long Sleep 27 | 28 | Sometimes `Promise.delay` or `setTimeout` are inaccurate for large wait times. To safely wait for these long times (e.g. in the 5+ minute range), you can use `longSleep`: 29 | 30 | ```js 31 | import { longSleep } from 'asyncbox'; 32 | 33 | async function myFn () { 34 | await longSleep(10 * 60 * 1000); // wait for 10 mins 35 | await longSleep(5000, {thresholdMs: 10000}); // wait for 5s. Anything below the thresholdMs will use a single sleep 36 | await longSleep(5000, {intervalMs: 500}); // check the clock every 500ms to see if waiting should stop 37 | } 38 | ``` 39 | 40 | You can also pass a `progressCb` option which is a callback function that receives an object with the properties `elapsedMs`, `timeLeft`, and `progress`. This will be called on every wait interval so you can do your wait logging or whatever. 41 | 42 | ```js 43 | function progressCb({elapsedMs, timeLeft, progress}) { 44 | console.log(`We are {progress * 100}% complete waiting`); 45 | } 46 | await longSleep(10 * 60 * 1000, {progressCb}); 47 | ``` 48 | 49 | ### Retry 50 | 51 | An async/await way of running a method until it doesn't throw an error 52 | 53 | ```js 54 | import { sleep, retry } from 'asyncbox'; 55 | 56 | async function flakeyFunction (val1, val2) { 57 | if (val1 < 10) { 58 | throw new Error("this is a flakey value"); 59 | } 60 | await sleep(1000); 61 | return val1 + val2; 62 | } 63 | 64 | async function myFn () { 65 | let randVals = [Math.random() * 100, Math.random() * 100]; 66 | 67 | // run flakeyFunction up to 3 times until it succeeds. 68 | // if it doesn't, we'll get the error thrown in this context 69 | let randSum = await retry(3, flakeyFunction, ...randVals); 70 | } 71 | ``` 72 | 73 | You can also use `retryInterval` to add a sleep in between retries. This can be 74 | useful if you want to throttle how fast we retry: 75 | 76 | ```js 77 | await retryInterval(3, 1500, expensiveFunction, ...args); 78 | ``` 79 | 80 | ### Filter/Map 81 | 82 | Filter and map are pretty handy concepts, and now you can write filter and map 83 | functions that execute asynchronously! 84 | 85 | ```js 86 | import { asyncmap, asyncfilter } from 'asyncbox'; 87 | ``` 88 | 89 | Then in your async functions, you can do: 90 | 91 | ```js 92 | const items = [1, 2, 3, 4]; 93 | const slowSquare = async (n) => { await sleep(5); return n * 2; }; 94 | let newItems = await asyncmap(items, async (i) => { return await slowSquare(i); }); 95 | console.log(newItems); // [1, 4, 9, 16]; 96 | 97 | const slowEven = async (n) => { await sleep(5); return n % 2 === 0; }; 98 | newItems = await asyncfilter(items, async (i) => { return await slowEven(i); }); 99 | console.log(newItems); // [2, 4]; 100 | ``` 101 | 102 | By default, `asyncmap` and `asyncfilter` run their operations in parallel; you 103 | can pass `false` as a third argument to make sure it happens serially. 104 | 105 | ### Nodeify 106 | 107 | Export async functions (Promises) and import this with your ES5 code to use it 108 | with Node. 109 | 110 | ```js 111 | var asyncbox = require('asyncbox') 112 | , sleep = asyncbox.sleep 113 | , nodeify = asyncbox.nodeify; 114 | 115 | nodeify(sleep(1000), function (err, timer) { 116 | console.log(err); // null 117 | console.log(timer); // timer obj 118 | }); 119 | ``` 120 | 121 | ### nodeifyAll 122 | 123 | If you have a whole library you want to export nodeified versions of, it's pretty easy: 124 | 125 | ```js 126 | import { nodeifyAll } from 'asyncbox'; 127 | 128 | async function foo () { ... } 129 | async function bar () { ... } 130 | let cb = nodeifyAll({foo, bar}); 131 | export { foo, bar, cb }; 132 | ``` 133 | 134 | Then in my ES5 script I can do: 135 | 136 | ```js 137 | var myLib = require('mylib').cb; 138 | 139 | myLib.foo(function (err) { ... }); 140 | myLib.bar(function (err) { ... }); 141 | ``` 142 | 143 | ### waitForCondition 144 | 145 | Takes a condition (a function returning a boolean or boolean promise), 146 | and waits until the condition is true. 147 | 148 | Throws a `/Condition unmet/` error if the condition has not been 149 | satisfied within the allocated time, unless an error is provided in 150 | the options, as the `error` property, which is either thrown itself, or 151 | used as the message. 152 | 153 | The condition result is returned if it is not falsy. If the condition 154 | throws an error then this exception will be immediately passed through. 155 | 156 | The default options are: `{ waitMs: 5000, intervalMs: 500 }` 157 | 158 | ```js 159 | // define your own condition 160 | function condFn () { return Math.random()*1000 > 995; } 161 | 162 | // with default params 163 | await waitForCondition(condFn); 164 | 165 | // with options 166 | await waitForCondition(condFn, { 167 | waitMs: 300000, 168 | intervalMs: 10000 169 | }); 170 | 171 | // pass a logger to get extra debug info 172 | await waitForCondition(condFn, { 173 | waitMs: 300000, 174 | intervalMs: 10000 175 | logger: myLogger // expects a debug method 176 | }); 177 | 178 | // pass an error string to get that message in the resulting exception 179 | try { 180 | await waitForCondition(condFn, { 181 | error: 'Unable to satisfy condition' 182 | }); 183 | } catch (err) { 184 | // err.message === 'Unable to satisfy condition' 185 | } 186 | 187 | // pass an error instance to be thrown 188 | const error = new Error('Unable to satisfy condition'); 189 | try { 190 | await waitForCondition(condFn, { 191 | error: error 192 | }); 193 | } catch (err) { 194 | // err === error 195 | } 196 | ``` 197 | 198 | ### Run the tests 199 | 200 | ``` 201 | npm test 202 | ``` 203 | -------------------------------------------------------------------------------- /lib/asyncbox.ts: -------------------------------------------------------------------------------- 1 | import B from 'bluebird'; 2 | import _ from 'lodash'; 3 | import type {LongSleepOptions, WaitForConditionOptions} from './types.js'; 4 | 5 | const LONG_SLEEP_THRESHOLD = 5000; // anything over 5000ms will turn into a spin 6 | 7 | /** 8 | * An async/await version of setTimeout 9 | */ 10 | export async function sleep(ms: number): Promise { 11 | return await B.delay(ms); 12 | } 13 | 14 | /** 15 | * Sometimes `Promise.delay` or `setTimeout` are inaccurate for large wait 16 | * times. To safely wait for these long times (e.g. in the 5+ minute range), you 17 | * can use `longSleep`. 18 | * 19 | * You can also pass a `progressCb` option which is a callback function that 20 | * receives an object with the properties `elapsedMs`, `timeLeft`, and 21 | * `progress`. This will be called on every wait interval so you can do your 22 | * wait logging or whatever. 23 | */ 24 | export async function longSleep( 25 | ms: number, 26 | {thresholdMs = LONG_SLEEP_THRESHOLD, intervalMs = 1000, progressCb = null}: LongSleepOptions = {}, 27 | ): Promise { 28 | if (ms < thresholdMs) { 29 | return await sleep(ms); 30 | } 31 | const endAt = Date.now() + ms; 32 | let timeLeft: number; 33 | let elapsedMs = 0; 34 | do { 35 | const pre = Date.now(); 36 | await sleep(intervalMs); 37 | const post = Date.now(); 38 | timeLeft = endAt - post; 39 | elapsedMs = elapsedMs + (post - pre); 40 | if (_.isFunction(progressCb)) { 41 | progressCb({elapsedMs, timeLeft, progress: elapsedMs / ms}); 42 | } 43 | } while (timeLeft > 0); 44 | } 45 | 46 | /** 47 | * An async/await way of running a method until it doesn't throw an error 48 | */ 49 | export async function retry( 50 | times: number, 51 | fn: (...args: any[]) => Promise, 52 | ...args: any[] 53 | ): Promise { 54 | let tries = 0; 55 | let done = false; 56 | let res: T | null = null; 57 | while (!done && tries < times) { 58 | tries++; 59 | try { 60 | res = await fn(...args); 61 | done = true; 62 | } catch (err) { 63 | if (tries >= times) { 64 | throw err; 65 | } 66 | } 67 | } 68 | return res; 69 | } 70 | 71 | /** 72 | * You can also use `retryInterval` to add a sleep in between retries. This can 73 | * be useful if you want to throttle how fast we retry. 74 | */ 75 | export async function retryInterval( 76 | times: number, 77 | sleepMs: number, 78 | fn: (...args: any[]) => Promise, 79 | ...args: any[] 80 | ): Promise { 81 | let count = 0; 82 | const wrapped = async (): Promise => { 83 | count++; 84 | let res: T; 85 | try { 86 | res = await fn(...args); 87 | } catch (e) { 88 | // do not pause when finished the last retry 89 | if (count !== times) { 90 | await sleep(sleepMs); 91 | } 92 | throw e; 93 | } 94 | return res; 95 | }; 96 | return await retry(times, wrapped); 97 | } 98 | 99 | export const parallel = B.all; 100 | 101 | /** 102 | * Export async functions (Promises) and import this with your ES5 code to use 103 | * it with Node. 104 | */ 105 | // eslint-disable-next-line promise/prefer-await-to-callbacks 106 | export function nodeify(promisey: any, cb: (err: any, value?: R) => void): Promise { 107 | return B.resolve(promisey).nodeify(cb); 108 | } 109 | 110 | /** 111 | * Node-ify an entire object of `Promise`-returning functions 112 | */ 113 | export function nodeifyAll any>>( 114 | promiseyMap: T, 115 | ): Record void> { 116 | const cbMap: Record void> = {}; 117 | for (const [name, fn] of _.toPairs(promiseyMap)) { 118 | cbMap[name] = function (...args: any[]) { 119 | const _cb = args.slice(-1)[0] as (err: any, ...values: any[]) => void; 120 | const fnArgs = args.slice(0, -1); 121 | nodeify(fn(...fnArgs), _cb); 122 | }; 123 | } 124 | return cbMap; 125 | } 126 | 127 | /** 128 | * Fire and forget async function execution 129 | */ 130 | export function asyncify(fn: (...args: any[]) => any | Promise, ...args: any[]): void { 131 | B.resolve(fn(...args)).done(); 132 | } 133 | 134 | /** 135 | * Similar to `Array.prototype.map`; runs in serial or parallel 136 | */ 137 | export async function asyncmap( 138 | coll: T[], 139 | mapper: (value: T) => R | Promise, 140 | runInParallel = true, 141 | ): Promise { 142 | if (runInParallel) { 143 | return parallel(coll.map(mapper)); 144 | } 145 | 146 | const newColl: R[] = []; 147 | for (const item of coll) { 148 | newColl.push(await mapper(item)); 149 | } 150 | return newColl; 151 | } 152 | 153 | /** 154 | * Similar to `Array.prototype.filter` 155 | */ 156 | export async function asyncfilter( 157 | coll: T[], 158 | filter: (value: T) => boolean | Promise, 159 | runInParallel = true, 160 | ): Promise { 161 | const newColl: T[] = []; 162 | if (runInParallel) { 163 | const bools = await parallel(coll.map(filter)); 164 | for (let i = 0; i < coll.length; i++) { 165 | if (bools[i]) { 166 | newColl.push(coll[i]); 167 | } 168 | } 169 | } else { 170 | for (const item of coll) { 171 | if (await filter(item)) { 172 | newColl.push(item); 173 | } 174 | } 175 | } 176 | return newColl; 177 | } 178 | 179 | /** 180 | * Takes a condition (a function returning a boolean or boolean promise), and 181 | * waits until the condition is true. 182 | * 183 | * Throws a `/Condition unmet/` error if the condition has not been satisfied 184 | * within the allocated time, unless an error is provided in the options, as the 185 | * `error` property, which is either thrown itself, or used as the message. 186 | * 187 | * The condition result is returned if it is not falsy. If the condition throws an 188 | * error then this exception will be immediately passed through. 189 | * 190 | * The default options are: `{ waitMs: 5000, intervalMs: 500 }` 191 | */ 192 | export async function waitForCondition( 193 | condFn: () => Promise | T, 194 | options: WaitForConditionOptions = {}, 195 | ): Promise { 196 | const opts: WaitForConditionOptions & {waitMs: number; intervalMs: number} = _.defaults(options, { 197 | waitMs: 5000, 198 | intervalMs: 500, 199 | }); 200 | const debug = opts.logger ? opts.logger.debug.bind(opts.logger) : _.noop; 201 | const error = opts.error; 202 | const begunAt = Date.now(); 203 | const endAt = begunAt + opts.waitMs; 204 | const spin = async function spin(): Promise { 205 | const result = await condFn(); 206 | if (result) { 207 | return result; 208 | } 209 | const now = Date.now(); 210 | const waited = now - begunAt; 211 | const remainingTime = endAt - now; 212 | if (now < endAt) { 213 | debug(`Waited for ${waited} ms so far`); 214 | await B.delay(Math.min(opts.intervalMs, remainingTime)); 215 | return await spin(); 216 | } 217 | // if there is an error option, it is either a string message or an error itself 218 | throw error 219 | ? _.isString(error) 220 | ? new Error(error) 221 | : error 222 | : new Error(`Condition unmet after ${waited} ms. Timing out.`); 223 | }; 224 | return await spin(); 225 | } 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2018 JS Foundation and other contributors 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2012 JS Foundation and other contributors 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /test/asyncbox-specs.ts: -------------------------------------------------------------------------------- 1 | import {expect, use} from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { 4 | sleep, 5 | longSleep, 6 | retry, 7 | retryInterval, 8 | nodeify, 9 | nodeifyAll, 10 | parallel, 11 | asyncmap, 12 | asyncfilter, 13 | waitForCondition, 14 | } from '../lib/asyncbox.js'; 15 | import B from 'bluebird'; 16 | import sinon from 'sinon'; 17 | 18 | use(chaiAsPromised); 19 | 20 | describe('sleep', function () { 21 | it('should work like setTimeout', async function () { 22 | const now = Date.now(); 23 | await sleep(20); 24 | expect(Date.now() - now).to.be.at.least(19); 25 | }); 26 | }); 27 | 28 | describe('longSleep', function () { 29 | it('should work like sleep in general', async function () { 30 | const now = Date.now(); 31 | await longSleep(20); 32 | expect(Date.now() - now).to.be.at.least(19); 33 | }); 34 | it('should work like sleep with values less than threshold', async function () { 35 | const now = Date.now(); 36 | await longSleep(20, {thresholdMs: 100}); 37 | expect(Date.now() - now).to.be.at.least(19); 38 | }); 39 | it('should work like sleep with values above threshold, but quantized', async function () { 40 | const now = Date.now(); 41 | await longSleep(50, {thresholdMs: 20, intervalMs: 40}); 42 | expect(Date.now() - now).to.be.at.least(79); 43 | }); 44 | it('should trigger a progress callback if specified', async function () { 45 | let callCount = 0; 46 | let curElapsed = 0; 47 | let curTimeLeft = 10000; 48 | let curProgress = 0; 49 | const progressCb = function ({elapsedMs, timeLeft, progress}: {elapsedMs: number, timeLeft: number, progress: number}) { 50 | expect(elapsedMs).to.be.above(curElapsed); 51 | expect(timeLeft).to.be.below(curTimeLeft); 52 | expect(progress).to.be.above(curProgress); 53 | curElapsed = elapsedMs; 54 | curTimeLeft = timeLeft; 55 | curProgress = progress; 56 | callCount += 1; 57 | }; 58 | const now = Date.now(); 59 | await longSleep(500, {thresholdMs: 1, intervalMs: 100, progressCb}); 60 | expect(Date.now() - now).to.be.above(49); 61 | expect(callCount).to.be.above(3); 62 | expect(curProgress >= 1).to.be.true; 63 | expect(curTimeLeft <= 0).to.be.true; 64 | expect(curElapsed >= 50).to.be.true; 65 | }); 66 | }); 67 | 68 | describe('retry', function () { 69 | let okFnCalls = 0; 70 | const okFn = async function (val1: number, val2: number): Promise { 71 | await sleep(15); 72 | okFnCalls++; 73 | return val1 * val2; 74 | }; 75 | let badFnCalls = 0; 76 | const badFn = async function (): Promise { 77 | await sleep(15); 78 | badFnCalls++; 79 | throw new Error('bad'); 80 | }; 81 | let eventuallyOkFnCalls = 0; 82 | const eventuallyOkFn = async function (times: number): Promise { 83 | await sleep(15); 84 | eventuallyOkFnCalls++; 85 | if (eventuallyOkFnCalls < times) { 86 | throw new Error('not ok yet'); 87 | } 88 | return times * times; 89 | }; 90 | const eventuallyOkNoSleepFn = async function (times: number): Promise { 91 | eventuallyOkFnCalls++; 92 | if (eventuallyOkFnCalls < times) { 93 | throw new Error('not ok yet'); 94 | } 95 | return times * times; 96 | }; 97 | beforeEach(function () { 98 | okFnCalls = 0; 99 | badFnCalls = 0; 100 | eventuallyOkFnCalls = 0; 101 | }); 102 | it('should return the result of a passing function', async function () { 103 | const start = Date.now(); 104 | const res = await retry(3, okFn, 5, 4); 105 | expect(res).to.equal(20); 106 | expect(Date.now() - start).to.be.at.least(14); 107 | expect(okFnCalls).to.equal(1); 108 | }); 109 | it('should retry a failing function and eventually throw the same err', async function () { 110 | let err: Error | null = null; 111 | const start = Date.now(); 112 | try { 113 | await retry(3, badFn); 114 | } catch (e) { 115 | err = e as Error; 116 | } 117 | expect(err).to.exist; 118 | expect(err!.message).to.equal('bad'); 119 | expect(badFnCalls).to.equal(3); 120 | expect(Date.now() - start).to.be.at.least(44); 121 | }); 122 | it('should return the correct value with a function that eventually passes', async function () { 123 | let err: Error | null = null; 124 | let start = Date.now(); 125 | try { 126 | await retry(3, eventuallyOkFn, 4); 127 | } catch (e) { 128 | err = e as Error; 129 | } 130 | expect(err).to.exist; 131 | expect(err!.message).to.equal('not ok yet'); 132 | expect(eventuallyOkFnCalls).to.equal(3); 133 | expect(Date.now() - start).to.be.above(35); 134 | 135 | // rerun with ok number of calls 136 | start = Date.now(); 137 | eventuallyOkFnCalls = 0; 138 | const res = await retry(3, eventuallyOkFn, 3); 139 | expect(eventuallyOkFnCalls).to.equal(3); 140 | expect(res).to.equal(9); 141 | expect(Date.now() - start).to.be.above(35); 142 | }); 143 | describe('retryInterval', function () { 144 | it('should return the correct value with a function that eventually passes', async function () { 145 | eventuallyOkFnCalls = 0; 146 | let err: Error | null = null; 147 | let start = Date.now(); 148 | try { 149 | await retryInterval(3, 15, eventuallyOkNoSleepFn, 4); 150 | } catch (e) { 151 | err = e as Error; 152 | } 153 | expect(err).to.exist; 154 | expect(err!.message).to.equal('not ok yet'); 155 | expect(eventuallyOkFnCalls).to.equal(3); 156 | expect(Date.now() - start).to.be.at.least(30); 157 | 158 | // rerun with ok number of calls 159 | start = Date.now(); 160 | eventuallyOkFnCalls = 0; 161 | const res = await retryInterval(3, 15, eventuallyOkNoSleepFn, 3); 162 | expect(eventuallyOkFnCalls).to.equal(3); 163 | expect(res).to.equal(9); 164 | // XXX: flaky 165 | expect(Date.now() - start).to.be.at.least(30); 166 | }); 167 | it('should not wait on the final error', async function () { 168 | const start = Date.now(); 169 | try { 170 | await retryInterval(3, 2000, badFn); 171 | } catch { 172 | expect(Date.now() - start).to.be.below(4100); 173 | } 174 | }); 175 | }); 176 | }); 177 | 178 | describe('nodeifyAll', function () { 179 | const asyncFn = async function (val: string): Promise { 180 | await sleep(15); 181 | return val; 182 | }; 183 | const asyncFn2 = async function (val: string): Promise { 184 | await sleep(15); 185 | return [val, val + val]; 186 | }; 187 | const badAsyncFn = async function (): Promise { 188 | await sleep(15); 189 | throw new Error('boo'); 190 | }; 191 | const cbMap = nodeifyAll({asyncFn, asyncFn2, badAsyncFn}); 192 | it('should turn async functions into nodey things', function (done) { 193 | const start = Date.now(); 194 | nodeify(asyncFn('foo'), function (err: Error | null, val?: string, val2?: string) { // eslint-disable-line promise/prefer-await-to-callbacks 195 | expect(err).to.not.exist; 196 | expect(val2).to.not.exist; 197 | expect(val!).to.equal('foo'); 198 | expect(Date.now() - start).to.be.at.least(14); 199 | done(); 200 | }); 201 | }); 202 | it('should turn async functions into nodey things via nodeifyAll', function (done) { 203 | const start = Date.now(); 204 | cbMap.asyncFn('foo', function (err: Error | null, val?: string, val2?: string) { // eslint-disable-line promise/prefer-await-to-callbacks 205 | expect(err).to.not.exist; 206 | expect(val2).to.not.exist; 207 | expect(val!).to.equal('foo'); 208 | expect(Date.now() - start).to.be.at.least(14); 209 | done(); 210 | }); 211 | }); 212 | it('should turn async functions into nodey things with mult params', function (done) { 213 | const start = Date.now(); 214 | nodeify(asyncFn2('foo'), function (err: Error | null, val?: string[]) { // eslint-disable-line promise/prefer-await-to-callbacks 215 | expect(err).to.not.exist; 216 | expect(val!).to.eql(['foo', 'foofoo']); 217 | expect(Date.now() - start).to.be.at.least(14); 218 | done(); 219 | }); 220 | }); 221 | it('should handle errors correctly', function (done) { 222 | const start = Date.now(); 223 | nodeify(badAsyncFn(), function (err: Error | null, val?: string) { // eslint-disable-line promise/prefer-await-to-callbacks 224 | expect(val).to.not.exist; 225 | expect(err!.message).to.equal('boo'); 226 | expect(Date.now() - start).to.be.at.least(14); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('parallel', function () { 233 | const asyncFn = async function (val: number): Promise { 234 | await sleep(50); 235 | return val; 236 | }; 237 | const badAsyncFn = async function (): Promise { 238 | await sleep(20); 239 | throw new Error('boo'); 240 | }; 241 | it('should perform tasks in parallel and return results', async function () { 242 | const vals = [1, 2, 3]; 243 | const promises: Promise[] = []; 244 | const start = Date.now(); 245 | for (const v of vals) { 246 | promises.push(asyncFn(v)); 247 | } 248 | const res = await parallel(promises); 249 | expect(Date.now() - start).to.be.at.least(49); 250 | expect(Date.now() - start).to.be.below(59); 251 | expect(res.sort()).to.eql([1, 2, 3]); 252 | }); 253 | it('should error with first response', async function () { 254 | const vals = [1, 2, 3]; 255 | const promises: Promise[] = []; 256 | const start = Date.now(); 257 | for (const v of vals) { 258 | promises.push(asyncFn(v)); 259 | } 260 | promises.push(badAsyncFn()); 261 | let err: Error | null = null; 262 | let res: number[] = []; 263 | try { 264 | res = await parallel(promises); 265 | } catch (e) { 266 | err = e as Error; 267 | } 268 | expect(Date.now() - start).to.be.at.least(19); 269 | expect(Date.now() - start).to.be.below(49); 270 | expect(err).to.exist; 271 | expect(res).to.eql([]); 272 | }); 273 | 274 | describe('waitForCondition', function () { 275 | let requestSpy: sinon.SinonSpy; 276 | beforeEach(function () { 277 | requestSpy = sinon.spy(B, 'delay'); 278 | }); 279 | afterEach(function () { 280 | requestSpy.restore(); 281 | }); 282 | it('should wait and succeed', async function () { 283 | const ref = Date.now(); 284 | function condFn (): boolean { 285 | return Date.now() - ref > 200; 286 | } 287 | const result = await waitForCondition(condFn, {waitMs: 1000, intervalMs: 10}); 288 | const duration = Date.now() - ref; 289 | expect(duration).to.be.above(200); 290 | expect(duration).to.be.below(250); 291 | expect(result).to.be.true; 292 | }); 293 | it('should wait and fail', async function () { 294 | const ref = Date.now(); 295 | function condFn (): boolean { 296 | return Date.now() - ref > 200; 297 | } 298 | try { 299 | await waitForCondition(condFn, {waitMs: 100, intervalMs: 10}); 300 | expect.fail('Should have thrown an error'); 301 | } catch (err: any) { 302 | expect(err.message).to.match(/Condition unmet/); 303 | } 304 | }); 305 | it('should not exceed implicit wait timeout', async function () { 306 | const ref = Date.now(); 307 | function condFn (): boolean { 308 | return Date.now() - ref > 15; 309 | } 310 | await (waitForCondition(condFn, {waitMs: 20, intervalMs: 10})); 311 | const getLastCall = requestSpy.getCall(1); 312 | expect(getLastCall.args[0]).to.be.at.most(10); 313 | }); 314 | }); 315 | }); 316 | 317 | describe('asyncmap', function () { 318 | const mapper = async function (el: number): Promise { 319 | await sleep(10); 320 | return el * 2; 321 | }; 322 | const coll = [1, 2, 3]; 323 | it('should map elements one at a time', async function () { 324 | const start = Date.now(); 325 | expect(await asyncmap(coll, mapper, false)).to.eql([2, 4, 6]); 326 | expect(Date.now() - start).to.be.at.least(30); 327 | }); 328 | it('should map elements in parallel', async function () { 329 | const start = Date.now(); 330 | expect(await asyncmap(coll, mapper)).to.eql([2, 4, 6]); 331 | expect(Date.now() - start).to.be.at.most(20); 332 | }); 333 | it('should handle an empty array', async function () { 334 | expect(await asyncmap([], mapper, false)).to.eql([]); 335 | }); 336 | it('should handle an empty array in parallel', async function () { 337 | expect(await asyncmap([], mapper)).to.eql([]); 338 | }); 339 | }); 340 | 341 | describe('asyncfilter', function () { 342 | const filter = async function (el: number): Promise { 343 | await sleep(5); 344 | return el % 2 === 0; 345 | }; 346 | const coll = [1, 2, 3, 4, 5]; 347 | it('should filter elements one at a time', async function () { 348 | const start = Date.now(); 349 | expect(await asyncfilter(coll, filter, false)).to.eql([2, 4]); 350 | expect(Date.now() - start).to.be.at.least(19); 351 | }); 352 | it('should filter elements in parallel', async function () { 353 | const start = Date.now(); 354 | expect(await asyncfilter(coll, filter)).to.eql([2, 4]); 355 | expect(Date.now() - start).to.be.below(9); 356 | }); 357 | it('should handle an empty array', async function () { 358 | const start = Date.now(); 359 | expect(await asyncfilter([], filter, false)).to.eql([]); 360 | expect(Date.now() - start).to.be.below(9); 361 | }); 362 | it('should handle an empty array in parallel', async function () { 363 | const start = Date.now(); 364 | expect(await asyncfilter([], filter)).to.eql([]); 365 | expect(Date.now() - start).to.be.below(9); 366 | }); 367 | }); 368 | --------------------------------------------------------------------------------