@aikirun/task 0.17.0 → 0.19.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/README.md +0 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +85 -61
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -43,7 +43,6 @@ export const notificationWorkflowV1 = notificationWorkflow.v("1.0.0", {
|
|
|
43
43
|
|
|
44
44
|
- **Automatic Retries** - Configurable retry strategies (fixed, exponential, jittered)
|
|
45
45
|
- **Idempotent Execution** - Same input returns cached result
|
|
46
|
-
- **Reference IDs** - Custom identifiers for deduplication
|
|
47
46
|
- **Schema Validation** - Validate input and output at runtime
|
|
48
47
|
- **Type Safety** - Full TypeScript support
|
|
49
48
|
|
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,7 @@ type IsSubtype<SubT, SuperT> = SubT extends SuperT ? true : false;
|
|
|
11
11
|
type And<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? false extends First ? false : Rest extends NonEmptyArray<boolean> ? And<Rest> : true : never;
|
|
12
12
|
type Or<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? true extends First ? true : Rest extends NonEmptyArray<boolean> ? Or<Rest> : false : never;
|
|
13
13
|
type PathFromObject<T, IncludeArrayKeys extends boolean = false> = T extends T ? PathFromObjectInternal<T, IncludeArrayKeys> : never;
|
|
14
|
-
type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[
|
|
15
|
-
IsSubtype<T, object>,
|
|
16
|
-
Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>
|
|
17
|
-
]> extends true ? {
|
|
14
|
+
type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[IsSubtype<T, object>, Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>]> extends true ? {
|
|
18
15
|
[K in Exclude<keyof T, symbol>]-?: And<[
|
|
19
16
|
IsSubtype<NonNullable<T[K]>, object>,
|
|
20
17
|
Or<[IncludeArrayKeys, NonArrayObject<NonNullable<T[K]>> extends never ? false : true]>
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ../../lib/address/index.ts
|
|
2
|
-
function getTaskAddress(name,
|
|
3
|
-
return `${name}:${
|
|
2
|
+
function getTaskAddress(name, inputHash) {
|
|
3
|
+
return `${name}:${inputHash}`;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// ../../lib/async/delay.ts
|
|
@@ -156,6 +156,7 @@ function getRetryParams(attempts, strategy) {
|
|
|
156
156
|
import { INTERNAL } from "@aikirun/types/symbols";
|
|
157
157
|
import { TaskFailedError } from "@aikirun/types/task";
|
|
158
158
|
import {
|
|
159
|
+
NonDeterminismError,
|
|
159
160
|
WorkflowRunFailedError,
|
|
160
161
|
WorkflowRunRevisionConflictError,
|
|
161
162
|
WorkflowRunSuspendedError
|
|
@@ -183,40 +184,18 @@ var TaskImpl = class {
|
|
|
183
184
|
const inputRaw = args[0];
|
|
184
185
|
const input = await this.parse(handle, this.params.schema?.input, inputRaw, run.logger);
|
|
185
186
|
const inputHash = await hashInput(input);
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
if (existingTaskInfo?.state.status === "failed") {
|
|
196
|
-
const { state } = existingTaskInfo;
|
|
197
|
-
throw new TaskFailedError(existingTaskInfo.id, state.attempts, state.error.message);
|
|
187
|
+
const address = getTaskAddress(this.name, inputHash);
|
|
188
|
+
const replayManifest = run[INTERNAL].replayManifest;
|
|
189
|
+
if (replayManifest.hasUnconsumedEntries()) {
|
|
190
|
+
const existingTaskInfo = replayManifest.consumeNextTask(address);
|
|
191
|
+
if (existingTaskInfo) {
|
|
192
|
+
return this.getExistingTaskResult(run, handle, startOpts, input, existingTaskInfo);
|
|
193
|
+
}
|
|
194
|
+
await this.throwNonDeterminismError(run, handle, inputHash, replayManifest);
|
|
198
195
|
}
|
|
199
|
-
|
|
196
|
+
const attempts = 1;
|
|
200
197
|
const retryStrategy = startOpts.retry ?? { type: "never" };
|
|
201
|
-
|
|
202
|
-
const taskId2 = existingTaskInfo.id;
|
|
203
|
-
const state = existingTaskInfo?.state;
|
|
204
|
-
attempts = state.attempts;
|
|
205
|
-
this.assertRetryAllowed(taskId2, attempts, retryStrategy, run.logger);
|
|
206
|
-
run.logger.debug("Retrying task", {
|
|
207
|
-
"aiki.taskName": this.name,
|
|
208
|
-
"aiki.taskId": taskId2,
|
|
209
|
-
"aiki.attempts": attempts,
|
|
210
|
-
"aiki.taskStatus": state.status
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
attempts++;
|
|
214
|
-
const { taskId } = existingTaskInfo ? await handle[INTERNAL].transitionTaskState({
|
|
215
|
-
type: "retry",
|
|
216
|
-
taskId: existingTaskInfo.id,
|
|
217
|
-
options: startOpts,
|
|
218
|
-
taskState: { status: "running", attempts, input }
|
|
219
|
-
}) : await handle[INTERNAL].transitionTaskState({
|
|
198
|
+
const taskInfo = await handle[INTERNAL].transitionTaskState({
|
|
220
199
|
type: "create",
|
|
221
200
|
taskName: this.name,
|
|
222
201
|
options: startOpts,
|
|
@@ -225,20 +204,89 @@ var TaskImpl = class {
|
|
|
225
204
|
const logger = run.logger.child({
|
|
226
205
|
"aiki.component": "task-execution",
|
|
227
206
|
"aiki.taskName": this.name,
|
|
228
|
-
"aiki.taskId":
|
|
207
|
+
"aiki.taskId": taskInfo.id
|
|
229
208
|
});
|
|
230
209
|
logger.info("Task started", { "aiki.attempts": attempts });
|
|
231
210
|
const { output, lastAttempt } = await this.tryExecuteTask(
|
|
232
211
|
handle,
|
|
233
212
|
input,
|
|
234
|
-
|
|
213
|
+
taskInfo.id,
|
|
235
214
|
retryStrategy,
|
|
236
215
|
attempts,
|
|
237
216
|
run[INTERNAL].options.spinThresholdMs,
|
|
238
217
|
logger
|
|
239
218
|
);
|
|
240
219
|
await handle[INTERNAL].transitionTaskState({
|
|
220
|
+
taskId: taskInfo.id,
|
|
221
|
+
taskState: { status: "completed", attempts: lastAttempt, output }
|
|
222
|
+
});
|
|
223
|
+
logger.info("Task complete", { "aiki.attempts": lastAttempt });
|
|
224
|
+
return output;
|
|
225
|
+
}
|
|
226
|
+
async getExistingTaskResult(run, handle, startOpts, input, existingTaskInfo) {
|
|
227
|
+
const existingTaskState = existingTaskInfo.state;
|
|
228
|
+
if (existingTaskState.status === "completed") {
|
|
229
|
+
return this.parse(handle, this.params.schema?.output, existingTaskState.output, run.logger);
|
|
230
|
+
}
|
|
231
|
+
if (existingTaskState.status === "failed") {
|
|
232
|
+
throw new TaskFailedError(
|
|
233
|
+
existingTaskInfo.id,
|
|
234
|
+
existingTaskState.attempts,
|
|
235
|
+
existingTaskState.error.message
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
existingTaskState.status;
|
|
239
|
+
const attempts = existingTaskState.attempts;
|
|
240
|
+
const retryStrategy = startOpts.retry ?? { type: "never" };
|
|
241
|
+
this.assertRetryAllowed(existingTaskInfo.id, attempts, retryStrategy, run.logger);
|
|
242
|
+
run.logger.debug("Retrying task", {
|
|
243
|
+
"aiki.taskName": this.name,
|
|
244
|
+
"aiki.taskId": existingTaskInfo.id,
|
|
245
|
+
"aiki.attempts": attempts,
|
|
246
|
+
"aiki.taskStatus": existingTaskState.status
|
|
247
|
+
});
|
|
248
|
+
return this.retryAndExecute(run, handle, input, existingTaskInfo.id, startOpts, retryStrategy, attempts);
|
|
249
|
+
}
|
|
250
|
+
async throwNonDeterminismError(run, handle, inputHash, manifest) {
|
|
251
|
+
const unconsumedManifestEntries = manifest.getUnconsumedEntries();
|
|
252
|
+
run.logger.error("Replay divergence", {
|
|
253
|
+
"aiki.taskName": this.name,
|
|
254
|
+
"aiki.inputHash": inputHash,
|
|
255
|
+
"aiki.unconsumedManifestEntries": unconsumedManifestEntries
|
|
256
|
+
});
|
|
257
|
+
const error = new NonDeterminismError(run.id, handle.run.attempts, unconsumedManifestEntries);
|
|
258
|
+
await handle[INTERNAL].transitionState({
|
|
259
|
+
status: "failed",
|
|
260
|
+
cause: "self",
|
|
261
|
+
error: createSerializableError(error)
|
|
262
|
+
});
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
async retryAndExecute(run, handle, input, taskId, startOpts, retryStrategy, previousAttempts) {
|
|
266
|
+
const attempts = previousAttempts + 1;
|
|
267
|
+
const taskInfo = await handle[INTERNAL].transitionTaskState({
|
|
268
|
+
type: "retry",
|
|
241
269
|
taskId,
|
|
270
|
+
options: startOpts,
|
|
271
|
+
taskState: { status: "running", attempts, input }
|
|
272
|
+
});
|
|
273
|
+
const logger = run.logger.child({
|
|
274
|
+
"aiki.component": "task-execution",
|
|
275
|
+
"aiki.taskName": this.name,
|
|
276
|
+
"aiki.taskId": taskInfo.id
|
|
277
|
+
});
|
|
278
|
+
logger.info("Task started", { "aiki.attempts": attempts });
|
|
279
|
+
const { output, lastAttempt } = await this.tryExecuteTask(
|
|
280
|
+
handle,
|
|
281
|
+
input,
|
|
282
|
+
taskInfo.id,
|
|
283
|
+
retryStrategy,
|
|
284
|
+
attempts,
|
|
285
|
+
run[INTERNAL].options.spinThresholdMs,
|
|
286
|
+
logger
|
|
287
|
+
);
|
|
288
|
+
await handle[INTERNAL].transitionTaskState({
|
|
289
|
+
taskId: taskInfo.id,
|
|
242
290
|
taskState: { status: "completed", attempts: lastAttempt, output }
|
|
243
291
|
});
|
|
244
292
|
logger.info("Task complete", { "aiki.attempts": lastAttempt });
|
|
@@ -252,7 +300,7 @@ var TaskImpl = class {
|
|
|
252
300
|
const output = await this.parse(handle, this.params.schema?.output, outputRaw, logger);
|
|
253
301
|
return { output, lastAttempt: attempts };
|
|
254
302
|
} catch (error) {
|
|
255
|
-
if (error instanceof
|
|
303
|
+
if (error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError || error instanceof WorkflowRunRevisionConflictError) {
|
|
256
304
|
throw error;
|
|
257
305
|
}
|
|
258
306
|
const serializableError = createSerializableError(error);
|
|
@@ -291,30 +339,6 @@ var TaskImpl = class {
|
|
|
291
339
|
}
|
|
292
340
|
}
|
|
293
341
|
}
|
|
294
|
-
async assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, logger) {
|
|
295
|
-
if (existingTaskInfo.inputHash !== inputHash && reference) {
|
|
296
|
-
const conflictPolicy = reference.conflictPolicy ?? "error";
|
|
297
|
-
if (conflictPolicy !== "error") {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
logger.error("Reference ID already used by another task", {
|
|
301
|
-
"aiki.taskName": this.name,
|
|
302
|
-
"aiki.referenceId": reference.id,
|
|
303
|
-
"aiki.existingTaskId": existingTaskInfo.id
|
|
304
|
-
});
|
|
305
|
-
const error = new WorkflowRunFailedError(
|
|
306
|
-
handle.run.id,
|
|
307
|
-
handle.run.attempts,
|
|
308
|
-
`Reference ID "${reference.id}" already used by another task ${existingTaskInfo.id}`
|
|
309
|
-
);
|
|
310
|
-
await handle[INTERNAL].transitionState({
|
|
311
|
-
status: "failed",
|
|
312
|
-
cause: "self",
|
|
313
|
-
error: createSerializableError(error)
|
|
314
|
-
});
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
342
|
assertRetryAllowed(taskId, attempts, retryStrategy, logger) {
|
|
319
343
|
const retryParams = getRetryParams(attempts, retryStrategy);
|
|
320
344
|
if (!retryParams.retriesLeft) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikirun/task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
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.
|
|
22
|
-
"@aikirun/workflow": "0.
|
|
21
|
+
"@aikirun/types": "0.19.0",
|
|
22
|
+
"@aikirun/workflow": "0.19.0",
|
|
23
23
|
"@standard-schema/spec": "^1.1.0"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|