@aikirun/task 0.8.0 → 0.9.1

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 (3) hide show
  1. package/README.md +24 -132
  2. package/dist/index.js +32 -33
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @aikirun/task
2
2
 
3
- Task SDK for Aiki durable execution platform - define reliable tasks with automatic retries, idempotency, and error
4
- handling.
3
+ Task SDK for Aiki durable execution platform.
5
4
 
6
5
  ## Installation
7
6
 
@@ -11,159 +10,52 @@ npm install @aikirun/task
11
10
 
12
11
  ## Quick Start
13
12
 
14
- ### Define a Simple Task
15
-
16
13
  ```typescript
17
14
  import { task } from "@aikirun/task";
18
15
 
19
- export const sendVerificationEmail = task({
20
- name: "send-verification",
21
- async handler(input: { email: string }) {
22
- return emailService.sendVerification(input.email);
16
+ export const sendEmail = task({
17
+ name: "send-email",
18
+ async handler(input: { email: string; message: string }) {
19
+ return emailService.send(input.email, input.message);
23
20
  },
24
21
  });
25
22
  ```
26
23
 
27
- ### Task with Retry Configuration
28
-
29
- ```typescript
30
- export const ringAlarm = task({
31
- name: "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
24
+ Execute in a workflow:
46
25
 
47
26
  ```typescript
48
27
  import { workflow } from "@aikirun/workflow";
49
28
 
50
- export const morningWorkflow = workflow({ name: "morning-routine" });
29
+ export const notificationWorkflow = workflow({ name: "notifications" });
51
30
 
