duron 0.3.0-beta.7 → 0.3.0-beta.9
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/action-job.d.ts.map +1 -1
- package/dist/action-job.js +8 -6
- package/dist/action.js +2 -2
- package/dist/errors.d.ts +26 -6
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +37 -10
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +20 -1
- package/package.json +1 -1
- package/src/action-job.ts +13 -6
- package/src/action.ts +3 -3
- package/src/errors.ts +58 -19
- package/src/step-manager.ts +27 -0
package/dist/action-job.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-job.d.ts","sourceRoot":"","sources":["../src/action-job.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAGpD,OAAO,KAAK,EAAQ,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAGpE,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;IACrE,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IACxF,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,MAAM,EAAE,MAAM,CAAA;CACf;AAQD,qBAAa,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;;gBAuB9C,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC;IAsCxC,OAAO;
|
|
1
|
+
{"version":3,"file":"action-job.d.ts","sourceRoot":"","sources":["../src/action-job.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAGpD,OAAO,KAAK,EAAQ,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAGpE,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;IACrE,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IACxF,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,MAAM,EAAE,MAAM,CAAA;CACf;AAQD,qBAAa,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;;gBAuB9C,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC;IAsCxC,OAAO;IAkIb,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B,MAAM;CAmBP"}
|
package/dist/action-job.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionCancelError, ActionTimeoutError, isCancelError,
|
|
1
|
+
import { ActionCancelError, ActionTimeoutError, isCancelError, isTimeoutError, serializeError } from './errors.js';
|
|
2
2
|
import { StepManager } from './step-manager.js';
|
|
3
3
|
import waitForAbort from './utils/wait-for-abort.js';
|
|
4
4
|
export class ActionJob {
|
|
@@ -28,7 +28,7 @@ export class ActionJob {
|
|
|
28
28
|
adapter: options.database,
|
|
29
29
|
telemetry: options.telemetry,
|
|
30
30
|
logger: options.logger,
|
|
31
|
-
concurrencyLimit: options.action.concurrency,
|
|
31
|
+
concurrencyLimit: options.action.steps.concurrency,
|
|
32
32
|
});
|
|
33
33
|
this.#done = new Promise((resolve) => {
|
|
34
34
|
this.#resolve = resolve;
|
|
@@ -84,6 +84,10 @@ export class ActionJob {
|
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
86
|
catch (error) {
|
|
87
|
+
if (!this.#abortController.signal.aborted) {
|
|
88
|
+
this.#abortController.abort(error);
|
|
89
|
+
}
|
|
90
|
+
await this.#stepManager.drain();
|
|
87
91
|
if (isCancelError(error) ||
|
|
88
92
|
(error instanceof Error && error.name === 'AbortError' && isCancelError(error.cause))) {
|
|
89
93
|
this.#logger.warn({ jobId: this.#job.id, actionName: this.#action.name }, '[ActionJob] Job cancelled');
|
|
@@ -93,11 +97,9 @@ export class ActionJob {
|
|
|
93
97
|
}
|
|
94
98
|
return;
|
|
95
99
|
}
|
|
96
|
-
const message = error
|
|
100
|
+
const message = isTimeoutError(error)
|
|
97
101
|
? '[ActionJob] Job timed out'
|
|
98
|
-
:
|
|
99
|
-
? '[ActionJob] Step timed out'
|
|
100
|
-
: '[ActionJob] Job failed';
|
|
102
|
+
: '[ActionJob] Job failed';
|
|
101
103
|
this.#logger.error({ jobId: this.#job.id, actionName: this.#action.name }, message);
|
|
102
104
|
await this.#database.failJob({ jobId: this.#job.id, error: serializeError(error) });
|
|
103
105
|
if (this.#jobSpan) {
|
package/dist/action.js
CHANGED
|
@@ -48,7 +48,7 @@ export function createActionDefinitionSchema() {
|
|
|
48
48
|
.optional(),
|
|
49
49
|
steps: z
|
|
50
50
|
.object({
|
|
51
|
-
concurrency: z.number().default(
|
|
51
|
+
concurrency: z.number().default(100).describe('How many steps can run concurrently for this action'),
|
|
52
52
|
retry: RetryOptionsSchema.describe('How to retry on failure for the steps of this action'),
|
|
53
53
|
expire: z
|
|
54
54
|
.number()
|
|
@@ -56,7 +56,7 @@ export function createActionDefinitionSchema() {
|
|
|
56
56
|
.describe('How long a step can run for (milliseconds)'),
|
|
57
57
|
})
|
|
58
58
|
.default({
|
|
59
|
-
concurrency:
|
|
59
|
+
concurrency: 100,
|
|
60
60
|
retry: { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 },
|
|
61
61
|
expire: 5 * 60 * 1000,
|
|
62
62
|
}),
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
|
+
export declare const ERROR_CODES: {
|
|
2
|
+
readonly DURON_ERROR: "DURON_ERROR";
|
|
3
|
+
readonly STEP_ALREADY_EXECUTED: "STEP_ALREADY_EXECUTED";
|
|
4
|
+
readonly NON_RETRIABLE: "NON_RETRIABLE";
|
|
5
|
+
readonly ACTION_TIMEOUT: "ACTION_TIMEOUT";
|
|
6
|
+
readonly STEP_TIMEOUT: "STEP_TIMEOUT";
|
|
7
|
+
readonly ACTION_CANCEL: "ACTION_CANCEL";
|
|
8
|
+
readonly UNHANDLED_CHILD_STEPS: "UNHANDLED_CHILD_STEPS";
|
|
9
|
+
};
|
|
10
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
1
11
|
export declare abstract class DuronError extends Error {
|
|
12
|
+
readonly code: ErrorCode;
|
|
13
|
+
readonly nonRetriable: boolean;
|
|
2
14
|
readonly cause?: unknown;
|
|
3
15
|
constructor(message: string, options?: {
|
|
4
16
|
cause?: unknown;
|
|
5
17
|
});
|
|
6
18
|
}
|
|
7
19
|
export declare class StepAlreadyExecutedError extends DuronError {
|
|
20
|
+
readonly code: "STEP_ALREADY_EXECUTED";
|
|
21
|
+
readonly nonRetriable = true;
|
|
8
22
|
constructor(stepName: string, jobId: string, actionName: string);
|
|
9
23
|
}
|
|
10
24
|
export declare class NonRetriableError extends DuronError {
|
|
25
|
+
readonly code: ErrorCode;
|
|
26
|
+
readonly nonRetriable = true;
|
|
11
27
|
}
|
|
12
28
|
export declare class ActionTimeoutError extends DuronError {
|
|
29
|
+
readonly code: "ACTION_TIMEOUT";
|
|
30
|
+
readonly nonRetriable = true;
|
|
13
31
|
constructor(actionName: string, timeoutMs: number, options?: {
|
|
14
32
|
cause?: unknown;
|
|
15
33
|
});
|
|
16
34
|
}
|
|
17
35
|
export declare class StepTimeoutError extends DuronError {
|
|
36
|
+
readonly code: "STEP_TIMEOUT";
|
|
37
|
+
readonly nonRetriable = false;
|
|
18
38
|
constructor(stepName: string, jobId: string, timeoutMs: number, options?: {
|
|
19
39
|
cause?: unknown;
|
|
20
40
|
});
|
|
21
41
|
}
|
|
22
42
|
export declare class ActionCancelError extends DuronError {
|
|
43
|
+
readonly code: "ACTION_CANCEL";
|
|
44
|
+
readonly nonRetriable = true;
|
|
23
45
|
constructor(actionName: string, jobId: string, options?: {
|
|
24
46
|
cause?: unknown;
|
|
25
47
|
});
|
|
26
48
|
}
|
|
27
49
|
export declare class UnhandledChildStepsError extends NonRetriableError {
|
|
50
|
+
readonly code: "UNHANDLED_CHILD_STEPS";
|
|
28
51
|
readonly stepName: string;
|
|
29
52
|
readonly pendingCount: number;
|
|
30
53
|
constructor(stepName: string, pendingCount: number);
|
|
@@ -37,13 +60,10 @@ export declare function isCancelError(error: unknown): error is ActionCancelErro
|
|
|
37
60
|
export type SerializableError = {
|
|
38
61
|
name: string;
|
|
39
62
|
message: string;
|
|
63
|
+
code?: ErrorCode;
|
|
64
|
+
nonRetriable?: boolean;
|
|
40
65
|
cause?: unknown;
|
|
41
66
|
stack?: string;
|
|
42
67
|
};
|
|
43
|
-
export declare function serializeError(error: unknown):
|
|
44
|
-
name: string;
|
|
45
|
-
message: string;
|
|
46
|
-
cause?: unknown;
|
|
47
|
-
stack?: string;
|
|
48
|
-
};
|
|
68
|
+
export declare function serializeError(error: unknown): SerializableError;
|
|
49
69
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW;;;;;;;;CAQd,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAA;AAMtE,8BAAsB,UAAW,SAAQ,KAAK;IAI5C,SAAgB,IAAI,EAAE,SAAS,CAA0B;IAKzD,SAAgB,YAAY,EAAE,OAAO,CAAQ;IAO7C,SAAyB,KAAK,CAAC,EAAE,OAAO,CAAA;gBAGtC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAMR,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB;CAWJ;AAKD,qBAAa,wBAAyB,SAAQ,UAAU;IACtD,SAAyB,IAAI,0BAAoC;IACjE,SAAyB,YAAY,QAAO;gBAShC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;CAGhE;AAQD,qBAAa,iBAAkB,SAAQ,UAAU;IAC/C,SAAyB,IAAI,EAAE,SAAS,CAA4B;IACpE,SAAyB,YAAY,QAAO;CAC7C;AAKD,qBAAa,kBAAmB,SAAQ,UAAU;IAChD,SAAyB,IAAI,mBAA6B;IAC1D,SAAyB,YAAY,QAAO;gBAU1C,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB;CAIJ;AAKD,qBAAa,gBAAiB,SAAQ,UAAU;IAC9C,SAAyB,IAAI,iBAA2B;IACxD,SAAyB,YAAY,SAAQ;gBAW3C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB;CAIJ;AAKD,qBAAa,iBAAkB,SAAQ,UAAU;IAC/C,SAAyB,IAAI,kBAA4B;IACzD,SAAyB,YAAY,QAAO;gBAU1C,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB;CAIJ;AAQD,qBAAa,wBAAyB,SAAQ,iBAAiB;IAC7D,SAAyB,IAAI,0BAAoC;IAKjE,SAAgB,QAAQ,EAAE,MAAM,CAAA;IAKhC,SAAgB,YAAY,EAAE,MAAM,CAAA;gBAQxB,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAOnD;AAKD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAGhE;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAE9E;AAKD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,wBAAwB,CAE5F;AAKD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,kBAAkB,GAAG,gBAAgB,CAG7F;AAKD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAExE;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAuChE"}
|
package/dist/errors.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
export const ERROR_CODES = {
|
|
2
|
+
DURON_ERROR: 'DURON_ERROR',
|
|
3
|
+
STEP_ALREADY_EXECUTED: 'STEP_ALREADY_EXECUTED',
|
|
4
|
+
NON_RETRIABLE: 'NON_RETRIABLE',
|
|
5
|
+
ACTION_TIMEOUT: 'ACTION_TIMEOUT',
|
|
6
|
+
STEP_TIMEOUT: 'STEP_TIMEOUT',
|
|
7
|
+
ACTION_CANCEL: 'ACTION_CANCEL',
|
|
8
|
+
UNHANDLED_CHILD_STEPS: 'UNHANDLED_CHILD_STEPS',
|
|
9
|
+
};
|
|
1
10
|
export class DuronError extends Error {
|
|
11
|
+
code = ERROR_CODES.DURON_ERROR;
|
|
12
|
+
nonRetriable = false;
|
|
2
13
|
cause;
|
|
3
14
|
constructor(message, options) {
|
|
4
15
|
super(message);
|
|
@@ -10,28 +21,39 @@ export class DuronError extends Error {
|
|
|
10
21
|
}
|
|
11
22
|
}
|
|
12
23
|
export class StepAlreadyExecutedError extends DuronError {
|
|
24
|
+
code = ERROR_CODES.STEP_ALREADY_EXECUTED;
|
|
25
|
+
nonRetriable = true;
|
|
13
26
|
constructor(stepName, jobId, actionName) {
|
|
14
27
|
super(`Step "${stepName}" has already been executed for job "${jobId}" and action "${actionName}"`);
|
|
15
28
|
}
|
|
16
29
|
}
|
|
17
30
|
export class NonRetriableError extends DuronError {
|
|
31
|
+
code = ERROR_CODES.NON_RETRIABLE;
|
|
32
|
+
nonRetriable = true;
|
|
18
33
|
}
|
|
19
34
|
export class ActionTimeoutError extends DuronError {
|
|
35
|
+
code = ERROR_CODES.ACTION_TIMEOUT;
|
|
36
|
+
nonRetriable = true;
|
|
20
37
|
constructor(actionName, timeoutMs, options) {
|
|
21
38
|
super(`Action "${actionName}" timed out after ${timeoutMs}ms`, options);
|
|
22
39
|
}
|
|
23
40
|
}
|
|
24
41
|
export class StepTimeoutError extends DuronError {
|
|
42
|
+
code = ERROR_CODES.STEP_TIMEOUT;
|
|
43
|
+
nonRetriable = false;
|
|
25
44
|
constructor(stepName, jobId, timeoutMs, options) {
|
|
26
45
|
super(`Step "${stepName}" in job "${jobId}" timed out after ${timeoutMs}ms`, options);
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
export class ActionCancelError extends DuronError {
|
|
49
|
+
code = ERROR_CODES.ACTION_CANCEL;
|
|
50
|
+
nonRetriable = true;
|
|
30
51
|
constructor(actionName, jobId, options) {
|
|
31
52
|
super(`Action "${actionName}" in job "${jobId}" was cancelled`, options);
|
|
32
53
|
}
|
|
33
54
|
}
|
|
34
55
|
export class UnhandledChildStepsError extends NonRetriableError {
|
|
56
|
+
code = ERROR_CODES.UNHANDLED_CHILD_STEPS;
|
|
35
57
|
stepName;
|
|
36
58
|
pendingCount;
|
|
37
59
|
constructor(stepName, pendingCount) {
|
|
@@ -41,36 +63,41 @@ export class UnhandledChildStepsError extends NonRetriableError {
|
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
export function isDuronError(error) {
|
|
44
|
-
|
|
66
|
+
const code = error?.code;
|
|
67
|
+
return code !== undefined && Object.values(ERROR_CODES).includes(code);
|
|
45
68
|
}
|
|
46
69
|
export function isNonRetriableError(error) {
|
|
47
|
-
return
|
|
48
|
-
error instanceof ActionCancelError ||
|
|
49
|
-
error instanceof ActionTimeoutError ||
|
|
50
|
-
error instanceof UnhandledChildStepsError);
|
|
70
|
+
return error?.nonRetriable === true;
|
|
51
71
|
}
|
|
52
72
|
export function isUnhandledChildStepsError(error) {
|
|
53
|
-
return error
|
|
73
|
+
return error?.code === ERROR_CODES.UNHANDLED_CHILD_STEPS;
|
|
54
74
|
}
|
|
55
75
|
export function isTimeoutError(error) {
|
|
56
|
-
|
|
76
|
+
const code = error?.code;
|
|
77
|
+
return code === ERROR_CODES.ACTION_TIMEOUT || code === ERROR_CODES.STEP_TIMEOUT;
|
|
57
78
|
}
|
|
58
79
|
export function isCancelError(error) {
|
|
59
|
-
return error
|
|
80
|
+
return error?.code === ERROR_CODES.ACTION_CANCEL;
|
|
60
81
|
}
|
|
61
82
|
export function serializeError(error) {
|
|
62
|
-
|
|
83
|
+
const code = error?.code;
|
|
84
|
+
const nonRetriable = error?.nonRetriable;
|
|
85
|
+
if (isTimeoutError(error)) {
|
|
63
86
|
return {
|
|
64
87
|
name: error.name,
|
|
65
88
|
message: error.message,
|
|
89
|
+
code,
|
|
90
|
+
nonRetriable,
|
|
66
91
|
cause: error.cause,
|
|
67
92
|
stack: undefined,
|
|
68
93
|
};
|
|
69
94
|
}
|
|
70
|
-
if (error
|
|
95
|
+
if (isDuronError(error)) {
|
|
71
96
|
return {
|
|
72
97
|
name: error.name,
|
|
73
98
|
message: error.message,
|
|
99
|
+
code,
|
|
100
|
+
nonRetriable,
|
|
74
101
|
cause: error.cause,
|
|
75
102
|
stack: error.stack,
|
|
76
103
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-manager.d.ts","sourceRoot":"","sources":["../src/step-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAClC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,EACL,KAAK,MAAM,EACX,KAAK,oBAAoB,EAGzB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAEjB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAgC,MAAM,uBAAuB,CAAA;AAClF,OAAO,EAAoE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"step-manager.d.ts","sourceRoot":"","sources":["../src/step-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAClC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,EACL,KAAK,MAAM,EACX,KAAK,oBAAoB,EAGzB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAEjB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAgC,MAAM,uBAAuB,CAAA;AAClF,OAAO,EAAoE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAYlH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,EAAsB,MAAM,wBAAwB,CAAA;AA2CxG,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAC7C,OAAO,EAAE,WAAW,CAAA;IACpB,WAAW,EAAE,WAAW,CAAA;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,EAAE,OAAO,CAAA;CAClB;AAMD,qBAAa,SAAS;;gBAYR,OAAO,EAAE,OAAO;IAoBtB,WAAW,CACf,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,YAAY,GAAE,MAAM,GAAG,IAAW,EAClC,QAAQ,GAAE,OAAe;;;;;;;;;;IAyBrB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB7F,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC;CAG3E;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAMD,qBAAa,WAAW;;gBAyBV,OAAO,EAAE,kBAAkB;IAqBvC,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAU5B,eAAe,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,WAAW,KAAK,kBAAkB,CAAC,KAAK,CAAC,GAAG,IAAI;IAoBpH,mBAAmB,CAAC,MAAM,SAAS,CAAC,CAAC,SAAS,EAAE,OAAO,SAAS,CAAC,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/G,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAC9D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,EAC3C,SAAS,EAAE,UAAU,EACrB,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,cAAc,GAC7B,oBAAoB,CAAC,MAAM,EAAE,UAAU,CAAC;IAO3C,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAgClD,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;IAuBlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA4T7B"}
|
package/dist/step-manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fastq from 'fastq';
|
|
2
2
|
import { StepOptionsSchema, } from './action.js';
|
|
3
3
|
import { STEP_STATUS_CANCELLED, STEP_STATUS_COMPLETED, STEP_STATUS_FAILED } from './constants.js';
|
|
4
|
-
import { ActionCancelError, isCancelError, isNonRetriableError, NonRetriableError, StepAlreadyExecutedError, StepTimeoutError, serializeError, UnhandledChildStepsError, } from './errors.js';
|
|
4
|
+
import { ActionCancelError, isCancelError, isNonRetriableError, isTimeoutError, NonRetriableError, StepAlreadyExecutedError, StepTimeoutError, serializeError, UnhandledChildStepsError, } from './errors.js';
|
|
5
5
|
const noopTracerSpan = {
|
|
6
6
|
setAttribute() {
|
|
7
7
|
},
|
|
@@ -121,6 +121,17 @@ export class StepManager {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
async push(task) {
|
|
124
|
+
if (task.parentStepId !== null && this.#queue.running() >= this.#queue.concurrency) {
|
|
125
|
+
this.#logger.warn({
|
|
126
|
+
jobId: this.#jobId,
|
|
127
|
+
actionName: this.#actionName,
|
|
128
|
+
stepName: task.name,
|
|
129
|
+
parentStepId: task.parentStepId,
|
|
130
|
+
running: this.#queue.running(),
|
|
131
|
+
waiting: this.#queue.length(),
|
|
132
|
+
concurrency: this.#queue.concurrency,
|
|
133
|
+
}, '[StepManager] Potential starvation: child step queued while all concurrency slots are occupied by parent steps. Consider increasing steps.concurrency.');
|
|
134
|
+
}
|
|
124
135
|
return this.#queue.push(task);
|
|
125
136
|
}
|
|
126
137
|
async drain() {
|
|
@@ -287,6 +298,14 @@ export class StepManager {
|
|
|
287
298
|
throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`);
|
|
288
299
|
}
|
|
289
300
|
}
|
|
301
|
+
else {
|
|
302
|
+
if (isTimeoutError(error)) {
|
|
303
|
+
;
|
|
304
|
+
error.nonRetriable = true;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
throw new NonRetriableError(`Failed to execute step="${name}", jobId="${this.#jobId}", action="${this.#actionName}", parentStepId="${parentStepId}"`, { cause: error });
|
|
308
|
+
}
|
|
290
309
|
},
|
|
291
310
|
}).catch(async (error) => {
|
|
292
311
|
if (step) {
|
package/package.json
CHANGED
package/src/action-job.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Logger } from 'pino'
|
|
|
2
2
|
|
|
3
3
|
import type { Action } from './action.js'
|
|
4
4
|
import type { Adapter } from './adapters/adapter.js'
|
|
5
|
-
import { ActionCancelError, ActionTimeoutError, isCancelError,
|
|
5
|
+
import { ActionCancelError, ActionTimeoutError, isCancelError, isTimeoutError, serializeError } from './errors.js'
|
|
6
6
|
import { StepManager } from './step-manager.js'
|
|
7
7
|
import type { Span, TelemetryAdapter } from './telemetry/adapter.js'
|
|
8
8
|
import waitForAbort from './utils/wait-for-abort.js'
|
|
@@ -61,7 +61,7 @@ export class ActionJob<TAction extends Action<any, any, any>> {
|
|
|
61
61
|
adapter: options.database,
|
|
62
62
|
telemetry: options.telemetry,
|
|
63
63
|
logger: options.logger,
|
|
64
|
-
concurrencyLimit: options.action.concurrency,
|
|
64
|
+
concurrencyLimit: options.action.steps.concurrency,
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
this.#done = new Promise((resolve) => {
|
|
@@ -164,6 +164,15 @@ export class ActionJob<TAction extends Action<any, any, any>> {
|
|
|
164
164
|
|
|
165
165
|
return result
|
|
166
166
|
} catch (error) {
|
|
167
|
+
// Abort all running steps when an error occurs
|
|
168
|
+
// This ensures cascading failure and stops any steps still running
|
|
169
|
+
if (!this.#abortController.signal.aborted) {
|
|
170
|
+
this.#abortController.abort(error)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Wait for step manager to drain (all steps to settle)
|
|
174
|
+
await this.#stepManager.drain()
|
|
175
|
+
|
|
167
176
|
if (
|
|
168
177
|
isCancelError(error) ||
|
|
169
178
|
(error instanceof Error && error.name === 'AbortError' && isCancelError(error.cause))
|
|
@@ -179,11 +188,9 @@ export class ActionJob<TAction extends Action<any, any, any>> {
|
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
const message =
|
|
182
|
-
error
|
|
191
|
+
isTimeoutError(error)
|
|
183
192
|
? '[ActionJob] Job timed out'
|
|
184
|
-
:
|
|
185
|
-
? '[ActionJob] Step timed out'
|
|
186
|
-
: '[ActionJob] Job failed'
|
|
193
|
+
: '[ActionJob] Job failed'
|
|
187
194
|
|
|
188
195
|
this.#logger.error({ jobId: this.#job.id, actionName: this.#action.name }, message)
|
|
189
196
|
await this.#database.failJob({ jobId: this.#job.id, error: serializeError(error) })
|
package/src/action.ts
CHANGED
|
@@ -353,9 +353,9 @@ export function createActionDefinitionSchema<
|
|
|
353
353
|
* Function to determine the concurrency limit for a step.
|
|
354
354
|
* The concurrency limit is stored with each step and used during fetch operations.
|
|
355
355
|
* When fetching steps, the latest step's concurrency limit is used for each stepKey.
|
|
356
|
-
* If not provided, defaults to
|
|
356
|
+
* If not provided, defaults to 100.
|
|
357
357
|
*/
|
|
358
|
-
concurrency: z.number().default(
|
|
358
|
+
concurrency: z.number().default(100).describe('How many steps can run concurrently for this action'),
|
|
359
359
|
retry: RetryOptionsSchema.describe('How to retry on failure for the steps of this action'),
|
|
360
360
|
expire: z
|
|
361
361
|
.number()
|
|
@@ -363,7 +363,7 @@ export function createActionDefinitionSchema<
|
|
|
363
363
|
.describe('How long a step can run for (milliseconds)'),
|
|
364
364
|
})
|
|
365
365
|
.default({
|
|
366
|
-
concurrency:
|
|
366
|
+
concurrency: 100,
|
|
367
367
|
retry: { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 },
|
|
368
368
|
expire: 5 * 60 * 1000,
|
|
369
369
|
}),
|
package/src/errors.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
+
// Error codes for type checking without instanceof
|
|
2
|
+
export const ERROR_CODES = {
|
|
3
|
+
DURON_ERROR: 'DURON_ERROR',
|
|
4
|
+
STEP_ALREADY_EXECUTED: 'STEP_ALREADY_EXECUTED',
|
|
5
|
+
NON_RETRIABLE: 'NON_RETRIABLE',
|
|
6
|
+
ACTION_TIMEOUT: 'ACTION_TIMEOUT',
|
|
7
|
+
STEP_TIMEOUT: 'STEP_TIMEOUT',
|
|
8
|
+
ACTION_CANCEL: 'ACTION_CANCEL',
|
|
9
|
+
UNHANDLED_CHILD_STEPS: 'UNHANDLED_CHILD_STEPS',
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]
|
|
13
|
+
|
|
1
14
|
/**
|
|
2
15
|
* Base class for all built-in errors in Duron.
|
|
3
16
|
* All errors include a cause property that can be serialized.
|
|
4
17
|
*/
|
|
5
18
|
export abstract class DuronError extends Error {
|
|
19
|
+
/**
|
|
20
|
+
* Error code for type checking without instanceof.
|
|
21
|
+
*/
|
|
22
|
+
public readonly code: ErrorCode = ERROR_CODES.DURON_ERROR
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether this error should prevent retries.
|
|
26
|
+
*/
|
|
27
|
+
public readonly nonRetriable: boolean = false
|
|
28
|
+
|
|
6
29
|
/**
|
|
7
30
|
* The underlying cause of the error, if any.
|
|
8
31
|
*
|
|
@@ -36,6 +59,9 @@ export abstract class DuronError extends Error {
|
|
|
36
59
|
* Error thrown when attempting to execute a step that has already been executed.
|
|
37
60
|
*/
|
|
38
61
|
export class StepAlreadyExecutedError extends DuronError {
|
|
62
|
+
public override readonly code = ERROR_CODES.STEP_ALREADY_EXECUTED
|
|
63
|
+
public override readonly nonRetriable = true
|
|
64
|
+
|
|
39
65
|
/**
|
|
40
66
|
* Create a new StepAlreadyExecutedError.
|
|
41
67
|
*
|
|
@@ -55,13 +81,17 @@ export class StepAlreadyExecutedError extends DuronError {
|
|
|
55
81
|
* without retrying, even if retry options are configured.
|
|
56
82
|
*/
|
|
57
83
|
export class NonRetriableError extends DuronError {
|
|
58
|
-
|
|
84
|
+
public override readonly code: ErrorCode = ERROR_CODES.NON_RETRIABLE
|
|
85
|
+
public override readonly nonRetriable = true
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
/**
|
|
62
89
|
* Error thrown when an action exceeds its timeout.
|
|
63
90
|
*/
|
|
64
91
|
export class ActionTimeoutError extends DuronError {
|
|
92
|
+
public override readonly code = ERROR_CODES.ACTION_TIMEOUT
|
|
93
|
+
public override readonly nonRetriable = true
|
|
94
|
+
|
|
65
95
|
/**
|
|
66
96
|
* Create a new ActionTimeoutError.
|
|
67
97
|
*
|
|
@@ -84,6 +114,9 @@ export class ActionTimeoutError extends DuronError {
|
|
|
84
114
|
* Error thrown when a step exceeds its timeout.
|
|
85
115
|
*/
|
|
86
116
|
export class StepTimeoutError extends DuronError {
|
|
117
|
+
public override readonly code = ERROR_CODES.STEP_TIMEOUT
|
|
118
|
+
public override readonly nonRetriable = false
|
|
119
|
+
|
|
87
120
|
/**
|
|
88
121
|
* Create a new StepTimeoutError.
|
|
89
122
|
*
|
|
@@ -108,6 +141,9 @@ export class StepTimeoutError extends DuronError {
|
|
|
108
141
|
* Error thrown when an action is cancelled.
|
|
109
142
|
*/
|
|
110
143
|
export class ActionCancelError extends DuronError {
|
|
144
|
+
public override readonly code = ERROR_CODES.ACTION_CANCEL
|
|
145
|
+
public override readonly nonRetriable = true
|
|
146
|
+
|
|
111
147
|
/**
|
|
112
148
|
* Create a new ActionCancelError.
|
|
113
149
|
*
|
|
@@ -133,6 +169,8 @@ export class ActionCancelError extends DuronError {
|
|
|
133
169
|
* but not properly awaited. All child steps must be awaited before the parent returns.
|
|
134
170
|
*/
|
|
135
171
|
export class UnhandledChildStepsError extends NonRetriableError {
|
|
172
|
+
public override readonly code = ERROR_CODES.UNHANDLED_CHILD_STEPS
|
|
173
|
+
|
|
136
174
|
/**
|
|
137
175
|
* The name of the parent step that completed with unhandled children.
|
|
138
176
|
*/
|
|
@@ -162,45 +200,44 @@ export class UnhandledChildStepsError extends NonRetriableError {
|
|
|
162
200
|
* Checks if an error is a DuronError instance.
|
|
163
201
|
*/
|
|
164
202
|
export function isDuronError(error: unknown): error is DuronError {
|
|
165
|
-
|
|
203
|
+
const code = (error as any)?.code
|
|
204
|
+
return code !== undefined && Object.values(ERROR_CODES).includes(code)
|
|
166
205
|
}
|
|
167
206
|
|
|
168
207
|
/**
|
|
169
208
|
* Checks if an error is a NonRetriableError instance.
|
|
170
209
|
*/
|
|
171
210
|
export function isNonRetriableError(error: unknown): error is NonRetriableError {
|
|
172
|
-
return (
|
|
173
|
-
error instanceof NonRetriableError ||
|
|
174
|
-
error instanceof ActionCancelError ||
|
|
175
|
-
error instanceof ActionTimeoutError ||
|
|
176
|
-
error instanceof UnhandledChildStepsError
|
|
177
|
-
)
|
|
211
|
+
return (error as any)?.nonRetriable === true
|
|
178
212
|
}
|
|
179
213
|
|
|
180
214
|
/**
|
|
181
215
|
* Checks if an error is an UnhandledChildStepsError instance.
|
|
182
216
|
*/
|
|
183
217
|
export function isUnhandledChildStepsError(error: unknown): error is UnhandledChildStepsError {
|
|
184
|
-
return error
|
|
218
|
+
return (error as any)?.code === ERROR_CODES.UNHANDLED_CHILD_STEPS
|
|
185
219
|
}
|
|
186
220
|
|
|
187
221
|
/**
|
|
188
222
|
* Checks if an error is a timeout error (ActionTimeoutError or StepTimeoutError).
|
|
189
223
|
*/
|
|
190
224
|
export function isTimeoutError(error: unknown): error is ActionTimeoutError | StepTimeoutError {
|
|
191
|
-
|
|
225
|
+
const code = (error as any)?.code
|
|
226
|
+
return code === ERROR_CODES.ACTION_TIMEOUT || code === ERROR_CODES.STEP_TIMEOUT
|
|
192
227
|
}
|
|
193
228
|
|
|
194
229
|
/**
|
|
195
230
|
* Checks if an error is a cancel error (ActionCancelError or StepCancelError).
|
|
196
231
|
*/
|
|
197
232
|
export function isCancelError(error: unknown): error is ActionCancelError {
|
|
198
|
-
return error
|
|
233
|
+
return (error as any)?.code === ERROR_CODES.ACTION_CANCEL
|
|
199
234
|
}
|
|
200
235
|
|
|
201
236
|
export type SerializableError = {
|
|
202
237
|
name: string
|
|
203
238
|
message: string
|
|
239
|
+
code?: ErrorCode
|
|
240
|
+
nonRetriable?: boolean
|
|
204
241
|
cause?: unknown
|
|
205
242
|
stack?: string
|
|
206
243
|
}
|
|
@@ -209,25 +246,27 @@ export type SerializableError = {
|
|
|
209
246
|
* Serializes an error for storage in the database.
|
|
210
247
|
* Handles DuronError instances specially to preserve their type information.
|
|
211
248
|
*/
|
|
212
|
-
export function serializeError(error: unknown): {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} {
|
|
218
|
-
if (error instanceof StepTimeoutError || error instanceof ActionTimeoutError) {
|
|
249
|
+
export function serializeError(error: unknown): SerializableError {
|
|
250
|
+
const code = (error as any)?.code
|
|
251
|
+
const nonRetriable = (error as any)?.nonRetriable
|
|
252
|
+
|
|
253
|
+
if (isTimeoutError(error)) {
|
|
219
254
|
return {
|
|
220
255
|
name: error.name,
|
|
221
256
|
message: error.message,
|
|
257
|
+
code,
|
|
258
|
+
nonRetriable,
|
|
222
259
|
cause: error.cause,
|
|
223
260
|
stack: undefined,
|
|
224
261
|
}
|
|
225
262
|
}
|
|
226
263
|
|
|
227
|
-
if (error
|
|
264
|
+
if (isDuronError(error)) {
|
|
228
265
|
return {
|
|
229
266
|
name: error.name,
|
|
230
267
|
message: error.message,
|
|
268
|
+
code,
|
|
269
|
+
nonRetriable,
|
|
231
270
|
cause: error.cause,
|
|
232
271
|
stack: error.stack,
|
|
233
272
|
}
|
package/src/step-manager.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ActionCancelError,
|
|
18
18
|
isCancelError,
|
|
19
19
|
isNonRetriableError,
|
|
20
|
+
isTimeoutError,
|
|
20
21
|
NonRetriableError,
|
|
21
22
|
StepAlreadyExecutedError,
|
|
22
23
|
StepTimeoutError,
|
|
@@ -303,6 +304,21 @@ export class StepManager {
|
|
|
303
304
|
* @returns Promise resolving to the step result
|
|
304
305
|
*/
|
|
305
306
|
async push(task: TaskStep): Promise<any> {
|
|
307
|
+
// Warn about potential starvation when child steps are queued and all slots are occupied
|
|
308
|
+
if (task.parentStepId !== null && this.#queue.running() >= this.#queue.concurrency) {
|
|
309
|
+
this.#logger.warn(
|
|
310
|
+
{
|
|
311
|
+
jobId: this.#jobId,
|
|
312
|
+
actionName: this.#actionName,
|
|
313
|
+
stepName: task.name,
|
|
314
|
+
parentStepId: task.parentStepId,
|
|
315
|
+
running: this.#queue.running(),
|
|
316
|
+
waiting: this.#queue.length(),
|
|
317
|
+
concurrency: this.#queue.concurrency,
|
|
318
|
+
},
|
|
319
|
+
'[StepManager] Potential starvation: child step queued while all concurrency slots are occupied by parent steps. Consider increasing steps.concurrency.',
|
|
320
|
+
)
|
|
321
|
+
}
|
|
306
322
|
return this.#queue.push(task)
|
|
307
323
|
}
|
|
308
324
|
|
|
@@ -478,6 +494,7 @@ export class StepManager {
|
|
|
478
494
|
.catch(() => {
|
|
479
495
|
trackedChild.settled = true
|
|
480
496
|
// Swallow the error here - it will be re-thrown to the caller via the returned promise
|
|
497
|
+
// Note: sibling steps will be aborted when the error propagates to the action level
|
|
481
498
|
})
|
|
482
499
|
|
|
483
500
|
return childPromise
|
|
@@ -592,6 +609,15 @@ export class StepManager {
|
|
|
592
609
|
if (!delayed) {
|
|
593
610
|
throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
|
|
594
611
|
}
|
|
612
|
+
} else {
|
|
613
|
+
if (isTimeoutError(error)) {
|
|
614
|
+
;(error as any).nonRetriable = true
|
|
615
|
+
throw error
|
|
616
|
+
}
|
|
617
|
+
throw new NonRetriableError(
|
|
618
|
+
`Failed to execute step="${name}", jobId="${this.#jobId}", action="${this.#actionName}", parentStepId="${parentStepId}"`,
|
|
619
|
+
{ cause: error },
|
|
620
|
+
)
|
|
595
621
|
}
|
|
596
622
|
},
|
|
597
623
|
}).catch(async (error) => {
|
|
@@ -743,6 +769,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
743
769
|
...this.#action.steps,
|
|
744
770
|
...options,
|
|
745
771
|
})
|
|
772
|
+
|
|
746
773
|
return this.#stepManager.push({
|
|
747
774
|
name,
|
|
748
775
|
cb,
|