• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

panates / power-tasks / 23252366116

18 Mar 2026 03:22PM UTC coverage: 92.096% (+0.07%) from 92.028%
23252366116

push

github

erayhanoglu
1.13.1

255 of 291 branches covered (87.63%)

Branch coverage included in aggregate %.

1050 of 1126 relevant lines covered (93.25%)

51.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.81
/src/task.ts
1
import { AsyncEventEmitter } from "node-events-async";
1✔
2
import * as os from "os";
1✔
3
import { plural } from "./utils.js";
1✔
4

1✔
5
/**
1✔
6
 * Represents a function that defines the work to be performed by a task.
1✔
7
 *
1✔
8
 * @template T - The type of the result returned by the task.
1✔
9
 * @param args - The arguments provided to the task function, including a task instance and an abort signal.
1✔
10
 * @returns The result of the task, which can be a value or a promise.
1✔
11
 */
1✔
12
export type TaskFunction<T = any> = (args: TaskFunctionArgs) => T | Promise<T>;
1✔
13

1✔
14
/**
1✔
15
 * Represents a task-like object, which can be either a {@link Task} instance or a {@link TaskFunction}.
1✔
16
 *
1✔
17
 * @template T - The type of the result produced by the task.
1✔
18
 */
1✔
19
export type TaskLike<T = any> = Task<T> | TaskFunction;
1✔
20

1✔
21
/**
1✔
22
 * Represents the possible statuses of a task.
1✔
23
 *
1✔
24
 * - `idle`: The task has been created but not yet started.
1✔
25
 * - `waiting`: The task is waiting for its dependencies to complete.
1✔
26
 * - `running`: The task is currently executing.
1✔
27
 * - `fulfilled`: The task has completed successfully.
1✔
28
 * - `failed`: The task has failed with an error.
1✔
29
 * - `aborting`: The task is in the process of being aborted.
1✔
30
 * - `aborted`: The task has been aborted.
1✔
31
 */
1✔
32
export type TaskStatus =
1✔
33
  | "idle"
1✔
34
  | "waiting"
1✔
35
  | "running"
1✔
36
  | "fulfilled"
1✔
37
  | "failed"
1✔
38
  | "aborting"
1✔
39
  | "aborted";
1✔
40
const osCPUs = os.cpus().length;
1✔
41

1✔
42
/**
1✔
43
 * Arguments passed to a {@link TaskFunction}.
1✔
44
 */
1✔
45
export interface TaskFunctionArgs {
1✔
46
  /**
1✔
47
   * The {@link Task} instance executing the function.
1✔
48
   */
1✔
49
  task: Task;
1✔
50
  /**
1✔
51
   * An `AbortSignal` that can be used to monitor if the task has been aborted.
1✔
52
   */
1✔
53
  signal: AbortSignal;
1✔
54
}
1✔
55

1✔
56
/**
1✔
57
 * Options for configuring a {@link Task}.
1✔
58
 */
