├── .circleci └── config.yml ├── .github ├── CODEOWNERS └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── Makefile ├── README.md ├── bin └── assert_no_file_modifications.sh ├── docs ├── current-version.js └── update-versions.js ├── package-lock.json ├── package.json ├── src ├── all.ts ├── delay.ts ├── errors.ts ├── filter.ts ├── index.ts ├── invert.ts ├── map.ts ├── memoize.ts ├── retry.ts ├── settleAll.ts └── timeout.ts ├── test ├── all.test.ts ├── errors.test.ts ├── filter.test.ts ├── flatMap.test.ts ├── invert.test.ts ├── map.test.ts ├── mapLimit.test.ts ├── mapSeries.test.ts ├── memoize.test.ts ├── retry.test.ts ├── settleAll.test.ts ├── timeout.test.ts └── until.test.ts ├── tsconfig.json ├── tslint.js └── tslint.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.15 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | - run: npm ci 12 | - run: npm test 13 | - run: ./node_modules/coveralls/bin/coveralls.js < coverage/lcov.info 14 | # - run: make docs # this is broken on https://github.com/TypeStrong/typedoc/pull/1014 15 | 16 | - store_artifacts: 17 | path: coverage 18 | destination: coverage 19 | - store_artifacts: 20 | path: docs-build 21 | destination: docs-build 22 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ftrimble @hapoore 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '25 21 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | venv 3 | coverage 4 | logs 5 | dist 6 | resources 7 | *.log 8 | .nyc_output 9 | .idea 10 | *.pyc 11 | tscommand-* 12 | .tscache/ 13 | docs-build/ 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | Gruntfile.js 3 | Makefile 4 | tsconfig.json 5 | tslint.* 6 | bin/ 7 | docs/ 8 | dist/test 9 | 10 | node_modules 11 | venv 12 | coverage 13 | logs 14 | resources 15 | *.log 16 | .nyc_output 17 | .idea 18 | *.pyc 19 | tscommand-* 20 | .tscache/ 21 | docs-build/ 22 | .git 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 1.16.1 4 | ### Bugfixes 5 | - [DS-1262](https://blendlabs.atlassian.net/browse/DS-1262) fix settle all race condition so it returns results in the right order. [#26](https://git.blendlabs.com/blend/promise-utils/pull/26) 6 | 7 | ## 1.16.0 8 | ### Improvements 9 | - [LEND-3824](https://blendlabs.atlassian.net/browse/LEND-3824) Make error messages optional for invert/timeout. [#24](https://git.blendlabs.com/blend/promise-utils/pull/24) 10 | 11 | ## 1.15.0 12 | ### Features 13 | - [DS-1105](https://blendlabs.atlassian.net/browse/DS-1105) settleAll. [#22](https://git.blendlabs.com/blend/promise-utils/pull/22) 14 | 15 | ## 1.14.0 16 | ### Features 17 | - [LEND-3861](https://blendlabs.atlassian.net/browse/LEND-3861) Add an optional cache invalidation timeout to memoize. [#23](https://git.blendlabs.com/blend/promise-utils/pull/23) 18 | 19 | ## 1.13.0 20 | ### Internal 21 | - [LEND-3513](https://blendlabs.atlassian.net/browse/LEND-3513) Add function documentation for promise-utils. [#21](https://git.blendlabs.com/blend/promise-utils/pull/21) 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | const TS_SRC_FILES = ['src/**/*.ts']; 5 | const TS_TEST_FILES = ['test/**/*.ts']; 6 | const ALL_TS_FILES = [...TS_SRC_FILES, ...TS_TEST_FILES]; 7 | const ALL_FILES = [...ALL_TS_FILES, 'Gruntfile.js', 'package-lock.json', 'package.json', 'tsconfig.json', 'tslint.*']; 8 | 9 | grunt.initConfig({ 10 | run: { 11 | testFix: { 12 | cmd: 'npm', 13 | args: ['run', 'testFix'] 14 | }, 15 | compile: { 16 | cmd: 'npm', 17 | args: ['run', 'prepare'] 18 | } 19 | }, 20 | 21 | watch: { 22 | compile: { 23 | files: ALL_FILES, 24 | tasks: [ 'test'], 25 | options: { 26 | spawn: false, 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | grunt.loadNpmTasks('grunt-contrib-watch'); 33 | grunt.loadNpmTasks('grunt-force-task'); 34 | grunt.loadNpmTasks('grunt-run'); 35 | 36 | grunt.registerTask('test', ['run:testFix']); 37 | grunt.registerTask('default', ['force:test', 'watch']); 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Blend Labs, Inc. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help gh-pages docs push-docs 2 | 3 | CURRENT_VERSION := $(shell node docs/current-version.js) 4 | DOCS_DIR := ./docs-build/$(CURRENT_VERSION) 5 | 6 | help: 7 | @echo 'Makefile for `promise-utils` package' 8 | @echo '' 9 | @echo 'Usage:' 10 | @echo ' make gh-pages Checkout `gh-pages` in `docs-build/`' 11 | @echo ' make docs Generate the documentation in `docs-build/`' 12 | @echo ' make push-docs Push the documentation to Github' 13 | @echo '' 14 | 15 | gh-pages: 16 | rm -fr ./docs-build/ # Clean up old state 17 | git worktree prune # Clean up old state 18 | git fetch origin # Make sure up to date. 19 | git worktree add --checkout ./docs-build/ gh-pages 20 | 21 | docs: 22 | # Make sure `npm ci` is run. 23 | [ -d ./node_modules ] || npm ci 24 | 25 | rm -rf $(DOCS_DIR) 26 | 27 | # Generate new docs. 28 | ./node_modules/.bin/typedoc \ 29 | --excludeNotExported \ 30 | --excludePrivate \ 31 | --readme none \ 32 | --mode file \ 33 | --out $(DOCS_DIR) \ 34 | ./src 35 | 36 | push-docs: gh-pages docs 37 | # Remove latest and place newly generated docs there. 38 | rm -fr ./docs-build/latest/ 39 | cp -r $(DOCS_DIR) ./docs-build/latest/ 40 | 41 | # Update `versions.json` in `docs-build`. 42 | node docs/update-versions.js $(CURRENT_VERSION) 43 | 44 | # `git` add all updated paths 45 | (cd ./docs-build/ && \ 46 | git add versions.json && \ 47 | git add $(CURRENT_VERSION) && \ 48 | git add latest) 49 | 50 | # `git` push all the updates to the remote 51 | cd ./docs-build/ && \ 52 | git push origin gh-pages 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | promise-utils 2 | ============= 3 | 4 | [![Build Status](https://circleci.com/gh/blend/promise-utils.svg?style=shield)](https://circleci.com/gh/blend/promise-utils) 5 | [![Coverage Status](https://coveralls.io/repos/github/blend/promise-utils/badge.svg?branch=master)](https://coveralls.io/github/blend/promise-utils?branch=master) 6 | ![Minzipped size](https://img.shields.io/bundlephobia/minzip/blend-promise-utils.svg) 7 | 8 | Promise-utils is a dependency-free JavaScript/TypeScript library that 9 | provides Lodash-like utility functions for dealing with native ES6 10 | promises. 11 | 12 | ## Installation 13 | 14 | ``` 15 | $ npm install blend-promise-utils 16 | ``` 17 | 18 | ## Usage Example 19 | 20 | ```js 21 | const promiseUtils = require('blend-promise-utils') 22 | const { promises: fs } = require('fs') 23 | const request = require('request-promise-native'); 24 | const isEmpty = require('lodash.isempty'); 25 | 26 | const MS_IN_SECOND = 1000; 27 | 28 | async function main() { 29 | const cachedResponse = promiseUtils.memoize( 30 | async (contents) => request(contents.url), 31 | contents => contents.url, 32 | 15 * MS_IN_SECOND // contents could change 33 | ); 34 | 35 | const fileContents = await promiseUtils.map( 36 | ['file1', 'file2', 'file3'], 37 | async fileName => { 38 | const rawData = await fs.readFile(fileName); 39 | return JSON.parse(rawData); 40 | }, 41 | ); 42 | 43 | while (true) { 44 | await promiseUtils.delay(150); // avoid slamming CPU 45 | 46 | await promiseUtils.mapSeries( 47 | fileContents, 48 | async contents => { 49 | const remoteData = await cachedResponse(contents); 50 | 51 | const { results, errors } = await promiseUtils.settleAll([ 52 | asyncFunction1(), 53 | asyncFunction2(), 54 | asyncFunction3(), 55 | ]); 56 | 57 | if (!isEmpty(errors)) { 58 | throw new Error(`Unable to settle all functions: ${JSON.stringify(errors)}`); 59 | } else { 60 | return results; 61 | } 62 | } 63 | ) 64 | } 65 | 66 | await promiseUtils.retry(flakyFunction, { maxAttempts: 3, delayMs: 150 })(flakyFunctionArgument); 67 | 68 | await promiseUtils.timeout(longFunction, 60 * MS_IN_SECOND)(longFunctionArgument); 69 | } 70 | 71 | main() 72 | ``` 73 | 74 | ## API 75 | 76 | - [Documentation][2] 77 | - [Past versions][3] 78 | 79 | ## Test 80 | 81 | ``` 82 | $ npm test 83 | ``` 84 | 85 | ## Documentation 86 | 87 | Build docs 88 | ``` 89 | $ make docs 90 | ``` 91 | 92 | Push docs to Github 93 | ``` 94 | $ make push-docs 95 | ``` 96 | 97 | ## License 98 | 99 | [MIT](LICENSE) 100 | 101 | [1]: https://blend.github.io/promise-utils 102 | [2]: https://blend.github.io/promise-utils/latest/ 103 | [3]: https://blend.github.io/promise-utils/versions.html 104 | -------------------------------------------------------------------------------- /bin/assert_no_file_modifications.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | status=`git status | grep modified` 4 | 5 | if [[ ! -z $status ]]; then 6 | echo "Failing because files have been modified by tests" 7 | echo "$status" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /docs/current-version.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function main() { 4 | const filename = path.join(__dirname, '../package.json'); 5 | var pjson = require(filename); 6 | console.log(pjson.version); 7 | } 8 | 9 | if (require.main === module) { 10 | main(); 11 | } 12 | -------------------------------------------------------------------------------- /docs/update-versions.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const process = require('process'); 4 | 5 | function main() { 6 | const currentVersion = process.argv[2]; 7 | const filename = path.join(__dirname, '../docs-build/versions.json'); 8 | const versions = require(filename); 9 | versions.push(currentVersion); 10 | fs.writeFileSync(filename, JSON.stringify(versions)); 11 | } 12 | 13 | if (require.main === module) { 14 | main(); 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blend-promise-utils", 3 | "version": "1.29.2", 4 | "author": "Blend", 5 | "license": "MIT", 6 | "homepage": "https://blend.github.io/promise-utils", 7 | "description": "Lodash-like utilities for dealing with promises", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/blend/promise-utils.git" 11 | }, 12 | "main": "dist/src/index.js", 13 | "types": "dist/src/index.d.ts", 14 | "scripts": { 15 | "prepare": "tsc", 16 | "prettier:base": "prettier '{src,test}/**/**/*.ts'", 17 | "prettier": "npm run prettier:base -- --write", 18 | "lintSrc": "tslint --project tsconfig.json --config tslint.js 'src/**/*.ts'", 19 | "lintTest": "tslint --project tsconfig.json --config tslint.test.js 'test/**/*.ts'", 20 | "lint": "npm run prettier:base -- --list-different && npm run lintSrc && npm run lintTest", 21 | "lintFix": "npm run prettier && npm run lintSrc -- --fix && npm run lintTest -- --fix", 22 | "testCode": "nyc ava", 23 | "test": "tsc && npm run testCode && npm run lint && ./bin/assert_no_file_modifications.sh", 24 | "testFix": "tsc && npm run testCode && npm run lintFix", 25 | "start": "grunt" 26 | }, 27 | "prettier": { 28 | "singleQuote": true, 29 | "trailingComma": "all", 30 | "printWidth": 100 31 | }, 32 | "ava": { 33 | "files": [ 34 | "dist/test/**/*.test.js" 35 | ], 36 | "sources": [ 37 | "src/*.ts", 38 | "src/**/*.ts" 39 | ], 40 | "concurrency": 5, 41 | "verbose": true, 42 | "timeout": "10000", 43 | "failFast": false, 44 | "powerAssert": true 45 | }, 46 | "nyc": { 47 | "reporter": [ 48 | "html", 49 | "json", 50 | "lcov", 51 | "text" 52 | ], 53 | "require": [ 54 | "source-map-support/register" 55 | ], 56 | "extension": [ 57 | ".ts" 58 | ], 59 | "skip-full": true, 60 | "check-coverage": true, 61 | "lines": 100, 62 | "functions": 100, 63 | "branches": 100, 64 | "statements": 100, 65 | "exclude": [ 66 | "dist/test/**/*.js" 67 | ] 68 | }, 69 | "devDependencies": { 70 | "@types/lodash": "4.14.136", 71 | "@types/sinon": "7.0.13", 72 | "ava": "2.2.0", 73 | "coveralls": "3.0.4", 74 | "grunt": "1.3.0", 75 | "grunt-contrib-watch": "1.1.0", 76 | "grunt-force-task": "2.0.0", 77 | "grunt-run": "0.8.1", 78 | "lodash": "4.17.21", 79 | "nyc": "14.1.1", 80 | "prettier": "1.18.2", 81 | "sinon": "7.3.2", 82 | "tslint": "5.18.0", 83 | "typedoc": "0.14.2", 84 | "typescript": "3.5.3" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/all.ts: -------------------------------------------------------------------------------- 1 | import { settleAll } from './settleAll'; 2 | 3 | /** 4 | * Attempts to resolve all promises in promises in parallel. 5 | * This does essentially what `Promise.all` does, but we have found that Promise.all 6 | * is unsafe when multiple promises reject. This will handle failures appropriately, 7 | * though it will defer handling until the last promise has resolved/rejected. 8 | * 9 | * In the case of a rejection, the thrown error will be the rejection of the first 10 | * promise in the array (as opposed to the first promise to be rejected temporally), 11 | * with all other errors attached in the `otherErrors` property. 12 | * 13 | * @param {Promise[]} promises - An array of promises to attempt to resolve. 14 | * @returns A list of resolved values of promises. 15 | */ 16 | export async function all(promises: readonly Promise[]): Promise { 17 | const intermediateResults = await settleAll(promises); 18 | if (intermediateResults.errors && intermediateResults.errors.length > 0) { 19 | const primaryError = intermediateResults.errors[0]; 20 | if (intermediateResults.errors.length > 1 && primaryError instanceof Error) { 21 | primaryError.message = `${primaryError.message}... and ${intermediateResults.errors.length - 22 | 1} other errors`; 23 | // tslint:disable-next-line:no-any (intentionally augmenting error) 24 | (primaryError as any).otherErrors = intermediateResults.errors.slice(1); 25 | } 26 | throw primaryError; 27 | } 28 | 29 | return intermediateResults.results; 30 | } 31 | -------------------------------------------------------------------------------- /src/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Optionally returns a value after a delay. This is useful if you want to add jitter or need to 3 | * wait for some external reason. 4 | * 5 | * @param {number} delayTime - the amount of milliseconds to wait before returning 6 | * @optionalParam {any} value - the value to return after the delay 7 | * @returns value - if defined 8 | */ 9 | export async function delay(delayTimeMs: number, value: T): Promise; 10 | export async function delay(delayTimeMs: number): Promise; 11 | // tslint:disable-next-line:no-any typedef (typed by overload signatures) 12 | export async function delay(delayTime: any, value?: T): Promise { 13 | return new Promise( 14 | // tslint:disable-next-line:no-any (typed by overload signatures) 15 | resolve => setTimeout(() => resolve(value), delayTime), 16 | ); 17 | } 18 | 19 | /** 20 | * Optionally returns a value after deferring execution. This is useful if you need to wait for 21 | * anything left on the event loop. 22 | * 23 | * @optionalParam {any} value - the value to return 24 | * @returns value - if defined 25 | */ 26 | export async function immediate(value: T): Promise; 27 | export async function immediate(): Promise; 28 | // tslint:disable-next-line:no-any typedef (typed by overload signatures) 29 | export async function immediate(value?: any) { 30 | return new Promise( 31 | // tslint:disable-next-line:no-any (typed by overload signatures) 32 | resolve => setImmediate(() => resolve(value)), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Rather than just blanket propagating errors, allows you to specify an error handler that can 3 | * transform it to something useful or throw a wrapped error. 4 | * 5 | * @param {Function} fn - An async function to wrap 6 | * @param {Function} errorHandler 7 | * - a function that will process any errors produced by the original function 8 | * Note that this can be used to either return something that isn't an error (for expected 9 | * errors), to add additional context to an error and throw that error, or even just to create 10 | * side effects for error throwing (e.g. logging) 11 | * @returns A wrapped version of function that uses error handler 12 | */ 13 | export function transformErrors(fn: T, errorHandler: Function): T { 14 | // tslint:disable-next-line:no-any (casting as any to preserve original function type) 15 | return ((async (...args: any[]): Promise => { 16 | try { 17 | return await fn(...args); 18 | } catch (err) { 19 | return errorHandler(err); 20 | } 21 | // tslint:disable-next-line:no-any (casting as any to preserve original function type) 22 | }) as any) as T; 23 | } 24 | -------------------------------------------------------------------------------- /src/filter.ts: -------------------------------------------------------------------------------- 1 | import { map } from './map'; 2 | 3 | /** 4 | * Returns a new array of all the values in coll which pass an async truth test. This operation is 5 | * performed in parallel, but the results array will be in the same order as the original. 6 | * 7 | * @param coll {Array | Object} A collection to iterate over. 8 | * @param predicate {Function} - A truth test to apply to each item in coll. Invoked with (item, 9 | * key/index), must return a boolean. 10 | * @returns The filtered collection 11 | */ 12 | export async function filter( 13 | input: readonly T[], 14 | predicate: (value: T, index: number) => Promise, 15 | ): Promise; 16 | export async function filter( 17 | input: readonly T[], 18 | predicate: (value: T) => Promise, 19 | ): Promise; 20 | export async function filter( 21 | input: T, 22 | predicate: (value: T[keyof T], key: keyof T) => Promise, 23 | ): Promise; 24 | export async function filter( 25 | input: T, 26 | predicate: (value: T[keyof T]) => Promise, 27 | ): Promise; 28 | // tslint:disable-next-line:no-any (types are enforced by overload signatures, validated by tests) 29 | export async function filter(input: any, predicate: any): Promise { 30 | if (!input) { 31 | return []; 32 | } 33 | 34 | // tslint:disable-next-line:no-any 35 | const predicateResults: any = Array.isArray(input) ? new Array(input.length) : {}; 36 | // tslint:disable-next-line:no-any 37 | await map(input, async (value: any, key: any) => { 38 | predicateResults[key] = await predicate(value, key); 39 | }); 40 | 41 | // tslint:disable-next-line:no-any 42 | const output: any[] = []; 43 | for (const k in input) { 44 | if (predicateResults[k]) { 45 | output.push(input[k]); 46 | } 47 | } 48 | return output; 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delay'; 2 | export * from './filter'; 3 | export * from './invert'; 4 | export * from './map'; 5 | export * from './errors'; 6 | export * from './retry'; 7 | export * from './settleAll'; 8 | export * from './all'; 9 | export * from './timeout'; 10 | export * from './memoize'; 11 | -------------------------------------------------------------------------------- /src/invert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inverts the given promise - i.e. throws an error if it completes successfully and returns the 3 | * error if it throws one. 4 | * 5 | * @param {Promise} promise - the promise to invert 6 | * @param {string} message - the message to throw if the promise resolves 7 | * @returns the error thrown by the promise 8 | */ 9 | // tslint:disable-next-line:no-any (returns the rejection which is untyped) 10 | export async function invert(promise: Promise, message?: string): Promise { 11 | message = message || 'Expected promise to reject'; 12 | return promise.then( 13 | () => { 14 | throw new Error(message); 15 | }, 16 | err => err, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_MAP_PARALLELISM = 10; 2 | 3 | /** 4 | * Produces a new collection of values by mapping each value in coll through the async iteratee 5 | * function. The iteratee is called with an item from coll and the key (or index) of that item. 6 | * 7 | * Note, that since this function applies the iteratee to each item in parallel, there is no 8 | * guarantee that the iteratee functions will complete in order. However, the results array will be 9 | * in the same order as the original coll. 10 | * 11 | * WARNING: because of how the iteratee is applied, there is a possibility that if your 12 | * input is an array of promises, they could be settled before the iteratee is applied - if they 13 | * reject in this scenario, it would result in an unhandledRejection. As such, you should use 14 | * settleAll to deal with arrays of promises, which will avoid this scenario. 15 | * 16 | * @param {Array | Iterable | Object} input - A collection to iterate over. 17 | * @param {AsyncFunction} iteratee - An async function to apply to each item in coll. The iteratee 18 | * should return the transformed item. Invoked with (item, key). 19 | */ 20 | export async function map( 21 | input: readonly T[], 22 | iteratee: (value: T, index: number) => Promise, 23 | ): Promise; 24 | export async function map( 25 | input: readonly T[], 26 | iteratee: (value: T) => Promise, 27 | ): Promise; 28 | export async function map( 29 | input: T, 30 | iteratee: (value: T[keyof T], key: string) => Promise, 31 | ): Promise; 32 | export async function map( 33 | input: T, 34 | iteratee: (value: T[keyof T]) => Promise, 35 | ): Promise; 36 | // tslint:disable-next-line:no-any (types are enforced by overload signatures, validated by tests) 37 | export async function map(input: any, iteratee: any): Promise { 38 | return mapLimit(input, DEFAULT_MAP_PARALLELISM, iteratee); 39 | } 40 | 41 | /** 42 | * The same as map but runs a maximum of limit async operations at a time with the same ordering 43 | * guarantees. 44 | * 45 | * @param {Array | Iterable | Object} input - A collection to iterate over. 46 | * @param {number} limit - The maximum number of async operations at a time. 47 | * @param {AsyncFunction} iteratee - An async function to apply to each item in coll. The iteratee 48 | * should complete with the transformed item. Invoked with (item, key). 49 | */ 50 | export async function mapLimit( 51 | input: readonly T[], 52 | limit: number, 53 | iteratee: (value: T, index: number) => Promise, 54 | ): Promise; 55 | export async function mapLimit( 56 | input: readonly T[], 57 | limit: number, 58 | iteratee: (value: T) => Promise, 59 | ): Promise; 60 | export async function mapLimit( 61 | input: T, 62 | limit: number, 63 | iteratee: (value: T[keyof T], key: string) => Promise, 64 | ): Promise; 65 | export async function mapLimit( 66 | input: T, 67 | limit: number, 68 | iteratee: (value: T[keyof T]) => Promise, 69 | ): Promise; 70 | // tslint:disable-next-line:no-any (types are enforced by overload signatures, validated by tests) 71 | export async function mapLimit(input: any, limit: number, iteratee: any): Promise { 72 | if (!input) { 73 | return []; 74 | } 75 | 76 | const isArray = Array.isArray(input); 77 | const size = (() => { 78 | if (isArray) { 79 | return input.length; 80 | } 81 | 82 | let count = 0; 83 | for (const __ in input) { 84 | ++count; 85 | } 86 | return count; 87 | })(); 88 | 89 | const allValues = new Array(size); 90 | const results = new Array(size); 91 | 92 | let i = 0; 93 | for (const key in input) { 94 | const possiblyNumericKey = isArray ? i : key; 95 | allValues[size - 1 - i] = [input[key], i, possiblyNumericKey]; 96 | ++i; 97 | } 98 | 99 | const execute = async () => { 100 | while (allValues.length > 0) { 101 | // tslint:disable-next-line:no-any 102 | const [val, index, key] = allValues.pop(); 103 | results[index] = await iteratee(val, key); 104 | } 105 | }; 106 | 107 | const allExecutors = []; 108 | for (let j = 0; j < limit; ++j) { 109 | allExecutors.push(execute()); 110 | } 111 | await Promise.all(allExecutors); 112 | 113 | return results; 114 | } 115 | 116 | /** 117 | * The same as mapLimit but with only 1 operation at a time, and maintains the ordering guarantees 118 | * of map. 119 | * 120 | * @param {Array | Iterable | Object} input - A collection to iterate over. 121 | * @param {AsyncFunction} iteratee - An async function to apply to each item in coll. The iteratee 122 | * should complete with the transformed item. Invoked with (item, key). 123 | */ 124 | export async function mapSeries( 125 | input: readonly T[], 126 | iteratee: (value: T, index: number) => Promise, 127 | ): Promise; 128 | export async function mapSeries( 129 | input: readonly T[], 130 | iteratee: (value: T) => Promise, 131 | ): Promise; 132 | export async function mapSeries( 133 | input: T, 134 | iteratee: (value: T[keyof T], key: string) => Promise, 135 | ): Promise; 136 | export async function mapSeries( 137 | input: T, 138 | iteratee: (value: T[keyof T]) => Promise, 139 | ): Promise; 140 | // tslint:disable-next-line:no-any (types are enforced by overload signatures, validated by tests) 141 | export async function mapSeries(input: any, iteratee: any): Promise { 142 | return mapLimit(input, 1, iteratee); 143 | } 144 | 145 | /** 146 | * The same as map but will flatten the results. 147 | * 148 | * @param {Array | Iterable | Object} input - A collection to iterate over. 149 | * @param {AsyncFunction} iteratee - An async function to apply to each item in coll. The iteratee 150 | * should complete with the transformed item. Invoked with (item, key). 151 | */ 152 | export async function flatMap( 153 | input: readonly T[], 154 | iteratee: (value: T, index: number) => Promise, 155 | ): Promise; 156 | export async function flatMap( 157 | input: readonly T[], 158 | iteratee: (value: T) => Promise, 159 | ): Promise; 160 | export async function flatMap( 161 | input: T, 162 | iteratee: (value: T[keyof T], key: string) => Promise, 163 | ): Promise; 164 | export async function flatMap( 165 | input: T, 166 | iteratee: (value: T[keyof T]) => Promise, 167 | ): Promise; 168 | // tslint:disable-next-line:no-any (types are enforced by overload signatures, validated by tests) 169 | export async function flatMap(input: any, iteratee: any): Promise { 170 | if (!input) { 171 | return []; 172 | } 173 | let output = []; 174 | const nestedOutput = await map(input, iteratee); 175 | for (const partialOutput of nestedOutput) { 176 | // tslint:disable-next-line:no-any (could possibly be an array) 177 | if (partialOutput && (partialOutput as any).length !== undefined) { 178 | // tslint:disable-next-line:no-any (is definitely an array) 179 | output = output.concat(partialOutput) as any[]; 180 | } else { 181 | output.push(partialOutput); 182 | } 183 | } 184 | return output; 185 | } 186 | -------------------------------------------------------------------------------- /src/memoize.ts: -------------------------------------------------------------------------------- 1 | type ThenReturn = T extends Promise 2 | ? U // tslint:disable:no-any 3 | : T extends ((...args: any[]) => Promise) 4 | ? V 5 | : T; 6 | 7 | /** 8 | * Caches the results of an async function. It takes a synchronous hasher that uses the input to 9 | * the function to determine when to return a memoized result. 10 | * 11 | * If no hash function is specified, the first argument is used as a hash key, which may work 12 | * reasonably if it is a string or a data type that converts to a distinct string. Note that objects 13 | * and arrays will not behave reasonably. Neither will cases where the other arguments are 14 | * significant. In such cases, specify your own hash function. 15 | * 16 | * WARNING: This function uses memory for each unique hasher output and does not clean it up, even 17 | * after the timeout has passed. If you have many unique values that could hash and they shift over 18 | * time, you will need to manage the memory of the map. The return of this function does expose 19 | * memory management operations; if this sounds like your use case, setInterval(memoizedFn.clear, 20 | * timeoutMs) is a good starting point. 21 | * 22 | * @param {AsyncFunction} fn - The async function to proxy and cache results from. 23 | * @param {Function} hasher - An optional function for generating a custom hash for storing 24 | * results. It has all the arguments applied to it and must be synchronous. 25 | * @returns a memoized version of fn 26 | */ 27 | // tslint:disable:no-any defining it this way is more precise than Function so is still preferable 28 | export function memoize Promise>( 29 | fn: FnType, 30 | // tslint:disable:no-any hasher can return any value that can be used as a map key 31 | hasher: (...args: Parameters) => any = (...args) => args[0], 32 | timeoutMs?: number, 33 | ): FnType & { reset: (...args: Parameters) => void; clear: () => void } { 34 | const memos: Map< 35 | ReturnType, 36 | { value: ThenReturn; expiration: number } 37 | > = new Map(); 38 | const queues: Map, Promise>> = new Map(); 39 | 40 | const returnFn = async (...args: Parameters): Promise> => { 41 | const key = hasher(...args); 42 | if (memos.has(key)) { 43 | if (!timeoutMs || Date.now() < memos.get(key)!.expiration) { 44 | return memos.get(key)!.value; 45 | } 46 | } 47 | 48 | if (queues.has(key)) { 49 | return await queues.get(key)!; 50 | } 51 | 52 | const promise = fn(...args); 53 | queues.set(key, promise); 54 | 55 | try { 56 | const ret = await queues.get(key)!; 57 | memos.set(key, { value: ret, expiration: Date.now() + (timeoutMs || 0) }); 58 | return ret; 59 | } finally { 60 | queues.delete(key); 61 | } 62 | }; 63 | 64 | const reset = (...args: Parameters): void => { 65 | const key = hasher(...args); 66 | if (memos.has(key)) { 67 | memos.delete(key); 68 | } 69 | }; 70 | 71 | const clear = (): void => { 72 | memos.clear(); 73 | }; 74 | 75 | (returnFn as any).reset = reset; 76 | (returnFn as any).clear = clear; 77 | 78 | return returnFn as FnType & { reset: FnType; clear: () => void }; 79 | // tslint:enable:no-any (unfortunately we can't give the FnType any more clarity or it limits what 80 | // you can do with it) 81 | } 82 | -------------------------------------------------------------------------------- /src/retry.ts: -------------------------------------------------------------------------------- 1 | import { delay } from './delay'; 2 | 3 | export interface BaseRetryOpts { 4 | maxAttempts: number; 5 | delayMs?: number; 6 | } 7 | 8 | export interface RetryOpts extends BaseRetryOpts { 9 | isRetryable?: (err: Error) => boolean; 10 | } 11 | 12 | /** 13 | * Attempts to get a successful response from task no more than maxAttempts times before 14 | * returning an error. If the task is successful, the return will be the result of the 15 | * successful task. If all attempts fail, it will throw the error of the final attempt. 16 | * 17 | * @param {AsyncFunction} fn - An async function to retry. 18 | * @param {RetryOpts} opts 19 | * - maxAttempts - The number of attempts to make before giving up. 20 | * - delayMs - The time to wait between retries, in milliseconds. The default is 0. 21 | * - isRetryable - An optional synchronous function that is invoked on erroneous result. If it 22 | * returns true the retry attempts will continue; if the function returns false the retry 23 | * flow is aborted with the current attempt's error and result being returned. Invoked 24 | * with (err). 25 | * @returns A wrapped version of function that performs retries 26 | */ 27 | export function retry(fn: T, retryOpts: RetryOpts): T { 28 | // tslint:disable-next-line:no-any (casting as any to preserve original function type) 29 | return ((async (...args: any[]): Promise => { 30 | let lastErr: Error = new Error( 31 | `Could not complete function within ${retryOpts.maxAttempts} attempts`, 32 | ); 33 | for (let i = 0; i < retryOpts.maxAttempts; ++i) { 34 | try { 35 | return await fn(...args); 36 | } catch (err) { 37 | if (retryOpts.isRetryable && !retryOpts.isRetryable(err)) { 38 | throw err; 39 | } 40 | lastErr = err; 41 | } 42 | if (retryOpts.delayMs && i < retryOpts.maxAttempts - 1) { 43 | await delay(retryOpts.delayMs); 44 | } 45 | } 46 | throw lastErr; 47 | // tslint:disable-next-line:no-any (casting as any to preserve original function type) 48 | }) as any) as T; 49 | } 50 | 51 | /** 52 | * Attempts to get a truthy response from task no more than maxAttempts times before 53 | * throwing an error. If the task is successful, it will return the result of the 54 | * successful task. If all attempts fail, it will throw an error indicating as such. 55 | * 56 | * @param {AsyncFunction} fn - An async function to retry. 57 | * @param {BaseRetryOpts} opts 58 | * - maxAttempts - The number of attempts to make before giving up. 59 | * - delayMs - The time to wait between retries, in milliseconds. The default is 0. 60 | * @returns A wrapped version of fn that performs retries on falsey results 61 | */ 62 | export function until< 63 | // tslint:disable-next-line:no-any (need to support arbitrary args) 64 | T extends (...args: any[]) => Promise 65 | >(fn: T, retryOpts: BaseRetryOpts): T { 66 | // tslint:disable-next-line:no-any (need to support arbitrary args) 67 | return (async (...args: any[]): Promise => { 68 | for (let i = 0; i < retryOpts.maxAttempts; ++i) { 69 | const result = await fn(...args); 70 | if (!!result) { 71 | return result; 72 | } 73 | if (retryOpts.delayMs) { 74 | await delay(retryOpts.delayMs); 75 | } 76 | } 77 | throw new Error(`Could not complete function within ${retryOpts.maxAttempts} attempts`); 78 | }) as T; 79 | } 80 | -------------------------------------------------------------------------------- /src/settleAll.ts: -------------------------------------------------------------------------------- 1 | export interface SettledPromises { 2 | errors: V[]; 3 | results: T[]; 4 | } 5 | /** 6 | * Attempts to settle all promises in promises in parallel, calling errFn when a promise rejects. 7 | * Similar to Promise.all, but does not fail fast. For resolved promises, the result array contains 8 | * resolved values in the same order as the promises. For rejected promises, the error array 9 | * contains the return values of errFn in the same order as the promises. 10 | * 11 | * @param {Promise[]} promises - An array of promises to attempt to settle. 12 | * @param {Function} errFn - The function to call when a promise rejects. 13 | * Accepts an error and optionally the index of the promise that caused that error. 14 | * 15 | * @returns A list of resolved and rejected values of promises. 16 | */ 17 | export async function settleAll( 18 | promises: readonly Promise[], 19 | // tslint:disable-next-line:no-any (no way to guarantee error typings) 20 | errFn?: (err: any, ind: number) => Promise, 21 | ): Promise>; 22 | export async function settleAll( 23 | promises: readonly Promise[], 24 | // tslint:disable-next-line:no-any (no way to guarantee error typings) 25 | errFn?: (err: any) => Promise, 26 | ): Promise>; 27 | export async function settleAll( 28 | promises: readonly Promise[], 29 | // tslint:disable-next-line:no-any (no way to guarantee error typings) 30 | errFn?: (err: any, ind: number) => V, 31 | ): Promise>; 32 | export async function settleAll( 33 | promises: readonly Promise[], 34 | // tslint:disable-next-line:no-any (no way to guarantee error typings) 35 | errFn?: (err: any) => V, 36 | ): Promise>; 37 | export async function settleAll( 38 | promises: readonly Promise[], 39 | // tslint:disable-next-line:no-any (no way to guarantee error typings) 40 | errFn: (err: any, ind: number) => V = err => err, 41 | ): Promise> { 42 | const intermediateResults: { errors?: V; results?: T }[] = await Promise.all( 43 | (promises || []).map(async (p, i) => { 44 | try { 45 | return { results: await p }; 46 | } catch (err) { 47 | return { errors: await errFn(err, i) }; 48 | } 49 | }), 50 | ); 51 | const settledPromises: SettledPromises = { results: [], errors: [] }; 52 | for (const result of intermediateResults) { 53 | for (const key in result) { 54 | // @ts-ignore typings line up, but typescript is hard pressed to agree 55 | settledPromises[key].push(result[key]); 56 | } 57 | } 58 | return settledPromises; 59 | } 60 | -------------------------------------------------------------------------------- /src/timeout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets a time limit on an asynchronous function. If the function does return within 3 | * the specified milliseconds, it will throw a timeout error. 4 | * 5 | * WARNING: due to limitations of node, this does not actually abort the pending promise. If you 6 | * need to prevent continuation of the operations within the function, you need to essentially 7 | * create progress checkers that determine whether the function should terminate if sufficient time 8 | * has passed. 9 | * 10 | * @param {AsyncFunction} fn - The async function to limit in time. 11 | * @param {number} expirationTime - The specified time limit. 12 | * @param {string} errorMessage - The message that should be sent if the function times out 13 | * @returns Returns a wrapped function that will throw an error if it takes too long. Invoke this 14 | * function with the same parameters as you would fn. 15 | */ 16 | export function timeout( 17 | fn: T, 18 | expirationTime: number, 19 | errorMessage?: string, 20 | ): T { 21 | errorMessage = 22 | errorMessage || `Could not resolve ${fn.name || ''} within ${expirationTime} ms`; 23 | // tslint:disable-next-line:typedef no-any (typedef is hacked because we're hijacking fn) 24 | return (async function race(this: any) { 25 | return Promise.race([ 26 | new Promise((__: Function, reject: Function): void => { 27 | setTimeout((): void => reject(new Error(errorMessage)), expirationTime); 28 | }) as Promise, 29 | fn.apply(this, arguments), 30 | ]); 31 | // tslint:disable-next-line:no-any (need to cast to any because we hacked together the typedef) 32 | } as any) as T; 33 | } 34 | -------------------------------------------------------------------------------- /test/all.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as promiseUtils from '../src/index'; 4 | 5 | test('throws errors in order with multiple errors', async t => { 6 | const testPromises: Promise[] = [ 7 | promiseUtils.invert(promiseUtils.delay(500, null), 'delayed error'), 8 | promiseUtils.delay(500, { success: true }), 9 | Promise.resolve(3), 10 | Promise.reject(new Error('failed')), 11 | Promise.resolve('success'), 12 | Promise.reject('also failed'), 13 | ]; 14 | const err = await promiseUtils.invert(promiseUtils.all(testPromises)); 15 | t.is(err.message, 'delayed error... and 2 other errors'); 16 | t.deepEqual(err.otherErrors, [new Error('failed'), 'also failed']); 17 | }); 18 | 19 | test('returns values in order of array not execution speed', async t => { 20 | const testPromises: Promise[] = [ 21 | promiseUtils.delay(500, { success: true }), 22 | Promise.resolve(3), 23 | Promise.resolve('success'), 24 | ]; 25 | const res = await promiseUtils.all(testPromises); 26 | t.deepEqual(res, [{ success: true }, 3, 'success']); 27 | }); 28 | 29 | test('throws single error', async t => { 30 | const testPromises: Promise[] = [ 31 | Promise.reject(new Error('failed')), 32 | Promise.resolve('success'), 33 | ]; 34 | const err = await promiseUtils.invert(promiseUtils.all(testPromises)); 35 | t.deepEqual(err, new Error('failed')); 36 | }); 37 | 38 | test('settles null value', async t => { 39 | const res = await promiseUtils.all(null as any); 40 | t.deepEqual(res, []); 41 | }); 42 | 43 | test('works for results', async t => { 44 | const testPromises: Promise[] = [Promise.resolve('a'), Promise.resolve('b')]; 45 | const res = await promiseUtils.all(testPromises); 46 | t.deepEqual(res, ['a', 'b']); 47 | }); 48 | 49 | test('handles empty promise array', async t => { 50 | const res = await promiseUtils.all([]); 51 | t.deepEqual(res, []); 52 | }); 53 | -------------------------------------------------------------------------------- /test/errors.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as promiseUtils from '../src/index'; 4 | 5 | test('throws wrapped error', async t => { 6 | const err = await promiseUtils.invert( 7 | promiseUtils.transformErrors( 8 | async () => { 9 | throw new Error('broken'); 10 | }, 11 | () => { 12 | throw new Error('real error'); 13 | }, 14 | )(), 15 | ); 16 | t.is(err.message, 'real error'); 17 | }); 18 | 19 | test('returns a new result', async t => { 20 | const message = await promiseUtils.transformErrors( 21 | async () => { 22 | throw new Error('broken'); 23 | }, 24 | () => 'swallowed!', 25 | )(); 26 | t.is(message, 'swallowed!'); 27 | }); 28 | -------------------------------------------------------------------------------- /test/filter.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import test from 'ava'; 4 | 5 | import * as promiseUtils from '../src/index'; 6 | 7 | test('returns empty array when given no input', async t => { 8 | const output = await promiseUtils.filter(null as any, _.identity); 9 | t.deepEqual(output, []); 10 | }); 11 | 12 | test('filters arrays and maintains order', async t => { 13 | const input = [1, 2, 3, 4, 5, 6, 7, 8]; 14 | 15 | let nextIndexToRelease = input.length - 1; 16 | const getNextIndexToRelease = () => nextIndexToRelease; 17 | 18 | const output = await promiseUtils.filter(input, async (value: any, index: number) => { 19 | // Returns in reverse order 20 | while (getNextIndexToRelease() !== index) { 21 | await promiseUtils.immediate(); 22 | } 23 | nextIndexToRelease--; 24 | return value > 3; 25 | }); 26 | t.deepEqual(output, [4, 5, 6, 7, 8]); 27 | }); 28 | 29 | test('filters arrays with indices and maintains order', async t => { 30 | const input = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; 31 | 32 | let nextIndexToRelease = input.length - 1; 33 | const getNextIndexToRelease = () => nextIndexToRelease; 34 | 35 | const output = await promiseUtils.filter(input, async (value: any, index: number) => { 36 | // Returns in reverse order 37 | while (getNextIndexToRelease() !== index) { 38 | await promiseUtils.immediate(); 39 | } 40 | nextIndexToRelease--; 41 | return index % 2 === 0; 42 | }); 43 | t.deepEqual(output, ['a', 'c', 'e', 'g']); 44 | }); 45 | 46 | test('filters objects with numeric keys', async t => { 47 | const input = { 1: 'asdf', 2: 'abcd' }; 48 | const output = await promiseUtils.filter(input, async (value, i) => { 49 | return (i as any) === 1 || (i as any) === 2; 50 | }); 51 | t.deepEqual(output, []); 52 | }); 53 | 54 | test('filters objects', async t => { 55 | const input = { a: 1, b: 2, c: 3 }; 56 | const output = await promiseUtils.filter(input, async (value: any) => { 57 | return value > 1; 58 | }); 59 | t.deepEqual(output, [2, 3]); 60 | }); 61 | 62 | test('filters objects without keys', async t => { 63 | const input = { a: 1, b: 2, c: 3 }; 64 | const output = await promiseUtils.filter(input, async (value: any, key: any) => { 65 | return key !== 'b'; 66 | }); 67 | t.deepEqual(output, [1, 3]); 68 | }); 69 | -------------------------------------------------------------------------------- /test/flatMap.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as _ from 'lodash'; 3 | 4 | import * as promiseUtils from '../src/index'; 5 | 6 | test('returns all values', async t => { 7 | const output = await promiseUtils.flatMap(_.range(10), async n => n * n); 8 | t.deepEqual(output, _.flatMap(_.range(10), n => n * n)); 9 | }); 10 | 11 | test('flattens', async t => { 12 | const output = await promiseUtils.flatMap(_.range(10), async n => [n, n]); 13 | t.deepEqual(output, _.flatMap(_.range(10), n => [n, n])); 14 | }); 15 | 16 | test('ignores empty arrays', async t => { 17 | const output = await promiseUtils.flatMap(_.range(10), async n => (n % 2 === 0 ? [] : [n, n])); 18 | t.deepEqual(output, _.flatMap(_.range(10), n => (n % 2 === 0 ? [] : [n, n]))); 19 | }); 20 | 21 | test('works for objects', async t => { 22 | const input = { a: 1, b: 2, c: 3, d: 4 }; 23 | const output = await promiseUtils.map(input, async (a, b) => [b, a]); 24 | t.deepEqual(output, _.toPairs(input)); 25 | }); 26 | 27 | test('works for objects without keys', async t => { 28 | const input = { a: 1, b: 2, c: 3, d: 4 }; 29 | const output = await promiseUtils.flatMap(input, async a => a); 30 | t.deepEqual(output, _.flatMap(input)); 31 | }); 32 | 33 | test('handles null input group', async t => { 34 | const output = await promiseUtils.flatMap(null as any, _.identity); 35 | t.deepEqual(output, []); 36 | }); 37 | 38 | test('handles empty input group', async t => { 39 | const output = await promiseUtils.flatMap([], _.identity); 40 | t.deepEqual(output, []); 41 | }); 42 | 43 | test('handles large sub-lists', async t => { 44 | const output = await promiseUtils.flatMap(_.range(10), async () => _.range(1_000_000)); 45 | t.is(output.length, 10_000_000); 46 | }); 47 | -------------------------------------------------------------------------------- /test/invert.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as promiseUtils from '../src/index'; 4 | 5 | test('throws error when promise resolves', async t => { 6 | try { 7 | await promiseUtils.invert(Promise.resolve('test'), 'Should not resolve'); 8 | t.fail('The promise did not reject'); 9 | } catch (err) { 10 | t.is(err.message, 'Should not resolve'); 11 | } 12 | }); 13 | 14 | test('uses default message', async t => { 15 | try { 16 | await promiseUtils.invert(Promise.resolve('test')); 17 | t.fail('The promise did not reject'); 18 | } catch (err) { 19 | t.is(err.message, 'Expected promise to reject'); 20 | } 21 | }); 22 | 23 | test('returns reject value when promise rejects', async t => { 24 | const ret: any = await promiseUtils.invert(Promise.reject('test'), 'Should not resolve'); 25 | t.is(ret, 'test'); 26 | }); 27 | -------------------------------------------------------------------------------- /test/map.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import test from 'ava'; 4 | 5 | import * as promiseUtils from '../src/index'; 6 | 7 | test('returns all values', async t => { 8 | const output = await promiseUtils.map(_.range(10), async n => n * n); 9 | t.deepEqual(output, _.map(_.range(10), n => n * n)); 10 | }); 11 | 12 | test('runs with indices', async t => { 13 | const output = await promiseUtils.map(_.reverse(_.range(10)), async (n, i) => i * i); 14 | t.deepEqual(output, _.map(_.range(10), n => n * n)); 15 | }); 16 | 17 | test('infers types on tuples', async t => { 18 | const val = [1, 2, 3, 4] as const; 19 | const output = await promiseUtils.map(val, async (n, i) => i * i); 20 | t.deepEqual(output, _.map(_.range(4), n => n * n)); 21 | }); 22 | 23 | test('works for objects', async t => { 24 | const input = { a: 1, b: 2, c: 3, d: 4 }; 25 | const output = await promiseUtils.map(input, async (a, b) => [b, a]); 26 | t.deepEqual(output, _.toPairs(input)); 27 | }); 28 | 29 | test('works for objects without keys', async t => { 30 | const input = { a: 1, b: 2, c: 3, d: 4 }; 31 | const output = await promiseUtils.map(input, async a => a); 32 | t.deepEqual(output, _.map(input)); 33 | }); 34 | 35 | test('handles null input group', async t => { 36 | const output = await promiseUtils.map(null as any, _.identity); 37 | t.deepEqual(output, []); 38 | }); 39 | 40 | test('handles empty input group', async t => { 41 | const output = await promiseUtils.map([], _.identity); 42 | t.deepEqual(output, []); 43 | }); 44 | -------------------------------------------------------------------------------- /test/mapLimit.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import test from 'ava'; 4 | 5 | import * as promiseUtils from '../src/index'; 6 | 7 | test('returns all values', async t => { 8 | const output = await promiseUtils.mapLimit(_.range(10), 5, async n => n * n); 9 | t.deepEqual(_.sortBy(output), _.sortBy(_.map(_.range(10), n => n * n))); 10 | }); 11 | 12 | test('handles null input group', async t => { 13 | const output = await promiseUtils.mapLimit(null as any, 5, _.identity); 14 | t.deepEqual(output, []); 15 | }); 16 | 17 | test('handles empty input group', async t => { 18 | const output = await promiseUtils.mapLimit([], 5, _.identity); 19 | t.deepEqual(output, []); 20 | }); 21 | 22 | test('works for objects', async t => { 23 | const input = { a: 1, b: 2, c: 3, d: 4 }; 24 | const output = await promiseUtils.mapLimit(input, 1, async (a, b) => [b, a]); 25 | t.deepEqual(_.sortBy(output), _.sortBy(_.toPairs(input))); 26 | }); 27 | 28 | test('works for objects without keys', async t => { 29 | const input = { a: 1, b: 2, c: 3, d: 4 }; 30 | const output = await promiseUtils.mapLimit(input, 1, async a => a); 31 | t.deepEqual(_.sortBy(output), _.sortBy(_.map(input))); 32 | }); 33 | -------------------------------------------------------------------------------- /test/mapSeries.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import test from 'ava'; 4 | 5 | import * as promiseUtils from '../src/index'; 6 | 7 | test('returns all values', async t => { 8 | const output = await promiseUtils.mapSeries(_.range(10), async n => n * n); 9 | t.deepEqual(output, _.map(_.range(10), n => n * n)); 10 | }); 11 | 12 | test('handles null input group', async t => { 13 | const output = await promiseUtils.mapSeries(null as any, _.identity); 14 | t.deepEqual(output, []); 15 | }); 16 | 17 | test('handles empty input group', async t => { 18 | const output = await promiseUtils.mapSeries([], _.identity); 19 | t.deepEqual(output, []); 20 | }); 21 | 22 | test('works for objects', async t => { 23 | const input = { a: 1, b: 2, c: 3, d: 4 }; 24 | const output = await promiseUtils.mapSeries(input, async (a, b) => [b, a]); 25 | t.deepEqual(output, _.toPairs(input)); 26 | }); 27 | 28 | test('works for objects without keys', async t => { 29 | const input = { a: 1, b: 2, c: 3, d: 4 }; 30 | const output = await promiseUtils.mapSeries(input, async a => a); 31 | t.deepEqual(output, _.map(input)); 32 | }); 33 | -------------------------------------------------------------------------------- /test/memoize.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as sinon from 'sinon'; 3 | 4 | import test from 'ava'; 5 | 6 | import * as promiseUtils from '../src/index'; 7 | 8 | const sandbox = sinon.createSandbox(); 9 | 10 | test('stores values', async t => { 11 | const fnToMemoize = sandbox 12 | .stub() 13 | .onFirstCall() 14 | .returns('correct'); 15 | const memoized = promiseUtils.memoize(fnToMemoize); 16 | 17 | const ret = await promiseUtils.map(_.range(100), val => memoized('identical')); 18 | _.each(ret, val => t.is(val, 'correct')); 19 | }); 20 | 21 | test('honors hasher', async t => { 22 | const fnToMemoize = sandbox 23 | .stub() 24 | .onFirstCall() 25 | .returns('first'); 26 | const memoized = promiseUtils.memoize(fnToMemoize); 27 | 28 | const ret = await promiseUtils.map(_.range(100), val => memoized(val)); 29 | const cacheCount = _.filter(ret, val => val === 'first').length; 30 | t.is(cacheCount, 1); 31 | }); 32 | 33 | test('typescript def works for multiple args', async t => { 34 | let count = 0; 35 | async function stuffToMemoize(a: string, b: number): Promise { 36 | if (count === 0) { 37 | ++count; 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | const memoized = promiseUtils.memoize(stuffToMemoize, (a: string) => a); 44 | 45 | const ret = await promiseUtils.map(_.range(100), async val => memoized(`${val}`, 1)); 46 | const cacheCount = _.filter(ret).length; 47 | t.is(cacheCount, 1); 48 | }); 49 | 50 | test('honors user provided hasher', async t => { 51 | const fnToMemoize = sandbox 52 | .stub() 53 | .onFirstCall() 54 | .returns('first'); 55 | const memoized = promiseUtils.memoize(fnToMemoize, () => 1); 56 | 57 | const ret = await promiseUtils.map(_.range(100), memoized); 58 | const cacheCount = _.filter(ret, val => val === 'first').length; 59 | t.is(cacheCount, 100); 60 | }); 61 | 62 | test('uses memos on subsequent calls (tested by coverage)', async t => { 63 | const fnToMemoize = sandbox 64 | .stub() 65 | .onFirstCall() 66 | .returns('first') 67 | .onSecondCall() 68 | .throws(new Error('this should not happen')); 69 | const memoized = promiseUtils.memoize(fnToMemoize, () => 1); 70 | 71 | const ret = await promiseUtils.map(_.range(100), val => memoized(val)); 72 | const cacheCount = _.filter(ret, val => val === 'first').length; 73 | t.is(cacheCount, 100); 74 | const ret2 = await promiseUtils.map(_.range(100), memoized); 75 | const cacheCount2 = _.filter(ret2, val => val === 'first').length; 76 | t.is(cacheCount2, 100); 77 | }); 78 | 79 | test('reset resets a memo', async t => { 80 | const fnToMemoize = sandbox 81 | .stub() 82 | .onFirstCall() 83 | .returns('first') 84 | .onSecondCall() 85 | .returns('second') 86 | .onThirdCall() 87 | .returns('third'); 88 | const memoized = promiseUtils.memoize(fnToMemoize); 89 | 90 | const ret = await promiseUtils.map(_.range(100), val => memoized(1)); 91 | const ret2 = await promiseUtils.map(_.range(100), val => memoized(2)); 92 | const cacheCount = _.filter(ret, val => val === 'first').length; 93 | const cacheCount2 = _.filter(ret2, val => val === 'second').length; 94 | t.is(cacheCount, 100); 95 | t.is(cacheCount2, 100); 96 | memoized.reset(1); 97 | const ret3 = await promiseUtils.map(_.range(100), val => memoized(2)); 98 | const ret4 = await promiseUtils.map(_.range(100), val => memoized(1)); 99 | const cacheCount3 = _.filter(ret3, val => val === 'second').length; 100 | const cacheCount4 = _.filter(ret4, val => val === 'third').length; 101 | t.is(cacheCount3, 100); 102 | t.is(cacheCount4, 100); 103 | }); 104 | 105 | test('reset ignores things not memoized', async t => { 106 | const fnToMemoize = sandbox.stub(); 107 | const memoized = promiseUtils.memoize(fnToMemoize); 108 | 109 | // should not throw 110 | memoized.reset(1); 111 | t.pass(); 112 | }); 113 | 114 | test('clear resets all memos', async t => { 115 | const fnToMemoize = sandbox 116 | .stub() 117 | .onFirstCall() 118 | .returns('first') 119 | .onSecondCall() 120 | .returns('second') 121 | .onThirdCall() 122 | .returns('third') 123 | .onCall(3) 124 | .returns('fourth'); 125 | const memoized = promiseUtils.memoize(fnToMemoize); 126 | 127 | const ret = await promiseUtils.map(_.range(100), val => memoized(1)); 128 | const ret2 = await promiseUtils.map(_.range(100), val => memoized(2)); 129 | const cacheCount = _.filter(ret, val => val === 'first').length; 130 | const cacheCount2 = _.filter(ret2, val => val === 'second').length; 131 | t.is(cacheCount, 100); 132 | t.is(cacheCount2, 100); 133 | memoized.clear(); 134 | const ret3 = await promiseUtils.map(_.range(100), val => memoized(2)); 135 | const ret4 = await promiseUtils.map(_.range(100), val => memoized(1)); 136 | const cacheCount3 = _.filter(ret3, val => val === 'third').length; 137 | const cacheCount4 = _.filter(ret4, val => val === 'fourth').length; 138 | t.is(cacheCount3, 100); 139 | t.is(cacheCount4, 100); 140 | }); 141 | 142 | test('uses memos on subsequent calls with timeout', async t => { 143 | const fnToMemoize = sandbox 144 | .stub() 145 | .onFirstCall() 146 | .returns('first') 147 | .onSecondCall() 148 | .throws(new Error('this should not happen')); 149 | const memoized = promiseUtils.memoize(fnToMemoize, () => 1, 1000); 150 | 151 | const ret = await promiseUtils.mapLimit(_.range(100), 100, val => memoized(val)); 152 | const cacheCount = _.filter(ret, val => val === 'first').length; 153 | t.is(cacheCount, 100); 154 | const ret2 = await promiseUtils.mapLimit(_.range(100), 100, memoized); 155 | const cacheCount2 = _.filter(ret2, val => val === 'first').length; 156 | t.is(cacheCount2, 100); 157 | }); 158 | 159 | test('invalidates cache after timeout', async t => { 160 | const fnToMemoize = sandbox 161 | .stub() 162 | .onFirstCall() 163 | .returns('first') 164 | .onSecondCall() 165 | .throws(new Error('should throw here')); 166 | const memoized = promiseUtils.memoize(fnToMemoize, () => 1, 100); 167 | const clock = sinon.useFakeTimers(); 168 | 169 | const ret = await promiseUtils.map(_.range(100), memoized); 170 | const cacheCount = _.filter(ret, val => val === 'first').length; 171 | t.is(cacheCount, 100); 172 | 173 | clock.tick(200); 174 | const err = await promiseUtils.invert(memoized(1), 'Not correctly invalidating expect'); 175 | t.is(err.message, 'should throw here'); 176 | 177 | clock.restore(); 178 | }); 179 | -------------------------------------------------------------------------------- /test/retry.test.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | 3 | import test from 'ava'; 4 | 5 | import * as delay from '../src/delay'; 6 | import * as promiseUtils from '../src/index'; 7 | 8 | const sandbox = sinon.createSandbox(); 9 | 10 | test.afterEach(() => sandbox.restore()); 11 | 12 | test('fails eventually', async t => { 13 | const maxAttempts = 3; 14 | const delayStub = sandbox.stub(delay, 'delay'); 15 | 16 | await t.throwsAsync( 17 | promiseUtils.retry( 18 | () => { 19 | throw new Error('testing failures'); 20 | }, 21 | { maxAttempts: 3, delayMs: 100 }, 22 | ), 23 | /testing failure/, 24 | ); 25 | t.is(delayStub.callCount, maxAttempts - 1); 26 | }); 27 | 28 | test('honors immediate failure scenarios', async t => { 29 | let count = 0; 30 | const testFn = async () => { 31 | if (count++ === 0) { 32 | throw new Error('Not a retryable error'); 33 | } else { 34 | return true; 35 | } 36 | }; 37 | await t.throwsAsync( 38 | promiseUtils.retry(testFn, { 39 | maxAttempts: 3, 40 | isRetryable: err => err.message !== 'Not a retryable error', 41 | }), 42 | /Not a retryable error/, 43 | ); 44 | }); 45 | 46 | test.serial('delays appropriately', async t => { 47 | let count = 0; 48 | const testFn = async () => { 49 | if (count++ === 0) { 50 | throw new Error('first fail'); 51 | } else { 52 | return true; 53 | } 54 | }; 55 | const delayStub = sandbox.stub(delay, 'delay'); 56 | 57 | t.true( 58 | await promiseUtils.retry(testFn, { 59 | maxAttempts: 3, 60 | delayMs: 100, 61 | })(), 62 | ); 63 | t.is(delayStub.callCount, 1); 64 | t.is(delayStub.args[0][0], 100); 65 | }); 66 | 67 | test('succeeds on retry', async t => { 68 | let count = 0; 69 | const testFn = async () => { 70 | if (count++ === 0) { 71 | throw new Error('retryable error'); 72 | } else { 73 | return true; 74 | } 75 | }; 76 | t.true(await promiseUtils.retry(testFn, { maxAttempts: 3 })()); 77 | }); 78 | 79 | test('currys multiple args properly', async t => { 80 | const expectedFirstArg: string = 'first'; 81 | const expectedSecondArg: string = 'second'; 82 | 83 | const testFn = async (firstArg: string, secondArg: string) => { 84 | t.is(firstArg, expectedFirstArg); 85 | t.is(secondArg, expectedSecondArg); 86 | }; 87 | 88 | await promiseUtils.retry(testFn, { maxAttempts: 1 })(expectedFirstArg, expectedSecondArg); 89 | }); 90 | -------------------------------------------------------------------------------- /test/settleAll.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as promiseUtils from '../src/index'; 4 | 5 | test('returns settled promise values in order when no errFn provided', async t => { 6 | const testPromises: Promise[] = [ 7 | promiseUtils.invert(promiseUtils.delay(500, null), 'delayed error'), 8 | promiseUtils.delay(500, { success: true }), 9 | Promise.resolve(3), 10 | Promise.reject(new Error('failed')), 11 | Promise.resolve('success'), 12 | Promise.reject('also failed'), 13 | ]; 14 | const res = await promiseUtils.settleAll(testPromises); 15 | t.deepEqual(res, { 16 | errors: [new Error('delayed error'), new Error('failed'), 'also failed'], 17 | results: [{ success: true }, 3, 'success'], 18 | }); 19 | }); 20 | 21 | test('settles null value', async t => { 22 | const res = await promiseUtils.settleAll(null as any); 23 | t.deepEqual(res, { 24 | errors: [], 25 | results: [], 26 | }); 27 | }); 28 | 29 | test('runs and returns result of errFn on failed promises', async t => { 30 | const testPromises: Promise[] = [ 31 | Promise.resolve('a'), 32 | Promise.reject(new Error('errorA')), 33 | Promise.reject(new Error('errorB')), 34 | Promise.resolve('b'), 35 | ]; 36 | const errFn = (err: Error) => err.message; 37 | const res = await promiseUtils.settleAll(testPromises, errFn); 38 | t.deepEqual(res, { 39 | errors: ['errorA', 'errorB'], 40 | results: ['a', 'b'], 41 | }); 42 | }); 43 | 44 | test('allows errFn to take an index', async t => { 45 | const testPromises: Promise[] = [ 46 | Promise.reject(new Error('errorA')), 47 | Promise.reject(new Error('errorB')), 48 | Promise.reject(new Error('errorC')), 49 | ]; 50 | const errFn = (err: Error, ind: number) => `${ind}-${err.message}`; 51 | const res = await promiseUtils.settleAll(testPromises, errFn); 52 | t.deepEqual(res, { 53 | errors: ['0-errorA', '1-errorB', '2-errorC'], 54 | results: [], 55 | }); 56 | }); 57 | 58 | test('allows errFn to take async functions', async t => { 59 | const testPromises: Promise[] = [ 60 | Promise.reject(new Error('errorA')), 61 | Promise.reject(new Error('errorB')), 62 | Promise.reject(new Error('errorC')), 63 | ]; 64 | const errFn = async (err: Error) => `async-${err.message}`; 65 | const res = await promiseUtils.settleAll(testPromises, errFn); 66 | t.deepEqual(res, { 67 | errors: ['async-errorA', 'async-errorB', 'async-errorC'], 68 | results: [], 69 | }); 70 | }); 71 | 72 | test('handles empty promise array', async t => { 73 | const res = await promiseUtils.settleAll([]); 74 | t.deepEqual(res, { 75 | errors: [], 76 | results: [], 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/timeout.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import * as promiseUtils from '../src/index'; 4 | 5 | test('resolves appropriate value when function finishes first', async t => { 6 | const ret = await promiseUtils.timeout( 7 | () => true, 8 | 1000, 9 | 'Test function did not complete within 1 second', 10 | )(); 11 | t.is(ret, true); 12 | }); 13 | 14 | test('handles argument passing appropriately', async t => { 15 | const ret = await promiseUtils.timeout( 16 | async (a: string, b: string, c: string) => promiseUtils.immediate(b), 17 | 1000, 18 | 'Test function did not complete within 1 second', 19 | )('who cares', 'really important', 'garbage'); 20 | t.is(ret, 'really important'); 21 | }); 22 | 23 | test('throws errors when delays are too long', async t => { 24 | const ret = await promiseUtils.invert( 25 | promiseUtils.timeout( 26 | async () => promiseUtils.delay(50, undefined), 27 | 0, 28 | 'Test function completed too fast!', 29 | )(), 30 | ); 31 | t.is(ret.message, 'Test function completed too fast!'); 32 | }); 33 | 34 | test('uses default error message', async t => { 35 | const ret = await promiseUtils.invert( 36 | promiseUtils.timeout(async () => promiseUtils.delay(50, undefined), 0)(), 37 | ); 38 | t.is(ret.message, 'Could not resolve within 0 ms'); 39 | }); 40 | 41 | test('default error message uses function name', async t => { 42 | const ret = await promiseUtils.invert( 43 | promiseUtils.timeout(async function delay() { 44 | await promiseUtils.delay(50, undefined); 45 | }, 0)(), 46 | ); 47 | t.is(ret.message, 'Could not resolve delay within 0 ms'); 48 | }); 49 | -------------------------------------------------------------------------------- /test/until.test.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | 3 | import test from 'ava'; 4 | 5 | import * as delay from '../src/delay'; 6 | import * as promiseUtils from '../src/index'; 7 | 8 | const sandbox = sinon.createSandbox(); 9 | 10 | test('fails eventually', async t => { 11 | await t.throwsAsync( 12 | promiseUtils.until(async () => 0, { maxAttempts: 3 }), 13 | /Could not complete function within/, 14 | ); 15 | }); 16 | 17 | test.serial('delays appropriately', async t => { 18 | let count = 0; 19 | const testFn = async () => { 20 | if (count++ === 0) { 21 | return false; 22 | } else { 23 | return true; 24 | } 25 | }; 26 | const delayStub = sandbox.stub(delay, 'delay'); 27 | 28 | t.true( 29 | await promiseUtils.until(testFn, { 30 | maxAttempts: 3, 31 | delayMs: 100, 32 | })(), 33 | ); 34 | t.is(delayStub.callCount, 1); 35 | t.is(delayStub.args[0][0], 100); 36 | }); 37 | 38 | test('succeeds on retry', async t => { 39 | let count = 0; 40 | const testFn = async () => { 41 | if (count++ === 0) { 42 | return false; 43 | } else { 44 | return true; 45 | } 46 | }; 47 | t.true(await promiseUtils.until(testFn, { maxAttempts: 3 })()); 48 | }); 49 | 50 | test('currys multiple args properly', async t => { 51 | const expectedFirstArg: string = 'first'; 52 | const expectedSecondArg: string = 'second'; 53 | 54 | const testFn = async (firstArg: string, secondArg: string) => { 55 | t.is(firstArg, expectedFirstArg); 56 | t.is(secondArg, expectedSecondArg); 57 | return true; 58 | }; 59 | 60 | await promiseUtils.until(testFn, { maxAttempts: 1 })(expectedFirstArg, expectedSecondArg); 61 | }); 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "removeComments": false, 5 | "preserveConstEnums": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "noImplicitReturns": true, 8 | "noEmitOnError": true, 9 | "pretty": true, 10 | "noUnusedLocals": true, 11 | "strict": true, 12 | "target": "es2017", 13 | "rootDir": ".", 14 | "outDir": "dist", 15 | "sourceMap": true, 16 | "declaration": true 17 | }, 18 | "include": [ 19 | "src/**/*", 20 | "test/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'adjacent-overload-signatures': true, 4 | 'member-access': true, 5 | 'member-ordering': [true, { order: 'fields-first' }], 6 | 'no-any': true, 7 | 'no-empty-interface': true, 8 | 'no-internal-module': true, 9 | 'no-magic-numbers': [true, 0, 1, 200, 204, 404, 500, 503, 504], 10 | 'prefer-for-of': true, 11 | 'no-for-in-array': true, 12 | 'promise-function-async': true, 13 | 'restrict-plus-operands': true, 14 | typedef: [ 15 | true, 16 | 'call-signature', 17 | 'parameter', 18 | 'property-declaration', 19 | 'member-variable-declaration' 20 | ], 21 | 'typedef-whitespace': [ 22 | true, 23 | { 24 | 'call-signature': 'nospace', 25 | 'index-signature': 'nospace', 26 | parameter: 'nospace', 27 | 'property-declaration': 'nospace', 28 | 'variable-declaration': 'nospace' 29 | }, 30 | { 31 | 'call-signature': 'onespace', 32 | 'index-signature': 'onespace', 33 | parameter: 'onespace', 34 | 'property-declaration': 'onespace', 35 | 'variable-declaration': 'onespace' 36 | } 37 | ], 38 | curly: true, 39 | 'label-position': true, 40 | 'no-arg': true, 41 | 'no-bitwise': true, 42 | 'no-conditional-assignment': true, 43 | 'no-console': [true, 'log', 'error'], 44 | 'no-construct': true, 45 | 'no-default-export': true, 46 | 'no-debugger': true, 47 | 'no-duplicate-variable': true, 48 | 'no-empty': true, 49 | 'no-eval': true, 50 | 'no-invalid-this': true, 51 | 'no-shadowed-variable': true, 52 | 'no-string-throw': true, 53 | 'no-switch-case-fall-through': true, 54 | 'no-unsafe-finally': true, 55 | 'no-unused-expression': true, 56 | 'no-use-before-declare': true, 57 | 'no-var-keyword': true, 58 | 'no-var-requires': true, 59 | 'no-require-imports': true, 60 | 'no-reference': true, 61 | radix: true, 62 | 'switch-default': true, 63 | 'triple-equals': true, 64 | 'use-isnan': true, 65 | 'cyclomatic-complexity': true, 66 | 'eofline': true, 67 | indent: [true, 'spaces'], 68 | 'linebreak-style': [true, 'LF'], 69 | 'max-classes-per-file': [true, 1], 70 | 'max-file-line-count': [true, 300], 71 | 'max-line-length': [true, 100], 72 | 'no-mergeable-namespace': true, 73 | 'no-namespace': true, 74 | 'no-trailing-whitespace': true, 75 | 'object-literal-sort-keys': true, 76 | 'prefer-const': true, 77 | 'array-type': [true, 'array'], 78 | 'callable-types': true, 79 | 'class-name': true, 80 | 'comment-format': [true, 'check-space'], 81 | 'interface-over-type-literal': true, 82 | 'jsdoc-format': true, 83 | 'completed-docs': [true, 'classes', 'functions', 'methods'], 84 | 'new-parens': true, 85 | 'no-angle-bracket-type-assertion': true, 86 | 'no-consecutive-blank-lines': true, 87 | 'no-parameter-properties': true, 88 | 'object-literal-key-quotes': [true, 'as-needed'], 89 | 'one-line': [ 90 | true, 91 | 'check-catch', 92 | 'check-finally', 93 | 'check-else', 94 | 'check-whitespace', 95 | 'check-open-brace' 96 | ], 97 | 'one-variable-per-declaration': true, 98 | 'ordered-imports': [ 99 | true, 100 | { 101 | 'import-sources-order': 'lowercase-last', 102 | 'named-imports-order': 'lowercase-last' 103 | } 104 | ], 105 | quotemark: [true, 'single'], 106 | 'trailing-comma': [true, {multiline: 'always', singleline: 'never'}], 107 | semicolon: [true, 'always'], 108 | 'variable-name': true, 109 | whitespace: [ 110 | true, 111 | 'check-branch', 112 | 'check-decl', 113 | 'check-operator', 114 | 'check-module', 115 | 'check-separator', 116 | 'check-type', 117 | 'check-typecast' 118 | ] 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /tslint.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = { 4 | rules: _.omit(require('./tslint.js').rules, [ 5 | 'no-invalid-this', 6 | 'max-classes-per-file', 7 | 'no-magic-numbers', 8 | 'no-unused-new', 9 | 'max-file-line-count', 10 | 'object-literal-sort-keys', 11 | 'no-any', 12 | 'no-var-requires', 13 | 'no-require-imports', 14 | 'no-empty', 15 | 'typedef', 16 | 'completed-docs' 17 | ]) 18 | }; 19 | --------------------------------------------------------------------------------