├── .nvmrc ├── .npmrc ├── .yarnrc.yml ├── docs ├── public │ ├── CNAME │ ├── favicon.png │ └── logo.svg ├── tsconfig.json ├── .vscode │ ├── extensions.json │ └── launch.json ├── src │ ├── env.d.ts │ ├── components │ │ ├── References.astro │ │ └── Configuration.astro │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── reference │ │ │ ├── properties.mdx │ │ │ ├── types-and-enums.mdx │ │ │ ├── methods.mdx │ │ │ └── configuration.mdx │ │ │ ├── introduction │ │ │ ├── index.mdx │ │ │ ├── getting-started.mdx │ │ │ └── done-callback.mdx │ │ │ ├── index.mdx │ │ │ └── guides │ │ │ └── migrating-from-v1-v3.mdx │ └── tailwind.css ├── .gitignore ├── package.json ├── tailwind.config.mjs ├── astro.config.mjs └── README.md ├── .npmignore ├── icons ├── icon-x64.png ├── icon-x512.png └── icon.svg ├── src ├── index.ts ├── Namespace.ts ├── DefaultOptions.ts ├── Task.ts ├── Utils.ts ├── __tests__ │ ├── Task.test.ts │ ├── Utils.test.ts │ ├── List.test.ts │ └── TaskRunner.test.ts ├── Interface.ts ├── List.ts └── TaskRunner.ts ├── .editorconfig ├── tsconfig.build.json ├── .gitignore ├── .prettierrc ├── .size-limit.json ├── bench ├── helpers │ ├── is-last.ts │ ├── cpu-usage.ts │ └── benchmarker.ts ├── units │ ├── for-each.ts │ ├── for-loop.ts │ ├── while-loop.ts │ └── ct.ts ├── benchmark-setup.ts ├── bench.d.ts └── benchmarks.ts ├── .vscode └── settings.json ├── .release-it.json ├── jest.config.ts ├── .github ├── ISSUE_TEMPLATE │ ├── issue--help-request.md │ ├── feature_request.md │ └── bug_report.md ├── setup │ └── action.yml └── workflows │ ├── github-pages.yml │ └── checks.yml ├── commitlint.config.js ├── lefthook.yml ├── testing-utils ├── setup │ └── test.setup.ts └── utils │ ├── generate-tasks.ts │ └── create-runner.ts ├── SECURITY.md ├── tsconfig.json ├── .eslintrc ├── LICENSE ├── scripts └── release.ts ├── rollup.config.js ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── CONTRIBUTING.md ├── README.md ├── package.json └── CHANGELOG.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=false -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /docs/public/CNAME: -------------------------------------------------------------------------------- 1 | concurrent-tasks.samrith.dev 2 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | node_modules/ 3 | coverage/ 4 | .cache/ 5 | *.log 6 | src/__tests__ -------------------------------------------------------------------------------- /icons/icon-x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samrith-s/concurrent-tasks/HEAD/icons/icon-x64.png -------------------------------------------------------------------------------- /icons/icon-x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samrith-s/concurrent-tasks/HEAD/icons/icon-x512.png -------------------------------------------------------------------------------- /docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samrith-s/concurrent-tasks/HEAD/docs/public/favicon.png -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export * as CT from "./Namespace"; 4 | export { TaskRunner } from "./TaskRunner"; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | -------------------------------------------------------------------------------- /docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/Namespace.ts: -------------------------------------------------------------------------------- 1 | export { DefaultOptions } from "./DefaultOptions"; 2 | export * from "./Task"; 3 | export * from "./Interface"; 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2015" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/triple-slash-reference */ 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/coverage/ 2 | es/ 3 | lib/ 4 | node_modules/ 5 | umd/ 6 | dist/ 7 | dev-dist/ 8 | .cache/ 9 | html/ 10 | npm-debug.log* 11 | *.log 12 | .DS_Store 13 | .eslintcache 14 | .yarn 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "editorconfig": true, 5 | "semi": true, 6 | "trailingComma": "es5", 7 | "printWidth": 80, 8 | "singleQuote": false 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/components/References.astro: -------------------------------------------------------------------------------- 1 |
2 | Type: 3 |
-------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "lib/cjs/index.js", 4 | "limit": "5 kB" 5 | }, 6 | { 7 | "path": "lib/es/index.js", 8 | "limit": "5 kB" 9 | }, 10 | { 11 | "path": "lib/umd/index.js", 12 | "limit": "5 kB" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /bench/helpers/is-last.ts: -------------------------------------------------------------------------------- 1 | export function whatIsLast(last: number) { 2 | return function isLast(value: number, callback: () => void) { 3 | if (value === last) { 4 | callback(); 5 | } 6 | }; 7 | } 8 | 9 | export type IsLast = ReturnType; 10 | -------------------------------------------------------------------------------- /docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /bench/units/for-each.ts: -------------------------------------------------------------------------------- 1 | import { bench } from "../helpers/benchmarker"; 2 | 3 | export const for_each = bench("for-each", ({ tasks, isLast, done }) => { 4 | tasks.forEach((task, index) => { 5 | task(() => { 6 | isLast(index, done); 7 | }, index); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /bench/units/for-loop.ts: -------------------------------------------------------------------------------- 1 | import { bench } from "../helpers/benchmarker"; 2 | 3 | export const for_loop = bench("for-loop", ({ tasks, isLast, done }) => { 4 | for (const idx in tasks) { 5 | const task = tasks[idx]; 6 | const count = Number(idx); 7 | 8 | task(() => { 9 | isLast(count, done); 10 | }, count); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { docsSchema, i18nSchema } from "@astrojs/starlight/schema"; 3 | import { defineCollection } from "astro:content"; 4 | 5 | export const collections = { 6 | docs: defineCollection({ schema: docsSchema() }), 7 | i18n: defineCollection({ type: "data", schema: i18nSchema() }), 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": false, 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": "explicit", 7 | "source.organizeImports": "never" 8 | }, 9 | "editor.wordWrap": "wordWrapColumn", 10 | "editor.wordWrapColumn": 80 11 | } 12 | -------------------------------------------------------------------------------- /bench/benchmark-setup.ts: -------------------------------------------------------------------------------- 1 | const actualResults: BenchmarkResults = {}; 2 | 3 | global.benchConfig = { 4 | taskCount: 100, 5 | timeout: 5, 6 | }; 7 | 8 | global.results = { 9 | add(name, result) { 10 | actualResults[name] = result; 11 | }, 12 | delete(name) { 13 | delete actualResults[name]; 14 | }, 15 | print() { 16 | console.table(actualResults); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/DefaultOptions.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { RunnerOptions } from "./Interface"; 4 | 5 | export function DefaultOptions(name: string) { 6 | return { 7 | name, 8 | concurrency: 3, 9 | onAdd: undefined, 10 | onStart: undefined, 11 | onRun: undefined, 12 | onDone: undefined, 13 | onEnd: undefined, 14 | onRemove: undefined, 15 | } as unknown as RunnerOptions; 16 | } 17 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "release: v${version}", 4 | "commitArgs": ["-n"], 5 | "requireCleanWorkingDir": false, 6 | "push": false 7 | }, 8 | "npm": { 9 | "release": true 10 | }, 11 | "github": { 12 | "release": true 13 | }, 14 | "plugins": { 15 | "@release-it/conventional-changelog": { 16 | "preset": { 17 | "name": "angular" 18 | }, 19 | "infile": "CHANGELOG.md" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bench/helpers/cpu-usage.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | export function cpuUsage(startUsage: NodeJS.CpuUsage, startTime: number) { 3 | // Normalize the one returned by process.cpuUsage() 4 | // (microseconds VS miliseconds) 5 | const endUsage = process.cpuUsage(startUsage); 6 | const totalUsage = endUsage.system + endUsage.user; 7 | 8 | const total = (100 * totalUsage) / ((Date.now() - startTime) * 1000); 9 | 10 | return total; 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest"; 2 | 3 | const config: JestConfigWithTsJest = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | setupFilesAfterEnv: ["./testing-utils/setup/test.setup.ts"], 7 | coveragePathIgnorePatterns: ["src/Namespace.ts", "tests/*"], 8 | coverageThreshold: { 9 | global: { 10 | branches: 95, 11 | statements: 95, 12 | functions: 95, 13 | lines: 95, 14 | }, 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue--help-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Issue: Help Request" 3 | about: Any help requests for the community to fulfil 4 | title: "[Help]" 5 | labels: help wanted 6 | assignees: "" 7 | --- 8 | 9 | **What help is needed?** 10 | What part of the project needs help and with what? 11 | 12 | **What solutions have you tried?** 13 | What possible solutions have you tried to get it working? 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /** 3 | * @type {import('@commitlint/types').UserConfig} 4 | */ 5 | module.exports = { 6 | extends: ["@commitlint/config-conventional"], 7 | rules: { 8 | "type-enum": [ 9 | 2, 10 | "always", 11 | ["feat", "fix", "refactor", "deps", "docs", "test", "chore", "release"], 12 | ], 13 | "scope-enum": [ 14 | 2, 15 | "always", 16 | ["core", "interface", "runner", "utils", "options", "logs"], 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /bench/bench.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | declare global { 3 | declare var benchConfig: { 4 | taskCount?: number; 5 | timeout?: number; 6 | }; 7 | 8 | declare type BenchmarkResult = { 9 | Duration: string; 10 | CPU: string; 11 | }; 12 | 13 | declare type BenchmarkResults = Record; 14 | 15 | declare var results = {} as { 16 | add: (name: string, result: BenchmarkResult) => void; 17 | delete(name: string): void; 18 | print(): void; 19 | }; 20 | } 21 | 22 | export {}; 23 | -------------------------------------------------------------------------------- /bench/units/while-loop.ts: -------------------------------------------------------------------------------- 1 | import { bench } from "../helpers/benchmarker"; 2 | 3 | export const while_loop = bench("while-loop", ({ tasks, isLast, done }) => { 4 | const slicedTasks = tasks.slice(); 5 | 6 | let resolved = false; 7 | let count = 0; 8 | 9 | while (!resolved && slicedTasks.length) { 10 | const task = slicedTasks.shift()!; 11 | 12 | task(() => { 13 | count++; 14 | isLast(count, () => { 15 | resolved = true; 16 | done(); 17 | }); 18 | }, slicedTasks.length); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | output: 2 | - failure 3 | no_tty: true 4 | pre-commit: 5 | parallel: true 6 | commands: 7 | eslint: 8 | glob: "*.{js,ts,jsx,tsx}" 9 | run: node_modules/.bin/eslint {staged_files} 10 | prettier: 11 | glob: "*.{ts,tsx,json}" 12 | run: node_modules/.bin/prettier --write {staged_files} 13 | commit-msg: 14 | commands: 15 | check: 16 | run: node_modules/.bin/commitlint --edit $1 17 | # scripts: 18 | # "hello.js": 19 | # runner: node 20 | # "any.go": 21 | # runner: go run 22 | -------------------------------------------------------------------------------- /testing-utils/setup/test.setup.ts: -------------------------------------------------------------------------------- 1 | process.on("uncaughtExceptionMonitor", (reason) => { 2 | console.log("FAILED TO CATCH EXCEPTION:", reason); 3 | }); 4 | 5 | process.on("unhandledRejection", (reason) => { 6 | console.log(`FAILED TO HANDLE PROMISE REJECTION:`, reason); 7 | }); 8 | 9 | process.on("uncaughtException", (reason) => { 10 | console.log(`FAILED TO HANDLE PROMISE REJECTION:`, reason); 11 | }); 12 | 13 | process.on("rejectionHandled", async (reason) => { 14 | const rej = await reason; 15 | console.log("rejectionHandled:", rej); 16 | }); 17 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /testing-utils/utils/generate-tasks.ts: -------------------------------------------------------------------------------- 1 | import { CT } from "../../src"; 2 | import { TaskWithDone } from "../../src/Interface"; 3 | 4 | export function generateTask(result = -1, timeout = 5) { 5 | return (done: CT.Done) => { 6 | setTimeout(() => { 7 | done(result); 8 | }, timeout); 9 | }; 10 | } 11 | 12 | export function generateTasks(count = 10, timeout = 5) { 13 | const tasks: TaskWithDone[] = []; 14 | 15 | for (let i = 0; i < count; i++) { 16 | tasks.push(generateTask(i, timeout)); 17 | } 18 | 19 | return tasks; 20 | } 21 | -------------------------------------------------------------------------------- /bench/units/ct.ts: -------------------------------------------------------------------------------- 1 | import { TaskRunner } from "../../src/TaskRunner"; 2 | import { bench } from "../helpers/benchmarker"; 3 | 4 | const withCT = (name: string, concurrency = 3) => 5 | bench(`with-ct-${name}`, ({ done, tasks }) => { 6 | const runner = new TaskRunner({ 7 | concurrency, 8 | onEnd: done, 9 | }); 10 | 11 | runner.addMultiple(tasks); 12 | 13 | runner.start(); 14 | }); 15 | 16 | export const with_ct_default = withCT("default"); 17 | export const with_ct_10 = withCT("10", 10); 18 | export const with_ct_100 = withCT("100", 100); 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.3.1", 14 | "@astrojs/starlight": "^0.13.0", 15 | "@astrojs/starlight-tailwind": "^2.0.1", 16 | "@astrojs/tailwind": "^5.0.2", 17 | "astro": "^3.2.3", 18 | "sharp": "^0.32.5", 19 | "tailwindcss": "^3.0.24", 20 | "typescript": "^5.2.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testing-utils/utils/create-runner.ts: -------------------------------------------------------------------------------- 1 | import { CT, TaskRunner } from "../../src"; 2 | 3 | import { generateTasks } from "./generate-tasks"; 4 | 5 | export const RUNNER_NAME = "test-runner"; 6 | 7 | export function createRunner( 8 | options?: Partial & { 9 | autoStart?: boolean; 10 | taskCount?: number; 11 | taskDuration?: number; 12 | } 13 | ) { 14 | const runner = new TaskRunner({ 15 | name: RUNNER_NAME, 16 | ...(options ?? {}), 17 | }); 18 | const tasks = generateTasks(options?.taskCount, options?.taskDuration); 19 | runner.addMultiple(tasks); 20 | 21 | if (options?.autoStart) { 22 | runner.start(); 23 | } 24 | 25 | return runner; 26 | } 27 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Concurrent Tasks will continue receiving security updates which will majorly go as patches. 4 | 5 | ## Supported Versions 6 | 7 | Use this section to tell people about which versions of your project are 8 | currently being supported with security updates. 9 | 10 | | Version | Supported | 11 | | ------- | ------------------ | 12 | | 1.0.x | :x: | 13 | | 3.0.x | :white_check_mark: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Package name: 18 | Version: 19 | 20 | ### Vulnerability 21 | 22 | 23 | ### Fix 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Enhancement]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | import starlightPlugin from "@astrojs/starlight-tailwind"; 2 | 3 | // Generated color palettes 4 | const accent = { 5 | 200: "#fefefe", 6 | 600: "#111111", 7 | 900: "#222222", 8 | 950: "#111111", 9 | }; 10 | const gray = { 11 | 100: "#f4f4f5", 12 | 200: "#e5e5e5", 13 | 300: "#d4d4d4", 14 | 400: "#a3a3a3", 15 | 500: "#737373", 16 | 600: "#525252", 17 | 700: "#404040", 18 | 800: "#262626", 19 | 900: "#171717", 20 | 950: "#0a0a0a", 21 | }; 22 | 23 | /** @type {import('tailwindcss').Config} */ 24 | export default { 25 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 26 | theme: { 27 | extend: { 28 | colors: { accent, gray }, 29 | }, 30 | }, 31 | plugins: [starlightPlugin()], 32 | }; 33 | -------------------------------------------------------------------------------- /src/Task.ts: -------------------------------------------------------------------------------- 1 | import { TaskWithDone, TaskID, TaskStatus, Done } from "./Interface"; 2 | 3 | export type Tasks = Task[]; 4 | 5 | export class Task { 6 | readonly #_task: TaskWithDone; 7 | readonly #_id: TaskID; 8 | 9 | #_status: TaskStatus = TaskStatus.PENDING; 10 | 11 | constructor(id: TaskID, task: TaskWithDone) { 12 | this.#_task = task; 13 | this.#_id = id; 14 | } 15 | 16 | get id(): TaskID { 17 | return this.#_id; 18 | } 19 | 20 | get status(): TaskStatus { 21 | return this.#_status; 22 | } 23 | 24 | set status(status: TaskStatus) { 25 | if (Object.values(TaskStatus).includes(status)) { 26 | this.#_status = status; 27 | } 28 | } 29 | 30 | public run(done: Done): void { 31 | this.#_task(done, this.id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "bench/**/*.ts"], 3 | "exclude": [ 4 | "node_modules", 5 | "rollup.config.js", 6 | "commitlint.config.js", 7 | "testing-utils/**/*" 8 | ], 9 | "compilerOptions": { 10 | "baseUrl": ".", 11 | "module": "CommonJS", 12 | "target": "ES2015", 13 | "lib": ["esnext"], 14 | "importHelpers": false, 15 | "declaration": true, 16 | "declarationDir": "lib/dts", 17 | "strict": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "moduleResolution": "node", 23 | "esModuleInterop": true, 24 | "skipLibCheck": true, 25 | "skipDefaultLibCheck": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "noEmit": true, 28 | "experimentalDecorators": true, 29 | "resolveJsonModule": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/components/Configuration.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | type: string 4 | defaultValue: string 5 | required?: boolean 6 | } 7 | 8 | const { type, defaultValue, required } = Astro.props 9 | --- 10 | 11 |
12 |
13 | Required: 14 | {required ? 'true' : 'false'} 15 |
16 | 17 |
18 | Type: 19 | {type} 20 |
21 | 22 |
23 | Default: 24 | {defaultValue} 25 |
26 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /docs/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | details > ul * { 6 | font-weight: normal !important; 7 | font-size: var(--sl-text-sm) !important; 8 | } 9 | 10 | .starlight-aside { 11 | @apply rounded; 12 | } 13 | 14 | .card { 15 | @apply rounded; 16 | } 17 | 18 | .reference p { 19 | margin: 0 !important; 20 | display: inline; 21 | } 22 | 23 | site-search > button[data-open-modal] { 24 | width: 100%; 25 | max-width: 100% !important; 26 | } 27 | 28 | :root, 29 | :root[data-theme="dark"] .expressive-code, 30 | :root[data-theme="dark"] .expressive-code[data-theme="dark"] { 31 | --ec-tm-delBg: #862d2722 !important; 32 | --ec-tm-insBg: #1e571533 !important; 33 | } 34 | 35 | :root, 36 | :root[data-theme="light"] .expressive-code, 37 | :root[data-theme="light"] .expressive-code[data-theme="light"] { 38 | --ec-tm-delBg: #862d2711 !important; 39 | --ec-tm-insBg: #1e571522 !important; 40 | } 41 | 42 | header.header { 43 | @apply bg-white bg-opacity-5 backdrop-blur-lg; 44 | } 45 | -------------------------------------------------------------------------------- /.github/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: "Setup node and dependencies" 3 | 4 | inputs: 5 | lockfile: 6 | required: true 7 | description: "Location of lockfile" 8 | default: "./yarn.lock" 9 | working-directory: 10 | required: false 11 | description: "Working directory" 12 | default: "." 13 | node-version: 14 | description: "Node version" 15 | required: false 16 | default: "18" 17 | 18 | runs: 19 | using: "composite" 20 | steps: 21 | - name: Enable corepack 22 | shell: bash 23 | run: corepack enable 24 | 25 | - name: Setup Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ inputs.node-version }} 29 | cache: yarn 30 | cache-dependency-path: ${{ inputs.lockfile }} 31 | 32 | - name: Enable corepack 33 | shell: bash 34 | run: corepack enable 35 | 36 | - name: Install dependencies 37 | shell: bash 38 | working-directory: ${{ inputs.working-directory }} 39 | run: yarn install --immutable 40 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "import", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:import/warnings", 9 | "plugin:import/errors", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-namespace": "off", 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "import/order": [ 16 | "error", 17 | { 18 | "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], 19 | "newlines-between": "always", 20 | "alphabetize": { 21 | "order": "asc", 22 | "caseInsensitive": false 23 | } 24 | } 25 | ] 26 | }, 27 | "settings": { 28 | "import/parsers": { 29 | "@typescript-eslint/parser": [".ts", ".js", ".json"] 30 | }, 31 | "import/resolver": { 32 | "typescript": { 33 | "alwaysTryTypes": true, 34 | "project": "tsconfig.json" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | type Item = any; 2 | 3 | export function isFunction(item: Item): item is (...args: any) => any { 4 | return typeof item === "function"; 5 | } 6 | 7 | export function isNumber(item: Item): item is number { 8 | return typeof item === "number" && !isNaN(item); 9 | } 10 | 11 | export function isString(item: Item): item is string { 12 | return typeof item === "string"; 13 | } 14 | 15 | export function isArray(item: Item): item is Array { 16 | return item.constructor === Array; 17 | } 18 | 19 | export function isEmptyString(item: Item): item is string { 20 | return isString(item) && !item; 21 | } 22 | 23 | export const assignFunction = (item: any): typeof item => { 24 | if (isFunction(item)) { 25 | return item; 26 | } 27 | }; 28 | 29 | export const assignNumber = ( 30 | number: number, 31 | defaultNumber: number, 32 | listLength: number 33 | ): number => { 34 | if (isNumber(number)) { 35 | if (number === 0 || number === Infinity) { 36 | return listLength; 37 | } 38 | 39 | return number; 40 | } 41 | 42 | return defaultNumber; 43 | }; 44 | -------------------------------------------------------------------------------- /bench/benchmarks.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import "./benchmark-setup"; 4 | 5 | import yoctoSpinner from "yocto-spinner"; 6 | 7 | import { with_ct_10, with_ct_100, with_ct_default } from "./units/ct"; 8 | import { for_each } from "./units/for-each"; 9 | import { for_loop } from "./units/for-loop"; 10 | import { while_loop } from "./units/while-loop"; 11 | 12 | benchConfig.taskCount = 10000; 13 | 14 | async function main() { 15 | const fns = [ 16 | while_loop, 17 | for_loop, 18 | for_each, 19 | with_ct_default, 20 | with_ct_10, 21 | with_ct_100, 22 | ]; 23 | 24 | console.log("\nConcurrent Tasks - Benchmarks"); 25 | console.log("Total tasks:", benchConfig.taskCount, "\n"); 26 | 27 | for (const index in fns) { 28 | const idx = Number(index); 29 | const fn = fns[idx]; 30 | 31 | const spinner = yoctoSpinner({ 32 | text: `(${idx + 1}/${fns.length}) ${fn.displayName}`, 33 | spinner: undefined, 34 | }).start(); 35 | 36 | await fn(); 37 | 38 | spinner.success(); 39 | } 40 | 41 | console.log(""); 42 | results.print(); 43 | } 44 | 45 | main(); 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Samrith Shankar 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 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { spawnSync } from "child_process"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | 6 | import pkg from "../package.json"; 7 | 8 | const argv = process.argv.slice(2); 9 | 10 | const ROOT = path.resolve(__dirname, ".."); 11 | const PKG_JSON_PATH = path.resolve(ROOT, "package.json"); 12 | 13 | /** 14 | * Clean up package.json 15 | */ 16 | const pkgClone: Partial = { 17 | ...pkg, 18 | }; 19 | 20 | delete pkgClone.devDependencies; 21 | delete pkgClone.scripts; 22 | 23 | fs.writeFileSync( 24 | PKG_JSON_PATH, 25 | [JSON.stringify(pkgClone, null, 2), "\n"].join("") 26 | ); 27 | 28 | /** 29 | * Execute release 30 | */ 31 | try { 32 | spawnSync("yarn", ["release-it", ...argv], { 33 | stdio: "inherit", 34 | }); 35 | 36 | spawnSync("git", ["reset", "--soft", "HEAD~1"], { 37 | stdio: "inherit", 38 | }); 39 | spawnSync("git", ["restore", "--staged", PKG_JSON_PATH], { 40 | stdio: "inherit", 41 | }); 42 | spawnSync("git", ["commit", "-c", "ORIG_HEAD"], { 43 | stdio: "inherit", 44 | }); 45 | } catch (e) { 46 | console.error(e); 47 | fs.writeFileSync( 48 | PKG_JSON_PATH, 49 | [JSON.stringify(pkg, null, 2), "\n"].join("") 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/__tests__/Task.test.ts: -------------------------------------------------------------------------------- 1 | import { generateTask } from "../../testing-utils/utils/generate-tasks"; 2 | import { TaskStatus } from "../Interface"; 3 | import { Task } from "../Task"; 4 | 5 | describe("Task", () => { 6 | describe("id", () => { 7 | it("should return the correct id", () => { 8 | const task = new Task(1, generateTask()); 9 | expect(task.id).toBe(1); 10 | }); 11 | }); 12 | 13 | describe("status", () => { 14 | it("should return the correct status", () => { 15 | const task = new Task(1, generateTask()); 16 | expect(task.status).toBe(TaskStatus.PENDING); 17 | }); 18 | 19 | it("should not set status if it is not a valid status", () => { 20 | const task = new Task(1, generateTask()); 21 | 22 | task.status = "Hello!" as TaskStatus; 23 | 24 | expect(task.status).toBe(TaskStatus.PENDING); 25 | }); 26 | }); 27 | 28 | describe("run", () => { 29 | const taskFn = jest.fn().mockImplementation((done) => { 30 | done(); 31 | }); 32 | const doneFn = jest.fn(); 33 | 34 | const task = new Task(1, taskFn); 35 | 36 | task.run(doneFn); 37 | 38 | expect(taskFn).toHaveBeenCalledTimes(1); 39 | expect(taskFn).toHaveBeenCalledWith(doneFn, 1); 40 | 41 | expect(doneFn).toHaveBeenCalledTimes(1); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/properties.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Properties 3 | description: List of properties provided by Concurrent Tasks runner instance. 4 | sidebar: 5 | order: 1 6 | --- 7 | 8 | import References from "../../../components/References.astro"; 9 | 10 | ##### tasks 11 | 12 | [`TasksList`][TasksList] 13 | 14 | - List of tasks that are in the runner 15 | - Returns a copy of the tasks 16 | - Any modifications to this list will not change the underlying list of tasks 17 | - The `all` property returns the tasks ordered by: 18 | - `running` 19 | - `pending` 20 | - `completed` 21 | 22 | --- 23 | 24 | ##### count 25 | 26 | [`TasksCount`][TasksCount] 27 | 28 | - Counts of tasks at various statuses: `completed`, `running`, `pending`, and `total` 29 | 30 | --- 31 | 32 | ##### busy 33 | 34 | `boolean` 35 | 36 | - Signified whether the instance is currently working or idle 37 | 38 | ##### paused 39 | 40 | `boolean` 41 | 42 | - Signified whether the instance is currently paused 43 | 44 | --- 45 | 46 | ##### destroyed 47 | 48 | `boolean` 49 | 50 | - Signified whether the instance is destroyed or not 51 | 52 | [TasksList]: /reference/types-and-enums#taskslist 53 | [TasksCount]: /reference/types-and-enums#taskscount 54 | -------------------------------------------------------------------------------- /docs/src/content/docs/introduction/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: About the project 3 | description: A background on the what and why of Concurrent Tasks 4 | prev: false 5 | sidebar: 6 | order: 1 7 | --- 8 | 9 | Concurrent Tasks mimics a queue by using JavaScript's inbuilt array data type. Each task is a function which signals completion back to the runner. 10 | 11 | Once tasks are added, the instance starts executing them until the concurrency criteria is met. Whenever a task is complete, the next task in the queue is picked up. 12 | 13 | ## Why another task runner? 14 | 15 | While writing fragments or a priority queue in JavaScript, we often come across quite a few hurdles. There's either no real native option, or we need to write a lot of code to get the desired results. 16 | 17 | Concurrent Tasks aims to solve this by providing a simplistic queue. The queue not only maintains the order, but also enables you to manipulate it. 18 | 19 | ## What can I use it with? 20 | 21 | The minimalism of Concurrent Tasks makes it an easy-to-use solution across any framework or flavour of JavaScript. It has **ZERO dependencies** and can be used virtually in any scenario. With a **minified and gzipped size of 2.7kB**, it is the ultimate lightweight tool for your concurrency needs. 22 | 23 | - [x] Vanilla JavaScript 24 | - [x] Frontend Frameworks (React, Vue, Angular, etc) 25 | - [x] Backend Frameworks (Express, Hapi, Koa, etc) 26 | - [x] NPM Module 27 | - [x] Node CLI Application 28 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import starlight from "@astrojs/starlight"; 2 | import tailwind from "@astrojs/tailwind"; 3 | import { defineConfig } from "astro/config"; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | integrations: [ 8 | starlight({ 9 | title: "Concurrent Tasks", 10 | logo: { 11 | src: "./public/logo.svg", 12 | }, 13 | social: { 14 | github: "https://github.com/samrith-s/concurrent-tasks", 15 | }, 16 | favicon: "/favicon.png", 17 | customCss: ["./src/tailwind.css"], 18 | sidebar: [ 19 | { 20 | label: "Introduction", 21 | autogenerate: { 22 | directory: "introduction", 23 | }, 24 | }, 25 | { 26 | label: "Reference", 27 | autogenerate: { 28 | directory: "reference", 29 | }, 30 | }, 31 | { 32 | label: "Guides", 33 | badge: "New", 34 | autogenerate: { 35 | directory: "guides", 36 | }, 37 | }, 38 | ], 39 | tableOfContents: { 40 | maxHeadingLevel: 5, 41 | }, 42 | expressiveCode: { 43 | themes: ["material-theme-darker", "material-theme-lighter"], 44 | styleOverrides: { borderRadius: "0.25rem" }, 45 | frames: { 46 | removeCommentsWhenCopyingTerminalFrames: true, 47 | }, 48 | }, 49 | }), 50 | tailwind({ 51 | applyBaseStyles: false, 52 | }), 53 | ], 54 | }); 55 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "docs/**" 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: false 19 | 20 | env: 21 | BUILD_PATH: "./docs" 22 | 23 | jobs: 24 | build: 25 | name: Build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup 32 | uses: ./.github/setup 33 | with: 34 | working-directory: ${{ env.BUILD_PATH }} 35 | lockfile: ${{ env.BUILD_PATH }}/yarn.lock 36 | 37 | - name: Setup pages 38 | id: pages 39 | uses: actions/configure-pages@v4 40 | 41 | - name: Build 42 | working-directory: ${{ env.BUILD_PATH }} 43 | run: | 44 | yarn astro build \ 45 | --site "${{ steps.pages.outputs.origin }}" \ 46 | --base "${{ steps.pages.outputs.base_path }}" 47 | 48 | - name: Upload artifact 49 | uses: actions/upload-pages-artifact@v2 50 | with: 51 | path: ${{ env.BUILD_PATH }}/dist 52 | 53 | deploy: 54 | environment: 55 | name: github-pages 56 | url: ${{ steps.deployment.outputs.page_url }} 57 | needs: build 58 | runs-on: ubuntu-latest 59 | name: Deploy 60 | steps: 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v3 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* eslint-disable no-undef */ 3 | 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | import json from "@rollup/plugin-json"; 6 | import resolve from "@rollup/plugin-node-resolve"; 7 | import strip from "@rollup/plugin-strip"; 8 | import terser from "@rollup/plugin-terser"; 9 | import builtins from "builtin-modules"; 10 | import typescript from "rollup-plugin-typescript2"; 11 | 12 | import * as pkg from "./package.json"; 13 | 14 | const isDev = process.env.NODE_ENV === "development"; 15 | 16 | const input = pkg.entries || "src/index.ts"; 17 | 18 | export default { 19 | input, 20 | output: [output("cjs", "main"), output("es", "module"), output("umd", "umd")], 21 | plugins: [ 22 | resolve(), 23 | commonjs(), 24 | json(), 25 | typescript({ 26 | typescript: require("typescript"), 27 | clean: true, 28 | tsconfig: "./tsconfig.build.json", 29 | exclude: ["**/*.test.ts"], 30 | useTsconfigDeclarationDir: true, 31 | }), 32 | strip(), 33 | ], 34 | external: [...builtins], 35 | }; 36 | 37 | function output(format) { 38 | const isUMDProd = !isDev && format === "umd"; 39 | 40 | return { 41 | dir: `lib/${format}`, 42 | name: "ConcurrentTasks", 43 | format, 44 | sourcemap: isDev, 45 | exports: "auto", 46 | plugins: [ 47 | !isDev && 48 | terser({ 49 | mangle: isUMDProd, 50 | compress: isUMDProd, 51 | format: { 52 | beautify: !isUMDProd, 53 | }, 54 | }), 55 | ], 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Concurrent Tasks 3 | description: Get started with concurrent execution in JavaScript. 4 | template: splash 5 | hero: 6 | title: Concurrent Tasks 7 | tagline: The only JavaScript task runner you need. 8 | actions: 9 | - text: Get started 10 | link: /introduction 11 | icon: right-arrow 12 | variant: primary 13 | --- 14 | 15 | import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; 16 | 17 | ## Installation 18 | 19 | 20 | 21 | 22 | ```bash 23 | npm i concurrent-tasks 24 | ``` 25 | 26 | 27 | 28 | 29 | ```bash 30 | yarn add concurrent-tasks 31 | ``` 32 | 33 | 34 | 35 | 36 | ```bash 37 | pnpm add concurrent-tasks 38 | ``` 39 | 40 | 41 | 42 | 43 | ## Features 44 | 45 | 46 | 47 | CT contains **ZERO** runtime dependencies. 48 | 49 | 50 | A minified and gzipped size of **2.7kB**, making it super lightweight even 51 | in high-latency scenarios. 52 | 53 | 54 | 55 | 56 | The minimalism of Concurrent Tasks makes it an easy-to-use solution across any 57 | framework or flavour of JavaScript. 58 |
    59 |
  • Vanilla JavaScript
  • 60 |
  • Frontend (React, Vue, Angular, Svelte, etc.)
  • 61 |
  • Mobile (React Native, NativeScript, etc.)
  • 62 |
  • Backend (Express, Hapi, Koa, etc)
  • 63 |
  • NPM modules
  • 64 |
  • Node CLI application
  • 65 |
66 |
67 | -------------------------------------------------------------------------------- /docs/src/content/docs/introduction/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | description: Use Concurrent Tasks in your project today 4 | sidebar: 5 | order: 2 6 | --- 7 | 8 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 9 | 10 | Getting started with Concurrent Tasks is super easy. The practical nomenclature and the simplicity of it's API is the true power behind it. 11 | 12 | ## Installation 13 | 14 | **Concurrent Tasks** is available to install directly via NPM. 15 | 16 | 17 | 18 | 19 | ```bash 20 | npm i concurrent-tasks 21 | ``` 22 | 23 | 24 | 25 | 26 | ```bash 27 | yarn add concurrent-tasks 28 | ``` 29 | 30 | 31 | 32 | 33 | ```bash 34 | pnpm add concurrent-tasks 35 | ``` 36 | 37 | 38 | 39 | 40 | ### Browsers 41 | 42 | ```html 43 | 47 | ``` 48 | 49 | ### Bun 50 | 51 | ```bash 52 | bun install concurrent-tasks 53 | ``` 54 | 55 | ### Deno 56 | 57 | ```ts 58 | import { TaskRunner } from "https://cdn.jsdelivr.net/npm/concurrent-tasks/src/index.ts"; 59 | ``` 60 | 61 | ## Usage 62 | 63 | ```ts 64 | import { TaskRunner } from "concurrent-tasks"; 65 | 66 | const runner = new TaskRunner(); 67 | 68 | function generateTasks() { 69 | const tasks = []; 70 | let count = 1000; 71 | while (count) { 72 | tasks.push((done) => { 73 | setTimeout(() => { 74 | done(); 75 | }, Math.random() * 1000); 76 | }); 77 | count--; 78 | } 79 | return tasks; 80 | } 81 | 82 | runner.addMultiple(generateTasks()); 83 | ``` 84 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "docs/**" 9 | - "README.md" 10 | pull_request: 11 | paths-ignore: 12 | - "docs/**" 13 | - "README.md" 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | lint: 21 | name: Lint 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup 28 | uses: ./.github/setup 29 | 30 | - name: Lint 31 | run: yarn lint 32 | 33 | types: 34 | name: Typecheck 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup 41 | uses: ./.github/setup 42 | 43 | - name: Typecheck 44 | run: yarn tsc -p . 45 | 46 | tests: 47 | name: Tests 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | 53 | - name: Setup 54 | uses: ./.github/setup 55 | 56 | - name: Test 57 | run: yarn test --max-workers="75%" --coverage 58 | 59 | build: 60 | name: Build 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Setup 67 | uses: ./.github/setup 68 | 69 | - name: Build 70 | run: yarn build 71 | 72 | benchmarks: 73 | name: Benchmarks 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | 79 | - name: Setup 80 | uses: ./.github/setup 81 | 82 | - name: Benchmark 83 | run: yarn bench 84 | -------------------------------------------------------------------------------- /bench/helpers/benchmarker.ts: -------------------------------------------------------------------------------- 1 | import { CT } from "../../src"; 2 | import { generateTasks } from "../../testing-utils/utils/generate-tasks"; 3 | 4 | import { cpuUsage } from "./cpu-usage"; 5 | 6 | function whatIsLast(last: number) { 7 | return function isLast(value: number, callback: () => void) { 8 | if (value === last) { 9 | callback(); 10 | } 11 | }; 12 | } 13 | 14 | type IsLast = ReturnType; 15 | 16 | export type BenchmarkArgs = { 17 | tasks: CT.TasksWithDone; 18 | total: number; 19 | isLast: IsLast; 20 | done: () => void; 21 | }; 22 | 23 | function benchInner( 24 | name: string, 25 | benchmark: (args: BenchmarkArgs) => Promise | void 26 | ) { 27 | const startMark = `${name}-start`; 28 | const endMark = `${name}-end`; 29 | 30 | const task = performance.timerify(() => { 31 | // eslint-disable-next-line no-async-promise-executor 32 | return new Promise((resolve) => { 33 | const tasks = generateTasks(benchConfig.taskCount, benchConfig.timeout); 34 | const total = tasks.length; 35 | const isLast = whatIsLast(total - 1); 36 | 37 | performance.mark(startMark); 38 | 39 | const startTime = Date.now(); 40 | const startUsage = process.cpuUsage(); 41 | 42 | benchmark({ 43 | tasks, 44 | total, 45 | done: () => { 46 | resolve(true); 47 | performance.mark(endMark); 48 | const entry = performance.measure(name, startMark, endMark); 49 | const usage = cpuUsage(startUsage, startTime); 50 | 51 | results.add(name, { 52 | Duration: `${entry.duration.toFixed(2)}ms`, 53 | CPU: `${usage.toFixed(2)}%`, 54 | }); 55 | }, 56 | isLast, 57 | }); 58 | }); 59 | }); 60 | 61 | const value = task as typeof task & { 62 | displayName: string; 63 | }; 64 | 65 | value.displayName = name; 66 | 67 | return value; 68 | } 69 | 70 | export const bench = benchInner as typeof benchInner & { 71 | displayName: string; 72 | }; 73 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/types-and-enums.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Types & Enums 3 | description: References of all types and enums provided by Concurrent Tasks 4 | next: false 5 | sidebar: 6 | order: 3 7 | --- 8 | 9 | ## Types 10 | 11 | ##### TasksList 12 | 13 | ```ts 14 | type TasksList = { 15 | readonly running: Tasks; 16 | readonly pending: Tasks; 17 | readonly completed: Tasks; 18 | }; 19 | ``` 20 | 21 | ##### TasksCount 22 | 23 | ```ts 24 | type TasksCount = { 25 | total: number; 26 | completed: number; 27 | list: Task[]; 28 | }; 29 | ``` 30 | 31 | ##### RunnerDuration 32 | 33 | ```ts 34 | type RunnerDuration = { 35 | start: number; 36 | end: number; 37 | total: number; 38 | }; 39 | ``` 40 | 41 | ##### TaskWithDone 42 | 43 | ```ts 44 | type TaskWithDone = ( 45 | done: Done, 46 | id: TaskID 47 | ) => TaskReturn | Promise>; 48 | ``` 49 | 50 | ##### TasksWithDone 51 | 52 | ```ts 53 | type TasksWithDone = TaskWithDone[]; 54 | ``` 55 | 56 | ##### Done 57 | 58 | ```ts 59 | type Done = (result?: T) => void; 60 | ``` 61 | 62 | --- 63 | 64 | ## Enums 65 | 66 | ##### TaskStatus 67 | 68 | ```ts 69 | enum TaskStatus { 70 | PENDING = "pending", 71 | RUNNING = "running", 72 | CANCELLED = "cancelled", 73 | DONE = "done", 74 | } 75 | ``` 76 | 77 | ##### RunnerEvents 78 | 79 | ```ts 80 | enum RunnerEvents { 81 | START = "onStart", 82 | PAUSE = "onPause", 83 | DESTROY = "onDestroy", 84 | ADD = "onAdd", 85 | REMOVE = "onRemove", 86 | RUN = "onRun", 87 | DONE = "onDone", 88 | END = "onEnd", 89 | } 90 | ``` 91 | 92 | ##### AdditionMethods 93 | 94 | ```ts 95 | enum AdditionMethods { 96 | FIRST = "first", 97 | LAST = "last", 98 | AT_INDEX = "at-index", 99 | MULTIPLE_FIRST = "multiple-first", 100 | MULTIPLE_LAST = "multiple-range", 101 | } 102 | ``` 103 | 104 | ##### RemovalMethods 105 | 106 | ```ts 107 | enum RemovalMethods { 108 | ALL = "all", 109 | BY_INDEX = "by-index", 110 | RANGE = "range", 111 | FIRST = "first", 112 | LAST = "last", 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then 4 | set -x 5 | fi 6 | 7 | if [ "$LEFTHOOK" = "0" ]; then 8 | exit 0 9 | fi 10 | 11 | call_lefthook() 12 | { 13 | if test -n "$LEFTHOOK_BIN" 14 | then 15 | "$LEFTHOOK_BIN" "$@" 16 | elif lefthook -h >/dev/null 2>&1 17 | then 18 | lefthook "$@" 19 | else 20 | dir="$(git rev-parse --show-toplevel)" 21 | osArch=$(uname | tr '[:upper:]' '[:lower:]') 22 | cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') 23 | if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" 24 | then 25 | "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" 26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" 27 | then 28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" 29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" 30 | then 31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" 32 | elif test -f "$dir/node_modules/lefthook/bin/index.js" 33 | then 34 | "$dir/node_modules/lefthook/bin/index.js" "$@" 35 | 36 | elif go tool lefthook -h >/dev/null 2>&1 37 | then 38 | go tool lefthook "$@" 39 | elif bundle exec lefthook -h >/dev/null 2>&1 40 | then 41 | bundle exec lefthook "$@" 42 | elif yarn lefthook -h >/dev/null 2>&1 43 | then 44 | yarn lefthook "$@" 45 | elif pnpm lefthook -h >/dev/null 2>&1 46 | then 47 | pnpm lefthook "$@" 48 | elif swift package lefthook >/dev/null 2>&1 49 | then 50 | swift package --build-path .build/lefthook --disable-sandbox lefthook "$@" 51 | elif command -v mint >/dev/null 2>&1 52 | then 53 | mint run csjones/lefthook-plugin "$@" 54 | elif uv run lefthook -h >/dev/null 2>&1 55 | then 56 | uv run lefthook "$@" 57 | elif mise exec -- lefthook -h >/dev/null 2>&1 58 | then 59 | mise exec -- lefthook "$@" 60 | elif devbox run lefthook -h >/dev/null 2>&1 61 | then 62 | devbox run lefthook "$@" 63 | else 64 | echo "Can't find lefthook in PATH" 65 | fi 66 | fi 67 | } 68 | 69 | call_lefthook run "commit-msg" "$@" 70 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then 4 | set -x 5 | fi 6 | 7 | if [ "$LEFTHOOK" = "0" ]; then 8 | exit 0 9 | fi 10 | 11 | call_lefthook() 12 | { 13 | if test -n "$LEFTHOOK_BIN" 14 | then 15 | "$LEFTHOOK_BIN" "$@" 16 | elif lefthook -h >/dev/null 2>&1 17 | then 18 | lefthook "$@" 19 | else 20 | dir="$(git rev-parse --show-toplevel)" 21 | osArch=$(uname | tr '[:upper:]' '[:lower:]') 22 | cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') 23 | if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" 24 | then 25 | "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" 26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" 27 | then 28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" 29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" 30 | then 31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" 32 | elif test -f "$dir/node_modules/lefthook/bin/index.js" 33 | then 34 | "$dir/node_modules/lefthook/bin/index.js" "$@" 35 | 36 | elif go tool lefthook -h >/dev/null 2>&1 37 | then 38 | go tool lefthook "$@" 39 | elif bundle exec lefthook -h >/dev/null 2>&1 40 | then 41 | bundle exec lefthook "$@" 42 | elif yarn lefthook -h >/dev/null 2>&1 43 | then 44 | yarn lefthook "$@" 45 | elif pnpm lefthook -h >/dev/null 2>&1 46 | then 47 | pnpm lefthook "$@" 48 | elif swift package lefthook >/dev/null 2>&1 49 | then 50 | swift package --build-path .build/lefthook --disable-sandbox lefthook "$@" 51 | elif command -v mint >/dev/null 2>&1 52 | then 53 | mint run csjones/lefthook-plugin "$@" 54 | elif uv run lefthook -h >/dev/null 2>&1 55 | then 56 | uv run lefthook "$@" 57 | elif mise exec -- lefthook -h >/dev/null 2>&1 58 | then 59 | mise exec -- lefthook "$@" 60 | elif devbox run lefthook -h >/dev/null 2>&1 61 | then 62 | devbox run lefthook "$@" 63 | else 64 | echo "Can't find lefthook in PATH" 65 | fi 66 | fi 67 | } 68 | 69 | call_lefthook run "pre-commit" "$@" 70 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then 4 | set -x 5 | fi 6 | 7 | if [ "$LEFTHOOK" = "0" ]; then 8 | exit 0 9 | fi 10 | 11 | call_lefthook() 12 | { 13 | if test -n "$LEFTHOOK_BIN" 14 | then 15 | "$LEFTHOOK_BIN" "$@" 16 | elif lefthook -h >/dev/null 2>&1 17 | then 18 | lefthook "$@" 19 | else 20 | dir="$(git rev-parse --show-toplevel)" 21 | osArch=$(uname | tr '[:upper:]' '[:lower:]') 22 | cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') 23 | if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" 24 | then 25 | "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" 26 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" 27 | then 28 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" 29 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" 30 | then 31 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" 32 | elif test -f "$dir/node_modules/lefthook/bin/index.js" 33 | then 34 | "$dir/node_modules/lefthook/bin/index.js" "$@" 35 | 36 | elif go tool lefthook -h >/dev/null 2>&1 37 | then 38 | go tool lefthook "$@" 39 | elif bundle exec lefthook -h >/dev/null 2>&1 40 | then 41 | bundle exec lefthook "$@" 42 | elif yarn lefthook -h >/dev/null 2>&1 43 | then 44 | yarn lefthook "$@" 45 | elif pnpm lefthook -h >/dev/null 2>&1 46 | then 47 | pnpm lefthook "$@" 48 | elif swift package lefthook >/dev/null 2>&1 49 | then 50 | swift package --build-path .build/lefthook --disable-sandbox lefthook "$@" 51 | elif command -v mint >/dev/null 2>&1 52 | then 53 | mint run csjones/lefthook-plugin "$@" 54 | elif uv run lefthook -h >/dev/null 2>&1 55 | then 56 | uv run lefthook "$@" 57 | elif mise exec -- lefthook -h >/dev/null 2>&1 58 | then 59 | mise exec -- lefthook "$@" 60 | elif devbox run lefthook -h >/dev/null 2>&1 61 | then 62 | devbox run lefthook "$@" 63 | else 64 | echo "Can't find lefthook in PATH" 65 | fi 66 | fi 67 | } 68 | 69 | call_lefthook run "prepare-commit-msg" "$@" 70 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | 12 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 13 | 14 | ## 🚀 Project Structure 15 | 16 | Inside of your Astro + Starlight project, you'll see the following folders and files: 17 | 18 | ``` 19 | . 20 | ├── public/ 21 | ├── src/ 22 | │ ├── assets/ 23 | │ ├── content/ 24 | │ │ ├── docs/ 25 | │ │ └── config.ts 26 | │ └── env.d.ts 27 | ├── astro.config.mjs 28 | ├── package.json 29 | └── tsconfig.json 30 | ``` 31 | 32 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 33 | 34 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 35 | 36 | Static assets, like favicons, can be placed in the `public/` directory. 37 | 38 | ## 🧞 Commands 39 | 40 | All commands are run from the root of the project, from a terminal: 41 | 42 | | Command | Action | 43 | | :------------------------ | :----------------------------------------------- | 44 | | `npm install` | Installs dependencies | 45 | | `npm run dev` | Starts local dev server at `localhost:4321` | 46 | | `npm run build` | Build your production site to `./dist/` | 47 | | `npm run preview` | Preview your build locally, before deploying | 48 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 49 | | `npm run astro -- --help` | Get help using the Astro CLI | 50 | 51 | ## 👀 Want to learn more? 52 | 53 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 54 | -------------------------------------------------------------------------------- /docs/src/content/docs/introduction/done-callback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Done callback 3 | description: Understanding the done callback in Concurrent Tasks 4 | sidebar: 5 | order: 3 6 | --- 7 | 8 | What is the `done` callback and why is it important? 9 | 10 | ## Problem 11 | 12 | Concurrent Tasks is a JavaScript module, which runs multiple tasks concurrently until all the tasks are complete. It needs a way to figure out when a particular task has been completed. 13 | 14 | ## Solution 15 | 16 | Gulp solves this problem by either accepting a return of a Gulp task, or by calling a function done. Similarly, to solve the exact same problem, each task passed to the TaskRunner has access to a special function called `done` (ingenuity max). 17 | 18 | ## Purpose 19 | 20 | The purpose of this function is simple: Tell the instance when a particular task is complete! 21 | 22 | Internally, the done function does a fair amount of work: 23 | 24 | - Makes a free slot available for the internal runner 25 | - Updates completion counts and calls the internal runner 26 | - Updates the time elapsed from start, until the function calling done's completion 27 | - Calls the internal runner to pick up the next task in the queue 28 | 29 | ## Examples 30 | 31 | It's a very simplistic approach, which can be used across anything. Be it functions, Promises, timeouts etc. 32 | 33 | ### Functions 34 | 35 | ```ts 36 | import { TaskRunner } from "concurrent-tasks"; 37 | 38 | const runner = new TaskRunner(); 39 | 40 | function randomNumbers(done) { 41 | const numbers = []; 42 | let count = 1000; 43 | while (count) { 44 | numbers.push(Math.round(Math.random() * 100)); 45 | count--; 46 | } 47 | done(); 48 | } 49 | 50 | runner.add(randomNumbers); 51 | ``` 52 | 53 | ### Promises 54 | 55 | ```ts 56 | import { TaskRunner } from "concurrent-tasks"; 57 | 58 | const runner = new TaskRunner(); 59 | 60 | function getGoogle(done) { 61 | fetch("http://www.google.com") 62 | .then((data) => { 63 | console.log("data", data); 64 | done(); 65 | }) 66 | .catch((error) => { 67 | console.log("error", error); 68 | done(); 69 | runner.add(getGoogle); // retry fetching data 70 | }); 71 | } 72 | 73 | runner.add(getGoogle); 74 | ``` 75 | 76 | ### Timeouts 77 | 78 | ```ts 79 | import { TaskRunner } from "concurrent-tasks"; 80 | 81 | const runner = new TaskRunner(); 82 | 83 | function afterTimeout(done) { 84 | const timeout = Math.floor(Math.random() * 1000); 85 | setTimeout(() => { 86 | console.log(`This resolved after ${timeout}ms`); 87 | done(); 88 | }, timeout); 89 | } 90 | 91 | runner.add(afterTimeout); 92 | ``` 93 | -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/migrating-from-v1-v3.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Migrating from v1 to v3 3 | description: Upgrading to the latest version of Concurrent Tasks 4 | sidebar: 5 | order: 1 6 | --- 7 | 8 | Concurrent Tasks v3 comes with a host of changes to the API to make it simpler and easier to use. 9 | 10 | > The entire code has been migrated to TypeScript to provide auto-completion while ensuring type safety. 11 | 12 | ## Breaking changes 13 | 14 | There are a few breaking changes which you need to be aware of while migrating. 15 | 16 | #### Dropped support for Node < 18 17 | 18 | With [Node 16 reaching EOL September 11th, 2023](https://nodejs.org/en/blog/announcements/nodejs16-eol), it just made more sense to drop support for older versions when running in Node-based environments. 19 | 20 | This majorly only affects you if you use Concurrent Tasks in Node environments, for example: 21 | 22 | - Bundling 23 | - Servers 24 | - CLIs 25 | 26 | If you use Node versions below 18, the ideal solution is for you to upgrade. 27 | 28 | If for some reason you are unable to upgrade, no worries. With NPM and PNPM, you should be able to install just fine. With Yarn, you can simply use the `--ignore-engines` flag: 29 | 30 | ```bash 31 | yarn add concurrent-tasks --ignore-engines 32 | ``` 33 | 34 | #### Named exports over default exports 35 | 36 | Concurrent Tasks now only provides named exports. This is to align with the move to TypeScript which means we also need to make types available for you to consume for better type safety. 37 | 38 | ```diff 39 | - import TaskRunner from 'concurrent-tasks'; 40 | + import { TaskRunner } from 'concurrent-tasks'; 41 | ``` 42 | 43 | #### Removed `autoStart` configuration option 44 | 45 | Previously with Concurrent Tasks, you could pass `autoStart` as `true` while creating the runner instance. 46 | 47 | Whenever you added tasks via `add` or `addMultiple` methods, it would cause the runner to start executing. This was a source of a lot of confusion since the runner also provides a `start` method. 48 | 49 | With v3, we are removing this option in favour of `start` method to remove implicitness. 50 | 51 | ```diff 52 | import { TaskRunner } from 'concurrent-tasks'; 53 | 54 | const runner = new TaskRunner({ 55 | concurrency: 10, 56 | - autoStart: true 57 | }); 58 | 59 | runner.add((done) => { 60 | console.log("I am a task!"); 61 | done(); 62 | }) 63 | 64 | + runner.start(); 65 | ``` 66 | 67 | #### Busy status is now a property instead of a method 68 | 69 | If you are using the `isBusy` method, you will now have to use the `busy` property instead. 70 | 71 | ```diff 72 | import { TaskRunner } from 'concurrent-tasks'; 73 | 74 | const runner = new TaskRunner() 75 | 76 | - console.log(runner.isBusy()) 77 | + console.log(runner.busy) 78 | ``` 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 👩🏻‍💻 Contributing to Concurrent Tasks 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## 🙋🏻‍ We develop with Github 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## 👷🏻‍ All code changes happen through pull requests 16 | 17 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 18 | 19 | - Fork the repo and create your branch from `master`. 20 | - If you've added code that should be tested, add tests. 21 | - If you've changed APIs, update the documentation. 22 | - Ensure the test suite passes. 23 | - Make sure your code lints. 24 | - Issue that pull request! 25 | 26 | ## 👀 Any contributions you make will be under the MIT Software License 27 | 28 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 29 | 30 | ## Report bugs using Github's [issues](https://github.com/samrith-s/concurrent-tasks/issues) 31 | 32 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/samrith-s/concurrent-tasks/issues/new/choose); it's that easy! 33 | 34 | ## ✍🏼 Issue creation 35 | 36 | We have templates setup for all kinds of issues. If your issue does not fall under any of the templates, you can create an issue from scratch. Do try to include as much info and explanation as possible as it will help us a lot in understanding and resolving it. 37 | 38 | **Great Bug Reports** tend to have: 39 | 40 | - A quick summary and/or background 41 | - Steps to reproduce 42 | - Be specific! 43 | - Give sample code if you can. 44 | - What you expected would happen 45 | - What actually happens 46 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 47 | 48 | People _love_ thorough bug reports. We're not even kidding. 49 | 50 | ## 🤓 Feature requests 51 | 52 | We absolutely welcome whatever feature requests, ideas you guys might have. Please create a [Feature Request](https://github.com/samrith-s/concurrent-tasks/issues/new?template=feature_request.md) and we will collaborate with you on it. We love it when people create PRs for new features. Please feel free to do the same! 53 | 54 | Before requesting for a feature, though, please go through our [Roadmap](https://github.com/samrith-s/concurrent-tasks#roadmap) and see if it's already in the pipeline or something we do not intend to do. 55 | 56 | ## 🧐 Use a consistent coding style 57 | 58 | Our linters will majorly take care of everything. All you need to remember is to write "attractive" code. 59 | 60 | ## 🔑 License 61 | 62 | By contributing, you agree that your contributions will be licensed under its MIT License. 63 | 64 | ## 📖 References 65 | 66 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Concurrent Tasks logo 3 |

4 | 5 |

Concurrent Tasks

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 |

23 | A simple task runner which will run all tasks till completion, while maintaining concurrency limits. 24 |

25 | 26 |

27 | Read the full documentation at the website 28 |

29 | 30 | --- 31 | 32 | # Introduction 33 | 34 | Concurrent Tasks mimics a queue by using JavaScript's inbuilt array data type. Each task is a function which signals completion back to the runner. 35 | 36 | The minimalism of Concurrent Tasks makes it an easy-to-use solution across any framework or flavour of JavaScript. It has **ZERO dependencies** and can be used virtually in any scenario. With a **minified and gzipped size of 2.7kB**, it is the ultimate lightweight tool for your concurrency needs. 37 | 38 | - [x] Vanilla JavaScript 39 | - [x] Frontend Frameworks (React, Vue, Angular, etc) 40 | - [x] Backend Frameworks (Express, Hapi, Koa, etc) 41 | - [x] NPM Module 42 | - [x] Node CLI Application 43 | 44 | # Installation 45 | 46 | ### Node 47 | 48 | ```bash 49 | # NPM 50 | npm i concurrent-tasks 51 | 52 | # Yarn 53 | yarn add concurrent-tasks 54 | 55 | # PNPM 56 | pnpm i concurrent-tasks 57 | ``` 58 | 59 | ### Browser 60 | 61 | ```html 62 | 66 | ``` 67 | 68 | ### Bun 69 | 70 | ```bash 71 | bun install concurrent-tasks 72 | ``` 73 | 74 | ### Deno 75 | 76 | ```ts 77 | import { TaskRunner } from "https://cdn.jsdelivr.net/npm/concurrent-tasks/src/index.ts"; 78 | ``` 79 | 80 | # Usage 81 | 82 | > **Important:** Each task passed to the task runner, necessarily has to call the done function. If not, your queue won't process properly. 83 | 84 | ```typescript 85 | import TaskRunner from "concurrent-tasks"; 86 | 87 | const runner = new TaskRunner(); 88 | 89 | function generateTasks() { 90 | const tasks = []; 91 | let count = 1000; 92 | while (count) { 93 | tasks.push((done) => { 94 | setTimeout(() => { 95 | done(); 96 | }, Math.random() * 1000); 97 | }); 98 | count--; 99 | } 100 | return tasks; 101 | } 102 | 103 | runner.addMultiple(generateTasks()); 104 | 105 | runner.start(); 106 | ``` 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "concurrent-tasks", 3 | "version": "3.0.2", 4 | "description": "A simple task runner which will run tasks concurrently while maintaining limits.", 5 | "author": "Samrith Shankar", 6 | "license": "MIT", 7 | "homepage": "https://concurrent-tasks.samrith.dev", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/samrith-s/concurrent-tasks.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/samrith-s/concurrent-tasks/issues/new/choose" 14 | }, 15 | "main": "lib/cjs/index.js", 16 | "module": "lib/es/index.js", 17 | "types": "lib/dts/src/index.d.ts", 18 | "source": "src/index.ts", 19 | "react-native": "src/index.ts", 20 | "files": [ 21 | "lib", 22 | "LICENSE", 23 | "README", 24 | "src", 25 | "!src/__tests__" 26 | ], 27 | "publishConfig": { 28 | "access": "public", 29 | "registry": "https://registry.npmjs.org" 30 | }, 31 | "engines": { 32 | "node": ">=18.0.0" 33 | }, 34 | "scripts": { 35 | "bootstrap": "yarn install && yarn --cwd docs install", 36 | "prepublishOnly": "yarn build", 37 | "prebuild": "yarn clean", 38 | "build": "NODE_ENV=production rollup -c rollup.config.js --bundleConfigAsCjs && size-limit", 39 | "clean": "rm -rf lib/", 40 | "test": "jest", 41 | "lint": "eslint src/**/*.ts --cache", 42 | "lint:docs": "eslint docs/**/*.ts --cache", 43 | "prettify": "prettier --write {src,tests,docs}/**/*", 44 | "docs": "yarn --cwd docs", 45 | "release": "vite-node scripts/release", 46 | "bench": "vite-node bench/benchmarks.ts" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "^18.4.3", 50 | "@commitlint/config-conventional": "^18.4.3", 51 | "@release-it/conventional-changelog": "^8.0.1", 52 | "@rollup/plugin-commonjs": "^25.0.7", 53 | "@rollup/plugin-json": "^6.0.1", 54 | "@rollup/plugin-node-resolve": "^15.2.3", 55 | "@rollup/plugin-strip": "^3.0.4", 56 | "@rollup/plugin-terser": "^0.4.4", 57 | "@size-limit/preset-small-lib": "^11.0.1", 58 | "@types/jest": "^29.5.11", 59 | "@types/node": "^20.10.4", 60 | "@typescript-eslint/eslint-plugin": "^6.13.2", 61 | "@typescript-eslint/parser": "^6.13.2", 62 | "builtin-modules": "^3.3.0", 63 | "eslint": "^8.55.0", 64 | "eslint-config-prettier": "^9.1.0", 65 | "eslint-import-resolver-typescript": "^3.6.1", 66 | "eslint-plugin-import": "^2.29.0", 67 | "eslint-plugin-prettier": "^4.2.1", 68 | "jest": "^29.7.0", 69 | "lefthook": "^1.8.2", 70 | "prettier": "^3.1.0", 71 | "release-it": "^15.7.0", 72 | "rollup": "^4.6.1", 73 | "rollup-plugin-typescript2": "^0.36.0", 74 | "size-limit": "^11.0.1", 75 | "ts-jest": "^29.1.1", 76 | "ts-node": "^10.9.2", 77 | "typescript": "^5.3.3", 78 | "vite-node": "^2.1.4", 79 | "yocto-spinner": "^0.1.1" 80 | }, 81 | "keywords": [ 82 | "concurrent", 83 | "tasks", 84 | "processes", 85 | "threads", 86 | "tasks", 87 | "task", 88 | "worker", 89 | "process", 90 | "run", 91 | "concurrent tasks", 92 | "react concurrent task", 93 | "priority", 94 | "queue", 95 | "priority queue", 96 | "fifo", 97 | "scheduler", 98 | "schedule task" 99 | ], 100 | "packageManager": "yarn@4.9.4" 101 | } 102 | -------------------------------------------------------------------------------- /src/Interface.ts: -------------------------------------------------------------------------------- 1 | import type { Task, Tasks } from "./Task"; 2 | 3 | export type Readonly = { 4 | readonly [K in keyof T]: T[K]; 5 | }; 6 | 7 | export type Maybe = T | undefined; 8 | 9 | export type TaskID = number | string; 10 | export type Done = (result?: T) => void; 11 | export type TaskReturn = T | void; 12 | 13 | export type TaskWithDone = ( 14 | done: Done, 15 | id: TaskID 16 | ) => TaskReturn | Promise>; 17 | 18 | export type TasksWithDone = TaskWithDone[]; 19 | 20 | export type TasksList = { 21 | readonly running: Tasks; 22 | readonly pending: Tasks; 23 | readonly completed: Tasks; 24 | }; 25 | 26 | export type TasksCount = { 27 | total: number; 28 | completed: number; 29 | pending: number; 30 | running: number; 31 | }; 32 | 33 | export enum TaskStatus { 34 | PENDING = "pending", 35 | RUNNING = "running", 36 | CANCELLED = "cancelled", 37 | DONE = "done", 38 | } 39 | 40 | export enum RemovalMethods { 41 | ALL = "all", 42 | BY_INDEX = "by-index", 43 | RANGE = "range", 44 | FIRST = "first", 45 | LAST = "last", 46 | } 47 | 48 | export enum AdditionMethods { 49 | FIRST = "first", 50 | LAST = "last", 51 | AT_INDEX = "at-index", 52 | MULTIPLE_FIRST = "multiple-first", 53 | MULTIPLE_LAST = "multiple-range", 54 | } 55 | 56 | export type RunnerDuration = { 57 | start: number; 58 | end: number; 59 | total: number; 60 | }; 61 | 62 | export type HookDefaults = { 63 | tasks: TasksList; 64 | count: TasksCount; 65 | duration: RunnerDuration 66 | }; 67 | 68 | type HookFn< 69 | T = any, 70 | Data extends Record = Record 71 | > = ( 72 | args: Data extends Record 73 | ? Readonly> 74 | : Readonly & Data> 75 | ) => void; 76 | 77 | export type OnStart = HookFn; 78 | 79 | export type OnPause = HookFn; 80 | 81 | export type onDestroy = HookFn; 82 | 83 | export type OnAdd = HookFn< 84 | T, 85 | { 86 | method: AdditionMethods; 87 | } 88 | >; 89 | 90 | export type OnRemove = HookFn< 91 | T, 92 | { method: RemovalMethods; removedTasks: Tasks } 93 | >; 94 | 95 | export type OnRun = HookFn< 96 | T, 97 | { 98 | task: Task; 99 | } 100 | >; 101 | 102 | export type OnDone = HookFn< 103 | T, 104 | { 105 | task: Task; 106 | result?: T; 107 | } 108 | >; 109 | 110 | export type OnEnd = HookFn; 111 | 112 | export enum RunnerEvents { 113 | START = "onStart", 114 | PAUSE = "onPause", 115 | DESTROY = "onDestroy", 116 | ADD = "onAdd", 117 | REMOVE = "onRemove", 118 | RUN = "onRun", 119 | DONE = "onDone", 120 | END = "onEnd", 121 | } 122 | 123 | export type RunnerHooks = { 124 | [RunnerEvents.START]: OnStart; 125 | [RunnerEvents.PAUSE]: OnPause; 126 | [RunnerEvents.DESTROY]: onDestroy; 127 | [RunnerEvents.ADD]: OnAdd; 128 | [RunnerEvents.REMOVE]: OnRemove; 129 | [RunnerEvents.RUN]: OnRun; 130 | [RunnerEvents.DONE]: OnDone; 131 | [RunnerEvents.END]: OnEnd; 132 | }; 133 | 134 | export type RunnerDefaultOptions = RunnerHooks & { 135 | concurrency: number; 136 | name: string | (() => string); 137 | }; 138 | 139 | export type RunnerOptions = { 140 | [K in keyof RunnerDefaultOptions]: RunnerDefaultOptions[K]; 141 | }; 142 | -------------------------------------------------------------------------------- /src/List.ts: -------------------------------------------------------------------------------- 1 | import { Maybe, TaskID } from "./Interface"; 2 | import { Task, Tasks } from "./Task"; 3 | import { isNumber } from "./Utils"; 4 | 5 | export class List { 6 | private list: Tasks; 7 | 8 | private _length = 0; 9 | 10 | constructor(list: Tasks = []) { 11 | this.list = list; 12 | this._length = list.length; 13 | } 14 | 15 | private indexBound(index: number): boolean { 16 | return !index || (index > -1 && index < this._length); 17 | } 18 | 19 | private throwRangeError(index: number): undefined { 20 | throw new RangeError( 21 | `Index out of bounds. Expected index to be within range [0..${ 22 | this._length - 1 23 | }]. Found ${index} instead.` 24 | ); 25 | } 26 | 27 | get size() { 28 | return this._length; 29 | } 30 | 31 | public entries(): Tasks { 32 | return [...this.list]; 33 | } 34 | 35 | public first(): Maybe> { 36 | return this.list.at(0); 37 | } 38 | 39 | public last(): Maybe> { 40 | return this.list.at(-1); 41 | } 42 | 43 | public at(index: number): Maybe> { 44 | if (this.indexBound(index)) { 45 | return this.list[index]; 46 | } 47 | 48 | return this.throwRangeError(index); 49 | } 50 | 51 | public range(index: number, count?: number): Maybe> { 52 | if (this.indexBound(index)) { 53 | const end = isNumber(count) ? count + 1 : undefined; 54 | return this.list.slice(index, end); 55 | } 56 | 57 | return this.throwRangeError(index); 58 | } 59 | 60 | public concat(list: Tasks, prepend?: boolean): Tasks { 61 | if (prepend) { 62 | this.list = list.concat(this.list); 63 | } else { 64 | this.list = this.list.concat(list); 65 | } 66 | 67 | this._length = this.list.length; 68 | 69 | return this.entries(); 70 | } 71 | 72 | public add(item: Task): number { 73 | this._length++; 74 | return this.list.push(item); 75 | } 76 | 77 | public addFirst(item: Task): number { 78 | this._length++; 79 | return this.list.unshift(item); 80 | } 81 | 82 | public addAt(index: number, item: Task): Maybe { 83 | if (this.indexBound(index)) { 84 | this._length++; 85 | this.list.splice(index, 0, item); 86 | return index; 87 | } 88 | 89 | return this.throwRangeError(index); 90 | } 91 | 92 | public remove(): Maybe> { 93 | if (this._length > 0) { 94 | this._length--; 95 | return this.list.pop() as Task; 96 | } 97 | 98 | return void 0; 99 | } 100 | 101 | public removeFirst(): Maybe> { 102 | if (this._length > 0) { 103 | this._length--; 104 | return this.list.shift() as Task; 105 | } 106 | 107 | return void 0; 108 | } 109 | 110 | public removeAt(index: number): Maybe> { 111 | if (this.indexBound(index)) { 112 | this._length--; 113 | return this.list.splice(index, 1)[0] as Task; 114 | } 115 | 116 | return this.throwRangeError(index); 117 | } 118 | 119 | public removeRange(index: number, count?: number): Maybe> { 120 | if (this.indexBound(index)) { 121 | const removed = this.list.splice(index, count); 122 | this._length -= removed.length; 123 | 124 | return removed; 125 | } 126 | 127 | return this.throwRangeError(index); 128 | } 129 | 130 | public removeById(id: TaskID): Maybe> { 131 | const index = this.list.findIndex((task) => task.id === id); 132 | 133 | if (index > -1) { 134 | this._length--; 135 | return this.list.splice(index, 1).at(0); 136 | } 137 | 138 | return void 0; 139 | } 140 | 141 | public clear(): Tasks { 142 | const removedTasks = this.list.slice(); 143 | this._length = 0; 144 | this.list = []; 145 | 146 | return removedTasks; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [3.0.3](https://github.com/samrith-s/concurrent-tasks/compare/v3.0.2...v3.0.3) (2025-09-15) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * move the busy flag check to enable starting synchronization queue ([7fe66ae](https://github.com/samrith-s/concurrent-tasks/commit/7fe66ae1c5679888b63fd384f4a6f9ce7f232089)), closes [#56](https://github.com/samrith-s/concurrent-tasks/issues/56) 9 | 10 | ## [3.0.2](https://github.com/samrith-s/concurrent-tasks/compare/v3.0.1...v3.0.2) (2024-11-13) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * update `homepage` field in `package.json` ([da4ee4a](https://github.com/samrith-s/concurrent-tasks/commit/da4ee4ac2a0e17469dc4907d6fc8b8ab5efc1164)) 16 | 17 | # [3.0.0](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2023-12-10) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * `onStart` firing whenever runner is paused/unpaused ([b146cab](https://github.com/samrith-s/concurrent-tasks/commit/b146cab2531a544d650878d3fddaff2cfed2fd85)) 23 | * prettify files ([cc469a7](https://github.com/samrith-s/concurrent-tasks/commit/cc469a78ad2a2579165f0d5b0af6ee0e825a78c7)) 24 | * throw respective errors if concurrency and task type are not respected ([34e1cd8](https://github.com/samrith-s/concurrent-tasks/commit/34e1cd87dbe75efda112a06d4ac36919463344fd)) 25 | 26 | 27 | ### Features 28 | 29 | * add `List` data structure and simplify usage ([a388adc](https://github.com/samrith-s/concurrent-tasks/commit/a388adc80282c8a9deec585da89e2189fb5ad61f)) 30 | * add `pause` method, property and hooks ([1140e82](https://github.com/samrith-s/concurrent-tasks/commit/1140e826dddcf70454d7afb501ab54faefebd2e3)) 31 | * add `removeAt` method ([a7c68fd](https://github.com/samrith-s/concurrent-tasks/commit/a7c68fd28164a9b5061a91bac80e6297cd4d70d2)) 32 | * convert everything to typescript ([7b7dfa9](https://github.com/samrith-s/concurrent-tasks/commit/7b7dfa9ef285602767e7244c2818f5e4827e0c4d)) 33 | * implement basic runner working and cleanup ([c4561f4](https://github.com/samrith-s/concurrent-tasks/commit/c4561f4f3d3d6a470dd0e10c1ce781ca2ef21a80)) 34 | * support `prepend` in `add` and throw type errors when mismatch ([38ba6b6](https://github.com/samrith-s/concurrent-tasks/commit/38ba6b68824af7a042c0a15645b21b8e4f772ed3)) 35 | 36 | 37 | 38 | ## [1.0.7](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2020-09-27) 39 | 40 | 41 | 42 | ## [1.0.6](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2019-07-16) 43 | 44 | 45 | 46 | ## [1.0.5](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2019-02-05) 47 | 48 | 49 | 50 | ## [1.0.4](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2019-02-05) 51 | 52 | 53 | 54 | ## [1.0.3](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-16) 55 | 56 | 57 | 58 | ## [1.0.2](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-16) 59 | 60 | 61 | 62 | ## [1.0.1](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-06) 63 | 64 | 65 | 66 | # [1.0.0](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 67 | 68 | 69 | 70 | ## [0.0.11](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 71 | 72 | 73 | 74 | ## [0.0.10](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 75 | 76 | 77 | 78 | ## [0.0.9](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 79 | 80 | 81 | 82 | ## [0.0.8](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 83 | 84 | 85 | 86 | ## [0.0.7](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 87 | 88 | 89 | 90 | ## [0.0.6](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 91 | 92 | 93 | 94 | ## [0.0.5](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 95 | 96 | 97 | 98 | ## [0.0.4](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 99 | 100 | 101 | 102 | ## [0.0.3](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-03) 103 | 104 | 105 | 106 | ## [0.0.2](https://github.com/samrith-s/concurrent-tasks/compare/v2.1.0-beta.0...v1.0.7) (2018-12-01) 107 | -------------------------------------------------------------------------------- /src/__tests__/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assignFunction, 3 | assignNumber, 4 | isArray, 5 | isEmptyString, 6 | isFunction, 7 | isNumber, 8 | isString, 9 | } from "../Utils"; 10 | 11 | describe("Utils", () => { 12 | describe("isFunction", () => { 13 | it("should return `true` if item is a function", () => { 14 | expect(isFunction(() => void 0)).toBeTruthy(); 15 | expect( 16 | isFunction(function () { 17 | void 0; 18 | }) 19 | ).toBeTruthy(); 20 | }); 21 | 22 | it("should return `false` if item is not a function", () => { 23 | expect(isFunction(true)).toBeFalsy(); 24 | expect(isFunction(100)).toBeFalsy(); 25 | expect(isFunction("foo")).toBeFalsy(); 26 | expect(isFunction({})).toBeFalsy(); 27 | expect(isFunction([])).toBeFalsy(); 28 | }); 29 | }); 30 | 31 | describe("isNumber", () => { 32 | it("should return `true` if item is a number", () => { 33 | expect(isNumber(1)).toBeTruthy(); 34 | expect(isNumber(10.0)).toBeTruthy(); 35 | }); 36 | 37 | it("should return `false` if item is not a number or is NaN", () => { 38 | expect(isNumber(NaN)).toBeFalsy(); 39 | expect(isNumber(true)).toBeFalsy(); 40 | expect(isNumber("foo")).toBeFalsy(); 41 | expect(isNumber({})).toBeFalsy(); 42 | expect(isNumber([])).toBeFalsy(); 43 | expect(isNumber(() => void 0)).toBeFalsy(); 44 | }); 45 | }); 46 | 47 | describe("isString", () => { 48 | it("should return `true` if item is a string", () => { 49 | expect(isString("hello")).toBeTruthy(); 50 | expect(isString("")).toBeTruthy(); 51 | expect(isString(String("hello"))).toBeTruthy(); 52 | }); 53 | 54 | it("should return `false` if item is not a string", () => { 55 | expect(isString(true)).toBeFalsy(); 56 | expect(isString(100)).toBeFalsy(); 57 | expect(isString({})).toBeFalsy(); 58 | expect(isString([])).toBeFalsy(); 59 | expect(isString(() => void 0)).toBeFalsy(); 60 | }); 61 | }); 62 | 63 | describe("isArray", () => { 64 | it("should return `true` if item is an array", () => { 65 | expect(isArray([])).toBeTruthy(); 66 | expect(isArray(new Array(10))).toBeTruthy(); 67 | expect(isArray(Array.from({ length: 10 }))).toBeTruthy(); 68 | expect(isArray(Array.from([1, 2, 3, 4, 5]))).toBeTruthy(); 69 | }); 70 | 71 | it("should return `false` if item is not an array", () => { 72 | expect(isArray(true)).toBeFalsy(); 73 | expect(isArray(100)).toBeFalsy(); 74 | expect(isArray("foo")).toBeFalsy(); 75 | expect(isArray({})).toBeFalsy(); 76 | expect(isArray(() => void 0)).toBeFalsy(); 77 | }); 78 | }); 79 | 80 | describe("isEmptyString", () => { 81 | it("should return `true` if item is an empty string", () => { 82 | expect(isEmptyString("")).toBeTruthy(); 83 | expect(isEmptyString(String())).toBeTruthy(); 84 | }); 85 | 86 | it("should return `false` if item is not an empty string", () => { 87 | expect(isEmptyString(true)).toBeFalsy(); 88 | expect(isEmptyString(100)).toBeFalsy(); 89 | expect(isEmptyString("foo")).toBeFalsy(); 90 | expect(isEmptyString({})).toBeFalsy(); 91 | expect(isEmptyString([])).toBeFalsy(); 92 | expect(isEmptyString(() => void 0)).toBeFalsy(); 93 | }); 94 | }); 95 | 96 | describe("assignFunction", () => { 97 | it("should assign if item is a function", () => { 98 | const fn = jest.fn(); 99 | expect(assignFunction(fn)).toBe(fn); 100 | }); 101 | 102 | it("should not assign if item is not a function", () => { 103 | expect(assignFunction(true)).toBeUndefined(); 104 | expect(assignFunction(100)).toBeUndefined(); 105 | expect(assignFunction("foo")).toBeUndefined(); 106 | expect(assignFunction({})).toBeUndefined(); 107 | expect(assignFunction([])).toBeUndefined(); 108 | }); 109 | }); 110 | 111 | describe("assignNumber", () => { 112 | it("should assign the number if it is >0 and { 113 | expect(assignNumber(10, 100, 1)).toBe(10); 114 | expect(assignNumber(5, 100, 1)).toBe(5); 115 | expect(assignNumber(2, 100, 1)).toBe(2); 116 | expect(assignNumber(2, 100, 1)).toBe(2); 117 | }); 118 | 119 | it("should assign the list length if the number is 0 or Infinity", () => { 120 | expect(assignNumber(0, 100, 1)).toBe(1); 121 | expect(assignNumber(Infinity, 100, 1)).toBe(1); 122 | }); 123 | 124 | it("should assign the default value if the value is not a number", () => { 125 | expect(assignNumber("hello" as any, 100, 1)).toBe(100); 126 | expect(assignNumber(true as any, 100, 1)).toBe(100); 127 | expect(assignNumber([] as any, 100, 1)).toBe(100); 128 | expect(assignNumber({} as any, 100, 1)).toBe(100); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/methods.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Methods 3 | description: List of methods provided by Concurrent Task runner instance. 4 | sidebar: 5 | order: 2 6 | --- 7 | 8 | import References from "../../../components/References.astro"; 9 | 10 | ##### start 11 | 12 | ```ts 13 | start(): boolean 14 | ``` 15 | 16 | - Start the runner and begin execution of tasks 17 | - Returns `false` if the runner is busy or destroyed 18 | - The [`start`](#start) method can be used to unpause the runner 19 | 20 | --- 21 | 22 | ##### pause 23 | 24 | ```ts 25 | pause(): boolean 26 | ``` 27 | 28 | :::note 29 | 30 | The `pause` event is fired once all the running tasks are done, to ensure the duration and task list reported are correct. 31 | 32 | ::: 33 | 34 | - Pauses the runner instance 35 | - Running tasks are unaffected 36 | - Once all running tasks are done, the runner will be marked as idle 37 | - Returns `false` if the runner is destroyed 38 | - The [`start`](#start) method can be used to unpause the runner 39 | 40 | --- 41 | 42 | ##### destroy 43 | 44 | ```ts 45 | destroy(): void 46 | ``` 47 | 48 | :::caution 49 | 50 | - Destroyed instances cannot be started 51 | - Any add operations on tasks will fail silently 52 | 53 | ::: 54 | 55 | :::note 56 | 57 | The `destroy` event is fired once all the running tasks are done, to ensure the duration and task list reported are correct. 58 | 59 | ::: 60 | 61 | - Destroys the runner instance 62 | - Running tasks are unaffected 63 | - Once all running tasks are done, the runner will be marked as idle 64 | 65 | --- 66 | 67 | ##### setConcurrency 68 | 69 | ```ts 70 | setConcurrency(concurrency: number): void 71 | ``` 72 | 73 | - Sets the concurrency of the runner 74 | - Concurrency should be a positive integer, or `-1` 75 | - Setting concurrency as `-1` would run all tasks at once 76 | - If concurrency is not bound, throws a `RangeError` 77 | 78 | --- 79 | 80 | ##### add 81 | 82 | ```ts 83 | add(task: TaskWithDone, prepend?: boolean): void 84 | ``` 85 | 86 | [`TaskWithDone`][TaskWithDone] 87 | 88 | - Add a task to end of the pending list 89 | - Passing `true` as the second argument will add the task to the beginning of the pending list 90 | 91 | --- 92 | 93 | ##### addFirst 94 | 95 | ```ts 96 | addFirst(task: TaskWithDone): void 97 | ``` 98 | 99 | [`TaskWithDone`][TaskWithDone] 100 | 101 | - Add a task to the beginning of the pending list 102 | - Alias for `add` with the second argument as `true` 103 | 104 | --- 105 | 106 | ##### addAt 107 | 108 | ```ts 109 | addAt(index: number, task: TaskWithDone): void 110 | ``` 111 | 112 | [`TaskWithDone`][TaskWithDone] 113 | 114 | - Add a task at a particular index to the pending list 115 | - Index should always be bound, otherwise throws a `RangeError` 116 | 117 | --- 118 | 119 | ##### addMultiple 120 | 121 | ```ts 122 | addMultiple(tasks: TasksWithDone, prepend?: boolean): void 123 | ``` 124 | 125 | [`TasksWithDone`][TasksWithDone] 126 | 127 | - Add multiple tasks to end of the pending list 128 | - Passing `true` as the second argument will add the tasks to the beginning of the pending list 129 | 130 | --- 131 | 132 | ##### addMultipleFirst 133 | 134 | ```ts 135 | addMultipleFirst(tasks: TasksWithDone): void 136 | ``` 137 | 138 | [`TasksWithDone`][TasksWithDone] 139 | 140 | - Add multiple tasks to the beginning of the pending list 141 | - Alias for `addMultiple` with the second argument as `true` 142 | 143 | --- 144 | 145 | ##### remove 146 | 147 | ```ts 148 | remove(first?: boolean): void 149 | ``` 150 | 151 | - Remove the task at the end of the pending list 152 | - Passing `true` as the first argument will remove the task at the beginning of the pending list 153 | 154 | --- 155 | 156 | ##### removeFirst 157 | 158 | ```ts 159 | removeFirst(): void 160 | ``` 161 | 162 | - Remove the task at the beginning of the pending list 163 | - Alias for `remove` with the first argument as `true` 164 | 165 | --- 166 | 167 | ##### removeAt 168 | 169 | ```ts 170 | removeAt(index: number): void 171 | ``` 172 | 173 | - Remove a task at a particular index 174 | - Index should always be bound, otherwise throws a `RangeError` 175 | 176 | --- 177 | 178 | ##### removeRange 179 | 180 | ```ts 181 | removeRange(start: number, count: number): void 182 | ``` 183 | 184 | - Remove tasks at a particular range from the pending list 185 | - The count is inclusive of the starting index 186 | - Starting index should always be bound, otherwise throws a `RangeError` 187 | 188 | --- 189 | 190 | ##### removeAll 191 | 192 | ```ts 193 | removeAll(): void 194 | ``` 195 | 196 | - Remove all tasks from the pending list 197 | 198 | [TasksList]: /reference/types-and-enums#taskslist 199 | [TasksCount]: /reference/types-and-enums#taskscount 200 | [TaskWithDone]: /reference/types-and-enums#taskwithdone 201 | [TasksWithDone]: /reference/types-and-enums#taskswithdone 202 | [Done]: /reference/types-and-enums#done 203 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/configuration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | description: List of configurations provided by Concurrent Tasks runner instance. 4 | slug: configuration 5 | sidebar: 6 | order: 1 7 | prev: false 8 | --- 9 | 10 | import Configuration from "../../../components/Configuration.astro"; 11 | import References from "../../../components/References.astro"; 12 | 13 | ## Options 14 | 15 | ##### name 16 | 17 | 18 | 19 | - A unique name to identify the runner instance 20 | 21 | ##### concurrency 22 | 23 | 24 | 25 | - Sets the concurrency of the runner 26 | - Concurrency should be a positive integer, or `-1` 27 | - Setting concurrency as `-1` would run all tasks at once 28 | - If concurrency is not bound, throws a `RangeError` 29 | 30 | --- 31 | 32 | ## Hooks 33 | 34 | ##### onStart 35 | 36 | 37 | 38 | - Fired whenever the runner goes from idle/paused to working state 39 | 40 | ```ts title="Signature" frame="terminal" 41 | OnStart = ({ 42 | tasks, 43 | count, 44 | duration, 45 | }: { 46 | tasks: TasksList; 47 | count: TasksCount; 48 | duration: RunnerDuration 49 | }) => void; 50 | ``` 51 | 52 | 53 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 54 | [`RunnerDuration`][RunnerDuration] 55 | 56 | 57 | ##### onPause 58 | 59 | 60 | 61 | - Fired whenever the runner goes from working to paused state 62 | 63 | ```ts title="Signature" frame="terminal" 64 | OnPause = ({ 65 | tasks, 66 | count, 67 | duration, 68 | }: { 69 | tasks: TasksList; 70 | count: TasksCount; 71 | duration: RunnerDuration 72 | }) => void; 73 | ``` 74 | 75 | 76 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 77 | [`RunnerDuration`][RunnerDuration] 78 | 79 | 80 | ##### onDestroy 81 | 82 | 83 | 84 | - Fired whenever the runner is destroyed 85 | 86 | ```ts title="Signature" frame="terminal" 87 | OnDestroy = ({ 88 | tasks, 89 | count, 90 | duration, 91 | }: { 92 | tasks: TasksList; 93 | count: TasksCount; 94 | duration: RunnerDuration 95 | }) => void; 96 | ``` 97 | 98 | 99 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 100 | [`RunnerDuration`][RunnerDuration] 101 | 102 | 103 | ##### onAdd 104 | 105 | 106 | 107 | - Fired every time a task/tasks are added to the queue 108 | 109 | ```ts title="Signature" frame="terminal" 110 | OnAdd = ({ 111 | tasks, 112 | count, 113 | duration, 114 | method, 115 | }: { 116 | tasks: TasksList; 117 | count: TasksCount; 118 | duration: RunnerDuration; 119 | method: AdditionMethods; 120 | }) => void; 121 | ``` 122 | 123 | 124 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 125 | [`RunnerDuration`][RunnerDuration], [`AdditionMethods`][AdditionMethods] 126 | 127 | 128 | ##### onRun 129 | 130 | 131 | 132 | - Fired every time a task is picked up for execution 133 | 134 | ```ts title="Signature" frame="terminal" 135 | OnRun = ({ 136 | tasks, 137 | count, 138 | duration, 139 | task, 140 | }: { 141 | tasks: TasksList; 142 | count: TasksCount; 143 | duration: RunnerDuration; 144 | task: Task; 145 | }) => void; 146 | ``` 147 | 148 | [`TasksList`][TasksList], [`TasksCount`][TasksCount] 149 | 150 | ##### onDone 151 | 152 | 153 | 154 | :::caution 155 | 156 | - Not calling the `done` function from within your task will result in `onDone` never getting fired. 157 | - This also means that your queue will never proceed if no task in the queue calls the `done` function 158 | 159 | See: [The Done callback](/done-callback) 160 | 161 | ::: 162 | 163 | - Fired after every task's completion 164 | 165 | ```ts title="Signature" frame="terminal" 166 | OnDone = ({ 167 | tasks, 168 | count, 169 | duration, 170 | task, 171 | result, 172 | }: { 173 | tasks: TasksList; 174 | count: TasksCount; 175 | duration: RunnerDuration; 176 | task: Task; 177 | result?: T; 178 | }) => void; 179 | ``` 180 | 181 | 182 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 183 | [`RunnerDuration`][RunnerDuration] 184 | 185 | 186 | ##### onRemove 187 | 188 | 189 | 190 | - Fired every time a task/tasks are removed from the queue 191 | 192 | ```ts title="Signature" frame="terminal" 193 | OnRemove = ({ 194 | tasks, 195 | count, 196 | duration, 197 | method, 198 | removedTasks, 199 | }: { 200 | tasks: TasksList; 201 | count: TasksCount; 202 | duration: RunnerDuration; 203 | method: RemovalMethods; 204 | removedTasks: Task[]; 205 | }) => void; 206 | ``` 207 | 208 | 209 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 210 | [`RunnerDuration`][RunnerDuration], [`RemovalMethods`][RemovalMethods] 211 | 212 | 213 | ##### onEnd 214 | 215 | 216 | 217 | - Fired whenever the runner goes from working to idle state 218 | 219 | ```ts title="Signature" frame="terminal" 220 | OnEnd = ({ 221 | tasks, 222 | count, 223 | duration, 224 | }: { 225 | tasks: TasksList; 226 | count: TasksCount; 227 | duration: RunnerDuration; 228 | }) => void; 229 | ``` 230 | 231 | 232 | [`TasksList`][TasksList], [`TasksCount`][TasksCount], 233 | [`RunnerDuration`][RunnerDuration] 234 | 235 | 236 | [TasksList]: /reference/types-and-enums#taskslist 237 | [TasksCount]: /reference/types-and-enums#taskscount 238 | [AdditionMethods]: /reference/types-and-enums/#additionmethods 239 | [RemovalMethods]: /reference/types-and-enums/#removalmethods 240 | [RunnerDuration]: /reference/types-and-enums/#runnerduration 241 | -------------------------------------------------------------------------------- /src/__tests__/List.test.ts: -------------------------------------------------------------------------------- 1 | import { generateTask } from "../../testing-utils/utils/generate-tasks"; 2 | import { List } from "../List"; 3 | import { Task } from "../Task"; 4 | 5 | function createTask(id?: number): Task { 6 | return new Task(id ?? Math.random(), generateTask()); 7 | } 8 | let list: List; 9 | 10 | beforeEach(() => { 11 | list = new List(); 12 | }); 13 | 14 | afterEach(() => { 15 | list.clear(); 16 | }); 17 | 18 | describe("List", () => { 19 | describe("properties", () => { 20 | describe("size", () => { 21 | it("should print the correct size", () => { 22 | list.add(createTask()); 23 | 24 | expect(list.size).toBe(1); 25 | 26 | list.remove(); 27 | 28 | expect(list.size).toBe(0); 29 | }); 30 | }); 31 | }); 32 | 33 | describe("methods", () => { 34 | describe("entries", () => { 35 | it("should return the correct entries", () => { 36 | const ids = [1, 2, 3]; 37 | 38 | ids.forEach((id) => { 39 | list.add(createTask(id)); 40 | }); 41 | 42 | list.entries().forEach((task, index) => { 43 | expect(task.id).toBe(ids[index]); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("first", () => { 49 | it("should return the first item in the list", () => { 50 | list.add(createTask(0)); 51 | list.add(createTask(1)); 52 | list.add(createTask(2)); 53 | 54 | expect(list.first()?.id).toBe(0); 55 | }); 56 | 57 | it("should return undefined if the list is empty", () => { 58 | expect(list.first()).toBeUndefined(); 59 | }); 60 | }); 61 | 62 | describe("last", () => { 63 | it("should return the last item in the list", () => { 64 | list.add(createTask(0)); 65 | list.add(createTask(1)); 66 | list.add(createTask(2)); 67 | 68 | expect(list.last()?.id).toBe(2); 69 | }); 70 | 71 | it("should return undefined if the list is empty", () => { 72 | expect(list.last()).toBeUndefined(); 73 | }); 74 | }); 75 | 76 | describe("at", () => { 77 | it("should return the item at the index", () => { 78 | list.add(createTask(0)); 79 | list.add(createTask(1)); 80 | list.add(createTask(2)); 81 | 82 | expect(list.at(1)?.id).toBe(1); 83 | }); 84 | 85 | it("should throw an error if index is out of bounds", () => { 86 | list.add(createTask(0)); 87 | 88 | expect(() => list.at(1)).toThrow(RangeError); 89 | }); 90 | }); 91 | 92 | describe("range", () => { 93 | it("should return the range", () => { 94 | list.add(createTask(0)); 95 | list.add(createTask(1)); 96 | list.add(createTask(2)); 97 | list.add(createTask(3)); 98 | 99 | const range = list.range(1, 2); 100 | 101 | expect(range?.length).toBe(2); 102 | 103 | range?.forEach((task, index) => { 104 | expect(task.id).toBe(index + 1); 105 | }); 106 | }); 107 | 108 | it("should return all elements from index to end if no count provided", () => { 109 | list.add(createTask(0)); 110 | list.add(createTask(1)); 111 | list.add(createTask(2)); 112 | list.add(createTask(3)); 113 | list.add(createTask(4)); 114 | list.add(createTask(5)); 115 | 116 | const range = list.range(1); 117 | 118 | expect(range?.length).toBe(5); 119 | 120 | range?.forEach((task, index) => { 121 | expect(task.id).toBe(index + 1); 122 | }); 123 | }); 124 | 125 | it("should throw an error if index is out of bounds", () => { 126 | list.add(createTask(0)); 127 | 128 | expect(() => list.range(1, 2)).toThrow(RangeError); 129 | }); 130 | }); 131 | 132 | describe("add", () => { 133 | it("should add an item to the end of the list", () => { 134 | expect(list.add(createTask(1))).toBe(list.size); 135 | expect(list.at(0)?.id).toBe(1); 136 | }); 137 | }); 138 | 139 | describe("addFirst", () => { 140 | it("should add an item to the start of the list", () => { 141 | list.add(createTask(0)); 142 | list.add(createTask(1)); 143 | list.add(createTask(2)); 144 | 145 | expect(list.addFirst(createTask(3))).toBe(list.size); 146 | expect(list.entries().shift()?.id).toBe(3); 147 | }); 148 | }); 149 | 150 | describe("addAt", () => { 151 | it("should add at a particular index", () => { 152 | list.add(createTask(0)); 153 | list.add(createTask(1)); 154 | list.add(createTask(2)); 155 | 156 | list.addAt(1, createTask(3)); 157 | 158 | expect(list.at(1)?.id).toBe(3); 159 | }); 160 | 161 | it("should throw an error if index is out of bounds", () => { 162 | list.add(createTask(0)); 163 | list.add(createTask(1)); 164 | list.add(createTask(2)); 165 | 166 | expect(() => list.addAt(4, createTask(3))).toThrow(RangeError); 167 | }); 168 | }); 169 | 170 | describe("remove", () => { 171 | it("should remove an item from the end of the list", () => { 172 | list.add(createTask(0)); 173 | list.add(createTask(1)); 174 | 175 | const task = list.remove(); 176 | 177 | expect(list.size).toBe(1); 178 | expect(task?.id).toBe(1); 179 | }); 180 | 181 | it("should return undefined if there are no items", () => { 182 | expect(list.remove()).toBeUndefined(); 183 | }); 184 | }); 185 | 186 | describe("removeFirst", () => { 187 | it("should remove an item from the beginning of the list", () => { 188 | list.add(createTask(0)); 189 | list.add(createTask(1)); 190 | 191 | const task = list.removeFirst(); 192 | 193 | expect(list.size).toBe(1); 194 | expect(task?.id).toBe(0); 195 | }); 196 | 197 | it("should return undefined if there are no items", () => { 198 | expect(list.removeFirst()).toBeUndefined(); 199 | }); 200 | }); 201 | 202 | describe("removeAt", () => { 203 | it("should remove at a particular index", () => { 204 | list.add(createTask(0)); 205 | list.add(createTask(1)); 206 | list.add(createTask(2)); 207 | 208 | const task = list.removeAt(1); 209 | 210 | expect(task?.id).toBe(1); 211 | }); 212 | 213 | it("should throw an error if index is out of bounds", () => { 214 | list.add(createTask(0)); 215 | list.add(createTask(1)); 216 | list.add(createTask(2)); 217 | 218 | expect(() => list.removeAt(4)).toThrow(RangeError); 219 | }); 220 | }); 221 | 222 | describe("removeRange", () => { 223 | it("should remove a particular range", () => { 224 | list.add(createTask(0)); 225 | list.add(createTask(1)); 226 | list.add(createTask(2)); 227 | 228 | const tasks = list.removeRange(1, 2); 229 | 230 | tasks?.forEach((task, index) => { 231 | expect(task.id).toBe(index + 1); 232 | }); 233 | }); 234 | 235 | it("should throw an error if index is out of bounds", () => { 236 | list.add(createTask(0)); 237 | list.add(createTask(1)); 238 | list.add(createTask(2)); 239 | 240 | expect(() => list.removeRange(4, 2)).toThrow(RangeError); 241 | }); 242 | }); 243 | 244 | describe("removeById", () => { 245 | it("should remove a task by id", () => { 246 | list.add(createTask(0)); 247 | list.add(createTask(1)); 248 | list.add(createTask(2)); 249 | 250 | const task = list.removeById(1); 251 | 252 | expect(task?.id).toBe(1); 253 | expect(list.size).toBe(2); 254 | }); 255 | 256 | it("should return undefined if a task by that id does not exist", () => { 257 | expect(list.removeById(1)).toBeUndefined(); 258 | }); 259 | }); 260 | 261 | describe("clear", () => { 262 | it("should clear the list", () => { 263 | list.clear(); 264 | 265 | expect(list.size).toBe(0); 266 | }); 267 | }); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /src/TaskRunner.ts: -------------------------------------------------------------------------------- 1 | import { DefaultOptions } from "./DefaultOptions"; 2 | import { 3 | AdditionMethods, 4 | Done, 5 | HookDefaults, 6 | RemovalMethods, 7 | RunnerDuration, 8 | RunnerEvents, 9 | RunnerHooks, 10 | RunnerOptions, 11 | TasksCount, 12 | TaskStatus, 13 | TasksWithDone, 14 | TaskWithDone, 15 | } from "./Interface"; 16 | import { List } from "./List"; 17 | import { Task, Tasks } from "./Task"; 18 | import { isArray, isFunction } from "./Utils"; 19 | 20 | export class TaskRunner { 21 | static #instances = 0; 22 | 23 | #_busy = false; 24 | #_paused = false; 25 | #_destroyed = false; 26 | 27 | readonly #_pending = new List(); 28 | readonly #_completed = new List(); 29 | readonly #_running = new List(); 30 | readonly #_options: RunnerOptions; 31 | readonly #_duration: RunnerDuration = { 32 | start: 0, 33 | end: 0, 34 | total: 0, 35 | }; 36 | 37 | #_concurrency = 0; 38 | #_taskIds = 0; 39 | 40 | set #concurrency(concurrency: number) { 41 | if (!concurrency || concurrency < -1) { 42 | throw new RangeError( 43 | `Invalid range for concurrency. Range should be a positive integer, or -1. Found ${concurrency} instead` 44 | ); 45 | } 46 | 47 | if (concurrency === -1) { 48 | this.#_concurrency = Infinity; 49 | } else { 50 | this.#_concurrency = concurrency; 51 | } 52 | } 53 | 54 | get #total(): number { 55 | return this.#pending + this.#running + this.#completed; 56 | } 57 | 58 | get #completed(): number { 59 | return this.#_completed.size; 60 | } 61 | 62 | get #pending(): number { 63 | return this.#_pending.size; 64 | } 65 | 66 | get #running(): number { 67 | return this.#_running.size; 68 | } 69 | 70 | #runHook< 71 | Hook extends keyof RunnerHooks, 72 | Data extends Omit< 73 | Parameters[Hook]>[0], 74 | keyof HookDefaults 75 | > extends infer D 76 | ? D extends Record 77 | ? null 78 | : D 79 | : null, 80 | >( 81 | ...[hook, data]: Data extends null ? [hook: Hook] : [hook: Hook, data: Data] 82 | ) { 83 | (this as any)[hook]?.({ 84 | ...(data || {}), 85 | tasks: this.tasks, 86 | count: this.count, 87 | duration: this.duration, 88 | }); 89 | } 90 | 91 | #createTask(task: TaskWithDone): Task { 92 | return new Task(this.#_taskIds++, task); 93 | } 94 | 95 | #done(task: Task): Done { 96 | return ((result: T) => { 97 | task.status = TaskStatus.DONE; 98 | 99 | this.#_running.removeById(task.id); 100 | this.#_completed.add(task); 101 | 102 | this.#runHook(RunnerEvents.DONE, { 103 | task, 104 | result, 105 | }); 106 | 107 | if (!this.#_paused && !this.#_destroyed) { 108 | return this.#run(); 109 | } 110 | 111 | if (!this.#running) { 112 | if (this.#_paused) { 113 | return this.#runHook(RunnerEvents.PAUSE); 114 | } 115 | 116 | if (this.#_destroyed) { 117 | return this.#runHook(RunnerEvents.DESTROY); 118 | } 119 | } 120 | }) as unknown as Done; 121 | } 122 | 123 | #run() { 124 | if (this.#completed !== this.#total) { 125 | if (!this.#_paused && this.#running < this.#_concurrency) { 126 | const difference = this.#_concurrency - this.#running; 127 | 128 | const tasks = this.#_pending.removeRange(0, difference) as Tasks; 129 | 130 | this.#_busy = true; 131 | 132 | tasks.forEach((task) => { 133 | task.status = TaskStatus.RUNNING; 134 | 135 | this.#_running.add(task); 136 | 137 | task.run(this.#done(task)); 138 | 139 | /* istanbul ignore next */ 140 | this.#runHook(RunnerEvents.RUN, { 141 | task, 142 | }); 143 | }); 144 | } 145 | } else { 146 | this.#_duration.end = Date.now(); 147 | this.#_duration.total = Math.ceil( 148 | this.#_duration.end - this.#_duration.start 149 | ); 150 | 151 | /* istanbul ignore next */ 152 | this.#runHook(RunnerEvents.END); 153 | 154 | this.#_busy = false; 155 | } 156 | } 157 | 158 | #provideRemovedTasks(removedTasks: Tasks | Task | void): Tasks { 159 | if (isArray(removedTasks)) { 160 | return removedTasks; 161 | } 162 | 163 | if (removedTasks) { 164 | return [removedTasks]; 165 | } 166 | 167 | /* istanbul ignore next */ 168 | return []; 169 | } 170 | 171 | constructor(options?: Partial>) { 172 | this.#_options = { 173 | ...DefaultOptions(`Runner-${TaskRunner.#instances++}`), 174 | ...options, 175 | }; 176 | 177 | this.#concurrency = this.#_options.concurrency; 178 | 179 | Object.values(RunnerEvents).forEach((value) => { 180 | (this as any)[value] = this.#_options[value]; 181 | }); 182 | } 183 | 184 | /** 185 | * Returns the concurrency of the runner. 186 | */ 187 | public get concurrency(): number { 188 | return this.#_concurrency; 189 | } 190 | 191 | /** 192 | * Get whether the runner is busy executing tasks or not. 193 | */ 194 | public get busy(): boolean { 195 | return this.#_busy; 196 | } 197 | 198 | /** 199 | * Get whether the runner is paused or not. 200 | */ 201 | public get paused(): boolean { 202 | return this.#_paused; 203 | } 204 | 205 | /** 206 | * Get whether the runner is destroyed or not. 207 | */ 208 | public get destroyed(): boolean { 209 | return this.#_destroyed; 210 | } 211 | 212 | /** 213 | * Get the counts for the runner 214 | */ 215 | public get count(): TasksCount { 216 | return { 217 | total: this.#total, 218 | completed: this.#completed, 219 | running: this.#running, 220 | pending: this.#pending, 221 | }; 222 | } 223 | 224 | public get duration(): RunnerDuration { 225 | return { 226 | ...this.#_duration, 227 | }; 228 | } 229 | 230 | /** 231 | * Get the list of tasks of the runner. 232 | */ 233 | public get tasks() { 234 | const completed = this.#_completed.entries(); 235 | const pending = this.#_pending.entries(); 236 | const running = this.#_running.entries(); 237 | return { 238 | completed, 239 | pending, 240 | running, 241 | all: [...running, ...pending, ...completed], 242 | }; 243 | } 244 | 245 | /** 246 | * Start task execution. 247 | */ 248 | public start(): boolean { 249 | if (this.#_destroyed) { 250 | return false; 251 | } 252 | 253 | const previousPause = this.#_paused; 254 | 255 | if (previousPause) { 256 | this.#_paused = false; 257 | } 258 | 259 | if (this.#_busy) { 260 | return false; 261 | } 262 | 263 | this.#_duration.start = Date.now(); 264 | 265 | /* istanbul ignore next */ 266 | if (!previousPause) { 267 | this.#runHook(RunnerEvents.START); 268 | } 269 | this.#run(); 270 | 271 | return true; 272 | } 273 | 274 | /** 275 | * Pause task execution. 276 | */ 277 | public pause(): boolean { 278 | if (this.#_destroyed) { 279 | return false; 280 | } 281 | 282 | this.#_paused = true; 283 | 284 | return true; 285 | } 286 | 287 | /** 288 | * Destroy a runner instance. This will ensure that the current instance is marked as dead, and no additional tasks are run. 289 | * 290 | * This does not affect currently running tasks. 291 | */ 292 | public destroy(): void { 293 | this.#_destroyed = true; 294 | 295 | this.#runHook(RunnerEvents.DESTROY); 296 | } 297 | 298 | /** 299 | * Set the concurrency value 300 | */ 301 | public setConcurrency(concurrency: number): void { 302 | if (!this.#_destroyed) { 303 | this.#concurrency = concurrency; 304 | this.#run(); 305 | } 306 | } 307 | 308 | /** 309 | * Add a single task to the end of the list. 310 | * 311 | * ```ts 312 | * console.log(runner.tasks.pending) // [] 313 | * runner.add(t1) 314 | * console.log(runner.tasks.pending) // [t1] 315 | * ``` 316 | */ 317 | public add(task: TaskWithDone, prepend?: boolean): void { 318 | if (!this.#_destroyed) { 319 | if (!isFunction(task) || task instanceof Task) { 320 | throw new TypeError( 321 | "A task cannot be anything but a function, nor an instance of `Task`. Pass a function instead." 322 | ); 323 | } else { 324 | if (prepend) { 325 | this.#_pending.addFirst(this.#createTask(task)); 326 | } else { 327 | this.#_pending.add(this.#createTask(task)); 328 | } 329 | } 330 | 331 | /* istanbul ignore next */ 332 | 333 | this.#runHook(RunnerEvents.ADD, { 334 | method: prepend ? AdditionMethods.FIRST : AdditionMethods.LAST, 335 | }); 336 | } 337 | } 338 | 339 | /** 340 | * Add a single task to the beginning of the list. 341 | * 342 | * ```ts 343 | * console.log(runner.tasks.pending) // [t1, t2, t3] 344 | * runner.addFirst(t4) 345 | * console.log(runner.tasks.pending) // [t4, t1, t2, t3] 346 | * ``` 347 | */ 348 | public addFirst(task: TaskWithDone): void { 349 | this.add(task, true); 350 | } 351 | 352 | /** 353 | * Add a single task at a particular index. 354 | * 355 | * ```ts 356 | * console.log(runner.tasks) // [t1, t2, t3, t4, t5, t6] 357 | * runner.addAt(1, t) 358 | * console.log(runner.tasks.pending) // [t1, t7, t2, t3, t4, t5, t6] 359 | * ``` 360 | */ 361 | public addAt(index: number, task: TaskWithDone): void { 362 | if (!this.#_destroyed) { 363 | this.#_pending.addAt(index, this.#createTask(task)); 364 | 365 | /* istanbul ignore next */ 366 | this.#runHook(RunnerEvents.ADD, { 367 | method: AdditionMethods.AT_INDEX, 368 | }); 369 | } 370 | } 371 | 372 | /** 373 | * Add multiple tasks to the end of the list. 374 | * 375 | * ```ts 376 | * console.log(runner.tasks) // [t1, t2, t3, t4, t5, t6] 377 | * runner.addMultiple([t7, t8, t9, t10, t11, t12]) 378 | * console.log(runner.tasks.pending) // [t1, t2, t4, t5, t6, t7, t8, t9, t10, t11, t12] 379 | * ``` 380 | */ 381 | public addMultiple(tasks: TasksWithDone, prepend?: boolean): void { 382 | if (!this.#_destroyed) { 383 | this.#_pending.concat(tasks.map(this.#createTask.bind(this)), prepend); 384 | 385 | /* istanbul ignore next */ 386 | this.#runHook(RunnerEvents.ADD, { 387 | method: prepend 388 | ? AdditionMethods.MULTIPLE_FIRST 389 | : AdditionMethods.MULTIPLE_LAST, 390 | }); 391 | } 392 | } 393 | 394 | /** 395 | * Add multiple tasks to the beginning of the list. 396 | * 397 | * ```ts 398 | * console.log(runner.tasks) // [t1, t2, t3, t4, t5, t6] 399 | * runner.addMultiple([t7, t8, t9, t10, t11, t12]) 400 | * console.log(runner.tasks.pending) // [t1, t2, t4, t5, t6, t7, t8, t9, t10, t11, t12] 401 | * ``` 402 | */ 403 | public addMultipleFirst(tasks: TasksWithDone): void { 404 | this.addMultiple(tasks, true); 405 | } 406 | 407 | /** 408 | * Remove the last pending task in the list. 409 | * 410 | * ```ts 411 | * runner.addMultiple([t1, t2, t3, t4, t5, t6]) 412 | * runner.remove() 413 | * console.log(runner.tasks.pending) // [t1, t2, t3, t4, t5] 414 | * ``` 415 | */ 416 | public remove(first?: boolean): void { 417 | const removedTasks = first 418 | ? this.#_pending.removeFirst() 419 | : this.#_pending.remove(); 420 | 421 | /* istanbul ignore next */ 422 | this.#runHook(RunnerEvents.REMOVE, { 423 | removedTasks: this.#provideRemovedTasks(removedTasks), 424 | method: first ? RemovalMethods.FIRST : RemovalMethods.LAST, 425 | }); 426 | } 427 | 428 | /** 429 | * Remove the first pending task in the list. 430 | * 431 | * ```ts 432 | * runner.addMultiple([t1, t2, t3, t4, t5, t6]) 433 | * runner.removeFirst() 434 | * console.log(runner.tasks.pending) // [t2, t3, t4, t5, t6] 435 | * ``` 436 | */ 437 | public removeFirst(): void { 438 | this.remove(true); 439 | } 440 | 441 | /** 442 | * Remove a pending task at a particular index. 443 | * 444 | * ```ts 445 | * runner.addMultiple([t1, t2, t3, t4, t5, t6]) 446 | * runner.removeAt(2) 447 | * console.log(runner.tasks.pending) // [t1, t2, t4, t5, t6] 448 | * ``` 449 | */ 450 | public removeAt(index: number): void { 451 | const removedTasks = this.#_pending.removeAt(index); 452 | 453 | /* istanbul ignore next */ 454 | this.#runHook(RunnerEvents.REMOVE, { 455 | removedTasks: this.#provideRemovedTasks(removedTasks), 456 | method: RemovalMethods.BY_INDEX, 457 | }); 458 | } 459 | 460 | /** 461 | * Remove a range of pending tasks. The range is inclusive of the starting index specified. 462 | * 463 | * ```ts 464 | * runner.addMultiple([t1, t2, t3, t4, t5, t6]) 465 | * runner.removeRange(2, 2) 466 | * console.log(runner.tasks.pending) // [t1, t4, t5, t6] 467 | * ``` 468 | */ 469 | public removeRange(start: number, count: number): void { 470 | const removedTasks = this.#_pending.removeRange(start, count); 471 | 472 | /* istanbul ignore next */ 473 | this.#runHook(RunnerEvents.REMOVE, { 474 | removedTasks: this.#provideRemovedTasks(removedTasks), 475 | method: RemovalMethods.RANGE, 476 | }); 477 | } 478 | 479 | /** 480 | * Remove all pending tasks. 481 | * 482 | * ```ts 483 | * runner.addMultiple([t1, t2, t3, t4, t5, t6]) 484 | * runner.removeAll() 485 | * console.log(runner.tasks.pending) // [] 486 | * ``` 487 | */ 488 | public removeAll(): void { 489 | const removedTasks = this.#_pending.clear(); 490 | 491 | /* istanbul ignore next */ 492 | this.#runHook(RunnerEvents.REMOVE, { 493 | removedTasks: this.#provideRemovedTasks(removedTasks), 494 | method: RemovalMethods.ALL, 495 | }); 496 | } 497 | 498 | /** 499 | * Add a listener to any of the supported events of the runner. Only one listener per event can be attached at any time. Adding another will overwrite the existing listener. 500 | */ 501 | public addListener( 502 | event: `${E}`, 503 | listener: E extends RunnerEvents ? RunnerHooks[E] : never 504 | ): void { 505 | if (!this.#_destroyed) { 506 | if (!isFunction(listener)) { 507 | throw new TypeError( 508 | `Invalid listener callback provided. Expected ${event} to be a function, but found ${typeof listener} instead.` 509 | ); 510 | } 511 | 512 | Object.assign(this, { 513 | [event]: listener, 514 | }); 515 | } 516 | } 517 | 518 | /** 519 | * Remove a previously registered listener for an event supported by the runner. 520 | */ 521 | public removeListener}`>(event: E): void { 522 | Object.assign(this, { 523 | [event]: undefined, 524 | }); 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/__tests__/TaskRunner.test.ts: -------------------------------------------------------------------------------- 1 | import { CT, TaskRunner } from ".."; 2 | import { createRunner } from "../../testing-utils/utils/create-runner"; 3 | import { 4 | generateTask, 5 | generateTasks, 6 | } from "../../testing-utils/utils/generate-tasks"; 7 | import { RemovalMethods } from "../Interface"; 8 | 9 | const TASK_COUNT = 10; 10 | 11 | describe("TaskRunner", () => { 12 | describe("constructor", () => { 13 | it("should set the concurrency if it is a positive integer or -1", () => { 14 | const runner = createRunner(); 15 | 16 | runner.setConcurrency(10); 17 | 18 | expect(runner.concurrency).toBe(10); 19 | 20 | runner.setConcurrency(-1); 21 | 22 | expect(runner.concurrency).toBe(Infinity); 23 | }); 24 | 25 | it("should throw an error if concurrency is not in range", () => { 26 | expect( 27 | () => 28 | new TaskRunner({ 29 | concurrency: 0, 30 | }) 31 | ).toThrow(RangeError); 32 | expect( 33 | () => 34 | new TaskRunner({ 35 | concurrency: -2, 36 | }) 37 | ).toThrow(RangeError); 38 | }); 39 | }); 40 | 41 | describe("properties", () => { 42 | describe("concurrency", () => { 43 | it("should return the correct concurrency", () => { 44 | const runner = createRunner({ 45 | taskCount: 0, 46 | concurrency: 10, 47 | }); 48 | 49 | expect(runner.concurrency).toBe(10); 50 | }); 51 | }); 52 | 53 | describe("busy", () => { 54 | it("should show the correct status", () => { 55 | jest.useFakeTimers(); 56 | 57 | const runner = createRunner({ 58 | autoStart: true, 59 | }); 60 | 61 | expect(runner.busy).toBeTruthy(); 62 | 63 | runner.removeAll(); 64 | 65 | jest.advanceTimersByTime(10000); 66 | 67 | expect(runner.busy).toBeFalsy(); 68 | 69 | runner.destroy(); 70 | 71 | jest.clearAllTimers(); 72 | }); 73 | }); 74 | 75 | describe("paused", () => { 76 | it("should show the correct status", () => { 77 | const runner = createRunner({ 78 | autoStart: true, 79 | }); 80 | 81 | expect(runner.paused).toBeFalsy(); 82 | 83 | runner.pause(); 84 | 85 | expect(runner.paused).toBeTruthy(); 86 | 87 | runner.destroy(); 88 | }); 89 | }); 90 | 91 | describe("destroyed", () => { 92 | it("should show the correct status", () => { 93 | const runner = createRunner({ 94 | autoStart: true, 95 | }); 96 | 97 | expect(runner.destroyed).toBeFalsy(); 98 | 99 | runner.destroy(); 100 | 101 | expect(runner.destroyed).toBeTruthy(); 102 | }); 103 | }); 104 | 105 | describe("count", () => { 106 | it("should return the correct counts", () => { 107 | const taskCount = 5; 108 | const runner = createRunner({ 109 | taskCount, 110 | }); 111 | 112 | expect(runner.count).toEqual({ 113 | total: taskCount, 114 | running: 0, 115 | completed: 0, 116 | pending: taskCount, 117 | }); 118 | }); 119 | }); 120 | 121 | describe("tasks", () => { 122 | it("should return the correct tasks", () => { 123 | const tasks = generateTasks(); 124 | 125 | const runner = createRunner({ 126 | taskCount: 0, 127 | }); 128 | 129 | runner.tasks.pending.forEach((task, index) => { 130 | expect(task).toBe(tasks.at(index)); 131 | }); 132 | }); 133 | }); 134 | }); 135 | 136 | describe("methods", () => { 137 | describe("addListener", () => { 138 | it("should add the listener", () => { 139 | const onAdd = jest.fn(); 140 | 141 | const runner = createRunner({ 142 | taskCount: 0, 143 | }); 144 | 145 | runner.addListener(CT.RunnerEvents.ADD, onAdd); 146 | 147 | runner.add(generateTask()); 148 | 149 | expect(onAdd).toHaveBeenCalledTimes(1); 150 | }); 151 | 152 | it("should throw an error if the provided value is not a function", () => { 153 | const runner = createRunner({ 154 | taskCount: 0, 155 | }); 156 | 157 | expect(() => 158 | runner.addListener(CT.RunnerEvents.ADD, "hello" as any) 159 | ).toThrow(TypeError); 160 | }); 161 | }); 162 | 163 | describe("removeListener", () => { 164 | it("should remove the listener", () => { 165 | const onAdd = jest.fn(); 166 | 167 | const runner = createRunner({ 168 | taskCount: 0, 169 | }); 170 | 171 | runner.addListener(CT.RunnerEvents.ADD, onAdd); 172 | runner.add(generateTask()); 173 | runner.removeListener(CT.RunnerEvents.ADD); 174 | runner.add(generateTask()); 175 | 176 | expect(onAdd).toHaveBeenCalledTimes(1); 177 | }); 178 | }); 179 | 180 | describe("setConcurrency", () => { 181 | it("should set the concurrency", () => { 182 | const runner = createRunner({ 183 | taskCount: 0, 184 | concurrency: 10, 185 | }); 186 | 187 | expect(runner.concurrency).toBe(10); 188 | 189 | runner.setConcurrency(3); 190 | 191 | expect(runner.concurrency).toBe(3); 192 | }); 193 | }); 194 | 195 | describe("start", () => { 196 | it("should start the runner", () => { 197 | const runner = createRunner(); 198 | 199 | expect(runner.start()).toBeTruthy(); 200 | expect(runner.count.running).toBe(3); 201 | }); 202 | 203 | it("should not start if the runner is already busy", () => { 204 | const runner = createRunner(); 205 | 206 | expect(runner.start()).toBeTruthy(); 207 | expect(runner.count.running).toBe(3); 208 | expect(runner.start()).toBeFalsy(); 209 | }); 210 | 211 | it("should resume the runner if it is paused", () => { 212 | const runner = createRunner({ 213 | autoStart: true, 214 | }); 215 | 216 | expect(runner.paused).toBeFalsy(); 217 | 218 | runner.pause(); 219 | 220 | expect(runner.paused).toBeTruthy(); 221 | 222 | runner.start(); 223 | 224 | expect(runner.paused).toBeFalsy(); 225 | 226 | runner.destroy(); 227 | }); 228 | 229 | it("should not start if the runner is destroyed", () => { 230 | const runner = createRunner(); 231 | 232 | runner.destroy(); 233 | 234 | expect(runner.start()).toBeFalsy(); 235 | }); 236 | }); 237 | 238 | describe("destroy", () => { 239 | it("should destroy the runner", () => { 240 | const runner = createRunner(); 241 | 242 | runner.destroy(); 243 | 244 | expect(runner.start()).toBeFalsy(); 245 | expect(runner.busy).toBeFalsy(); 246 | }); 247 | }); 248 | 249 | describe("pause", () => { 250 | it("should pause the runner", () => { 251 | const runner = createRunner(); 252 | 253 | runner.pause(); 254 | 255 | expect(runner.paused).toBeTruthy(); 256 | 257 | runner.start(); 258 | 259 | expect(runner.paused).toBeFalsy(); 260 | }); 261 | 262 | it("should return `false` if the runner is destroyed", () => { 263 | const runner = createRunner(); 264 | 265 | expect(runner.pause()).toBeTruthy(); 266 | 267 | runner.destroy(); 268 | 269 | expect(runner.pause()).toBeFalsy(); 270 | }); 271 | }); 272 | 273 | describe("add", () => { 274 | it("should add a task", () => { 275 | const runner = createRunner({ 276 | taskCount: 0, 277 | }); 278 | 279 | runner.add(generateTask()); 280 | 281 | expect(runner.count.pending).toBe(1); 282 | }); 283 | 284 | it("should throw an error if task is an invalid type", () => { 285 | const runner = createRunner(); 286 | 287 | expect(() => runner.add("hello" as any)).toThrow(TypeError); 288 | expect(() => runner.add(new CT.Task(1, console.log) as any)).toThrow( 289 | TypeError 290 | ); 291 | }); 292 | }); 293 | 294 | describe("addFirst", () => { 295 | it("should add to the beginning of the list", () => { 296 | const runner = createRunner({ 297 | taskCount: 5, 298 | }); 299 | 300 | runner.addFirst(console.log); 301 | 302 | expect(runner.tasks.pending.at(0)?.id).toBe(5); 303 | }); 304 | }); 305 | 306 | describe("addAt", () => { 307 | it("should add the task at a particular index", () => { 308 | const runner = createRunner(); 309 | 310 | runner.addAt(3, console.log); 311 | 312 | expect(runner.tasks.pending.at(3)?.id).toBe(10); 313 | }); 314 | }); 315 | 316 | describe("addMultiple", () => { 317 | it("should add tasks to the list", () => { 318 | const runner = createRunner(); 319 | 320 | runner.addMultiple(generateTasks(5)); 321 | 322 | runner.tasks.pending.slice(10).forEach((task, idx) => { 323 | expect(task.id).toBe(idx + 10); 324 | }); 325 | }); 326 | 327 | it("should add tasks to the beginning of the list", () => { 328 | const runner = createRunner({ 329 | taskCount: TASK_COUNT, 330 | }); 331 | 332 | runner.addMultiple(generateTasks(5), true); 333 | 334 | runner.tasks.pending.slice(0, 5).forEach((task, idx) => { 335 | expect(task.id).toBe(TASK_COUNT + idx); 336 | }); 337 | }); 338 | }); 339 | 340 | describe("addMultipleFirst", () => { 341 | it("should add tasks to the beginning of the list", () => { 342 | const runner = createRunner({ 343 | taskCount: TASK_COUNT, 344 | }); 345 | 346 | runner.addMultipleFirst(generateTasks(5)); 347 | 348 | runner.tasks.pending.slice(0, 5).forEach((task, idx) => { 349 | expect(task.id).toBe(TASK_COUNT + idx); 350 | }); 351 | }); 352 | }); 353 | 354 | describe("remove", () => { 355 | it("should remove the task at the end of the list", () => { 356 | const onRemove = jest.fn(); 357 | const runner = createRunner({ onRemove }); 358 | const tasks = runner.tasks; 359 | 360 | expect(runner.tasks.pending.at(-1)?.id).toBe(9); 361 | 362 | runner.remove(); 363 | 364 | expect(runner.tasks.pending.at(-1)?.id).toBe(8); 365 | expect(onRemove).toHaveBeenCalledWith({ 366 | tasks: runner.tasks, 367 | count: runner.count, 368 | duration: runner.duration, 369 | method: RemovalMethods.LAST, 370 | removedTasks: [tasks.all.at(-1)], 371 | }); 372 | }); 373 | }); 374 | 375 | describe("removeFirst", () => { 376 | it("should remove the task at the beginning of the list", () => { 377 | const onRemove = jest.fn(); 378 | const runner = createRunner({ onRemove }); 379 | const tasks = runner.tasks; 380 | 381 | expect(runner.tasks.pending.at(0)?.id).toBe(0); 382 | 383 | runner.removeFirst(); 384 | 385 | expect(runner.tasks.pending.at(0)?.id).toBe(1); 386 | expect(onRemove).toHaveBeenCalledWith({ 387 | tasks: runner.tasks, 388 | count: runner.count, 389 | duration: runner.duration, 390 | method: RemovalMethods.FIRST, 391 | removedTasks: [tasks.all.at(0)], 392 | }); 393 | }); 394 | }); 395 | 396 | describe("removeAt", () => { 397 | it("should remove the task at a particular index", () => { 398 | const onRemove = jest.fn(); 399 | const runner = createRunner({ onRemove }); 400 | const tasks = runner.tasks; 401 | 402 | expect(runner.tasks.pending.at(1)?.id).toBe(1); 403 | 404 | runner.removeAt(1); 405 | 406 | expect(runner.tasks.pending.at(1)?.id).toBe(2); 407 | expect(onRemove).toHaveBeenCalledWith({ 408 | tasks: runner.tasks, 409 | count: runner.count, 410 | duration: runner.duration, 411 | method: RemovalMethods.BY_INDEX, 412 | removedTasks: [tasks.all.at(1)], 413 | }); 414 | }); 415 | }); 416 | 417 | describe("removeRange", () => { 418 | it("should remove tasks from a particular range", () => { 419 | const onRemove = jest.fn(); 420 | const runner = createRunner({ onRemove }); 421 | const tasks = runner.tasks; 422 | 423 | expect(runner.tasks.pending.at(1)?.id).toBe(1); 424 | expect(runner.tasks.pending.at(2)?.id).toBe(2); 425 | 426 | runner.removeRange(1, 2); 427 | 428 | expect(runner.tasks.pending.at(1)?.id).toBe(3); 429 | expect(runner.tasks.pending.at(2)?.id).toBe(4); 430 | expect(onRemove).toHaveBeenCalledWith({ 431 | tasks: runner.tasks, 432 | count: runner.count, 433 | duration: runner.duration, 434 | method: RemovalMethods.RANGE, 435 | removedTasks: tasks.all.slice(1, 3), 436 | }); 437 | }); 438 | }); 439 | 440 | describe("removeAll", () => { 441 | const onRemove = jest.fn(); 442 | const runner = createRunner({ onRemove }); 443 | const tasks = runner.tasks; 444 | 445 | expect(runner.count.pending).toBe(10); 446 | 447 | runner.removeAll(); 448 | 449 | expect(runner.count.pending).toBe(0); 450 | expect(onRemove).toHaveBeenCalledWith({ 451 | tasks: runner.tasks, 452 | count: runner.count, 453 | duration: runner.duration, 454 | method: RemovalMethods.ALL, 455 | removedTasks: tasks.all, 456 | }); 457 | }); 458 | }); 459 | 460 | describe("events", () => { 461 | describe("start", () => { 462 | describe("on", () => { 463 | it("should fire `start` hook when calling `start`", () => { 464 | const runner = createRunner(); 465 | const onStart = jest.fn(); 466 | 467 | expect(onStart).not.toHaveBeenCalled(); 468 | 469 | runner.addListener(CT.RunnerEvents.START, onStart); 470 | runner.start(); 471 | 472 | expect(onStart).toHaveBeenCalled(); 473 | 474 | runner.destroy(); 475 | }); 476 | 477 | it("should not fire `start` hook when going from paused to unpaused state", () => { 478 | jest.useFakeTimers(); 479 | 480 | const onStart = jest.fn(); 481 | const onPause = jest.fn(); 482 | const runner = createRunner(); 483 | 484 | runner.addListener(CT.RunnerEvents.START, onStart); 485 | runner.addListener(CT.RunnerEvents.PAUSE, onPause); 486 | 487 | runner.start(); 488 | 489 | expect(onStart).toHaveBeenCalledTimes(1); 490 | 491 | runner.pause(); 492 | 493 | jest.advanceTimersByTime(10000); 494 | 495 | expect(onPause).toHaveBeenCalledTimes(1); 496 | 497 | runner.start(); 498 | 499 | expect(onStart).toHaveBeenCalledTimes(1); 500 | 501 | runner.destroy(); 502 | }); 503 | }); 504 | 505 | describe("off", () => { 506 | it("should not fire `start` hook when calling `start`", () => { 507 | const runner = createRunner(); 508 | const onStart = jest.fn(); 509 | 510 | expect(onStart).not.toHaveBeenCalled(); 511 | 512 | runner.addListener(CT.RunnerEvents.START, onStart); 513 | runner.removeListener(CT.RunnerEvents.START); 514 | runner.start(); 515 | 516 | expect(onStart).not.toHaveBeenCalled(); 517 | 518 | runner.destroy(); 519 | }); 520 | }); 521 | }); 522 | 523 | describe("pause", () => { 524 | describe("on", () => { 525 | it("should fire `pause` hook when calling `pause`", () => { 526 | jest.useFakeTimers(); 527 | 528 | const runner = createRunner(); 529 | const onPause = jest.fn(); 530 | 531 | expect(onPause).not.toHaveBeenCalled(); 532 | 533 | runner.addListener(CT.RunnerEvents.PAUSE, onPause); 534 | runner.start(); 535 | 536 | runner.pause(); 537 | 538 | jest.advanceTimersByTime(1000); 539 | 540 | expect(onPause).toHaveBeenCalled(); 541 | 542 | runner.destroy(); 543 | 544 | jest.clearAllTimers(); 545 | }); 546 | }); 547 | 548 | describe("off", () => { 549 | it("should not fire `pause` hook when calling `pause`", () => { 550 | jest.useFakeTimers(); 551 | 552 | const runner = createRunner(); 553 | const onPause = jest.fn(); 554 | 555 | expect(onPause).not.toHaveBeenCalled(); 556 | 557 | runner.addListener(CT.RunnerEvents.PAUSE, onPause); 558 | runner.start(); 559 | runner.removeListener(CT.RunnerEvents.PAUSE); 560 | 561 | runner.pause(); 562 | 563 | jest.advanceTimersByTime(1000); 564 | 565 | expect(onPause).not.toHaveBeenCalled(); 566 | 567 | runner.destroy(); 568 | 569 | jest.clearAllTimers(); 570 | }); 571 | }); 572 | }); 573 | 574 | describe("destroy", () => { 575 | describe("on", () => { 576 | it("should fire `destroy` hook when calling `destroy`", () => { 577 | jest.useFakeTimers(); 578 | 579 | const runner = createRunner(); 580 | const onDestroy = jest.fn(); 581 | 582 | expect(onDestroy).not.toHaveBeenCalled(); 583 | 584 | runner.addListener(CT.RunnerEvents.DESTROY, onDestroy); 585 | runner.start(); 586 | 587 | runner.destroy(); 588 | 589 | jest.advanceTimersByTime(1000); 590 | 591 | expect(onDestroy).toHaveBeenCalled(); 592 | }); 593 | }); 594 | 595 | describe("off", () => { 596 | it("should not fire `destroy` hook when calling `destroy`", () => { 597 | jest.useFakeTimers(); 598 | 599 | const runner = createRunner(); 600 | const onDestroy = jest.fn(); 601 | 602 | expect(onDestroy).not.toHaveBeenCalled(); 603 | 604 | runner.addListener(CT.RunnerEvents.DESTROY, onDestroy); 605 | runner.start(); 606 | runner.removeListener(CT.RunnerEvents.DESTROY); 607 | 608 | runner.destroy(); 609 | 610 | jest.advanceTimersByTime(1000); 611 | 612 | expect(onDestroy).not.toHaveBeenCalled(); 613 | 614 | jest.clearAllTimers(); 615 | }); 616 | }); 617 | }); 618 | 619 | describe("add", () => { 620 | describe("on", () => { 621 | it("should fire `add` event when one task is added", () => { 622 | const runner = createRunner({ taskCount: 0 }); 623 | const onAdd = jest.fn(); 624 | 625 | runner.addListener(CT.RunnerEvents.ADD, onAdd); 626 | runner.add(generateTask()); 627 | runner.start(); 628 | 629 | expect(onAdd).toHaveBeenCalled(); 630 | 631 | runner.destroy(); 632 | }); 633 | 634 | it("should fire `add` event as many times when multiple tasks are added", () => { 635 | const runner = createRunner({ taskCount: 0 }); 636 | const onAdd = jest.fn(); 637 | 638 | runner.addListener(CT.RunnerEvents.ADD, onAdd); 639 | runner.addMultiple(generateTasks(TASK_COUNT)); 640 | 641 | expect(onAdd).toHaveBeenCalledTimes(1); 642 | 643 | runner.destroy(); 644 | }); 645 | }); 646 | 647 | describe("off", () => { 648 | it("should not fire `add` event when one task is added", () => { 649 | const runner = createRunner({ taskCount: 0 }); 650 | const onAdd = jest.fn(); 651 | 652 | runner.addListener(CT.RunnerEvents.ADD, onAdd); 653 | runner.removeListener(CT.RunnerEvents.ADD); 654 | runner.add(generateTask()); 655 | runner.start(); 656 | 657 | expect(onAdd).not.toHaveBeenCalled(); 658 | 659 | runner.destroy(); 660 | }); 661 | 662 | it("should not fire `add` event as many times when multiple tasks are added", () => { 663 | const runner = createRunner({ taskCount: 0 }); 664 | const onAdd = jest.fn(); 665 | 666 | runner.addListener(CT.RunnerEvents.ADD, onAdd); 667 | runner.removeListener(CT.RunnerEvents.ADD); 668 | runner.addMultiple(generateTasks(TASK_COUNT)); 669 | 670 | expect(onAdd).not.toHaveBeenCalled(); 671 | 672 | runner.destroy(); 673 | }); 674 | }); 675 | }); 676 | 677 | describe("remove", () => { 678 | describe("on", () => { 679 | it("should fire `remove` event when removing one", () => { 680 | const runner = createRunner(); 681 | const onRemove = jest.fn(); 682 | 683 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 684 | runner.remove(); 685 | 686 | expect(onRemove).toHaveBeenCalled(); 687 | 688 | runner.destroy(); 689 | }); 690 | 691 | it("should fire `remove` event when removing all", () => { 692 | const runner = createRunner(); 693 | const onRemove = jest.fn(); 694 | 695 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 696 | runner.removeAll(); 697 | 698 | expect(onRemove).toHaveBeenCalled(); 699 | 700 | runner.destroy(); 701 | }); 702 | 703 | it("should fire `remove` event when removing range", () => { 704 | const runner = createRunner(); 705 | const onRemove = jest.fn(); 706 | 707 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 708 | runner.removeRange(1, 1); 709 | 710 | expect(onRemove).toHaveBeenCalled(); 711 | 712 | runner.destroy(); 713 | }); 714 | 715 | it("should fire `remove` event when removing at", () => { 716 | const runner = createRunner(); 717 | const onRemove = jest.fn(); 718 | 719 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 720 | runner.removeAt(1); 721 | 722 | expect(onRemove).toHaveBeenCalled(); 723 | 724 | runner.destroy(); 725 | }); 726 | }); 727 | 728 | describe("off", () => { 729 | it("should not fire `remove` event when removing one", () => { 730 | const runner = createRunner(); 731 | const onRemove = jest.fn(); 732 | 733 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 734 | runner.removeListener(CT.RunnerEvents.REMOVE); 735 | runner.remove(); 736 | 737 | expect(onRemove).not.toHaveBeenCalled(); 738 | 739 | runner.destroy(); 740 | }); 741 | 742 | it("should not fire `remove` event when removing all", () => { 743 | const runner = createRunner(); 744 | const onRemove = jest.fn(); 745 | 746 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 747 | runner.removeListener(CT.RunnerEvents.REMOVE); 748 | runner.removeAll(); 749 | 750 | expect(onRemove).not.toHaveBeenCalled(); 751 | 752 | runner.destroy(); 753 | }); 754 | 755 | it("should not fire `remove` event when removing range", () => { 756 | const runner = createRunner(); 757 | const onRemove = jest.fn(); 758 | 759 | runner.addListener(CT.RunnerEvents.REMOVE, onRemove); 760 | runner.removeListener(CT.RunnerEvents.REMOVE); 761 | runner.removeRange(1, 1); 762 | 763 | expect(onRemove).not.toHaveBeenCalled(); 764 | 765 | runner.destroy(); 766 | }); 767 | }); 768 | }); 769 | 770 | describe("run", () => { 771 | describe("on", () => { 772 | it("should fire `run` event whenever a task in run", (done) => { 773 | jest.useFakeTimers(); 774 | 775 | const runner = createRunner({ 776 | onEnd() { 777 | expect(onRun).toHaveBeenCalledTimes(TASK_COUNT); 778 | jest.clearAllTimers(); 779 | runner.destroy(); 780 | done(); 781 | }, 782 | }); 783 | const onRun = jest.fn(); 784 | 785 | runner.addListener(CT.RunnerEvents.RUN, onRun); 786 | 787 | runner.start(); 788 | 789 | jest.advanceTimersByTime(10000); 790 | }); 791 | }); 792 | 793 | describe("off", () => { 794 | it("should not fire `run` event whenever a task in run", (done) => { 795 | jest.useFakeTimers(); 796 | 797 | const runner = createRunner(); 798 | const onRun = jest.fn(); 799 | 800 | runner.addListener(CT.RunnerEvents.RUN, onRun); 801 | runner.removeListener(CT.RunnerEvents.RUN); 802 | runner.addListener(CT.RunnerEvents.END, () => { 803 | expect(onRun).not.toHaveBeenCalled(); 804 | jest.clearAllTimers(); 805 | runner.destroy(); 806 | done(); 807 | }); 808 | 809 | runner.start(); 810 | 811 | jest.advanceTimersByTime(10000); 812 | }); 813 | }); 814 | }); 815 | 816 | describe("done", () => { 817 | describe("on", () => { 818 | it("should fire `done` event whenever a task is done", (done) => { 819 | jest.useFakeTimers(); 820 | 821 | const runner = createRunner(); 822 | const onDone = jest.fn(); 823 | 824 | runner.addListener(CT.RunnerEvents.DONE, onDone); 825 | runner.addListener(CT.RunnerEvents.END, () => { 826 | expect(onDone).toHaveBeenCalledTimes(TASK_COUNT); 827 | jest.clearAllTimers(); 828 | runner.destroy(); 829 | done(); 830 | }); 831 | runner.start(); 832 | 833 | jest.advanceTimersByTime(10000); 834 | }); 835 | }); 836 | 837 | describe("off", () => { 838 | it("should not fire `done` event whenever a task is done", (done) => { 839 | jest.useFakeTimers(); 840 | 841 | const runner = createRunner(); 842 | const onDone = jest.fn(); 843 | 844 | runner.addListener(CT.RunnerEvents.DONE, onDone); 845 | runner.removeListener(CT.RunnerEvents.DONE); 846 | runner.addListener(CT.RunnerEvents.END, () => { 847 | expect(onDone).not.toHaveBeenCalled(); 848 | jest.clearAllTimers(); 849 | runner.destroy(); 850 | done(); 851 | }); 852 | runner.start(); 853 | 854 | jest.advanceTimersByTime(10000); 855 | }); 856 | }); 857 | }); 858 | 859 | describe("end", () => { 860 | describe("on", () => { 861 | it("should fire `end` event when all tasks are done", (done) => { 862 | jest.useFakeTimers(); 863 | 864 | const onEnd = jest.fn().mockImplementation(() => { 865 | expect(onEnd).toHaveBeenCalled(); 866 | jest.clearAllTimers(); 867 | runner.destroy(); 868 | done(); 869 | }); 870 | const runner = createRunner(); 871 | 872 | runner.addListener(CT.RunnerEvents.END, onEnd); 873 | runner.start(); 874 | 875 | jest.advanceTimersByTime(10000); 876 | }); 877 | }); 878 | 879 | describe("off", () => { 880 | it("should not fire `end` event when all tasks are done", () => { 881 | jest.useFakeTimers(); 882 | 883 | const onEnd = jest.fn(); 884 | const runner = createRunner(); 885 | 886 | runner.addListener(CT.RunnerEvents.END, onEnd); 887 | runner.start(); 888 | 889 | jest.advanceTimersByTime(10000); 890 | 891 | expect(onEnd).toHaveBeenCalledTimes(1); 892 | 893 | runner.removeListener(CT.RunnerEvents.END); 894 | runner.addMultiple(generateTasks()); 895 | runner.start(); 896 | 897 | jest.advanceTimersByTime(10000); 898 | 899 | expect(onEnd).toHaveBeenCalledTimes(1); 900 | 901 | jest.clearAllTimers(); 902 | }); 903 | }); 904 | }); 905 | }); 906 | }); 907 | --------------------------------------------------------------------------------