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

panates / power-tasks / 23430198069

23 Mar 2026 09:24AM UTC coverage: 90.292% (-1.8%) from 92.138%
23430198069

push

github

erayhanoglu
1.13.5

255 of 295 branches covered (86.44%)

Branch coverage included in aggregate %.

1196 of 1312 relevant lines covered (91.16%)

44.89 hits per line

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

90.16
/src/task.ts
1
import { AsyncEventEmitter } from "node-events-async";
1✔
2
import * as os from "os";
1✔
3
import type { TaskEvents } from "./interfaces/task-events.js";
1✔
4
import type { TaskOptions } from "./interfaces/task-options.js";
1✔
5
import type { TaskFunction, TaskLike, TaskStatus } from "./interfaces/types.js";
1✔
6
import { plural } from "./utils.js";
1✔
7

1✔
8
const osCPUs = os.cpus().length;
1✔
9

1✔
10
/**
1✔
11
 * Values used to update a task's state.
1✔
12
 */
1✔
13
export interface TaskUpdateValues {
1✔
14
  /**
1✔
15
   * The new status of the task.
1✔
16
   */
1✔
17
  status?: TaskStatus;
1✔
18
  /**
1✔
19
   * A message associated with the task's current state.
1✔
20
   */
1✔
21
  message?: string;
1✔
22
  /**
1✔
23
   * The error that occurred during task execution, if any.
1✔
24
   */
1✔
25
  error?: any;
1✔
26
  /**
1✔
27
   * The result produced by the task, if any.
1✔
28
   */
1✔
29
  result?: any;
1✔
30
  /**
1✔
31
   * Whether the task is currently waiting for something.
1✔
32
   */
1✔
33
  waitingFor?: boolean;
1✔
34
}
1✔
35

1✔
36
/**
1✔
37
 * Internal context shared among tasks for execution management.
1✔
38
 * @internal
1✔
39
 */
1✔
40
class TaskContext {
1✔
41
  /**
1✔
42
   * Tasks currently being executed.
1✔
43
   */
1✔
44
  executingTasks = new Set<Task>();
1✔
45
  /**
40✔
46
   * Tasks queued for execution.
40✔
47
   */
40✔
48
  queue = new Set<Task>();
40✔
49
  /**
40✔
50
   * Maximum number of concurrent tasks.
40✔
51
   */
40✔
52
  concurrency!: number;
40✔
53
  /**
40✔
54
   * Function to trigger the execution pulse.
40✔
55
   */
40✔
56
  triggerPulse!: () => void;
40✔
57
}
1✔
58

1✔
59
const noOp = () => undefined;
1✔
60
const taskContextKey = Symbol.for("power-tasks.Task.context");
1✔
61

1✔
62
let idGen = 0;
1✔
63

1✔
64
/**
1✔
65
 * A `Task` represents a unit of work that can be executed.
1✔
66
 * It supports hierarchical tasks, dependencies, and emits events throughout its lifecycle.
1✔
67
 *
1✔
68
 * @template T - The type of the result produced by the task.
1✔
69
 * @extends AsyncEventEmitter
1✔
70
 */
