├── CODEOWNERS ├── .npmrc ├── .husky └── pre-commit ├── .github ├── FUNDING.yml ├── auto_assign.yml ├── pull_request_template.md ├── PULL_REQUEST_TEMPLATE │ └── default.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── pr.yml │ ├── ci.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .editorconfig ├── src ├── tests │ └── index.tests.ts ├── index.ts └── utils │ └── create_tests.ts ├── tests └── index.tests.ts ├── jest.config.integration.mjs ├── .npmignore ├── jest.config.unit.mjs ├── jest.config.base.mjs ├── .codeclimate.yml ├── typedoc.config.mjs ├── tsconfig.json ├── prettier.config.mjs ├── LICENSE ├── eslint.config.mjs ├── rollup.config.mjs ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── CHANGELOG.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @devlato 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: devlato 4 | custom: http://paypal.me/devlatoau 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .idea/ 3 | coverage/ 4 | dist/ 5 | docs/ 6 | lint/ 7 | node_modules/ 8 | 9 | # Garbage 10 | *.DS_Store 11 | *.lock 12 | *.log 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | max_line_length=120 10 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: true 2 | addAssignees: true 3 | 4 | reviewers: 5 | - devlato 6 | 7 | numberOfReviewers: 0 8 | 9 | assignees: 10 | - devlato 11 | 12 | numberOfAssignees: 0 13 | 14 | skipKeywords: 15 | - WIP 16 | -------------------------------------------------------------------------------- /src/tests/index.tests.ts: -------------------------------------------------------------------------------- 1 | import { createTests } from 'utils/create_tests'; 2 | import { waitUntil, TimeoutError, DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS, WAIT_FOREVER } from '../'; 3 | 4 | createTests({ 5 | waitUntil, 6 | TimeoutError, 7 | WAIT_FOREVER, 8 | DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS, 9 | }); 10 | -------------------------------------------------------------------------------- /tests/index.tests.ts: -------------------------------------------------------------------------------- 1 | import { createTests } from 'utils/create_tests'; 2 | import { waitUntil, TimeoutError, DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS, WAIT_FOREVER } from '../dist'; 3 | 4 | createTests({ 5 | waitUntil, 6 | TimeoutError, 7 | WAIT_FOREVER, 8 | DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS, 9 | }); 10 | -------------------------------------------------------------------------------- /jest.config.integration.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import jestConfig from './jest.config.base.mjs'; 3 | import url from 'node:url'; 4 | 5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | export default { 8 | ...jestConfig, 9 | testPathIgnorePatterns: [...jestConfig.testPathIgnorePatterns, path.resolve(__dirname, 'src')], 10 | testRegex: '.*\\.tests\\.ts$', 11 | }; 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .git/ 3 | .github/ 4 | .idea/ 5 | coverage/ 6 | docs/ 7 | lint/ 8 | node_modules/ 9 | src/* 10 | dist/utils 11 | 12 | # Configuration files 13 | .editorconfig 14 | eslint.config.mjs 15 | .gitignore 16 | .npmignore 17 | CODEOWNERS 18 | jest.config.unit.mjs 19 | package-lock.json 20 | prettier.config.mjs 21 | rollup.config.mjs 22 | tsconfig.json 23 | typedoc.config.mjs 24 | 25 | # Garbage 26 | *.DS_Store 27 | *.lock 28 | *.log 29 | -------------------------------------------------------------------------------- /jest.config.unit.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import jestConfig from './jest.config.base.mjs'; 3 | import url from 'node:url'; 4 | 5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | export default { 8 | ...jestConfig, 9 | collectCoverage: true, 10 | collectCoverageFrom: ['src/**/*.ts', '!src/utils/create_tests.ts'], 11 | testPathIgnorePatterns: [...jestConfig.testPathIgnorePatterns, path.resolve(__dirname, 'tests')], 12 | testRegex: '.*\\.tests\\.ts$', 13 | }; 14 | -------------------------------------------------------------------------------- /jest.config.base.mjs: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | export default { 4 | preset: 'ts-jest', 5 | automock: false, 6 | clearMocks: true, 7 | resetMocks: true, 8 | resetModules: true, 9 | collectCoverage: false, 10 | displayName: 'async-wait-until', 11 | maxConcurrency: os.cpus().length, 12 | testEnvironmentOptions: { 13 | url: 'http://localhost', 14 | }, 15 | moduleDirectories: ['node_modules', 'src'], 16 | testPathIgnorePatterns: ['.cache', 'coverage', 'dist', 'docs', '.git', '.github', '.idea', 'node_modules'], 17 | testRegex: '.*\\.tests\\.ts$', 18 | }; 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Pull Request 3 | about: Create a report to help us improve 4 | title: '[PR]' 5 | labels: '' 6 | assignees: 'devlato' 7 | --- 8 | 9 | **Describe the problem this PR solves** 10 | A clear and concise description of what the pull request solves. 11 | 12 | **Features and behaviour** 13 | A list of all the new features this PR adds, along with changed behavior. 14 | 15 | **Describe how it solves the problem** 16 | A bit more detailed explanation of technical implementation. 17 | 18 | **Additional context** 19 | Add any other context about the PR here, link issues, etc. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Pull Request 3 | about: Create a report to help us improve 4 | title: '[PR]' 5 | labels: '' 6 | assignees: 'devlato' 7 | --- 8 | 9 | **Describe the problem this PR solves** 10 | A clear and concise description of what the pull request solves. 11 | 12 | **Features and behaviour** 13 | A list of all the new features this PR adds, along with changed behavior. 14 | 15 | **Describe how it solves the problem** 16 | A bit more detailed explanation of technical implementation. 17 | 18 | **Additional context** 19 | Add any other context about the PR here, link issues, etc. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug Report]' 5 | labels: Bug Report 6 | assignees: 'devlato' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. ... 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 4 6 | complex-logic: 7 | config: 8 | threshold: 8 9 | file-lines: 10 | config: 11 | threshold: 1000 12 | method-complexity: 13 | config: 14 | threshold: 8 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 120 21 | nested-control-flow: 22 | config: 23 | threshold: 5 24 | return-statements: 25 | config: 26 | threshold: 5 27 | similar-code: 28 | enabled: false 29 | identical-code: 30 | enabled: false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Feature Request]' 5 | labels: Feature Request 6 | assignees: 'devlato' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /typedoc.config.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module'; 2 | 3 | const require = createRequire(import.meta.url); 4 | const pckg = require('./package.json'); 5 | 6 | /** @type {Partial} */ 7 | export default { 8 | entryPoints: ['src/index.ts'], 9 | out: 'docs', 10 | excludeInternal: false, 11 | excludePrivate: false, 12 | excludeProtected: false, 13 | name: `Documentation for ${pckg.name} v${pckg.version}`, 14 | categorizeByGroup: false, 15 | defaultCategory: pckg.name, 16 | categoryOrder: [pckg.name, 'Defaults', 'Exceptions', 'Common Types', 'Utilities'], 17 | gitRevision: pckg.version, 18 | plugin: ['typedoc-plugin-dt-links', 'typedoc-plugin-mdn-links', 'typedoc-plugin-merge-modules'], 19 | validation: { 20 | invalidLink: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "alwaysStrict": true, 5 | "baseUrl": "./src", 6 | "declaration": true, 7 | "declarationDir": "dist", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "isolatedModules": true, 11 | "lib": ["es5", "es6", "es7", "esnext", "dom"], 12 | "module": "es2015", 13 | "moduleResolution": "bundler", 14 | "newLine": "LF", 15 | "noFallthroughCasesInSwitch": true, 16 | "outDir": "dist", 17 | "removeComments": true, 18 | "resolveJsonModule": true, 19 | "strict": true, 20 | "target": "es6", 21 | "pretty": true, 22 | "inlineSourceMap": true, 23 | "inlineSources": true 24 | }, 25 | "exclude": ["node_modules", "src/tests"], 26 | "include": ["src/**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | trailingComma: 'all', 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | bracketSpacing: true, 10 | arrowParens: 'always', 11 | parser: 'typescript', 12 | htmlWhitespaceSensitivity: 'strict', 13 | endOfLine: 'lf', 14 | printWidth: 120, 15 | overrides: [ 16 | { 17 | files: '*.json', 18 | options: { parser: 'json' }, 19 | }, 20 | { 21 | files: '*.js', 22 | options: { parser: 'babel' }, 23 | }, 24 | { 25 | files: '*.ts', 26 | options: { parser: 'typescript' }, 27 | }, 28 | { 29 | files: '*.yml', 30 | options: { parser: 'yaml' }, 31 | }, 32 | { 33 | files: '*.md', 34 | options: { parser: 'markdown' }, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Denis Tokarev (https://github.com/devlato) 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 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // https://www.npmjs.com/package/@eslint/eslintrc 2 | 3 | import * as eslintCompat from '@eslint/compat'; 4 | import * as eslintRc from '@eslint/eslintrc'; 5 | import js from '@eslint/js'; 6 | import path from 'node:path'; 7 | import url from 'node:url'; 8 | 9 | const baseDir = path.dirname(url.fileURLToPath(import.meta.url)); 10 | 11 | const compat = new eslintRc.FlatCompat({ 12 | allConfig: js.configs.all, 13 | // optional unless you're using "eslint:recommended" 14 | baseDirectory: baseDir, 15 | // optional 16 | recommendedConfig: js.configs.recommended, 17 | // optional; default: process.cwd() 18 | resolvePluginsRelativeTo: baseDir, // optional unless you're using "eslint:all" 19 | }); 20 | 21 | export default eslintCompat.fixupConfigRules([ 22 | ...compat.config({ 23 | parser: '@typescript-eslint/parser', 24 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:prettier/recommended'], 25 | plugins: ['@typescript-eslint', 'prettier'], 26 | parserOptions: { 27 | ecmaVersion: 'latest', 28 | sourceType: 'module', 29 | }, 30 | rules: { 31 | '@typescript-eslint/explicit-function-return-type': [0], 32 | '@typescript-eslint/no-use-before-define': [0], 33 | 'prettier/prettier': 'error', 34 | }, 35 | env: { 36 | browser: true, 37 | node: true, 38 | jest: true, 39 | es6: true, 40 | }, 41 | root: true, 42 | }), 43 | { 44 | ignores: [ 45 | '.git/', 46 | '.github/', 47 | '.idea/', 48 | 'coverage/', 49 | 'dist/', 50 | 'docs/', 51 | 'lint/', 52 | 'node_modules/', 53 | '*.DS_Store', 54 | '*.lock', 55 | '*.log', 56 | ], 57 | }, 58 | ]); 59 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import terser from '@rollup/plugin-terser'; 5 | import { createRequire } from 'node:module'; 6 | 7 | const require = createRequire(import.meta.url); 8 | const pckg = require('./package.json'); 9 | 10 | const IS_PRODUCTION = process.env.NODE_ENV === 'production'; 11 | const OUTPUT_CONFIG = { 12 | banner: [ 13 | '/**', 14 | ` * ${pckg.name} ${pckg.version}`, 15 | ` * ${pckg.description}`, 16 | ` * ${pckg.homepage}`, 17 | ` * (c) ${pckg.author}, under the ${pckg.license} license`, 18 | ' */', 19 | ].join('\n'), 20 | compact: true, 21 | sourcemap: true, 22 | }; 23 | 24 | export default { 25 | input: 'src/index.ts', 26 | output: [ 27 | { 28 | ...OUTPUT_CONFIG, 29 | exports: 'named', 30 | file: 'dist/index.js', 31 | format: 'umd', 32 | name: 'async-wait-until', 33 | }, 34 | { 35 | ...OUTPUT_CONFIG, 36 | exports: 'named', 37 | file: 'dist/amd.js', 38 | format: 'amd', 39 | }, 40 | { 41 | ...OUTPUT_CONFIG, 42 | exports: 'named', 43 | file: 'dist/commonjs.js', 44 | format: 'cjs', 45 | }, 46 | { 47 | ...OUTPUT_CONFIG, 48 | exports: 'named', 49 | file: 'dist/index.esm.js', 50 | format: 'es', 51 | }, 52 | { 53 | ...OUTPUT_CONFIG, 54 | exports: 'named', 55 | file: 'dist/iife.js', 56 | format: 'iife', 57 | name: 'asyncWaitUntil', 58 | }, 59 | { 60 | ...OUTPUT_CONFIG, 61 | exports: 'named', 62 | file: 'dist/systemjs.js', 63 | format: 'system', 64 | }, 65 | ], 66 | plugins: [ 67 | nodeResolve(), 68 | typescript({ 69 | tsconfig: './tsconfig.json', 70 | }), 71 | commonjs({ extensions: ['.js', '.ts'] }), 72 | ...(IS_PRODUCTION ? [terser()] : []), 73 | ], 74 | }; 75 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | - gh-pages 8 | tags-ignore: 9 | - '*' 10 | 11 | jobs: 12 | create_pull_request: 13 | name: Create or update a pull request 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | ref: '${{ github.event.pull_request.head.sha }}' 20 | persist-credentials: false 21 | fetch-depth: 0 22 | set-safe-directory: 'true' 23 | - name: Create a pull request 24 | uses: devops-infra/action-pull-request@v0.5.5 25 | with: 26 | github_token: '${{ secrets.GITHUB_TOKEN }}' 27 | target_branch: '${{ secrets.NEW_PR_DEFAULT_DESTINATION_BRANCH }}' 28 | title: '${{ env.PR_TITLE }}' 29 | body: '${{ env.PR_DESCRIPTION }}' 30 | template: '.github/PULL_REQUEST_TEMPLATE/default.md' 31 | reviewer: '${{ secrets.NEW_PR_DEFAULT_REVIEWERS }}' 32 | assignee: '${{ secrets.NEW_PR_DEFAULT_ASSIGNEES }}' 33 | label: '${{ secrets.NEW_PR_DEFAULT_LABELS }}' 34 | milestone: '${{ secrets.NEW_PR_DEFAULT_MILESTONE }}' 35 | ignore_users: 'dependabot' 36 | - name: Generate the changelog for this pull request 37 | id: generate-changelog 38 | run: | 39 | CURRENT_BRANCH="${GITHUB_SHA}" 40 | pr_title="$( git log --reverse --format='%s' "origin/${MAIN_BRANCH}".."${CURRENT_BRANCH}" | head -n 1 )" 41 | pr_changelog="$( git log --reverse --format='- (%h) %s (%an)' "origin/${MAIN_BRANCH}".."${CURRENT_BRANCH}" )" 42 | pr_description=$( echo "## ${pr_title}"; echo; echo; echo "### Changelog"; echo; echo "${pr_changelog}" ) 43 | 44 | echo "PR_TITLE<> ${GITHUB_ENV} 45 | echo "${pr_title}" >> ${GITHUB_ENV} 46 | echo 'EOF' >> ${GITHUB_ENV} 47 | 48 | echo "PR_DESCRIPTION<> ${GITHUB_ENV} 49 | echo "${pr_description}" >> ${GITHUB_ENV} 50 | echo 'EOF' >> ${GITHUB_ENV} 51 | env: 52 | MAIN_BRANCH: '${{ secrets.NEW_PR_DEFAULT_DESTINATION_BRANCH }}' 53 | - name: Update pull request 54 | uses: kt3k/update-pr-description@v1.0.4 55 | if: always() 56 | with: 57 | pr_title: '${{ env.PR_TITLE }}' 58 | pr_body: '${{ env.PR_DESCRIPTION }}' 59 | github_token: '${{ secrets.GITHUB_TOKEN }}' 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | - gh-pages 8 | tags-ignore: 9 | - '*' 10 | pull_request: {} 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: '${{ github.event.pull_request.head.sha }}' 21 | set-safe-directory: 'true' 22 | - name: Install dependencies 23 | run: npm ci --ignore-scripts 24 | - name: Lint the codebase 25 | run: npm run lint:ci 26 | test: 27 | name: Test 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | ref: '${{ github.event.pull_request.head.sha }}' 34 | set-safe-directory: 'true' 35 | - name: Install dependencies 36 | run: npm ci --ignore-scripts 37 | - name: Run tests 38 | run: npm run test:ci 39 | - name: Upload coverage 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: test_coverage 43 | path: coverage 44 | build_code: 45 | name: Build code 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | with: 51 | ref: '${{ github.event.pull_request.head.sha }}' 52 | set-safe-directory: 'true' 53 | - name: Install dependencies 54 | run: npm ci --ignore-scripts 55 | - name: Build code 56 | run: npm run build:ci 57 | - name: Upload the dist folder 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: dist 61 | path: dist 62 | integration_tests: 63 | name: Run integration tests 64 | runs-on: ubuntu-latest 65 | needs: build_code 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@v4 69 | with: 70 | ref: '${{ github.event.pull_request.head.sha }}' 71 | set-safe-directory: 'true' 72 | - name: Install dependencies 73 | run: npm ci --ignore-scripts 74 | - name: Download dist/ folder 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: dist 78 | path: dist 79 | - name: Run integration tests 80 | run: npm run test:integration:ci 81 | build_storybook: 82 | name: Build docs 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v4 87 | with: 88 | ref: '${{ github.event.pull_request.head.sha }}' 89 | set-safe-directory: 'true' 90 | - name: Install dependencies 91 | run: npm ci --ignore-scripts 92 | - name: Build docs 93 | run: npm run docs:ci 94 | - name: Upload the storybook 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: docs 98 | path: docs 99 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to async-wait-until 2 | 3 | First off, thank you for considering contributing to async-wait-until! It's people like you that make this project better for everyone. 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Getting Started](#getting-started) 9 | - [Pull Requests](#pull-requests) 10 | - [Development Workflow](#development-workflow) 11 | - [Coding Style](#coding-style) 12 | - [Testing](#testing) 13 | - [Reporting Bugs](#reporting-bugs) 14 | - [Suggesting Features](#suggesting-features) 15 | - [License](#license) 16 | 17 | ## Code of Conduct 18 | 19 | This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers. 20 | 21 | ## Getting Started 22 | 23 | ### Prerequisites 24 | 25 | - Node.js (version specified in package.json) 26 | - npm or yarn 27 | 28 | ### Setup 29 | 30 | 1. Fork the repository 31 | 2. Clone your fork: `git clone https://github.com/YOUR-USERNAME/async-wait-until.git` 32 | 3. Navigate to the project directory: `cd async-wait-until` 33 | 4. Install dependencies: `npm install` or `yarn install` 34 | 5. Create a new branch for your changes: `git checkout -b feature/your-feature-name` 35 | 36 | ## Pull Requests 37 | 38 | 1. Ensure your code passes all tests 39 | 2. Update documentation if necessary 40 | 3. Include relevant tests for your changes 41 | 4. Ensure your commits are well-formatted and descriptive 42 | 5. Submit a pull request to the `main` branch 43 | 44 | ### Pull Request Process 45 | 46 | 1. Update the README.md with details of changes if applicable 47 | 2. Increase the version numbers in package.json and other relevant files following [Semantic Versioning](https://semver.org/) 48 | 3. The PR will be merged once it has been reviewed and approved by a maintainer 49 | 50 | ## Development Workflow 51 | 52 | 1. Make your changes 53 | 2. Run tests: `npm test` or `yarn test` 54 | 3. Lint your code: `npm run lint` or `yarn lint` 55 | 4. Fix any issues before committing 56 | 57 | ## Coding Style 58 | 59 | - This project uses ESLint and Prettier for code formatting 60 | - Follow the existing code style 61 | - Use descriptive variable and function names 62 | - Add comments for complex logic 63 | 64 | ## Testing 65 | 66 | - Write tests for new features and bug fixes 67 | - Ensure all tests pass before submitting a PR 68 | - Aim for good test coverage 69 | 70 | ## Reporting Bugs 71 | 72 | When reporting bugs, please include: 73 | 74 | - A clear and descriptive title 75 | - Steps to reproduce the issue 76 | - Expected behavior 77 | - Actual behavior 78 | - Environment information (OS, Node.js version, etc.) 79 | - Any relevant logs or screenshots 80 | 81 | ## Suggesting Features 82 | 83 | When suggesting features: 84 | 85 | - Clearly describe the feature and its benefits 86 | - Provide examples of how it would be used 87 | - Indicate if you're willing to help implement it 88 | 89 | ## License 90 | 91 | By contributing, you agree that your contributions will be licensed under the same license as the project. 92 | 93 | Thank you for contributing to async-wait-until! 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-wait-until", 3 | "version": "2.0.31", 4 | "description": "Waits until the given predicate function returns a truthy value, then resolves", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.esm.js", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "lint-staged": "lint-staged", 10 | "prepare": "husky install", 11 | "lint": "cross-env NODE_ENV=production eslint -c eslint.config.mjs .", 12 | "lint:clean": "cross-env NODE_ENV=production rimraf lint", 13 | "test": "cross-env NODE_ENV=test jest --config=jest.config.unit.mjs --detectOpenHandles", 14 | "test:integration": "cross-env NODE_ENV=test jest --config=jest.config.integration.mjs --detectOpenHandles", 15 | "test:clean": "cross-env NODE_ENV=production rimraf coverage", 16 | "build": "cross-env NODE_ENV=production rollup -c rollup.config.mjs", 17 | "build:clean": "cross-env NODE_ENV=production rimraf dist", 18 | "docs": "cross-env NODE_ENV=production typedoc --options typedoc.config.mjs", 19 | "docs:view": "npm run docs && serve -p 3000 docs", 20 | "docs:clean": "cross-env NODE_ENV=production rimraf docs", 21 | "lint:ci": "npm run lint:clean && cross-env NODE_ENV=production eslint -c eslint.config.mjs -o lint/report.log .", 22 | "test:ci": "npm run test:clean && npm run test", 23 | "test:integration:ci": "npm run test:clean && npm run test:integration", 24 | "build:ci": "npm run build:clean && npm run build", 25 | "docs:ci": "npm run docs:clean && npm run docs", 26 | "format": "cross-env NODE_ENV=production eslint -c eslint.config.mjs --fix .", 27 | "clean": "npm run lint:clean & npm run test:clean & npm run build:clean & npm run docs:clean & wait", 28 | "release": "npm run lint:ci && npm run test:ci && npm run build:ci && npm run test:integration && npm run docs:ci", 29 | "prepublishOnly": "npm run clean && npm run release" 30 | }, 31 | "sideEffects": false, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/devlato/async-wait-until.git" 35 | }, 36 | "lint-staged": { 37 | "./**/*.{ts,js,json}": [ 38 | "cross-env NODE_ENV=production eslint -c eslint.config.mjs --fix" 39 | ] 40 | }, 41 | "keywords": [ 42 | "apply", 43 | "when", 44 | "async", 45 | "then", 46 | "promise", 47 | "wait", 48 | "timeout", 49 | "interval", 50 | "until", 51 | "for", 52 | "while", 53 | "predicate", 54 | "resolve", 55 | "reject" 56 | ], 57 | "author": "devlato (https://devlato.com/)", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/devlato/async-wait-until/issues" 61 | }, 62 | "homepage": "https://github.com/devlato/async-wait-until#readme", 63 | "files": [ 64 | "package.json", 65 | "README.md", 66 | "CHANGELOG.md", 67 | "docs", 68 | "LICENSE", 69 | "dist/*.js", 70 | "dist/*.d.ts", 71 | "dist/*.map" 72 | ], 73 | "funding": { 74 | "type": "individual", 75 | "url": "http://paypal.me/devlatoau" 76 | }, 77 | "devDependencies": { 78 | "@eslint/compat": "^1.2.8", 79 | "@rollup/plugin-commonjs": "^28.0.3", 80 | "@rollup/plugin-node-resolve": "^16.0.1", 81 | "@rollup/plugin-terser": "^0.4.4", 82 | "@rollup/plugin-typescript": "^12.1.2", 83 | "@types/jest": "^29.5.14", 84 | "@types/node": "^22.14.0", 85 | "@typescript-eslint/eslint-plugin": "^8.29.0", 86 | "@typescript-eslint/parser": "^8.29.0", 87 | "@typhonjs-typedoc/typedoc-pkg": "^0.3.1", 88 | "cross-env": "^7.0.3", 89 | "eslint": "^9.23.0", 90 | "eslint-config-prettier": "^10.1.1", 91 | "eslint-plugin-prettier": "^5.2.6", 92 | "husky": "^9.1.7", 93 | "jest": "^29.7.0", 94 | "lint-staged": "^15.5.0", 95 | "prettier": "^3.5.3", 96 | "rimraf": "^6.0.1", 97 | "rollup": "^4.39.0", 98 | "serve": "^14.2.4", 99 | "ts-jest": "^29.3.1", 100 | "tslib": "^2.8.1", 101 | "typedoc": "^0.28.1", 102 | "typedoc-plugin-dt-links": "^2.0.0", 103 | "typedoc-plugin-mdn-links": "^5.0.1", 104 | "typedoc-plugin-merge-modules": "^7.0.0", 105 | "typescript": "^5.8.2" 106 | }, 107 | "engines": { 108 | "node": ">= 0.14.0", 109 | "npm": ">= 1.0.0" 110 | }, 111 | "exports": { 112 | ".": { 113 | "import": "./dist/index.esm.js", 114 | "require": "./dist/index.js", 115 | "default": "./dist/index.js" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: {} 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | set-safe-directory: 'true' 18 | - name: Install dependencies 19 | run: npm ci --ignore-scripts 20 | - name: Lint the codebase 21 | run: npm run lint:ci 22 | test: 23 | name: Tests 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | set-safe-directory: 'true' 30 | - name: Install dependencies 31 | run: npm ci --ignore-scripts 32 | - name: Run tests 33 | run: npm run test:ci 34 | - name: Upload coverage 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: test_coverage 38 | path: coverage 39 | build_code: 40 | name: Build code 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | with: 46 | set-safe-directory: 'true' 47 | - name: Install dependencies 48 | run: npm ci --ignore-scripts 49 | - name: Run tests 50 | run: npm run build:ci 51 | - name: Upload dist 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: dist 55 | path: dist 56 | integration_tests: 57 | name: Run integration tests 58 | runs-on: ubuntu-latest 59 | needs: build_code 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | with: 64 | set-safe-directory: 'true' 65 | - name: Install dependencies 66 | run: npm ci --ignore-scripts 67 | - name: Download dist/ folder 68 | uses: actions/download-artifact@v4 69 | with: 70 | name: dist 71 | path: dist 72 | - name: Run integration tests 73 | run: npm run test:integration:ci 74 | build_docs: 75 | name: Build Docs 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Checkout 79 | uses: actions/checkout@v4 80 | - name: Install dependencies 81 | run: npm ci --ignore-scripts 82 | - name: Build storybook 83 | run: npm run docs:ci 84 | - name: Upload the storybook 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: docs 88 | path: docs 89 | deploy_docs: 90 | needs: build_docs 91 | name: Deploy docs 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Checkout 95 | uses: actions/checkout@v4 96 | with: 97 | set-safe-directory: 'true' 98 | - name: Install dependencies 99 | run: npm ci --ignore-scripts 100 | - name: Download storybook 101 | uses: actions/download-artifact@v4 102 | with: 103 | name: docs 104 | path: docs 105 | - name: Fetch all 106 | run: git fetch --all 107 | - name: Get the latest tag version 108 | id: get-last-tag 109 | run: | 110 | last_tag=$( git describe --tags --abbrev=0 ) 111 | echo "::set-output name=LAST_TAG::${last_tag}" 112 | - name: Deploy Docs to GitHub Pages 113 | uses: crazy-max/ghaction-github-pages@v4 114 | with: 115 | target_branch: '${{ secrets.DOCS_TARGET_BRANCH }}' 116 | build_dir: docs 117 | author: '${{ secrets.AUTOCOMMITTER_NAME }} <${{ secrets.AUTOCOMMITTER_EMAIL }}>' 118 | commit_message: 'Update docs for package v${{ steps.get-last-tag.LAST_TAG }}' 119 | env: 120 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 121 | GITHUB_PAT: '${{ secrets.GH_PAT }}' 122 | publish: 123 | needs: [lint, test, build_code, build_docs, integration_tests] 124 | name: Publish to npm 125 | runs-on: ubuntu-latest 126 | steps: 127 | - name: Checkout 128 | uses: actions/checkout@v4 129 | with: 130 | set-safe-directory: 'true' 131 | - name: Install dependencies 132 | run: npm ci --ignore-scripts 133 | - name: Publish 134 | run: | 135 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc 136 | npm publish 137 | env: 138 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 139 | NPM_TOKEN: '${{ secrets.NPM_AUTH_TOKEN }}' 140 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | * Trolling, insulting or derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others’ private information, such as a physical or email address, without their explicit permission 25 | * Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. 74 | 75 | Community Impact Guidelines were inspired by [Mozilla’s code of conduct enforcement ladder](https://github.com/mozilla/diversity). 76 | 77 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 78 | 79 | [homepage]: https://www.contributor-covenant.org 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module async-wait-until 4 | * 5 | * Provides utility functions for waiting asynchronously until a specified condition is met. 6 | */ 7 | 8 | /** 9 | * Error thrown when a timeout occurs while waiting for a condition. 10 | */ 11 | export class TimeoutError extends Error { 12 | /** 13 | * Creates a new `TimeoutError` instance. 14 | * @param timeoutInMs - The timeout duration in milliseconds, if provided. 15 | * 16 | * @example 17 | * ```typescript 18 | * throw new TimeoutError(5000); 19 | * // Throws: Timed out after waiting for 5000 ms 20 | * ``` 21 | */ 22 | constructor(timeoutInMs?: number) { 23 | super(timeoutInMs != null ? `Timed out after waiting for ${timeoutInMs} ms` : 'Timed out'); 24 | Object.setPrototypeOf(this, TimeoutError.prototype); 25 | } 26 | } 27 | 28 | /** 29 | * Default interval (in milliseconds) between attempts to evaluate the predicate. 30 | */ 31 | export const DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS = 50; 32 | 33 | /** 34 | * Default timeout duration (in milliseconds) for the `waitUntil` function. 35 | */ 36 | export const DEFAULT_TIMEOUT_IN_MS = 5000; 37 | 38 | /** 39 | * Special constant representing an infinite timeout. 40 | */ 41 | export const WAIT_FOREVER = Number.POSITIVE_INFINITY; 42 | 43 | /** 44 | * Represents values considered falsy in JavaScript. 45 | */ 46 | export type FalsyValue = null | undefined | false | '' | 0 | void; 47 | 48 | /** 49 | * Represents values considered truthy in JavaScript. 50 | */ 51 | export type TruthyValue = 52 | | Record 53 | | unknown[] 54 | | symbol 55 | // eslint-disable-next-line no-unused-vars 56 | | ((..._args: unknown[]) => unknown) 57 | | Exclude 58 | | Exclude 59 | | true; 60 | 61 | /** 62 | * Represents the possible return values of a predicate. 63 | */ 64 | export type PredicateReturnValue = TruthyValue | FalsyValue; 65 | 66 | /** 67 | * Represents a predicate function that evaluates a condition. 68 | * 69 | * @typeParam T - The type of value the predicate returns. 70 | */ 71 | export type Predicate = () => T | Promise; 72 | 73 | /** 74 | * Options for configuring the behavior of the `waitUntil` function. 75 | */ 76 | export type Options = { 77 | /** 78 | * The maximum duration (in milliseconds) to wait before timing out. 79 | */ 80 | timeout?: number; 81 | 82 | /** 83 | * The interval (in milliseconds) between attempts to evaluate the predicate. 84 | */ 85 | intervalBetweenAttempts?: number; 86 | }; 87 | 88 | /** 89 | * Waits until a predicate evaluates to a truthy value or the specified timeout is reached. 90 | * 91 | * @typeParam T - The type of value the predicate returns. 92 | * 93 | * @param predicate - The function to evaluate repeatedly until it returns a truthy value. 94 | * @param options - Either the timeout duration in milliseconds, or an options object for configuring the wait behavior. 95 | * @param intervalBetweenAttempts - The interval (in milliseconds) between predicate evaluations. Ignored if `options` is an object. 96 | * 97 | * @returns A promise that resolves to the truthy value returned by the predicate, or rejects with a `TimeoutError` if the timeout is reached. 98 | * 99 | * @example 100 | * Basic usage with a simple condition: 101 | * ```typescript 102 | * import waitUntil from 'async-wait-until'; 103 | * 104 | * const isConditionMet = () => Math.random() > 0.9; 105 | * 106 | * try { 107 | * const result = await waitUntil(isConditionMet, { timeout: 5000 }); 108 | * console.log('Condition met:', result); 109 | * } catch (error) { 110 | * console.error('Timed out:', error); 111 | * } 112 | * ``` 113 | * 114 | * @example 115 | * Usage with custom interval between attempts: 116 | * ```typescript 117 | * import waitUntil from 'async-wait-until'; 118 | * 119 | * const isReady = async () => { 120 | * const value = await checkAsyncCondition(); 121 | * return value > 10; 122 | * }; 123 | * 124 | * try { 125 | * const result = await waitUntil(isReady, { timeout: 10000, intervalBetweenAttempts: 100 }); 126 | * console.log('Ready:', result); 127 | * } catch (error) { 128 | * console.error('Timeout reached:', error); 129 | * } 130 | * ``` 131 | * 132 | * @example 133 | * Infinite wait with manual timeout handling: 134 | * ```typescript 135 | * import waitUntil, { WAIT_FOREVER } from 'async-wait-until'; 136 | * 137 | * const someCondition = () => Boolean(getConditionValue()); 138 | * 139 | * const controller = new AbortController(); 140 | * setTimeout(() => controller.abort(), 20000); // Cancel after 20 seconds 141 | * 142 | * try { 143 | * const result = await waitUntil(someCondition, { timeout: WAIT_FOREVER }); 144 | * console.log('Condition met:', result); 145 | * } catch (error) { 146 | * if (controller.signal.aborted) { 147 | * console.error('Aborted by the user'); 148 | * } else { 149 | * console.error('Error:', error); 150 | * } 151 | * } 152 | * ``` 153 | */ 154 | export const waitUntil = ( 155 | predicate: Predicate, 156 | options?: number | Options, 157 | intervalBetweenAttempts?: number, 158 | ): Promise => { 159 | const timeoutMs = (typeof options === 'number' ? options : options?.timeout) ?? DEFAULT_TIMEOUT_IN_MS; 160 | const intervalMs = 161 | (typeof options === 'number' ? intervalBetweenAttempts : options?.intervalBetweenAttempts) ?? 162 | DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS; 163 | 164 | let timeoutHandle: NodeJS.Timeout | undefined; 165 | let intervalHandle: NodeJS.Timeout | undefined; 166 | 167 | return Promise.race([ 168 | ...(timeoutMs !== WAIT_FOREVER 169 | ? [ 170 | new Promise((_, reject) => { 171 | timeoutHandle = setTimeout(() => { 172 | reject(new TimeoutError(timeoutMs)); 173 | }, timeoutMs); 174 | }), 175 | ] 176 | : []), 177 | new Promise((resolve, reject) => { 178 | const check = async () => { 179 | try { 180 | const value = await predicate(); 181 | if (value) { 182 | resolve(value); 183 | return; 184 | } 185 | intervalHandle = setTimeout(check, intervalMs); 186 | } catch (err) { 187 | reject(err); 188 | } 189 | }; 190 | void check(); 191 | }), 192 | ]).finally(() => { 193 | timeoutHandle && clearTimeout(timeoutHandle); 194 | intervalHandle && clearTimeout(intervalHandle); 195 | }); 196 | }; 197 | 198 | export default waitUntil; 199 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - CHANGELOG.md 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | set-safe-directory: 'true' 19 | - name: Install dependencies 20 | run: npm ci --ignore-scripts 21 | - name: Lint the codebase 22 | run: npm run lint:ci 23 | test_and_report_coverage: 24 | name: Coverage 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | set-safe-directory: 'true' 31 | - name: Install dependencies 32 | run: npm ci --ignore-scripts 33 | - name: Run tests 34 | run: npm run test:ci 35 | - name: Report coverage 36 | uses: qltysh/qlty-action/coverage@v2 37 | with: 38 | token: '${{secrets.QLTY_COVERAGE_TOKEN}}' 39 | files: coverage/lcov.info 40 | - name: Upload coverage 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: test_coverage 44 | path: coverage 45 | build_code: 46 | name: Build code 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | with: 52 | set-safe-directory: 'true' 53 | - name: Install dependencies 54 | run: npm ci --ignore-scripts 55 | - name: Build code 56 | run: npm run build:ci 57 | - name: Upload the dist folder 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: dist 61 | path: dist 62 | integration_tests: 63 | name: Run integration tests 64 | runs-on: ubuntu-latest 65 | needs: build_code 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@v4 69 | with: 70 | set-safe-directory: 'true' 71 | - name: Install dependencies 72 | run: npm ci --ignore-scripts 73 | - name: Download dist/ folder 74 | uses: actions/download-artifact@v4 75 | with: 76 | name: dist 77 | path: dist 78 | - name: Run integration tests 79 | run: npm run test:integration:ci 80 | build_docs: 81 | name: Build Docs 82 | runs-on: ubuntu-latest 83 | steps: 84 | - name: Checkout 85 | uses: actions/checkout@v4 86 | with: 87 | set-safe-directory: 'true' 88 | - name: Install dependencies 89 | run: npm ci --ignore-scripts 90 | - name: Build storybook 91 | run: npm run docs:ci 92 | - name: Upload the docs 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: docs 96 | path: docs 97 | changelog: 98 | name: Generate new changelog 99 | runs-on: ubuntu-latest 100 | outputs: 101 | commit_hash: '${{ steps.committing.outputs.commit_hash }}' 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v4 105 | with: 106 | set-safe-directory: 'true' 107 | ssh-key: '${{ secrets.SSH_PRIVATE_KEY }}' 108 | persist-credentials: 'true' 109 | fetch-tags: 'true' 110 | fetch-depth: 0 111 | - name: Generate changelog 112 | run: | 113 | changelog="" 114 | 115 | # Get tags newest to oldest 116 | tags=($(git tag --sort=-version:refname)) 117 | 118 | # Untagged commits after latest tag 119 | if [ "${#tags[@]}" -gt 0 ]; then 120 | latest_tag="${tags[0]}" 121 | untagged_commits=$(git --no-pager log --format="%s (%an) [%h]" "${latest_tag}..HEAD") 122 | else 123 | untagged_commits=$(git --no-pager log --format="%s (%an) [%h]") 124 | fi 125 | 126 | # Get future version from package.json 127 | future_tag="$(awk -F'"' '/"version": ".+"/{ print $4; exit; }' package.json)" 128 | 129 | if [ -n "$untagged_commits" ]; then 130 | tag_log="### ${future_tag}\n" 131 | while IFS= read -r commit; do 132 | tag_log+="- ${commit}\n" 133 | done <<< "$untagged_commits" 134 | changelog="${tag_log}\n${changelog}" 135 | fi 136 | 137 | # Loop from i=0 to i <= tags.length (YES, intentionally +1) 138 | for ((i=0; i<=${#tags[@]}; i++)); do 139 | current="${tags[$i]}" 140 | next="${tags[$((i+1))]}" 141 | 142 | # Skip if current is empty (e.g., i == len) 143 | if [ -z "$current" ]; then 144 | continue 145 | fi 146 | 147 | if [ -n "$next" ]; then 148 | commits=$(git --no-pager log --format="%s (%an) [%h]" "${next}..${current}") 149 | else 150 | # last tag (oldest), no previous one 151 | commits=$(git --no-pager log --format="%s (%an) [%h]" "${current}") 152 | fi 153 | 154 | if [ -n "$commits" ]; then 155 | tag_log="### ${current}\n" 156 | while IFS= read -r commit; do 157 | tag_log+="- ${commit}\n" 158 | done <<< "$commits" 159 | changelog="${changelog}\n${tag_log}" 160 | fi 161 | done 162 | 163 | changelog="# Changelog\n${changelog}" 164 | echo -e "$changelog" 165 | printf '%b' "$changelog" > CHANGELOG.md 166 | - name: Upload new changelog 167 | uses: actions/upload-artifact@v4 168 | with: 169 | name: changelog 170 | path: CHANGELOG.md 171 | - name: Commit and push the new changelog 172 | id: committing 173 | run: | 174 | if [ -z $(git status -uno --porcelain) ]; then 175 | printf 'Changelogs are identical, nothing to commit\n' 176 | else 177 | printf 'Committing the updated changelog\n' 178 | git config --local user.name "github-actions[bot]" 179 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 180 | 181 | git add CHANGELOG.md 182 | git commit -am "Update the project changelog" 183 | 184 | commit_hash=$( git --no-pager log -1 --format=%H ) 185 | echo "commit_hash=${commit_hash}" >> "$GITHUB_OUTPUT" 186 | fi 187 | - name: Push changes 188 | uses: ad-m/github-push-action@v0.8.0 189 | with: 190 | ssh: true 191 | branch: '${{ github.ref }}' 192 | maybe_tag: 193 | name: Maybe tag the release 194 | runs-on: ubuntu-latest 195 | needs: [lint, test_and_report_coverage, build_code, build_docs, integration_tests, changelog] 196 | outputs: 197 | tagname: '${{ steps.tagging.outputs.tagname }}' 198 | steps: 199 | - name: Checkout 200 | uses: actions/checkout@v4 201 | with: 202 | set-safe-directory: 'true' 203 | ref: '${{ needs.changelog.outputs.commit_hash }}' 204 | - name: Install dependencies 205 | run: npm ci --ignore-scripts 206 | - name: Maybe generate tag 207 | uses: Klemensas/action-autotag@stable 208 | id: tagging 209 | with: 210 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 211 | tag_prefix: '' 212 | tag_suffix: '' 213 | changelog_structure: "**{{messageHeadline}}** {{author}}\n" 214 | trigger_publish: 215 | name: Maybe trigger publish 216 | runs-on: ubuntu-latest 217 | needs: [maybe_tag] 218 | if: ${{ needs.maybe_tag.outputs.tagname != '' }} 219 | steps: 220 | - name: Checkout 221 | uses: actions/checkout@v4 222 | with: 223 | set-safe-directory: 'true' 224 | fetch-tags: 'true' 225 | - name: Trigger the publish workflow 226 | run: gh workflow run publish.yml --ref ${{ needs.maybe_tag.outputs.tagname }} 227 | env: 228 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 229 | trigger_release: 230 | name: Maybe trigger GitHub release 231 | runs-on: ubuntu-latest 232 | needs: [maybe_tag] 233 | if: ${{ needs.maybe_tag.outputs.tagname != '' }} 234 | steps: 235 | - name: Checkout 236 | uses: actions/checkout@v4 237 | with: 238 | set-safe-directory: 'true' 239 | fetch-tags: 'true' 240 | - name: Create a GitHub release 241 | uses: ncipollo/release-action@v1 242 | with: 243 | token: '${{ secrets.GITHUB_TOKEN }}' 244 | tag: '${{ needs.maybe_tag.outputs.tagname }}' 245 | generateReleaseNotes: true 246 | allowUpdates: true 247 | env: 248 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 249 | -------------------------------------------------------------------------------- /src/utils/create_tests.ts: -------------------------------------------------------------------------------- 1 | import { Options, Predicate, PredicateReturnValue } from '../index'; 2 | 3 | const sleep = (delayInMs: number): Promise => 4 | new Promise((resolve) => { 5 | setTimeout(resolve, delayInMs); 6 | }); 7 | 8 | const DEFAULT_TEST_TIMEOUT = 10_000; 9 | 10 | export const createTests = ({ 11 | waitUntil, 12 | TimeoutError, 13 | WAIT_FOREVER, 14 | DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS, 15 | TEST_TIMEOUT = DEFAULT_TEST_TIMEOUT, 16 | }: { 17 | /* eslint-disable no-unused-vars */ 18 | waitUntil: ( 19 | predicate: Predicate, 20 | options?: number | Options, 21 | intervalBetweenAttempts?: number, 22 | ) => Promise; 23 | TimeoutError: { 24 | new (timeoutInMs: number): Error; 25 | }; 26 | WAIT_FOREVER: number; 27 | DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS: number; 28 | TEST_TIMEOUT?: number; 29 | /* eslint-enable no-unused-vars */ 30 | }) => { 31 | jest.setTimeout(TEST_TIMEOUT); 32 | 33 | describe('waitUntil', () => { 34 | describe('> New behaviour', () => { 35 | it('Calls the predicate and resolves with a truthy result', async () => { 36 | expect.assertions(1); 37 | 38 | const initialTime = Date.now(); 39 | const result = await waitUntil(() => Date.now() - initialTime > 200); 40 | 41 | expect(result).toEqual(true); 42 | }); 43 | 44 | it('Calls the predicate and resolves with a non-boolean truthy result', async () => { 45 | expect.assertions(1); 46 | 47 | const initialTime = Date.now(); 48 | const result = await waitUntil(() => (Date.now() - initialTime > 200 ? { a: 10, b: 20 } : false)); 49 | 50 | expect(result).toEqual({ a: 10, b: 20 }); 51 | }); 52 | 53 | it('Supports a custom retry interval', async () => { 54 | expect.assertions(3); 55 | 56 | const initialTime = Date.now(); 57 | const predicate = jest.fn(() => (Date.now() - initialTime > 1000 ? { a: 10, b: 20 } : false)); 58 | expect(predicate).not.toHaveBeenCalled(); 59 | const result = await waitUntil(predicate, { 60 | timeout: 1500, 61 | intervalBetweenAttempts: 500, 62 | }); 63 | 64 | expect(predicate.mock.calls.length < Math.floor(1500 / DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS) - 1).toBe(true); 65 | expect(result).toEqual({ a: 10, b: 20 }); 66 | }); 67 | 68 | it('Supports waiting forever', async () => { 69 | expect.assertions(3); 70 | 71 | const initialTime = Date.now(); 72 | const predicate = jest.fn(() => (Date.now() - initialTime > 7000 ? { a: 10, b: 20 } : false)); 73 | expect(predicate).not.toHaveBeenCalled(); 74 | const result = await waitUntil(predicate, { 75 | timeout: WAIT_FOREVER, 76 | }); 77 | 78 | expect(predicate).toHaveBeenCalled(); 79 | expect(result).toEqual({ a: 10, b: 20 }); 80 | }); 81 | 82 | it('Stops executing the predicate after timing out', async () => { 83 | expect.assertions(5); 84 | 85 | const initialTime = Date.now(); 86 | const predicate = jest.fn(() => Date.now() - initialTime > 200); 87 | expect(predicate).not.toHaveBeenCalled(); 88 | try { 89 | await waitUntil(predicate, { timeout: 200 }); 90 | } catch (e) { 91 | expect(predicate).toHaveBeenCalled(); 92 | const callNumber = predicate.mock.calls.length; 93 | assertIsTimeoutError(e, 200); 94 | await sleep(400); 95 | expect(predicate).toHaveBeenCalledTimes(callNumber); 96 | } 97 | }); 98 | 99 | it('Rejects with a timeout error when timed out', async () => { 100 | expect.assertions(2); 101 | 102 | try { 103 | const initialTime = Date.now(); 104 | await waitUntil(() => Date.now() - initialTime > 500, { 105 | timeout: 100, 106 | }); 107 | } catch (e) { 108 | assertIsTimeoutError(e, 100); 109 | } 110 | }); 111 | 112 | it('Rejects on timeout only once', async () => { 113 | expect.assertions(2); 114 | 115 | try { 116 | const initialTime = Date.now(); 117 | await waitUntil(() => Date.now() - initialTime > 200, { 118 | timeout: 200, 119 | }); 120 | } catch (e) { 121 | assertIsTimeoutError(e, 200); 122 | } 123 | }); 124 | 125 | it('Rejects on timeout once when the predicate throws an error', async () => { 126 | expect.assertions(2); 127 | 128 | try { 129 | const initialTime = Date.now(); 130 | await waitUntil( 131 | () => { 132 | if (Date.now() - initialTime >= 190) { 133 | throw new TestError('Nooo!'); 134 | } 135 | }, 136 | { 137 | timeout: 200, 138 | }, 139 | ); 140 | } catch (e) { 141 | assertIsTimeoutError(e, 200); 142 | } 143 | }); 144 | 145 | it('Rejects when the predicate throws an error', async () => { 146 | expect.assertions(3); 147 | 148 | try { 149 | await waitUntil(() => { 150 | throw new TestError('Crap!'); 151 | }); 152 | } catch (e) { 153 | assertIsError(e, 'Expected e to be an Error'); 154 | expect(e).toBeInstanceOf(TestError); 155 | expect(e).not.toBeInstanceOf(TimeoutError); 156 | expect(e.toString()).toEqual('Error: Crap!'); 157 | } 158 | }); 159 | 160 | // https://github.com/devlato/async-wait-until/issues/32 161 | describe('Issue #32', () => { 162 | it('does not leave open handlers when predicate returns false', async () => { 163 | expect.assertions(1); 164 | 165 | const end = Date.now() + 1000; 166 | const result = await waitUntil(() => Date.now() < end); 167 | expect(result).toBe(true); 168 | }); 169 | }); 170 | }); 171 | 172 | describe('> Classic behaviour', () => { 173 | it('Calls the predicate and resolves with a truthy result', async () => { 174 | expect.assertions(1); 175 | 176 | const initialTime = Date.now(); 177 | const result = await waitUntil(() => Date.now() - initialTime > 200); 178 | 179 | expect(result).toEqual(true); 180 | }); 181 | 182 | it('Calls the predicate and resolves with a non-boolean truthy result', async () => { 183 | expect.assertions(1); 184 | 185 | const initialTime = Date.now(); 186 | const result = await waitUntil(() => (Date.now() - initialTime > 200 ? { a: 10, b: 20 } : false)); 187 | 188 | expect(result).toEqual({ a: 10, b: 20 }); 189 | }); 190 | 191 | it('Supports a custom retry interval', async () => { 192 | expect.assertions(3); 193 | 194 | const initialTime = Date.now(); 195 | const predicate = jest.fn(() => (Date.now() - initialTime > 1000 ? { a: 10, b: 20 } : false)); 196 | expect(predicate).not.toHaveBeenCalled(); 197 | const result = await waitUntil(predicate, 2000, 500); 198 | 199 | expect(predicate.mock.calls.length < Math.floor(1500 / DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS) - 1).toBe(true); 200 | expect(result).toEqual({ a: 10, b: 20 }); 201 | }); 202 | 203 | it('Supports waiting forever', async () => { 204 | expect.assertions(3); 205 | 206 | const initialTime = Date.now(); 207 | const predicate = jest.fn(() => (Date.now() - initialTime > 7000 ? { a: 10, b: 20 } : false)); 208 | expect(predicate).not.toHaveBeenCalled(); 209 | const result = await waitUntil(predicate, WAIT_FOREVER); 210 | 211 | expect(predicate).toHaveBeenCalled(); 212 | expect(result).toEqual({ a: 10, b: 20 }); 213 | }); 214 | 215 | it('Rejects with a timeout error when timed out', async () => { 216 | expect.assertions(2); 217 | 218 | try { 219 | const initialTime = Date.now(); 220 | await waitUntil(() => Date.now() - initialTime > 500, 100); 221 | } catch (e) { 222 | assertIsTimeoutError(e, 100); 223 | } 224 | }); 225 | 226 | it('Rejects on timeout only once', async () => { 227 | expect.assertions(2); 228 | 229 | try { 230 | const initialTime = Date.now(); 231 | await waitUntil(() => Date.now() - initialTime > 200, 200); 232 | } catch (e) { 233 | assertIsTimeoutError(e, 200); 234 | } 235 | }); 236 | 237 | it('Stops executing the predicate after timing out', async () => { 238 | expect.assertions(5); 239 | 240 | const initialTime = Date.now(); 241 | const predicate = jest.fn(() => Date.now() - initialTime > 200); 242 | expect(predicate).not.toHaveBeenCalled(); 243 | try { 244 | await waitUntil(predicate, 200); 245 | } catch (e) { 246 | expect(predicate).toHaveBeenCalled(); 247 | const callNumber = predicate.mock.calls.length; 248 | assertIsTimeoutError(e, 200); 249 | await sleep(400); 250 | expect(predicate).toHaveBeenCalledTimes(callNumber); 251 | } 252 | }); 253 | 254 | it('Rejects on timeout once when the predicate throws an error', async () => { 255 | expect.assertions(2); 256 | 257 | try { 258 | const initialTime = Date.now(); 259 | await waitUntil(() => { 260 | if (Date.now() - initialTime >= 190) { 261 | throw new TestError('Nooo!'); 262 | } 263 | }, 200); 264 | } catch (e) { 265 | assertIsTimeoutError(e, 200); 266 | } 267 | }); 268 | 269 | it('Rejects when the predicate throws an error', async () => { 270 | expect.assertions(3); 271 | 272 | try { 273 | await waitUntil(() => { 274 | throw new TestError('Crap!'); 275 | }); 276 | } catch (e) { 277 | assertIsError(e, 'Expected e to be an Error'); 278 | expect(e).toBeInstanceOf(TestError); 279 | expect(e).not.toBeInstanceOf(TimeoutError); 280 | expect(e.toString()).toEqual('Error: Crap!'); 281 | } 282 | }); 283 | 284 | // https://github.com/devlato/async-wait-until/issues/32 285 | describe('Issue #32', () => { 286 | it('does not leave open handlers when predicate returns false', async () => { 287 | expect.assertions(1); 288 | 289 | const end = Date.now() + 1000; 290 | const result = await waitUntil(() => Date.now() < end); 291 | expect(result).toBe(true); 292 | }); 293 | }); 294 | }); 295 | }); 296 | 297 | class TestError extends Error { 298 | constructor(message: string) { 299 | super(message); 300 | 301 | Object.setPrototypeOf(this, TestError.prototype); 302 | } 303 | } 304 | 305 | // eslint-disable-next-line no-unused-vars 306 | const assertIsError: (e: unknown, message: string) => asserts e is Error = (e, message) => { 307 | if (!(e instanceof Error)) { 308 | throw new Error(message); 309 | } 310 | }; 311 | 312 | // eslint-disable-next-line no-unused-vars 313 | const assertIsTimeoutError: (e: unknown, timeoutInMs: number) => asserts e is typeof TimeoutError = ( 314 | e, 315 | timeoutInMs, 316 | ) => { 317 | assertIsError(e, 'Expected e to be an Error'); 318 | expect(e).toBeInstanceOf(TimeoutError); 319 | expect(e.toString()).toEqual(`Error: Timed out after waiting for ${timeoutInMs} ms`); 320 | }; 321 | }; 322 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-wait-until 2 | 3 | A lightweight, zero-dependency library for waiting asynchronously until a specific condition is met. Works in any JavaScript environment that supports [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), including older Node.js versions and browsers (with polyfills if necessary). 4 | 5 | ![npm version](https://img.shields.io/npm/v/async-wait-until) 6 | [![npm downloads](https://img.shields.io/npm/dw/async-wait-until)](https://npmjs.org/package/async-wait-until) 7 | ![MIT License](https://img.shields.io/npm/l/async-wait-until) 8 | [![Maintainability](https://qlty.sh/gh/devlato/projects/async-wait-until/maintainability.svg)](https://qlty.sh/gh/devlato/projects/async-wait-until) 9 | 10 | ## ✨ Features 11 | 12 | - 🚀 **Zero dependencies** - Lightweight and fast 13 | - 🔧 **TypeScript support** - Full TypeScript definitions included 14 | - 🌐 **Universal compatibility** - Works in Node.js and browsers 15 | - ⚡ **Flexible configuration** - Customizable timeouts and intervals 16 | - 🎯 **Promise-based** - Clean async/await syntax 17 | - 📦 **Multiple formats** - UMD, ESM, and additional format bundles 18 | - 🛡️ **Error handling** - Built-in timeout error handling 19 | 20 | ## 📚 Table of Contents 21 | 22 | - [Installation](#-installation) 23 | - [How to Use](#️-how-to-use) 24 | - [API Reference](#-api) 25 | - [TypeScript Usage](#-typescript-usage) 26 | - [Recipes](#-recipes) 27 | - [Browser Compatibility](#-browser-compatibility) 28 | - [Troubleshooting](#-troubleshooting) 29 | - [Development and Testing](#-development-and-testing) 30 | - [Links](#-links) 31 | 32 | ## 📖 Detailed Documentation 33 | 34 | For detailed documentation, visit [https://devlato.github.io/async-wait-until/](https://devlato.github.io/async-wait-until/) 35 | 36 | --- 37 | 38 | ## 🚀 Installation 39 | 40 | Install using npm: 41 | 42 | ```sh 43 | npm install async-wait-until 44 | ``` 45 | 46 | The library includes UMD and ESM bundles (plus additional formats), so you can use it in any environment. 47 | 48 | ```javascript 49 | import { waitUntil } from 'async-wait-until'; 50 | 51 | // Example: Wait for an element to appear 52 | await waitUntil(() => document.querySelector('#target') !== null); 53 | ``` 54 | 55 | --- 56 | 57 | ## 🛠️ How to Use 58 | 59 | ### Basic Example: Wait for a DOM Element 60 | 61 | ```javascript 62 | import { waitUntil } from 'async-wait-until'; 63 | 64 | const waitForElement = async () => { 65 | // Wait for an element with the ID "target" to appear 66 | const element = await waitUntil(() => document.querySelector('#target'), { timeout: 5000 }); 67 | console.log('Element found:', element); 68 | }; 69 | 70 | waitForElement(); 71 | ``` 72 | 73 | ### Handling Timeouts 74 | 75 | If the condition is not met within the timeout, a `TimeoutError` is thrown. 76 | 77 | ```javascript 78 | import { waitUntil, TimeoutError } from 'async-wait-until'; 79 | 80 | const waitForElement = async () => { 81 | try { 82 | const element = await waitUntil(() => document.querySelector('#target'), { timeout: 5000 }); 83 | console.log('Element found:', element); 84 | } catch (error) { 85 | if (error instanceof TimeoutError) { 86 | console.error('Timeout: Element not found'); 87 | } else { 88 | console.error('Unexpected error:', error); 89 | } 90 | } 91 | }; 92 | 93 | waitForElement(); 94 | ``` 95 | 96 | --- 97 | 98 | ## 📚 API Reference 99 | 100 | ### `waitUntil(predicate, options)` 101 | 102 | Waits for the `predicate` function to return a truthy value and resolves with that value. 103 | 104 | **Parameters:** 105 | 106 | | Name | Type | Required | Default | Description | 107 | | --------------------------------- | ---------- | -------- | --------- | ------------------------------------------------------------------------------------ | 108 | | `predicate` | `Function` | ✅ Yes | - | A function that returns a truthy value (or a Promise for one). | 109 | | `options.timeout` | `number` | 🚫 No | `5000` ms | Maximum wait time before throwing `TimeoutError`. Use `WAIT_FOREVER` for no timeout. | 110 | | `options.intervalBetweenAttempts` | `number` | 🚫 No | `50` ms | Interval between predicate evaluations. | 111 | 112 | ### Exported Constants 113 | 114 | | Name | Value | Description | 115 | | --------------------------------------- | ----- | ---------------------------------------------- | 116 | | `WAIT_FOREVER` | `∞` | Use for infinite timeout (no time limit). | 117 | | `DEFAULT_TIMEOUT_IN_MS` | `5000`| Default timeout duration in milliseconds. | 118 | | `DEFAULT_INTERVAL_BETWEEN_ATTEMPTS_IN_MS`| `50` | Default interval between attempts in milliseconds. | 119 | 120 | ### Exported Classes 121 | 122 | - **`TimeoutError`** - Error thrown when timeout is reached before condition is met. 123 | 124 | --- 125 | 126 | ## 🔧 TypeScript Usage 127 | 128 | This library is written in TypeScript and includes full type definitions. Here are some TypeScript-specific examples: 129 | 130 | ### Basic TypeScript Usage 131 | 132 | ```typescript 133 | import { waitUntil, TimeoutError, WAIT_FOREVER } from 'async-wait-until'; 134 | 135 | // The return type is automatically inferred 136 | const element = await waitUntil(() => document.querySelector('#target')); 137 | // element is typed as Element | null 138 | 139 | // With custom timeout and interval 140 | const result = await waitUntil( 141 | () => someAsyncCondition(), 142 | { 143 | timeout: 10000, 144 | intervalBetweenAttempts: 100 145 | } 146 | ); 147 | ``` 148 | 149 | ### Using with Async Predicates 150 | 151 | ```typescript 152 | // Async predicate example 153 | const checkApiStatus = async (): Promise => { 154 | const response = await fetch('/api/health'); 155 | return response.ok; 156 | }; 157 | 158 | try { 159 | await waitUntil(checkApiStatus, { timeout: 30000 }); 160 | console.log('API is ready!'); 161 | } catch (error) { 162 | if (error instanceof TimeoutError) { 163 | console.error('API failed to become ready within 30 seconds'); 164 | } 165 | } 166 | ``` 167 | 168 | ### Type-Safe Options 169 | 170 | ```typescript 171 | import { Options } from 'async-wait-until'; 172 | 173 | const customOptions: Options = { 174 | timeout: 15000, 175 | intervalBetweenAttempts: 200 176 | }; 177 | 178 | await waitUntil(() => someCondition(), customOptions); 179 | ``` 180 | 181 | --- 182 | 183 | ## 💡 Recipes 184 | 185 | ### Wait Indefinitely 186 | 187 | Use `WAIT_FOREVER` to wait without a timeout: 188 | 189 | ```javascript 190 | import { waitUntil, WAIT_FOREVER } from 'async-wait-until'; 191 | 192 | await waitUntil(() => someCondition, { timeout: WAIT_FOREVER }); 193 | ``` 194 | 195 | ### Adjust Retry Interval 196 | 197 | Change how often the predicate is evaluated: 198 | 199 | ```javascript 200 | await waitUntil(() => someCondition, { intervalBetweenAttempts: 1000 }); // Check every 1 second 201 | ``` 202 | 203 | ### Wait for API Response 204 | 205 | ```javascript 206 | const waitForApi = async () => { 207 | const response = await waitUntil(async () => { 208 | try { 209 | const res = await fetch('/api/status'); 210 | return res.ok ? res : null; 211 | } catch { 212 | return null; // Keep trying on network errors 213 | } 214 | }, { timeout: 30000, intervalBetweenAttempts: 1000 }); 215 | 216 | return response.json(); 217 | }; 218 | ``` 219 | 220 | ### Wait for File System Changes (Node.js) 221 | 222 | ```javascript 223 | import fs from 'fs'; 224 | import { waitUntil } from 'async-wait-until'; 225 | 226 | // Wait for a file to be created 227 | const filePath = './important-file.txt'; 228 | await waitUntil(() => fs.existsSync(filePath), { timeout: 10000 }); 229 | 230 | // Wait for file to have content 231 | await waitUntil(() => { 232 | if (fs.existsSync(filePath)) { 233 | return fs.readFileSync(filePath, 'utf8').trim().length > 0; 234 | } 235 | return false; 236 | }); 237 | ``` 238 | 239 | ### Wait for Database Connection 240 | 241 | ```javascript 242 | const waitForDatabase = async (db) => { 243 | await waitUntil(async () => { 244 | try { 245 | await db.ping(); 246 | return true; 247 | } catch { 248 | return false; 249 | } 250 | }, { timeout: 60000, intervalBetweenAttempts: 2000 }); 251 | 252 | console.log('Database is ready!'); 253 | }; 254 | ``` 255 | 256 | ### Wait with Custom Conditions 257 | 258 | ```javascript 259 | // Wait for multiple conditions 260 | const waitForComplexCondition = async () => { 261 | return waitUntil(() => { 262 | const user = getCurrentUser(); 263 | const permissions = getPermissions(); 264 | const apiReady = isApiReady(); 265 | 266 | // All conditions must be true 267 | return user && permissions.length > 0 && apiReady; 268 | }); 269 | }; 270 | 271 | // Wait for specific value ranges 272 | const waitForTemperature = async () => { 273 | return waitUntil(async () => { 274 | const temp = await getSensorTemperature(); 275 | return temp >= 20 && temp <= 25 ? temp : null; 276 | }); 277 | }; 278 | ``` 279 | 280 | --- 281 | 282 | ## 🌐 Browser Compatibility 283 | 284 | This library works in any JavaScript environment that supports Promises: 285 | 286 | **Node.js:** ✅ Version 0.14.0 and above 287 | **Modern Browsers:** ✅ Chrome 32+, Firefox 29+, Safari 8+, Edge 12+ 288 | **Legacy Browsers:** ✅ With Promise polyfill (e.g., es6-promise) 289 | 290 | ### CDN Usage 291 | 292 | ```html 293 | 294 | 295 | 300 | ``` 301 | 302 | ### ES Modules in Browser 303 | 304 | ```html 305 | 311 | ``` 312 | 313 | --- 314 | 315 | ## 🔍 Troubleshooting 316 | 317 | ### Common Issues 318 | 319 | **Q: My predicate never resolves, what's wrong?** 320 | A: Make sure your predicate function returns a truthy value when the condition is met. Common mistakes: 321 | - Forgetting to return a value: `() => { someCheck(); }` ❌ 322 | - Correct: `() => { return someCheck(); }` ✅ or `() => someCheck()` ✅ 323 | 324 | **Q: I'm getting unexpected timeout errors** 325 | A: Check that: 326 | - Your timeout is long enough for the condition to be met 327 | - Your predicate function doesn't throw unhandled errors 328 | - Network requests in predicates have proper error handling 329 | 330 | **Q: The function seems to run forever** 331 | A: This happens when: 332 | - Using `WAIT_FOREVER` without proper condition logic 333 | - Predicate always returns falsy values 334 | - Add logging to debug: `() => { const result = myCheck(); console.log(result); return result; }` 335 | 336 | **Q: TypeScript compilation errors** 337 | A: Ensure you're importing types correctly: 338 | ```typescript 339 | import { waitUntil, Options, TimeoutError } from 'async-wait-until'; 340 | ``` 341 | 342 | ### Performance Tips 343 | 344 | - Use reasonable intervals (50-1000ms) to balance responsiveness and CPU usage 345 | - For expensive operations, increase the interval: `{ intervalBetweenAttempts: 1000 }` 346 | - Implement proper error handling in async predicates to avoid unnecessary retries 347 | - Consider using `WAIT_FOREVER` with external cancellation for long-running waits 348 | 349 | --- 350 | 351 | ## 🧪 Development and Testing 352 | 353 | Contributions are welcome! To contribute: 354 | 355 | 1. Fork and clone the repository. 356 | 2. Install dependencies: `npm install`. 357 | 3. Use the following commands: 358 | 359 | - **Run Tests:** `npm test` 360 | - **Lint Code:** `npm run lint` 361 | - **Format Code:** `npm run format` 362 | - **Build Library:** `npm run build` 363 | - **Generate Docs:** `npm run docs` 364 | 365 | --- 366 | 367 | ## 📝 Links 368 | 369 | - [License](./LICENSE) 370 | - [Detailed Documentation](https://devlato.github.io/async-wait-until/) 371 | - [Changelog](./CHANGELOG.md) - Track version updates and changes 372 | - [Contributing Guidelines](./CONTRIBUTING.md) - How to contribute to the project 373 | - [Code of Conduct](./CODE_OF_CONDUCT.md) - Community standards and expectations 374 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ### 2.0.31 3 | - Update maintainability badge link in README.md (#78) (Denis Tokarev) [0b4470d] 4 | - Update the project changelog (github-actions[bot]) [3c0dcaa] 5 | 6 | 7 | ### 2.0.30 8 | - Update README & release 2.0.30 (#76) (github-actions[bot]) [27d589a] 9 | - Update the project changelog (github-actions[bot]) [e371e4b] 10 | 11 | ### 2.0.29 12 | - Fix typo & release 2.0.29 (#75) (github-actions[bot]) [94e38db] 13 | - Update the project changelog (github-actions[bot]) [a5c7a99] 14 | 15 | ### 2.0.28 16 | - Release 2.0.28 (#74) (github-actions[bot]) [b90b068] 17 | - Update the project changelog (github-actions[bot]) [bc72317] 18 | - Fix #72: include source maps in npm releases (#73) (github-actions[bot]) [a70735c] 19 | - Update the project changelog (github-actions[bot]) [cb3724b] 20 | 21 | ### 2.0.27 22 | - Another attempt at including all tags in the changelog (#71) (github-actions[bot]) [9744550] 23 | - Fix bug in changelog generation (github-actions[bot]) [3e9edb6] 24 | - Update the project changelog (github-actions[bot]) [643b101] 25 | 26 | ### 2.0.25 27 | - Update release.yml (#69) (github-actions[bot]) [7b28273] 28 | - Update the project changelog (github-actions[bot]) [65317e5] 29 | 30 | ### 2.0.24 31 | - Bump version to 2.0.24 and add standard files (#68) (github-actions[bot]) [cb2c5aa] 32 | - Update the project changelog (github-actions[bot]) [8a63b3b] 33 | 34 | ### 2.0.23 35 | - Auto-release to GH (#67) (github-actions[bot]) [67ac746] 36 | - Update the project changelog (github-actions[bot]) [263ad5d] 37 | 38 | ### 2.0.22 39 | - Remove the tag file download (#66) (github-actions[bot]) [eb0baa0] 40 | - Update the project changelog (github-actions[bot]) [76843aa] 41 | 42 | ### 2.0.21 43 | - Add missing tagging step id (#65) (github-actions[bot]) [c6cdf39] 44 | - Update the project changelog (github-actions[bot]) [a368c83] 45 | 46 | ### 2.0.20 47 | - Fix typo in publish pipeline (#64) (github-actions[bot]) [ce7359a] 48 | 49 | ### 2.0.19 50 | - Update package.json (#63) (Denis Tokarev) [e659ebb] 51 | - Bump undici from 6.21.0 to 6.21.1 in the npm_and_yarn group (#62) (dependabot[bot]) [cb7dcbe] 52 | - Only trigger the publish workflow when the release is tagged (Denis Tokarev) [95b3e9f] 53 | - Update the project changelog (github-actions[bot]) [9f60b68] 54 | - Ignore CHANGELOG.md in release.yml (Denis Tokarev) [845ab5d] 55 | - Update the project changelog (github-actions[bot]) [e3a7de5] 56 | 57 | ### 2.0.18 58 | - Release 2.0.18 (#61) (github-actions[bot]) [a1b08be] 59 | - Update the project changelog (github-actions[bot]) [a676bea] 60 | 61 | ### 2.0.17 62 | - Release 2.0.17 (#60) (github-actions[bot]) [3b9442b] 63 | - Update the project changelog (github-actions[bot]) [2a673a0] 64 | - Update the project changelog (github-actions[bot]) [34d7511] 65 | - Update the project changelog (github-actions[bot]) [f33c8eb] 66 | - Update the project changelog (github-actions[bot]) [65c337e] 67 | - Update the project changelog (github-actions[bot]) [824b16d] 68 | - Update the project changelog (github-actions[bot]) [296e634] 69 | - Update the project changelog (github-actions[bot]) [50d3206] 70 | - Update the project changelog (github-actions[bot]) [ed554ce] 71 | - Update the project changelog (github-actions[bot]) [4b079d8] 72 | - Update the project changelog (github-actions[bot]) [c230436] 73 | - Update the project changelog (github-actions[bot]) [3482dd8] 74 | - Update the project changelog (github-actions[bot]) [d1db247] 75 | - Update the project changelog (github-actions[bot]) [be4ace2] 76 | - Update the project changelog (github-actions[bot]) [d9deb10] 77 | - Update the project changelog (github-actions[bot]) [507319a] 78 | - Update the project changelog (github-actions[bot]) [8b5e494] 79 | - Update the project changelog (github-actions[bot]) [06d0490] 80 | - Update the project changelog (github-actions[bot]) [88f3db7] 81 | - Update the project changelog (github-actions[bot]) [9294bb6] 82 | - Update the project changelog (github-actions[bot]) [e46f9fb] 83 | - Update the project changelog (github-actions[bot]) [68e6086] 84 | - Update the project changelog (github-actions[bot]) [edbfcd4] 85 | - Update the project changelog (github-actions[bot]) [a913b1a] 86 | - Update the project changelog (github-actions[bot]) [cccb781] 87 | - Update the project changelog (github-actions[bot]) [59b0e6b] 88 | - Update the project changelog (github-actions[bot]) [9f09b49] 89 | - Update the project changelog (github-actions[bot]) [4735728] 90 | - Update the project changelog (github-actions[bot]) [dff1c2e] 91 | - Update the project changelog (github-actions[bot]) [3bab86b] 92 | - Update the project changelog (github-actions[bot]) [c5efb94] 93 | - Update the project changelog (github-actions[bot]) [dee49ef] 94 | - Update the project changelog (github-actions[bot]) [ef20b13] 95 | - Update the project changelog (github-actions[bot]) [0fd1d09] 96 | - Update the project changelog (github-actions[bot]) [0017049] 97 | - Update the project changelog (github-actions[bot]) [61936c8] 98 | - Update the project changelog (github-actions[bot]) [0642ec2] 99 | - Update the project changelog (github-actions[bot]) [b241ad1] 100 | - Update the project changelog (github-actions[bot]) [1c3cb87] 101 | - Update the project changelog (github-actions[bot]) [6e0cce2] 102 | - Update the project changelog (github-actions[bot]) [09009d9] 103 | - Update the project changelog (github-actions[bot]) [ff8763c] 104 | - Update the project changelog (github-actions[bot]) [99c26a7] 105 | - Update the project changelog (github-actions[bot]) [057ed1e] 106 | - Update the project changelog (github-actions[bot]) [02b4cf7] 107 | - Update the project changelog (github-actions[bot]) [7dd237b] 108 | - Update the project changelog (github-actions[bot]) [702abea] 109 | - Update the project changelog (github-actions[bot]) [f563d80] 110 | - Update the project changelog (github-actions[bot]) [dc8cc80] 111 | - Update the project changelog (github-actions[bot]) [620eff0] 112 | - Update the project changelog (github-actions[bot]) [93ec81a] 113 | - Update the project changelog (github-actions[bot]) [9629e11] 114 | - Update the project changelog (github-actions[bot]) [196dec5] 115 | - Update the project changelog (github-actions[bot]) [4cb0989] 116 | - Update the project changelog (github-actions[bot]) [4357168] 117 | - Update the project changelog (github-actions[bot]) [c392d02] 118 | - Update the project changelog (github-actions[bot]) [5323d22] 119 | - Update the project changelog (github-actions[bot]) [4b0f1e9] 120 | - Update the project changelog (github-actions[bot]) [dfa9d1d] 121 | - Update the project changelog (github-actions[bot]) [409a048] 122 | - Update the project changelog (github-actions[bot]) [812bd6f] 123 | - Update the project changelog (github-actions[bot]) [fca0bc3] 124 | - Update the project changelog (github-actions[bot]) [b21c7e6] 125 | - Update the project changelog (github-actions[bot]) [1b084b0] 126 | - Update the project changelog (github-actions[bot]) [75f1e00] 127 | - Update the project changelog (github-actions[bot]) [6b88368] 128 | - Update the project changelog (github-actions[bot]) [c2a92ca] 129 | - Update the project changelog (github-actions[bot]) [ee273ab] 130 | - Update the project changelog (github-actions[bot]) [927f194] 131 | - Update the project changelog (github-actions[bot]) [88228e0] 132 | - Update the project changelog (github-actions[bot]) [4b07f62] 133 | - Update the project changelog (github-actions[bot]) [9529a8c] 134 | - Update the project changelog (github-actions[bot]) [ebcb9ae] 135 | - Update the project changelog (github-actions[bot]) [f577b64] 136 | - Update the project changelog (github-actions[bot]) [42305ca] 137 | - Update the project changelog (github-actions[bot]) [9b0f9f9] 138 | - Update the project changelog (github-actions[bot]) [9fdb766] 139 | - Update the project changelog (github-actions[bot]) [c30e96d] 140 | - Update the project changelog (github-actions[bot]) [6db41b4] 141 | - Update the project changelog (github-actions[bot]) [3236888] 142 | - Update the project changelog (github-actions[bot]) [df4d82d] 143 | - Update the project changelog (github-actions[bot]) [60b67e5] 144 | - Update the project changelog (github-actions[bot]) [7ea82b5] 145 | - Update the project changelog (github-actions[bot]) [d993b66] 146 | - Update the project changelog (github-actions[bot]) [b12ca88] 147 | - Update the project changelog (github-actions[bot]) [cd7ab4e] 148 | - Update the project changelog (github-actions[bot]) [03a440e] 149 | - Update the project changelog (github-actions[bot]) [bc73fdc] 150 | - Update the project changelog (github-actions[bot]) [3f55211] 151 | - Update the project changelog (github-actions[bot]) [bd7aa90] 152 | - Update the project changelog (github-actions[bot]) [3763921] 153 | - Update the project changelog (github-actions[bot]) [5ae4c34] 154 | - Update the project changelog (github-actions[bot]) [2e0d4a1] 155 | - Update the project changelog (github-actions[bot]) [89da19a] 156 | - Update the project changelog (github-actions[bot]) [961e9bd] 157 | - Update the project changelog (github-actions[bot]) [5974ad6] 158 | - Update the project changelog (github-actions[bot]) [23a8adc] 159 | - Update the project changelog (github-actions[bot]) [ee44c2e] 160 | - Update the project changelog (github-actions[bot]) [b6fcd40] 161 | - Update the project changelog (github-actions[bot]) [4781e92] 162 | - Update the project changelog (github-actions[bot]) [ea8c362] 163 | - Update the project changelog (github-actions[bot]) [1a735f4] 164 | - Update the project changelog (github-actions[bot]) [f40b1fc] 165 | - Update the project changelog (github-actions[bot]) [cb49108] 166 | - Update the project changelog (github-actions[bot]) [35e4661] 167 | - Update the project changelog (github-actions[bot]) [5b29653] 168 | - Update the project changelog (github-actions[bot]) [eedab45] 169 | - Update the project changelog (github-actions[bot]) [4ff6086] 170 | - Update the project changelog (github-actions[bot]) [b44ee38] 171 | - Update the project changelog (github-actions[bot]) [321cc7a] 172 | - Update the project changelog (github-actions[bot]) [80e1e15] 173 | - Update the project changelog (github-actions[bot]) [f88501d] 174 | - Update the project changelog (github-actions[bot]) [e53aba8] 175 | - Update the project changelog (github-actions[bot]) [07868e6] 176 | - Update the project changelog (github-actions[bot]) [cc02cc1] 177 | - Update the project changelog (github-actions[bot]) [e0db6d0] 178 | - Update the project changelog (github-actions[bot]) [fad3fc4] 179 | - Update the project changelog (github-actions[bot]) [5cc6a9a] 180 | - Update the project changelog (github-actions[bot]) [5f5c123] 181 | - Update the project changelog (github-actions[bot]) [919d4cc] 182 | - Update the project changelog (github-actions[bot]) [7846820] 183 | - Update the project changelog (github-actions[bot]) [02cf6e9] 184 | - Update the project changelog (github-actions[bot]) [0455fd7] 185 | - Update the project changelog (github-actions[bot]) [5252e50] 186 | - Update the project changelog (github-actions[bot]) [abc8bb0] 187 | - Update the project changelog (github-actions[bot]) [694345f] 188 | - Update the project changelog (github-actions[bot]) [ebc01d0] 189 | - Update the project changelog (github-actions[bot]) [43f2859] 190 | - Update the project changelog (github-actions[bot]) [edfe01a] 191 | - Update the project changelog (github-actions[bot]) [d7178ef] 192 | - Update the project changelog (github-actions[bot]) [8885113] 193 | - Update the project changelog (github-actions[bot]) [24a58a5] 194 | - Update the project changelog (github-actions[bot]) [727674b] 195 | - Update the project changelog (github-actions[bot]) [99682d8] 196 | - Update the project changelog (github-actions[bot]) [f522d57] 197 | - Update the project changelog (github-actions[bot]) [02c6ab5] 198 | - Update the project changelog (github-actions[bot]) [37548de] 199 | - Update the project changelog (github-actions[bot]) [9d6761d] 200 | - Update the project changelog (github-actions[bot]) [930d776] 201 | - Update the project changelog (github-actions[bot]) [edafa0b] 202 | - Update the project changelog (github-actions[bot]) [53fe293] 203 | - Update the project changelog (github-actions[bot]) [bd875cb] 204 | - Update the project changelog (github-actions[bot]) [e42da0f] 205 | - Update the project changelog (github-actions[bot]) [ca8e74e] 206 | - Update the project changelog (github-actions[bot]) [4890eaf] 207 | - Update the project changelog (github-actions[bot]) [8a297a6] 208 | - Update the project changelog (github-actions[bot]) [7107f77] 209 | - Update the project changelog (github-actions[bot]) [d686808] 210 | - Update the project changelog (github-actions[bot]) [2881378] 211 | - Update the project changelog (github-actions[bot]) [7dbb5b0] 212 | - Update the project changelog (github-actions[bot]) [847a723] 213 | - Update the project changelog (github-actions[bot]) [0b321a5] 214 | - Update the project changelog (github-actions[bot]) [6b5412b] 215 | - Update the project changelog (github-actions[bot]) [f6c663d] 216 | - Update the project changelog (github-actions[bot]) [186fb33] 217 | - Update the project changelog (github-actions[bot]) [304ad4d] 218 | - Update the project changelog (github-actions[bot]) [fda117e] 219 | - Update the project changelog (github-actions[bot]) [8351db1] 220 | - Update the project changelog (github-actions[bot]) [2476f34] 221 | - Update the project changelog (github-actions[bot]) [4b55adc] 222 | 223 | ### 2.0.16 224 | - Release 2.0.16 (#59) (github-actions[bot]) [7c9a91d] 225 | - Update the project changelog (github-actions[bot]) [7b4721a] 226 | 227 | ### 2.0.15 228 | - Use deploykeys (#58) (github-actions[bot]) [d00c644] 229 | 230 | ### 2.0.14 231 | - 2.0.14 (#57) (github-actions[bot]) [75a1ac6] 232 | 233 | ### 2.0.13 234 | - Prepare 2.0.13-RC (#55) (Denis Tokarev) [e5e3094] 235 | - Update README.md (#54) (Denis Tokarev) [4255dfa] 236 | 237 | ### 2.0.12 238 | - Fix #29 (#30) (github-actions[bot]) [83482cc] 239 | - Fix tests (#28) (github-actions[bot]) [9d6f73d] 240 | 241 | ### 2.0.10 242 | - Update the project changelog (github-actions[bot]) [ac4486b] 243 | - Bump version to 2.0.10 (Denis Tokarev) [af4e364] 244 | - Update CI to publish only when integration tests has passed (#26) (github-actions[bot]) [8b06c25] 245 | - Create codeql-analysis.yml (#25) (Denis Tokarev) [668b957] 246 | 247 | ### 2.0.9 248 | - Auto-release and auto-publish daily when possible (#24) (github-actions[bot]) [4d3d2cc] 249 | - Add re-labeling (Denis Tokarev) [8641a51] 250 | - Update auto-labeling (Denis Tokarev) [d47ce0c] 251 | - Set test timeout to 10s (Denis Tokarev) [154f35e] 252 | - Update the CI configuration to properly set PR bodies (Denis Tokarev) [71f8658] 253 | - Update codeclimate config (Denis Tokarev) [208bd20] 254 | - Update codeclimate config (Denis Tokarev) [87d3f35] 255 | - Fix (Denis Tokarev) [d151065] 256 | - Update scripts (Denis Tokarev) [7763e70] 257 | - Upgrade all deps and prepare v2.0.9 (Denis Tokarev) [a999686] 258 | 259 | ### 2.0.8 260 | - Adjust spelling in readme and index (#21) (Lukasz Kokot) [7e7178d] 261 | - GH_PAT -> GITHUB_TOKEN (Denis Tokarev) [ef0219c] 262 | - Fix workflows (Denis Tokarev) [5b1fa8f] 263 | - Upgrade automerge action (Denis Tokarev) [7c72d54] 264 | - Fix timeout default value in README (#20) (Simon Smith) [2a51437] 265 | 266 | ### 2.0.7 267 | - Aaa (Denis Tokarev) [dbf7cdf] 268 | - Update README (#18) (github-actions[bot]) [43d46df] 269 | 270 | ### 2.0.6 271 | - Fix #16: add 'module' to package.json, manually fix dependabot alerts (#17) (github-actions[bot]) [2ef43fa] 272 | 273 | ### 2.0.5 274 | - Increase allowed complexity (Denis Tokarev) [f6397ea] 275 | - Fix #14 (Denis Tokarev) [bfea826] 276 | 277 | ### 2.0.4 278 | - Disable secret scan (#13) (github-actions[bot]) [0ae085e] 279 | - Update README -> 2.0.4 (#12) (github-actions[bot]) [16cbe84] 280 | 281 | ### 2.0.3 282 | - Further get rid of global variables (#11) (Denis Tokarev) [38ebf94] 283 | - Further get rid of global variables (Denis Tokarev) [a1cc16f] 284 | 285 | ### 2.0.2 286 | - Fix issue w/wrong setTimeout call (#10) (github-actions[bot]) [8aa1f79] 287 | 288 | ### 2.0.1 289 | - Fix accidental index.d.ts file removal (#8) (github-actions[bot]) [742d063] 290 | - Delete docs directory (Denis Tokarev) [f33d9b1] 291 | - Delete index.md (Denis Tokarev) [c1219a4] 292 | - Set theme jekyll-theme-slate (Denis Tokarev) [b03c05c] 293 | 294 | ### 2.0.0 295 | - Early 2.0 (#5) (github-actions[bot]) [960d93a] 296 | 297 | ### 1.2.6 298 | - Set 1.2.6 (Denis Tokarev) [06d2ce2] 299 | - Update package.json (Denis Tokarev) [5d801b2] 300 | - Update .travis.yml (Denis Tokarev) [a7dc1e7] 301 | - Update README.md (Denis Tokarev) [05c3a74] 302 | - Update .travis.yml (Denis Tokarev) [f860c6e] 303 | 304 | ### 1.2.5 305 | - Update index.d.ts (Denis Tokarev) [2250371] 306 | - Update README.md (Denis Tokarev) [34f0703] 307 | - Update README.md (Denis Tokarev) [37eda17] 308 | - Update README.md (Denis Tokarev) [1cf89a3] 309 | - Update README.md (Denis Tokarev) [2ec477c] 310 | 311 | ### 1.2.4 312 | - Update Readme (Denis Tokarev) [f062270] 313 | 314 | ### 1.2.3 315 | - Update token (Denis Tokarev) [c4bf260] 316 | 317 | ### 1.2.2 318 | - Add Node 10 (Denis Tokarev) [001a0d6] 319 | 320 | ### 1.2.1 321 | - Fix codeclimate config (Denis Tokarev) [db8db68] 322 | 323 | ### 1.2.0 324 | - Add TypeScript definitions (Denis Tokarev) [8ea2d41] 325 | 326 | ### 1.1.7 327 | - Update token (Denis Tokarev) [83f880c] 328 | 329 | ### 1.1.6 330 | - Release 1.1.6 (Denis Tokarev) [652a576] 331 | - Remove Bluebird (Denis Tokarev) [18de9d6] 332 | 333 | ### 1.1.5 334 | - [*] Update README (devlato) [e5b0cc3] 335 | 336 | ### 1.1.4 337 | - [+] Add Issues Badge (devlato) [7190d49] 338 | - [*] Fix codeclimate config (devlato) [28763d6] 339 | - [*] Fix codeclimate config (devlato) [44cac7e] 340 | 341 | ### 1.1.3 342 | - [*] Download rank (devlato) [8ea78c5] 343 | 344 | ### 1.1.2 345 | - [*] Rating support (devlato) [23a2b13] 346 | - [*] Fix (devlato) [15c740e] 347 | - [*] Fix code climate (devlato) [b63b926] 348 | - [*] Better CI & CD Integration (devlato) [bf113e2] 349 | - [*] Ignore travis file for building (devlato) [219752f] 350 | - [*] Travis CI Integration (devlato) [d9df56e] 351 | - [*] Update README, exclude files from shipping (devlato) [8a2b2bb] 352 | 353 | ### 1.1.0 354 | - [*] Add support for predicate rvalue (devlato) [b63e0e9] 355 | 356 | ### 1.0.3 357 | - [*] Integrate Husky (devlato) [1dd0165] 358 | 359 | ### 1.0.2 360 | - [*] Update README (devlato) [c0ab7df] 361 | 362 | ### 1.0.1 363 | - [*] Fix package name (devlato) [95c3bab] 364 | 365 | ### 1.0.0 366 | - [+] Implement waitUntil 1.0, provide 100% coverage with tests (devlato) [d6b7e7a] 367 | --------------------------------------------------------------------------------