@sonamu-kit/tasks 0.1.3 → 0.3.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/.oxlintrc.json +3 -0
- package/AGENTS.md +21 -0
- package/dist/backend.d.ts +126 -103
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +4 -1
- package/dist/backend.js.map +1 -1
- package/dist/client.d.ts +145 -132
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +220 -212
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +15 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -17
- package/dist/config.js.map +1 -1
- package/dist/core/duration.d.ts +5 -4
- package/dist/core/duration.d.ts.map +1 -1
- package/dist/core/duration.js +54 -59
- package/dist/core/duration.js.map +1 -1
- package/dist/core/error.d.ts +10 -7
- package/dist/core/error.d.ts.map +1 -1
- package/dist/core/error.js +21 -21
- package/dist/core/error.js.map +1 -1
- package/dist/core/json.d.ts +8 -3
- package/dist/core/json.d.ts.map +1 -1
- package/dist/core/result.d.ts +10 -14
- package/dist/core/result.d.ts.map +1 -1
- package/dist/core/result.js +21 -16
- package/dist/core/result.js.map +1 -1
- package/dist/core/retry.d.ts +42 -20
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +49 -20
- package/dist/core/retry.js.map +1 -1
- package/dist/core/schema.d.ts +57 -53
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/step.d.ts +28 -78
- package/dist/core/step.d.ts.map +1 -1
- package/dist/core/step.js +53 -63
- package/dist/core/step.js.map +1 -1
- package/dist/core/workflow.d.ts +33 -61
- package/dist/core/workflow.d.ts.map +1 -1
- package/dist/core/workflow.js +31 -41
- package/dist/core/workflow.js.map +1 -1
- package/dist/database/backend.d.ts +53 -46
- package/dist/database/backend.d.ts.map +1 -1
- package/dist/database/backend.js +544 -545
- package/dist/database/backend.js.map +1 -1
- package/dist/database/base.js +48 -25
- package/dist/database/base.js.map +1 -1
- package/dist/database/migrations/20251212000000_0_init.d.ts +10 -0
- package/dist/database/migrations/20251212000000_0_init.d.ts.map +1 -0
- package/dist/database/migrations/20251212000000_0_init.js +8 -4
- package/dist/database/migrations/20251212000000_0_init.js.map +1 -1
- package/dist/database/migrations/20251212000000_1_tables.d.ts +10 -0
- package/dist/database/migrations/20251212000000_1_tables.d.ts.map +1 -0
- package/dist/database/migrations/20251212000000_1_tables.js +81 -83
- package/dist/database/migrations/20251212000000_1_tables.js.map +1 -1
- package/dist/database/migrations/20251212000000_2_fk.d.ts +10 -0
- package/dist/database/migrations/20251212000000_2_fk.d.ts.map +1 -0
- package/dist/database/migrations/20251212000000_2_fk.js +20 -43
- package/dist/database/migrations/20251212000000_2_fk.js.map +1 -1
- package/dist/database/migrations/20251212000000_3_indexes.d.ts +10 -0
- package/dist/database/migrations/20251212000000_3_indexes.d.ts.map +1 -0
- package/dist/database/migrations/20251212000000_3_indexes.js +88 -102
- package/dist/database/migrations/20251212000000_3_indexes.js.map +1 -1
- package/dist/database/pubsub.d.ts +7 -16
- package/dist/database/pubsub.d.ts.map +1 -1
- package/dist/database/pubsub.js +75 -73
- package/dist/database/pubsub.js.map +1 -1
- package/dist/execution.d.ts +20 -57
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +175 -174
- package/dist/execution.js.map +1 -1
- package/dist/index.d.ts +5 -8
- package/dist/index.js +5 -5
- package/dist/internal.d.ts +12 -12
- package/dist/internal.js +4 -4
- package/dist/registry.d.ts +33 -27
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +58 -49
- package/dist/registry.js.map +1 -1
- package/dist/worker.d.ts +57 -50
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +194 -198
- package/dist/worker.js.map +1 -1
- package/dist/workflow.d.ts +26 -27
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +20 -15
- package/dist/workflow.js.map +1 -1
- package/nodemon.json +1 -1
- package/package.json +18 -20
- package/src/backend.ts +28 -8
- package/src/chaos.test.ts +3 -1
- package/src/client.test.ts +2 -0
- package/src/client.ts +32 -8
- package/src/config.test.ts +1 -0
- package/src/config.ts +3 -2
- package/src/core/duration.test.ts +2 -1
- package/src/core/duration.ts +1 -1
- package/src/core/error.test.ts +1 -0
- package/src/core/error.ts +1 -1
- package/src/core/result.test.ts +1 -0
- package/src/core/retry.test.ts +181 -11
- package/src/core/retry.ts +95 -19
- package/src/core/schema.ts +2 -2
- package/src/core/step.test.ts +2 -1
- package/src/core/step.ts +4 -3
- package/src/core/workflow.test.ts +2 -1
- package/src/core/workflow.ts +4 -3
- package/src/database/backend.test.ts +1 -0
- package/src/database/backend.testsuite.ts +162 -39
- package/src/database/backend.ts +271 -35
- package/src/database/base.test.ts +41 -0
- package/src/database/base.ts +51 -2
- package/src/database/migrations/20251212000000_0_init.ts +2 -1
- package/src/database/migrations/20251212000000_1_tables.ts +2 -1
- package/src/database/migrations/20251212000000_2_fk.ts +2 -1
- package/src/database/migrations/20251212000000_3_indexes.ts +2 -1
- package/src/database/pubsub.test.ts +6 -3
- package/src/database/pubsub.ts +55 -33
- package/src/execution.test.ts +117 -0
- package/src/execution.ts +65 -10
- package/src/internal.ts +21 -1
- package/src/practices/01-remote-workflow.ts +1 -0
- package/src/registry.test.ts +1 -0
- package/src/registry.ts +1 -1
- package/src/testing/connection.ts +3 -1
- package/src/worker.test.ts +2 -0
- package/src/worker.ts +31 -9
- package/src/workflow.test.ts +1 -0
- package/src/workflow.ts +5 -2
- package/templates/openworkflow.config.ts +2 -1
- package/tsdown.config.ts +31 -0
- package/.swcrc +0 -17
- package/dist/chaos.test.d.ts +0 -2
- package/dist/chaos.test.d.ts.map +0 -1
- package/dist/chaos.test.js +0 -92
- package/dist/chaos.test.js.map +0 -1
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.d.ts.map +0 -1
- package/dist/client.test.js +0 -340
- package/dist/client.test.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -24
- package/dist/config.test.js.map +0 -1
- package/dist/core/duration.test.d.ts +0 -2
- package/dist/core/duration.test.d.ts.map +0 -1
- package/dist/core/duration.test.js +0 -265
- package/dist/core/duration.test.js.map +0 -1
- package/dist/core/error.test.d.ts +0 -2
- package/dist/core/error.test.d.ts.map +0 -1
- package/dist/core/error.test.js +0 -63
- package/dist/core/error.test.js.map +0 -1
- package/dist/core/json.js +0 -3
- package/dist/core/json.js.map +0 -1
- package/dist/core/result.test.d.ts +0 -2
- package/dist/core/result.test.d.ts.map +0 -1
- package/dist/core/result.test.js +0 -19
- package/dist/core/result.test.js.map +0 -1
- package/dist/core/retry.test.d.ts +0 -2
- package/dist/core/retry.test.d.ts.map +0 -1
- package/dist/core/retry.test.js +0 -37
- package/dist/core/retry.test.js.map +0 -1
- package/dist/core/schema.js +0 -4
- package/dist/core/schema.js.map +0 -1
- package/dist/core/step.test.d.ts +0 -2
- package/dist/core/step.test.d.ts.map +0 -1
- package/dist/core/step.test.js +0 -356
- package/dist/core/step.test.js.map +0 -1
- package/dist/core/workflow.test.d.ts +0 -2
- package/dist/core/workflow.test.d.ts.map +0 -1
- package/dist/core/workflow.test.js +0 -172
- package/dist/core/workflow.test.js.map +0 -1
- package/dist/database/backend.test.d.ts +0 -2
- package/dist/database/backend.test.d.ts.map +0 -1
- package/dist/database/backend.test.js +0 -19
- package/dist/database/backend.test.js.map +0 -1
- package/dist/database/backend.testsuite.d.ts +0 -20
- package/dist/database/backend.testsuite.d.ts.map +0 -1
- package/dist/database/backend.testsuite.js +0 -1174
- package/dist/database/backend.testsuite.js.map +0 -1
- package/dist/database/base.d.ts +0 -12
- package/dist/database/base.d.ts.map +0 -1
- package/dist/database/pubsub.test.d.ts +0 -2
- package/dist/database/pubsub.test.d.ts.map +0 -1
- package/dist/database/pubsub.test.js +0 -86
- package/dist/database/pubsub.test.js.map +0 -1
- package/dist/execution.test.d.ts +0 -2
- package/dist/execution.test.d.ts.map +0 -1
- package/dist/execution.test.js +0 -558
- package/dist/execution.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/practices/01-remote-workflow.d.ts +0 -2
- package/dist/practices/01-remote-workflow.d.ts.map +0 -1
- package/dist/practices/01-remote-workflow.js +0 -70
- package/dist/practices/01-remote-workflow.js.map +0 -1
- package/dist/registry.test.d.ts +0 -2
- package/dist/registry.test.d.ts.map +0 -1
- package/dist/registry.test.js +0 -95
- package/dist/registry.test.js.map +0 -1
- package/dist/testing/connection.d.ts +0 -7
- package/dist/testing/connection.d.ts.map +0 -1
- package/dist/testing/connection.js +0 -39
- package/dist/testing/connection.js.map +0 -1
- package/dist/worker.test.d.ts +0 -2
- package/dist/worker.test.d.ts.map +0 -1
- package/dist/worker.test.js +0 -1164
- package/dist/worker.test.js.map +0 -1
- package/dist/workflow.test.d.ts +0 -2
- package/dist/workflow.test.d.ts.map +0 -1
- package/dist/workflow.test.js +0 -73
- package/dist/workflow.test.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonamu-kit/tasks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.3.0",
|
|
5
4
|
"description": "Sonamu Task - Simple & Distributed Task Queue",
|
|
5
|
+
"author": "CartaNova <dev@cartanova.ai>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cartanova-ai/sonamu.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
6
11
|
"main": "./dist/index.js",
|
|
7
12
|
"exports": {
|
|
8
13
|
".": {
|
|
@@ -16,12 +21,8 @@
|
|
|
16
21
|
"development": "./src/internal.ts"
|
|
17
22
|
}
|
|
18
23
|
},
|
|
19
|
-
"author": "CartaNova <dev@cartanova.ai>",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "https://github.com/cartanova-ai/sonamu.git"
|
|
23
|
-
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"c12": "^3.3.2",
|
|
25
26
|
"date-fns": "^4.1.0",
|
|
26
27
|
"date-fns-tz": "^3.2.0",
|
|
27
28
|
"inflection": "^3.0.2",
|
|
@@ -30,33 +31,30 @@
|
|
|
30
31
|
"pg": "^8.16.3",
|
|
31
32
|
"rou3": "^0.7.10",
|
|
32
33
|
"uuid": "^13.0.0",
|
|
33
|
-
"zod": "^4.
|
|
34
|
-
"c12": "^3.3.2"
|
|
34
|
+
"zod": "^4.3.6"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/inflection": "^2.0.0",
|
|
38
|
-
"@
|
|
39
|
-
"@swc/cli": "^0.7.8",
|
|
40
|
-
"@swc/core": "^1.13.5",
|
|
41
|
-
"@types/node": "25.0.3",
|
|
38
|
+
"@types/node": "25.0.7",
|
|
42
39
|
"nodemon": "^3.1.10",
|
|
40
|
+
"tsdown": "^0.12.5",
|
|
43
41
|
"tsx": "^4.20.6",
|
|
44
|
-
"typescript": "^
|
|
45
|
-
"vitest": "^4.
|
|
42
|
+
"typescript": "^6.0.0",
|
|
43
|
+
"vitest": "^4.1.2"
|
|
46
44
|
},
|
|
47
45
|
"peerDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
46
|
+
"@logtape/logtape": "2.0.0",
|
|
47
|
+
"knex": "^3.1.0"
|
|
50
48
|
},
|
|
51
49
|
"optionalDependencies": {
|
|
52
50
|
"pg-native": "^3.5.2"
|
|
53
51
|
},
|
|
54
52
|
"scripts": {
|
|
55
53
|
"dev": "nodemon exec",
|
|
56
|
-
"build": "
|
|
57
|
-
"check": "tsc --noEmit &&
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"check": "tsc --noEmit && oxlint src && oxfmt --check src",
|
|
58
56
|
"check:type": "tsc --noEmit",
|
|
59
|
-
"check:format": "
|
|
57
|
+
"check:format": "oxfmt src",
|
|
60
58
|
"pretest": "tsx scripts/migrate.ts",
|
|
61
59
|
"test": "vitest run",
|
|
62
60
|
"test:watch": "vitest watch --standalone",
|
package/src/backend.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
import
|
|
4
|
-
import type
|
|
5
|
-
import type
|
|
1
|
+
import { type SerializedError } from "./core/error";
|
|
2
|
+
import { type JsonValue } from "./core/json";
|
|
3
|
+
import { type SerializableRetryPolicy } from "./core/retry";
|
|
4
|
+
import { type StepAttempt, type StepAttemptContext, type StepKind } from "./core/step";
|
|
5
|
+
import { type WorkflowRun } from "./core/workflow";
|
|
6
|
+
import { type OnSubscribed } from "./database/pubsub";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_NAMESPACE_ID = "default";
|
|
8
9
|
|
|
@@ -26,6 +27,8 @@ export interface Backend {
|
|
|
26
27
|
completeWorkflowRun(params: Readonly<CompleteWorkflowRunParams>): Promise<WorkflowRun>;
|
|
27
28
|
failWorkflowRun(params: Readonly<FailWorkflowRunParams>): Promise<WorkflowRun>;
|
|
28
29
|
cancelWorkflowRun(params: Readonly<CancelWorkflowRunParams>): Promise<WorkflowRun>;
|
|
30
|
+
pauseWorkflowRun(params: Readonly<PauseWorkflowRunParams>): Promise<WorkflowRun>;
|
|
31
|
+
resumeWorkflowRun(params: Readonly<ResumeWorkflowRunParams>): Promise<WorkflowRun>;
|
|
29
32
|
|
|
30
33
|
// Step Attempts
|
|
31
34
|
createStepAttempt(params: Readonly<CreateStepAttemptParams>): Promise<StepAttempt>;
|
|
@@ -33,8 +36,8 @@ export interface Backend {
|
|
|
33
36
|
listStepAttempts(
|
|
34
37
|
params: Readonly<ListStepAttemptsParams>,
|
|
35
38
|
): Promise<PaginatedResponse<StepAttempt>>;
|
|
36
|
-
completeStepAttempt(params: Readonly<CompleteStepAttemptParams>): Promise<StepAttempt>;
|
|
37
|
-
failStepAttempt(params: Readonly<FailStepAttemptParams>): Promise<StepAttempt>;
|
|
39
|
+
completeStepAttempt(params: Readonly<CompleteStepAttemptParams>): Promise<StepAttempt | null>;
|
|
40
|
+
failStepAttempt(params: Readonly<FailStepAttemptParams>): Promise<StepAttempt | null>;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export interface CreateWorkflowRunParams {
|
|
@@ -46,13 +49,19 @@ export interface CreateWorkflowRunParams {
|
|
|
46
49
|
input: JsonValue | null;
|
|
47
50
|
availableAt: Date | null; // null = immediately
|
|
48
51
|
deadlineAt: Date | null; // null = no deadline
|
|
52
|
+
retryPolicy?: SerializableRetryPolicy;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export interface GetWorkflowRunParams {
|
|
52
56
|
workflowRunId: string;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
export
|
|
59
|
+
export interface ListWorkflowRunsParams extends PaginationOptions {
|
|
60
|
+
status?: string[];
|
|
61
|
+
workflowName?: string;
|
|
62
|
+
createdAfter?: Date;
|
|
63
|
+
createdBefore?: Date;
|
|
64
|
+
}
|
|
56
65
|
|
|
57
66
|
export interface ClaimWorkflowRunParams {
|
|
58
67
|
workerId: string;
|
|
@@ -81,12 +90,22 @@ export interface FailWorkflowRunParams {
|
|
|
81
90
|
workflowRunId: string;
|
|
82
91
|
workerId: string;
|
|
83
92
|
error: SerializedError;
|
|
93
|
+
forceComplete?: boolean;
|
|
94
|
+
customDelayMs?: number;
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
export interface CancelWorkflowRunParams {
|
|
87
98
|
workflowRunId: string;
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
export interface PauseWorkflowRunParams {
|
|
102
|
+
workflowRunId: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ResumeWorkflowRunParams {
|
|
106
|
+
workflowRunId: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
export interface CreateStepAttemptParams {
|
|
91
110
|
workflowRunId: string;
|
|
92
111
|
workerId: string;
|
|
@@ -122,6 +141,7 @@ export interface PaginationOptions {
|
|
|
122
141
|
limit?: number;
|
|
123
142
|
after?: string;
|
|
124
143
|
before?: string;
|
|
144
|
+
order?: "asc" | "desc";
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
export interface PaginatedResponse<T> {
|
package/src/chaos.test.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomInt } from "node:crypto";
|
|
2
|
+
|
|
2
3
|
import { describe, expect, test } from "vitest";
|
|
4
|
+
|
|
3
5
|
import { OpenWorkflow } from "./client";
|
|
4
6
|
import { createBackend } from "./testing/connection";
|
|
5
|
-
import type
|
|
7
|
+
import { type Worker } from "./worker";
|
|
6
8
|
|
|
7
9
|
const TOTAL_STEPS = 50;
|
|
8
10
|
const WORKER_COUNT = 3;
|
package/src/client.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
3
4
|
import { z } from "zod";
|
|
5
|
+
|
|
4
6
|
import { BackendPostgres } from ".";
|
|
5
7
|
import { declareWorkflow, OpenWorkflow } from "./client";
|
|
6
8
|
import { KNEX_GLOBAL_CONFIG } from "./testing/connection";
|
package/src/client.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { type Backend } from "./backend";
|
|
2
|
+
import { serializeRetryPolicy } from "./core/retry";
|
|
3
|
+
import { type StandardSchemaV1 } from "./core/schema";
|
|
4
|
+
import { type SchemaInput, type SchemaOutput, type WorkflowRun } from "./core/workflow";
|
|
4
5
|
import { validateInput } from "./core/workflow";
|
|
5
|
-
import type
|
|
6
|
+
import { type WorkflowFunction } from "./execution";
|
|
6
7
|
import { WorkflowRegistry } from "./registry";
|
|
7
|
-
import { Worker
|
|
8
|
-
import {
|
|
8
|
+
import { Worker } from "./worker";
|
|
9
|
+
import { type WorkerOptions } from "./worker";
|
|
10
|
+
import { defineWorkflow, defineWorkflowSpec } from "./workflow";
|
|
11
|
+
import { type Workflow, type WorkflowSpec } from "./workflow";
|
|
9
12
|
|
|
10
13
|
const DEFAULT_RESULT_POLL_INTERVAL_MS = 1000; // 1s
|
|
11
14
|
const DEFAULT_RESULT_TIMEOUT_MS = 5 * 60 * 1000; // 5m
|
|
@@ -101,6 +104,7 @@ export class OpenWorkflow {
|
|
|
101
104
|
input: parsedInput ?? null,
|
|
102
105
|
availableAt: null,
|
|
103
106
|
deadlineAt: options?.deadlineAt ?? null,
|
|
107
|
+
retryPolicy: spec.retryPolicy ? serializeRetryPolicy(spec.retryPolicy) : undefined,
|
|
104
108
|
});
|
|
105
109
|
|
|
106
110
|
if (options?.publishToChannel) {
|
|
@@ -319,12 +323,32 @@ export class WorkflowRunHandle<Output> {
|
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
/**
|
|
322
|
-
* Cancels the workflow run. Only workflows in pending, running,
|
|
323
|
-
* status can be canceled.
|
|
326
|
+
* Cancels the workflow run. Only workflows in pending, running, sleeping,
|
|
327
|
+
* or paused status can be canceled.
|
|
324
328
|
*/
|
|
325
329
|
async cancel(): Promise<void> {
|
|
326
330
|
await this.backend.cancelWorkflowRun({
|
|
327
331
|
workflowRunId: this.workflowRun.id,
|
|
328
332
|
});
|
|
329
333
|
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Pauses the workflow run. Only workflows in pending, running, or sleeping
|
|
337
|
+
* status can be paused.
|
|
338
|
+
*/
|
|
339
|
+
async pause(): Promise<void> {
|
|
340
|
+
await this.backend.pauseWorkflowRun({
|
|
341
|
+
workflowRunId: this.workflowRun.id,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resumes a paused workflow run. Sets the status back to pending so that
|
|
347
|
+
* a worker can reclaim it.
|
|
348
|
+
*/
|
|
349
|
+
async resume(): Promise<void> {
|
|
350
|
+
await this.backend.resumeWorkflowRun({
|
|
351
|
+
workflowRunId: this.workflowRun.id,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
330
354
|
}
|
package/src/config.test.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadConfig as loadC12Config } from "c12";
|
|
2
|
-
|
|
3
|
-
import type
|
|
2
|
+
|
|
3
|
+
import { type Backend } from "./backend";
|
|
4
|
+
import { type WorkerOptions } from "./worker";
|
|
4
5
|
|
|
5
6
|
export interface OpenWorkflowConfig {
|
|
6
7
|
backend: Backend;
|
package/src/core/duration.ts
CHANGED
package/src/core/error.test.ts
CHANGED
package/src/core/error.ts
CHANGED
package/src/core/result.test.ts
CHANGED
package/src/core/retry.test.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { type SerializedError } from "./error";
|
|
4
|
+
import { type DynamicRetryPolicy, type StaticRetryPolicy } from "./retry";
|
|
5
|
+
import {
|
|
6
|
+
calculateRetryDelayMs,
|
|
7
|
+
DEFAULT_RETRY_POLICY,
|
|
8
|
+
isDynamicRetryPolicy,
|
|
9
|
+
isStaticRetryPolicy,
|
|
10
|
+
mergeRetryPolicy,
|
|
11
|
+
serializeRetryPolicy,
|
|
12
|
+
shouldRetry,
|
|
13
|
+
shouldRetryByPolicy,
|
|
14
|
+
} from "./retry";
|
|
3
15
|
|
|
4
16
|
describe("calculateRetryDelayMs", () => {
|
|
5
17
|
test("calculates exponential backoff correctly", () => {
|
|
@@ -9,14 +21,15 @@ describe("calculateRetryDelayMs", () => {
|
|
|
9
21
|
expect(calculateRetryDelayMs(4)).toBe(8000);
|
|
10
22
|
expect(calculateRetryDelayMs(5)).toBe(16_000);
|
|
11
23
|
expect(calculateRetryDelayMs(6)).toBe(32_000);
|
|
12
|
-
|
|
24
|
+
// attempt 7: 1s * 2^6 = 64s = 64000ms, capped at 60000ms (max)
|
|
25
|
+
expect(calculateRetryDelayMs(7)).toBe(60_000);
|
|
13
26
|
});
|
|
14
27
|
|
|
15
28
|
test("caps delay at maximum interval", () => {
|
|
16
29
|
const { maximumIntervalMs } = DEFAULT_RETRY_POLICY;
|
|
17
30
|
|
|
18
|
-
// attempt
|
|
19
|
-
expect(calculateRetryDelayMs(
|
|
31
|
+
// attempt 7: 1s * 2^6 = 64s = 64000ms, capped at 60000ms (max)
|
|
32
|
+
expect(calculateRetryDelayMs(7)).toBe(maximumIntervalMs);
|
|
20
33
|
|
|
21
34
|
// attempts 10 & 100: should still be capped
|
|
22
35
|
expect(calculateRetryDelayMs(10)).toBe(maximumIntervalMs);
|
|
@@ -26,16 +39,173 @@ describe("calculateRetryDelayMs", () => {
|
|
|
26
39
|
test("handles edge cases", () => {
|
|
27
40
|
// attempt 0: 1s * 2^-1 = 0.5s = 500ms
|
|
28
41
|
expect(calculateRetryDelayMs(0)).toBe(500);
|
|
29
|
-
expect(calculateRetryDelayMs(Infinity)).toBe(
|
|
42
|
+
expect(calculateRetryDelayMs(Infinity)).toBe(60_000);
|
|
30
43
|
});
|
|
31
44
|
});
|
|
32
45
|
|
|
33
46
|
describe("shouldRetry", () => {
|
|
34
|
-
test("
|
|
35
|
-
|
|
36
|
-
expect(shouldRetry(
|
|
37
|
-
expect(shouldRetry(
|
|
38
|
-
expect(shouldRetry(
|
|
39
|
-
expect(shouldRetry(
|
|
47
|
+
test("returns false when attempt reaches maxAttempts", () => {
|
|
48
|
+
// 기본 정책: maxAttempts = 5
|
|
49
|
+
expect(shouldRetry(DEFAULT_RETRY_POLICY, 1)).toBe(true);
|
|
50
|
+
expect(shouldRetry(DEFAULT_RETRY_POLICY, 4)).toBe(true);
|
|
51
|
+
expect(shouldRetry(DEFAULT_RETRY_POLICY, 5)).toBe(false);
|
|
52
|
+
expect(shouldRetry(DEFAULT_RETRY_POLICY, 10)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("shouldRetryByPolicy", () => {
|
|
57
|
+
test("respects maxAttempts from policy", () => {
|
|
58
|
+
expect(shouldRetryByPolicy({ maxAttempts: 3 }, 1)).toBe(true);
|
|
59
|
+
expect(shouldRetryByPolicy({ maxAttempts: 3 }, 2)).toBe(true);
|
|
60
|
+
expect(shouldRetryByPolicy({ maxAttempts: 3 }, 3)).toBe(false);
|
|
61
|
+
expect(shouldRetryByPolicy({ maxAttempts: 3 }, 4)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("uses default maxAttempts when not specified", () => {
|
|
65
|
+
expect(shouldRetryByPolicy({}, 1)).toBe(true);
|
|
66
|
+
expect(shouldRetryByPolicy({}, 4)).toBe(true);
|
|
67
|
+
expect(shouldRetryByPolicy({}, 5)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("isDynamicRetryPolicy", () => {
|
|
72
|
+
test("returns true for policy with shouldRetry function", () => {
|
|
73
|
+
const dynamicPolicy: DynamicRetryPolicy = {
|
|
74
|
+
maxAttempts: 3,
|
|
75
|
+
shouldRetry: () => ({ shouldRetry: true, delayMs: 1000 }),
|
|
76
|
+
};
|
|
77
|
+
expect(isDynamicRetryPolicy(dynamicPolicy)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("returns false for static policy without shouldRetry", () => {
|
|
81
|
+
const staticPolicy: StaticRetryPolicy = {
|
|
82
|
+
maxAttempts: 5,
|
|
83
|
+
initialIntervalMs: 1000,
|
|
84
|
+
};
|
|
85
|
+
expect(isDynamicRetryPolicy(staticPolicy)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("returns false for empty policy", () => {
|
|
89
|
+
expect(isDynamicRetryPolicy({})).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("isStaticRetryPolicy", () => {
|
|
94
|
+
test("returns true for static policy without shouldRetry", () => {
|
|
95
|
+
const staticPolicy: StaticRetryPolicy = {
|
|
96
|
+
maxAttempts: 5,
|
|
97
|
+
initialIntervalMs: 1000,
|
|
98
|
+
};
|
|
99
|
+
expect(isStaticRetryPolicy(staticPolicy)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("returns true for empty policy", () => {
|
|
103
|
+
expect(isStaticRetryPolicy({})).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("returns false for dynamic policy", () => {
|
|
107
|
+
const dynamicPolicy: DynamicRetryPolicy = {
|
|
108
|
+
maxAttempts: 3,
|
|
109
|
+
shouldRetry: () => ({ shouldRetry: true, delayMs: 1000 }),
|
|
110
|
+
};
|
|
111
|
+
expect(isStaticRetryPolicy(dynamicPolicy)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("mergeRetryPolicy", () => {
|
|
116
|
+
test("returns default values when policy is undefined", () => {
|
|
117
|
+
const merged = mergeRetryPolicy(undefined);
|
|
118
|
+
expect(merged.maxAttempts).toBe(5);
|
|
119
|
+
expect(merged.initialIntervalMs).toBe(1000);
|
|
120
|
+
expect(merged.backoffCoefficient).toBe(2);
|
|
121
|
+
expect(merged.maximumIntervalMs).toBe(60_000);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("uses provided values and fills missing with defaults for static policy", () => {
|
|
125
|
+
const merged = mergeRetryPolicy({ maxAttempts: 10, initialIntervalMs: 500 });
|
|
126
|
+
expect(merged.maxAttempts).toBe(10);
|
|
127
|
+
expect(merged.initialIntervalMs).toBe(500);
|
|
128
|
+
expect(merged.backoffCoefficient).toBe(2);
|
|
129
|
+
expect(merged.maximumIntervalMs).toBe(60_000);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns only maxAttempts and shouldRetry for dynamic policy", () => {
|
|
133
|
+
const customFn = (_error: SerializedError, _attempt: number) => ({
|
|
134
|
+
shouldRetry: false,
|
|
135
|
+
delayMs: 5000,
|
|
136
|
+
});
|
|
137
|
+
const dynamicPolicy: DynamicRetryPolicy = {
|
|
138
|
+
maxAttempts: 3,
|
|
139
|
+
shouldRetry: customFn,
|
|
140
|
+
};
|
|
141
|
+
const merged = mergeRetryPolicy(dynamicPolicy);
|
|
142
|
+
|
|
143
|
+
expect(merged.maxAttempts).toBe(3);
|
|
144
|
+
expect(merged.shouldRetry).toBe(customFn);
|
|
145
|
+
// 동적 정책에서는 backoff 필드들이 없어야 합니다.
|
|
146
|
+
expect("initialIntervalMs" in merged).toBe(false);
|
|
147
|
+
expect("backoffCoefficient" in merged).toBe(false);
|
|
148
|
+
expect("maximumIntervalMs" in merged).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("uses default maxAttempts for dynamic policy when not specified", () => {
|
|
152
|
+
const customFn = () => ({ shouldRetry: true, delayMs: 1000 });
|
|
153
|
+
const dynamicPolicy: DynamicRetryPolicy = {
|
|
154
|
+
shouldRetry: customFn,
|
|
155
|
+
};
|
|
156
|
+
const merged = mergeRetryPolicy(dynamicPolicy);
|
|
157
|
+
|
|
158
|
+
expect(merged.maxAttempts).toBe(5); // 기본값
|
|
159
|
+
expect(merged.shouldRetry).toBe(customFn);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("serializeRetryPolicy", () => {
|
|
164
|
+
test("returns empty object with hasDynamicPolicy=false for undefined", () => {
|
|
165
|
+
const serialized = serializeRetryPolicy(undefined);
|
|
166
|
+
expect(serialized.hasDynamicPolicy).toBe(false);
|
|
167
|
+
expect(serialized.maxAttempts).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("serializes static fields for static policy", () => {
|
|
171
|
+
const serialized = serializeRetryPolicy({
|
|
172
|
+
maxAttempts: 10,
|
|
173
|
+
initialIntervalMs: 2000,
|
|
174
|
+
});
|
|
175
|
+
expect(serialized.maxAttempts).toBe(10);
|
|
176
|
+
expect(serialized.initialIntervalMs).toBe(2000);
|
|
177
|
+
expect(serialized.hasDynamicPolicy).toBe(false);
|
|
178
|
+
expect("shouldRetry" in serialized).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("excludes backoff fields for dynamic policy", () => {
|
|
182
|
+
const dynamicPolicy: DynamicRetryPolicy = {
|
|
183
|
+
maxAttempts: 3,
|
|
184
|
+
shouldRetry: () => ({ shouldRetry: true, delayMs: 1000 }),
|
|
185
|
+
};
|
|
186
|
+
const serialized = serializeRetryPolicy(dynamicPolicy);
|
|
187
|
+
|
|
188
|
+
expect(serialized.maxAttempts).toBe(3);
|
|
189
|
+
expect(serialized.hasDynamicPolicy).toBe(true);
|
|
190
|
+
// 동적 정책에서는 backoff 필드들이 없어야 합니다.
|
|
191
|
+
expect(serialized.initialIntervalMs).toBeUndefined();
|
|
192
|
+
expect(serialized.backoffCoefficient).toBeUndefined();
|
|
193
|
+
expect(serialized.maximumIntervalMs).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("includes backoff fields for static policy", () => {
|
|
197
|
+
const staticPolicy: StaticRetryPolicy = {
|
|
198
|
+
maxAttempts: 5,
|
|
199
|
+
initialIntervalMs: 2000,
|
|
200
|
+
backoffCoefficient: 3,
|
|
201
|
+
maximumIntervalMs: 30000,
|
|
202
|
+
};
|
|
203
|
+
const serialized = serializeRetryPolicy(staticPolicy);
|
|
204
|
+
|
|
205
|
+
expect(serialized.maxAttempts).toBe(5);
|
|
206
|
+
expect(serialized.initialIntervalMs).toBe(2000);
|
|
207
|
+
expect(serialized.backoffCoefficient).toBe(3);
|
|
208
|
+
expect(serialized.maximumIntervalMs).toBe(30000);
|
|
209
|
+
expect(serialized.hasDynamicPolicy).toBe(false);
|
|
40
210
|
});
|
|
41
211
|
});
|
package/src/core/retry.ts
CHANGED
|
@@ -1,29 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type SerializedError } from "./error";
|
|
2
|
+
|
|
3
|
+
export interface RetryDecision {
|
|
4
|
+
shouldRetry: boolean;
|
|
5
|
+
delayMs: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type RetryDecisionFn = (error: SerializedError, attempt: number) => RetryDecision;
|
|
9
|
+
|
|
10
|
+
export interface StaticRetryPolicy {
|
|
11
|
+
maxAttempts?: number;
|
|
12
|
+
initialIntervalMs?: number;
|
|
13
|
+
backoffCoefficient?: number;
|
|
14
|
+
maximumIntervalMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DynamicRetryPolicy {
|
|
18
|
+
maxAttempts?: number;
|
|
19
|
+
shouldRetry: RetryDecisionFn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type RetryPolicy = StaticRetryPolicy | DynamicRetryPolicy;
|
|
23
|
+
|
|
24
|
+
export interface SerializableRetryPolicy extends StaticRetryPolicy {
|
|
25
|
+
hasDynamicPolicy?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type MergedStaticRetryPolicy = Required<StaticRetryPolicy>;
|
|
29
|
+
|
|
30
|
+
export interface MergedDynamicRetryPolicy {
|
|
31
|
+
maxAttempts: number;
|
|
32
|
+
shouldRetry: RetryDecisionFn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type MergedRetryPolicy = MergedStaticRetryPolicy | MergedDynamicRetryPolicy;
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_RETRY_POLICY: Required<StaticRetryPolicy> = {
|
|
38
|
+
maxAttempts: 5,
|
|
39
|
+
initialIntervalMs: 1000,
|
|
3
40
|
backoffCoefficient: 2,
|
|
4
|
-
maximumIntervalMs:
|
|
5
|
-
|
|
6
|
-
} as const;
|
|
41
|
+
maximumIntervalMs: 60_000,
|
|
42
|
+
};
|
|
7
43
|
|
|
8
|
-
export
|
|
44
|
+
export function isDynamicRetryPolicy(policy: RetryPolicy): policy is DynamicRetryPolicy {
|
|
45
|
+
return "shouldRetry" in policy && typeof policy.shouldRetry === "function";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isStaticRetryPolicy(policy: RetryPolicy): policy is StaticRetryPolicy {
|
|
49
|
+
return !isDynamicRetryPolicy(policy);
|
|
50
|
+
}
|
|
9
51
|
|
|
10
|
-
/**
|
|
11
|
-
* Calculate the next retry delay using exponential backoff.
|
|
12
|
-
* @param attemptNumber - Attempt number (1-based)
|
|
13
|
-
* @returns Delay in milliseconds
|
|
14
|
-
*/
|
|
15
52
|
export function calculateRetryDelayMs(attemptNumber: number): number {
|
|
16
53
|
const { initialIntervalMs, backoffCoefficient, maximumIntervalMs } = DEFAULT_RETRY_POLICY;
|
|
17
54
|
const backoffMs = initialIntervalMs * backoffCoefficient ** (attemptNumber - 1);
|
|
18
55
|
return Math.min(backoffMs, maximumIntervalMs);
|
|
19
56
|
}
|
|
20
57
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return attemptNumber <
|
|
58
|
+
export function shouldRetry(retryPolicy: StaticRetryPolicy, attemptNumber: number): boolean {
|
|
59
|
+
const maxAttempts = retryPolicy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts;
|
|
60
|
+
return attemptNumber < maxAttempts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function shouldRetryByPolicy(policy: StaticRetryPolicy, attemptNumber: number): boolean {
|
|
64
|
+
const maxAttempts = policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts;
|
|
65
|
+
return attemptNumber < maxAttempts;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function mergeRetryPolicy(policy: StaticRetryPolicy | undefined): MergedStaticRetryPolicy;
|
|
69
|
+
export function mergeRetryPolicy(policy: DynamicRetryPolicy): MergedDynamicRetryPolicy;
|
|
70
|
+
export function mergeRetryPolicy(policy?: RetryPolicy): MergedRetryPolicy;
|
|
71
|
+
export function mergeRetryPolicy(policy?: RetryPolicy): MergedRetryPolicy {
|
|
72
|
+
if (policy && isDynamicRetryPolicy(policy)) {
|
|
73
|
+
return {
|
|
74
|
+
maxAttempts: policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,
|
|
75
|
+
shouldRetry: policy.shouldRetry,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
maxAttempts: policy?.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts,
|
|
80
|
+
initialIntervalMs: policy?.initialIntervalMs ?? DEFAULT_RETRY_POLICY.initialIntervalMs,
|
|
81
|
+
backoffCoefficient: policy?.backoffCoefficient ?? DEFAULT_RETRY_POLICY.backoffCoefficient,
|
|
82
|
+
maximumIntervalMs: policy?.maximumIntervalMs ?? DEFAULT_RETRY_POLICY.maximumIntervalMs,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function serializeRetryPolicy(policy?: RetryPolicy): SerializableRetryPolicy {
|
|
87
|
+
if (!policy) {
|
|
88
|
+
return { hasDynamicPolicy: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isDynamicRetryPolicy(policy)) {
|
|
92
|
+
return {
|
|
93
|
+
maxAttempts: policy.maxAttempts,
|
|
94
|
+
hasDynamicPolicy: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
maxAttempts: policy.maxAttempts,
|
|
100
|
+
initialIntervalMs: policy.initialIntervalMs,
|
|
101
|
+
backoffCoefficient: policy.backoffCoefficient,
|
|
102
|
+
maximumIntervalMs: policy.maximumIntervalMs,
|
|
103
|
+
hasDynamicPolicy: false,
|
|
104
|
+
};
|
|
29
105
|
}
|
package/src/core/schema.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare namespace StandardSchemaV1 {
|
|
|
68
68
|
Schema["~standard"]["types"]
|
|
69
69
|
>["output"];
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
71
|
+
// needed for granular visibility control of TS namespace
|
|
72
|
+
// oxlint-disable-next-line
|
|
73
73
|
export {};
|
|
74
74
|
}
|
package/src/core/step.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
2
3
|
import { ok } from "./result";
|
|
3
|
-
import type
|
|
4
|
+
import { type StepAttempt, type StepAttemptCache } from "./step";
|
|
4
5
|
import {
|
|
5
6
|
addToStepAttemptCache,
|
|
6
7
|
calculateSleepResumeAt,
|