@calmo/task-runner 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +30 -0
  3. package/coverage/coverage-final.json +7 -6
  4. package/coverage/index.html +12 -12
  5. package/coverage/lcov-report/index.html +12 -12
  6. package/coverage/lcov-report/src/EventBus.ts.html +4 -4
  7. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +33 -33
  8. package/coverage/lcov-report/src/TaskRunner.ts.html +30 -18
  9. package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +1 -1
  10. package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +17 -2
  11. package/coverage/lcov-report/src/TaskStateManager.ts.html +35 -35
  12. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +94 -34
  13. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  14. package/coverage/lcov-report/src/contracts/index.html +1 -1
  15. package/coverage/lcov-report/src/index.html +7 -7
  16. package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
  17. package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +355 -0
  18. package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +3 -3
  19. package/coverage/lcov-report/src/strategies/index.html +20 -5
  20. package/coverage/lcov.info +328 -240
  21. package/coverage/src/EventBus.ts.html +4 -4
  22. package/coverage/src/TaskGraphValidator.ts.html +33 -33
  23. package/coverage/src/TaskRunner.ts.html +30 -18
  24. package/coverage/src/TaskRunnerBuilder.ts.html +1 -1
  25. package/coverage/src/TaskRunnerExecutionConfig.ts.html +17 -2
  26. package/coverage/src/TaskStateManager.ts.html +35 -35
  27. package/coverage/src/WorkflowExecutor.ts.html +94 -34
  28. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  29. package/coverage/src/contracts/index.html +1 -1
  30. package/coverage/src/index.html +7 -7
  31. package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
  32. package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +355 -0
  33. package/coverage/src/strategies/StandardExecutionStrategy.ts.html +3 -3
  34. package/coverage/src/strategies/index.html +20 -5
  35. package/dist/TaskRunner.js +3 -2
  36. package/dist/TaskRunner.js.map +1 -1
  37. package/dist/TaskRunnerExecutionConfig.d.ts +5 -0
  38. package/dist/TaskStep.d.ts +3 -0
  39. package/dist/WorkflowExecutor.d.ts +4 -1
  40. package/dist/WorkflowExecutor.js +19 -4
  41. package/dist/WorkflowExecutor.js.map +1 -1
  42. package/dist/contracts/TaskRetryConfig.d.ts +8 -0
  43. package/dist/contracts/TaskRetryConfig.js +2 -0
  44. package/dist/contracts/TaskRetryConfig.js.map +1 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/strategies/RetryingExecutionStrategy.d.ts +12 -0
  49. package/dist/strategies/RetryingExecutionStrategy.js +74 -0
  50. package/dist/strategies/RetryingExecutionStrategy.js.map +1 -0
  51. package/openspec/changes/archive/2026-01-18-add-task-retry-policy/tasks.md +10 -0
  52. package/openspec/changes/archive/2026-01-18-add-workflow-preview/tasks.md +7 -0
  53. package/openspec/project.md +1 -0
  54. package/package.json +1 -1
  55. package/src/TaskRunner.ts +6 -2
  56. package/src/TaskRunnerExecutionConfig.ts +5 -0
  57. package/src/TaskStep.ts +3 -0
  58. package/src/WorkflowExecutor.ts +24 -4
  59. package/src/contracts/TaskRetryConfig.ts +8 -0
  60. package/src/index.ts +2 -0
  61. package/src/strategies/RetryingExecutionStrategy.ts +90 -0
  62. package/test-report.xml +122 -92
  63. package/openspec/changes/add-task-retry-policy/tasks.md +0 -10
  64. package/openspec/changes/add-workflow-preview/tasks.md +0 -7
  65. /package/openspec/changes/{add-task-retry-policy → archive/2026-01-18-add-task-retry-policy}/proposal.md +0 -0
  66. /package/openspec/changes/{add-workflow-preview → archive/2026-01-18-add-workflow-preview}/proposal.md +0 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { IExecutionStrategy } from "./IExecutionStrategy.js";
