@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.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +30 -0
- package/coverage/coverage-final.json +7 -6
- package/coverage/index.html +12 -12
- package/coverage/lcov-report/index.html +12 -12
- package/coverage/lcov-report/src/EventBus.ts.html +4 -4
- package/coverage/lcov-report/src/TaskGraphValidator.ts.html +33 -33
- package/coverage/lcov-report/src/TaskRunner.ts.html +30 -18
- package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +1 -1
- package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +17 -2
- package/coverage/lcov-report/src/TaskStateManager.ts.html +35 -35
- package/coverage/lcov-report/src/WorkflowExecutor.ts.html +94 -34
- package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/lcov-report/src/contracts/index.html +1 -1
- package/coverage/lcov-report/src/index.html +7 -7
- package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
- package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +355 -0
- package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +3 -3
- package/coverage/lcov-report/src/strategies/index.html +20 -5
- package/coverage/lcov.info +328 -240
- package/coverage/src/EventBus.ts.html +4 -4
- package/coverage/src/TaskGraphValidator.ts.html +33 -33
- package/coverage/src/TaskRunner.ts.html +30 -18
- package/coverage/src/TaskRunnerBuilder.ts.html +1 -1
- package/coverage/src/TaskRunnerExecutionConfig.ts.html +17 -2
- package/coverage/src/TaskStateManager.ts.html +35 -35
- package/coverage/src/WorkflowExecutor.ts.html +94 -34
- package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/src/contracts/index.html +1 -1
- package/coverage/src/index.html +7 -7
- package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
- package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +355 -0
- package/coverage/src/strategies/StandardExecutionStrategy.ts.html +3 -3
- package/coverage/src/strategies/index.html +20 -5
- package/dist/TaskRunner.js +3 -2
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskRunnerExecutionConfig.d.ts +5 -0
- package/dist/TaskStep.d.ts +3 -0
- package/dist/WorkflowExecutor.d.ts +4 -1
- package/dist/WorkflowExecutor.js +19 -4
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/contracts/TaskRetryConfig.d.ts +8 -0
- package/dist/contracts/TaskRetryConfig.js +2 -0
- package/dist/contracts/TaskRetryConfig.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/strategies/RetryingExecutionStrategy.d.ts +12 -0
- package/dist/strategies/RetryingExecutionStrategy.js +74 -0
- package/dist/strategies/RetryingExecutionStrategy.js.map +1 -0
- package/openspec/changes/archive/2026-01-18-add-task-retry-policy/tasks.md +10 -0
- package/openspec/changes/archive/2026-01-18-add-workflow-preview/tasks.md +7 -0
- package/openspec/project.md +1 -0
- package/package.json +1 -1
- package/src/TaskRunner.ts +6 -2
- package/src/TaskRunnerExecutionConfig.ts +5 -0
- package/src/TaskStep.ts +3 -0
- package/src/WorkflowExecutor.ts +24 -4
- package/src/contracts/TaskRetryConfig.ts +8 -0
- package/src/index.ts +2 -0
- package/src/strategies/RetryingExecutionStrategy.ts +90 -0
- package/test-report.xml +122 -92
- package/openspec/changes/add-task-retry-policy/tasks.md +0 -10
- package/openspec/changes/add-workflow-preview/tasks.md +0 -7
- /package/openspec/changes/{add-task-retry-policy → archive/2026-01-18-add-task-retry-policy}/proposal.md +0 -0
- /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).
|
package/openspec/project.md
CHANGED
|
@@ -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
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
|
|
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.
|
package/src/WorkflowExecutor.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|