1✔
59
export interface TaskOptions {
1✔
60
  /**
1✔
61
   * Unique identifier for the task.
1✔
62
   */
1✔
63
  id?: any;
1✔
64

1✔
65
  /**
1✔
66
   * Name of the task. This value is used for dependency management.
1✔
67
   */
1✔
68
  name?: string;
1✔
69

1✔
70
  /**
1✔
71
   * Arguments to be passed to the task function.
1✔
72
   */
1✔
73
  args?: any[];
1✔
74

1✔
75
  /**
1✔
76
   * A list of child tasks or a function that returns them.
1✔
77
   */
1✔
78
  children?: TaskLike[] | (() => TaskLike[] | Promise<TaskLike[]>);
1✔
79

1✔
80
  /**
1✔
81
   * A list of tasks (instances or names) that must complete before this task starts.
1✔
82
   */
1✔
83
  dependencies?: (Task | string)[];
1✔
84

1✔
85
  /**
1✔
86
   * The maximum number of child tasks to run in parallel.
1✔
87
   * Defaults to the number of OS CPUs.
1✔
88
   */
1✔
89
  concurrency?: number;
1✔
90

1✔
91
  /**
1✔
92
   * Whether to abort remaining child tasks if one fails.
1✔
93
   * @default true
1✔
94
   */
1✔
95
  bail?: boolean;
1✔
96

1✔
97
  /**
1✔
98
   * Whether to run child tasks sequentially (one by one).
1✔
99
   * Equivalent to setting `concurrency: 1`.
1✔
100
   */
1✔
101
  serial?: boolean;
1✔
102

1✔
103
  /**
1✔
104
   * Whether the task should run exclusively.
1✔
105
   * If true, a task queue will wait for this task to complete before starting other tasks,
1✔
106
   * even if concurrency is greater than 1.
1✔
107
   */
1✔
108
  exclusive?: boolean;
1✔
109

1✔
110
  /**
1✔
111
   * An optional AbortSignal object that can be used to communicate with, or to abort, an operation.
1✔
112
   * The abortSignal allows you to signal cancellation requests or abort ongoing tasks.
1✔
113
   * Typically used for managing the lifecycle of async operations.
1✔
114
   */
1✔
115
  abortSignal?: AbortSignal;
1✔
116

1✔
117
  /**
1✔
118
   * Timeout in milliseconds to wait for the task to abort before forcing an 'aborted' status.
1✔
119
   * @default 30000
1✔
120
   */
1✔
121
  abortTimeout?: number;
1✔
122

1✔
123
  /**
1✔
124
   * Callback invoked when the task starts.
1✔
125
   */
1✔
126
  onStart?: (task: Task) => void;
1✔
127
  /**
1✔
128
   * Callback invoked when the task finishes (successfully, failed, or aborted).
1✔
129
   */
1✔
130
  onFinish?: (task: Task) => void;
1✔
131
  /**
1✔
132
   * Callback invoked when the task's execution function begins.
1✔
133
   */
1✔
134
  onRun?: (task: Task) => void;
1✔
135
  /**
1✔
136
   * Callback invoked when the task's status changes.
1✔
137
   */
1✔
138
  onStatusChange?: (task: Task) => void;
1✔
139
  /**
1✔
140
   * Callback invoked when the task's properties are updated.
1✔
141
   */
1✔
142
  onUpdate?: (task: Task, properties: string[]) => void;
1✔
143
  /**
1✔
144
   * Callback invoked when the task or any of its children's properties are updated.
1✔
145
   */
1✔
146
  onUpdateRecursive?: (task: Task, properties: string[]) => void;
1✔
147
}
1✔
148

1✔
149
export interface TaskUpdateValues {
1✔
150
  status?: TaskStatus;
1✔
151
  message?: string;
1✔
152
  error?: any;
1✔
153
  result?: any;
1✔
154
  waitingFor?: boolean;
1✔
155
}
1✔
156

1✔
157
class TaskContext {
1✔
158
  // allTasks = new Set<Task>();
1✔
159
  executingTasks = new Set<Task>();
1✔
160
  queue = new Set<Task>();
40✔
161
  concurrency!: number;
40✔
162
  triggerPulse!: () => void;
40✔
163
}
1✔
164

1✔
165
const noOp = () => undefined;
1✔
166
const taskContextKey = Symbol.for("power-tasks.Task.context");
1✔
167

1✔
168
let idGen = 0;
1✔
169

1✔
170
/**
1✔
171
 * A `Task` represents a unit of work that can be executed.
1✔
172
 * It supports hierarchical tasks, dependencies, and emits events throughout its lifecycle.
1✔
173
 *
1✔
174
 * @template T - The type of the result produced by the task.
1✔
175
 * @extends AsyncEventEmitter
1✔
176
 */