2
+ import { TaskStep } from "../TaskStep.js";
3
+ import { TaskResult } from "../TaskResult.js";
4
+ /**
5
+ * Execution strategy that retries tasks upon failure based on their retry configuration.
6
+ */
7
+ export declare class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<TContext> {
8
+ private innerStrategy;
9
+ constructor(innerStrategy: IExecutionStrategy<TContext>);
10
+ execute(step: TaskStep<TContext>, context: TContext, signal?: AbortSignal): Promise<TaskResult>;
11
+ private sleep;
12
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Execution strategy that retries tasks upon failure based on their retry configuration.
3
+ */
4
+ export class RetryingExecutionStrategy {
5
+ innerStrategy;
6
+ constructor(innerStrategy) {
7
+ this.innerStrategy = innerStrategy;
8
+ }
9
+ async execute(step, context, signal) {
10
+ const config = step.retry;
11
+ if (!config) {
12
+ return this.innerStrategy.execute(step, context, signal);
13
+ }
14
+ let attempt = 0;
15
+ while (true) {
16
+ // Check for cancellation before execution
17
+ if (signal?.aborted) {
18
+ return {
19
+ status: "cancelled",
20
+ message: "Task cancelled before execution",
21
+ };
22
+ }
23
+ const result = await this.innerStrategy.execute(step, context, signal);
24
+ if (result.status === "success" || result.status === "cancelled" || result.status === "skipped") {
25
+ return result;
26
+ }
27
+ // Task failed, check if we should retry
28
+ if (attempt >= config.attempts) {
29
+ return result; // Max attempts reached, return failure
30
+ }
31
+ attempt++;
32
+ // Calculate delay
33
+ let delay = config.delay;
34
+ if (config.backoff === "exponential") {
35
+ delay = config.delay * Math.pow(2, attempt - 1);
36
+ }
37
+ // Wait for delay, respecting cancellation
38
+ try {
39
+ await this.sleep(delay, signal);
40
+ }
41
+ catch (e) {
42
+ if (signal?.aborted) {
43
+ return {
44
+ status: "cancelled",
45
+ message: "Task cancelled during retry delay",
46
+ };
47
+ }
48
+ throw e;
49
+ }
50
+ }
51
+ }
52
+ sleep(ms, signal) {
53
+ return new Promise((resolve, reject) => {
54
+ if (signal?.aborted) {
55
+ reject(new Error("AbortError"));
56
+ return;
57
+ }
58
+ const timer = setTimeout(() => {
59
+ cleanup();
60
+ resolve();
61
+ }, ms);
62
+ const onAbort = () => {
63
+ clearTimeout(timer);
64
+ cleanup();
65
+ reject(new Error("AbortError"));
66
+ };
67
+ const cleanup = () => {
68
+ signal?.removeEventListener("abort", onAbort);
69
+ };
70
+ signal?.addEventListener("abort", onAbort);
71
+ });
72
+ }
73
+ }
74
+ //# sourceMappingURL=RetryingExecutionStrategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RetryingExecutionStrategy.js","sourceRoot":"","sources":["../../src/strategies/RetryingExecutionStrategy.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,OAAO,yBAAyB;IAChB;IAApB,YAAoB,aAA2C;QAA3C,kBAAa,GAAb,aAAa,CAA8B;IAAG,CAAC;IAEnE,KAAK,CAAC,OAAO,CACX,IAAwB,EACxB,OAAiB,EACjB,MAAoB;QAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;QAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAC3D,CAAC;QAED,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,OAAO,IAAI,EAAE,CAAC;YACZ,0CAA0C;YAC1C,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACpB,OAAO;oBACL,MAAM,EAAE,WAAW;oBACnB,OAAO,EAAE,iCAAiC;iBAC3C,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YAEvE,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAChG,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,wCAAwC;YACxC,IAAI,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC/B,OAAO,MAAM,CAAC,CAAC,uCAAuC;YACxD,CAAC;YAED,OAAO,EAAE,CAAC;YAEV,kBAAkB;YAClB,IAAI,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YACzB,IAAI,MAAM,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;gBACrC,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;YAClD,CAAC;YAED,0CAA0C;YAC1C,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAClC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACb,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACpB,OAAO;wBACL,MAAM,EAAE,WAAW;wBACnB,OAAO,EAAE,mCAAmC;qBAC7C,CAAC;gBACJ,CAAC;gBACD,MAAM,CAAC,CAAC;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,EAAU,EAAE,MAAoB;QAC5C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACpB,MAAM,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;gBAChC,OAAO;YACT,CAAC;YAED,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,OAAO,EAAE,CAAC;gBACV,OAAO,EAAE,CAAC;YACZ,CAAC,EAAE,EAAE,CAAC,CAAC;YAEP,MAAM,OAAO,GAAG,GAAG,EAAE;gBACnB,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,OAAO,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;YAClC,CAAC,CAAC;YAEF,MAAM,OAAO,GAAG,GAAG,EAAE;gBACnB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAChD,CAAC,CAAC;YAEF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ ## Implementation
2
+ - [x] 1.1 Create `TaskRetryConfig` interface with `attempts`, `delay`, and `backoff`.
3
+ - [x] 1.2 Update `TaskStep` interface to include optional `retry: TaskRetryConfig`.
4
+ - [x] 1.3 Update execution logic to catch task failures.
5
+ - [x] 1.4 Implement retry loop/recursion checking `attempts` count.
6
+ - [x] 1.5 Implement delay logic with support for `fixed` and `exponential` backoff.
7
+ - [x] 1.6 Ensure `TaskResult` reflects the final status after retries (success if eventually succeeds, failure if all attempts fail).
8
+ - [x] 1.7 Add unit tests for successful retry after failure.
9
+ - [x] 1.8 Add unit tests for exhaustion of retry attempts (final failure).
10
+ - [x] 1.9 Add unit tests for backoff timing (mock timers).
@@ -0,0 +1,7 @@
1
+ ## Implementation
2
+ - [x] 1.1 Update `TaskRunnerExecutionConfig` to include an optional `dryRun: boolean` property.
3
+ - [x] 1.2 Implement `dryRun` logic in `WorkflowExecutor` (traverse graph, validate order, skip `step.run()`, return `skipped` or `success` pseudo-status).
4
+ - [x] 1.3 Implement `getMermaidGraph(steps: TaskStep[])` method (can be static or instance method on `TaskRunner`).
5
+ - [x] 1.4 Ensure `dryRun` respects other configs like `concurrency` (if applicable) to simulate actual timing/order if possible, or just strict topological order.
6
+ - [x] 1.5 Add unit tests for `dryRun` ensuring no side effects occur.
7
+ - [x] 1.6 Add unit tests for `getMermaidGraph` output correctness (nodes and edges).
@@ -23,6 +23,7 @@ The project follows a modular architecture with distinct components for managing
23
23
  - **Commit Messages:** Follows conventional commits enforced by Commitlint.
24
24
  - **Git Hooks:** Utilizes Husky for pre-commit and commit-msg hooks.
25
25
  - **Testing:** Uses Vitest for unit and integration testing.
26
+ - **Atomic Commits:** When working on complex multi-task features, commit after each distinct task, ensuring build, lint, and test success to establish safe rollback points.
26
27
 
27
28
  ## Build/Test/Run Commands
28
29
  - **Install Dependencies:** `pnpm install`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmo/task-runner",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/src/TaskRunner.ts CHANGED
@@ -9,6 +9,7 @@ import { TaskRunnerExecutionConfig } from "./TaskRunnerExecutionConfig.js";
9
9
  import { TaskStateManager } from "./TaskStateManager.js";
10
10
  import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
11
11
  import { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
12
+ import { RetryingExecutionStrategy } from "./strategies/RetryingExecutionStrategy.js";
12
13
  import { DryRunExecutionStrategy } from "./strategies/DryRunExecutionStrategy.js";
13
14
 
14
15
  // Re-export types for backward compatibility
@@ -22,7 +23,9 @@ export { RunnerEventPayloads, RunnerEventListener, TaskRunnerExecutionConfig };
22
23
  export class TaskRunner<TContext> {
23
24
  private eventBus = new EventBus<TContext>();
24
25
  private validator = new TaskGraphValidator();
25
- private executionStrategy: IExecutionStrategy<TContext> = new StandardExecutionStrategy();
26
+ private executionStrategy: IExecutionStrategy<TContext> = new RetryingExecutionStrategy(
27
+ new StandardExecutionStrategy()
28
+ );
26
29
 
27
30
  /**
28
31
  * @param context The shared context object to be passed to each task.
@@ -149,7 +152,8 @@ export class TaskRunner<TContext> {
149
152
  this.context,
150
153
  this.eventBus,
151
154
  stateManager,
152
- strategy
155
+ strategy,
156
+ config?.concurrency
153
157
  );
154
158
 
155
159
  // We need to handle the timeout cleanup properly.
@@ -15,4 +15,9 @@ export interface TaskRunnerExecutionConfig {
15
15
  * Useful for verifying the execution order and graph structure.
16
16
  */
17
17
  dryRun?: boolean;
18
+ /**
19
+ * The maximum number of tasks to run concurrently.
20
+ * If undefined, all ready tasks will be run in parallel.
21
+ */
22
+ concurrency?: number;
18
23
  }
package/src/TaskStep.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { TaskResult } from "./TaskResult.js";
2
+ import { TaskRetryConfig } from "./contracts/TaskRetryConfig.js";
2
3
 
3
4
  /**
4
5
  * Represents a single, executable step within a workflow.
@@ -9,6 +10,8 @@ export interface TaskStep<TContext> {
9
10
  name: string;
10
11
  /** An optional list of task names that must complete successfully before this step can run. */
11
12
  dependencies?: string[];
13
+ /** Optional retry configuration for the task. */
14
+ retry?: TaskRetryConfig;
12
15
  /**
13
16
  * The core logic of the task.
14
17
  * @param context The shared context object, allowing for state to be passed between tasks.
@@ -9,17 +9,21 @@ import { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
9
9
  * @template TContext The shape of the shared context object.
10
10
  */
11
11
  export class WorkflowExecutor<TContext> {
12
+ private readyQueue: TaskStep<TContext>[] = [];
13
+
12
14
  /**
13
15
  * @param context The shared context object.
14
16
  * @param eventBus The event bus to emit events.
15
17
  * @param stateManager Manages execution state.
16
18
  * @param strategy Execution strategy.
19
+ * @param concurrency Maximum number of concurrent tasks.
17
20
  */
18
21
  constructor(
19
22
  private context: TContext,
20
23
  private eventBus: EventBus<TContext>,
21
24
  private stateManager: TaskStateManager<TContext>,
22
- private strategy: IExecutionStrategy<TContext>
25
+ private strategy: IExecutionStrategy<TContext>,
26
+ private concurrency?: number
23
27
  ) {}
24
28
 
25
29
  /**
@@ -100,10 +104,24 @@ export class WorkflowExecutor<TContext> {
100
104
  executingPromises: Set<Promise<void>>,
101
105
  signal?: AbortSignal
102
106
  ): void {
103
- const toRun = this.stateManager.processDependencies();
107
+ const newlyReady = this.stateManager.processDependencies();
108
+
109
+ // Add newly ready tasks to the queue
110
+ for (const task of newlyReady) {
111
+ this.readyQueue.push(task);
112
+ }
113
+
114
+ // Execute ready tasks while respecting concurrency limit
115
+ while (this.readyQueue.length > 0) {
116
+ if (
117
+ this.concurrency !== undefined &&
118
+ executingPromises.size >= this.concurrency
119
+ ) {
120
+ break;
121
+ }
122
+
123
+ const step = this.readyQueue.shift()!;
104
124
 
105
- // Execute ready tasks
106
- for (const step of toRun) {
107
125
  this.stateManager.markRunning(step);
108
126
 
109
127
  const taskPromise = this.strategy.execute(step, this.context, signal)
@@ -112,6 +130,8 @@ export class WorkflowExecutor<TContext> {
112
130
  })
113
131
  .finally(() => {
114
132
  executingPromises.delete(taskPromise);
133
+ // When a task finishes, we try to run more
134
+ this.processLoop(executingPromises, signal);
115
135
  });
116
136
 
117
137
  executingPromises.add(taskPromise);
@@ -0,0 +1,8 @@
1
+ export interface TaskRetryConfig {
2
+ /** Number of retries (excluding the initial run). */
3
+ attempts: number;
4
+ /** Delay in milliseconds between retries. */
5
+ delay: number;
6
+ /** Backoff strategy: 'fixed' (default) or 'exponential'. */
7
+ backoff?: "fixed" | "exponential";
8
+ }
package/src/index.ts CHANGED
@@ -2,7 +2,9 @@ export { TaskRunner } from "./TaskRunner.js";
2
2
  export { TaskRunnerBuilder } from "./TaskRunnerBuilder.js";
3
3
  export { TaskStateManager } from "./TaskStateManager.js";
4
4
  export { StandardExecutionStrategy } from "./strategies/StandardExecutionStrategy.js";
5
+ export { RetryingExecutionStrategy } from "./strategies/RetryingExecutionStrategy.js";
5
6
  export type { IExecutionStrategy } from "./strategies/IExecutionStrategy.js";
7
+ export type { TaskRetryConfig } from "./contracts/TaskRetryConfig.js";
6
8
  export type { TaskStep } from "./TaskStep.js";
7
9
  export type { TaskResult } from "./TaskResult.js";
8
10
  export type { TaskStatus } from "./TaskStatus.js";
@@ -0,0 +1,90 @@
1
+ import { IExecutionStrategy } from "./IExecutionStrategy.js";
2
+ import { TaskStep } from "../TaskStep.js";
3
+ import { TaskResult } from "../TaskResult.js";
4
+
5
+ /**
6
+ * Execution strategy that retries tasks upon failure based on their retry configuration.
7
+ */
8
+ export class RetryingExecutionStrategy<TContext> implements IExecutionStrategy<TContext> {
9
+ constructor(private innerStrategy: IExecutionStrategy<TContext>) {}
10
+
11
+ async execute(
12
+ step: TaskStep<TContext>,
13
+ context: TContext,
14
+ signal?: AbortSignal
15
+ ): Promise<TaskResult> {
16
+ const config = step.retry;
17
+ if (!config) {
18
+ return this.innerStrategy.execute(step, context, signal);
19
+ }
20
+
21
+ let attempt = 0;
22
+ while (true) {
23
+ // Check for cancellation before execution
24
+ if (signal?.aborted) {
25
+ return {
26
+ status: "cancelled",
27
+ message: "Task cancelled before execution",
28
+ };
29
+ }
30
+
31
+ const result = await this.innerStrategy.execute(step, context, signal);
32
+
33
+ if (result.status === "success" || result.status === "cancelled" || result.status === "skipped") {
34
+ return result;
35
+ }
36
+
37
+ // Task failed, check if we should retry
38
+ if (attempt >= config.attempts) {
39
+ return result; // Max attempts reached, return failure
40
+ }
41
+
42
+ attempt++;
43
+
44
+ // Calculate delay
45
+ let delay = config.delay;
46
+ if (config.backoff === "exponential") {
47
+ delay = config.delay * Math.pow(2, attempt - 1);
48
+ }
49
+
50
+ // Wait for delay, respecting cancellation
51
+ try {
52
+ await this.sleep(delay, signal);
53
+ } catch (e) {
54
+ if (signal?.aborted) {
55
+ return {
56
+ status: "cancelled",
57
+ message: "Task cancelled during retry delay",
58
+ };
59
+ }
60
+ throw e;
61
+ }
62
+ }
63
+ }
64
+
65
+ private sleep(ms: number, signal?: AbortSignal): Promise<void> {
66
+ return new Promise((resolve, reject) => {
67
+ if (signal?.aborted) {
68
+ reject(new Error("AbortError"));
69
+ return;
70
+ }
71
+
72
+ const timer = setTimeout(() => {
73
+ cleanup();
74
+ resolve();
75
+ }, ms);
76
+
77
+ const onAbort = () => {
78
+ clearTimeout(timer);
79
+ cleanup();
80
+ reject(new Error("AbortError"));
81
+ };
82
+
83
+ const cleanup = () => {
84
+ signal?.removeEventListener("abort", onAbort);
85
+ };
86
+
87
+ signal?.addEventListener("abort", onAbort);
88
+ });
89
+ }
90
+ }