@aikirun/task 0.16.0 → 0.18.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/dist/index.d.ts +1 -4
- package/dist/index.js +88 -61
- package/package.json +3 -3
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
|
|
@@ -22,6 +22,9 @@ function delay(ms, options) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// ../../lib/crypto/hash.ts
|
|
26
|
+
import { createHash } from "crypto";
|
|
27
|
+
|
|
25
28
|
// ../../lib/json/stable-stringify.ts
|
|
26
29
|
function stableStringify(value) {
|
|
27
30
|
return stringifyValue(value);
|
|
@@ -153,6 +156,7 @@ function getRetryParams(attempts, strategy) {
|
|
|
153
156
|
import { INTERNAL } from "@aikirun/types/symbols";
|
|
154
157
|
import { TaskFailedError } from "@aikirun/types/task";
|
|
155
158
|
import {
|
|
159
|
+
NonDeterminismError,
|
|
156
160
|
WorkflowRunFailedError,
|
|
157
161
|
WorkflowRunRevisionConflictError,
|
|
158
162
|
WorkflowRunSuspendedError
|
|
@@ -180,40 +184,18 @@ var TaskImpl = class {
|
|
|
180
184
|
const inputRaw = args[0];
|
|
181
185
|
const input = await this.parse(handle, this.params.schema?.input, inputRaw, run.logger);
|
|
182
186
|
const inputHash = await hashInput(input);
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
if (existingTaskInfo?.state.status === "failed") {
|
|
193
|
-
const { state } = existingTaskInfo;
|
|
194
|
-
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);
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
const attempts = 1;
|
|
197
197
|
const retryStrategy = startOpts.retry ?? { type: "never" };
|
|
198
|
-
|
|
199
|
-
const taskId2 = existingTaskInfo.id;
|
|
200
|
-
const state = existingTaskInfo?.state;
|
|
201
|
-
attempts = state.attempts;
|
|
202
|
-
this.assertRetryAllowed(taskId2, attempts, retryStrategy, run.logger);
|
|
203
|
-
run.logger.debug("Retrying task", {
|
|
204
|
-
"aiki.taskName": this.name,
|
|
205
|
-
"aiki.taskId": taskId2,
|
|
206
|
-
"aiki.attempts": attempts,
|
|
207
|
-
"aiki.taskStatus": state.status
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
attempts++;
|
|
211
|
-
const { taskId } = existingTaskInfo ? await handle[INTERNAL].transitionTaskState({
|
|
212
|
-
type: "retry",
|
|
213
|
-
taskId: existingTaskInfo.id,
|
|
214
|
-
options: startOpts,
|
|
215
|
-
taskState: { status: "running", attempts, input }
|
|
216
|
-
}) : await handle[INTERNAL].transitionTaskState({
|
|
198
|
+
const taskInfo = await handle[INTERNAL].transitionTaskState({
|
|
217
199
|
type: "create",
|
|
218
200
|
taskName: this.name,
|
|
219
201
|
options: startOpts,
|
|
@@ -222,20 +204,89 @@ var TaskImpl = class {
|
|
|
222
204
|
const logger = run.logger.child({
|
|
223
205
|
"aiki.component": "task-execution",
|
|
224
206
|
"aiki.taskName": this.name,
|
|
225
|
-
"aiki.taskId":
|
|
207
|
+
"aiki.taskId": taskInfo.id
|
|
226
208
|
});
|
|
227
209
|
logger.info("Task started", { "aiki.attempts": attempts });
|
|
228
210
|
const { output, lastAttempt } = await this.tryExecuteTask(
|
|
229
211
|
handle,
|
|
230
212
|
input,
|
|
231
|
-
|
|
213
|
+
taskInfo.id,
|
|
232
214
|
retryStrategy,
|
|
233
215
|
attempts,
|
|
234
216
|
run[INTERNAL].options.spinThresholdMs,
|
|
235
217
|
logger
|
|
236
218
|
);
|
|
237
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",
|
|
238
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,
|
|
239
290
|
taskState: { status: "completed", attempts: lastAttempt, output }
|
|
240
291
|
});
|
|
241
292
|
logger.info("Task complete", { "aiki.attempts": lastAttempt });
|
|
@@ -249,7 +300,7 @@ var TaskImpl = class {
|
|
|
249
300
|
const output = await this.parse(handle, this.params.schema?.output, outputRaw, logger);
|
|
250
301
|
return { output, lastAttempt: attempts };
|
|
251
302
|
} catch (error) {
|
|
252
|
-
if (error instanceof
|
|
303
|
+
if (error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError || error instanceof WorkflowRunRevisionConflictError) {
|
|
253
304
|
throw error;
|
|
254
305
|
}
|
|
255
306
|
const serializableError = createSerializableError(error);
|
|
@@ -288,30 +339,6 @@ var TaskImpl = class {
|
|
|
288
339
|
}
|
|
289
340
|
}
|
|
290
341
|
}
|
|
291
|
-
async assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, logger) {
|
|
292
|
-
if (existingTaskInfo.inputHash !== inputHash && reference) {
|
|
293
|
-
const conflictPolicy = reference.conflictPolicy ?? "error";
|
|
294
|
-
if (conflictPolicy !== "error") {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
logger.error("Reference ID already used by another task", {
|
|
298
|
-
"aiki.taskName": this.name,
|
|
299
|
-
"aiki.referenceId": reference.id,
|
|
300
|
-
"aiki.existingTaskId": existingTaskInfo.id
|
|
301
|
-
});
|
|
302
|
-
const error = new WorkflowRunFailedError(
|
|
303
|
-
handle.run.id,
|
|
304
|
-
handle.run.attempts,
|
|
305
|
-
`Reference ID "${reference.id}" already used by another task ${existingTaskInfo.id}`
|
|
306
|
-
);
|
|
307
|
-
await handle[INTERNAL].transitionState({
|
|
308
|
-
status: "failed",
|
|
309
|
-
cause: "self",
|
|
310
|
-
error: createSerializableError(error)
|
|
311
|
-
});
|
|
312
|
-
throw error;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
342
|
assertRetryAllowed(taskId, attempts, retryStrategy, logger) {
|
|
316
343
|
const retryParams = getRetryParams(attempts, retryStrategy);
|
|
317
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.18.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.18.0",
|
|
22
|
+
"@aikirun/workflow": "0.18.0",
|
|
23
23
|
"@standard-schema/spec": "^1.1.0"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|