1✔
177
export class Task<T = any> extends AsyncEventEmitter {
1✔
178
  protected [taskContextKey]?: TaskContext;
1✔
179
  protected _id = "";
101✔
180
  protected _options: TaskOptions;
101✔
181
  protected _executeFn?: TaskFunction;
101✔
182
  protected _children?: Task[];
101✔
183
  protected _dependencies?: Task[];
101✔
184
  protected _status: TaskStatus = "idle";
101✔
185
  protected _message?: string;
101✔
186
  protected _executeDuration?: number;
101✔
187
  protected _error?: any;
101✔
188
  protected _result?: T;
101✔
189
  protected _isManaged?: boolean;
101✔
190
  protected _abortController = new AbortController();
101✔
191
  protected _abortTimer?: NodeJS.Timeout;
101✔
192
  protected _waitingFor?: Set<Task>;
101✔
193
  protected _failedChildren?: Task[];
101✔
194
  protected _failedDependencies?: Task[];
101✔
195
  protected _childrenLeft?: Set<Task>;
101✔
196

1✔
197
  /**
1✔
198
   * Constructs a new Task with child tasks.
1✔
199
   *
1✔
200
   * @param children - An array of child tasks or task functions.
1✔
201
   * @param options - Configuration options for the task.
1✔
202
   */
1✔
203
  constructor(children: TaskLike[], options?: Omit<TaskOptions, "children">);
1✔
204
  /**
1✔
205
   * Constructs a new Task with an execution function.
1✔
206
   *
1✔
207
   * @param execute - The function to be executed by the task.
1✔
208
   * @param options - Configuration options for the task.
1✔
209
   */
1✔
210
  constructor(execute: TaskFunction, options?: TaskOptions);
1✔
211
  constructor(arg0: any, options?: TaskOptions) {
1✔
212
    super();
101✔
213
    this.setMaxListeners(100);
101✔
214
    options = options || {};
101✔
215
    if (Array.isArray(arg0)) {
101✔
216
      options.children = arg0;
10✔
217
    } else this._executeFn = arg0;
101✔
218
    this._options = { ...options };
101✔
219
    this._id = this._options.id || "";
101✔
220
    if (this._options.bail == null) this._options.bail = true;
101✔
221
    if (options.onStart) this.on("start", options.onStart);
101!
222
    if (options.onFinish) this.on("finish", options.onFinish);
101!
223
    if (options.onRun) this.on("run", options.onRun);
101!
224
    if (options.onStatusChange)
101✔
225
      this.on("status-change", options.onStatusChange);
101!
226
    if (options.onUpdate) this.on("update", options.onUpdate);
101!
227
    if (options.onUpdateRecursive)
101✔
228
      this.on("update-recursive", options.onUpdateRecursive);
101✔
229
    if (options.abortSignal)
101✔
230
      options.abortSignal.addEventListener("abort", () => this.abort());
101✔
231
  }
101✔
232

1✔
233
  /**
1✔
234
   * Gets the unique identifier of the task.
1✔
235
   */
1✔
236
  get id(): string {
1✔
237
    return this._id;
120✔
238
  }
120✔
239

1✔
240
  /**
1✔
241
   * Gets the name of the task.
1✔
242
   */
1✔
243
  get name(): string | undefined {
1✔
244
    return this._options.name;
224✔
245
  }
224✔
246

1✔
247
  /**
1✔
248
   * Gets the list of child tasks.
1✔
249
   */
1✔
250
  get children(): Task[] | undefined {
1✔
251
    return this._children;
39✔
252
  }
39✔
253

1✔
254
  /**
1✔
255
   * Gets the task configuration options.
1✔
256
   */
1✔
257
  get options(): TaskOptions {
1✔
258
    return this._options;
671✔
259
  }
671✔
260

1✔
261
  /**
1✔
262
   * Gets the current message of the task.
1✔
263
   */
1✔
264
  get message(): string {
1✔
265
    return this._message || "";
1!
266
  }
1✔
267

1✔
268
  /**
1✔
269
   * Gets the current status of the task.
1✔
270
   */
1✔
271
  get status(): TaskStatus {
1✔
272
    return this._status;
7,011✔
273
  }
7,011✔
274

1✔
275
  /**
1✔
276
   * Whether the task has started but not yet finished.
1✔
277
   */
1✔
278
  get isStarted(): boolean {
1✔
279
    return this.status !== "idle" && !this.isFinished;
824✔
280
  }
824✔
281

1✔
282
  /**
1✔
283
   * Whether the task has completed (successfully, failed, or aborted).
1✔
284
   */
1✔
285
  get isFinished(): boolean {
1✔
286
    return (
1,646✔
287
      this.status === "fulfilled" ||
1,646✔
288
      this.status === "failed" ||
1,546✔
289
      this.status === "aborted"
1,509✔
290
    );
1,646✔
291
  }
1,646✔
292

1✔
293
  /**
1✔
294
   * Whether the task has failed.
1✔
295
   */
1✔
296
  get isFailed(): boolean {
1✔
297
    return this.status === "failed";
128✔
298
  }
128✔
299

1✔
300
  /**
1✔
301
   * Gets the duration of the task execution in milliseconds.
1✔
302
   */
1✔
303
  get executeDuration(): number | undefined {
1✔
304
    return this._executeDuration;
×
305
  }
×
306

1✔
307
  /**
1✔
308
   * Gets the result produced by the task.
1✔
309
   */
1✔
310
  get result(): any {
1✔
311
    return this._result;
53✔
312
  }
53✔
313

1✔
314
  /**
1✔
315
   * Gets the error if the task failed.
1✔
316
   */
1✔
317
  get error(): any {
1✔
318
    return this._error;
127✔
319
  }
127✔
320

1✔
321
  /**
1✔
322
   * Gets the list of tasks this task depends on.
1✔
323
   */
1✔
324
  get dependencies(): Task[] | undefined {
1✔
325
    return this._dependencies;
×
326
  }
×
327

1✔
328
  /**
1✔
329
   * Gets the list of child tasks that failed.
1✔
330
   */
1✔
331
  get failedChildren(): Task[] | undefined {
1✔
332
    return this._failedChildren;
×
333
  }
×
334

1✔
335
  /**
1✔
336
   * Gets the list of dependencies that failed.
1✔
337
   */
1✔
338
  get failedDependencies(): Task[] | undefined {
1✔
339
    return this._failedDependencies;
×
340
  }
×
341

1✔
342
  /**
1✔
343
   * Whether the task is currently waiting for children or dependencies to finish.
1✔
344
   */
1✔
345
  get needWaiting(): boolean {
1✔
346
    if (this._waitingFor && this._waitingFor.size) return true;
×
347
    if (this._children) {
×
348
      for (const c of this._children) {
×
349
        if (c.needWaiting) return true;
×
350
      }
×
351
    }
×
352
    return false;
×
353
  }
×
354

1✔
355
  /**
1✔
356
   * Gets a list of tasks that this task is currently waiting for.
1✔
357
   *
1✔
358
   * @returns An array of {@link Task} instances or `undefined`.
1✔
359
   */
1✔
360
  getWaitingTasks(): Task[] | undefined {
1✔
361
    if (
171✔
362
      !(this.status === "waiting" && this._waitingFor && this._waitingFor.size)
171✔
363
    )
171✔
364
      return;
171✔
365
    const out = Array.from(this._waitingFor);
12✔
366
    if (this._children) {
171!
367
      for (const c of this._children) {
×
368
        const childTasks = c.getWaitingTasks();
×
369
        if (childTasks) {
×
370
          childTasks.forEach((t) => {
×
371
            if (!out.includes(t)) out.push(t);
×
372
          });
×
373
        }
×
374
      }
×
375
    }
✔
376
    return out;
12✔
377
  }
12✔
378

1✔
379
  /**
1✔
380
   * Aborts the task execution.
1✔
381
   *
1✔
382
   * @returns The task instance.
1✔
383
   */
1✔
384
  abort(): this {
1✔
385
    if (this.isFinished || this.status === "aborting") return this;
39✔
386

23✔
387
    if (!this.isStarted) {
39✔
388
      this._update({ status: "aborted", message: "aborted" });
5✔
389
      return this;
5✔
390
    }
5✔
391

18✔
392
    const ctx = this[taskContextKey] as TaskContext;
18✔
393
    const timeout = this.options.abortTimeout || 30000;
39✔
394
    this._update({ status: "aborting", message: "Aborting" });
39✔
395
    if (timeout) {
39✔
396
      this._abortTimer = setTimeout(() => {
18✔
397
        delete this._abortTimer;
1✔
398
        this._update({ status: "aborted", message: "aborted" });
1✔
399
      }, timeout).unref();
18✔
400
    }
18✔
401
    this._abortChildren()
18✔
402
      .catch(noOp)
18✔
403
      .then(() => {
18✔
404
        if (this.isFinished) return;
18✔
405
        if (ctx.executingTasks.has(this)) {
18✔
406
          this._abortController.abort();
12✔
407
          return;
12✔
408
        }
12✔
409
        this._update({ status: "aborted", message: "aborted" });
5✔
410
      })
5✔
411
      .catch(noOp);
18✔
412
    return this;
18✔
413
  }
18✔
414

1✔
415
  /**
1✔
416
   * Starts the task execution.
1✔
417
   *
1✔
418
   * @returns The task instance.
1✔
419
   */
1✔
420
  start(): this {
1✔
421
    if (this.isStarted) return this;
40!
422
    this._id = this._id || "t" + ++idGen;
40✔
423
    const ctx = (this[taskContextKey] = new TaskContext());
40✔
424
    ctx.concurrency = this.options.concurrency || osCPUs;
40✔
425
    let pulseTimer: NodeJS.Timer | undefined;
40✔
426
    ctx.triggerPulse = () => {
40✔
427
      if (pulseTimer || this.isFinished) return;
94✔
428
      pulseTimer = setTimeout(() => {
28✔
429
        pulseTimer = undefined;
28✔
430
        this._pulse();
28✔
431
      }, 1);
28✔
432
    };
28✔
433
    if (this.options.children) {
40✔
434
      this._determineChildrenTree((err) => {
14✔
435
        if (err) {
14!
436
          this._update({
×
437
            status: "failed",
×
438
            error: err,
×
439
            message: "Unable to fetch child tasks. " + (err.message || err),
×
440
          });
×
441
          return;
×
442
        }
×
443
        this._determineChildrenDependencies([]);
14✔
444
        this._start();
14✔
445
      });
14✔
446
    } else this._start();
40✔
447
    return this;
39✔
448
  }
39✔
449

1✔
450
  /**
1✔
451
   * Returns a promise that resolves with the task result or rejects with the task error.
1✔
452
   * If the task has not started and is not managed by a queue, it will be started automatically.
1✔
453
   *
1✔
454
   * @returns A promise that resolves when the task completes.
1✔
455
   */
1✔
456
  toPromise(): Promise<T> {
1✔
457
    return new Promise((resolve, reject) => {
52✔
458
      if (this.isFinished) {
52✔
459
        if (this.isFailed) reject(this.error);
3!
460
        else resolve(this.result);
3✔
461
        return;
3✔
462
      }
3✔
463
      this.once("finish", () => {
49✔
464
        if (this.isFailed) return reject(this.error);
48✔
465
        resolve(this.result);
42✔
466
      });
42✔
467
      if (!this.isStarted && !this._isManaged) this.start();
52✔
468
    });
52✔
469
  }
52✔
470

1✔
471
  protected _determineChildrenTree(callback: (err?: any) => void): void {
1✔
472
    const ctx = this[taskContextKey] as TaskContext;
15✔
473
    const options = this._options;
15✔
474
    const handler = (err?: any, value?: any) => {
15✔
475
      if (err) return callback(err);
19!
476
      if (!value) return callback();
19!
477

19✔
478
      if (typeof value === "function") {
19✔
479
        try {
2✔
480
          const x: any = value();
2✔
481
          handler(undefined, x);
2✔
482
        } catch (err2) {
2!
483
          handler(err2);
×
484
        }
×
485
        return;
2✔
486
      }
2✔
487

17✔
488
      if (Array.isArray(value)) {
19✔
489
        let idx = 1;
15✔
490
        const children = value.reduce<Task[]>((a, v) => {
15✔
491
          // noinspection SuspiciousTypeOfGuard
58✔
492
          if (typeof v === "function") {
58✔
493
            v = new Task(v, {
5✔
494
              concurrency: options.concurrency,
5✔
495
              bail: options.bail,
5✔
496
            });
5✔
497
          }
5✔
498
          if (v instanceof Task) {
58✔
499
            v[taskContextKey] = ctx;
58✔
500
            v._id = v._id || this._id + "-" + idx++;
58✔
501
            const listeners = this.listeners("update-recursive");
58✔
502
            listeners.forEach((listener) =>
58✔
503
              v.on("update-recursive", listener as any),
55✔
504
            );
58✔
505
            a.push(v);
58✔
506
          }
58✔
507
          return a;
58✔
508
        }, []);
15✔
509

15✔
510
        if (children && children.length) {
15✔
511
          this._children = children;
15✔
512
          let i = 0;
15✔
513
          const next = (err2?: any) => {
15✔
514
            if (err2) return callback(err2);
73!
515
            if (i >= children.length) return callback();
73✔
516
            const c = children[i++];
58✔
517
            if (c.options.children)
58✔
518
              c._determineChildrenTree((err3) => next(err3));
73✔
519
            else next();
57✔
520
          };
73✔
521
          next();
15✔
522
        } else callback();
15!
523
        return;
14✔
524
      }
14✔
525
      if (value && typeof value.then === "function") {
19✔
526
        (value as Promise<TaskLike[]>)
2✔
527
          .then((v) => handler(undefined, v))
2✔
528
          .catch((e) => handler(e));
2✔
529
        return;
2✔
530
      }
2!
531

×
532
      callback(new Error("Invalid value returned from children() method."));
×
533
    };
×
534
    handler(undefined, this._options.children);
15✔
535
  }
15✔
536

1✔
537
  protected _determineChildrenDependencies(scope: Task[]): void {
1✔
538
    if (!this._children) return;
72✔
539

15✔
540
    const detectCircular = (
15✔
541
      t: Task,
20✔
542
      dependencies: Task[],
20✔
543
      path: string = "",
20✔
544
      list?: Set<Task>,
20✔
545
    ) => {
20✔
546
      path = path || t.name || t.id;
20!
547
      list = list || new Set();
20✔
548
      for (const l1 of dependencies.values()) {
20✔
549
        if (l1 === t) throw new Error(`Circular dependency detected. ${path}`);
20✔
550
        if (list.has(l1)) continue;
20!
551
        list.add(l1);
19✔
552
        if (l1._dependencies)
19✔
553
          detectCircular(
20✔
554
            t,
5✔
555
            l1._dependencies,
5✔
556
            path + " > " + (l1.name || l1.id),
5!
557
            list,
5✔
558
          );
17✔
559

17✔
560
        if (l1.children) {
20!
561
          for (const c of l1.children) {
×
562
            if (c === t)
×
563
              throw new Error(`Circular dependency detected. ${path}`);
×
564
            if (list.has(c)) continue;
×
565
            list.add(c);
×
566
            if (c._dependencies) detectCircular(t, c._dependencies, path, list);
×
567
          }
×
568
        }
×
569
      }
20✔
570
    };
17✔
571

15✔
572
    const subScope = [...scope, ...Array.from(this._children)];
15✔
573
    for (const c of this._children.values()) {
72✔
574
      c._determineChildrenDependencies(subScope);
58✔
575
      if (!c.options.dependencies) continue;
58✔
576

15✔
577
      const dependencies: Task[] = [];
15✔
578
      const waitingFor = new Set<Task>();
15✔
579
      for (const dep of c.options.dependencies) {
15✔
580
        const dependentTask = subScope.find((x) =>
15✔
581
          typeof dep === "string" ? x.name === dep : x === dep,
48✔
582
        );
15✔
583
        if (!dependentTask || c === dependentTask) continue;
15!
584
        dependencies.push(dependentTask);
15✔
585
        if (!dependentTask.isFinished) waitingFor.add(dependentTask);
15✔
586
      }
15✔
587
      detectCircular(c, dependencies);
15✔
588
      if (dependencies.length) c._dependencies = dependencies;
58✔
589
      if (waitingFor.size) c._waitingFor = waitingFor;
14✔
590
      c._captureDependencies();
14✔
591
    }
14✔
592
  }
14✔
593

1✔
594
  protected _captureDependencies(): void {
1✔
595
    if (!this._waitingFor) return;
14!
596
    const failedDependencies: Task[] = [];
14✔
597
    const waitingFor = this._waitingFor;
14✔
598
    const signal = this._abortController.signal;
14✔
599

14✔
600
    const abortSignalCallback = () => clearWait();
14✔
601
    signal.addEventListener("abort", abortSignalCallback, { once: true });
14✔
602

14✔
603
    const handleDependentAborted = () => {
14✔
604
      signal.removeEventListener("abort", abortSignalCallback);
×
605
      this._abortChildren()
×
606
        .then(() => {
×
607
          const isFailed = !!failedDependencies.find(
×
608
            (d) => d.status === "failed",
×
609
          );
×
610
          const error: any = new Error(
×
611
            "Aborted due to " +
×
612
              (isFailed ? "fail" : "cancellation") +
×
613
              " of dependent " +
×
614
              plural("task", !!failedDependencies.length),
×
615
          );
×
616
          error.failedDependencies = failedDependencies;
×
617
          this._failedDependencies = failedDependencies;
×
618
          this._update({
×
619
            status: isFailed ? "failed" : "aborted",
×
620
            message: error.message,
×
621
            error,
×
622
          });
×
623
        })
×
624
        .catch(noOp);
×
625
    };
×
626

14✔
627
    const clearWait = () => {
14✔
628
      for (const t of waitingFor) {
4✔
629
        t.removeListener("finish", finishCallback);
4✔
630
      }
4✔
631
      delete this._waitingFor;
4✔
632
    };
4✔
633

14✔
634
    const finishCallback = async (t) => {
14✔
635
      if (this.isStarted && this.status !== "waiting") {
12✔
636
        clearWait();
4✔
637
        return;
4✔
638
      }
4✔
639
      waitingFor.delete(t);
8✔
640
      if (t.isFailed || t.status === "aborted") {
12!
641
        failedDependencies.push(t);
×
642
      }
✔
643

8✔
644
      // If all dependent tasks completed
8✔
645
      if (!waitingFor.size) {
8✔
646
        delete this._waitingFor;
8✔
647
        signal.removeEventListener("abort", abortSignalCallback);
8✔
648

8✔
649
        // If any of dependent tasks are failed
8✔
650
        if (failedDependencies.length) {
8!
651
          handleDependentAborted();
×
652
          return;
×
653
        }
×
654
        // If all dependent tasks completed successfully we continue to next step (startChildren)
8✔
655
        if (this.isStarted) this._startChildren();
8✔
656
        else await this.emitAsync("wait-end");
1✔
657
      }
8✔
658
    };
12✔
659

14✔
660
    for (const t of waitingFor.values()) {
14✔
661
      if (t.isFailed || t.status === "aborted") {
14!
662
        waitingFor.delete(t);
×
663
        failedDependencies.push(t);
×
664
      } else t.prependOnceListener("finish", finishCallback);
14✔
665
    }
14✔
666
    if (!waitingFor.size) handleDependentAborted();
14!
667
  }
14✔
668

1✔
669
  protected _start(): void {
1✔
670
    if (this.isStarted || this.isFinished) return;
92!
671

92✔
672
    if (this._waitingFor) {
92✔
673
      this._update({
12✔
674
        status: "waiting",
12✔
675
        message: "Waiting for dependencies",
12✔
676
        waitingFor: true,
12✔
677
      });
12✔
678
      return;
12✔
679
    }
12✔
680
    this._startChildren();
80✔
681
  }
80✔
682

1✔
683
  protected _startChildren() {
1✔
684
    const children = this._children;
87✔
685
    if (!children) {
87✔
686
      this._pulse();
73✔
687
      return;
73✔
688
    }
73✔
689

14✔
690
    const options = this.options;
14✔
691
    const childrenLeft = (this._childrenLeft = new Set(children));
14✔
692
    const failedChildren: Task[] = [];
14✔
693

14✔
694
    const statusChangeCallback = async (t: Task) => {
14✔
695
      if (this.status === "aborting") return;
128✔
696
      if (t.status === "running")
104✔
697
        this._update({ status: "running", message: "Running" });
128✔
698
      if (t.status === "waiting")
104✔
699
        this._update({ status: "waiting", message: "Waiting" });
128✔
700
    };
128✔
701

14✔
702
    const finishCallback = async (t: Task) => {
14✔
703
      t.removeListener("status-change", statusChangeCallback);
55✔
704
      childrenLeft.delete(t);
55✔
705
      if (t.isFailed || t.status === "aborted") {
55✔
706
        failedChildren.push(t);
19✔
707
        if (options.bail && childrenLeft.size) {
19✔
708
          const running = !!children.find((c) => c.isStarted);
13✔
709
          if (running)
13✔
710
            this._update({ status: "aborting", message: "Aborting" });
13✔
711
          this._abortChildren().catch(noOp);
13✔
712
          return;
13✔
713
        }
13✔
714
      }
19✔
715

42✔
716
      if (!childrenLeft.size) {
55✔
717
        delete this._childrenLeft;
14✔
718
        if (failedChildren.length) {
14✔
719
          const isFailed = !!failedChildren.find((d) => d.status === "failed");
7✔
720
          const error: any = new Error(
7✔
721
            "Aborted due to " +
7✔
722
              (isFailed ? "fail" : "cancellation") +
7✔
723
              " of child " +
7✔
724
              plural("task", !!failedChildren.length),
7✔
725
          );
7✔
726
          error.failedChildren = failedChildren;
7✔
727
          this._failedChildren = failedChildren;
7✔
728
          this._update({
7✔
729
            status: isFailed ? "failed" : "aborted",
7✔
730
            error,
7✔
731
            message: error.message,
7✔
732
          });
7✔
733
          return;
7✔
734
        }
7✔
735
      }
14✔
736
      this._pulse();
35✔
737
    };
35✔
738

14✔
739
    for (const c of children) {
87✔
740
      c.prependOnceListener("wait-end", () => this._pulse());
55✔
741
      c.prependOnceListener("finish", finishCallback);
55✔
742
      c.prependListener("status-change", statusChangeCallback);
55✔
743
    }
55✔
744

14✔
745
    this._pulse();
14✔
746
  }
14✔
747

1✔
748
  protected _pulse() {
1✔
749
    const ctx = this[taskContextKey] as TaskContext;
224✔
750

224✔
751
    if (
224✔
752
      this.isFinished ||
224✔
753
      this._waitingFor ||
224✔
754
      this.status === "aborting" ||
185✔
755
      ctx.executingTasks.has(this)
182✔
756
    )
224✔
757
      return;
224✔
758

140✔
759
    const options = this.options;
140✔
760
    if (this._childrenLeft) {
224✔
761
      // Check if we can run multiple child tasks
60✔
762
      for (const c of this._childrenLeft) {
60✔
763
        if (
169✔
764
          (c.isStarted && options.serial) ||
169✔
765
          (c.status === "running" && c.options.exclusive)
169✔
766
        ) {
169✔
767
          c._pulse();
4✔
768
          return;
4✔
769
        }
4✔
770
      }
169✔
771

56✔
772
      // Check waiting children
56✔
773
      let hasExclusive = false;
56✔
774
      let hasRunning = false;
56✔
775
      for (const c of this._childrenLeft) {
60✔
776
        if (c.isFinished) continue;
165!
777
        hasExclusive = hasExclusive || !!c.options.exclusive;
165✔
778
        hasRunning = hasRunning || c.status === "running";
165✔
779
      }
165✔
780
      if (hasExclusive && hasRunning) return;
60!
781

56✔
782
      // start children
56✔
783
      let k = ctx.concurrency - ctx.executingTasks.size;
56✔
784
      for (const c of this._childrenLeft) {
60✔
785
        if (c.isStarted) {
131✔
786
          c._pulse();
69✔
787
          continue;
69✔
788
        }
69✔
789
        if (k-- <= 0) return;
131✔
790
        if (
54✔
791
          c.options.exclusive &&
54✔
792
          (ctx.executingTasks.size || ctx.executingTasks.size)
2✔
793
        )
131✔
794
          return;
131✔
795
        c._start();
53✔
796
        if (options.serial || (c.status === "running" && c.options.exclusive))
131✔
797
          return;
131✔
798
      }
131✔
799
    }
38✔
800

118✔
801
    if (
118✔
802
      (this._childrenLeft && this._childrenLeft.size) ||
224✔
803
      ctx.executingTasks.size >= ctx.concurrency
80✔
804
    )
224✔
805
      return;
224✔
806

80✔
807
    this._update({ status: "running", message: "Running" });
80✔
808
    ctx.executingTasks.add(this);
80✔
809
    const t = Date.now();
80✔
810
    const signal = this._abortController.signal;
80✔
811
    (async () =>
80✔
812
      (this._executeFn || noOp)({
80✔
813
        task: this,
80✔
814
        signal,
80✔
815
      }))()
80✔
816
      .then((result: any) => {
80✔
817
        ctx.executingTasks.delete(this);
63✔
818
        this._executeDuration = Date.now() - t;
63✔
819
        this._update({
63✔
820
          status: "fulfilled",
63✔
821
          message: "Task completed",
63✔
822
          result,
63✔
823
        });
63✔
824
      })
63✔
825
      .catch((error) => {
80✔
826
        ctx.executingTasks.delete(this);
17✔
827
        this._executeDuration = Date.now() - t;
17✔
828
        if (error.code === "ABORT_ERR") {
17✔
829
          this._update({
9✔
830
            status: "aborted",
9✔
831
            error,
9✔
832
            message: error instanceof Error ? error.message : "" + error,
9!
833
          });
9✔
834
          return;
9✔
835
        }
9✔
836
        this._update({
8✔
837
          status: "failed",
8✔
838
          error,
8✔
839
          message: error instanceof Error ? error.message : "" + error,
17!
840
        });
17✔
841
      });
17✔
842
  }
80✔
843

1✔
844
  protected _update(prop: TaskUpdateValues) {
1✔
845
    const oldFinished = this.isFinished;
279✔
846
    const keys: string[] = [];
279✔
847
    const oldStarted = this.isStarted;
279✔
848
    if (prop.status && this._status !== prop.status) {
279✔
849
      this._status = prop.status;
221✔
850
      keys.push("status");
221✔
851
    }
221✔
852
    if (prop.message && this._message !== prop.message) {
279✔
853
      this._message = prop.message;
212✔
854
      keys.push("message");
212✔
855
    }
212✔
856
    if (prop.error && this._error !== prop.error) {
279✔
857
      this._error = prop.error;
24✔
858
      keys.push("error");
24✔
859
    }
24✔
860
    if (prop.result && this._result !== prop.result) {
279✔
861
      this._result = prop.result;
15✔
862
      keys.push("result");
15✔
863
    }
15✔
864
    if (prop.waitingFor) {
279✔
865
      keys.push("waitingFor");
12✔
866
    }
12✔
867
    if (keys.length) {
279✔
868
      if (keys.includes("status")) {
221✔
869
        if (!oldStarted) this.emitAsync("start", this).catch(noOp);
221✔
870
        this.emitAsync("status-change", this).catch(noOp);
221✔
871
        if (this._status === "running") this.emitAsync("run", this).catch(noOp);
221✔
872
      }
221✔
873
      this.emitAsync("update", this, keys).catch(noOp);
221✔
874
      this.emitAsync("update-recursive", this, keys).catch(noOp);
221✔
875
      if (this.isFinished && !oldFinished) {
221✔
876
        const ctx = this[taskContextKey];
97✔
877
        if (this._abortTimer) {
97✔
878
          clearTimeout(this._abortTimer);
17✔
879
          delete this._abortTimer;
17✔
880
        }
17✔
881
        delete this[taskContextKey];
97✔
882
        if (this.error) this.emitAsync("error", this.error).catch(noOp);
97✔
883
        this.emitAsync("finish", this).catch(noOp);
97✔
884
        if (ctx) ctx.triggerPulse();
97✔
885
      }
97✔
886
    }
221✔
887
  }
279✔
888

1✔
889
  protected async _abortChildren(): Promise<void> {
1✔
890
    const promises: Promise<void>[] = [];
31✔
891
    if (this._children) {
31✔
892
      for (let i = this._children.length - 1; i >= 0; i--) {
14✔
893
        const child = this._children[i];
62✔
894
        if (!child.isFinished) {
62✔
895
          child.abort();
29✔
896
          promises.push(child.toPromise());
29✔
897
        }
29✔
898
      }
62✔
899
    }
14✔
900
    if (promises.length) await Promise.all(promises);
31✔
901
  }
31✔
902
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc