@calmo/task-runner 4.0.4 → 4.2.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/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.jules/nexus.md +10 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +46 -0
- package/CODE_OF_CONDUCT.md +131 -0
- package/CONTRIBUTING.md +89 -0
- package/README.md +34 -0
- package/conductor/code_styleguides/general.md +23 -0
- package/conductor/code_styleguides/javascript.md +51 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +14 -0
- package/conductor/product.md +16 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +19 -0
- package/conductor/workflow.md +334 -0
- package/dist/EventBus.js +16 -16
- package/dist/EventBus.js.map +1 -1
- package/dist/PluginManager.d.ts +22 -0
- package/dist/PluginManager.js +39 -0
- package/dist/PluginManager.js.map +1 -0
- package/dist/TaskGraphValidator.d.ts +1 -1
- package/dist/TaskGraphValidator.js +16 -21
- package/dist/TaskGraphValidator.js.map +1 -1
- package/dist/TaskResult.d.ts +9 -0
- package/dist/TaskRunner.d.ts +8 -1
- package/dist/TaskRunner.js +64 -41
- package/dist/TaskRunner.js.map +1 -1
- package/dist/TaskStateManager.d.ts +22 -6
- package/dist/TaskStateManager.js +105 -45
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/WorkflowExecutor.js +36 -20
- package/dist/WorkflowExecutor.js.map +1 -1
- package/dist/contracts/Plugin.d.ts +30 -0
- package/dist/contracts/Plugin.js +2 -0
- package/dist/contracts/Plugin.js.map +1 -0
- package/dist/strategies/DryRunExecutionStrategy.d.ts +1 -1
- package/dist/strategies/DryRunExecutionStrategy.js +2 -4
- package/dist/strategies/DryRunExecutionStrategy.js.map +1 -1
- package/dist/utils/PriorityQueue.d.ts +13 -0
- package/dist/utils/PriorityQueue.js +82 -0
- package/dist/utils/PriorityQueue.js.map +1 -0
- package/openspec/changes/add-middleware-support/proposal.md +19 -0
- package/openspec/changes/add-middleware-support/specs/task-runner/spec.md +34 -0
- package/openspec/changes/add-middleware-support/tasks.md +9 -0
- package/openspec/changes/add-resource-concurrency/proposal.md +18 -0
- package/openspec/changes/add-resource-concurrency/specs/task-runner/spec.md +25 -0
- package/openspec/changes/add-resource-concurrency/tasks.md +10 -0
- package/openspec/changes/allow-plugin-hooks/design.md +51 -0
- package/openspec/changes/allow-plugin-hooks/proposal.md +12 -0
- package/openspec/changes/allow-plugin-hooks/specs/post-task/spec.md +21 -0
- package/openspec/changes/allow-plugin-hooks/specs/pre-task/spec.md +32 -0
- package/openspec/changes/allow-plugin-hooks/tasks.md +7 -0
- package/openspec/changes/{feat-task-metrics → archive/2026-01-22-feat-task-metrics}/proposal.md +1 -1
- package/openspec/changes/archive/2026-01-22-feat-task-metrics/tasks.md +6 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/design.md +45 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/proposal.md +13 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-context/spec.md +17 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/specs/plugin-loading/spec.md +27 -0
- package/openspec/changes/archive/2026-02-15-implement-plugin-system/tasks.md +7 -0
- package/openspec/changes/feat-completion-dependencies/proposal.md +58 -0
- package/openspec/changes/feat-completion-dependencies/tasks.md +46 -0
- package/openspec/changes/feat-conditional-retries/proposal.md +18 -0
- package/openspec/changes/feat-conditional-retries/specs/task-runner/spec.md +23 -0
- package/openspec/changes/feat-conditional-retries/tasks.md +37 -0
- package/openspec/changes/feat-state-persistence/specs/task-runner/spec.md +47 -0
- package/openspec/changes/feat-task-loop/proposal.md +22 -0
- package/openspec/changes/feat-task-loop/specs/task-runner/spec.md +34 -0
- package/openspec/changes/feat-task-loop/tasks.md +8 -0
- package/openspec/specs/plugin-context/spec.md +19 -0
- package/openspec/specs/plugin-loading/spec.md +29 -0
- package/openspec/specs/release-pr/spec.md +31 -0
- package/openspec/specs/task-runner/spec.md +12 -0
- package/package.json +1 -1
- package/src/EventBus.ts +7 -8
- package/src/PluginManager.ts +41 -0
- package/src/TaskGraphValidator.ts +22 -24
- package/src/TaskResult.ts +9 -0
- package/src/TaskRunner.ts +78 -46
- package/src/TaskStateManager.ts +118 -46
- package/src/WorkflowExecutor.ts +45 -22
- package/src/contracts/Plugin.ts +32 -0
- package/src/strategies/DryRunExecutionStrategy.ts +2 -3
- package/src/utils/PriorityQueue.ts +101 -0
- package/openspec/changes/feat-task-metrics/tasks.md +0 -6
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/design.md +0 -0
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/proposal.md +0 -0
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/specs/release-pr/spec.md +0 -0
- /package/openspec/changes/{adopt-release-pr → archive/2026-01-22-adopt-release-pr}/tasks.md +0 -0
- /package/openspec/changes/{feat-task-metrics → archive/2026-01-22-feat-task-metrics}/specs/001-generic-task-runner/spec.md +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Conditional Retries
|
|
4
|
+
|
|
5
|
+
The system SHALL support conditional retries where a user-defined predicate determines if a task failure warrants a retry attempt.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Retry allowed by predicate
|
|
8
|
+
- **GIVEN** a task configured with retries and a `shouldRetry` predicate
|
|
9
|
+
- **WHEN** the task fails with an error
|
|
10
|
+
- **AND** the `shouldRetry` predicate returns `true` for that error
|
|
11
|
+
- **THEN** the system SHALL proceed with the retry logic (respecting attempt limits and delays).
|
|
12
|
+
|
|
13
|
+
#### Scenario: Retry denied by predicate
|
|
14
|
+
- **GIVEN** a task configured with retries and a `shouldRetry` predicate
|
|
15
|
+
- **WHEN** the task fails with an error
|
|
16
|
+
- **AND** the `shouldRetry` predicate returns `false` for that error
|
|
17
|
+
- **THEN** the system SHALL NOT retry the task.
|
|
18
|
+
- **AND** the task status SHALL be immediately marked as 'failure'.
|
|
19
|
+
|
|
20
|
+
#### Scenario: Default retry behavior
|
|
21
|
+
- **GIVEN** a task configured with retries but NO `shouldRetry` predicate
|
|
22
|
+
- **WHEN** the task fails with an error
|
|
23
|
+
- **THEN** the system SHALL retry the task (respecting attempt limits and delays), preserving backward compatibility.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Engineering Tasks
|
|
2
|
+
|
|
3
|
+
- [ ] **Task 1: Update TaskRetryConfig Interface**
|
|
4
|
+
- Modify `src/contracts/TaskRetryConfig.ts`.
|
|
5
|
+
- Add `shouldRetry?: (error: unknown) => boolean;` to the `TaskRetryConfig` interface.
|
|
6
|
+
- Document the property with JSDoc explaining its purpose (return true to retry, false to abort).
|
|
7
|
+
|
|
8
|
+
- [ ] **Task 2: Update RetryingExecutionStrategy Logic**
|
|
9
|
+
- Modify `src/strategies/RetryingExecutionStrategy.ts`.
|
|
10
|
+
- Inside the `execute` loop, after receiving a "failure" result:
|
|
11
|
+
- Check if `config.shouldRetry` is defined.
|
|
12
|
+
- If defined, call it with `result.error`.
|
|
13
|
+
- If it returns `false`, break the loop and return the failure result immediately.
|
|
14
|
+
- If it returns `true` (or is undefined), proceed with the existing retry logic (check attempts, delay).
|
|
15
|
+
|
|
16
|
+
- [ ] **Task 3: Unit Tests**
|
|
17
|
+
- Create `tests/strategies/RetryingExecutionStrategy.conditional.test.ts` (or add to existing test file if small).
|
|
18
|
+
- **Scenario 1**: `shouldRetry` returns `true`.
|
|
19
|
+
- Setup a task that fails 2 times then succeeds.
|
|
20
|
+
- Configure `shouldRetry: () => true`.
|
|
21
|
+
- Verify it retries and eventually succeeds.
|
|
22
|
+
- **Scenario 2**: `shouldRetry` returns `false`.
|
|
23
|
+
- Setup a task that fails with a specific error "FatalError".
|
|
24
|
+
- Configure `shouldRetry: (err) => err !== "FatalError"`.
|
|
25
|
+
- Verify it does *not* retry and returns failure immediately after the first attempt.
|
|
26
|
+
- **Scenario 3**: `shouldRetry` is undefined (Legacy behavior).
|
|
27
|
+
- Setup a failing task.
|
|
28
|
+
- Verify it retries up to max attempts.
|
|
29
|
+
|
|
30
|
+
- [ ] **Task 4: Integration Test**
|
|
31
|
+
- Create `tests/integration-tests/conditional-retries.test.ts`.
|
|
32
|
+
- Define a workflow with two tasks:
|
|
33
|
+
- Task A: Fails with "Transient" error (retries allowed).
|
|
34
|
+
- Task B: Fails with "Permanent" error (retries blocked by predicate).
|
|
35
|
+
- Execute workflow.
|
|
36
|
+
- Verify Task A consumed its retries (or succeeded).
|
|
37
|
+
- Verify Task B failed immediately (did not consume retries).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Workflow State Persistence Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Enables the `TaskRunner` to save its execution state and resume from that state later, allowing for recovery from failures or pausing long-running workflows without re-executing completed tasks.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
### Requirement: State Snapshot Exposure
|
|
10
|
+
|
|
11
|
+
The system SHALL provide a mechanism to retrieve the current execution state of a workflow.
|
|
12
|
+
|
|
13
|
+
#### Scenario: Retrieving state from TaskRunner
|
|
14
|
+
- **WHEN** a workflow is running or completed
|
|
15
|
+
- **THEN** the `TaskRunner` (or its underlying `TaskStateManager`) SHALL expose a method to retrieve a snapshot of the current state.
|
|
16
|
+
- **AND** the snapshot SHALL contain the `results` of all executed tasks.
|
|
17
|
+
- **AND** the snapshot SHALL be serializable (e.g., to JSON).
|
|
18
|
+
|
|
19
|
+
### Requirement: Hydrated Initialization
|
|
20
|
+
|
|
21
|
+
The system SHALL allow initializing a `TaskRunner` with a pre-existing state snapshot.
|
|
22
|
+
|
|
23
|
+
#### Scenario: Initializing with a snapshot
|
|
24
|
+
- **GIVEN** a valid state snapshot from a previous execution
|
|
25
|
+
- **WHEN** the `TaskRunner` is built using `TaskRunnerBuilder`
|
|
26
|
+
- **THEN** the builder SHALL accept the snapshot as an initial state.
|
|
27
|
+
- **AND** the `TaskRunner` SHALL start with the internal state reflecting the snapshot (i.e., known task results).
|
|
28
|
+
|
|
29
|
+
### Requirement: Resumable Execution Logic
|
|
30
|
+
|
|
31
|
+
The `WorkflowExecutor` SHALL respect the initial hydrated state during execution, skipping already completed tasks.
|
|
32
|
+
|
|
33
|
+
#### Scenario: Skipping successful tasks
|
|
34
|
+
- **GIVEN** a `TaskRunner` initialized with a snapshot where Task A is marked as `success`
|
|
35
|
+
- **WHEN** `execute()` is called
|
|
36
|
+
- **THEN** Task A SHALL NOT be executed again.
|
|
37
|
+
- **AND** Task A SHALL be considered completed for the purpose of checking dependencies of downstream tasks.
|
|
38
|
+
|
|
39
|
+
#### Scenario: Re-running non-successful tasks
|
|
40
|
+
- **GIVEN** a `TaskRunner` initialized with a snapshot where Task B is marked as `failure`, `cancelled`, or `skipped`
|
|
41
|
+
- **WHEN** `execute()` is called
|
|
42
|
+
- **THEN** Task B SHOULD be evaluated for execution (subject to dependency checks).
|
|
43
|
+
|
|
44
|
+
#### Scenario: Handling Context
|
|
45
|
+
- **GIVEN** a resumed workflow
|
|
46
|
+
- **THEN** it is the caller's responsibility to provide the necessary `Context` for task execution.
|
|
47
|
+
- **AND** the `TaskRunner` SHALL NOT attempt to automatically restore the context object from the state snapshot (as it may contain non-serializable data).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Change: feat-task-loop
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Users often need to orchestrate tasks that involve waiting for a condition to be met, such as polling an API for a deployment status or waiting for a database to be healthy. Currently, this logic must be implemented imperatively within the `run` method of a task, which obscures the intent and mixes orchestration concerns with business logic.
|
|
6
|
+
|
|
7
|
+
## What Changes
|
|
8
|
+
|
|
9
|
+
- Add a `loop` configuration to the `TaskStep` interface.
|
|
10
|
+
- Introduce a `LoopingExecutionStrategy` that wraps other strategies to handle conditional re-execution.
|
|
11
|
+
- The `loop` configuration will support:
|
|
12
|
+
- `interval`: Time in milliseconds to wait between iterations.
|
|
13
|
+
- `maxIterations`: Maximum number of times to run the task.
|
|
14
|
+
- `until`: A predicate function `(context, result) => boolean` that determines when the loop should stop.
|
|
15
|
+
|
|
16
|
+
## Impact
|
|
17
|
+
|
|
18
|
+
- **Affected Specs**: `task-runner`
|
|
19
|
+
- **Affected Code**:
|
|
20
|
+
- `src/TaskStep.ts`: Update interface.
|
|
21
|
+
- `src/strategies/LoopingExecutionStrategy.ts`: New file.
|
|
22
|
+
- `src/TaskRunnerBuilder.ts`: Integrate the new strategy.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Task Looping
|
|
4
|
+
|
|
5
|
+
The `TaskStep` interface SHALL support an optional `loop` property of type `TaskLoopConfig` to enable conditional re-execution of a task (polling).
|
|
6
|
+
|
|
7
|
+
#### Scenario: Loop until condition met
|
|
8
|
+
|
|
9
|
+
- **GIVEN** a task with a `loop` configuration
|
|
10
|
+
- **AND** the `loop.until` predicate returns `false`
|
|
11
|
+
- **THEN** the task SHALL re-execute after the specified `interval`.
|
|
12
|
+
- **WHEN** the `loop.until` predicate returns `true`
|
|
13
|
+
- **THEN** the task SHALL complete successfully with the last result.
|
|
14
|
+
|
|
15
|
+
#### Scenario: Max iterations reached
|
|
16
|
+
|
|
17
|
+
- **GIVEN** a task with a `loop` configuration
|
|
18
|
+
- **AND** the task has re-executed `maxIterations` times without the predicate returning `true`
|
|
19
|
+
- **THEN** the task SHALL fail with an error indicating the loop limit was reached.
|
|
20
|
+
|
|
21
|
+
#### Scenario: Integration with Retries
|
|
22
|
+
|
|
23
|
+
- **GIVEN** a task with both `retry` and `loop` configurations
|
|
24
|
+
- **WHEN** the task fails (throws an error)
|
|
25
|
+
- **THEN** the `RetryingExecutionStrategy` SHALL handle the retry logic first.
|
|
26
|
+
- **AND** the loop iteration SHALL only count once the task executes successfully (or fails after retries).
|
|
27
|
+
|
|
28
|
+
#### Scenario: Loop Config Structure
|
|
29
|
+
|
|
30
|
+
- **GIVEN** a `TaskLoopConfig` object
|
|
31
|
+
- **THEN** it SHALL support:
|
|
32
|
+
- `interval`: Time in milliseconds to wait between iterations (default: 0).
|
|
33
|
+
- `maxIterations`: Maximum number of iterations (default: 1).
|
|
34
|
+
- `until`: A predicate function `(context: TContext, result: TaskResult) => boolean`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## 1. Implementation
|
|
2
|
+
|
|
3
|
+
- [ ] 1.1 Define `TaskLoopConfig` interface in `src/contracts/TaskLoopConfig.ts`.
|
|
4
|
+
- [ ] 1.2 Update `TaskStep` interface in `src/TaskStep.ts` to include optional `loop: TaskLoopConfig`.
|
|
5
|
+
- [ ] 1.3 Implement `LoopingExecutionStrategy` in `src/strategies/LoopingExecutionStrategy.ts`.
|
|
6
|
+
- [ ] 1.4 Update `TaskRunnerBuilder` in `src/TaskRunnerBuilder.ts` to include `LoopingExecutionStrategy`.
|
|
7
|
+
- [ ] 1.5 Add unit tests for `LoopingExecutionStrategy`.
|
|
8
|
+
- [ ] 1.6 Verify integration with `RetryingExecutionStrategy` (looping should happen outside retries).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# plugin-context Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
TBD - created by archiving change implement-plugin-system. Update Purpose after archive.
|
|
5
|
+
## Requirements
|
|
6
|
+
### Requirement: Expose EventBus to plugins
|
|
7
|
+
|
|
8
|
+
The `PluginContext` passed to `install` MUST expose the `EventBus` (or equivalent API) to allow listening to events.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Listening to task events
|
|
11
|
+
Given a plugin is being installed
|
|
12
|
+
When it accesses `context.events`
|
|
13
|
+
Then it can subscribe to `taskStart` and `taskEnd` events
|
|
14
|
+
|
|
15
|
+
#### Scenario: Emitting custom events (Future)
|
|
16
|
+
Given a plugin
|
|
17
|
+
When it has access to the event bus
|
|
18
|
+
Then it should be able to emit events (if allowed by design)
|
|
19
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# plugin-loading Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
TBD - created by archiving change implement-plugin-system. Update Purpose after archive.
|
|
5
|
+
## Requirements
|
|
6
|
+
### Requirement: Support registering plugins
|
|
7
|
+
|
|
8
|
+
The `TaskRunner` MUST allow registering plugins via a `use` method or configuration.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Registering a valid plugin
|
|
11
|
+
Given a `TaskRunner` instance
|
|
12
|
+
When I call `use` with a valid plugin object
|
|
13
|
+
Then the plugin is added to the internal plugin list
|
|
14
|
+
|
|
15
|
+
#### Scenario: Registering an invalid plugin
|
|
16
|
+
Given a `TaskRunner` instance
|
|
17
|
+
When I call `use` with an invalid object (missing install method)
|
|
18
|
+
Then it should throw an error or reject the plugin
|
|
19
|
+
|
|
20
|
+
### Requirement: Initialize plugins before execution
|
|
21
|
+
|
|
22
|
+
Plugins MUST be initialized (have their `install` method called) before the workflow starts.
|
|
23
|
+
|
|
24
|
+
#### Scenario: Plugin initialization
|
|
25
|
+
Given a registered plugin
|
|
26
|
+
When `execute` is called on the `TaskRunner`
|
|
27
|
+
Then the plugin's `install` method is called with the plugin context
|
|
28
|
+
And the workflow execution proceeds only after `install` completes
|
|
29
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# release-pr Specification
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
TBD - created by archiving change adopt-release-pr. Update Purpose after archive.
|
|
5
|
+
## Requirements
|
|
6
|
+
### Requirement: Release PR Generation
|
|
7
|
+
The system MUST automatically maintain a "Release PR" that targets the `main` branch. This PR must accumulate all conventional changes since the last release, calculating the next semantic version and generating a corresponding changelog entry.
|
|
8
|
+
|
|
9
|
+
#### Scenario: Feature commit triggers PR update
|
|
10
|
+
Given the latest release is `v1.0.0`
|
|
11
|
+
And a developer merges a commit with message `feat: add awesome feature` to `main`
|
|
12
|
+
Then the system should create or update the Release PR
|
|
13
|
+
And the PR title should be `chore: release 1.1.0`
|
|
14
|
+
And the PR body should contain the changelog entry for "add awesome feature"
|
|
15
|
+
And the `package.json` version in the PR should be `1.1.0`
|
|
16
|
+
|
|
17
|
+
#### Scenario: Fix commit triggers patch update
|
|
18
|
+
Given the latest release is `v1.0.0`
|
|
19
|
+
And a developer merges a commit `fix: urgent bug` to `main`
|
|
20
|
+
Then the Release PR should be updated to target version `1.0.1`
|
|
21
|
+
|
|
22
|
+
### Requirement: Release Publication
|
|
23
|
+
The system MUST execute the release process (git tag, GitHub Release, npm publish) ONLY when the Release PR is merged into `main`.
|
|
24
|
+
|
|
25
|
+
#### Scenario: Merge triggers publish
|
|
26
|
+
Given the Release PR for `v1.1.0` exists
|
|
27
|
+
When a maintainer merges the PR into `main`
|
|
28
|
+
Then the system should create a GitHub Release `v1.1.0`
|
|
29
|
+
And the system should publish the package to the configured registry (NPM)
|
|
30
|
+
And the system should NOT publish any other commits merged to `main` until the next Release PR merge
|
|
31
|
+
|
|
@@ -160,3 +160,15 @@ The system SHALL provide a `RetryingExecutionStrategy` that implements `IExecuti
|
|
|
160
160
|
|
|
161
161
|
- **WHEN** `retry.backoff` is 'exponential'
|
|
162
162
|
- **THEN** the delay SHALL increase for each attempt (e.g., `delay * 2^attempt`).
|
|
163
|
+
|
|
164
|
+
### Requirement: Task Execution Metrics
|
|
165
|
+
|
|
166
|
+
The system SHALL record timing metrics for each executed task, including start time, end time, and duration.
|
|
167
|
+
|
|
168
|
+
#### Scenario: Successful execution
|
|
169
|
+
- **WHEN** a task completes successfully
|
|
170
|
+
- **THEN** the task result contains the start timestamp, end timestamp, and duration in milliseconds
|
|
171
|
+
|
|
172
|
+
#### Scenario: Failed execution
|
|
173
|
+
- **WHEN** a task fails
|
|
174
|
+
- **THEN** the task result contains the start timestamp, end timestamp, and duration in milliseconds
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calmo/task-runner",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "A lightweight, type-safe, and domain-agnostic task orchestration engine. It resolves a Directed Acyclic Graph (DAG) of steps, executes independent tasks in parallel, and manages a shared context across the pipeline.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/EventBus.ts
CHANGED
|
@@ -61,11 +61,10 @@ export class EventBus<TContext> {
|
|
|
61
61
|
| undefined;
|
|
62
62
|
if (listeners) {
|
|
63
63
|
for (const listener of listeners) {
|
|
64
|
-
// We use
|
|
64
|
+
// We use queueMicrotask() to schedule the listener on the microtask queue,
|
|
65
65
|
// ensuring the emit method remains non-blocking.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.then(() => {
|
|
66
|
+
queueMicrotask(() => {
|
|
67
|
+
try {
|
|
69
68
|
try {
|
|
70
69
|
const result = listener(data);
|
|
71
70
|
if (result instanceof Promise) {
|
|
@@ -84,14 +83,14 @@ export class EventBus<TContext> {
|
|
|
84
83
|
error
|
|
85
84
|
);
|
|
86
85
|
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// detailed handling for the promise chain itself
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// detailed handling for the microtask execution itself
|
|
90
88
|
console.error(
|
|
91
89
|
`Unexpected error in event bus execution for ${String(event)}:`,
|
|
92
90
|
error
|
|
93
91
|
);
|
|
94
|
-
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
96
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Plugin, PluginContext } from "./contracts/Plugin.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages the lifecycle of plugins.
|
|
5
|
+
*/
|
|
6
|
+
export class PluginManager<TContext> {
|
|
7
|
+
private plugins: Plugin<TContext>[] = [];
|
|
8
|
+
|
|
9
|
+
constructor(private context: PluginContext<TContext>) {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registers a plugin.
|
|
13
|
+
* @param plugin The plugin to register.
|
|
14
|
+
*/
|
|
15
|
+
public use(plugin: Plugin<TContext>): void {
|
|
16
|
+
// Check if plugin is already registered
|
|
17
|
+
if (this.plugins.some((p) => p.name === plugin.name)) {
|
|
18
|
+
// For now, we allow overwriting or just warn?
|
|
19
|
+
// Let's just allow it but maybe log it if we had a logger.
|
|
20
|
+
// Strict check: don't allow duplicate names.
|
|
21
|
+
throw new Error(`Plugin with name '${plugin.name}' is already registered.`);
|
|
22
|
+
}
|
|
23
|
+
this.plugins.push(plugin);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initializes all registered plugins.
|
|
28
|
+
*/
|
|
29
|
+
public async initialize(): Promise<void> {
|
|
30
|
+
for (const plugin of this.plugins) {
|
|
31
|
+
await plugin.install(this.context);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the list of registered plugins.
|
|
37
|
+
*/
|
|
38
|
+
public getPlugins(): ReadonlyArray<Plugin<TContext>> {
|
|
39
|
+
return this.plugins;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ITaskGraphValidator } from "./contracts/ITaskGraphValidator.js";
|
|
2
2
|
import { ValidationResult } from "./contracts/ValidationResult.js";
|
|
3
3
|
import { ValidationError } from "./contracts/ValidationError.js";
|
|
4
|
-
import { TaskGraph } from "./TaskGraph.js";
|
|
4
|
+
import { TaskGraph, Task } from "./TaskGraph.js";
|
|
5
5
|
import {
|
|
6
6
|
ERROR_CYCLE,
|
|
7
7
|
ERROR_DUPLICATE_TASK,
|
|
@@ -22,11 +22,11 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
22
22
|
validate(taskGraph: TaskGraph): ValidationResult {
|
|
23
23
|
const errors: ValidationError[] = [];
|
|
24
24
|
|
|
25
|
-
// 1. Check
|
|
26
|
-
const
|
|
25
|
+
// 1. Build Map and Check Duplicates (Single Pass)
|
|
26
|
+
const taskMap = this.buildTaskMapAndCheckDuplicates(taskGraph, errors);
|
|
27
27
|
|
|
28
28
|
// 2. Check for missing dependencies
|
|
29
|
-
this.checkMissingDependencies(taskGraph,
|
|
29
|
+
this.checkMissingDependencies(taskGraph, taskMap, errors);
|
|
30
30
|
|
|
31
31
|
// 3. Check for cycles
|
|
32
32
|
// Only run cycle detection if there are no missing dependencies, otherwise we might chase non-existent nodes.
|
|
@@ -35,7 +35,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
if (!hasMissingDependencies) {
|
|
38
|
-
this.checkCycles(taskGraph, errors);
|
|
38
|
+
this.checkCycles(taskGraph, taskMap, errors);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
return {
|
|
@@ -54,33 +54,33 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
54
54
|
return `Task graph validation failed: ${errorDetails.join("; ")}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
private
|
|
57
|
+
private buildTaskMapAndCheckDuplicates(
|
|
58
58
|
taskGraph: TaskGraph,
|
|
59
59
|
errors: ValidationError[]
|
|
60
|
-
):
|
|
61
|
-
const
|
|
60
|
+
): Map<string, Task> {
|
|
61
|
+
const taskMap = new Map<string, Task>();
|
|
62
62
|
for (const task of taskGraph.tasks) {
|
|
63
|
-
if (
|
|
63
|
+
if (taskMap.has(task.id)) {
|
|
64
64
|
errors.push({
|
|
65
65
|
type: ERROR_DUPLICATE_TASK,
|
|
66
66
|
message: `Duplicate task detected with ID: ${task.id}`,
|
|
67
67
|
details: { taskId: task.id },
|
|
68
68
|
});
|
|
69
69
|
} else {
|
|
70
|
-
|
|
70
|
+
taskMap.set(task.id, task);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
return
|
|
73
|
+
return taskMap;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
private checkMissingDependencies(
|
|
77
77
|
taskGraph: TaskGraph,
|
|
78
|
-
|
|
78
|
+
taskMap: Map<string, Task>,
|
|
79
79
|
errors: ValidationError[]
|
|
80
80
|
): void {
|
|
81
81
|
for (const task of taskGraph.tasks) {
|
|
82
82
|
for (const dependenceId of task.dependencies) {
|
|
83
|
-
if (!
|
|
83
|
+
if (!taskMap.has(dependenceId)) {
|
|
84
84
|
errors.push({
|
|
85
85
|
type: ERROR_MISSING_DEPENDENCY,
|
|
86
86
|
message: `Task '${task.id}' depends on missing task '${dependenceId}'`,
|
|
@@ -91,13 +91,11 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
private checkCycles(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
94
|
+
private checkCycles(
|
|
95
|
+
taskGraph: TaskGraph,
|
|
96
|
+
taskMap: Map<string, Task>,
|
|
97
|
+
errors: ValidationError[]
|
|
98
|
+
): void {
|
|
101
99
|
const visited = new Set<string>();
|
|
102
100
|
const recursionStack = new Set<string>();
|
|
103
101
|
|
|
@@ -108,7 +106,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
108
106
|
|
|
109
107
|
const path: string[] = [];
|
|
110
108
|
if (
|
|
111
|
-
this.detectCycle(task.id, path, visited, recursionStack,
|
|
109
|
+
this.detectCycle(task.id, path, visited, recursionStack, taskMap)
|
|
112
110
|
) {
|
|
113
111
|
// Extract the actual cycle from the path
|
|
114
112
|
// The path might look like A -> B -> C -> B (if we started at A and found cycle B-C-B)
|
|
@@ -132,7 +130,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
132
130
|
path: string[],
|
|
133
131
|
visited: Set<string>,
|
|
134
132
|
recursionStack: Set<string>,
|
|
135
|
-
|
|
133
|
+
taskMap: Map<string, Task>
|
|
136
134
|
): boolean {
|
|
137
135
|
// Use an explicit stack to avoid maximum call stack size exceeded errors
|
|
138
136
|
const stack: { taskId: string; index: number; dependencies: string[] }[] =
|
|
@@ -145,7 +143,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
145
143
|
stack.push({
|
|
146
144
|
taskId: startTaskId,
|
|
147
145
|
index: 0,
|
|
148
|
-
dependencies:
|
|
146
|
+
dependencies: taskMap.get(startTaskId)!.dependencies,
|
|
149
147
|
});
|
|
150
148
|
|
|
151
149
|
while (stack.length > 0) {
|
|
@@ -170,7 +168,7 @@ export class TaskGraphValidator implements ITaskGraphValidator {
|
|
|
170
168
|
stack.push({
|
|
171
169
|
taskId: dependenceId,
|
|
172
170
|
index: 0,
|
|
173
|
-
dependencies:
|
|
171
|
+
dependencies: taskMap.get(dependenceId)!.dependencies,
|
|
174
172
|
});
|
|
175
173
|
}
|
|
176
174
|
} else {
|
package/src/TaskResult.ts
CHANGED
|
@@ -12,4 +12,13 @@ export interface TaskResult {
|
|
|
12
12
|
error?: string;
|
|
13
13
|
/** Optional data produced by the step for later inspection. */
|
|
14
14
|
data?: unknown;
|
|
15
|
+
/** Optional execution metrics for the task. */
|
|
16
|
+
metrics?: {
|
|
17
|
+
/** Start time in milliseconds (performance.now). */
|
|
18
|
+
startTime: number;
|
|
19
|
+
/** End time in milliseconds (performance.now). */
|
|
20
|
+
endTime: number;
|
|
21
|
+
/** Duration in milliseconds. */
|
|
22
|
+
duration: number;
|
|
23
|
+
};
|
|
15
24
|
}
|