1✔
71
export class Task<T = any> extends AsyncEventEmitter<TaskEvents> {
1✔
72
  protected [taskContextKey]?: TaskContext;
1✔
73
  protected _id = "";
101✔
74
  protected _options: TaskOptions;
101✔
75
  protected _executeFn?: TaskFunction;
101✔
76
  protected _children?: Task[];
101✔
77
  protected _dependencies?: Task[];
101✔
78
  protected _status: TaskStatus = "idle";
101✔
79
  protected _message?: string;
101✔
80
  protected _executeDuration?: number;
101✔
81
  protected _error?: any;
101✔
82
  protected _result?: T;
101✔
83
  protected _isManaged?: boolean;
101✔
84
  protected _abortController = new AbortController();
101✔
85
  protected _abortTimer?: NodeJS.Timeout;
101✔
86
  protected _waitingFor?: Set<Task>;
101✔
87
  protected _failedChildren?: Task[];
101✔
88
  protected _failedDependencies?: Task[];
101✔
89
  protected _childrenLeft?: Set<Task>;
101✔
90

1✔
91
  /**
1✔
92
   * Constructs a new Task with child tasks.
1✔
93
   *
1✔
94
   * @param children - An array of child tasks or task functions.
1✔
95
   * @param options - Configuration options for the task.
1✔
96
   */
1✔
97
  constructor(children: TaskLike[], options?: TaskOptions);
1✔
98
  /**
1✔
99
   * Constructs a new Task with an execution function.
1✔
100
   *
1✔
101
   * @param execute - The function to be executed by the task.
1✔
102
   * @param options - Configuration options for the task.
1✔
103
   */
1✔
104
  constructor(execute: TaskFunction, options?: TaskOptions);
1✔
105
  constructor(arg0: any, options?: TaskOptions) {
1✔
106
    super();
101✔
107
    this.setMaxListeners(100);
101✔
108
    options = options || {};
101✔
109
    if (Array.isArray(arg0)) {
101✔
110
      options.children = arg0;
10✔
111
    } else this._executeFn = arg0;
101✔
112
    this._options = { ...options };
101✔
113
    this._id = this._options.id || "";
101✔
114
    if (this._options.bail == null) this._options.bail = true;
101✔
115
    if (options.onStart) this.on("start", options.onStart);
101!
116
    if (options.onFinish) this.on("finish", options.onFinish);
101!
117
    if (options.onRun) this.on("run", options.onRun);
101!
118
    if (options.onStatusChange)
101✔
119
      this.on("status-change", options.onStatusChange);
101!
120
    if (options.onUpdate) this.on("update", options.onUpdate);
101!
121
    if (options.onUpdateRecursive)
101✔
122
      this.on("update-recursive", options.onUpdateRecursive);
101✔
123
    if (options.abortSignal)
101✔
124
      options.abortSignal.addEventListener("abort", () => this.abort());
101✔
125
  }
101✔
126

1✔
127
  /**
1✔
128
   * Gets the unique identifier of the task.
1✔
129
   */
1✔
130
  get id(): string {
1✔
131
    return this._id;
139✔
132
  }
139✔
133

1✔
134
  /**
1✔
135
   * Gets the name of the task.
1✔
136
   */
1✔
137
  get name(): string | undefined {
1✔
138
    return this._options.name;
224✔
139
  }
224✔
140

1✔
141
  /**
1✔
142
   * Gets the list of child tasks.
1✔
143
   */
1✔
144
  get children(): Task[] | undefined {
1✔
145
    return this._children;
39✔
146
  }
39✔
147

1✔
148
  /**
1✔
149
   * Gets the task configuration options.
1✔
150
   */
1✔
151
  get options(): TaskOptions {
1✔
152
    return this._options;
671✔
153
  }
671✔
154

1✔
155
  /**
1✔
156
   * Gets the current message of the task.
1✔
157
   */
1✔
158
  get message(): string {
1✔
159
    return this._message || "";
1!
160
  }
1✔
161

1✔
162
  /**
1✔
163
   * Gets the current status of the task.
1✔
164
   */
1✔
165
  get status(): TaskStatus {
1✔
166
    return this._status;
7,232✔
167
  }
7,232✔
168

1✔
169
  /**
1✔
170
   * Whether the task has started but not yet finished.
1✔
171
   */
1✔
172
  get isStarted(): boolean {
1✔
173
    return this.status !== "idle" && !this.isFinished;
824✔
174
  }
824✔
175

1✔
176
  /**
1✔
177
   * Whether the task has completed (successfully, failed, or aborted).
1✔
178
   */
1✔
179
  get isFinished(): boolean {
1✔
180
    return (
1,646✔
181
      this.status === "fulfilled" ||
1,646✔
182
      this.status === "failed" ||
1,546✔
183
      this.status === "aborted"
1,509✔
184
    );
1,646✔
185
  }
1,646✔
186

1✔
187
  /**
1✔
188
   * Whether the task has failed.
1✔
189
   */
1✔
190
  get isFailed(): boolean {
1✔
191
    return this.status === "failed";
128✔
192
  }
128✔
193

1✔
194
  /**
1✔
195
   * Whether the task has been aborted.
1✔
196
   */
1✔
197
  get isAborted(): boolean {
1✔
198
    return this.status === "aborted" || this.status === "aborting";
×
199
  }
×
200

1✔
201
  /**
1✔
202
   * Gets the duration of the task execution in milliseconds.
1✔
203
   */
1✔
204
  get executeDuration(): number | undefined {
1✔
205
    return this._executeDuration;
×
206
  }
×
207

1✔
208
  /**
1✔
209
   * Gets the result produced by the task.
1✔
210
   */
1✔
211
  get result(): any {
1✔
212
    return this._result;
53✔
213
  }
53✔
214

1✔
215
  /**
1✔
216
   * Gets the error if the task failed.
1✔
217
   */
1✔
218
  get error(): any {
1✔
219
    return this._error;
127✔
220
  }
127✔
221

1✔
222
  /**
1✔
223
   * Gets the list of tasks this task depends on.
1✔
224
   */
1✔
225
  get dependencies(): Task[] | undefined {
1✔
226
    return this._dependencies;
×
227
  }
×
228

1✔
229
  /**
1✔
230
   * Gets the list of child tasks that failed.
1✔
231
   */
1✔
232
  get failedChildren(): Task[] | undefined {
1✔
233
    return this._failedChildren;
×
234
  }
×
235

1✔
236
  /**
1✔
237
   * Gets the list of dependencies that failed.
1✔
238
   */
1✔
239
  get failedDependencies(): Task[] | undefined {
1✔
240
    return this._failedDependencies;
×
241
  }
×
242

1✔
243
  /**
1✔
244
   * Whether the task is currently waiting for children or dependencies to finish.
1✔
245
   */
1✔
246
  get needWaiting(): boolean {
1✔
247
    if (this._waitingFor && this._waitingFor.size) return true;
×
248
    if (this._children) {
×
249
      for (const c of this._children) {
×
250
        if (c.needWaiting) return true;
×
251
      }
×
252
    }
×
253
    return false;
×
254
  }
×
255

1✔
256
  /**
1✔
257
   * Gets a list of tasks that this task is currently waiting for.
1✔
258
   *
1✔
259
   * @returns An array of {@link Task} instances or `undefined`.
1✔
260
   */
1✔
261
  getWaitingTasks(): Task[] | undefined {
1✔
262
    if (
171✔
263
      !(this.status === "waiting" && this._waitingFor && this._waitingFor.size)
171✔
264
    )
171✔
265
      return;
171✔
266
    const out = Array.from(this._waitingFor);
12✔
267
    if (this._children) {
171!
268
      for (const c of this._children) {
×
269
        const childTasks = c.getWaitingTasks();
×
270
        if (childTasks) {
×
271
          childTasks.forEach((t) => {
×
272
            if (!out.includes(t)) out.push(t);
×
273
          });
×
274
        }
×
275
      }
×
276
    }
✔
277
    return out;
12✔
278
  }
12✔
279

1✔
280
  /**
1✔
281
   * Aborts the task execution.
1✔
282
   *
1✔
283
   * @returns The task instance.
1✔
284
   */
1✔
285
  abort(): this {
1✔
286
    if (this.isFinished || this.status === "aborting") return this;
39✔
287

23✔
288
    if (!this.isStarted) {
39✔
289
      this._update({ status: "aborted", message: "aborted" });
5✔
290
      return this;
5✔
291
    }
5✔
292

18✔
293
    const ctx = this[taskContextKey] as TaskContext;
18✔
294
    const timeout = this.options.abortTimeout || 30000;
39✔
295
    this._update({ status: "aborting", message: "Aborting" });
39✔
296
    if (timeout) {
39✔
297
      this._abortTimer = setTimeout(() => {
18✔
298
        delete this._abortTimer;
1✔
299
        this._update({ status: "aborted", message: "aborted" });
1✔
300
      }, timeout).unref();
18✔
301
    }
18✔
302
    this._abortChildren()
18✔
303
      .catch(noOp)
18✔
304
      .then(() => {
18✔
305
        if (this.isFinished) return;
18✔
306
        if (ctx.executingTasks.has(this)) {
18✔
307
          this._abortController.abort();
12✔
308
          return;
12✔
309
        }
12✔
310
        this._update({ status: "aborted", message: "aborted" });
5✔
311
      })
5✔
312
      .catch(noOp);
18✔
313
    return this;
18✔
314
  }
18✔
315

1✔
316
  /**
1✔
317
   * Starts the task execution.
1✔
318
   *
1✔
319
   * @returns The task instance.
1✔
320
   */
1✔
321
  start(): this {
1✔
322
    if (this.isStarted) return this;
40!
323
    this._id = this._id || "t" + ++idGen;
40✔
324
    const ctx = (this[taskContextKey] = new TaskContext());
40✔
325
    ctx.concurrency = this.options.concurrency || osCPUs;
40✔
326
    let pulseTimer: NodeJS.Timer | undefined;
40✔
327
    ctx.triggerPulse = () => {
40✔
328
      if (pulseTimer || this.isFinished) return;
94✔
329
      pulseTimer = setTimeout(() => {
28✔
330
        pulseTimer = undefined;
28✔
331
        this._pulse();
28✔
332
      }, 1);
28✔
333
    };
28✔
334
    if (this.options.children) {
40✔
335
      this._determineChildrenTree((err) => {
14✔
336
        if (err) {
14!
337
          this._update({
×
338
            status: "failed",
×
339
            error: err,
×
340
            message: "Unable to fetch child tasks. " + (err.message || err),
×
341
          });
×
342
          return;
×
343
        }
×
344
        this._determineChildrenDependencies([]);
14✔
345
        this._start();
14✔
346
      });
14✔
347
    } else this._start();
40✔
348
    return this;
39✔
349
  }
39✔
350

1✔
351
  /**
1✔
352
   * Returns a promise that resolves with the task result or rejects with the task error.
1✔
353
   * If the task has not started and is not managed by a queue, it will be started automatically.
1✔
354
   *
1✔
355
   * @returns A promise that resolves when the task completes.
1✔
356
   */
1✔
357
  toPromise(): Promise<T> {
1✔
358
    return new Promise((resolve, reject) => {
52✔
359
      if (this.isFinished) {
52✔
360
        if (this.isFailed) reject(this.error);
3!
361
        else resolve(this.result);
3✔
362
        return;
3✔
363
      }
3✔
364
      this.once("finish", () => {
49✔
365
        if (this.isFailed) return reject(this.error);
48✔
366
        resolve(this.result);
42✔
367
      });
42✔
368
      if (!this.isStarted && !this._isManaged) this.start();
52✔
369
    });
52✔
370
  }
52✔
371

1✔
372
  /**
1✔
373
   * Resolves the child tasks tree.
1✔
374
   *
1✔
375
   * @param callback - Callback to invoke when the tree is resolved.
1✔
376
   * @protected
1✔
377
   */
1✔
378
  protected _determineChildrenTree(callback: (err?: any) => void): void {
1✔
379
    const ctx = this[taskContextKey] as TaskContext;
15✔
380
    const options = this._options;
15✔
381
    const handler = (err?: any, value?: any) => {
15✔
382
      if (err) return callback(err);
19!
383
      if (!value) return callback();
19!
384

19✔
385
      if (typeof value === "function") {
19✔
386
        try {
2✔
387
          const x: any = value();
2✔
388
          handler(undefined, x);
2✔
389
        } catch (err2) {
2!
390
          handler(err2);
×
391
        }
×
392
        return;
2✔
393
      }
2✔
394

17✔
395
      if (Array.isArray(value)) {
19✔
396
        let idx = 1;
15✔
397
        const children = value.reduce<Task[]>((a, v) => {
15✔
398
          // noinspection SuspiciousTypeOfGuard
58✔
399
          if (typeof v === "function") {
58✔
400
            v = new Task(v, {
5✔
401
              concurrency: options.concurrency,
5✔
402
              bail: options.bail,
5✔
403
            });
5✔
404
          }
5✔
405
          if (v instanceof Task) {
58✔
406
            v[taskContextKey] = ctx;
58✔
407
            v._id = v._id || this._id + "-" + idx++;
58✔
408
            const listeners = this.listeners("update-recursive");
58✔
409
            listeners.forEach((listener) =>
58✔
410
              v.on("update-recursive", listener as any),
55✔
411
            );
58✔
412
            a.push(v);
58✔
413
          }
58✔
414
          return a;
58✔
415
        }, []);
15✔
416

15✔
417
        if (children && children.length) {
15✔
418
          this._children = children;
15✔
419
          let i = 0;
15✔
420
          const next = (err2?: any) => {
15✔
421
            if (err2) return callback(err2);
73!
422
            if (i >= children.length) return callback();
73✔
423
            const c = children[i++];
58✔
424
            if (c.options.children)
58✔
425
              c._determineChildrenTree((err3) => next(err3));
73✔
426
            else next();
57✔
427
          };
73✔
428
          next();
15✔
429
        } else callback();
15!
430
        return;
14✔
431
      }
14✔
432
      if (value && typeof value.then === "function") {
19✔
433
        (value as Promise<TaskLike[]>)
2✔
434
          .then((v) => handler(undefined, v))
2✔
435
          .catch((e) => handler(e));
2✔
436
        return;
2✔
437
      }
2!
438

×
439
      callback(new Error("Invalid value returned from children() method."));
×
440
    };
×
441
    handler(undefined, this._options.children);
15✔
442
  }
15✔
443

1✔
444
  /**
1✔
445
   * Resolves dependencies for child tasks.
1✔
446
   *
1✔
447
   * @param scope - The list of tasks to consider for dependency resolution.
1✔
448
   * @protected
1✔
449
   */
1✔
450
  protected _determineChildrenDependencies(scope: Task[]): void {
1✔
451
    if (!this._children) return;
72✔
452

15✔
453
    const detectCircular = (
15✔
454
      t: Task,
20✔
455
      dependencies: Task[],
20✔
456
      path: string = "",
20✔
457
      list?: Set<Task>,
20✔
458
    ) => {
20✔
459
      path = path || t.name || t.id;
20!
460
      list = list || new Set();
20✔
461
      for (const l1 of dependencies.values()) {
20✔
462
        if (l1 === t) throw new Error(`Circular dependency detected. ${path}`);
20✔
463
        if (list.has(l1)) continue;
20!
464
        list.add(l1);
19✔
465
        if (l1._dependencies)
19✔
466
          detectCircular(
20✔
467
            t,
5✔
468
            l1._dependencies,
5✔
469
            path + " > " + (l1.name || l1.id),
5!
470
            list,
5✔
471
          );
17✔
472

17✔
473
        if (l1.children) {
20!
474
          for (const c of l1.children) {
×
475
            if (c === t)
×
476
              throw new Error(`Circular dependency detected. ${path}`);
×
477
            if (list.has(c)) continue;
×
478
            list.add(c);
×
479
            if (c._dependencies) detectCircular(t, c._dependencies, path, list);
×
480
          }
×
481
        }
×
482
      }
20✔
483
    };
17✔
484

15✔
485
    const subScope = [...scope, ...Array.from(this._children)];
15✔
486
    for (const c of this._children.values()) {
72✔
487
      c._determineChildrenDependencies(subScope);
58✔
488
      if (!c.options.dependencies) continue;
58✔
489

15✔
490
      const dependencies: Task[] = [];
15✔
491
      const waitingFor = new Set<Task>();
15✔
492
      for (const dep of c.options.dependencies) {
15✔
493
        const dependentTask = subScope.find((x) =>
15✔
494
          typeof dep === "string" ? x.name === dep : x === dep,
48✔
495
        );
15✔
496
        if (!dependentTask || c === dependentTask) continue;
15!
497
        dependencies.push(dependentTask);
15✔
498
        if (!dependentTask.isFinished) waitingFor.add(dependentTask);
15✔
499
      }
15✔
500
      detectCircular(c, dependencies);
15✔
501
      if (dependencies.length) c._dependencies = dependencies;
58✔
502
      if (waitingFor.size) c._waitingFor = waitingFor;
14✔
503
      c._captureDependencies();
14✔
504
    }
14✔
505
  }
14✔
506

1✔
507
  /**
1✔
508
   * Captures dependencies and sets up listeners for their completion.
1✔
509
   * @protected
1✔
510
   */
1✔
511
  protected _captureDependencies(): void {
1✔
512
    if (!this._waitingFor) return;
14!
513
    const failedDependencies: Task[] = [];
14✔
514
    const waitingFor = this._waitingFor;
14✔
515
    const signal = this._abortController.signal;
14✔
516

14✔
517
    const abortSignalCallback = () => clearWait();
14✔
518
    signal.addEventListener("abort", abortSignalCallback, { once: true });
14✔
519

14✔
520
    const handleDependentAborted = () => {
14✔
521
      signal.removeEventListener("abort", abortSignalCallback);
×
522
      this._abortChildren()
×
523
        .then(() => {
×
524
          const isFailed = !!failedDependencies.find(
×
525
            (d) => d.status === "failed",
×
526
          );
×
527
          const error: any = new Error(
×
528
            "Aborted due to " +
×
529
              (isFailed ? "fail" : "cancellation") +
×
530
              " of dependent " +
×
531
              plural("task", !!failedDependencies.length),
×
532
          );
×
533
          error.failedDependencies = failedDependencies;
×
534
          this._failedDependencies = failedDependencies;
×
535
          this._update({
×
536
            status: isFailed ? "failed" : "aborted",
×
537
            message: error.message,
×
538
            error,
×
539
          });
×
540
        })
×
541
        .catch(noOp);
×
542
    };
×
543

14✔
544
    const clearWait = () => {
14✔
545
      for (const t of waitingFor) {
4✔
546
        t.removeListener("finish", finishCallback);
4✔
547
      }
4✔
548
      delete this._waitingFor;
4✔
549
    };
4✔
550

14✔
551
    const finishCallback = async (t) => {
14✔
552
      if (this.isStarted && this.status !== "waiting") {
12✔
553
        clearWait();
4✔
554
        return;
4✔
555
      }
4✔
556
      waitingFor.delete(t);
8✔
557
      if (t.isFailed || t.status === "aborted") {
12!
558
        failedDependencies.push(t);
×
559
      }
✔
560

8✔
561
      // If all dependent tasks completed
8✔
562
      if (!waitingFor.size) {
8✔
563
        delete this._waitingFor;
8✔
564
        signal.removeEventListener("abort", abortSignalCallback);
8✔
565

8✔
566
        // If any of dependent tasks are failed
8✔
567
        if (failedDependencies.length) {
8!
568
          handleDependentAborted();
×
569
          return;
×
570
        }
×
571
        // If all dependent tasks completed successfully we continue to next step (startChildren)
8✔
572
        if (this.isStarted) this._startChildren();
8✔
573
        else await this.emitAsync("wait-end");
1✔
574
      }
8✔
575
    };
12✔
576

14✔
577
    for (const t of waitingFor.values()) {
14✔
578
      if (t.isFailed || t.status === "aborted") {
14!
579
        waitingFor.delete(t);
×
580
        failedDependencies.push(t);
×
581
      } else t.prependOnceListener("finish", finishCallback);
14✔
582
    }
14✔
583
    if (!waitingFor.size) handleDependentAborted();
14!
584
  }
14✔
585

1✔
586
  /**
1✔
587
   * Internal method to initiate task execution.
1✔
588
   * @protected
1✔
589
   */
1✔
590
  protected _start(): void {
1✔
591
    if (this.isStarted || this.isFinished) return;
92!
592

92✔
593
    if (this._waitingFor) {
92✔
594
      this._update({
12✔
595
        status: "waiting",
12✔
596
        message: "Waiting for dependencies",
12✔
597
        waitingFor: true,
12✔
598
      });
12✔
599
      return;
12✔
600
    }
12✔
601
    this._startChildren();
80✔
602
  }
80✔
603

1✔
604
  /**
1✔
605
   * Internal method to start child tasks execution.
1✔
606
   * @protected
1✔
607
   */
1✔
608
  protected _startChildren() {
1✔
609
    const children = this._children;
87✔
610
    if (!children) {
87✔
611
      this._pulse();
73✔
612
      return;
73✔
613
    }
73✔
614

14✔
615
    const options = this.options;
14✔
616
    const childrenLeft = (this._childrenLeft = new Set(children));
14✔
617
    const failedChildren: Task[] = [];
14✔
618

14✔
619
    const statusChangeCallback = async (t: Task) => {
14✔
620
      if (this.status === "aborting") return;
128✔
621
      if (t.status === "running")
104✔
622
        this._update({ status: "running", message: "Running" });
128✔
623
      if (t.status === "waiting")
104✔
624
        this._update({ status: "waiting", message: "Waiting" });
128✔
625
    };
128✔
626

14✔
627
    const finishCallback = async (t: Task) => {
14✔
628
      t.removeListener("status-change", statusChangeCallback);
55✔
629
      childrenLeft.delete(t);
55✔
630
      if (t.isFailed || t.status === "aborted") {
55✔
631
        failedChildren.push(t);
19✔
632
        if (options.bail && childrenLeft.size) {
19✔
633
          const running = !!children.find((c) => c.isStarted);
13✔
634
          if (running)
13✔
635
            this._update({ status: "aborting", message: "Aborting" });
13✔
636
          this._abortChildren().catch(noOp);
13✔
637
          return;
13✔
638
        }
13✔
639
      }
19✔
640

42✔
641
      if (!childrenLeft.size) {
55✔
642
        delete this._childrenLeft;
14✔
643
        if (failedChildren.length) {
14✔
644
          const isFailed = !!failedChildren.find((d) => d.status === "failed");
7✔
645
          const error: any = new Error(
7✔
646
            "Aborted due to " +
7✔
647
              (isFailed ? "fail" : "cancellation") +
7✔
648
              " of child " +
7✔
649
              plural("task", !!failedChildren.length),
7✔
650
          );
7✔
651
          error.failedChildren = failedChildren;
7✔
652
          this._failedChildren = failedChildren;
7✔
653
          this._update({
7✔
654
            status: isFailed ? "failed" : "aborted",
7✔
655
            error,
7✔
656
            message: error.message,
7✔
657
          });
7✔
658
          return;
7✔
659
        }
7✔
660
      }
14✔
661
      this._pulse();
35✔
662
    };
35✔
663

14✔
664
    for (const c of children) {
87✔
665
      c.prependOnceListener("wait-end", () => this._pulse());
55✔
666
      c.prependOnceListener("finish", finishCallback);
55✔
667
      c.prependListener("status-change", statusChangeCallback);
55✔
668
    }
55✔
669

14✔
670
    this._pulse();
14✔
671
  }
14✔
672

1✔
673
  /**
1✔
674
   * Internal method that manages the execution flow of the task and its children.
1✔
675
   * @protected
1✔
676
   */
1✔
677
  protected _pulse() {
1✔
678
    const ctx = this[taskContextKey] as TaskContext;
224✔
679

224✔
680
    if (
224✔
681
      this.isFinished ||
224✔
682
      this._waitingFor ||
224✔
683
      this.status === "aborting" ||
185✔
684
      ctx.executingTasks.has(this)
182✔
685
    )
224✔
686
      return;
224✔
687

140✔
688
    const options = this.options;
140✔
689
    if (this._childrenLeft) {
224✔
690
      // Check if we can run multiple child tasks
60✔
691
      for (const c of this._childrenLeft) {
60✔
692
        if (
169✔
693
          (c.isStarted && options.serial) ||
169✔
694
          (c.status === "running" && c.options.exclusive)
169✔
695
        ) {
169✔
696
          c._pulse();
4✔
697
          return;
4✔
698
        }
4✔
699
      }
169✔
700

56✔
701
      // Check waiting children
56✔
702
      let hasExclusive = false;
56✔
703
      let hasRunning = false;
56✔
704
      for (const c of this._childrenLeft) {
60✔
705
        if (c.isFinished) continue;
165!
706
        hasExclusive = hasExclusive || !!c.options.exclusive;
165✔
707
        hasRunning = hasRunning || c.status === "running";
165✔
708
      }
165✔
709
      if (hasExclusive && hasRunning) return;
60!
710

56✔
711
      // start children
56✔
712
      let k = ctx.concurrency - ctx.executingTasks.size;
56✔
713
      for (const c of this._childrenLeft) {
60✔
714
        if (c.isStarted) {
131✔
715
          c._pulse();
69✔
716
          continue;
69✔
717
        }
69✔
718
        if (k-- <= 0) return;
131✔
719
        if (
54✔
720
          c.options.exclusive &&
54✔
721
          (ctx.executingTasks.size || ctx.executingTasks.size)
2✔
722
        )
131✔
723
          return;
131✔
724
        c._start();
53✔
725
        if (options.serial || (c.status === "running" && c.options.exclusive))
131✔
726
          return;
131✔
727
      }
131✔
728
    }
38✔
729

118✔
730
    if (
118✔
731
      (this._childrenLeft && this._childrenLeft.size) ||
224✔
732
      ctx.executingTasks.size >= ctx.concurrency
80✔
733
    )
224✔
734
      return;
224✔
735

80✔
736
    this._update({ status: "running", message: "Running" });
80✔
737
    ctx.executingTasks.add(this);
80✔
738
    const t = Date.now();
80✔
739
    this._execute()
80✔
740
      .then((result: any) => {
80✔
741
        ctx.executingTasks.delete(this);
63✔
742
        this._executeDuration = Date.now() - t;
63✔
743
        this._update({
63✔
744
          status: "fulfilled",
63✔
745
          message: "Task completed",
63✔
746
          result,
63✔
747
        });
63✔
748
      })
63✔
749
      .catch((error) => {
80✔
750
        ctx.executingTasks.delete(this);
17✔
751
        this._executeDuration = Date.now() - t;
17✔
752
        if (error.code === "ABORT_ERR") {
17✔
753
          this._update({
9✔
754
            status: "aborted",
9✔
755
            error,
9✔
756
            message: error instanceof Error ? error.message : "" + error,
9!
757
          });
9✔
758
          return;
9✔
759
        }
9✔
760
        this._update({
8✔
761
          status: "failed",
8✔
762
          error,
8✔
763
          message: error instanceof Error ? error.message : "" + error,
17!
764
        });
17✔
765
      });
17✔
766
  }
80✔
767

1✔
768
  /**
1✔
769
   * Executes the task's function.
1✔
770
   * @protected
1✔
771
   */
1✔
772
  protected async _execute() {
1✔
773
    return this._executeFn?.({
80✔
774
      task: this,
77✔
775
      signal: this._abortController.signal,
77✔
776
    });
77✔
777
  }
80✔
778

1✔
779
  /**
1✔
780
   * Updates task properties and emits relevant events.
1✔
781
   *
1✔
782
   * @param prop - The property values to update.
1✔
783
   * @protected
1✔
784
   */
1✔
785
  protected _update(prop: TaskUpdateValues) {
1✔
786
    const oldFinished = this.isFinished;
279✔
787
    const keys: string[] = [];
279✔
788
    const oldStarted = this.isStarted;
279✔
789
    if (prop.status && this._status !== prop.status) {
279✔
790
      this._status = prop.status;
221✔
791
      keys.push("status");
221✔
792
    }
221✔
793
    if (prop.message && this._message !== prop.message) {
279✔
794
      this._message = prop.message;
212✔
795
      keys.push("message");
212✔
796
    }
212✔
797
    if (prop.error && this._error !== prop.error) {
279✔
798
      this._error = prop.error;
24✔
799
      keys.push("error");
24✔
800
    }
24✔
801
    if (prop.result && this._result !== prop.result) {
279✔
802
      this._result = prop.result;
15✔
803
      keys.push("result");
15✔
804
    }
15✔
805
    if (prop.waitingFor) {
279✔
806
      keys.push("waitingFor");
12✔
807
    }
12✔
808
    if (keys.length) {
279✔
809
      if (keys.includes("status")) {
221✔
810
        if (!oldStarted) this.emitAsync("start", this).catch(noOp);
221✔
811
        this.emitAsync("status-change", this, this.status).catch(noOp);
221✔
812
        if (this._status === "running") this.emitAsync("run", this).catch(noOp);
221✔
813
      }
221✔
814
      this.emitAsync("update", this, keys).catch(noOp);
221✔
815
      this.emitAsync("update-recursive", this, keys).catch(noOp);
221✔
816
      if (this.isFinished && !oldFinished) {
221✔
817
        const ctx = this[taskContextKey];
97✔
818
        if (this._abortTimer) {
97✔
819
          clearTimeout(this._abortTimer);
17✔
820
          delete this._abortTimer;
17✔
821
        }
17✔
822
        delete this[taskContextKey];
97✔
823
        if (this.error) this.emitAsync("error", this.error, this).catch(noOp);
97✔
824
        this.emitAsync("finish", this).catch(noOp);
97✔
825
        if (ctx) ctx.triggerPulse();
97✔
826
      }
97✔
827
    }
221✔
828
  }
279✔
829

1✔
830
  /**
1✔
831
   * Internal method to abort all child tasks.
1✔
832
   * @protected
1✔
833
   */
1✔
834
  protected async _abortChildren(): Promise<void> {
1✔
835
    const promises: Promise<void>[] = [];
31✔
836
    if (this._children) {
31✔
837
      for (let i = this._children.length - 1; i >= 0; i--) {
14✔
838
        const child = this._children[i];
62✔
839
        if (!child.isFinished) {
62✔
840
          child.abort();
29✔
841
          promises.push(child.toPromise());
29✔
842
        }
29✔
843
      }
62✔
844
    }
14✔
845
    if (promises.length) await Promise.all(promises);
31✔
846
  }
31✔
847
}
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