52
- export const morningWorkflowV1 = morningWorkflow.v("1.0.0", {
53
- async handler(run, input) {
54
- const result = await ringAlarm.start(run, { song: "alarm.mp3" });
55
- console.log("Task completed:", result);
31
+ export const notificationWorkflowV1 = notificationWorkflow.v("1.0.0", {
32
+ async handler(run, input: { email: string }) {
33
+ await sendEmail.start(run, {
34
+ email: input.email,
35
+ message: "Welcome!",
36
+ });
37
+ return { sent: true };
56
38
  },
57
39
  });
58
40
  ```
59
41
 
60
42
  ## Features
61
43
 
62
- - **Idempotent Execution** - Tasks can be safely retried without unintended side effects
63
- - **Automatic Retries** - Multiple retry strategies (fixed, exponential, jittered)
64
- - **Reference IDs** - Custom identifiers for tracking and deduplication
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
44
+ - **Automatic Retries** - Configurable retry strategies (fixed, exponential, jittered)
45
+ - **Idempotent Execution** - Same input returns cached result
46
+ - **Reference IDs** - Custom identifiers for deduplication
47
+ - **Schema Validation** - Validate input and output at runtime
48
+ - **Type Safety** - Full TypeScript support
70
49
 
71
- ```typescript
72
- interface TaskOptions {
73
- retry?: RetryStrategy;
74
- reference?: { id: string; onConflict?: "error" | "return_existing" };
75
- }
76
- ```
50
+ ## Documentation
77
51
 
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
- name: "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.0", {
141
- async handler(run, input) {
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 Reference IDs** - Use custom reference IDs 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
52
+ For comprehensive documentation including retry strategies, schema validation, and best practices, see the [Tasks Guide](https://aiki.run/docs/core-concepts/tasks).
156
53
 
157
54
  ## Related Packages
158
55
 
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.
56
+ - [@aikirun/workflow](https://www.npmjs.com/package/@aikirun/workflow) - Define workflows
57
+ - [@aikirun/client](https://www.npmjs.com/package/@aikirun/client) - Start workflows
58
+ - [@aikirun/worker](https://www.npmjs.com/package/@aikirun/worker) - Execute workflows
167
59
 
168
60
  ## License
169
61
 
package/dist/index.js CHANGED
@@ -182,19 +182,7 @@ var TaskImpl = class _TaskImpl {
182
182
  const handle = run[INTERNAL].handle;
183
183
  handle[INTERNAL].assertExecutionAllowed();
184
184
  const inputRaw = isNonEmptyArray(args) ? args[0] : void 0;
185
- let input = inputRaw;
186
- if (this.params.schema?.input) {
187
- try {
188
- input = this.params.schema.input.parse(inputRaw);
189
- } catch (error) {
190
- await handle[INTERNAL].transitionState({
191
- status: "failed",
192
- cause: "self",
193
- error: createSerializableError(error)
194
- });
195
- throw new WorkflowRunFailedError(run.id, handle.run.attempts);
196
- }
197
- }
185
+ const input = await this.parse(handle, this.params.schema?.input, inputRaw);
198
186
  const inputHash = await hashInput(input);
199
187
  const reference = this.params.opts?.reference;
200
188
  const path = getTaskPath(this.name, reference?.id ?? inputHash);
@@ -203,7 +191,7 @@ var TaskImpl = class _TaskImpl {
203
191
  await this.assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, run.logger);
204
192
  }
205
193
  if (existingTaskInfo?.state.status === "completed") {
206
- return existingTaskInfo.state.output;
194
+ return this.parse(handle, this.params.schema?.output, existingTaskInfo.state.output);
207
195
  }
208
196
  if (existingTaskInfo?.state.status === "failed") {
209
197
  const { state } = existingTaskInfo;
@@ -245,7 +233,15 @@ var TaskImpl = class _TaskImpl {
245
233
  "aiki.taskId": taskId
246
234
  });
247
235
  logger.info("Task started", { "aiki.attempts": attempts });
248
- const { output, lastAttempt } = await this.tryExecuteTask(run, input, taskId, retryStrategy, attempts, logger);
236
+ const { output, lastAttempt } = await this.tryExecuteTask(
237
+ handle,
238
+ input,
239
+ taskId,
240
+ retryStrategy,
241
+ attempts,
242
+ run[INTERNAL].options.spinThresholdMs,
243
+ logger
244
+ );
249
245
  await handle[INTERNAL].transitionTaskState({
250
246
  taskId,
251
247
  taskState: { status: "completed", attempts: lastAttempt, output }
@@ -253,24 +249,12 @@ var TaskImpl = class _TaskImpl {
253
249
  logger.info("Task complete", { "aiki.attempts": lastAttempt });
254
250
  return output;
255
251
  }
256
- async tryExecuteTask(run, input, taskId, retryStrategy, currentAttempt, logger) {
252
+ async tryExecuteTask(handle, input, taskId, retryStrategy, currentAttempt, spinThresholdMs, logger) {
257
253
  let attempts = currentAttempt;
258
254
  while (true) {
259
255
  try {
260
256
  const outputRaw = await this.params.handler(input);
261
- let output = outputRaw;
262
- if (this.params.schema?.output) {
263
- try {
264
- output = this.params.schema.output.parse(outputRaw);
265
- } catch (error) {
266
- await run[INTERNAL].handle[INTERNAL].transitionState({
267
- status: "failed",
268
- cause: "self",
269
- error: createSerializableError(error)
270
- });
271
- throw new WorkflowRunFailedError(run.id, run[INTERNAL].handle.run.attempts);
272
- }
273
- }
257
+ const output = await this.parse(handle, this.params.schema?.output, outputRaw);
274
258
  return { output, lastAttempt: attempts };
275
259
  } catch (error) {
276
260
  if (error instanceof WorkflowRunFailedError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunConflictError) {
@@ -283,7 +267,7 @@ var TaskImpl = class _TaskImpl {
283
267
  "aiki.attempts": attempts,
284
268
  "aiki.reason": serializableError.message
285
269
  });
286
- await run[INTERNAL].handle[INTERNAL].transitionTaskState({
270
+ await handle[INTERNAL].transitionTaskState({
287
271
  taskId,
288
272
  taskState: { status: "failed", attempts, error: serializableError }
289
273
  });
@@ -294,12 +278,12 @@ var TaskImpl = class _TaskImpl {
294
278
  "aiki.nextAttemptInMs": retryParams.delayMs,
295
279
  "aiki.reason": serializableError.message
296
280
  });
297
- if (retryParams.delayMs <= run[INTERNAL].options.spinThresholdMs) {
281
+ if (retryParams.delayMs <= spinThresholdMs) {
298
282
  await delay(retryParams.delayMs);
299
283
  attempts++;
300
284
  continue;
301
285
  }
302
- await run[INTERNAL].handle[INTERNAL].transitionTaskState({
286
+ await handle[INTERNAL].transitionTaskState({
303
287
  taskId,
304
288
  taskState: {
305
289
  status: "awaiting_retry",
@@ -308,7 +292,7 @@ var TaskImpl = class _TaskImpl {
308
292
  nextAttemptInMs: retryParams.delayMs
309
293
  }
310
294
  });
311
- throw new WorkflowRunSuspendedError(run.id);
295
+ throw new WorkflowRunSuspendedError(handle.run.id);
312
296
  }
313
297
  }
314
298
  }
@@ -348,6 +332,21 @@ var TaskImpl = class _TaskImpl {
348
332
  throw new TaskFailedError(taskId, attempts, "Task retry not allowed");
349
333
  }
350
334
  }
335
+ async parse(handle, schema, data) {
336
+ if (!schema) {
337
+ return data;
338
+ }
339
+ try {
340
+ return schema.parse(data);
341
+ } catch (error) {
342
+ await handle[INTERNAL].transitionState({
343
+ status: "failed",
344
+ cause: "self",
345
+ error: createSerializableError(error)
346
+ });
347
+ throw new WorkflowRunFailedError(handle.run.id, handle.run.attempts);
348
+ }
349
+ }
351
350
  };
352
351
  export {
353
352
  task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/task",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Task SDK for Aiki - define reliable tasks with automatic retries, idempotency, and error handling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,8 +18,8 @@
18
18
  "build": "tsup"
19
19
  },
20
20
  "dependencies": {
21
- "@aikirun/types": "0.8.0",
22
- "@aikirun/workflow": "0.8.0"
21
+ "@aikirun/types": "0.9.1",
22
+ "@aikirun/workflow": "0.9.1"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"