@aikirun/task 0.5.3
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/README.md +170 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +262 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# @aikirun/task
|
|
2
|
+
|
|
3
|
+
Task SDK for Aiki durable execution platform - define reliable tasks with automatic retries, idempotency, and error
|
|
4
|
+
handling.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @aikirun/task
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
### Define a Simple Task
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { task } from "@aikirun/task";
|
|
18
|
+
|
|
19
|
+
export const sendVerificationEmail = task({
|
|
20
|
+
id: "send-verification",
|
|
21
|
+
async handler(input: { email: string }) {
|
|
22
|
+
return emailService.sendVerification(input.email);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Task with Retry Configuration
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
export const ringAlarm = task({
|
|
31
|
+
id: "ring-alarm",
|
|
32
|
+
handler(input: { song: string }) {
|
|
33
|
+
return Promise.resolve(audioService.play(input.song));
|
|
34
|
+
},
|
|
35
|
+
opts: {
|
|
36
|
+
retry: {
|
|
37
|
+
type: "fixed",
|
|
38
|
+
maxAttempts: 3,
|
|
39
|
+
delayMs: 1000,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Execute Task in a Workflow
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { workflow } from "@aikirun/workflow";
|
|
49
|
+
|
|
50
|
+
export const morningWorkflow = workflow({ id: "morning-routine" });
|
|
51
|
+
|
|
52
|
+
export const morningWorkflowV1 = morningWorkflow.v("1.0", {
|
|
53
|
+
async handler(input, run) {
|
|
54
|
+
const result = await ringAlarm.start(run, { song: "alarm.mp3" });
|
|
55
|
+
console.log("Task completed:", result);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Idempotent Execution** - Tasks can be safely retried without unintended side effects
|
|
63
|
+
- **Automatic Retries** - Multiple retry strategies (fixed, exponential, jittered)
|
|
64
|
+
- **Idempotency Keys** - Deduplicate task executions with custom keys
|
|
65
|
+
- **Error Handling** - Structured error information with recovery strategies
|
|
66
|
+
- **State Tracking** - Task execution state persists across failures
|
|
67
|
+
- **Type Safety** - Full TypeScript support with input/output types
|
|
68
|
+
|
|
69
|
+
## Task Configuration
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interface TaskOptions {
|
|
73
|
+
retry?: RetryStrategy; // Retry strategy
|
|
74
|
+
idempotencyKey?: string; // For deduplication
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Retry Strategies
|
|
79
|
+
|
|
80
|
+
#### Never Retry
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
opts: {
|
|
84
|
+
retry: { type: "never" },
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Fixed Delay
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
opts: {
|
|
92
|
+
retry: {
|
|
93
|
+
type: "fixed",
|
|
94
|
+
maxAttempts: 3,
|
|
95
|
+
delayMs: 1000,
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Exponential Backoff
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
opts: {
|
|
104
|
+
retry: {
|
|
105
|
+
type: "exponential",
|
|
106
|
+
maxAttempts: 5,
|
|
107
|
+
baseDelayMs: 1000,
|
|
108
|
+
factor: 2,
|
|
109
|
+
maxDelayMs: 30000,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Jittered Exponential
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
opts: {
|
|
118
|
+
retry: {
|
|
119
|
+
type: "jittered",
|
|
120
|
+
maxAttempts: 5,
|
|
121
|
+
baseDelayMs: 1000,
|
|
122
|
+
jitterFactor: 0.1,
|
|
123
|
+
maxDelayMs: 30000,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Execution Context
|
|
129
|
+
|
|
130
|
+
Tasks are executed within a workflow's execution context. Logging happens in the workflow:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
export const processPayment = task({
|
|
134
|
+
id: "process-payment",
|
|
135
|
+
async handler(input: { amount: number }) {
|
|
136
|
+
return { success: true, transactionId: "tx_123" };
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const paymentWorkflowV1 = paymentWorkflow.v("1.0", {
|
|
141
|
+
async handler(input, run) {
|
|
142
|
+
run.logger.info("Processing payment", { amount: input.amount });
|
|
143
|
+
const result = await processPayment.start(run, { amount: input.amount });
|
|
144
|
+
run.logger.info("Payment complete", result);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Best Practices
|
|
150
|
+
|
|
151
|
+
1. **Make Tasks Idempotent** - Tasks may be retried, so re-running should not cause unintended side effects
|
|
152
|
+
2. **Use Idempotency Keys** - Use custom keys to prevent duplicate processing
|
|
153
|
+
3. **Use Meaningful Errors** - Help diagnose failures
|
|
154
|
+
4. **Log Information** - Use `run.logger` for debugging
|
|
155
|
+
5. **Keep Tasks Focused** - One responsibility per task
|
|
156
|
+
|
|
157
|
+
## Related Packages
|
|
158
|
+
|
|
159
|
+
- [@aikirun/workflow](https://www.npmjs.com/package/@aikirun/workflow) - Use tasks in workflows
|
|
160
|
+
- [@aikirun/worker](https://www.npmjs.com/package/@aikirun/worker) - Execute tasks in workers
|
|
161
|
+
- [@aikirun/client](https://www.npmjs.com/package/@aikirun/client) - Manage task execution
|
|
162
|
+
- [@aikirun/types](https://www.npmjs.com/package/@aikirun/types) - Type definitions
|
|
163
|
+
|
|
164
|
+
## Changelog
|
|
165
|
+
|
|
166
|
+
See the [CHANGELOG](https://github.com/aikirun/aiki/blob/main/CHANGELOG.md) for version history.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { SerializableInput } from '@aikirun/types/error';
|
|
2
|
+
import { RetryStrategy } from '@aikirun/types/retry';
|
|
3
|
+
import { TaskId } from '@aikirun/types/task';
|
|
4
|
+
import { WorkflowRunContext } from '@aikirun/workflow';
|
|
5
|
+
|
|
6
|
+
type NonEmptyArray<T> = [T, ...T[]];
|
|
7
|
+
|
|
8
|
+
type NonArrayObject<T> = T extends object ? (T extends ReadonlyArray<unknown> ? never : T) : never;
|
|
9
|
+
type IsSubtype<SubT, SuperT> = SubT extends SuperT ? true : false;
|
|
10
|
+
type And<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? false extends First ? false : Rest extends NonEmptyArray<boolean> ? And<Rest> : true : never;
|
|
11
|
+
type Or<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? true extends First ? true : Rest extends NonEmptyArray<boolean> ? Or<Rest> : false : never;
|
|
12
|
+
type PathFromObject<T, IncludeArrayKeys extends boolean = false> = T extends T ? PathFromObjectInternal<T, IncludeArrayKeys> : never;
|
|
13
|
+
type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[
|
|
14
|
+
IsSubtype<T, object>,
|
|
15
|
+
Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>
|
|
16
|
+
]> extends true ? {
|
|
17
|
+
[K in Exclude<keyof T, symbol>]-?: And<[
|
|
18
|
+
IsSubtype<NonNullable<T[K]>, object>,
|
|
19
|
+
Or<[IncludeArrayKeys, NonArrayObject<NonNullable<T[K]>> extends never ? false : true]>
|
|
20
|
+
]> extends true ? K | `${K}.${PathFromObjectInternal<NonNullable<T[K]>, IncludeArrayKeys>}` : K;
|
|
21
|
+
}[Exclude<keyof T, symbol>] : "";
|
|
22
|
+
type ExtractObjectType<T> = T extends object ? T : never;
|
|
23
|
+
type TypeOfValueAtPath<T extends object, Path extends PathFromObject<T>> = Path extends keyof T ? T[Path] : Path extends `${infer First}.${infer Rest}` ? First extends keyof T ? undefined extends T[First] ? Rest extends PathFromObject<ExtractObjectType<T[First]>> ? TypeOfValueAtPath<ExtractObjectType<T[First]>, Rest> | undefined : never : Rest extends PathFromObject<ExtractObjectType<T[First]>> ? TypeOfValueAtPath<ExtractObjectType<T[First]>, Rest> : never : never : never;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Defines a durable task with deterministic execution and automatic retries.
|
|
27
|
+
*
|
|
28
|
+
* Tasks must be deterministic - the same input should always produce the same output.
|
|
29
|
+
* Tasks can be retried multiple times, so they should be idempotent when possible.
|
|
30
|
+
* Tasks execute within a workflow context and can access logging.
|
|
31
|
+
*
|
|
32
|
+
* @template Input - Type of task input (must be JSON serializable)
|
|
33
|
+
* @template Output - Type of task output (must be JSON serializable)
|
|
34
|
+
* @param params - Task configuration
|
|
35
|
+
* @param params.id - Unique task id used for execution tracking
|
|
36
|
+
* @param params.handler - Async function that executes the task logic
|
|
37
|
+
* @returns Task instance with retry and option configuration methods
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // Simple task without retry
|
|
42
|
+
* export const sendEmail = task({
|
|
43
|
+
* id: "send-email",
|
|
44
|
+
* handler(input: { email: string; message: string }) {
|
|
45
|
+
* return emailService.send(input.email, input.message);
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // Task with retry configuration
|
|
50
|
+
* export const chargeCard = task({
|
|
51
|
+
* id: "charge-card",
|
|
52
|
+
* handler(input: { cardId: string; amount: number }) {
|
|
53
|
+
* return paymentService.charge(input.cardId, input.amount);
|
|
54
|
+
* },
|
|
55
|
+
* opts: {
|
|
56
|
+
* retry: {
|
|
57
|
+
* type: "fixed",
|
|
58
|
+
* maxAttempts: 3,
|
|
59
|
+
* delayMs: 1000,
|
|
60
|
+
* },
|
|
61
|
+
* },
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Execute task in workflow
|
|
65
|
+
* const result = await chargeCard.start(run, { cardId: "123", amount: 9999 });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function task<Input extends SerializableInput = null, Output = void>(params: TaskParams<Input, Output>): Task<Input, Output>;
|
|
69
|
+
interface TaskParams<Input, Output> {
|
|
70
|
+
id: string;
|
|
71
|
+
handler: (input: Input) => Promise<Output>;
|
|
72
|
+
opts?: TaskOptions;
|
|
73
|
+
}
|
|
74
|
+
interface TaskOptions {
|
|
75
|
+
retry?: RetryStrategy;
|
|
76
|
+
idempotencyKey?: string;
|
|
77
|
+
}
|
|
78
|
+
interface TaskBuilder<Input, Output> {
|
|
79
|
+
opt<Path extends PathFromObject<TaskOptions>>(path: Path, value: TypeOfValueAtPath<TaskOptions, Path>): TaskBuilder<Input, Output>;
|
|
80
|
+
start: Task<Input, Output>["start"];
|
|
81
|
+
}
|
|
82
|
+
interface Task<Input, Output> {
|
|
83
|
+
id: TaskId;
|
|
84
|
+
with(): TaskBuilder<Input, Output>;
|
|
85
|
+
start: <WorkflowInput, WorkflowOutput>(run: WorkflowRunContext<WorkflowInput, WorkflowOutput>, ...args: Input extends null ? [] : [Input]) => Promise<Output>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { type Task, type TaskParams, task };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// ../../lib/array/utils.ts
|
|
2
|
+
function isNonEmptyArray(value) {
|
|
3
|
+
return value.length > 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ../../lib/async/delay.ts
|
|
7
|
+
function delay(ms, options) {
|
|
8
|
+
const abortSignal = options?.abortSignal;
|
|
9
|
+
if (abortSignal?.aborted) {
|
|
10
|
+
return Promise.reject(abortSignal.reason);
|
|
11
|
+
}
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const abort = () => {
|
|
14
|
+
clearTimeout(timeout);
|
|
15
|
+
reject(abortSignal?.reason);
|
|
16
|
+
};
|
|
17
|
+
const timeout = setTimeout(() => {
|
|
18
|
+
abortSignal?.removeEventListener("abort", abort);
|
|
19
|
+
resolve();
|
|
20
|
+
}, ms);
|
|
21
|
+
abortSignal?.addEventListener("abort", abort, { once: true });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ../../lib/crypto/hash.ts
|
|
26
|
+
async function sha256(input) {
|
|
27
|
+
const data = new TextEncoder().encode(input);
|
|
28
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
29
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
30
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ../../lib/error/serializable.ts
|
|
34
|
+
function createSerializableError(error) {
|
|
35
|
+
return error instanceof Error ? {
|
|
36
|
+
message: error.message,
|
|
37
|
+
name: error.name,
|
|
38
|
+
stack: error.stack,
|
|
39
|
+
cause: error.cause ? createSerializableError(error.cause) : void 0
|
|
40
|
+
} : {
|
|
41
|
+
message: String(error),
|
|
42
|
+
name: "UnknownError"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ../../lib/json/stable-stringify.ts
|
|
47
|
+
function stableStringify(value) {
|
|
48
|
+
if (value === null || value === void 0) {
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value !== "object") {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
56
|
+
}
|
|
57
|
+
const keys = Object.keys(value).sort();
|
|
58
|
+
const pairs = keys.map((key) => {
|
|
59
|
+
const val = value[key];
|
|
60
|
+
return `${JSON.stringify(key)}:${stableStringify(val)}`;
|
|
61
|
+
});
|
|
62
|
+
return `{${pairs.join(",")}}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ../../lib/object/overrider.ts
|
|
66
|
+
function set(obj, path, value) {
|
|
67
|
+
const keys = path.split(".");
|
|
68
|
+
let currentValue = obj;
|
|
69
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
70
|
+
const key = keys[i];
|
|
71
|
+
currentValue = currentValue[key];
|
|
72
|
+
if (currentValue === void 0 || currentValue === null) {
|
|
73
|
+
currentValue = {};
|
|
74
|
+
currentValue[key] = currentValue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const lastKey = keys[keys.length - 1];
|
|
78
|
+
currentValue[lastKey] = value;
|
|
79
|
+
}
|
|
80
|
+
var objectOverrider = (defaultObj) => (obj) => {
|
|
81
|
+
const createBuilder = (overrides) => ({
|
|
82
|
+
with: (path, value) => createBuilder([...overrides, { path: `${path}`, value }]),
|
|
83
|
+
build: () => {
|
|
84
|
+
const clonedObject = structuredClone(obj ?? defaultObj);
|
|
85
|
+
for (const { path, value } of overrides) {
|
|
86
|
+
set(clonedObject, path, value);
|
|
87
|
+
}
|
|
88
|
+
return clonedObject;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return createBuilder([]);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ../../lib/retry/strategy.ts
|
|
95
|
+
function getRetryParams(attempts, strategy) {
|
|
96
|
+
const strategyType = strategy.type;
|
|
97
|
+
switch (strategyType) {
|
|
98
|
+
case "never":
|
|
99
|
+
return {
|
|
100
|
+
retriesLeft: false
|
|
101
|
+
};
|
|
102
|
+
case "fixed":
|
|
103
|
+
if (attempts >= strategy.maxAttempts) {
|
|
104
|
+
return {
|
|
105
|
+
retriesLeft: false
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
retriesLeft: true,
|
|
110
|
+
delayMs: strategy.delayMs
|
|
111
|
+
};
|
|
112
|
+
case "exponential": {
|
|
113
|
+
if (attempts >= strategy.maxAttempts) {
|
|
114
|
+
return {
|
|
115
|
+
retriesLeft: false
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const delayMs = strategy.baseDelayMs * (strategy.factor ?? 2) ** (attempts - 1);
|
|
119
|
+
return {
|
|
120
|
+
retriesLeft: true,
|
|
121
|
+
delayMs: Math.min(delayMs, strategy.maxDelayMs ?? Number.POSITIVE_INFINITY)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "jittered": {
|
|
125
|
+
if (attempts >= strategy.maxAttempts) {
|
|
126
|
+
return {
|
|
127
|
+
retriesLeft: false
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const base = strategy.baseDelayMs * (strategy.jitterFactor ?? 2) ** (attempts - 1);
|
|
131
|
+
const delayMs = Math.random() * base;
|
|
132
|
+
return {
|
|
133
|
+
retriesLeft: true,
|
|
134
|
+
delayMs: Math.min(delayMs, strategy.maxDelayMs ?? Number.POSITIVE_INFINITY)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
return strategyType;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// task.ts
|
|
143
|
+
import { INTERNAL } from "@aikirun/types/symbols";
|
|
144
|
+
import { TaskFailedError } from "@aikirun/types/task";
|
|
145
|
+
function task(params) {
|
|
146
|
+
return new TaskImpl(params);
|
|
147
|
+
}
|
|
148
|
+
var TaskImpl = class _TaskImpl {
|
|
149
|
+
constructor(params) {
|
|
150
|
+
this.params = params;
|
|
151
|
+
this.id = params.id;
|
|
152
|
+
}
|
|
153
|
+
id;
|
|
154
|
+
with() {
|
|
155
|
+
const optsOverrider = objectOverrider(this.params.opts ?? {});
|
|
156
|
+
const createBuilder = (optsBuilder) => ({
|
|
157
|
+
opt: (path, value) => createBuilder(optsBuilder.with(path, value)),
|
|
158
|
+
start: (run, ...args) => new _TaskImpl({ ...this.params, opts: optsBuilder.build() }).start(run, ...args)
|
|
159
|
+
});
|
|
160
|
+
return createBuilder(optsOverrider());
|
|
161
|
+
}
|
|
162
|
+
async start(run, ...args) {
|
|
163
|
+
const handle = run[INTERNAL].handle;
|
|
164
|
+
handle[INTERNAL].assertExecutionAllowed();
|
|
165
|
+
const input = isNonEmptyArray(args) ? args[0] : (
|
|
166
|
+
// this cast is okay cos if args is empty, Input must be type null
|
|
167
|
+
null
|
|
168
|
+
);
|
|
169
|
+
const path = await this.getPath(input);
|
|
170
|
+
const taskState = handle.run.tasksState[path] ?? { status: "none" };
|
|
171
|
+
if (taskState.status === "completed") {
|
|
172
|
+
return taskState.output;
|
|
173
|
+
}
|
|
174
|
+
const logger = run.logger.child({
|
|
175
|
+
"aiki.component": "task-execution",
|
|
176
|
+
"aiki.taskPath": path
|
|
177
|
+
});
|
|
178
|
+
let attempts = 0;
|
|
179
|
+
const retryStrategy = this.params.opts?.retry ?? { type: "never" };
|
|
180
|
+
if ("attempts" in taskState) {
|
|
181
|
+
this.assertRetryAllowed(path, taskState.attempts, retryStrategy, logger);
|
|
182
|
+
logger.warn("Retrying task", {
|
|
183
|
+
"aiki.attempts": taskState.attempts,
|
|
184
|
+
"aiki.taskStatus": taskState.status
|
|
185
|
+
});
|
|
186
|
+
attempts = taskState.attempts;
|
|
187
|
+
}
|
|
188
|
+
if (taskState.status === "failed") {
|
|
189
|
+
await this.delayIfNecessary(taskState);
|
|
190
|
+
}
|
|
191
|
+
attempts++;
|
|
192
|
+
logger.info("Starting task", { "aiki.attempts": attempts });
|
|
193
|
+
await handle[INTERNAL].transitionTaskState(path, { status: "running", attempts });
|
|
194
|
+
const { output, lastAttempt } = await this.tryExecuteTask(run, input, path, retryStrategy, attempts, logger);
|
|
195
|
+
await handle[INTERNAL].transitionTaskState(path, { status: "completed", output });
|
|
196
|
+
logger.info("Task complete", { "aiki.attempts": lastAttempt });
|
|
197
|
+
return output;
|
|
198
|
+
}
|
|
199
|
+
async tryExecuteTask(run, input, path, retryStrategy, currentAttempt, logger) {
|
|
200
|
+
let attempts = currentAttempt;
|
|
201
|
+
while (true) {
|
|
202
|
+
const attemptedAt = Date.now();
|
|
203
|
+
try {
|
|
204
|
+
const output = await this.params.handler(input);
|
|
205
|
+
return { output, lastAttempt: attempts };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const serializableError = createSerializableError(error);
|
|
208
|
+
const taskFailedState = {
|
|
209
|
+
status: "failed",
|
|
210
|
+
reason: serializableError.message,
|
|
211
|
+
attempts,
|
|
212
|
+
attemptedAt,
|
|
213
|
+
error: serializableError
|
|
214
|
+
};
|
|
215
|
+
const retryParams = getRetryParams(attempts, retryStrategy);
|
|
216
|
+
if (!retryParams.retriesLeft) {
|
|
217
|
+
await run[INTERNAL].handle[INTERNAL].transitionTaskState(path, taskFailedState);
|
|
218
|
+
logger.error("Task failed", {
|
|
219
|
+
"aiki.attempts": attempts,
|
|
220
|
+
"aiki.reason": taskFailedState.reason
|
|
221
|
+
});
|
|
222
|
+
throw new TaskFailedError(path, attempts, taskFailedState.reason);
|
|
223
|
+
}
|
|
224
|
+
const nextAttemptAt = Date.now() + retryParams.delayMs;
|
|
225
|
+
await run[INTERNAL].handle[INTERNAL].transitionTaskState(path, { ...taskFailedState, nextAttemptAt });
|
|
226
|
+
logger.debug("Task failed. Retrying", {
|
|
227
|
+
"aiki.attempts": attempts,
|
|
228
|
+
"aiki.nextAttemptAt": nextAttemptAt,
|
|
229
|
+
"aiki.reason": taskFailedState.reason
|
|
230
|
+
});
|
|
231
|
+
await delay(retryParams.delayMs);
|
|
232
|
+
attempts++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
assertRetryAllowed(path, attempts, retryStrategy, logger) {
|
|
237
|
+
const retryParams = getRetryParams(attempts, retryStrategy);
|
|
238
|
+
if (!retryParams.retriesLeft) {
|
|
239
|
+
logger.error("Task retry not allowed", {
|
|
240
|
+
"aiki.attempts": attempts
|
|
241
|
+
});
|
|
242
|
+
throw new TaskFailedError(path, attempts, "Task retry not allowed");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async delayIfNecessary(taskState) {
|
|
246
|
+
if (taskState.nextAttemptAt !== void 0) {
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const remainingDelay = Math.max(0, taskState.nextAttemptAt - now);
|
|
249
|
+
if (remainingDelay > 0) {
|
|
250
|
+
await delay(remainingDelay);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async getPath(input) {
|
|
255
|
+
const inputHash = await sha256(stableStringify(input));
|
|
256
|
+
const taskPath = this.params.opts?.idempotencyKey ? `${this.id}/${inputHash}/${this.params.opts.idempotencyKey}` : `${this.id}/${inputHash}`;
|
|
257
|
+
return taskPath;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
export {
|
|
261
|
+
task
|
|
262
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aikirun/task",
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Task SDK for Aiki - define reliable tasks with automatic retries, idempotency, and error handling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@aikirun/types": "0.5.3",
|
|
22
|
+
"@aikirun/workflow": "0.5.3"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0"
|
|
28
|
+
}
|