awaitly 2.0.0 → 3.0.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/di-Biw_plBn.d.cts +15 -0
- package/dist/di-DxyH2N3i.d.ts +15 -0
- package/dist/durable.cjs +4670 -0
- package/dist/durable.cjs.map +1 -0
- package/dist/durable.d.cts +9 -0
- package/dist/durable.d.ts +9 -0
- package/dist/durable.js +4610 -0
- package/dist/durable.js.map +1 -0
- package/dist/{di-BbFFfO8y.d.ts → duration-BjVn3QpB.d.cts} +1 -15
- package/dist/{di-BDlT7InM.d.cts → duration-BjVn3QpB.d.ts} +1 -15
- package/dist/engine.cjs +4501 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +113 -0
- package/dist/engine.d.ts +113 -0
- package/dist/engine.js +4474 -0
- package/dist/engine.js.map +1 -0
- package/dist/{errors-DtXvrCiO.d.cts → errors-sDSeaZBO.d.cts} +1 -1
- package/dist/{errors-DtXvrCiO.d.ts → errors-sDSeaZBO.d.ts} +1 -1
- package/dist/guards-CwQZro1F.d.ts +72 -0
- package/dist/guards-DhhOJZda.d.cts +72 -0
- package/dist/hitl-BnnnLzh2.d.cts +468 -0
- package/dist/hitl-pYfkQMv_.d.ts +468 -0
- package/dist/hitl.cjs +646 -0
- package/dist/hitl.cjs.map +1 -0
- package/dist/hitl.d.cts +440 -0
- package/dist/hitl.d.ts +440 -0
- package/dist/hitl.js +605 -0
- package/dist/hitl.js.map +1 -0
- package/dist/index-Bew12SZ8.d.ts +417 -0
- package/dist/index-Bwt-iz50.d.cts +417 -0
- package/dist/{types-B8NfNRGX.d.ts → index-Cv4K_3sZ.d.ts} +2 -1162
- package/dist/{types-BZ2f4MRR.d.cts → index-ICmfQi08.d.cts} +2 -1162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1527
- package/dist/index.d.ts +11 -1527
- package/dist/persistence.cjs +552 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.cts +254 -0
- package/dist/persistence.d.ts +254 -0
- package/dist/persistence.js +499 -0
- package/dist/persistence.js.map +1 -0
- package/dist/reliability.cjs +1651 -0
- package/dist/reliability.cjs.map +1 -0
- package/dist/reliability.d.cts +1442 -0
- package/dist/reliability.d.ts +1442 -0
- package/dist/reliability.js +1589 -0
- package/dist/reliability.js.map +1 -0
- package/dist/result.d.cts +1 -1
- package/dist/result.d.ts +1 -1
- package/dist/run.cjs +2283 -0
- package/dist/run.cjs.map +1 -0
- package/dist/run.d.cts +90 -0
- package/dist/run.d.ts +90 -0
- package/dist/run.js +2247 -0
- package/dist/run.js.map +1 -0
- package/dist/saga.cjs +3699 -0
- package/dist/saga.cjs.map +1 -0
- package/dist/saga.d.cts +162 -0
- package/dist/saga.d.ts +162 -0
- package/dist/saga.js +3670 -0
- package/dist/saga.js.map +1 -0
- package/dist/store-contract-BI98VYmX.d.ts +66 -0
- package/dist/store-contract-C2HyR_o6.d.cts +66 -0
- package/dist/streaming.cjs +895 -0
- package/dist/streaming.cjs.map +1 -0
- package/dist/streaming.d.cts +594 -0
- package/dist/streaming.d.ts +594 -0
- package/dist/streaming.js +832 -0
- package/dist/streaming.js.map +1 -0
- package/dist/testing.d.cts +4 -2
- package/dist/testing.d.ts +4 -2
- package/dist/types-BDTxgKKc.d.ts +845 -0
- package/dist/types-C9Y71dua.d.cts +845 -0
- package/dist/types-d8q8iQlk.d.cts +323 -0
- package/dist/types-sObbY4mX.d.ts +323 -0
- package/dist/webhook.cjs +461 -0
- package/dist/webhook.cjs.map +1 -0
- package/dist/webhook.d.cts +497 -0
- package/dist/webhook.d.ts +497 -0
- package/dist/webhook.js +422 -0
- package/dist/webhook.js.map +1 -0
- package/dist/workflow.cjs +2 -2614
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +12 -3023
- package/dist/workflow.d.ts +12 -3023
- package/dist/workflow.js +2 -2535
- package/dist/workflow.js.map +1 -1
- package/package.json +46 -1
package/dist/engine.js
ADDED
|
@@ -0,0 +1,4474 @@
|
|
|
1
|
+
// src/engine/index.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/slugs.ts
|
|
5
|
+
var AWAITLY_SLUGS = {
|
|
6
|
+
// --- step-* ---
|
|
7
|
+
"step-require-id": "step-require-id",
|
|
8
|
+
"step-no-immediate-execution": "step-no-immediate-execution",
|
|
9
|
+
"step-require-thunk-for-key": "step-require-thunk-for-key",
|
|
10
|
+
"step-no-bare-await": "step-no-bare-await",
|
|
11
|
+
"step-no-try-catch-wrap": "step-no-try-catch-wrap",
|
|
12
|
+
"step-stable-cache-keys": "step-stable-cache-keys",
|
|
13
|
+
// --- workflow-* ---
|
|
14
|
+
"workflow-no-floating": "workflow-no-floating",
|
|
15
|
+
"workflow-options-position": "workflow-options-position",
|
|
16
|
+
"workflow-callback-shape": "workflow-callback-shape",
|
|
17
|
+
"workflow-no-callable-form": "workflow-no-callable-form",
|
|
18
|
+
"workflow-no-dynamic-import": "workflow-no-dynamic-import",
|
|
19
|
+
"workflow-prefer-step-if": "workflow-prefer-step-if",
|
|
20
|
+
"workflow-prefer-step-foreach": "workflow-prefer-step-foreach",
|
|
21
|
+
// --- result-* ---
|
|
22
|
+
"result-no-floating": "result-no-floating",
|
|
23
|
+
"result-require-handling": "result-require-handling",
|
|
24
|
+
"result-no-double-wrap": "result-no-double-wrap",
|
|
25
|
+
"result-no-manual-propagation": "result-no-manual-propagation",
|
|
26
|
+
"result-no-direct-ok-err": "result-no-direct-ok-err",
|
|
27
|
+
// --- error-* ---
|
|
28
|
+
"error-check-unexpected-first": "error-check-unexpected-first",
|
|
29
|
+
"error-access-cause": "error-access-cause",
|
|
30
|
+
"error-normalize": "error-normalize",
|
|
31
|
+
"error-no-throw-in-deps": "error-no-throw-in-deps",
|
|
32
|
+
// --- concurrency-* ---
|
|
33
|
+
"concurrency-no-promise-all": "concurrency-no-promise-all",
|
|
34
|
+
"concurrency-no-promise-race": "concurrency-no-promise-race",
|
|
35
|
+
"concurrency-no-promise-allsettled": "concurrency-no-promise-allsettled",
|
|
36
|
+
// --- runtime-* ---
|
|
37
|
+
"runtime-step-timeout": "runtime-step-timeout",
|
|
38
|
+
"runtime-step-aborted": "runtime-step-aborted",
|
|
39
|
+
"runtime-retry-exhausted": "runtime-retry-exhausted",
|
|
40
|
+
"runtime-rate-limit": "runtime-rate-limit",
|
|
41
|
+
"runtime-circuit-open": "runtime-circuit-open",
|
|
42
|
+
"runtime-unexpected": "runtime-unexpected",
|
|
43
|
+
"runtime-resolver-not-found": "runtime-resolver-not-found",
|
|
44
|
+
"runtime-saga-compensation": "runtime-saga-compensation"
|
|
45
|
+
};
|
|
46
|
+
function slugDocsUrl(slug) {
|
|
47
|
+
return `https://jagreehal.github.io/awaitly/rules/#${slug}`;
|
|
48
|
+
}
|
|
49
|
+
var ALL_SLUGS = Object.keys(
|
|
50
|
+
AWAITLY_SLUGS
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// src/tagged-error.ts
|
|
54
|
+
var InternalTaggedErrorBase = class extends Error {
|
|
55
|
+
type;
|
|
56
|
+
/** @deprecated Use `type`. */
|
|
57
|
+
_tag;
|
|
58
|
+
};
|
|
59
|
+
function TaggedError(tag, options) {
|
|
60
|
+
return class extends InternalTaggedErrorBase {
|
|
61
|
+
type = tag;
|
|
62
|
+
/** @deprecated Use `type`. */
|
|
63
|
+
_tag = tag;
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
constructor(props, errorOptions) {
|
|
66
|
+
const message = options?.message ? options.message(props ?? {}) : tag;
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = tag;
|
|
69
|
+
if (options?.slug !== void 0) {
|
|
70
|
+
if (!options.hint) {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
`TaggedError: 'hint' is required when 'slug' is set (slug: "${options.slug}")`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
Object.defineProperty(this, "code", {
|
|
76
|
+
value: options.slug,
|
|
77
|
+
enumerable: true,
|
|
78
|
+
writable: false,
|
|
79
|
+
configurable: false
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(this, "hint", {
|
|
82
|
+
value: options.hint,
|
|
83
|
+
enumerable: true,
|
|
84
|
+
writable: false,
|
|
85
|
+
configurable: false
|
|
86
|
+
});
|
|
87
|
+
Object.defineProperty(this, "docsUrl", {
|
|
88
|
+
value: slugDocsUrl(options.slug),
|
|
89
|
+
enumerable: true,
|
|
90
|
+
writable: false,
|
|
91
|
+
configurable: false
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
95
|
+
if (props && typeof props === "object") {
|
|
96
|
+
let safeProps;
|
|
97
|
+
if (options?.slug !== void 0) {
|
|
98
|
+
const {
|
|
99
|
+
_tag: _,
|
|
100
|
+
type: _t,
|
|
101
|
+
name: _n,
|
|
102
|
+
message: _m,
|
|
103
|
+
stack: _s,
|
|
104
|
+
code: _c,
|
|
105
|
+
hint: _h,
|
|
106
|
+
docsUrl: _d,
|
|
107
|
+
...rest
|
|
108
|
+
} = props;
|
|
109
|
+
safeProps = rest;
|
|
110
|
+
} else {
|
|
111
|
+
const {
|
|
112
|
+
_tag: _,
|
|
113
|
+
type: _t,
|
|
114
|
+
name: _n,
|
|
115
|
+
message: _m,
|
|
116
|
+
stack: _s,
|
|
117
|
+
...rest
|
|
118
|
+
} = props;
|
|
119
|
+
safeProps = rest;
|
|
120
|
+
}
|
|
121
|
+
const hasUserCause = Object.prototype.hasOwnProperty.call(
|
|
122
|
+
safeProps,
|
|
123
|
+
"cause"
|
|
124
|
+
);
|
|
125
|
+
const userCause = hasUserCause ? safeProps.cause : void 0;
|
|
126
|
+
if (hasUserCause) {
|
|
127
|
+
delete safeProps.cause;
|
|
128
|
+
}
|
|
129
|
+
const hasOptionsCause = errorOptions?.cause !== void 0;
|
|
130
|
+
if (hasUserCause && hasOptionsCause) {
|
|
131
|
+
throw new TypeError(
|
|
132
|
+
"TaggedError: cannot provide 'cause' in props when also setting ErrorOptions.cause"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
Object.assign(this, safeProps);
|
|
136
|
+
if (hasUserCause) {
|
|
137
|
+
this.cause = userCause;
|
|
138
|
+
}
|
|
139
|
+
if (hasOptionsCause) {
|
|
140
|
+
this.cause = errorOptions?.cause;
|
|
141
|
+
}
|
|
142
|
+
} else if (errorOptions?.cause !== void 0) {
|
|
143
|
+
this.cause = errorOptions.cause;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
Object.defineProperty(TaggedError, Symbol.hasInstance, {
|
|
149
|
+
value: (instance) => instance instanceof InternalTaggedErrorBase
|
|
150
|
+
});
|
|
151
|
+
((TaggedError2) => {
|
|
152
|
+
function isError(value) {
|
|
153
|
+
return value instanceof Error;
|
|
154
|
+
}
|
|
155
|
+
TaggedError2.isError = isError;
|
|
156
|
+
function isTaggedError(value) {
|
|
157
|
+
return value instanceof InternalTaggedErrorBase;
|
|
158
|
+
}
|
|
159
|
+
TaggedError2.isTaggedError = isTaggedError;
|
|
160
|
+
function match(error, handlers) {
|
|
161
|
+
const tag = error._tag;
|
|
162
|
+
const handler = handlers[tag];
|
|
163
|
+
return handler(
|
|
164
|
+
error
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
TaggedError2.match = match;
|
|
168
|
+
function matchPartial(error, handlers, otherwise) {
|
|
169
|
+
const tag = error._tag;
|
|
170
|
+
const handler = handlers[tag];
|
|
171
|
+
if (handler) {
|
|
172
|
+
return handler(
|
|
173
|
+
error
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return otherwise(error);
|
|
177
|
+
}
|
|
178
|
+
TaggedError2.matchPartial = matchPartial;
|
|
179
|
+
})(TaggedError || (TaggedError = {}));
|
|
180
|
+
|
|
181
|
+
// src/errors.ts
|
|
182
|
+
var UnexpectedError = class extends (/* @__PURE__ */ TaggedError("UnexpectedError", {
|
|
183
|
+
slug: "runtime-unexpected",
|
|
184
|
+
hint: "An unexpected exception escaped a step. Inspect cause; consider returning a typed Result instead of throwing.",
|
|
185
|
+
message: (p) => `UnexpectedError: ${p.cause instanceof Error ? p.cause.message : String(p.cause ?? "unknown")}`
|
|
186
|
+
})) {
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/result/index.ts
|
|
190
|
+
function ok(value) {
|
|
191
|
+
return { ok: true, value };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/core/bound-steps.ts
|
|
195
|
+
var isDepResultShaped = (value) => typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean" && (value.ok ? "value" in value : "error" in value);
|
|
196
|
+
var bindSteps = (deps, step) => {
|
|
197
|
+
const steps = {};
|
|
198
|
+
const invocationCounts = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const key of Object.keys(deps)) {
|
|
200
|
+
const dep = deps[key];
|
|
201
|
+
if (typeof dep !== "function") continue;
|
|
202
|
+
steps[key] = (...args) => {
|
|
203
|
+
const count = (invocationCounts.get(key) ?? 0) + 1;
|
|
204
|
+
invocationCounts.set(key, count);
|
|
205
|
+
const stepKey = count === 1 ? key : `${key}#${count}`;
|
|
206
|
+
return step(stepKey, async () => {
|
|
207
|
+
const value = await dep(...args);
|
|
208
|
+
return isDepResultShaped(value) ? value : ok(value);
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return steps;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/duration.ts
|
|
216
|
+
function millis(ms) {
|
|
217
|
+
return { _tag: "Duration", millis: ms };
|
|
218
|
+
}
|
|
219
|
+
function seconds(s) {
|
|
220
|
+
return { _tag: "Duration", millis: s * 1e3 };
|
|
221
|
+
}
|
|
222
|
+
function minutes(m) {
|
|
223
|
+
return { _tag: "Duration", millis: m * 60 * 1e3 };
|
|
224
|
+
}
|
|
225
|
+
function hours(h) {
|
|
226
|
+
return { _tag: "Duration", millis: h * 60 * 60 * 1e3 };
|
|
227
|
+
}
|
|
228
|
+
function days(d) {
|
|
229
|
+
return { _tag: "Duration", millis: d * 24 * 60 * 60 * 1e3 };
|
|
230
|
+
}
|
|
231
|
+
function toMillis(duration) {
|
|
232
|
+
return duration.millis;
|
|
233
|
+
}
|
|
234
|
+
function parse(input) {
|
|
235
|
+
const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i);
|
|
236
|
+
if (!match) return void 0;
|
|
237
|
+
const value = parseFloat(match[1]);
|
|
238
|
+
const unit = match[2].toLowerCase();
|
|
239
|
+
switch (unit) {
|
|
240
|
+
case "ms":
|
|
241
|
+
return millis(value);
|
|
242
|
+
case "s":
|
|
243
|
+
return seconds(value);
|
|
244
|
+
case "m":
|
|
245
|
+
return minutes(value);
|
|
246
|
+
case "h":
|
|
247
|
+
return hours(value);
|
|
248
|
+
case "d":
|
|
249
|
+
return days(value);
|
|
250
|
+
default:
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/core/index.ts
|
|
256
|
+
function parseDurationString(input) {
|
|
257
|
+
const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i);
|
|
258
|
+
if (!match) return void 0;
|
|
259
|
+
const value = parseFloat(match[1]);
|
|
260
|
+
const unit = match[2].toLowerCase();
|
|
261
|
+
const multipliers = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
262
|
+
return { _tag: "Duration", millis: value * (multipliers[unit] ?? 1) };
|
|
263
|
+
}
|
|
264
|
+
function defaultCatchUnexpected(cause) {
|
|
265
|
+
return new UnexpectedError({ cause });
|
|
266
|
+
}
|
|
267
|
+
function ok2(value) {
|
|
268
|
+
return { ok: true, value };
|
|
269
|
+
}
|
|
270
|
+
function err(error, options) {
|
|
271
|
+
const cause = options?.cause;
|
|
272
|
+
return { ok: false, error, ...cause !== void 0 ? { cause } : {} };
|
|
273
|
+
}
|
|
274
|
+
var isUnexpectedError = (e) => e instanceof UnexpectedError || typeof e === "object" && e !== null && "_tag" in e && e._tag === "UnexpectedError";
|
|
275
|
+
function extractErrorTag(error) {
|
|
276
|
+
if (error == null) return "unknown";
|
|
277
|
+
if (typeof error === "string") return error.trim() || "unknown";
|
|
278
|
+
if (typeof error === "object") {
|
|
279
|
+
const tagged = error;
|
|
280
|
+
if (typeof tagged._tag === "string") {
|
|
281
|
+
const trimmed = tagged._tag.trim();
|
|
282
|
+
if (trimmed) return trimmed;
|
|
283
|
+
}
|
|
284
|
+
if (typeof tagged.tag === "string") {
|
|
285
|
+
const trimmed = tagged.tag.trim();
|
|
286
|
+
if (trimmed) return trimmed;
|
|
287
|
+
}
|
|
288
|
+
if (typeof tagged.code === "string") {
|
|
289
|
+
const trimmed = tagged.code.trim();
|
|
290
|
+
if (trimmed) return trimmed;
|
|
291
|
+
} else if (typeof tagged.code === "number") {
|
|
292
|
+
return String(tagged.code);
|
|
293
|
+
}
|
|
294
|
+
if (error instanceof Error && error.name) {
|
|
295
|
+
const trimmed = error.name.trim();
|
|
296
|
+
if (trimmed) return trimmed;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return "unknown";
|
|
300
|
+
}
|
|
301
|
+
function lookupErrorClassification(tag, errorMeta) {
|
|
302
|
+
if (!errorMeta || !tag) return void 0;
|
|
303
|
+
return errorMeta[tag];
|
|
304
|
+
}
|
|
305
|
+
function extractStepMetadata(options) {
|
|
306
|
+
const { intent, domain, owner, tags, stateChanges, emits, calls } = options;
|
|
307
|
+
if (!intent && !domain && !owner && !tags?.length && !stateChanges?.length && !emits?.length && !calls?.length) {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
const metadata = {};
|
|
311
|
+
if (intent) metadata.intent = intent;
|
|
312
|
+
if (domain) metadata.domain = domain;
|
|
313
|
+
if (owner) metadata.owner = owner;
|
|
314
|
+
if (tags?.length) metadata.tags = tags;
|
|
315
|
+
if (stateChanges?.length) metadata.stateChanges = stateChanges;
|
|
316
|
+
if (emits?.length) metadata.emits = emits;
|
|
317
|
+
if (calls?.length) metadata.calls = calls;
|
|
318
|
+
return metadata;
|
|
319
|
+
}
|
|
320
|
+
function buildStepErrorPayload(error, errorMeta, origin, attempt, cumulativeDurationMs) {
|
|
321
|
+
const tag = extractErrorTag(error);
|
|
322
|
+
const classification = lookupErrorClassification(tag, errorMeta);
|
|
323
|
+
const diagnostics = { tag, origin };
|
|
324
|
+
if (classification !== void 0) diagnostics.classification = classification;
|
|
325
|
+
if (attempt !== void 0) diagnostics.attempt = attempt;
|
|
326
|
+
if (cumulativeDurationMs !== void 0) diagnostics.cumulativeDurationMs = cumulativeDurationMs;
|
|
327
|
+
return diagnostics;
|
|
328
|
+
}
|
|
329
|
+
var STEP_TIMEOUT_MARKER = /* @__PURE__ */ Symbol.for("step_timeout_marker");
|
|
330
|
+
function isStepTimeoutError(e) {
|
|
331
|
+
if (typeof e !== "object" || e === null) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (e.type === "STEP_TIMEOUT") {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return STEP_TIMEOUT_MARKER in e;
|
|
338
|
+
}
|
|
339
|
+
function getStepTimeoutMeta(e) {
|
|
340
|
+
if (typeof e !== "object" || e === null) {
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
if (e.type === "STEP_TIMEOUT") {
|
|
344
|
+
const err2 = e;
|
|
345
|
+
return {
|
|
346
|
+
timeoutMs: err2.timeoutMs,
|
|
347
|
+
stepName: err2.stepName,
|
|
348
|
+
stepKey: err2.stepKey,
|
|
349
|
+
attempt: err2.attempt
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (STEP_TIMEOUT_MARKER in e) {
|
|
353
|
+
return e[STEP_TIMEOUT_MARKER];
|
|
354
|
+
}
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
var EARLY_EXIT_SYMBOL = /* @__PURE__ */ Symbol("early-exit");
|
|
358
|
+
function createEarlyExit(error, meta) {
|
|
359
|
+
return {
|
|
360
|
+
[EARLY_EXIT_SYMBOL]: true,
|
|
361
|
+
error,
|
|
362
|
+
meta
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function isEarlyExit(e) {
|
|
366
|
+
return typeof e === "object" && e !== null && e[EARLY_EXIT_SYMBOL] === true;
|
|
367
|
+
}
|
|
368
|
+
var MAPPER_EXCEPTION_SYMBOL = /* @__PURE__ */ Symbol("mapper-exception");
|
|
369
|
+
function createMapperException(thrown) {
|
|
370
|
+
return { [MAPPER_EXCEPTION_SYMBOL]: true, thrown };
|
|
371
|
+
}
|
|
372
|
+
function isMapperException(e) {
|
|
373
|
+
return typeof e === "object" && e !== null && e[MAPPER_EXCEPTION_SYMBOL] === true;
|
|
374
|
+
}
|
|
375
|
+
function calculateRetryDelay(attempt, options) {
|
|
376
|
+
const { backoff, initialDelay, maxDelay, jitter } = options;
|
|
377
|
+
let delay;
|
|
378
|
+
switch (backoff) {
|
|
379
|
+
case "fixed":
|
|
380
|
+
delay = initialDelay;
|
|
381
|
+
break;
|
|
382
|
+
case "linear":
|
|
383
|
+
delay = initialDelay * attempt;
|
|
384
|
+
break;
|
|
385
|
+
case "exponential":
|
|
386
|
+
delay = initialDelay * Math.pow(2, attempt - 1);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
delay = Math.min(delay, maxDelay);
|
|
390
|
+
if (jitter) {
|
|
391
|
+
const jitterAmount = delay * 0.25 * Math.random();
|
|
392
|
+
delay = delay + jitterAmount;
|
|
393
|
+
}
|
|
394
|
+
return Math.floor(delay);
|
|
395
|
+
}
|
|
396
|
+
function sleep(ms) {
|
|
397
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
398
|
+
}
|
|
399
|
+
var TIMEOUT_SYMBOL = /* @__PURE__ */ Symbol("timeout");
|
|
400
|
+
var TIMEOUT_OPTION_SYMBOL = /* @__PURE__ */ Symbol("timeout-option");
|
|
401
|
+
function isTimeoutOptionMarker(value) {
|
|
402
|
+
return typeof value === "object" && value !== null && value[TIMEOUT_OPTION_SYMBOL] === true;
|
|
403
|
+
}
|
|
404
|
+
async function executeWithTimeout(operation, options, stepInfo, externalSignal) {
|
|
405
|
+
const controller = new AbortController();
|
|
406
|
+
const behavior = options.onTimeout ?? "error";
|
|
407
|
+
const createTimeoutError = () => {
|
|
408
|
+
if (typeof behavior === "function") {
|
|
409
|
+
return behavior({
|
|
410
|
+
name: stepInfo.name,
|
|
411
|
+
key: stepInfo.key,
|
|
412
|
+
ms: options.ms
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return options.error ?? {
|
|
416
|
+
type: "STEP_TIMEOUT",
|
|
417
|
+
stepName: stepInfo.name,
|
|
418
|
+
stepKey: stepInfo.key,
|
|
419
|
+
timeoutMs: options.ms,
|
|
420
|
+
attempt: stepInfo.attempt
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
let timeoutId;
|
|
424
|
+
if (externalSignal?.aborted) {
|
|
425
|
+
controller.abort(externalSignal.reason);
|
|
426
|
+
}
|
|
427
|
+
let externalAbortHandler;
|
|
428
|
+
if (externalSignal && !externalSignal.aborted) {
|
|
429
|
+
externalAbortHandler = () => controller.abort(externalSignal.reason);
|
|
430
|
+
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
431
|
+
}
|
|
432
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
433
|
+
timeoutId = setTimeout(() => {
|
|
434
|
+
if (behavior !== "disconnect") {
|
|
435
|
+
controller.abort();
|
|
436
|
+
}
|
|
437
|
+
if (behavior === "option") {
|
|
438
|
+
reject({ [TIMEOUT_OPTION_SYMBOL]: true, ms: options.ms });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
reject({ [TIMEOUT_SYMBOL]: true, error: createTimeoutError() });
|
|
442
|
+
}, options.ms);
|
|
443
|
+
});
|
|
444
|
+
let operationPromise;
|
|
445
|
+
if (options.signal) {
|
|
446
|
+
operationPromise = Promise.resolve(
|
|
447
|
+
operation(controller.signal)
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
operationPromise = Promise.resolve(operation());
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const result = await Promise.race([operationPromise, timeoutPromise]);
|
|
454
|
+
return result;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (typeof error === "object" && error !== null && error[TIMEOUT_OPTION_SYMBOL] === true) {
|
|
457
|
+
throw { [TIMEOUT_OPTION_SYMBOL]: true, ms: options.ms };
|
|
458
|
+
}
|
|
459
|
+
if (typeof error === "object" && error !== null && error[TIMEOUT_SYMBOL] === true) {
|
|
460
|
+
if (behavior === "disconnect") {
|
|
461
|
+
operationPromise.catch(() => {
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const errorToThrow = error.error;
|
|
465
|
+
if (typeof errorToThrow === "object" && errorToThrow !== null && errorToThrow.type !== "STEP_TIMEOUT") {
|
|
466
|
+
const meta = {
|
|
467
|
+
timeoutMs: options.ms,
|
|
468
|
+
stepName: stepInfo.name,
|
|
469
|
+
stepKey: stepInfo.key,
|
|
470
|
+
attempt: stepInfo.attempt
|
|
471
|
+
};
|
|
472
|
+
if (STEP_TIMEOUT_MARKER in errorToThrow) {
|
|
473
|
+
errorToThrow[STEP_TIMEOUT_MARKER] = meta;
|
|
474
|
+
} else {
|
|
475
|
+
Object.defineProperty(errorToThrow, STEP_TIMEOUT_MARKER, {
|
|
476
|
+
value: meta,
|
|
477
|
+
enumerable: false,
|
|
478
|
+
writable: true,
|
|
479
|
+
configurable: false
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
throw errorToThrow;
|
|
484
|
+
}
|
|
485
|
+
throw error;
|
|
486
|
+
} finally {
|
|
487
|
+
clearTimeout(timeoutId);
|
|
488
|
+
if (externalAbortHandler && externalSignal) {
|
|
489
|
+
externalSignal.removeEventListener("abort", externalAbortHandler);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
494
|
+
backoff: "exponential",
|
|
495
|
+
initialDelay: 100,
|
|
496
|
+
maxDelay: 3e4,
|
|
497
|
+
jitter: true,
|
|
498
|
+
shouldRetry: () => true,
|
|
499
|
+
onRetry: () => {
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
async function runFn(fnOrDeps, optionsOrFn, maybeOptions) {
|
|
503
|
+
if (typeof fnOrDeps !== "function") {
|
|
504
|
+
const deps = fnOrDeps;
|
|
505
|
+
const boundFn = optionsOrFn;
|
|
506
|
+
if (typeof boundFn !== "function") {
|
|
507
|
+
throw new TypeError(
|
|
508
|
+
"[awaitly] run(deps, fn) requires a callback as the second argument. Example: run({ getUser }, async (s) => s.getUser(id))"
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const classicRun = runFn;
|
|
512
|
+
return classicRun(
|
|
513
|
+
({ step }) => boundFn(bindSteps(deps, step), { step }),
|
|
514
|
+
maybeOptions
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const fn = fnOrDeps;
|
|
518
|
+
const options = optionsOrFn;
|
|
519
|
+
const {
|
|
520
|
+
onError,
|
|
521
|
+
onEvent,
|
|
522
|
+
catchUnexpected,
|
|
523
|
+
workflowId: providedWorkflowId,
|
|
524
|
+
workflowName,
|
|
525
|
+
context,
|
|
526
|
+
graph,
|
|
527
|
+
_workflowSignal
|
|
528
|
+
} = options && typeof options === "object" ? options : {};
|
|
529
|
+
const workflowId = providedWorkflowId ?? crypto.randomUUID();
|
|
530
|
+
const declaredIds = graph ? new Set(
|
|
531
|
+
Array.isArray(graph) ? graph : graph.states.map((state) => state.semanticId ?? state.id)
|
|
532
|
+
) : void 0;
|
|
533
|
+
const declaredPatterns = declaredIds ? [...declaredIds].filter((id) => id.includes("{")).map(
|
|
534
|
+
(id) => new RegExp(
|
|
535
|
+
`^${id.replaceAll(/[.*+?^$()[\]\\|]/g, String.raw`\$&`).replaceAll(/\{[^}]*\}/g, ".+")}$`
|
|
536
|
+
)
|
|
537
|
+
) : void 0;
|
|
538
|
+
const assertDeclared = (id, kind) => {
|
|
539
|
+
if (!declaredIds || declaredIds.has(id)) return;
|
|
540
|
+
if (declaredPatterns?.some((re) => re.test(id))) return;
|
|
541
|
+
throw new Error(
|
|
542
|
+
`[awaitly] ${kind} id "${id}" is not in the declared workflow graph. Declared ids: ${[...declaredIds].join(", ")}. Either add it to the graph or remove the graph option.`
|
|
543
|
+
);
|
|
544
|
+
};
|
|
545
|
+
const effectiveCatchUnexpected = catchUnexpected ?? defaultCatchUnexpected;
|
|
546
|
+
const activeScopeStack = [];
|
|
547
|
+
let stepIdCounter = 0;
|
|
548
|
+
const generateStepId = (stepKey) => {
|
|
549
|
+
return stepKey ?? `step_${++stepIdCounter}`;
|
|
550
|
+
};
|
|
551
|
+
const emitEvent = (event) => {
|
|
552
|
+
const eventWithContext = event.context !== void 0 || context === void 0 ? event : { ...event, context };
|
|
553
|
+
const eventWithName = workflowName !== void 0 && eventWithContext.workflowName === void 0 ? { ...eventWithContext, workflowName } : eventWithContext;
|
|
554
|
+
if (eventWithName.type === "step_success") {
|
|
555
|
+
const stepId = eventWithName.stepId;
|
|
556
|
+
for (let i = activeScopeStack.length - 1; i >= 0; i--) {
|
|
557
|
+
const scope = activeScopeStack[i];
|
|
558
|
+
if (scope.type === "race" && !scope.winnerId) {
|
|
559
|
+
scope.winnerId = stepId;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
onEvent?.(eventWithName, context);
|
|
565
|
+
};
|
|
566
|
+
const earlyExit = createEarlyExit;
|
|
567
|
+
const isEarlyExitE = (e) => isEarlyExit(e);
|
|
568
|
+
const wrapForStep = (error, _meta) => {
|
|
569
|
+
return error;
|
|
570
|
+
};
|
|
571
|
+
const isResultLike = (value) => {
|
|
572
|
+
if (typeof value === "function") return false;
|
|
573
|
+
if (value && typeof value === "object" && "ok" in value) return true;
|
|
574
|
+
if (value && typeof value === "object" && "then" in value && typeof value.then === "function") return true;
|
|
575
|
+
return false;
|
|
576
|
+
};
|
|
577
|
+
try {
|
|
578
|
+
let normalizeParallelOperations2 = function(rawOperations) {
|
|
579
|
+
const out = {};
|
|
580
|
+
for (const [key, value2] of Object.entries(rawOperations)) {
|
|
581
|
+
if (typeof value2 === "function") {
|
|
582
|
+
out[key] = value2;
|
|
583
|
+
} else if (value2 && typeof value2 === "object" && "fn" in value2) {
|
|
584
|
+
out[key] = value2.fn;
|
|
585
|
+
} else {
|
|
586
|
+
throw new TypeError(`step.all: operation "${key}" must be a function or { fn, errors? } object`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return out;
|
|
590
|
+
}, executeParallelArray2 = function(name, operation) {
|
|
591
|
+
const scopeId = `scope_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
592
|
+
return (async () => {
|
|
593
|
+
const startTime = performance.now();
|
|
594
|
+
let scopeEnded = false;
|
|
595
|
+
activeScopeStack.push({ scopeId, type: "parallel" });
|
|
596
|
+
const emitScopeEnd = () => {
|
|
597
|
+
if (scopeEnded) return;
|
|
598
|
+
scopeEnded = true;
|
|
599
|
+
const idx = activeScopeStack.findIndex((s) => s.scopeId === scopeId);
|
|
600
|
+
if (idx !== -1) activeScopeStack.splice(idx, 1);
|
|
601
|
+
emitEvent({
|
|
602
|
+
type: "scope_end",
|
|
603
|
+
workflowId,
|
|
604
|
+
scopeId,
|
|
605
|
+
ts: Date.now(),
|
|
606
|
+
durationMs: performance.now() - startTime
|
|
607
|
+
});
|
|
608
|
+
};
|
|
609
|
+
emitEvent({
|
|
610
|
+
type: "scope_start",
|
|
611
|
+
workflowId,
|
|
612
|
+
scopeId,
|
|
613
|
+
scopeType: "parallel",
|
|
614
|
+
name,
|
|
615
|
+
ts: Date.now()
|
|
616
|
+
});
|
|
617
|
+
try {
|
|
618
|
+
const result = await operation();
|
|
619
|
+
emitScopeEnd();
|
|
620
|
+
if (!result.ok) {
|
|
621
|
+
onError?.(result.error, name, context);
|
|
622
|
+
throw earlyExit(result.error, {
|
|
623
|
+
origin: "result",
|
|
624
|
+
resultCause: result.cause
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return result.value;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
emitScopeEnd();
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
})();
|
|
633
|
+
}, executeParallelNamed2 = function(operations, options2) {
|
|
634
|
+
const keys = Object.keys(operations);
|
|
635
|
+
const name = options2.name ?? `Parallel(${keys.join(", ")})`;
|
|
636
|
+
const scopeId = `scope_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
637
|
+
return (async () => {
|
|
638
|
+
const startTime = performance.now();
|
|
639
|
+
let scopeEnded = false;
|
|
640
|
+
activeScopeStack.push({ scopeId, type: "parallel" });
|
|
641
|
+
const emitScopeEnd = () => {
|
|
642
|
+
if (scopeEnded) return;
|
|
643
|
+
scopeEnded = true;
|
|
644
|
+
const idx = activeScopeStack.findIndex((s) => s.scopeId === scopeId);
|
|
645
|
+
if (idx !== -1) activeScopeStack.splice(idx, 1);
|
|
646
|
+
emitEvent({
|
|
647
|
+
type: "scope_end",
|
|
648
|
+
workflowId,
|
|
649
|
+
scopeId,
|
|
650
|
+
ts: Date.now(),
|
|
651
|
+
durationMs: performance.now() - startTime
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
emitEvent({
|
|
655
|
+
type: "scope_start",
|
|
656
|
+
workflowId,
|
|
657
|
+
scopeId,
|
|
658
|
+
scopeType: "parallel",
|
|
659
|
+
name,
|
|
660
|
+
ts: Date.now()
|
|
661
|
+
});
|
|
662
|
+
try {
|
|
663
|
+
const results = await new Promise((resolve) => {
|
|
664
|
+
if (keys.length === 0) {
|
|
665
|
+
resolve([]);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
let settled = false;
|
|
669
|
+
let pendingCount = keys.length;
|
|
670
|
+
const resultArray = new Array(keys.length);
|
|
671
|
+
for (let i = 0; i < keys.length; i++) {
|
|
672
|
+
const key = keys[i];
|
|
673
|
+
const index = i;
|
|
674
|
+
Promise.resolve(operations[key]()).catch((reason) => err(
|
|
675
|
+
{ type: "PROMISE_REJECTED", cause: reason },
|
|
676
|
+
{ cause: { type: "PROMISE_REJECTION", reason } }
|
|
677
|
+
)).then((result) => {
|
|
678
|
+
if (settled) return;
|
|
679
|
+
if (!result.ok) {
|
|
680
|
+
settled = true;
|
|
681
|
+
resolve([{ key, result }]);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
resultArray[index] = { key, result };
|
|
685
|
+
pendingCount--;
|
|
686
|
+
if (pendingCount === 0) {
|
|
687
|
+
resolve(resultArray);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
emitScopeEnd();
|
|
693
|
+
const output = {};
|
|
694
|
+
for (const { key, result } of results) {
|
|
695
|
+
if (!result.ok) {
|
|
696
|
+
onError?.(result.error, key, context);
|
|
697
|
+
throw earlyExit(result.error, {
|
|
698
|
+
origin: "result",
|
|
699
|
+
resultCause: result.cause
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
output[key] = result.value;
|
|
703
|
+
}
|
|
704
|
+
return output;
|
|
705
|
+
} catch (error) {
|
|
706
|
+
emitScopeEnd();
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
710
|
+
};
|
|
711
|
+
var normalizeParallelOperations = normalizeParallelOperations2, executeParallelArray = executeParallelArray2, executeParallelNamed = executeParallelNamed2;
|
|
712
|
+
const stepFn = (id, operationOrResult, stepOptions) => {
|
|
713
|
+
return (async () => {
|
|
714
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
'[awaitly] step() requires an explicit string ID as the first argument. Example: step("fetchUser", () => fetchUser(id))'
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
assertDeclared(id, "step");
|
|
720
|
+
const parsedOptions = stepOptions ?? {};
|
|
721
|
+
const stepMetadata = extractStepMetadata(parsedOptions);
|
|
722
|
+
const stepName = id;
|
|
723
|
+
const stepKey = parsedOptions.key ?? id;
|
|
724
|
+
const explicitKey = parsedOptions.key ?? id;
|
|
725
|
+
const { description: stepDescription, retry: retryConfig, timeout: timeoutConfig } = parsedOptions;
|
|
726
|
+
const stepId = generateStepId(stepKey);
|
|
727
|
+
const hasEventListeners = onEvent;
|
|
728
|
+
const overallStartTime = hasEventListeners ? performance.now() : 0;
|
|
729
|
+
const isDirectResult = isResultLike(operationOrResult);
|
|
730
|
+
const operation = isDirectResult ? () => operationOrResult : operationOrResult;
|
|
731
|
+
const maxAttempts = Math.max(1, retryConfig?.attempts ?? 1);
|
|
732
|
+
const effectiveRetry = {
|
|
733
|
+
attempts: maxAttempts,
|
|
734
|
+
backoff: retryConfig?.backoff ?? DEFAULT_RETRY_CONFIG.backoff,
|
|
735
|
+
initialDelay: retryConfig?.initialDelay ?? DEFAULT_RETRY_CONFIG.initialDelay,
|
|
736
|
+
maxDelay: retryConfig?.maxDelay ?? DEFAULT_RETRY_CONFIG.maxDelay,
|
|
737
|
+
jitter: retryConfig?.jitter ?? DEFAULT_RETRY_CONFIG.jitter,
|
|
738
|
+
shouldRetry: retryConfig?.shouldRetry ?? DEFAULT_RETRY_CONFIG.shouldRetry,
|
|
739
|
+
onRetry: retryConfig?.onRetry ?? DEFAULT_RETRY_CONFIG.onRetry
|
|
740
|
+
};
|
|
741
|
+
if (onEvent) {
|
|
742
|
+
emitEvent({
|
|
743
|
+
type: "step_start",
|
|
744
|
+
workflowId,
|
|
745
|
+
stepId,
|
|
746
|
+
stepKey,
|
|
747
|
+
name: stepName,
|
|
748
|
+
description: stepDescription,
|
|
749
|
+
ts: Date.now(),
|
|
750
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
let lastResult;
|
|
754
|
+
for (let attempt = 1; attempt <= effectiveRetry.attempts; attempt++) {
|
|
755
|
+
const attemptStartTime = hasEventListeners ? performance.now() : 0;
|
|
756
|
+
try {
|
|
757
|
+
let result;
|
|
758
|
+
if (timeoutConfig) {
|
|
759
|
+
result = await executeWithTimeout(
|
|
760
|
+
operation,
|
|
761
|
+
timeoutConfig,
|
|
762
|
+
{ name: stepName, key: stepKey, attempt },
|
|
763
|
+
_workflowSignal
|
|
764
|
+
);
|
|
765
|
+
} else {
|
|
766
|
+
result = await operation();
|
|
767
|
+
}
|
|
768
|
+
if (result.ok) {
|
|
769
|
+
const durationMs = performance.now() - overallStartTime;
|
|
770
|
+
emitEvent({
|
|
771
|
+
type: "step_success",
|
|
772
|
+
workflowId,
|
|
773
|
+
stepId,
|
|
774
|
+
stepKey,
|
|
775
|
+
name: stepName,
|
|
776
|
+
description: stepDescription,
|
|
777
|
+
ts: Date.now(),
|
|
778
|
+
durationMs,
|
|
779
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
780
|
+
});
|
|
781
|
+
if (explicitKey) {
|
|
782
|
+
emitEvent({
|
|
783
|
+
type: "step_complete",
|
|
784
|
+
workflowId,
|
|
785
|
+
stepKey: explicitKey,
|
|
786
|
+
name: stepName,
|
|
787
|
+
description: stepDescription,
|
|
788
|
+
ts: Date.now(),
|
|
789
|
+
durationMs,
|
|
790
|
+
result,
|
|
791
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return result.value;
|
|
795
|
+
}
|
|
796
|
+
lastResult = result;
|
|
797
|
+
if (attempt < effectiveRetry.attempts && effectiveRetry.shouldRetry(result.error, attempt)) {
|
|
798
|
+
const delay = calculateRetryDelay(attempt, effectiveRetry);
|
|
799
|
+
emitEvent({
|
|
800
|
+
type: "step_retry",
|
|
801
|
+
workflowId,
|
|
802
|
+
stepId,
|
|
803
|
+
stepKey,
|
|
804
|
+
name: stepName,
|
|
805
|
+
ts: Date.now(),
|
|
806
|
+
attempt: attempt + 1,
|
|
807
|
+
maxAttempts: effectiveRetry.attempts,
|
|
808
|
+
delayMs: delay,
|
|
809
|
+
error: result.error,
|
|
810
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
811
|
+
diagnostics: buildStepErrorPayload(result.error, parsedOptions.errorMeta, "result", attempt, performance.now() - overallStartTime)
|
|
812
|
+
});
|
|
813
|
+
effectiveRetry.onRetry(result.error, attempt, delay);
|
|
814
|
+
await sleep(delay);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (effectiveRetry.attempts > 1) {
|
|
818
|
+
emitEvent({
|
|
819
|
+
type: "step_retries_exhausted",
|
|
820
|
+
workflowId,
|
|
821
|
+
stepId,
|
|
822
|
+
stepKey,
|
|
823
|
+
name: stepName,
|
|
824
|
+
ts: Date.now(),
|
|
825
|
+
durationMs: performance.now() - overallStartTime,
|
|
826
|
+
attempts: attempt,
|
|
827
|
+
lastError: result.error,
|
|
828
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
829
|
+
diagnostics: buildStepErrorPayload(result.error, parsedOptions.errorMeta, "result", attempt, performance.now() - overallStartTime)
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
break;
|
|
833
|
+
} catch (thrown) {
|
|
834
|
+
const durationMs = performance.now() - attemptStartTime;
|
|
835
|
+
if (isTimeoutOptionMarker(thrown)) {
|
|
836
|
+
const timeoutMs = thrown.ms;
|
|
837
|
+
emitEvent({
|
|
838
|
+
type: "step_timeout",
|
|
839
|
+
workflowId,
|
|
840
|
+
stepId,
|
|
841
|
+
stepKey,
|
|
842
|
+
name: stepName,
|
|
843
|
+
ts: Date.now(),
|
|
844
|
+
timeoutMs,
|
|
845
|
+
attempt,
|
|
846
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
847
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "timeout", attempt)
|
|
848
|
+
});
|
|
849
|
+
emitEvent({
|
|
850
|
+
type: "step_success",
|
|
851
|
+
workflowId,
|
|
852
|
+
stepId,
|
|
853
|
+
stepKey,
|
|
854
|
+
name: stepName,
|
|
855
|
+
description: stepDescription,
|
|
856
|
+
ts: Date.now(),
|
|
857
|
+
durationMs: performance.now() - overallStartTime,
|
|
858
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
859
|
+
});
|
|
860
|
+
if (explicitKey) {
|
|
861
|
+
emitEvent({
|
|
862
|
+
type: "step_complete",
|
|
863
|
+
workflowId,
|
|
864
|
+
stepKey: explicitKey,
|
|
865
|
+
name: stepName,
|
|
866
|
+
description: stepDescription,
|
|
867
|
+
ts: Date.now(),
|
|
868
|
+
durationMs: performance.now() - overallStartTime,
|
|
869
|
+
result: ok2(void 0),
|
|
870
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
return void 0;
|
|
874
|
+
}
|
|
875
|
+
if (isEarlyExitE(thrown)) {
|
|
876
|
+
emitEvent({
|
|
877
|
+
type: "step_aborted",
|
|
878
|
+
workflowId,
|
|
879
|
+
stepId,
|
|
880
|
+
stepKey,
|
|
881
|
+
name: stepName,
|
|
882
|
+
description: stepDescription,
|
|
883
|
+
ts: Date.now(),
|
|
884
|
+
durationMs,
|
|
885
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
886
|
+
});
|
|
887
|
+
throw thrown;
|
|
888
|
+
}
|
|
889
|
+
if (isStepTimeoutError(thrown)) {
|
|
890
|
+
const timeoutMeta = getStepTimeoutMeta(thrown);
|
|
891
|
+
const timeoutMs = timeoutConfig?.ms ?? timeoutMeta?.timeoutMs ?? 0;
|
|
892
|
+
emitEvent({
|
|
893
|
+
type: "step_timeout",
|
|
894
|
+
workflowId,
|
|
895
|
+
stepId,
|
|
896
|
+
stepKey,
|
|
897
|
+
name: stepName,
|
|
898
|
+
ts: Date.now(),
|
|
899
|
+
timeoutMs,
|
|
900
|
+
attempt,
|
|
901
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
902
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "timeout", attempt)
|
|
903
|
+
});
|
|
904
|
+
if (attempt < effectiveRetry.attempts && effectiveRetry.shouldRetry(thrown, attempt)) {
|
|
905
|
+
const delay = calculateRetryDelay(attempt, effectiveRetry);
|
|
906
|
+
emitEvent({
|
|
907
|
+
type: "step_retry",
|
|
908
|
+
workflowId,
|
|
909
|
+
stepId,
|
|
910
|
+
stepKey,
|
|
911
|
+
name: stepName,
|
|
912
|
+
ts: Date.now(),
|
|
913
|
+
attempt: attempt + 1,
|
|
914
|
+
maxAttempts: effectiveRetry.attempts,
|
|
915
|
+
delayMs: delay,
|
|
916
|
+
error: thrown,
|
|
917
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
918
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "timeout", attempt, performance.now() - overallStartTime)
|
|
919
|
+
});
|
|
920
|
+
effectiveRetry.onRetry(thrown, attempt, delay);
|
|
921
|
+
await sleep(delay);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (effectiveRetry.attempts > 1) {
|
|
925
|
+
emitEvent({
|
|
926
|
+
type: "step_retries_exhausted",
|
|
927
|
+
workflowId,
|
|
928
|
+
stepId,
|
|
929
|
+
stepKey,
|
|
930
|
+
name: stepName,
|
|
931
|
+
ts: Date.now(),
|
|
932
|
+
durationMs: performance.now() - overallStartTime,
|
|
933
|
+
attempts: attempt,
|
|
934
|
+
lastError: thrown,
|
|
935
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
936
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "timeout", attempt, performance.now() - overallStartTime)
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const totalDurationMs3 = performance.now() - overallStartTime;
|
|
940
|
+
emitEvent({
|
|
941
|
+
type: "step_error",
|
|
942
|
+
workflowId,
|
|
943
|
+
stepId,
|
|
944
|
+
stepKey,
|
|
945
|
+
name: stepName,
|
|
946
|
+
description: stepDescription,
|
|
947
|
+
ts: Date.now(),
|
|
948
|
+
durationMs: totalDurationMs3,
|
|
949
|
+
error: thrown,
|
|
950
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
951
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "timeout", attempt, totalDurationMs3)
|
|
952
|
+
});
|
|
953
|
+
if (explicitKey) {
|
|
954
|
+
emitEvent({
|
|
955
|
+
type: "step_complete",
|
|
956
|
+
workflowId,
|
|
957
|
+
stepKey: explicitKey,
|
|
958
|
+
name: stepName,
|
|
959
|
+
description: stepDescription,
|
|
960
|
+
ts: Date.now(),
|
|
961
|
+
durationMs: totalDurationMs3,
|
|
962
|
+
result: err(thrown, { cause: thrown }),
|
|
963
|
+
meta: { origin: "throw", thrown },
|
|
964
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
onError?.(thrown, stepName, context);
|
|
968
|
+
throw earlyExit(thrown, { origin: "throw", thrown });
|
|
969
|
+
}
|
|
970
|
+
if (attempt < effectiveRetry.attempts && effectiveRetry.shouldRetry(thrown, attempt)) {
|
|
971
|
+
const delay = calculateRetryDelay(attempt, effectiveRetry);
|
|
972
|
+
emitEvent({
|
|
973
|
+
type: "step_retry",
|
|
974
|
+
workflowId,
|
|
975
|
+
stepId,
|
|
976
|
+
stepKey,
|
|
977
|
+
name: stepName,
|
|
978
|
+
ts: Date.now(),
|
|
979
|
+
attempt: attempt + 1,
|
|
980
|
+
maxAttempts: effectiveRetry.attempts,
|
|
981
|
+
delayMs: delay,
|
|
982
|
+
error: thrown,
|
|
983
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
984
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "throw", attempt, performance.now() - overallStartTime)
|
|
985
|
+
});
|
|
986
|
+
effectiveRetry.onRetry(thrown, attempt, delay);
|
|
987
|
+
await sleep(delay);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (effectiveRetry.attempts > 1 && !isStepTimeoutError(thrown)) {
|
|
991
|
+
emitEvent({
|
|
992
|
+
type: "step_retries_exhausted",
|
|
993
|
+
workflowId,
|
|
994
|
+
stepId,
|
|
995
|
+
stepKey,
|
|
996
|
+
name: stepName,
|
|
997
|
+
ts: Date.now(),
|
|
998
|
+
durationMs: performance.now() - overallStartTime,
|
|
999
|
+
attempts: attempt,
|
|
1000
|
+
lastError: thrown,
|
|
1001
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
1002
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "throw", attempt, performance.now() - overallStartTime)
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const totalDurationMs2 = performance.now() - overallStartTime;
|
|
1006
|
+
let mappedError;
|
|
1007
|
+
try {
|
|
1008
|
+
mappedError = effectiveCatchUnexpected(thrown);
|
|
1009
|
+
} catch (mapperError) {
|
|
1010
|
+
throw createMapperException(mapperError);
|
|
1011
|
+
}
|
|
1012
|
+
emitEvent({
|
|
1013
|
+
type: "step_error",
|
|
1014
|
+
workflowId,
|
|
1015
|
+
stepId,
|
|
1016
|
+
stepKey,
|
|
1017
|
+
name: stepName,
|
|
1018
|
+
description: stepDescription,
|
|
1019
|
+
ts: Date.now(),
|
|
1020
|
+
durationMs: totalDurationMs2,
|
|
1021
|
+
error: mappedError,
|
|
1022
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
1023
|
+
diagnostics: buildStepErrorPayload(thrown, parsedOptions.errorMeta, "throw", attempt, totalDurationMs2)
|
|
1024
|
+
});
|
|
1025
|
+
if (explicitKey) {
|
|
1026
|
+
emitEvent({
|
|
1027
|
+
type: "step_complete",
|
|
1028
|
+
workflowId,
|
|
1029
|
+
stepKey: explicitKey,
|
|
1030
|
+
name: stepName,
|
|
1031
|
+
description: stepDescription,
|
|
1032
|
+
ts: Date.now(),
|
|
1033
|
+
durationMs: totalDurationMs2,
|
|
1034
|
+
result: err(mappedError, { cause: thrown }),
|
|
1035
|
+
meta: { origin: "throw", thrown },
|
|
1036
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
onError?.(mappedError, stepName, context);
|
|
1040
|
+
throw earlyExit(mappedError, { origin: "throw", thrown });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const errorResult = lastResult;
|
|
1044
|
+
const totalDurationMs = performance.now() - overallStartTime;
|
|
1045
|
+
const wrappedError = wrapForStep(errorResult.error, {
|
|
1046
|
+
origin: "result",
|
|
1047
|
+
resultCause: errorResult.cause
|
|
1048
|
+
});
|
|
1049
|
+
emitEvent({
|
|
1050
|
+
type: "step_error",
|
|
1051
|
+
workflowId,
|
|
1052
|
+
stepId,
|
|
1053
|
+
stepKey,
|
|
1054
|
+
name: stepName,
|
|
1055
|
+
description: stepDescription,
|
|
1056
|
+
ts: Date.now(),
|
|
1057
|
+
durationMs: totalDurationMs,
|
|
1058
|
+
error: wrappedError,
|
|
1059
|
+
...stepMetadata && { metadata: stepMetadata },
|
|
1060
|
+
diagnostics: buildStepErrorPayload(errorResult.error, parsedOptions.errorMeta, "result", effectiveRetry.attempts, totalDurationMs)
|
|
1061
|
+
});
|
|
1062
|
+
if (explicitKey) {
|
|
1063
|
+
emitEvent({
|
|
1064
|
+
type: "step_complete",
|
|
1065
|
+
workflowId,
|
|
1066
|
+
stepKey: explicitKey,
|
|
1067
|
+
name: stepName,
|
|
1068
|
+
description: stepDescription,
|
|
1069
|
+
ts: Date.now(),
|
|
1070
|
+
durationMs: totalDurationMs,
|
|
1071
|
+
result: errorResult,
|
|
1072
|
+
meta: { origin: "result", resultCause: errorResult.cause },
|
|
1073
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
onError?.(wrappedError, stepName, context);
|
|
1077
|
+
throw earlyExit(wrappedError, {
|
|
1078
|
+
origin: "result",
|
|
1079
|
+
resultCause: errorResult.cause
|
|
1080
|
+
});
|
|
1081
|
+
})();
|
|
1082
|
+
};
|
|
1083
|
+
stepFn.try = (id, operation, opts) => {
|
|
1084
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1085
|
+
throw new Error(
|
|
1086
|
+
'[awaitly] step.try() requires an explicit string ID as the first argument. Example: step.try("parse", () => JSON.parse(str), { error: "PARSE_ERROR" })'
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
assertDeclared(id, "step");
|
|
1090
|
+
const mapToError = "error" in opts ? () => opts.error : opts.onError;
|
|
1091
|
+
if (opts.retry || opts.timeout) {
|
|
1092
|
+
return stepFn.retry(
|
|
1093
|
+
id,
|
|
1094
|
+
async () => {
|
|
1095
|
+
try {
|
|
1096
|
+
return ok2(await operation());
|
|
1097
|
+
} catch (cause) {
|
|
1098
|
+
return err(mapToError(cause), { cause });
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
attempts: opts.retry?.attempts ?? 1,
|
|
1103
|
+
...opts.retry ?? {},
|
|
1104
|
+
key: opts.key,
|
|
1105
|
+
timeout: opts.timeout
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
const stepKey = opts.key ?? id;
|
|
1110
|
+
const stepName = id;
|
|
1111
|
+
const stepId = id;
|
|
1112
|
+
const hasEventListeners = onEvent;
|
|
1113
|
+
return (async () => {
|
|
1114
|
+
const startTime = hasEventListeners ? performance.now() : 0;
|
|
1115
|
+
if (onEvent) {
|
|
1116
|
+
emitEvent({
|
|
1117
|
+
type: "step_start",
|
|
1118
|
+
workflowId,
|
|
1119
|
+
stepId,
|
|
1120
|
+
stepKey,
|
|
1121
|
+
name: stepName,
|
|
1122
|
+
ts: Date.now()
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const value2 = await operation();
|
|
1127
|
+
const durationMs = performance.now() - startTime;
|
|
1128
|
+
emitEvent({
|
|
1129
|
+
type: "step_success",
|
|
1130
|
+
workflowId,
|
|
1131
|
+
stepId,
|
|
1132
|
+
stepKey,
|
|
1133
|
+
name: stepName,
|
|
1134
|
+
ts: Date.now(),
|
|
1135
|
+
durationMs
|
|
1136
|
+
});
|
|
1137
|
+
if (stepKey) {
|
|
1138
|
+
emitEvent({
|
|
1139
|
+
type: "step_complete",
|
|
1140
|
+
workflowId,
|
|
1141
|
+
stepKey,
|
|
1142
|
+
name: stepName,
|
|
1143
|
+
ts: Date.now(),
|
|
1144
|
+
durationMs,
|
|
1145
|
+
result: ok2(value2)
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return value2;
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
const mapped = mapToError(error);
|
|
1151
|
+
const durationMs = performance.now() - startTime;
|
|
1152
|
+
const wrappedError = wrapForStep(mapped, { origin: "throw", thrown: error });
|
|
1153
|
+
emitEvent({
|
|
1154
|
+
type: "step_error",
|
|
1155
|
+
workflowId,
|
|
1156
|
+
stepId,
|
|
1157
|
+
stepKey,
|
|
1158
|
+
name: stepName,
|
|
1159
|
+
ts: Date.now(),
|
|
1160
|
+
durationMs,
|
|
1161
|
+
error: wrappedError
|
|
1162
|
+
});
|
|
1163
|
+
if (stepKey) {
|
|
1164
|
+
emitEvent({
|
|
1165
|
+
type: "step_complete",
|
|
1166
|
+
workflowId,
|
|
1167
|
+
stepKey,
|
|
1168
|
+
name: stepName,
|
|
1169
|
+
ts: Date.now(),
|
|
1170
|
+
durationMs,
|
|
1171
|
+
result: err(mapped, { cause: error }),
|
|
1172
|
+
meta: { origin: "throw", thrown: error }
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
onError?.(wrappedError, stepName, context);
|
|
1176
|
+
throw earlyExit(wrappedError, { origin: "throw", thrown: error });
|
|
1177
|
+
}
|
|
1178
|
+
})();
|
|
1179
|
+
};
|
|
1180
|
+
stepFn.fromResult = (id, operation, opts) => {
|
|
1181
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
'[awaitly] step.fromResult() requires an explicit string ID as the first argument. Example: step.fromResult("callProvider", () => callProvider(input), { onError: (e) => ({ type: "FAILED" }) })'
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
assertDeclared(id, "step");
|
|
1187
|
+
const stepKey = opts.key ?? id;
|
|
1188
|
+
const stepName = id;
|
|
1189
|
+
const stepId = id;
|
|
1190
|
+
const mapToError = "error" in opts ? () => opts.error : opts.onError;
|
|
1191
|
+
const hasEventListeners = onEvent;
|
|
1192
|
+
return (async () => {
|
|
1193
|
+
const startTime = hasEventListeners ? performance.now() : 0;
|
|
1194
|
+
if (onEvent) {
|
|
1195
|
+
emitEvent({
|
|
1196
|
+
type: "step_start",
|
|
1197
|
+
workflowId,
|
|
1198
|
+
stepId,
|
|
1199
|
+
stepKey,
|
|
1200
|
+
name: stepName,
|
|
1201
|
+
ts: Date.now()
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
const result = await operation();
|
|
1205
|
+
if (result.ok) {
|
|
1206
|
+
const durationMs = performance.now() - startTime;
|
|
1207
|
+
emitEvent({
|
|
1208
|
+
type: "step_success",
|
|
1209
|
+
workflowId,
|
|
1210
|
+
stepId,
|
|
1211
|
+
stepKey,
|
|
1212
|
+
name: stepName,
|
|
1213
|
+
ts: Date.now(),
|
|
1214
|
+
durationMs
|
|
1215
|
+
});
|
|
1216
|
+
if (stepKey) {
|
|
1217
|
+
emitEvent({
|
|
1218
|
+
type: "step_complete",
|
|
1219
|
+
workflowId,
|
|
1220
|
+
stepKey,
|
|
1221
|
+
name: stepName,
|
|
1222
|
+
ts: Date.now(),
|
|
1223
|
+
durationMs,
|
|
1224
|
+
result: ok2(result.value)
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
return result.value;
|
|
1228
|
+
} else {
|
|
1229
|
+
const mapped = mapToError(result.error);
|
|
1230
|
+
const durationMs = performance.now() - startTime;
|
|
1231
|
+
const wrappedError = wrapForStep(mapped, {
|
|
1232
|
+
origin: "result",
|
|
1233
|
+
resultCause: result.error
|
|
1234
|
+
});
|
|
1235
|
+
emitEvent({
|
|
1236
|
+
type: "step_error",
|
|
1237
|
+
workflowId,
|
|
1238
|
+
stepId,
|
|
1239
|
+
stepKey,
|
|
1240
|
+
name: stepName,
|
|
1241
|
+
ts: Date.now(),
|
|
1242
|
+
durationMs,
|
|
1243
|
+
error: wrappedError
|
|
1244
|
+
});
|
|
1245
|
+
if (stepKey) {
|
|
1246
|
+
emitEvent({
|
|
1247
|
+
type: "step_complete",
|
|
1248
|
+
workflowId,
|
|
1249
|
+
stepKey,
|
|
1250
|
+
name: stepName,
|
|
1251
|
+
ts: Date.now(),
|
|
1252
|
+
durationMs,
|
|
1253
|
+
result: err(mapped, { cause: result.error }),
|
|
1254
|
+
meta: { origin: "result", resultCause: result.error }
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
onError?.(wrappedError, stepName, context);
|
|
1258
|
+
throw earlyExit(wrappedError, {
|
|
1259
|
+
origin: "result",
|
|
1260
|
+
resultCause: result.error
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
})();
|
|
1264
|
+
};
|
|
1265
|
+
stepFn.fromNullable = (id, operation, onNull, options2) => {
|
|
1266
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1267
|
+
throw new Error(
|
|
1268
|
+
'[awaitly] step.fromNullable() requires an explicit string ID as the first argument. Example: step.fromNullable("getUser", () => db.find(id), () => ({ type: "NOT_FOUND" }))'
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
return stepFn(
|
|
1272
|
+
id,
|
|
1273
|
+
async () => {
|
|
1274
|
+
const value2 = await operation();
|
|
1275
|
+
return value2 != null ? ok2(value2) : err(onNull());
|
|
1276
|
+
},
|
|
1277
|
+
options2
|
|
1278
|
+
);
|
|
1279
|
+
};
|
|
1280
|
+
stepFn.retry = (id, operation, options2) => {
|
|
1281
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
'[awaitly] step.retry() requires an explicit string ID as the first argument. Example: step.retry("fetchData", () => fetchData(), { attempts: 3 })'
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
return stepFn(id, operation, {
|
|
1287
|
+
key: options2.key ?? id,
|
|
1288
|
+
retry: {
|
|
1289
|
+
attempts: options2.attempts,
|
|
1290
|
+
backoff: options2.backoff,
|
|
1291
|
+
initialDelay: options2.initialDelay,
|
|
1292
|
+
maxDelay: options2.maxDelay,
|
|
1293
|
+
jitter: options2.jitter,
|
|
1294
|
+
shouldRetry: options2.shouldRetry,
|
|
1295
|
+
onRetry: options2.onRetry
|
|
1296
|
+
},
|
|
1297
|
+
timeout: options2.timeout
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
stepFn.withTimeout = (id, operation, options2) => {
|
|
1301
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
'[awaitly] step.withTimeout() requires an explicit string ID as the first argument. Example: step.withTimeout("slowOp", () => slowOp(), { ms: 5000 })'
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return stepFn(
|
|
1307
|
+
id,
|
|
1308
|
+
operation,
|
|
1309
|
+
{
|
|
1310
|
+
key: options2.key ?? id,
|
|
1311
|
+
timeout: options2
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1314
|
+
};
|
|
1315
|
+
stepFn.sleep = (id, duration, options2) => {
|
|
1316
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
'[awaitly] step.sleep() requires an explicit string ID as the first argument. Example: step.sleep("delay", "5s")'
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const d = typeof duration === "string" ? parseDurationString(duration) : duration;
|
|
1322
|
+
if (!d) {
|
|
1323
|
+
throw new Error(`step.sleep: invalid duration '${duration}'`);
|
|
1324
|
+
}
|
|
1325
|
+
const ms = d.millis;
|
|
1326
|
+
const userSignal = options2?.signal;
|
|
1327
|
+
return stepFn(
|
|
1328
|
+
id,
|
|
1329
|
+
async () => {
|
|
1330
|
+
if (_workflowSignal?.aborted || userSignal?.aborted) {
|
|
1331
|
+
const e = new Error("Sleep aborted");
|
|
1332
|
+
e.name = "AbortError";
|
|
1333
|
+
throw e;
|
|
1334
|
+
}
|
|
1335
|
+
return new Promise((resolve, reject) => {
|
|
1336
|
+
const state = { timeoutId: void 0 };
|
|
1337
|
+
const onAbort = () => {
|
|
1338
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
1339
|
+
const e = new Error("Sleep aborted");
|
|
1340
|
+
e.name = "AbortError";
|
|
1341
|
+
reject(e);
|
|
1342
|
+
};
|
|
1343
|
+
_workflowSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1344
|
+
userSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1345
|
+
state.timeoutId = setTimeout(() => {
|
|
1346
|
+
_workflowSignal?.removeEventListener("abort", onAbort);
|
|
1347
|
+
userSignal?.removeEventListener("abort", onAbort);
|
|
1348
|
+
resolve(ok2(void 0));
|
|
1349
|
+
}, ms);
|
|
1350
|
+
});
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
key: options2?.key ?? id,
|
|
1354
|
+
description: options2?.description
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1357
|
+
};
|
|
1358
|
+
stepFn.all = ((...args) => {
|
|
1359
|
+
if (typeof args[0] !== "string") {
|
|
1360
|
+
throw new TypeError(
|
|
1361
|
+
"step.all(name, ...): first argument must be a string (step name). Example: step.all('Fetch data', { user: () => fetchUser(), posts: () => fetchPosts() })"
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
const name = args[0];
|
|
1365
|
+
const second = args[1];
|
|
1366
|
+
if (typeof second === "function") {
|
|
1367
|
+
return executeParallelArray2(name, second);
|
|
1368
|
+
}
|
|
1369
|
+
if (second && typeof second === "object" && !Array.isArray(second)) {
|
|
1370
|
+
const rawOperations = second;
|
|
1371
|
+
const normalizedOperations = normalizeParallelOperations2(rawOperations);
|
|
1372
|
+
return executeParallelNamed2(normalizedOperations, { name });
|
|
1373
|
+
}
|
|
1374
|
+
throw new TypeError(
|
|
1375
|
+
"step.all(name, ...): second argument must be a function (array form) or an object of operations (object form)."
|
|
1376
|
+
);
|
|
1377
|
+
});
|
|
1378
|
+
stepFn.race = (name, operation) => {
|
|
1379
|
+
const scopeId = `scope_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1380
|
+
return (async () => {
|
|
1381
|
+
const startTime = performance.now();
|
|
1382
|
+
let scopeEnded = false;
|
|
1383
|
+
const scopeEntry = { scopeId, type: "race", winnerId: void 0 };
|
|
1384
|
+
activeScopeStack.push(scopeEntry);
|
|
1385
|
+
const emitScopeEnd = () => {
|
|
1386
|
+
if (scopeEnded) return;
|
|
1387
|
+
scopeEnded = true;
|
|
1388
|
+
const idx = activeScopeStack.findIndex((s) => s.scopeId === scopeId);
|
|
1389
|
+
if (idx !== -1) activeScopeStack.splice(idx, 1);
|
|
1390
|
+
emitEvent({
|
|
1391
|
+
type: "scope_end",
|
|
1392
|
+
workflowId,
|
|
1393
|
+
scopeId,
|
|
1394
|
+
ts: Date.now(),
|
|
1395
|
+
durationMs: performance.now() - startTime,
|
|
1396
|
+
winnerId: scopeEntry.winnerId
|
|
1397
|
+
});
|
|
1398
|
+
};
|
|
1399
|
+
emitEvent({
|
|
1400
|
+
type: "scope_start",
|
|
1401
|
+
workflowId,
|
|
1402
|
+
scopeId,
|
|
1403
|
+
scopeType: "race",
|
|
1404
|
+
name,
|
|
1405
|
+
ts: Date.now()
|
|
1406
|
+
});
|
|
1407
|
+
try {
|
|
1408
|
+
const result = await operation();
|
|
1409
|
+
emitScopeEnd();
|
|
1410
|
+
if (!result.ok) {
|
|
1411
|
+
onError?.(result.error, name, context);
|
|
1412
|
+
throw earlyExit(result.error, {
|
|
1413
|
+
origin: "result",
|
|
1414
|
+
resultCause: result.cause
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
return result.value;
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
emitScopeEnd();
|
|
1420
|
+
throw error;
|
|
1421
|
+
}
|
|
1422
|
+
})();
|
|
1423
|
+
};
|
|
1424
|
+
stepFn.if = (id, conditionLabel, condition) => {
|
|
1425
|
+
assertDeclared(id, "decision");
|
|
1426
|
+
const value2 = condition();
|
|
1427
|
+
emitEvent({
|
|
1428
|
+
type: "decision",
|
|
1429
|
+
workflowId,
|
|
1430
|
+
decisionId: id,
|
|
1431
|
+
label: conditionLabel,
|
|
1432
|
+
branch: value2 ? "then" : "else",
|
|
1433
|
+
value: value2,
|
|
1434
|
+
ts: Date.now()
|
|
1435
|
+
});
|
|
1436
|
+
return value2;
|
|
1437
|
+
};
|
|
1438
|
+
stepFn.label = stepFn.if;
|
|
1439
|
+
stepFn.branch = async (id, options2) => {
|
|
1440
|
+
const { condition, then: thenFn, else: elseFn } = options2;
|
|
1441
|
+
assertDeclared(id, "decision");
|
|
1442
|
+
const conditionResult = condition();
|
|
1443
|
+
const branch = conditionResult ? "then" : "else";
|
|
1444
|
+
const startTime = performance.now();
|
|
1445
|
+
emitEvent({
|
|
1446
|
+
type: "decision",
|
|
1447
|
+
workflowId,
|
|
1448
|
+
decisionId: id,
|
|
1449
|
+
label: options2.conditionLabel,
|
|
1450
|
+
branch,
|
|
1451
|
+
value: conditionResult,
|
|
1452
|
+
phase: "start",
|
|
1453
|
+
ts: Date.now()
|
|
1454
|
+
});
|
|
1455
|
+
const emitEnd = () => {
|
|
1456
|
+
emitEvent({
|
|
1457
|
+
type: "decision",
|
|
1458
|
+
workflowId,
|
|
1459
|
+
decisionId: id,
|
|
1460
|
+
label: options2.conditionLabel,
|
|
1461
|
+
branch,
|
|
1462
|
+
value: conditionResult,
|
|
1463
|
+
phase: "end",
|
|
1464
|
+
durationMs: performance.now() - startTime,
|
|
1465
|
+
ts: Date.now()
|
|
1466
|
+
});
|
|
1467
|
+
};
|
|
1468
|
+
try {
|
|
1469
|
+
if (conditionResult) {
|
|
1470
|
+
return await thenFn();
|
|
1471
|
+
} else if (elseFn) {
|
|
1472
|
+
return await elseFn();
|
|
1473
|
+
}
|
|
1474
|
+
return void 0;
|
|
1475
|
+
} finally {
|
|
1476
|
+
emitEnd();
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
stepFn.arm = (fn2, errors) => {
|
|
1480
|
+
return { fn: fn2, errors };
|
|
1481
|
+
};
|
|
1482
|
+
stepFn.forEach = async (_id, items, options2) => {
|
|
1483
|
+
const results = [];
|
|
1484
|
+
const maxIterations = options2.maxIterations;
|
|
1485
|
+
let index = 0;
|
|
1486
|
+
const isRunForm = "run" in options2;
|
|
1487
|
+
const asyncItems = Symbol.asyncIterator in items ? items : (async function* () {
|
|
1488
|
+
yield* items;
|
|
1489
|
+
})();
|
|
1490
|
+
for await (const item of asyncItems) {
|
|
1491
|
+
if (maxIterations !== void 0 && index >= maxIterations) {
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
let result;
|
|
1495
|
+
if (isRunForm) {
|
|
1496
|
+
const runOptions = options2;
|
|
1497
|
+
result = await runOptions.run(item, index);
|
|
1498
|
+
} else {
|
|
1499
|
+
const itemOptions = options2;
|
|
1500
|
+
result = await itemOptions.item.handler(item, index, stepFn);
|
|
1501
|
+
}
|
|
1502
|
+
results.push(result);
|
|
1503
|
+
index++;
|
|
1504
|
+
}
|
|
1505
|
+
return results;
|
|
1506
|
+
};
|
|
1507
|
+
stepFn.item = (handler) => {
|
|
1508
|
+
return {
|
|
1509
|
+
__forEachItemHandler: true,
|
|
1510
|
+
handler
|
|
1511
|
+
};
|
|
1512
|
+
};
|
|
1513
|
+
stepFn.dep = (_name, fn2) => {
|
|
1514
|
+
return fn2;
|
|
1515
|
+
};
|
|
1516
|
+
stepFn.workflow = (id, getter, options2) => {
|
|
1517
|
+
return stepFn(id, getter, options2);
|
|
1518
|
+
};
|
|
1519
|
+
stepFn.map = async (id, items, mapper, options2) => {
|
|
1520
|
+
const concurrency = options2?.concurrency ?? items.length;
|
|
1521
|
+
return stepFn(
|
|
1522
|
+
id,
|
|
1523
|
+
() => {
|
|
1524
|
+
if (concurrency >= items.length) {
|
|
1525
|
+
return allAsync(items.map((item, index) => mapper(item, index)));
|
|
1526
|
+
} else {
|
|
1527
|
+
return (async () => {
|
|
1528
|
+
const results = [];
|
|
1529
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
1530
|
+
const batch = items.slice(i, i + concurrency);
|
|
1531
|
+
const batchResult = await allAsync(
|
|
1532
|
+
batch.map((item, batchIndex) => mapper(item, i + batchIndex))
|
|
1533
|
+
);
|
|
1534
|
+
if (!batchResult.ok) {
|
|
1535
|
+
return batchResult;
|
|
1536
|
+
}
|
|
1537
|
+
results.push(...batchResult.value);
|
|
1538
|
+
}
|
|
1539
|
+
return ok2(results);
|
|
1540
|
+
})();
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
{ key: options2?.key }
|
|
1544
|
+
);
|
|
1545
|
+
};
|
|
1546
|
+
stepFn.withFallback = (id, operation, options2) => {
|
|
1547
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1548
|
+
throw new Error(
|
|
1549
|
+
'[awaitly] step.withFallback() requires an explicit string ID as the first argument. Example: step.withFallback("getUser", () => fetchUser(id), { fallback: () => fetchFromCache(id) })'
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
assertDeclared(id, "step");
|
|
1553
|
+
const stepKey = options2.key ?? id;
|
|
1554
|
+
const stepName = id;
|
|
1555
|
+
const stepId = generateStepId(stepKey);
|
|
1556
|
+
const hasEventListeners = onEvent;
|
|
1557
|
+
return (async () => {
|
|
1558
|
+
const startTime = hasEventListeners ? performance.now() : 0;
|
|
1559
|
+
if (onEvent) {
|
|
1560
|
+
emitEvent({
|
|
1561
|
+
type: "step_start",
|
|
1562
|
+
workflowId,
|
|
1563
|
+
stepId,
|
|
1564
|
+
stepKey,
|
|
1565
|
+
name: stepName,
|
|
1566
|
+
ts: Date.now()
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
let primaryResult;
|
|
1570
|
+
try {
|
|
1571
|
+
primaryResult = await operation();
|
|
1572
|
+
} catch (thrown) {
|
|
1573
|
+
if (isEarlyExitE(thrown)) {
|
|
1574
|
+
emitEvent({
|
|
1575
|
+
type: "step_aborted",
|
|
1576
|
+
workflowId,
|
|
1577
|
+
stepId,
|
|
1578
|
+
stepKey,
|
|
1579
|
+
name: stepName,
|
|
1580
|
+
ts: Date.now(),
|
|
1581
|
+
durationMs: performance.now() - startTime
|
|
1582
|
+
});
|
|
1583
|
+
throw thrown;
|
|
1584
|
+
}
|
|
1585
|
+
let mappedError;
|
|
1586
|
+
try {
|
|
1587
|
+
mappedError = effectiveCatchUnexpected(thrown);
|
|
1588
|
+
} catch (mapperError) {
|
|
1589
|
+
throw createMapperException(mapperError);
|
|
1590
|
+
}
|
|
1591
|
+
if (options2.on !== void 0 && options2.on !== mappedError) {
|
|
1592
|
+
const durationMs2 = performance.now() - startTime;
|
|
1593
|
+
emitEvent({
|
|
1594
|
+
type: "step_error",
|
|
1595
|
+
workflowId,
|
|
1596
|
+
stepId,
|
|
1597
|
+
stepKey,
|
|
1598
|
+
name: stepName,
|
|
1599
|
+
ts: Date.now(),
|
|
1600
|
+
durationMs: durationMs2,
|
|
1601
|
+
error: mappedError
|
|
1602
|
+
});
|
|
1603
|
+
if (stepKey) {
|
|
1604
|
+
emitEvent({
|
|
1605
|
+
type: "step_complete",
|
|
1606
|
+
workflowId,
|
|
1607
|
+
stepKey,
|
|
1608
|
+
name: stepName,
|
|
1609
|
+
ts: Date.now(),
|
|
1610
|
+
durationMs: durationMs2,
|
|
1611
|
+
result: err(mappedError, { cause: thrown }),
|
|
1612
|
+
meta: { origin: "throw", thrown }
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
onError?.(mappedError, stepName, context);
|
|
1616
|
+
throw earlyExit(mappedError, { origin: "throw", thrown });
|
|
1617
|
+
}
|
|
1618
|
+
let fallbackResultFromThrow;
|
|
1619
|
+
try {
|
|
1620
|
+
fallbackResultFromThrow = await options2.fallback();
|
|
1621
|
+
} catch (fallbackThrown) {
|
|
1622
|
+
if (isEarlyExitE(fallbackThrown)) {
|
|
1623
|
+
emitEvent({
|
|
1624
|
+
type: "step_aborted",
|
|
1625
|
+
workflowId,
|
|
1626
|
+
stepId,
|
|
1627
|
+
stepKey,
|
|
1628
|
+
name: stepName,
|
|
1629
|
+
ts: Date.now(),
|
|
1630
|
+
durationMs: performance.now() - startTime
|
|
1631
|
+
});
|
|
1632
|
+
throw fallbackThrown;
|
|
1633
|
+
}
|
|
1634
|
+
let fallbackMappedError;
|
|
1635
|
+
try {
|
|
1636
|
+
fallbackMappedError = effectiveCatchUnexpected(fallbackThrown);
|
|
1637
|
+
} catch (mapperError) {
|
|
1638
|
+
throw createMapperException(mapperError);
|
|
1639
|
+
}
|
|
1640
|
+
const durationMs2 = performance.now() - startTime;
|
|
1641
|
+
emitEvent({
|
|
1642
|
+
type: "step_error",
|
|
1643
|
+
workflowId,
|
|
1644
|
+
stepId,
|
|
1645
|
+
stepKey,
|
|
1646
|
+
name: stepName,
|
|
1647
|
+
ts: Date.now(),
|
|
1648
|
+
durationMs: durationMs2,
|
|
1649
|
+
error: fallbackMappedError
|
|
1650
|
+
});
|
|
1651
|
+
if (stepKey) {
|
|
1652
|
+
emitEvent({
|
|
1653
|
+
type: "step_complete",
|
|
1654
|
+
workflowId,
|
|
1655
|
+
stepKey,
|
|
1656
|
+
name: stepName,
|
|
1657
|
+
ts: Date.now(),
|
|
1658
|
+
durationMs: durationMs2,
|
|
1659
|
+
result: err(fallbackMappedError, { cause: fallbackThrown }),
|
|
1660
|
+
meta: { origin: "throw", thrown: fallbackThrown }
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
onError?.(fallbackMappedError, stepName, context);
|
|
1664
|
+
throw earlyExit(fallbackMappedError, { origin: "throw", thrown: fallbackThrown });
|
|
1665
|
+
}
|
|
1666
|
+
if (fallbackResultFromThrow.ok) {
|
|
1667
|
+
const durationMs2 = performance.now() - startTime;
|
|
1668
|
+
emitEvent({
|
|
1669
|
+
type: "step_success",
|
|
1670
|
+
workflowId,
|
|
1671
|
+
stepId,
|
|
1672
|
+
stepKey,
|
|
1673
|
+
name: stepName,
|
|
1674
|
+
ts: Date.now(),
|
|
1675
|
+
durationMs: durationMs2
|
|
1676
|
+
});
|
|
1677
|
+
if (stepKey) {
|
|
1678
|
+
emitEvent({
|
|
1679
|
+
type: "step_complete",
|
|
1680
|
+
workflowId,
|
|
1681
|
+
stepKey,
|
|
1682
|
+
name: stepName,
|
|
1683
|
+
ts: Date.now(),
|
|
1684
|
+
durationMs: durationMs2,
|
|
1685
|
+
result: fallbackResultFromThrow,
|
|
1686
|
+
meta: { origin: "fallback", fallbackUsed: true, fallbackReason: String(mappedError) }
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
return fallbackResultFromThrow.value;
|
|
1690
|
+
} else {
|
|
1691
|
+
const durationMs2 = performance.now() - startTime;
|
|
1692
|
+
const wrappedError2 = wrapForStep(fallbackResultFromThrow.error, {
|
|
1693
|
+
origin: "result",
|
|
1694
|
+
resultCause: fallbackResultFromThrow.cause
|
|
1695
|
+
});
|
|
1696
|
+
emitEvent({
|
|
1697
|
+
type: "step_error",
|
|
1698
|
+
workflowId,
|
|
1699
|
+
stepId,
|
|
1700
|
+
stepKey,
|
|
1701
|
+
name: stepName,
|
|
1702
|
+
ts: Date.now(),
|
|
1703
|
+
durationMs: durationMs2,
|
|
1704
|
+
error: wrappedError2
|
|
1705
|
+
});
|
|
1706
|
+
if (stepKey) {
|
|
1707
|
+
emitEvent({
|
|
1708
|
+
type: "step_complete",
|
|
1709
|
+
workflowId,
|
|
1710
|
+
stepKey,
|
|
1711
|
+
name: stepName,
|
|
1712
|
+
ts: Date.now(),
|
|
1713
|
+
durationMs: durationMs2,
|
|
1714
|
+
result: fallbackResultFromThrow,
|
|
1715
|
+
meta: { origin: "result", resultCause: fallbackResultFromThrow.cause }
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
onError?.(wrappedError2, stepName, context);
|
|
1719
|
+
throw earlyExit(wrappedError2, {
|
|
1720
|
+
origin: "result",
|
|
1721
|
+
resultCause: fallbackResultFromThrow.cause
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (primaryResult.ok) {
|
|
1726
|
+
const durationMs2 = performance.now() - startTime;
|
|
1727
|
+
emitEvent({
|
|
1728
|
+
type: "step_success",
|
|
1729
|
+
workflowId,
|
|
1730
|
+
stepId,
|
|
1731
|
+
stepKey,
|
|
1732
|
+
name: stepName,
|
|
1733
|
+
ts: Date.now(),
|
|
1734
|
+
durationMs: durationMs2
|
|
1735
|
+
});
|
|
1736
|
+
if (stepKey) {
|
|
1737
|
+
emitEvent({
|
|
1738
|
+
type: "step_complete",
|
|
1739
|
+
workflowId,
|
|
1740
|
+
stepKey,
|
|
1741
|
+
name: stepName,
|
|
1742
|
+
ts: Date.now(),
|
|
1743
|
+
durationMs: durationMs2,
|
|
1744
|
+
result: primaryResult
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return primaryResult.value;
|
|
1748
|
+
}
|
|
1749
|
+
const primaryError = primaryResult.error;
|
|
1750
|
+
if (options2.on !== void 0 && options2.on !== primaryError) {
|
|
1751
|
+
const durationMs2 = performance.now() - startTime;
|
|
1752
|
+
const wrappedError2 = wrapForStep(primaryError, {
|
|
1753
|
+
origin: "result",
|
|
1754
|
+
resultCause: primaryResult.cause
|
|
1755
|
+
});
|
|
1756
|
+
emitEvent({
|
|
1757
|
+
type: "step_error",
|
|
1758
|
+
workflowId,
|
|
1759
|
+
stepId,
|
|
1760
|
+
stepKey,
|
|
1761
|
+
name: stepName,
|
|
1762
|
+
ts: Date.now(),
|
|
1763
|
+
durationMs: durationMs2,
|
|
1764
|
+
error: wrappedError2
|
|
1765
|
+
});
|
|
1766
|
+
if (stepKey) {
|
|
1767
|
+
emitEvent({
|
|
1768
|
+
type: "step_complete",
|
|
1769
|
+
workflowId,
|
|
1770
|
+
stepKey,
|
|
1771
|
+
name: stepName,
|
|
1772
|
+
ts: Date.now(),
|
|
1773
|
+
durationMs: durationMs2,
|
|
1774
|
+
result: primaryResult,
|
|
1775
|
+
meta: { origin: "result", resultCause: primaryResult.cause }
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
onError?.(wrappedError2, stepName, context);
|
|
1779
|
+
throw earlyExit(wrappedError2, {
|
|
1780
|
+
origin: "result",
|
|
1781
|
+
resultCause: primaryResult.cause
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
let fallbackResult;
|
|
1785
|
+
try {
|
|
1786
|
+
fallbackResult = await options2.fallback();
|
|
1787
|
+
} catch (thrown) {
|
|
1788
|
+
if (isEarlyExitE(thrown)) {
|
|
1789
|
+
emitEvent({
|
|
1790
|
+
type: "step_aborted",
|
|
1791
|
+
workflowId,
|
|
1792
|
+
stepId,
|
|
1793
|
+
stepKey,
|
|
1794
|
+
name: stepName,
|
|
1795
|
+
ts: Date.now(),
|
|
1796
|
+
durationMs: performance.now() - startTime
|
|
1797
|
+
});
|
|
1798
|
+
throw thrown;
|
|
1799
|
+
}
|
|
1800
|
+
let mappedError;
|
|
1801
|
+
try {
|
|
1802
|
+
mappedError = effectiveCatchUnexpected(thrown);
|
|
1803
|
+
} catch (mapperError) {
|
|
1804
|
+
throw createMapperException(mapperError);
|
|
1805
|
+
}
|
|
1806
|
+
const durationMs2 = performance.now() - startTime;
|
|
1807
|
+
emitEvent({
|
|
1808
|
+
type: "step_error",
|
|
1809
|
+
workflowId,
|
|
1810
|
+
stepId,
|
|
1811
|
+
stepKey,
|
|
1812
|
+
name: stepName,
|
|
1813
|
+
ts: Date.now(),
|
|
1814
|
+
durationMs: durationMs2,
|
|
1815
|
+
error: mappedError
|
|
1816
|
+
});
|
|
1817
|
+
if (stepKey) {
|
|
1818
|
+
emitEvent({
|
|
1819
|
+
type: "step_complete",
|
|
1820
|
+
workflowId,
|
|
1821
|
+
stepKey,
|
|
1822
|
+
name: stepName,
|
|
1823
|
+
ts: Date.now(),
|
|
1824
|
+
durationMs: durationMs2,
|
|
1825
|
+
result: err(mappedError, { cause: thrown }),
|
|
1826
|
+
meta: { origin: "throw", thrown }
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
onError?.(mappedError, stepName, context);
|
|
1830
|
+
throw earlyExit(mappedError, { origin: "throw", thrown });
|
|
1831
|
+
}
|
|
1832
|
+
if (fallbackResult.ok) {
|
|
1833
|
+
const durationMs2 = performance.now() - startTime;
|
|
1834
|
+
emitEvent({
|
|
1835
|
+
type: "step_success",
|
|
1836
|
+
workflowId,
|
|
1837
|
+
stepId,
|
|
1838
|
+
stepKey,
|
|
1839
|
+
name: stepName,
|
|
1840
|
+
ts: Date.now(),
|
|
1841
|
+
durationMs: durationMs2
|
|
1842
|
+
});
|
|
1843
|
+
if (stepKey) {
|
|
1844
|
+
emitEvent({
|
|
1845
|
+
type: "step_complete",
|
|
1846
|
+
workflowId,
|
|
1847
|
+
stepKey,
|
|
1848
|
+
name: stepName,
|
|
1849
|
+
ts: Date.now(),
|
|
1850
|
+
durationMs: durationMs2,
|
|
1851
|
+
result: fallbackResult,
|
|
1852
|
+
meta: { origin: "fallback", fallbackUsed: true, fallbackReason: String(primaryError) }
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return fallbackResult.value;
|
|
1856
|
+
}
|
|
1857
|
+
const durationMs = performance.now() - startTime;
|
|
1858
|
+
const wrappedError = wrapForStep(fallbackResult.error, {
|
|
1859
|
+
origin: "result",
|
|
1860
|
+
resultCause: fallbackResult.cause
|
|
1861
|
+
});
|
|
1862
|
+
emitEvent({
|
|
1863
|
+
type: "step_error",
|
|
1864
|
+
workflowId,
|
|
1865
|
+
stepId,
|
|
1866
|
+
stepKey,
|
|
1867
|
+
name: stepName,
|
|
1868
|
+
ts: Date.now(),
|
|
1869
|
+
durationMs,
|
|
1870
|
+
error: wrappedError
|
|
1871
|
+
});
|
|
1872
|
+
if (stepKey) {
|
|
1873
|
+
emitEvent({
|
|
1874
|
+
type: "step_complete",
|
|
1875
|
+
workflowId,
|
|
1876
|
+
stepKey,
|
|
1877
|
+
name: stepName,
|
|
1878
|
+
ts: Date.now(),
|
|
1879
|
+
durationMs,
|
|
1880
|
+
result: fallbackResult,
|
|
1881
|
+
meta: { origin: "result", resultCause: fallbackResult.cause }
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
onError?.(wrappedError, stepName, context);
|
|
1885
|
+
throw earlyExit(wrappedError, {
|
|
1886
|
+
origin: "result",
|
|
1887
|
+
resultCause: fallbackResult.cause
|
|
1888
|
+
});
|
|
1889
|
+
})();
|
|
1890
|
+
};
|
|
1891
|
+
stepFn.withResource = (id, options2) => {
|
|
1892
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1893
|
+
throw new Error(
|
|
1894
|
+
'[awaitly] step.withResource() requires an explicit string ID as the first argument. Example: step.withResource("useDb", { acquire: () => connect(), use: (db) => query(db), release: (db) => db.close() })'
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
assertDeclared(id, "step");
|
|
1898
|
+
const stepKey = id;
|
|
1899
|
+
const stepName = id;
|
|
1900
|
+
const stepId = generateStepId(stepKey);
|
|
1901
|
+
const hasEventListeners = onEvent;
|
|
1902
|
+
return (async () => {
|
|
1903
|
+
const startTime = hasEventListeners ? performance.now() : 0;
|
|
1904
|
+
if (onEvent) {
|
|
1905
|
+
emitEvent({
|
|
1906
|
+
type: "step_start",
|
|
1907
|
+
workflowId,
|
|
1908
|
+
stepId,
|
|
1909
|
+
stepKey,
|
|
1910
|
+
name: stepName,
|
|
1911
|
+
ts: Date.now()
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
let acquireResult;
|
|
1915
|
+
try {
|
|
1916
|
+
acquireResult = await options2.acquire();
|
|
1917
|
+
} catch (thrown) {
|
|
1918
|
+
if (isEarlyExitE(thrown)) {
|
|
1919
|
+
emitEvent({
|
|
1920
|
+
type: "step_aborted",
|
|
1921
|
+
workflowId,
|
|
1922
|
+
stepId,
|
|
1923
|
+
stepKey,
|
|
1924
|
+
name: stepName,
|
|
1925
|
+
ts: Date.now(),
|
|
1926
|
+
durationMs: performance.now() - startTime
|
|
1927
|
+
});
|
|
1928
|
+
throw thrown;
|
|
1929
|
+
}
|
|
1930
|
+
let mappedError;
|
|
1931
|
+
try {
|
|
1932
|
+
mappedError = effectiveCatchUnexpected(thrown);
|
|
1933
|
+
} catch (mapperError) {
|
|
1934
|
+
throw createMapperException(mapperError);
|
|
1935
|
+
}
|
|
1936
|
+
const durationMs2 = performance.now() - startTime;
|
|
1937
|
+
emitEvent({
|
|
1938
|
+
type: "step_error",
|
|
1939
|
+
workflowId,
|
|
1940
|
+
stepId,
|
|
1941
|
+
stepKey,
|
|
1942
|
+
name: stepName,
|
|
1943
|
+
ts: Date.now(),
|
|
1944
|
+
durationMs: durationMs2,
|
|
1945
|
+
error: mappedError
|
|
1946
|
+
});
|
|
1947
|
+
if (stepKey) {
|
|
1948
|
+
emitEvent({
|
|
1949
|
+
type: "step_complete",
|
|
1950
|
+
workflowId,
|
|
1951
|
+
stepKey,
|
|
1952
|
+
name: stepName,
|
|
1953
|
+
ts: Date.now(),
|
|
1954
|
+
durationMs: durationMs2,
|
|
1955
|
+
result: err(mappedError, { cause: thrown }),
|
|
1956
|
+
meta: { origin: "throw", thrown }
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
onError?.(mappedError, stepName, context);
|
|
1960
|
+
throw earlyExit(mappedError, { origin: "throw", thrown });
|
|
1961
|
+
}
|
|
1962
|
+
if (!acquireResult.ok) {
|
|
1963
|
+
const durationMs2 = performance.now() - startTime;
|
|
1964
|
+
const wrappedError2 = wrapForStep(acquireResult.error, {
|
|
1965
|
+
origin: "result",
|
|
1966
|
+
resultCause: acquireResult.cause
|
|
1967
|
+
});
|
|
1968
|
+
emitEvent({
|
|
1969
|
+
type: "step_error",
|
|
1970
|
+
workflowId,
|
|
1971
|
+
stepId,
|
|
1972
|
+
stepKey,
|
|
1973
|
+
name: stepName,
|
|
1974
|
+
ts: Date.now(),
|
|
1975
|
+
durationMs: durationMs2,
|
|
1976
|
+
error: wrappedError2
|
|
1977
|
+
});
|
|
1978
|
+
if (stepKey) {
|
|
1979
|
+
emitEvent({
|
|
1980
|
+
type: "step_complete",
|
|
1981
|
+
workflowId,
|
|
1982
|
+
stepKey,
|
|
1983
|
+
name: stepName,
|
|
1984
|
+
ts: Date.now(),
|
|
1985
|
+
durationMs: durationMs2,
|
|
1986
|
+
result: acquireResult,
|
|
1987
|
+
meta: { origin: "result", resultCause: acquireResult.cause }
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
onError?.(wrappedError2, stepName, context);
|
|
1991
|
+
throw earlyExit(wrappedError2, {
|
|
1992
|
+
origin: "result",
|
|
1993
|
+
resultCause: acquireResult.cause
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
const resource = acquireResult.value;
|
|
1997
|
+
let useResult;
|
|
1998
|
+
let useThrown;
|
|
1999
|
+
let useThrewNonResult = false;
|
|
2000
|
+
try {
|
|
2001
|
+
useResult = await options2.use(resource);
|
|
2002
|
+
} catch (thrown) {
|
|
2003
|
+
if (isEarlyExitE(thrown)) {
|
|
2004
|
+
try {
|
|
2005
|
+
await options2.release(resource);
|
|
2006
|
+
} catch (releaseErr) {
|
|
2007
|
+
console.warn(
|
|
2008
|
+
`[awaitly] step.withResource("${id}"): release threw after earlyExit:`,
|
|
2009
|
+
releaseErr
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
throw thrown;
|
|
2013
|
+
}
|
|
2014
|
+
useThrown = thrown;
|
|
2015
|
+
useThrewNonResult = true;
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
await options2.release(resource);
|
|
2019
|
+
} catch (releaseErr) {
|
|
2020
|
+
console.warn(
|
|
2021
|
+
`[awaitly] step.withResource("${id}"): release threw:`,
|
|
2022
|
+
releaseErr
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
if (useThrewNonResult) {
|
|
2026
|
+
let mappedError;
|
|
2027
|
+
try {
|
|
2028
|
+
mappedError = effectiveCatchUnexpected(useThrown);
|
|
2029
|
+
} catch (mapperError) {
|
|
2030
|
+
throw createMapperException(mapperError);
|
|
2031
|
+
}
|
|
2032
|
+
const durationMs2 = performance.now() - startTime;
|
|
2033
|
+
emitEvent({
|
|
2034
|
+
type: "step_error",
|
|
2035
|
+
workflowId,
|
|
2036
|
+
stepId,
|
|
2037
|
+
stepKey,
|
|
2038
|
+
name: stepName,
|
|
2039
|
+
ts: Date.now(),
|
|
2040
|
+
durationMs: durationMs2,
|
|
2041
|
+
error: mappedError
|
|
2042
|
+
});
|
|
2043
|
+
if (stepKey) {
|
|
2044
|
+
emitEvent({
|
|
2045
|
+
type: "step_complete",
|
|
2046
|
+
workflowId,
|
|
2047
|
+
stepKey,
|
|
2048
|
+
name: stepName,
|
|
2049
|
+
ts: Date.now(),
|
|
2050
|
+
durationMs: durationMs2,
|
|
2051
|
+
result: err(mappedError, { cause: useThrown }),
|
|
2052
|
+
meta: { origin: "throw", thrown: useThrown }
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
onError?.(mappedError, stepName, context);
|
|
2056
|
+
throw earlyExit(mappedError, { origin: "throw", thrown: useThrown });
|
|
2057
|
+
}
|
|
2058
|
+
const result = useResult;
|
|
2059
|
+
if (result.ok) {
|
|
2060
|
+
const durationMs2 = performance.now() - startTime;
|
|
2061
|
+
emitEvent({
|
|
2062
|
+
type: "step_success",
|
|
2063
|
+
workflowId,
|
|
2064
|
+
stepId,
|
|
2065
|
+
stepKey,
|
|
2066
|
+
name: stepName,
|
|
2067
|
+
ts: Date.now(),
|
|
2068
|
+
durationMs: durationMs2
|
|
2069
|
+
});
|
|
2070
|
+
if (stepKey) {
|
|
2071
|
+
emitEvent({
|
|
2072
|
+
type: "step_complete",
|
|
2073
|
+
workflowId,
|
|
2074
|
+
stepKey,
|
|
2075
|
+
name: stepName,
|
|
2076
|
+
ts: Date.now(),
|
|
2077
|
+
durationMs: durationMs2,
|
|
2078
|
+
result
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
return result.value;
|
|
2082
|
+
}
|
|
2083
|
+
const durationMs = performance.now() - startTime;
|
|
2084
|
+
const wrappedError = wrapForStep(result.error, {
|
|
2085
|
+
origin: "result",
|
|
2086
|
+
resultCause: result.cause
|
|
2087
|
+
});
|
|
2088
|
+
emitEvent({
|
|
2089
|
+
type: "step_error",
|
|
2090
|
+
workflowId,
|
|
2091
|
+
stepId,
|
|
2092
|
+
stepKey,
|
|
2093
|
+
name: stepName,
|
|
2094
|
+
ts: Date.now(),
|
|
2095
|
+
durationMs,
|
|
2096
|
+
error: wrappedError
|
|
2097
|
+
});
|
|
2098
|
+
if (stepKey) {
|
|
2099
|
+
emitEvent({
|
|
2100
|
+
type: "step_complete",
|
|
2101
|
+
workflowId,
|
|
2102
|
+
stepKey,
|
|
2103
|
+
name: stepName,
|
|
2104
|
+
ts: Date.now(),
|
|
2105
|
+
durationMs,
|
|
2106
|
+
result,
|
|
2107
|
+
meta: { origin: "result", resultCause: result.cause }
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
onError?.(wrappedError, stepName, context);
|
|
2111
|
+
throw earlyExit(wrappedError, {
|
|
2112
|
+
origin: "result",
|
|
2113
|
+
resultCause: result.cause
|
|
2114
|
+
});
|
|
2115
|
+
})();
|
|
2116
|
+
};
|
|
2117
|
+
const step = stepFn;
|
|
2118
|
+
const value = await fn({ step });
|
|
2119
|
+
if (process.env.NODE_ENV !== "production" && value !== null && typeof value === "object" && "ok" in value && typeof value.ok === "boolean") {
|
|
2120
|
+
const maybeResult = value;
|
|
2121
|
+
if (maybeResult.ok === true && "value" in maybeResult || maybeResult.ok === false && "error" in maybeResult) {
|
|
2122
|
+
console.warn(
|
|
2123
|
+
`awaitly: Workflow executor returned a Result-like object. Return raw values, not ok() or err().
|
|
2124
|
+
|
|
2125
|
+
Incorrect: return ok({ data });
|
|
2126
|
+
Correct: return { data };
|
|
2127
|
+
|
|
2128
|
+
See: https://jagreehal.github.io/awaitly/guides/troubleshooting/#returning-ok-from-workflow-executor-double-wrapping`
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return ok2(value);
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
if (isMapperException(error)) {
|
|
2135
|
+
throw error.thrown;
|
|
2136
|
+
}
|
|
2137
|
+
if (isEarlyExitE(error)) {
|
|
2138
|
+
const originalCause = error.meta.origin === "throw" ? error.meta.thrown : error.meta.origin === "result" ? error.meta.resultCause : void 0;
|
|
2139
|
+
return err(error.error, { cause: originalCause });
|
|
2140
|
+
}
|
|
2141
|
+
const mapped = effectiveCatchUnexpected(error);
|
|
2142
|
+
onError?.(mapped, "unexpected", context);
|
|
2143
|
+
return err(mapped, { cause: error });
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
var runStrict = (fn, options) => {
|
|
2147
|
+
return runFn(fn, options);
|
|
2148
|
+
};
|
|
2149
|
+
var run = /* @__PURE__ */ Object.assign(runFn, { strict: runStrict });
|
|
2150
|
+
async function allAsync(results) {
|
|
2151
|
+
if (results.length === 0) {
|
|
2152
|
+
return ok2([]);
|
|
2153
|
+
}
|
|
2154
|
+
return new Promise((resolve) => {
|
|
2155
|
+
let settled = false;
|
|
2156
|
+
let pendingCount = results.length;
|
|
2157
|
+
const values = new Array(results.length);
|
|
2158
|
+
for (let i = 0; i < results.length; i++) {
|
|
2159
|
+
const index = i;
|
|
2160
|
+
Promise.resolve(results[index]).catch((reason) => err(
|
|
2161
|
+
{ type: "PROMISE_REJECTED", cause: reason },
|
|
2162
|
+
{ cause: { type: "PROMISE_REJECTION", reason } }
|
|
2163
|
+
)).then((result) => {
|
|
2164
|
+
if (settled) return;
|
|
2165
|
+
if (!result.ok) {
|
|
2166
|
+
settled = true;
|
|
2167
|
+
resolve(result);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
values[index] = result.value;
|
|
2171
|
+
pendingCount--;
|
|
2172
|
+
if (pendingCount === 0) {
|
|
2173
|
+
resolve(ok2(values));
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/workflow/guards.ts
|
|
2181
|
+
function isStepComplete(event) {
|
|
2182
|
+
return event.type === "step_complete";
|
|
2183
|
+
}
|
|
2184
|
+
function isWorkflowCancelled(error) {
|
|
2185
|
+
return typeof error === "object" && error !== null && error.type === "WORKFLOW_CANCELLED";
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// src/persistence.ts
|
|
2189
|
+
var SnapshotFormatError = class extends Error {
|
|
2190
|
+
constructor(message, errors = []) {
|
|
2191
|
+
super(message);
|
|
2192
|
+
this.errors = errors;
|
|
2193
|
+
this.name = "SnapshotFormatError";
|
|
2194
|
+
}
|
|
2195
|
+
errors;
|
|
2196
|
+
};
|
|
2197
|
+
var SnapshotDecodeError = class extends Error {
|
|
2198
|
+
constructor(message, stepId, originalError) {
|
|
2199
|
+
super(message);
|
|
2200
|
+
this.stepId = stepId;
|
|
2201
|
+
this.originalError = originalError;
|
|
2202
|
+
this.name = "SnapshotDecodeError";
|
|
2203
|
+
}
|
|
2204
|
+
stepId;
|
|
2205
|
+
originalError;
|
|
2206
|
+
};
|
|
2207
|
+
function validateSnapshot(obj) {
|
|
2208
|
+
const errors = [];
|
|
2209
|
+
if (typeof obj !== "object" || obj === null) {
|
|
2210
|
+
return { valid: false, errors: ["Snapshot must be an object"] };
|
|
2211
|
+
}
|
|
2212
|
+
const snapshot = obj;
|
|
2213
|
+
if (!("formatVersion" in snapshot)) {
|
|
2214
|
+
errors.push("Missing required field: formatVersion");
|
|
2215
|
+
} else if (snapshot.formatVersion !== 1) {
|
|
2216
|
+
errors.push(`Invalid formatVersion: expected 1, got ${snapshot.formatVersion}`);
|
|
2217
|
+
}
|
|
2218
|
+
if (!("steps" in snapshot)) {
|
|
2219
|
+
errors.push("Missing required field: steps");
|
|
2220
|
+
} else if (typeof snapshot.steps !== "object" || snapshot.steps === null) {
|
|
2221
|
+
errors.push("steps must be an object");
|
|
2222
|
+
} else {
|
|
2223
|
+
const steps = snapshot.steps;
|
|
2224
|
+
for (const [stepId, stepResult] of Object.entries(steps)) {
|
|
2225
|
+
if (typeof stepResult !== "object" || stepResult === null) {
|
|
2226
|
+
errors.push(`steps["${stepId}"] must be an object`);
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
const step = stepResult;
|
|
2230
|
+
if (!("ok" in step)) {
|
|
2231
|
+
errors.push(`steps["${stepId}"] missing required field: ok`);
|
|
2232
|
+
} else if (typeof step.ok !== "boolean") {
|
|
2233
|
+
errors.push(`steps["${stepId}"].ok must be a boolean`);
|
|
2234
|
+
} else if (step.ok === false) {
|
|
2235
|
+
if (!("error" in step)) {
|
|
2236
|
+
errors.push(`steps["${stepId}"] is error result but missing error field`);
|
|
2237
|
+
}
|
|
2238
|
+
if (!("cause" in step)) {
|
|
2239
|
+
errors.push(`steps["${stepId}"] is error result but missing cause field`);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (!("execution" in snapshot)) {
|
|
2245
|
+
errors.push("Missing required field: execution");
|
|
2246
|
+
} else if (typeof snapshot.execution !== "object" || snapshot.execution === null) {
|
|
2247
|
+
errors.push("execution must be an object");
|
|
2248
|
+
} else {
|
|
2249
|
+
const execution = snapshot.execution;
|
|
2250
|
+
if (!("status" in execution)) {
|
|
2251
|
+
errors.push("execution missing required field: status");
|
|
2252
|
+
} else if (!["running", "completed", "failed"].includes(execution.status)) {
|
|
2253
|
+
errors.push(`execution.status must be one of: running, completed, failed`);
|
|
2254
|
+
}
|
|
2255
|
+
if (!("lastUpdated" in execution)) {
|
|
2256
|
+
errors.push("execution missing required field: lastUpdated");
|
|
2257
|
+
} else if (typeof execution.lastUpdated !== "string") {
|
|
2258
|
+
errors.push("execution.lastUpdated must be a string (ISO timestamp)");
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (errors.length > 0) {
|
|
2262
|
+
return { valid: false, errors };
|
|
2263
|
+
}
|
|
2264
|
+
return { valid: true, snapshot: obj };
|
|
2265
|
+
}
|
|
2266
|
+
function assertValidSnapshot(obj) {
|
|
2267
|
+
const result = validateSnapshot(obj);
|
|
2268
|
+
if (!result.valid) {
|
|
2269
|
+
throw new SnapshotFormatError(`Invalid snapshot format: ${result.errors[0]}`, result.errors);
|
|
2270
|
+
}
|
|
2271
|
+
return result.snapshot;
|
|
2272
|
+
}
|
|
2273
|
+
function mergeSnapshots(base, delta) {
|
|
2274
|
+
const mergedSteps = /* @__PURE__ */ Object.create(null);
|
|
2275
|
+
for (const [key, value] of Object.entries(base.steps)) {
|
|
2276
|
+
if (Object.prototype.hasOwnProperty.call(base.steps, key)) {
|
|
2277
|
+
mergedSteps[key] = value;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
for (const [key, value] of Object.entries(delta.steps)) {
|
|
2281
|
+
if (Object.prototype.hasOwnProperty.call(delta.steps, key)) {
|
|
2282
|
+
mergedSteps[key] = value;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
const mergedMetadata = base.metadata || delta.metadata ? { ...base.metadata, ...delta.metadata } : void 0;
|
|
2286
|
+
const baseWarnings = (base.warnings || []).filter(
|
|
2287
|
+
(w) => !Object.prototype.hasOwnProperty.call(delta.steps, w.stepId)
|
|
2288
|
+
);
|
|
2289
|
+
const mergedWarnings = [...baseWarnings, ...delta.warnings || []];
|
|
2290
|
+
return {
|
|
2291
|
+
formatVersion: 1,
|
|
2292
|
+
steps: mergedSteps,
|
|
2293
|
+
execution: { ...delta.execution },
|
|
2294
|
+
metadata: mergedMetadata,
|
|
2295
|
+
warnings: mergedWarnings.length > 0 ? mergedWarnings : void 0
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
var MAX_STRING_REPRESENTATION_LENGTH = 1e3;
|
|
2299
|
+
function serializeError(error) {
|
|
2300
|
+
const serialized = {
|
|
2301
|
+
type: "error",
|
|
2302
|
+
name: error.name,
|
|
2303
|
+
message: error.message
|
|
2304
|
+
};
|
|
2305
|
+
if (error.stack) {
|
|
2306
|
+
serialized.stack = error.stack;
|
|
2307
|
+
}
|
|
2308
|
+
const errorWithCause = error;
|
|
2309
|
+
if (errorWithCause.cause !== void 0) {
|
|
2310
|
+
if (errorWithCause.cause instanceof Error) {
|
|
2311
|
+
serialized.cause = serializeError(errorWithCause.cause);
|
|
2312
|
+
} else {
|
|
2313
|
+
serialized.cause = serializeThrown(errorWithCause.cause);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return serialized;
|
|
2317
|
+
}
|
|
2318
|
+
function serializeThrown(value) {
|
|
2319
|
+
let stringRepresentation;
|
|
2320
|
+
let truncated = false;
|
|
2321
|
+
try {
|
|
2322
|
+
stringRepresentation = String(value);
|
|
2323
|
+
if (stringRepresentation.length > MAX_STRING_REPRESENTATION_LENGTH) {
|
|
2324
|
+
stringRepresentation = stringRepresentation.slice(0, MAX_STRING_REPRESENTATION_LENGTH);
|
|
2325
|
+
truncated = true;
|
|
2326
|
+
}
|
|
2327
|
+
} catch {
|
|
2328
|
+
stringRepresentation = "[unable to convert to string]";
|
|
2329
|
+
}
|
|
2330
|
+
const originalType = value === null ? "null" : typeof value === "object" ? value.constructor?.name ?? "Object" : typeof value;
|
|
2331
|
+
let jsonValue;
|
|
2332
|
+
try {
|
|
2333
|
+
const serialized = JSON.stringify(value);
|
|
2334
|
+
if (serialized !== void 0) {
|
|
2335
|
+
jsonValue = JSON.parse(serialized);
|
|
2336
|
+
}
|
|
2337
|
+
} catch {
|
|
2338
|
+
}
|
|
2339
|
+
const result = {
|
|
2340
|
+
type: "thrown",
|
|
2341
|
+
originalType,
|
|
2342
|
+
stringRepresentation
|
|
2343
|
+
};
|
|
2344
|
+
if (jsonValue !== void 0) {
|
|
2345
|
+
result.value = jsonValue;
|
|
2346
|
+
}
|
|
2347
|
+
if (truncated) {
|
|
2348
|
+
result.truncated = true;
|
|
2349
|
+
}
|
|
2350
|
+
return result;
|
|
2351
|
+
}
|
|
2352
|
+
function deserializeCauseNew(serialized) {
|
|
2353
|
+
if (serialized.type === "error") {
|
|
2354
|
+
const error = new Error(serialized.message);
|
|
2355
|
+
error.name = serialized.name;
|
|
2356
|
+
if (serialized.stack) {
|
|
2357
|
+
error.stack = serialized.stack;
|
|
2358
|
+
}
|
|
2359
|
+
if (serialized.cause) {
|
|
2360
|
+
error.cause = deserializeCauseNew(serialized.cause);
|
|
2361
|
+
}
|
|
2362
|
+
return error;
|
|
2363
|
+
}
|
|
2364
|
+
return serialized.value !== void 0 ? serialized.value : serialized.stringRepresentation;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/workflow/cache-encoding.ts
|
|
2368
|
+
function isCachedErrorCause(cause) {
|
|
2369
|
+
return typeof cause === "object" && cause !== null && cause.__cachedMeta === true;
|
|
2370
|
+
}
|
|
2371
|
+
function encodeCachedError(error, meta, originalCause) {
|
|
2372
|
+
return err(error, {
|
|
2373
|
+
cause: { __cachedMeta: true, originalCause, meta }
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
function decodeCachedMeta(cause) {
|
|
2377
|
+
if (isCachedErrorCause(cause)) {
|
|
2378
|
+
return cause.meta;
|
|
2379
|
+
}
|
|
2380
|
+
return { origin: "result", resultCause: cause };
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// src/workflow/resume-state.ts
|
|
2384
|
+
function createResumeStateCollector() {
|
|
2385
|
+
const steps = /* @__PURE__ */ new Map();
|
|
2386
|
+
return {
|
|
2387
|
+
handleEvent: (event) => {
|
|
2388
|
+
if (isStepComplete(event)) {
|
|
2389
|
+
steps.set(event.stepKey, { result: event.result, meta: event.meta });
|
|
2390
|
+
}
|
|
2391
|
+
},
|
|
2392
|
+
getResumeState: () => ({ steps: new Map(steps) }),
|
|
2393
|
+
clear: () => steps.clear()
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/streaming/types.ts
|
|
2398
|
+
var STREAM_WRITE_ERROR = "STREAM_WRITE_ERROR";
|
|
2399
|
+
var STREAM_READ_ERROR = "STREAM_READ_ERROR";
|
|
2400
|
+
var STREAM_CLOSE_ERROR = "STREAM_CLOSE_ERROR";
|
|
2401
|
+
var STREAM_ENDED = "STREAM_ENDED";
|
|
2402
|
+
function streamWriteError(reason, message, cause) {
|
|
2403
|
+
return {
|
|
2404
|
+
type: STREAM_WRITE_ERROR,
|
|
2405
|
+
reason,
|
|
2406
|
+
message,
|
|
2407
|
+
...cause !== void 0 ? { cause } : {}
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function streamReadError(reason, message, cause) {
|
|
2411
|
+
return {
|
|
2412
|
+
type: STREAM_READ_ERROR,
|
|
2413
|
+
reason,
|
|
2414
|
+
message,
|
|
2415
|
+
...cause !== void 0 ? { cause } : {}
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function streamCloseError(reason, message, cause) {
|
|
2419
|
+
return {
|
|
2420
|
+
type: STREAM_CLOSE_ERROR,
|
|
2421
|
+
reason,
|
|
2422
|
+
message,
|
|
2423
|
+
...cause !== void 0 ? { cause } : {}
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
function streamEnded(finalPosition) {
|
|
2427
|
+
return {
|
|
2428
|
+
type: STREAM_ENDED,
|
|
2429
|
+
finalPosition
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/streaming/backpressure.ts
|
|
2434
|
+
function createBackpressureController(options = {}) {
|
|
2435
|
+
const highWaterMark = options.highWaterMark ?? 16;
|
|
2436
|
+
const lowWaterMark = options.lowWaterMark ?? Math.floor(highWaterMark / 2);
|
|
2437
|
+
const onStateChange = options.onStateChange;
|
|
2438
|
+
let state = "flowing";
|
|
2439
|
+
let bufferedCount = 0;
|
|
2440
|
+
let drainResolvers = [];
|
|
2441
|
+
function updateState(newState) {
|
|
2442
|
+
if (state !== newState) {
|
|
2443
|
+
state = newState;
|
|
2444
|
+
onStateChange?.(newState);
|
|
2445
|
+
if (newState === "flowing" && drainResolvers.length > 0) {
|
|
2446
|
+
const resolvers = drainResolvers;
|
|
2447
|
+
drainResolvers = [];
|
|
2448
|
+
for (const resolve of resolvers) {
|
|
2449
|
+
resolve();
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
function checkState() {
|
|
2455
|
+
if (state === "flowing" && bufferedCount >= highWaterMark) {
|
|
2456
|
+
updateState("paused");
|
|
2457
|
+
} else if (state === "paused" && bufferedCount <= lowWaterMark) {
|
|
2458
|
+
updateState("flowing");
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return {
|
|
2462
|
+
get state() {
|
|
2463
|
+
return state;
|
|
2464
|
+
},
|
|
2465
|
+
get bufferedCount() {
|
|
2466
|
+
return bufferedCount;
|
|
2467
|
+
},
|
|
2468
|
+
get highWaterMark() {
|
|
2469
|
+
return highWaterMark;
|
|
2470
|
+
},
|
|
2471
|
+
get lowWaterMark() {
|
|
2472
|
+
return lowWaterMark;
|
|
2473
|
+
},
|
|
2474
|
+
increment() {
|
|
2475
|
+
bufferedCount++;
|
|
2476
|
+
checkState();
|
|
2477
|
+
},
|
|
2478
|
+
decrement() {
|
|
2479
|
+
if (bufferedCount > 0) {
|
|
2480
|
+
bufferedCount--;
|
|
2481
|
+
checkState();
|
|
2482
|
+
}
|
|
2483
|
+
},
|
|
2484
|
+
setCount(count) {
|
|
2485
|
+
bufferedCount = Math.max(0, count);
|
|
2486
|
+
checkState();
|
|
2487
|
+
},
|
|
2488
|
+
waitForDrain() {
|
|
2489
|
+
if (state === "flowing") {
|
|
2490
|
+
return Promise.resolve();
|
|
2491
|
+
}
|
|
2492
|
+
return new Promise((resolve) => {
|
|
2493
|
+
drainResolvers.push(resolve);
|
|
2494
|
+
});
|
|
2495
|
+
},
|
|
2496
|
+
reset() {
|
|
2497
|
+
bufferedCount = 0;
|
|
2498
|
+
drainResolvers = [];
|
|
2499
|
+
updateState("flowing");
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/workflow/validation.ts
|
|
2505
|
+
async function validateInput(schema, input) {
|
|
2506
|
+
const result = schema["~standard"].validate(input);
|
|
2507
|
+
const resolved = result instanceof Promise ? await result : result;
|
|
2508
|
+
if (resolved.issues) {
|
|
2509
|
+
return err({
|
|
2510
|
+
type: "INPUT_VALIDATION_ERROR",
|
|
2511
|
+
issues: resolved.issues.map((i) => ({
|
|
2512
|
+
message: i.message,
|
|
2513
|
+
path: i.path?.map(
|
|
2514
|
+
(p) => typeof p === "object" ? p.key : p
|
|
2515
|
+
)
|
|
2516
|
+
})),
|
|
2517
|
+
message: `Input validation failed: ${resolved.issues.map((i) => i.message).join(", ")}`
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
return ok2(resolved.value);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// src/di.ts
|
|
2524
|
+
function withDeps(workflow, overrides) {
|
|
2525
|
+
const forward = (method) => ((...args) => {
|
|
2526
|
+
const last = args.at(-1);
|
|
2527
|
+
const hasConfig = args.length > 0 && typeof last === "object" && last !== null && !Array.isArray(last) && typeof last !== "function";
|
|
2528
|
+
const config = hasConfig ? last : void 0;
|
|
2529
|
+
const head = hasConfig ? args.slice(0, -1) : args;
|
|
2530
|
+
const mergedDeps = { ...overrides, ...config?.deps ?? {} };
|
|
2531
|
+
const mergedConfig = config ? { ...config, deps: mergedDeps } : { deps: mergedDeps };
|
|
2532
|
+
return workflow[method](...head, mergedConfig);
|
|
2533
|
+
});
|
|
2534
|
+
return {
|
|
2535
|
+
run: forward("run"),
|
|
2536
|
+
runWithState: forward("runWithState"),
|
|
2537
|
+
withDeps(nextOverrides) {
|
|
2538
|
+
return withDeps(workflow, { ...overrides, ...nextOverrides });
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// src/workflow/execute.ts
|
|
2544
|
+
function createWorkflow(workflowName, deps, options) {
|
|
2545
|
+
if (typeof workflowName !== "string" || workflowName.length === 0) {
|
|
2546
|
+
throw new TypeError(
|
|
2547
|
+
"createWorkflow(workflowName, deps, options?): first argument must be a non-empty string. Example: createWorkflow('checkout', { chargeCard, sendEmail })"
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
const depsActual = deps ?? {};
|
|
2551
|
+
const optionsActual = options;
|
|
2552
|
+
async function internalExecute(runName, userFn, config) {
|
|
2553
|
+
const workflowId = runName ?? crypto.randomUUID();
|
|
2554
|
+
const effectiveDeps = config?.deps ? { ...depsActual, ...config.deps } : depsActual;
|
|
2555
|
+
const compensations = [];
|
|
2556
|
+
const createContextFn = config?.createContext ?? optionsActual?.createContext;
|
|
2557
|
+
const context = createContextFn ? await createContextFn() : void 0;
|
|
2558
|
+
const workflowSignal = config?.signal ?? optionsActual?.signal;
|
|
2559
|
+
const onEventHandler = config?.onEvent ?? optionsActual?.onEvent;
|
|
2560
|
+
const onErrorHandler = config?.onError ?? optionsActual?.onError;
|
|
2561
|
+
const shouldRunHook = config?.shouldRun ?? optionsActual?.shouldRun;
|
|
2562
|
+
const onBeforeStartHook = config?.onBeforeStart ?? optionsActual?.onBeforeStart;
|
|
2563
|
+
const onAfterStepHook = config?.onAfterStep ?? optionsActual?.onAfterStep;
|
|
2564
|
+
const resumeStateOption = config?.resumeState ?? optionsActual?.resumeState;
|
|
2565
|
+
const catchUnexpected = optionsActual?.catchUnexpected ?? defaultCatchUnexpected;
|
|
2566
|
+
const declaredGraph = config?.graph ?? optionsActual?.graph;
|
|
2567
|
+
const workflowData = {};
|
|
2568
|
+
const devWarnings = (config?.devWarnings ?? optionsActual?.devWarnings) === true && process.env.NODE_ENV !== "production";
|
|
2569
|
+
const ctxSetWarned = /* @__PURE__ */ new Set();
|
|
2570
|
+
const ctxGetWarned = /* @__PURE__ */ new Set();
|
|
2571
|
+
const workflowContext = {
|
|
2572
|
+
workflowId,
|
|
2573
|
+
onEvent: onEventHandler,
|
|
2574
|
+
context: context !== void 0 ? context : void 0,
|
|
2575
|
+
signal: workflowSignal,
|
|
2576
|
+
// Data store for static analysis
|
|
2577
|
+
input: {},
|
|
2578
|
+
ref: (key) => workflowData[key],
|
|
2579
|
+
set: (key, value) => {
|
|
2580
|
+
if (devWarnings && !ctxSetWarned.has(key)) {
|
|
2581
|
+
ctxSetWarned.add(key);
|
|
2582
|
+
console.warn(
|
|
2583
|
+
`awaitly: ctx.set('${key}', ...) is deprecated for static analysis. Use step('id', fn, { out: '${key}' }) instead.`
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
workflowData[key] = value;
|
|
2587
|
+
},
|
|
2588
|
+
get: (key) => {
|
|
2589
|
+
if (devWarnings && !ctxGetWarned.has(key)) {
|
|
2590
|
+
ctxGetWarned.add(key);
|
|
2591
|
+
console.warn(
|
|
2592
|
+
`awaitly: ctx.get('${key}') is deprecated for static analysis. Use ctx.ref('${key}') instead for tracked dependencies.`
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
return workflowData[key];
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
const emitEvent = (event) => {
|
|
2599
|
+
const eventWithContext = event.context !== void 0 || context === void 0 ? event : { ...event, context };
|
|
2600
|
+
const eventWithName = eventWithContext.workflowName === void 0 ? { ...eventWithContext, workflowName } : eventWithContext;
|
|
2601
|
+
onEventHandler?.(eventWithName, context);
|
|
2602
|
+
};
|
|
2603
|
+
const createCancelledResult = (reason, lastStepKey2) => {
|
|
2604
|
+
const cancelledError = {
|
|
2605
|
+
type: "WORKFLOW_CANCELLED",
|
|
2606
|
+
reason,
|
|
2607
|
+
lastStepKey: lastStepKey2
|
|
2608
|
+
};
|
|
2609
|
+
return err(catchUnexpected(cancelledError), { cause: cancelledError });
|
|
2610
|
+
};
|
|
2611
|
+
if (workflowSignal?.aborted) {
|
|
2612
|
+
const reason = typeof workflowSignal.reason === "string" ? workflowSignal.reason : workflowSignal.reason instanceof Error ? workflowSignal.reason.message : void 0;
|
|
2613
|
+
emitEvent({
|
|
2614
|
+
type: "workflow_cancelled",
|
|
2615
|
+
workflowId,
|
|
2616
|
+
ts: Date.now(),
|
|
2617
|
+
durationMs: 0,
|
|
2618
|
+
reason
|
|
2619
|
+
});
|
|
2620
|
+
return createCancelledResult(reason);
|
|
2621
|
+
}
|
|
2622
|
+
if (shouldRunHook) {
|
|
2623
|
+
const hookStartTime = performance.now();
|
|
2624
|
+
try {
|
|
2625
|
+
const shouldRunResult = await shouldRunHook(workflowId, context);
|
|
2626
|
+
const hookDuration = performance.now() - hookStartTime;
|
|
2627
|
+
emitEvent({
|
|
2628
|
+
type: "hook_should_run",
|
|
2629
|
+
workflowId,
|
|
2630
|
+
ts: Date.now(),
|
|
2631
|
+
durationMs: hookDuration,
|
|
2632
|
+
result: shouldRunResult,
|
|
2633
|
+
skipped: !shouldRunResult
|
|
2634
|
+
});
|
|
2635
|
+
if (!shouldRunResult) {
|
|
2636
|
+
const skipCause = new Error("Workflow skipped by shouldRun hook");
|
|
2637
|
+
return err(catchUnexpected(skipCause), { cause: skipCause });
|
|
2638
|
+
}
|
|
2639
|
+
} catch (thrown) {
|
|
2640
|
+
const hookDuration = performance.now() - hookStartTime;
|
|
2641
|
+
emitEvent({
|
|
2642
|
+
type: "hook_should_run_error",
|
|
2643
|
+
workflowId,
|
|
2644
|
+
ts: Date.now(),
|
|
2645
|
+
durationMs: hookDuration,
|
|
2646
|
+
error: thrown
|
|
2647
|
+
});
|
|
2648
|
+
return err(catchUnexpected(thrown), { cause: thrown });
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (onBeforeStartHook) {
|
|
2652
|
+
const hookStartTime = performance.now();
|
|
2653
|
+
try {
|
|
2654
|
+
const beforeStartResult = await onBeforeStartHook(workflowId, context);
|
|
2655
|
+
const hookDuration = performance.now() - hookStartTime;
|
|
2656
|
+
emitEvent({
|
|
2657
|
+
type: "hook_before_start",
|
|
2658
|
+
workflowId,
|
|
2659
|
+
ts: Date.now(),
|
|
2660
|
+
durationMs: hookDuration,
|
|
2661
|
+
result: beforeStartResult,
|
|
2662
|
+
skipped: !beforeStartResult
|
|
2663
|
+
});
|
|
2664
|
+
if (!beforeStartResult) {
|
|
2665
|
+
const skipCause = new Error("Workflow skipped by onBeforeStart hook");
|
|
2666
|
+
return err(catchUnexpected(skipCause), { cause: skipCause });
|
|
2667
|
+
}
|
|
2668
|
+
} catch (thrown) {
|
|
2669
|
+
const hookDuration = performance.now() - hookStartTime;
|
|
2670
|
+
emitEvent({
|
|
2671
|
+
type: "hook_before_start_error",
|
|
2672
|
+
workflowId,
|
|
2673
|
+
ts: Date.now(),
|
|
2674
|
+
durationMs: hookDuration,
|
|
2675
|
+
error: thrown
|
|
2676
|
+
});
|
|
2677
|
+
return err(catchUnexpected(thrown), { cause: thrown });
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
const inputSchema = optionsActual?.inputSchema;
|
|
2681
|
+
const inputValue = optionsActual?.input;
|
|
2682
|
+
if (inputSchema) {
|
|
2683
|
+
const validationResult = await validateInput(inputSchema, inputValue);
|
|
2684
|
+
if (!validationResult.ok) {
|
|
2685
|
+
return err(validationResult.error);
|
|
2686
|
+
}
|
|
2687
|
+
workflowContext.input = validationResult.value;
|
|
2688
|
+
} else if (inputValue !== void 0) {
|
|
2689
|
+
workflowContext.input = inputValue;
|
|
2690
|
+
}
|
|
2691
|
+
const startTs = Date.now();
|
|
2692
|
+
const startTime = performance.now();
|
|
2693
|
+
emitEvent({
|
|
2694
|
+
type: "workflow_start",
|
|
2695
|
+
workflowId,
|
|
2696
|
+
ts: startTs
|
|
2697
|
+
});
|
|
2698
|
+
let cache = config?.cache ?? optionsActual?.cache;
|
|
2699
|
+
const streamStore = config?.streamStore ?? optionsActual?.streamStore;
|
|
2700
|
+
if (resumeStateOption && !cache) {
|
|
2701
|
+
cache = /* @__PURE__ */ new Map();
|
|
2702
|
+
}
|
|
2703
|
+
if (resumeStateOption && cache) {
|
|
2704
|
+
const resumeState = typeof resumeStateOption === "function" ? await resumeStateOption() : resumeStateOption;
|
|
2705
|
+
if (!(resumeState.steps instanceof Map)) {
|
|
2706
|
+
console.warn(
|
|
2707
|
+
`awaitly: resumeState.steps is not a Map (got ${typeof resumeState.steps}). This usually happens when state is serialized with JSON.stringify() directly.
|
|
2708
|
+
Use serializeResumeState() and deserializeResumeState() from 'awaitly/workflow' instead:
|
|
2709
|
+
import { serializeResumeState, deserializeResumeState } from 'awaitly/workflow';
|
|
2710
|
+
const json = JSON.stringify(serializeResumeState(state)); // Save this
|
|
2711
|
+
const restored = deserializeResumeState(JSON.parse(json)); // Load this`
|
|
2712
|
+
);
|
|
2713
|
+
if (typeof resumeState.steps === "object" && resumeState.steps !== null) {
|
|
2714
|
+
resumeState.steps = new Map(Object.entries(resumeState.steps));
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
for (const [key, entry] of resumeState.steps) {
|
|
2718
|
+
const { result: result2, meta } = entry;
|
|
2719
|
+
if (result2.ok) {
|
|
2720
|
+
cache.set(key, result2);
|
|
2721
|
+
} else {
|
|
2722
|
+
const effectiveMeta = meta ?? { origin: "result", resultCause: result2.cause };
|
|
2723
|
+
cache.set(key, encodeCachedError(result2.error, effectiveMeta, result2.cause));
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
const snapshotOption = config?.snapshot ?? optionsActual?.snapshot;
|
|
2728
|
+
const snapshotSerialization = optionsActual?.serialization;
|
|
2729
|
+
if (snapshotOption && !resumeStateOption) {
|
|
2730
|
+
if (!cache) {
|
|
2731
|
+
cache = /* @__PURE__ */ new Map();
|
|
2732
|
+
}
|
|
2733
|
+
const snapshot = snapshotOption;
|
|
2734
|
+
const decode = snapshotSerialization?.decode;
|
|
2735
|
+
for (const [stepId, stepResult] of Object.entries(snapshot.steps)) {
|
|
2736
|
+
if (!Object.prototype.hasOwnProperty.call(snapshot.steps, stepId)) {
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
try {
|
|
2740
|
+
if (stepResult.ok) {
|
|
2741
|
+
const value = decode ? decode(stepResult.value) : stepResult.value;
|
|
2742
|
+
cache.set(stepId, ok2(value));
|
|
2743
|
+
} else {
|
|
2744
|
+
const errorValue = decode ? decode(stepResult.error) : stepResult.error;
|
|
2745
|
+
const deserializedCause = deserializeCauseNew(stepResult.cause);
|
|
2746
|
+
const meta = stepResult.meta?.origin === "throw" ? { origin: "throw", thrown: deserializedCause } : { origin: "result", resultCause: deserializedCause };
|
|
2747
|
+
cache.set(stepId, encodeCachedError(errorValue, meta, deserializedCause));
|
|
2748
|
+
}
|
|
2749
|
+
} catch (e) {
|
|
2750
|
+
throw new SnapshotDecodeError(
|
|
2751
|
+
`Failed to decode step "${stepId}": ${e instanceof Error ? e.message : String(e)}`,
|
|
2752
|
+
stepId,
|
|
2753
|
+
e instanceof Error ? e : void 0
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
let abortedDuringExecution = false;
|
|
2759
|
+
let abortReason;
|
|
2760
|
+
let lastStepKey;
|
|
2761
|
+
const abortHandler = () => {
|
|
2762
|
+
abortedDuringExecution = true;
|
|
2763
|
+
abortReason = typeof workflowSignal?.reason === "string" ? workflowSignal.reason : workflowSignal?.reason instanceof Error ? workflowSignal.reason.message : void 0;
|
|
2764
|
+
};
|
|
2765
|
+
if (workflowSignal && !workflowSignal.aborted) {
|
|
2766
|
+
workflowSignal.addEventListener("abort", abortHandler, { once: true });
|
|
2767
|
+
}
|
|
2768
|
+
const checkCancellation = () => {
|
|
2769
|
+
if (abortedDuringExecution || workflowSignal?.aborted) {
|
|
2770
|
+
const reason = abortReason ?? (typeof workflowSignal?.reason === "string" ? workflowSignal.reason : workflowSignal?.reason instanceof Error ? workflowSignal.reason.message : void 0);
|
|
2771
|
+
const cancelledError = {
|
|
2772
|
+
type: "WORKFLOW_CANCELLED",
|
|
2773
|
+
reason,
|
|
2774
|
+
lastStepKey
|
|
2775
|
+
};
|
|
2776
|
+
throw cancelledError;
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
const callOnAfterStepHook = async (stepKey, result2, _meta) => {
|
|
2780
|
+
if (!onAfterStepHook) return;
|
|
2781
|
+
const hookStartTime = performance.now();
|
|
2782
|
+
try {
|
|
2783
|
+
await onAfterStepHook(stepKey, result2, workflowId, context);
|
|
2784
|
+
const hookDuration = performance.now() - hookStartTime;
|
|
2785
|
+
emitEvent({
|
|
2786
|
+
type: "hook_after_step",
|
|
2787
|
+
workflowId,
|
|
2788
|
+
stepKey,
|
|
2789
|
+
ts: Date.now(),
|
|
2790
|
+
durationMs: hookDuration
|
|
2791
|
+
});
|
|
2792
|
+
} catch (thrown) {
|
|
2793
|
+
const hookDuration = performance.now() - hookStartTime;
|
|
2794
|
+
emitEvent({
|
|
2795
|
+
type: "hook_after_step_error",
|
|
2796
|
+
workflowId,
|
|
2797
|
+
stepKey,
|
|
2798
|
+
ts: Date.now(),
|
|
2799
|
+
durationMs: hookDuration,
|
|
2800
|
+
error: thrown
|
|
2801
|
+
});
|
|
2802
|
+
throw thrown;
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
const createCachedStep = (realStep) => {
|
|
2806
|
+
const cachedStepFn = async (idOrOperationOrResult, operationOrOptions, stepOptions) => {
|
|
2807
|
+
if (typeof idOrOperationOrResult !== "string") {
|
|
2808
|
+
throw new Error(
|
|
2809
|
+
'[awaitly] step() requires a string ID as the first argument. Example: step("fetchUser", () => fetchUser(id))'
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
const id = idOrOperationOrResult;
|
|
2813
|
+
const opts = stepOptions ?? {};
|
|
2814
|
+
const name = id;
|
|
2815
|
+
const key = Object.prototype.hasOwnProperty.call(opts, "key") ? opts.key : id;
|
|
2816
|
+
const { ttl, out } = opts;
|
|
2817
|
+
checkCancellation();
|
|
2818
|
+
const stepMetadata = extractStepMetadata(opts);
|
|
2819
|
+
if (key && cache && cache.has(key)) {
|
|
2820
|
+
emitEvent({
|
|
2821
|
+
type: "step_cache_hit",
|
|
2822
|
+
workflowId,
|
|
2823
|
+
stepKey: key,
|
|
2824
|
+
name,
|
|
2825
|
+
ts: Date.now(),
|
|
2826
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
2827
|
+
});
|
|
2828
|
+
const cached = cache.get(key);
|
|
2829
|
+
if (cached.ok) {
|
|
2830
|
+
lastStepKey = key;
|
|
2831
|
+
if (out) {
|
|
2832
|
+
workflowData[out] = cached.value;
|
|
2833
|
+
}
|
|
2834
|
+
return cached.value;
|
|
2835
|
+
}
|
|
2836
|
+
const meta = decodeCachedMeta(cached.cause);
|
|
2837
|
+
throw createEarlyExit(cached.error, meta);
|
|
2838
|
+
}
|
|
2839
|
+
if (key && cache) {
|
|
2840
|
+
emitEvent({
|
|
2841
|
+
type: "step_cache_miss",
|
|
2842
|
+
workflowId,
|
|
2843
|
+
stepKey: key,
|
|
2844
|
+
name,
|
|
2845
|
+
ts: Date.now(),
|
|
2846
|
+
...stepMetadata && { metadata: stepMetadata }
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
try {
|
|
2850
|
+
const value = await realStep(
|
|
2851
|
+
id,
|
|
2852
|
+
operationOrOptions,
|
|
2853
|
+
opts
|
|
2854
|
+
);
|
|
2855
|
+
if (out) {
|
|
2856
|
+
workflowData[out] = value;
|
|
2857
|
+
}
|
|
2858
|
+
if (opts.compensate) {
|
|
2859
|
+
compensations.push({
|
|
2860
|
+
stepName: name,
|
|
2861
|
+
value,
|
|
2862
|
+
compensate: opts.compensate
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
if (key) {
|
|
2866
|
+
lastStepKey = key;
|
|
2867
|
+
if (cache) {
|
|
2868
|
+
cache.set(key, ok2(value), ttl ? { ttl } : void 0);
|
|
2869
|
+
}
|
|
2870
|
+
await callOnAfterStepHook(key, ok2(value));
|
|
2871
|
+
}
|
|
2872
|
+
return value;
|
|
2873
|
+
} catch (thrown) {
|
|
2874
|
+
if (key && isEarlyExit(thrown)) {
|
|
2875
|
+
const exit = thrown;
|
|
2876
|
+
const originalCause = exit.meta.origin === "result" ? exit.meta.resultCause : exit.meta.origin === "throw" ? exit.meta.thrown : void 0;
|
|
2877
|
+
const errorResult = encodeCachedError(exit.error, exit.meta, originalCause);
|
|
2878
|
+
if (cache) {
|
|
2879
|
+
cache.set(key, errorResult, ttl ? { ttl } : void 0);
|
|
2880
|
+
}
|
|
2881
|
+
await callOnAfterStepHook(key, errorResult, exit.meta);
|
|
2882
|
+
}
|
|
2883
|
+
throw thrown;
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
cachedStepFn.try = async (id, operation, opts) => {
|
|
2887
|
+
const { ttl } = opts;
|
|
2888
|
+
const key = opts.key ?? id;
|
|
2889
|
+
const name = id;
|
|
2890
|
+
if (cache && cache.has(key)) {
|
|
2891
|
+
emitEvent({
|
|
2892
|
+
type: "step_cache_hit",
|
|
2893
|
+
workflowId,
|
|
2894
|
+
stepKey: key,
|
|
2895
|
+
name,
|
|
2896
|
+
ts: Date.now()
|
|
2897
|
+
});
|
|
2898
|
+
const cached = cache.get(key);
|
|
2899
|
+
if (cached.ok) {
|
|
2900
|
+
return cached.value;
|
|
2901
|
+
}
|
|
2902
|
+
const meta = decodeCachedMeta(cached.cause);
|
|
2903
|
+
throw createEarlyExit(cached.error, meta);
|
|
2904
|
+
}
|
|
2905
|
+
if (cache) {
|
|
2906
|
+
emitEvent({
|
|
2907
|
+
type: "step_cache_miss",
|
|
2908
|
+
workflowId,
|
|
2909
|
+
stepKey: key,
|
|
2910
|
+
name,
|
|
2911
|
+
ts: Date.now()
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
try {
|
|
2915
|
+
const value = await realStep.try(id, operation, { ...opts, key });
|
|
2916
|
+
if (opts.compensate) {
|
|
2917
|
+
compensations.push({
|
|
2918
|
+
stepName: name,
|
|
2919
|
+
value,
|
|
2920
|
+
compensate: opts.compensate
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
if (cache) {
|
|
2924
|
+
cache.set(key, ok2(value), ttl ? { ttl } : void 0);
|
|
2925
|
+
}
|
|
2926
|
+
await callOnAfterStepHook(key, ok2(value));
|
|
2927
|
+
return value;
|
|
2928
|
+
} catch (thrown) {
|
|
2929
|
+
if (isEarlyExit(thrown)) {
|
|
2930
|
+
const exit = thrown;
|
|
2931
|
+
const originalCause = exit.meta.origin === "result" ? exit.meta.resultCause : exit.meta.origin === "throw" ? exit.meta.thrown : void 0;
|
|
2932
|
+
const errorResult = encodeCachedError(exit.error, exit.meta, originalCause);
|
|
2933
|
+
if (cache) {
|
|
2934
|
+
cache.set(key, errorResult, ttl ? { ttl } : void 0);
|
|
2935
|
+
}
|
|
2936
|
+
await callOnAfterStepHook(key, errorResult, exit.meta);
|
|
2937
|
+
}
|
|
2938
|
+
throw thrown;
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2941
|
+
cachedStepFn.fromResult = async (id, operation, opts) => {
|
|
2942
|
+
const { ttl } = opts;
|
|
2943
|
+
const key = opts.key ?? id;
|
|
2944
|
+
const name = id;
|
|
2945
|
+
if (cache && cache.has(key)) {
|
|
2946
|
+
emitEvent({
|
|
2947
|
+
type: "step_cache_hit",
|
|
2948
|
+
workflowId,
|
|
2949
|
+
stepKey: key,
|
|
2950
|
+
name,
|
|
2951
|
+
ts: Date.now()
|
|
2952
|
+
});
|
|
2953
|
+
const cached = cache.get(key);
|
|
2954
|
+
if (cached.ok) {
|
|
2955
|
+
return cached.value;
|
|
2956
|
+
}
|
|
2957
|
+
const meta = decodeCachedMeta(cached.cause);
|
|
2958
|
+
throw createEarlyExit(cached.error, meta);
|
|
2959
|
+
}
|
|
2960
|
+
if (cache) {
|
|
2961
|
+
emitEvent({
|
|
2962
|
+
type: "step_cache_miss",
|
|
2963
|
+
workflowId,
|
|
2964
|
+
stepKey: key,
|
|
2965
|
+
name,
|
|
2966
|
+
ts: Date.now()
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
const value = await realStep.fromResult(id, operation, { ...opts, key });
|
|
2971
|
+
if (cache) {
|
|
2972
|
+
cache.set(key, ok2(value), ttl ? { ttl } : void 0);
|
|
2973
|
+
}
|
|
2974
|
+
await callOnAfterStepHook(key, ok2(value));
|
|
2975
|
+
return value;
|
|
2976
|
+
} catch (thrown) {
|
|
2977
|
+
if (isEarlyExit(thrown)) {
|
|
2978
|
+
const exit = thrown;
|
|
2979
|
+
const originalCause = exit.meta.origin === "result" ? exit.meta.resultCause : exit.meta.origin === "throw" ? exit.meta.thrown : void 0;
|
|
2980
|
+
const errorResult = encodeCachedError(exit.error, exit.meta, originalCause);
|
|
2981
|
+
if (cache) {
|
|
2982
|
+
cache.set(key, errorResult, ttl ? { ttl } : void 0);
|
|
2983
|
+
}
|
|
2984
|
+
await callOnAfterStepHook(key, errorResult, exit.meta);
|
|
2985
|
+
}
|
|
2986
|
+
throw thrown;
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
cachedStepFn.race = realStep.race;
|
|
2990
|
+
cachedStepFn.withFallback = async (id, operation, options2) => {
|
|
2991
|
+
const key = options2.key ?? id;
|
|
2992
|
+
const name = id;
|
|
2993
|
+
if (cache && cache.has(key)) {
|
|
2994
|
+
emitEvent({
|
|
2995
|
+
type: "step_cache_hit",
|
|
2996
|
+
workflowId,
|
|
2997
|
+
stepKey: key,
|
|
2998
|
+
name,
|
|
2999
|
+
ts: Date.now()
|
|
3000
|
+
});
|
|
3001
|
+
const cached = cache.get(key);
|
|
3002
|
+
if (cached.ok) {
|
|
3003
|
+
return cached.value;
|
|
3004
|
+
}
|
|
3005
|
+
const meta = decodeCachedMeta(cached.cause);
|
|
3006
|
+
throw createEarlyExit(cached.error, meta);
|
|
3007
|
+
}
|
|
3008
|
+
if (cache) {
|
|
3009
|
+
emitEvent({
|
|
3010
|
+
type: "step_cache_miss",
|
|
3011
|
+
workflowId,
|
|
3012
|
+
stepKey: key,
|
|
3013
|
+
name,
|
|
3014
|
+
ts: Date.now()
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
try {
|
|
3018
|
+
const value = await realStep.withFallback(
|
|
3019
|
+
id,
|
|
3020
|
+
operation,
|
|
3021
|
+
options2
|
|
3022
|
+
);
|
|
3023
|
+
if (cache) {
|
|
3024
|
+
cache.set(key, ok2(value));
|
|
3025
|
+
}
|
|
3026
|
+
await callOnAfterStepHook(key, ok2(value));
|
|
3027
|
+
return value;
|
|
3028
|
+
} catch (thrown) {
|
|
3029
|
+
if (isEarlyExit(thrown)) {
|
|
3030
|
+
const exit = thrown;
|
|
3031
|
+
const originalCause = exit.meta.origin === "result" ? exit.meta.resultCause : exit.meta.origin === "throw" ? exit.meta.thrown : void 0;
|
|
3032
|
+
const errorResult = encodeCachedError(exit.error, exit.meta, originalCause);
|
|
3033
|
+
if (cache) {
|
|
3034
|
+
cache.set(key, errorResult);
|
|
3035
|
+
}
|
|
3036
|
+
await callOnAfterStepHook(key, errorResult, exit.meta);
|
|
3037
|
+
}
|
|
3038
|
+
throw thrown;
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
cachedStepFn.withResource = async (id, options2) => {
|
|
3042
|
+
const key = id;
|
|
3043
|
+
try {
|
|
3044
|
+
const value = await realStep.withResource(id, options2);
|
|
3045
|
+
await callOnAfterStepHook(key, ok2(value));
|
|
3046
|
+
return value;
|
|
3047
|
+
} catch (thrown) {
|
|
3048
|
+
if (isEarlyExit(thrown)) {
|
|
3049
|
+
const exit = thrown;
|
|
3050
|
+
const originalCause = exit.meta.origin === "result" ? exit.meta.resultCause : exit.meta.origin === "throw" ? exit.meta.thrown : void 0;
|
|
3051
|
+
const errorResult = encodeCachedError(exit.error, exit.meta, originalCause);
|
|
3052
|
+
await callOnAfterStepHook(key, errorResult, exit.meta);
|
|
3053
|
+
}
|
|
3054
|
+
throw thrown;
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
cachedStepFn.retry = (id, operation, options2) => {
|
|
3058
|
+
const stepOptions = {
|
|
3059
|
+
key: options2.key,
|
|
3060
|
+
// explicitly pass so undefined = don't cache
|
|
3061
|
+
retry: {
|
|
3062
|
+
attempts: options2.attempts,
|
|
3063
|
+
backoff: options2.backoff,
|
|
3064
|
+
initialDelay: options2.initialDelay,
|
|
3065
|
+
maxDelay: options2.maxDelay,
|
|
3066
|
+
jitter: options2.jitter,
|
|
3067
|
+
shouldRetry: options2.shouldRetry,
|
|
3068
|
+
onRetry: options2.onRetry
|
|
3069
|
+
},
|
|
3070
|
+
timeout: options2.timeout,
|
|
3071
|
+
ttl: options2.ttl
|
|
3072
|
+
};
|
|
3073
|
+
return cachedStepFn(id, operation, stepOptions);
|
|
3074
|
+
};
|
|
3075
|
+
cachedStepFn.withTimeout = (id, operation, options2) => {
|
|
3076
|
+
const stepOptions = {
|
|
3077
|
+
key: options2.key,
|
|
3078
|
+
timeout: options2,
|
|
3079
|
+
ttl: options2.ttl
|
|
3080
|
+
};
|
|
3081
|
+
return cachedStepFn(
|
|
3082
|
+
id,
|
|
3083
|
+
operation,
|
|
3084
|
+
stepOptions
|
|
3085
|
+
);
|
|
3086
|
+
};
|
|
3087
|
+
cachedStepFn.sleep = (id, duration, options2) => {
|
|
3088
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
3089
|
+
throw new Error(
|
|
3090
|
+
'[awaitly] step.sleep() requires an explicit string ID as the first argument. Example: step.sleep("delay", "5s")'
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
const d = typeof duration === "string" ? parse(duration) : duration;
|
|
3094
|
+
if (!d) {
|
|
3095
|
+
throw new Error(`step.sleep: invalid duration '${duration}'`);
|
|
3096
|
+
}
|
|
3097
|
+
const ms = toMillis(d);
|
|
3098
|
+
const userSignal = options2?.signal;
|
|
3099
|
+
const sleepOperation = async () => {
|
|
3100
|
+
if (workflowSignal?.aborted || userSignal?.aborted) {
|
|
3101
|
+
const e = new Error("Sleep aborted");
|
|
3102
|
+
e.name = "AbortError";
|
|
3103
|
+
throw e;
|
|
3104
|
+
}
|
|
3105
|
+
return new Promise((resolve, reject) => {
|
|
3106
|
+
const state = {
|
|
3107
|
+
timeoutId: void 0
|
|
3108
|
+
};
|
|
3109
|
+
const onAbort = () => {
|
|
3110
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
3111
|
+
const e = new Error("Sleep aborted");
|
|
3112
|
+
e.name = "AbortError";
|
|
3113
|
+
reject(e);
|
|
3114
|
+
};
|
|
3115
|
+
workflowSignal?.addEventListener("abort", onAbort, { once: true });
|
|
3116
|
+
userSignal?.addEventListener("abort", onAbort, { once: true });
|
|
3117
|
+
state.timeoutId = setTimeout(() => {
|
|
3118
|
+
workflowSignal?.removeEventListener("abort", onAbort);
|
|
3119
|
+
userSignal?.removeEventListener("abort", onAbort);
|
|
3120
|
+
resolve(ok2(void 0));
|
|
3121
|
+
}, ms);
|
|
3122
|
+
});
|
|
3123
|
+
};
|
|
3124
|
+
return cachedStepFn(id, sleepOperation, {
|
|
3125
|
+
key: options2?.key,
|
|
3126
|
+
ttl: options2?.ttl,
|
|
3127
|
+
description: options2?.description
|
|
3128
|
+
});
|
|
3129
|
+
};
|
|
3130
|
+
const activeWriters = /* @__PURE__ */ new Map();
|
|
3131
|
+
const activeReaders = /* @__PURE__ */ new Map();
|
|
3132
|
+
cachedStepFn.getWritable = (options2) => {
|
|
3133
|
+
const namespace = options2?.namespace ?? "default";
|
|
3134
|
+
const highWaterMark = options2?.highWaterMark ?? 16;
|
|
3135
|
+
if (!streamStore) {
|
|
3136
|
+
throw new Error(
|
|
3137
|
+
"streamStore is required to use getWritable(). Pass a streamStore to createWorkflow options."
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
const existingKey = `${workflowId}:${namespace}`;
|
|
3141
|
+
const existing = activeWriters.get(existingKey);
|
|
3142
|
+
if (existing && !existing.closed && !existing.aborted) {
|
|
3143
|
+
return existing.writer;
|
|
3144
|
+
}
|
|
3145
|
+
const backpressure = createBackpressureController({
|
|
3146
|
+
highWaterMark,
|
|
3147
|
+
onStateChange: (state) => {
|
|
3148
|
+
emitEvent({
|
|
3149
|
+
type: "stream_backpressure",
|
|
3150
|
+
workflowId,
|
|
3151
|
+
namespace,
|
|
3152
|
+
bufferedCount: backpressure.bufferedCount,
|
|
3153
|
+
state,
|
|
3154
|
+
ts: Date.now()
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
3158
|
+
let position = 0;
|
|
3159
|
+
let writable = true;
|
|
3160
|
+
let aborted = false;
|
|
3161
|
+
let closed = false;
|
|
3162
|
+
emitEvent({
|
|
3163
|
+
type: "stream_created",
|
|
3164
|
+
workflowId,
|
|
3165
|
+
namespace,
|
|
3166
|
+
ts: Date.now()
|
|
3167
|
+
});
|
|
3168
|
+
const writer = {
|
|
3169
|
+
async write(value) {
|
|
3170
|
+
if (closed) {
|
|
3171
|
+
return err(streamWriteError("closed", "Stream is closed"));
|
|
3172
|
+
}
|
|
3173
|
+
if (aborted) {
|
|
3174
|
+
return err(streamWriteError("aborted", "Stream was aborted"));
|
|
3175
|
+
}
|
|
3176
|
+
if (backpressure.state === "paused") {
|
|
3177
|
+
await backpressure.waitForDrain();
|
|
3178
|
+
}
|
|
3179
|
+
const item = {
|
|
3180
|
+
value,
|
|
3181
|
+
position,
|
|
3182
|
+
ts: Date.now()
|
|
3183
|
+
};
|
|
3184
|
+
const result2 = await streamStore.append(workflowId, namespace, item);
|
|
3185
|
+
if (!result2.ok) {
|
|
3186
|
+
emitEvent({
|
|
3187
|
+
type: "stream_error",
|
|
3188
|
+
workflowId,
|
|
3189
|
+
namespace,
|
|
3190
|
+
error: result2.error,
|
|
3191
|
+
position,
|
|
3192
|
+
ts: Date.now()
|
|
3193
|
+
});
|
|
3194
|
+
return err(streamWriteError("store_error", result2.error.message, result2.error));
|
|
3195
|
+
}
|
|
3196
|
+
emitEvent({
|
|
3197
|
+
type: "stream_write",
|
|
3198
|
+
workflowId,
|
|
3199
|
+
namespace,
|
|
3200
|
+
position,
|
|
3201
|
+
ts: Date.now()
|
|
3202
|
+
});
|
|
3203
|
+
position++;
|
|
3204
|
+
backpressure.increment();
|
|
3205
|
+
return ok2(void 0);
|
|
3206
|
+
},
|
|
3207
|
+
async close() {
|
|
3208
|
+
if (closed) {
|
|
3209
|
+
return err(streamCloseError("already_closed", "Stream is already closed"));
|
|
3210
|
+
}
|
|
3211
|
+
const result2 = await streamStore.closeStream(workflowId, namespace);
|
|
3212
|
+
if (!result2.ok) {
|
|
3213
|
+
return err(streamCloseError("store_error", result2.error.message, result2.error));
|
|
3214
|
+
}
|
|
3215
|
+
closed = true;
|
|
3216
|
+
writable = false;
|
|
3217
|
+
emitEvent({
|
|
3218
|
+
type: "stream_close",
|
|
3219
|
+
workflowId,
|
|
3220
|
+
namespace,
|
|
3221
|
+
finalPosition: position,
|
|
3222
|
+
ts: Date.now()
|
|
3223
|
+
});
|
|
3224
|
+
activeWriters.delete(existingKey);
|
|
3225
|
+
return ok2(void 0);
|
|
3226
|
+
},
|
|
3227
|
+
abort(reason) {
|
|
3228
|
+
aborted = true;
|
|
3229
|
+
writable = false;
|
|
3230
|
+
closed = true;
|
|
3231
|
+
emitEvent({
|
|
3232
|
+
type: "stream_error",
|
|
3233
|
+
workflowId,
|
|
3234
|
+
namespace,
|
|
3235
|
+
error: reason,
|
|
3236
|
+
position,
|
|
3237
|
+
ts: Date.now()
|
|
3238
|
+
});
|
|
3239
|
+
activeWriters.delete(existingKey);
|
|
3240
|
+
},
|
|
3241
|
+
get writable() {
|
|
3242
|
+
return writable;
|
|
3243
|
+
},
|
|
3244
|
+
get position() {
|
|
3245
|
+
return position;
|
|
3246
|
+
},
|
|
3247
|
+
get namespace() {
|
|
3248
|
+
return namespace;
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
activeWriters.set(existingKey, {
|
|
3252
|
+
writer,
|
|
3253
|
+
backpressure,
|
|
3254
|
+
aborted,
|
|
3255
|
+
closed
|
|
3256
|
+
});
|
|
3257
|
+
return writer;
|
|
3258
|
+
};
|
|
3259
|
+
cachedStepFn.getReadable = (options2) => {
|
|
3260
|
+
const namespace = options2?.namespace ?? "default";
|
|
3261
|
+
const startIndex = options2?.startIndex ?? 0;
|
|
3262
|
+
const pollInterval = options2?.pollInterval ?? 10;
|
|
3263
|
+
const pollTimeout = options2?.pollTimeout ?? 3e4;
|
|
3264
|
+
if (!streamStore) {
|
|
3265
|
+
throw new Error(
|
|
3266
|
+
"streamStore is required to use getReadable(). Pass a streamStore to createWorkflow options."
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
const existingKey = `${workflowId}:${namespace}:${startIndex}`;
|
|
3270
|
+
const existing = activeReaders.get(existingKey);
|
|
3271
|
+
if (existing && !existing.closed) {
|
|
3272
|
+
return existing.reader;
|
|
3273
|
+
}
|
|
3274
|
+
const decrementBackpressure = () => {
|
|
3275
|
+
const writerKey = `${workflowId}:${namespace}`;
|
|
3276
|
+
const activeWriter = activeWriters.get(writerKey);
|
|
3277
|
+
if (activeWriter) {
|
|
3278
|
+
activeWriter.backpressure.decrement();
|
|
3279
|
+
}
|
|
3280
|
+
};
|
|
3281
|
+
let position = startIndex;
|
|
3282
|
+
let readable = true;
|
|
3283
|
+
let closed = false;
|
|
3284
|
+
let bufferedItems = [];
|
|
3285
|
+
let bufferIndex = 0;
|
|
3286
|
+
const reader = {
|
|
3287
|
+
async read() {
|
|
3288
|
+
if (closed) {
|
|
3289
|
+
return err(streamReadError("closed", "Reader is closed"));
|
|
3290
|
+
}
|
|
3291
|
+
if (bufferIndex < bufferedItems.length) {
|
|
3292
|
+
const item = bufferedItems[bufferIndex++];
|
|
3293
|
+
position = item.position + 1;
|
|
3294
|
+
decrementBackpressure();
|
|
3295
|
+
emitEvent({
|
|
3296
|
+
type: "stream_read",
|
|
3297
|
+
workflowId,
|
|
3298
|
+
namespace,
|
|
3299
|
+
position: item.position,
|
|
3300
|
+
ts: Date.now()
|
|
3301
|
+
});
|
|
3302
|
+
return ok2(item.value);
|
|
3303
|
+
}
|
|
3304
|
+
const writerKey = `${workflowId}:${namespace}`;
|
|
3305
|
+
const pollStart = Date.now();
|
|
3306
|
+
let hasSeenWriter = activeWriters.has(writerKey);
|
|
3307
|
+
const initialMetaResult = await streamStore.getMetadata(workflowId, namespace);
|
|
3308
|
+
let hasSeenMetadata = initialMetaResult.ok && initialMetaResult.value !== void 0;
|
|
3309
|
+
while (Date.now() - pollStart < pollTimeout) {
|
|
3310
|
+
const result2 = await streamStore.read(workflowId, namespace, position, 100);
|
|
3311
|
+
if (!result2.ok) {
|
|
3312
|
+
return err(streamReadError("store_error", result2.error.message, result2.error));
|
|
3313
|
+
}
|
|
3314
|
+
const items = result2.value;
|
|
3315
|
+
if (items.length > 0) {
|
|
3316
|
+
bufferedItems = items;
|
|
3317
|
+
bufferIndex = 1;
|
|
3318
|
+
const item = items[0];
|
|
3319
|
+
position = item.position + 1;
|
|
3320
|
+
decrementBackpressure();
|
|
3321
|
+
emitEvent({
|
|
3322
|
+
type: "stream_read",
|
|
3323
|
+
workflowId,
|
|
3324
|
+
namespace,
|
|
3325
|
+
position: item.position,
|
|
3326
|
+
ts: Date.now()
|
|
3327
|
+
});
|
|
3328
|
+
return ok2(item.value);
|
|
3329
|
+
}
|
|
3330
|
+
const writerActive = activeWriters.has(writerKey);
|
|
3331
|
+
const metaResult = await streamStore.getMetadata(workflowId, namespace);
|
|
3332
|
+
const metadataExists = metaResult.ok && metaResult.value !== void 0;
|
|
3333
|
+
if (writerActive) hasSeenWriter = true;
|
|
3334
|
+
if (metadataExists) hasSeenMetadata = true;
|
|
3335
|
+
if (metaResult.ok && metaResult.value?.closed) {
|
|
3336
|
+
readable = false;
|
|
3337
|
+
return err(streamEnded(position));
|
|
3338
|
+
}
|
|
3339
|
+
if (hasSeenWriter && !writerActive && !metadataExists) {
|
|
3340
|
+
readable = false;
|
|
3341
|
+
return err(streamEnded(position));
|
|
3342
|
+
}
|
|
3343
|
+
if (hasSeenMetadata && !writerActive && metaResult.ok && !metaResult.value?.closed) {
|
|
3344
|
+
}
|
|
3345
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
3346
|
+
}
|
|
3347
|
+
readable = false;
|
|
3348
|
+
return err(streamEnded(position));
|
|
3349
|
+
},
|
|
3350
|
+
close() {
|
|
3351
|
+
closed = true;
|
|
3352
|
+
readable = false;
|
|
3353
|
+
bufferedItems = [];
|
|
3354
|
+
activeReaders.delete(existingKey);
|
|
3355
|
+
},
|
|
3356
|
+
get readable() {
|
|
3357
|
+
return readable;
|
|
3358
|
+
},
|
|
3359
|
+
get position() {
|
|
3360
|
+
return position;
|
|
3361
|
+
},
|
|
3362
|
+
get namespace() {
|
|
3363
|
+
return namespace;
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
activeReaders.set(existingKey, {
|
|
3367
|
+
reader,
|
|
3368
|
+
position,
|
|
3369
|
+
closed
|
|
3370
|
+
});
|
|
3371
|
+
return reader;
|
|
3372
|
+
};
|
|
3373
|
+
cachedStepFn.streamForEach = async (source, processor, options2) => {
|
|
3374
|
+
const checkpointInterval = options2?.checkpointInterval ?? 1;
|
|
3375
|
+
const concurrency = options2?.concurrency ?? 1;
|
|
3376
|
+
const results = [];
|
|
3377
|
+
let processedCount = 0;
|
|
3378
|
+
let lastPosition = -1;
|
|
3379
|
+
const isStreamReader = (s) => {
|
|
3380
|
+
return typeof s === "object" && s !== null && "read" in s && typeof s.read === "function";
|
|
3381
|
+
};
|
|
3382
|
+
const processItem = async (item, itemIndex, itemPosition, namespace) => {
|
|
3383
|
+
const shouldCheckpoint = checkpointInterval > 0 && itemIndex % checkpointInterval === 0;
|
|
3384
|
+
const stepKey = shouldCheckpoint ? `stream-foreach:${namespace}:pos-${itemPosition}` : void 0;
|
|
3385
|
+
const stepId = stepKey ?? `stream-item-${itemPosition}`;
|
|
3386
|
+
const stepResult = await cachedStepFn(
|
|
3387
|
+
stepId,
|
|
3388
|
+
() => processor(item, itemIndex),
|
|
3389
|
+
{ key: stepKey }
|
|
3390
|
+
);
|
|
3391
|
+
return { index: itemIndex, position: itemPosition, result: stepResult };
|
|
3392
|
+
};
|
|
3393
|
+
if (isStreamReader(source)) {
|
|
3394
|
+
if (concurrency <= 1) {
|
|
3395
|
+
let itemPosition = source.position;
|
|
3396
|
+
let readResult = await source.read();
|
|
3397
|
+
while (readResult.ok) {
|
|
3398
|
+
const item = readResult.value;
|
|
3399
|
+
const { result: result2 } = await processItem(item, processedCount, itemPosition, source.namespace);
|
|
3400
|
+
results.push(result2);
|
|
3401
|
+
lastPosition = itemPosition;
|
|
3402
|
+
processedCount++;
|
|
3403
|
+
itemPosition = source.position;
|
|
3404
|
+
readResult = await source.read();
|
|
3405
|
+
}
|
|
3406
|
+
} else {
|
|
3407
|
+
const resultsMap = /* @__PURE__ */ new Map();
|
|
3408
|
+
let itemIndex = 0;
|
|
3409
|
+
let totalItems = 0;
|
|
3410
|
+
const slots = new Array(concurrency).fill(null);
|
|
3411
|
+
const getSlot = async () => {
|
|
3412
|
+
for (let i = 0; i < slots.length; i++) {
|
|
3413
|
+
if (slots[i] === null) return i;
|
|
3414
|
+
}
|
|
3415
|
+
const activePromises = slots.filter((s) => s !== null);
|
|
3416
|
+
const completed = await Promise.race(activePromises);
|
|
3417
|
+
resultsMap.set(completed.index, { position: completed.position, result: completed.result });
|
|
3418
|
+
slots[completed.slotIndex] = null;
|
|
3419
|
+
return completed.slotIndex;
|
|
3420
|
+
};
|
|
3421
|
+
let itemPosition = source.position;
|
|
3422
|
+
let readResult = await source.read();
|
|
3423
|
+
while (readResult.ok) {
|
|
3424
|
+
const slotIndex = await getSlot();
|
|
3425
|
+
const currentIndex = itemIndex;
|
|
3426
|
+
const currentPosition = itemPosition;
|
|
3427
|
+
const currentItem = readResult.value;
|
|
3428
|
+
const currentSlot = slotIndex;
|
|
3429
|
+
slots[slotIndex] = processItem(currentItem, currentIndex, currentPosition, source.namespace).then((r) => ({ slotIndex: currentSlot, ...r }));
|
|
3430
|
+
totalItems++;
|
|
3431
|
+
itemIndex++;
|
|
3432
|
+
itemPosition = source.position;
|
|
3433
|
+
readResult = await source.read();
|
|
3434
|
+
}
|
|
3435
|
+
for (let i = 0; i < slots.length; i++) {
|
|
3436
|
+
if (slots[i] !== null) {
|
|
3437
|
+
const result2 = await slots[i];
|
|
3438
|
+
resultsMap.set(result2.index, { position: result2.position, result: result2.result });
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
for (let i = 0; i < totalItems; i++) {
|
|
3442
|
+
const entry = resultsMap.get(i);
|
|
3443
|
+
if (entry) {
|
|
3444
|
+
results.push(entry.result);
|
|
3445
|
+
lastPosition = entry.position;
|
|
3446
|
+
processedCount++;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
} else {
|
|
3451
|
+
if (concurrency <= 1) {
|
|
3452
|
+
let index = 0;
|
|
3453
|
+
for await (const item of source) {
|
|
3454
|
+
const { result: result2 } = await processItem(item, index, index, "async-iterable");
|
|
3455
|
+
results.push(result2);
|
|
3456
|
+
lastPosition = index;
|
|
3457
|
+
processedCount++;
|
|
3458
|
+
index++;
|
|
3459
|
+
}
|
|
3460
|
+
} else {
|
|
3461
|
+
const resultsMap = /* @__PURE__ */ new Map();
|
|
3462
|
+
let itemIndex = 0;
|
|
3463
|
+
let totalItems = 0;
|
|
3464
|
+
const slots = new Array(concurrency).fill(null);
|
|
3465
|
+
const getSlot = async () => {
|
|
3466
|
+
for (let i = 0; i < slots.length; i++) {
|
|
3467
|
+
if (slots[i] === null) return i;
|
|
3468
|
+
}
|
|
3469
|
+
const activePromises = slots.filter((s) => s !== null);
|
|
3470
|
+
const completed = await Promise.race(activePromises);
|
|
3471
|
+
resultsMap.set(completed.index, completed.result);
|
|
3472
|
+
slots[completed.slotIndex] = null;
|
|
3473
|
+
return completed.slotIndex;
|
|
3474
|
+
};
|
|
3475
|
+
for await (const item of source) {
|
|
3476
|
+
const slotIndex = await getSlot();
|
|
3477
|
+
const currentIndex = itemIndex;
|
|
3478
|
+
const currentSlot = slotIndex;
|
|
3479
|
+
slots[slotIndex] = processItem(item, currentIndex, currentIndex, "async-iterable").then((r) => ({ slotIndex: currentSlot, ...r }));
|
|
3480
|
+
totalItems++;
|
|
3481
|
+
itemIndex++;
|
|
3482
|
+
}
|
|
3483
|
+
for (let i = 0; i < slots.length; i++) {
|
|
3484
|
+
if (slots[i] !== null) {
|
|
3485
|
+
const result2 = await slots[i];
|
|
3486
|
+
resultsMap.set(result2.index, result2.result);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
for (let i = 0; i < totalItems; i++) {
|
|
3490
|
+
if (resultsMap.has(i)) {
|
|
3491
|
+
results.push(resultsMap.get(i));
|
|
3492
|
+
lastPosition = i;
|
|
3493
|
+
processedCount++;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
return {
|
|
3499
|
+
results,
|
|
3500
|
+
processedCount,
|
|
3501
|
+
lastPosition
|
|
3502
|
+
};
|
|
3503
|
+
};
|
|
3504
|
+
cachedStepFn.if = realStep.if;
|
|
3505
|
+
cachedStepFn.label = realStep.label;
|
|
3506
|
+
cachedStepFn.branch = realStep.branch;
|
|
3507
|
+
cachedStepFn.arm = realStep.arm;
|
|
3508
|
+
cachedStepFn.forEach = realStep.forEach;
|
|
3509
|
+
cachedStepFn.item = realStep.item;
|
|
3510
|
+
cachedStepFn.dep = realStep.dep;
|
|
3511
|
+
cachedStepFn.workflow = (id, getter, options2) => cachedStepFn(id, getter, options2);
|
|
3512
|
+
cachedStepFn.all = (id, shape, options2) => {
|
|
3513
|
+
const opts = options2 !== void 0 && Object.prototype.hasOwnProperty.call(options2, "key") ? options2 : { ...options2, key: void 0 };
|
|
3514
|
+
return cachedStepFn(id, async () => ok2(await realStep.all(id, shape)), opts);
|
|
3515
|
+
};
|
|
3516
|
+
cachedStepFn.map = (id, items, mapper, options2) => {
|
|
3517
|
+
const opts = options2 !== void 0 && Object.prototype.hasOwnProperty.call(options2, "key") ? options2 : { ...options2, key: void 0 };
|
|
3518
|
+
return cachedStepFn(id, async () => ok2(await realStep.map(id, items, mapper, options2)), opts);
|
|
3519
|
+
};
|
|
3520
|
+
return cachedStepFn;
|
|
3521
|
+
};
|
|
3522
|
+
const wrappedFn = ({ step }) => {
|
|
3523
|
+
const cachedStep = createCachedStep(step);
|
|
3524
|
+
return userFn({
|
|
3525
|
+
step: cachedStep,
|
|
3526
|
+
steps: bindSteps(effectiveDeps ?? {}, cachedStep),
|
|
3527
|
+
deps: effectiveDeps,
|
|
3528
|
+
ctx: workflowContext
|
|
3529
|
+
});
|
|
3530
|
+
};
|
|
3531
|
+
let result;
|
|
3532
|
+
try {
|
|
3533
|
+
result = await run(wrappedFn, {
|
|
3534
|
+
onError: onErrorHandler,
|
|
3535
|
+
onEvent: onEventHandler,
|
|
3536
|
+
catchUnexpected,
|
|
3537
|
+
workflowId,
|
|
3538
|
+
workflowName,
|
|
3539
|
+
context,
|
|
3540
|
+
graph: declaredGraph,
|
|
3541
|
+
_workflowSignal: workflowSignal
|
|
3542
|
+
});
|
|
3543
|
+
} finally {
|
|
3544
|
+
if (workflowSignal) {
|
|
3545
|
+
workflowSignal.removeEventListener("abort", abortHandler);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
if (!result.ok && compensations.length > 0) {
|
|
3549
|
+
const compensationErrors = [];
|
|
3550
|
+
for (let i = compensations.length - 1; i >= 0; i--) {
|
|
3551
|
+
const comp = compensations[i];
|
|
3552
|
+
try {
|
|
3553
|
+
await comp.compensate(comp.value);
|
|
3554
|
+
} catch (compErr) {
|
|
3555
|
+
compensationErrors.push({ stepName: comp.stepName, error: compErr });
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
if (compensationErrors.length > 0) {
|
|
3559
|
+
const sagaError = {
|
|
3560
|
+
type: "SAGA_COMPENSATION_ERROR",
|
|
3561
|
+
originalError: result.error,
|
|
3562
|
+
compensationErrors
|
|
3563
|
+
};
|
|
3564
|
+
result = err(sagaError, { cause: result.cause });
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
const durationMs = performance.now() - startTime;
|
|
3568
|
+
if (!result.ok) {
|
|
3569
|
+
let cancelledError;
|
|
3570
|
+
if (isWorkflowCancelled(result.cause)) {
|
|
3571
|
+
cancelledError = result.cause;
|
|
3572
|
+
}
|
|
3573
|
+
if (!cancelledError && abortedDuringExecution && isUnexpectedError(result.error)) {
|
|
3574
|
+
const thrown = result.cause;
|
|
3575
|
+
const isAbortError = thrown != null && typeof thrown === "object" && "name" in thrown && thrown.name === "AbortError";
|
|
3576
|
+
if (isAbortError) {
|
|
3577
|
+
const reason = abortReason ?? (typeof workflowSignal?.reason === "string" ? workflowSignal.reason : workflowSignal?.reason instanceof Error ? workflowSignal.reason.message : void 0);
|
|
3578
|
+
cancelledError = {
|
|
3579
|
+
type: "WORKFLOW_CANCELLED",
|
|
3580
|
+
reason,
|
|
3581
|
+
lastStepKey
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
if (cancelledError) {
|
|
3586
|
+
emitEvent({
|
|
3587
|
+
type: "workflow_cancelled",
|
|
3588
|
+
workflowId,
|
|
3589
|
+
ts: Date.now(),
|
|
3590
|
+
durationMs,
|
|
3591
|
+
reason: cancelledError.reason,
|
|
3592
|
+
lastStepKey: cancelledError.lastStepKey
|
|
3593
|
+
});
|
|
3594
|
+
if (cancelledError && !isWorkflowCancelled(result.cause)) {
|
|
3595
|
+
return err(result.error, { cause: cancelledError });
|
|
3596
|
+
}
|
|
3597
|
+
return result;
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
if (result.ok && abortedDuringExecution) {
|
|
3601
|
+
const reason = abortReason ?? (typeof workflowSignal?.reason === "string" ? workflowSignal.reason : workflowSignal?.reason instanceof Error ? workflowSignal.reason.message : void 0);
|
|
3602
|
+
emitEvent({
|
|
3603
|
+
type: "workflow_cancelled",
|
|
3604
|
+
workflowId,
|
|
3605
|
+
ts: Date.now(),
|
|
3606
|
+
durationMs,
|
|
3607
|
+
reason,
|
|
3608
|
+
lastStepKey
|
|
3609
|
+
});
|
|
3610
|
+
const cancelledError = {
|
|
3611
|
+
type: "WORKFLOW_CANCELLED",
|
|
3612
|
+
reason,
|
|
3613
|
+
lastStepKey
|
|
3614
|
+
};
|
|
3615
|
+
return err(catchUnexpected(cancelledError), { cause: cancelledError });
|
|
3616
|
+
}
|
|
3617
|
+
if (result.ok) {
|
|
3618
|
+
emitEvent({
|
|
3619
|
+
type: "workflow_success",
|
|
3620
|
+
workflowId,
|
|
3621
|
+
ts: Date.now(),
|
|
3622
|
+
durationMs
|
|
3623
|
+
});
|
|
3624
|
+
} else {
|
|
3625
|
+
emitEvent({
|
|
3626
|
+
type: "workflow_error",
|
|
3627
|
+
workflowId,
|
|
3628
|
+
ts: Date.now(),
|
|
3629
|
+
durationMs,
|
|
3630
|
+
error: result.error
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
return result;
|
|
3634
|
+
}
|
|
3635
|
+
function runMethod(fnOrName, maybeFnOrConfig, maybeConfig) {
|
|
3636
|
+
let runName;
|
|
3637
|
+
let fn;
|
|
3638
|
+
let config;
|
|
3639
|
+
if (typeof fnOrName === "string") {
|
|
3640
|
+
runName = fnOrName;
|
|
3641
|
+
fn = maybeFnOrConfig;
|
|
3642
|
+
config = maybeConfig;
|
|
3643
|
+
} else {
|
|
3644
|
+
fn = fnOrName;
|
|
3645
|
+
config = maybeFnOrConfig;
|
|
3646
|
+
}
|
|
3647
|
+
return internalExecute(runName, fn, config);
|
|
3648
|
+
}
|
|
3649
|
+
async function runWithStateMethod(fnOrName, maybeFnOrConfig, maybeConfig) {
|
|
3650
|
+
let runName;
|
|
3651
|
+
let fn;
|
|
3652
|
+
let config;
|
|
3653
|
+
if (typeof fnOrName === "string") {
|
|
3654
|
+
runName = fnOrName;
|
|
3655
|
+
fn = maybeFnOrConfig;
|
|
3656
|
+
config = maybeConfig;
|
|
3657
|
+
} else {
|
|
3658
|
+
fn = fnOrName;
|
|
3659
|
+
config = maybeFnOrConfig;
|
|
3660
|
+
}
|
|
3661
|
+
const collector = createResumeStateCollector();
|
|
3662
|
+
const userOnEvent = config?.onEvent;
|
|
3663
|
+
const mergedOnEvent = (event, ctx) => {
|
|
3664
|
+
collector.handleEvent(event);
|
|
3665
|
+
try {
|
|
3666
|
+
userOnEvent?.(event, ctx);
|
|
3667
|
+
} catch {
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
const mergedConfig = {
|
|
3671
|
+
...config,
|
|
3672
|
+
onEvent: mergedOnEvent
|
|
3673
|
+
};
|
|
3674
|
+
let result;
|
|
3675
|
+
let resumeState;
|
|
3676
|
+
try {
|
|
3677
|
+
result = await internalExecute(runName, fn, mergedConfig);
|
|
3678
|
+
} catch (thrown) {
|
|
3679
|
+
const catchUnexpected = optionsActual?.catchUnexpected ?? defaultCatchUnexpected;
|
|
3680
|
+
result = err(catchUnexpected(thrown), { cause: thrown });
|
|
3681
|
+
} finally {
|
|
3682
|
+
resumeState = collector.getResumeState();
|
|
3683
|
+
}
|
|
3684
|
+
return { result, resumeState };
|
|
3685
|
+
}
|
|
3686
|
+
const workflow = {
|
|
3687
|
+
withDeps: (overrides) => withDeps(workflow, overrides),
|
|
3688
|
+
run: runMethod,
|
|
3689
|
+
runWithState: runWithStateMethod
|
|
3690
|
+
};
|
|
3691
|
+
return workflow;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// src/durable/index.ts
|
|
3695
|
+
var defaultStore;
|
|
3696
|
+
function createMemorySnapshotStore() {
|
|
3697
|
+
const store = /* @__PURE__ */ new Map();
|
|
3698
|
+
return {
|
|
3699
|
+
async save(id, snapshot) {
|
|
3700
|
+
store.set(id, { snapshot, updatedAt: /* @__PURE__ */ new Date() });
|
|
3701
|
+
},
|
|
3702
|
+
async load(id) {
|
|
3703
|
+
const entry = store.get(id);
|
|
3704
|
+
return entry?.snapshot ?? null;
|
|
3705
|
+
},
|
|
3706
|
+
async delete(id) {
|
|
3707
|
+
store.delete(id);
|
|
3708
|
+
},
|
|
3709
|
+
async list(options) {
|
|
3710
|
+
const prefix = options?.prefix ?? "";
|
|
3711
|
+
const limit = options?.limit ?? 100;
|
|
3712
|
+
const results = [];
|
|
3713
|
+
for (const [id, entry] of store.entries()) {
|
|
3714
|
+
if (prefix && !id.startsWith(prefix)) continue;
|
|
3715
|
+
results.push({ id, updatedAt: entry.updatedAt.toISOString() });
|
|
3716
|
+
if (results.length >= limit) break;
|
|
3717
|
+
}
|
|
3718
|
+
results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
3719
|
+
return results;
|
|
3720
|
+
},
|
|
3721
|
+
async close() {
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
function getDefaultStore() {
|
|
3726
|
+
if (defaultStore === void 0) {
|
|
3727
|
+
defaultStore = createMemorySnapshotStore();
|
|
3728
|
+
}
|
|
3729
|
+
return defaultStore;
|
|
3730
|
+
}
|
|
3731
|
+
function hasWorkflowLock(store) {
|
|
3732
|
+
return typeof store.tryAcquire === "function" && typeof store.release === "function";
|
|
3733
|
+
}
|
|
3734
|
+
var activeWorkflows = /* @__PURE__ */ new Set();
|
|
3735
|
+
var pendingIdempotencyRuns = /* @__PURE__ */ new Map();
|
|
3736
|
+
var durable = {
|
|
3737
|
+
/**
|
|
3738
|
+
* Execute a workflow with automatic state persistence.
|
|
3739
|
+
*
|
|
3740
|
+
* Features:
|
|
3741
|
+
* - **Automatic checkpointing**: State is saved after each keyed step
|
|
3742
|
+
* - **Crash recovery**: Resume from the last completed step on restart
|
|
3743
|
+
* - **Version checking**: Reject resume if workflow logic version changed
|
|
3744
|
+
* - **Concurrency control**: Prevent duplicate executions of the same workflow ID
|
|
3745
|
+
* - **Cancellation support**: Integrates with AbortSignal for graceful shutdown
|
|
3746
|
+
*
|
|
3747
|
+
* ## How It Works
|
|
3748
|
+
*
|
|
3749
|
+
* 1. On start: Load existing state from store (if any)
|
|
3750
|
+
* 2. Check version compatibility (reject if mismatch)
|
|
3751
|
+
* 3. Pre-populate cache from loaded state (skip completed steps)
|
|
3752
|
+
* 4. Execute workflow, persisting state after each keyed step
|
|
3753
|
+
* 5. On completion: Delete stored state (clean up)
|
|
3754
|
+
* 6. On error/cancellation: State remains for future resume
|
|
3755
|
+
*
|
|
3756
|
+
* ## Important Notes
|
|
3757
|
+
*
|
|
3758
|
+
* - **Only keyed steps are durable**: Use `{ key: 'step-name' }` option
|
|
3759
|
+
* - **Steps should be idempotent**: They may be retried on resume
|
|
3760
|
+
* - **Serialization**: State is JSON-serialized; complex objects may lose fidelity
|
|
3761
|
+
*
|
|
3762
|
+
* @param deps - Workflow dependencies (Result-returning functions)
|
|
3763
|
+
* @param fn - Workflow function receiving ({ step, deps, ctx })
|
|
3764
|
+
* @param options - Durable execution options
|
|
3765
|
+
* @returns AsyncResult with workflow result or error
|
|
3766
|
+
*
|
|
3767
|
+
* @example
|
|
3768
|
+
* ```typescript
|
|
3769
|
+
* import { durable } from 'awaitly/durable';
|
|
3770
|
+
*
|
|
3771
|
+
* // Zero-config: uses in-memory store (per process)
|
|
3772
|
+
* const result = await durable.run(
|
|
3773
|
+
* { fetchUser, createOrder, sendEmail },
|
|
3774
|
+
* async ({ step, deps: { fetchUser, createOrder, sendEmail } }) => {
|
|
3775
|
+
* const user = await step(() => fetchUser('123'), { key: 'fetch-user' });
|
|
3776
|
+
* const order = await step(() => createOrder(user), { key: 'create-order' });
|
|
3777
|
+
* await step(() => sendEmail(order), { key: 'send-email' });
|
|
3778
|
+
* return order;
|
|
3779
|
+
* },
|
|
3780
|
+
* { id: 'checkout-123' }
|
|
3781
|
+
* );
|
|
3782
|
+
*
|
|
3783
|
+
* // Persistence across restarts: pass a store adapter
|
|
3784
|
+
* import { postgres } from 'awaitly-postgres'; // or awaitly-mongo, awaitly-libsql
|
|
3785
|
+
* const store = postgres(process.env.DATABASE_URL);
|
|
3786
|
+
* await durable.run(deps, fn, { id: 'checkout-123', store });
|
|
3787
|
+
*
|
|
3788
|
+
* if (result.ok) {
|
|
3789
|
+
* console.log('Order completed:', result.value);
|
|
3790
|
+
* } else if (isWorkflowCancelled(result.error)) {
|
|
3791
|
+
* console.log('Workflow cancelled at:', result.error.lastStepKey);
|
|
3792
|
+
* }
|
|
3793
|
+
* ```
|
|
3794
|
+
*/
|
|
3795
|
+
async run(deps, fn, options) {
|
|
3796
|
+
const {
|
|
3797
|
+
id,
|
|
3798
|
+
store: storeOption,
|
|
3799
|
+
version = 1,
|
|
3800
|
+
allowConcurrent = false,
|
|
3801
|
+
lockTtlMs = 6e4,
|
|
3802
|
+
heartbeatIntervalMs,
|
|
3803
|
+
abortOnLeaseLoss,
|
|
3804
|
+
metadata,
|
|
3805
|
+
signal,
|
|
3806
|
+
createContext,
|
|
3807
|
+
onEvent,
|
|
3808
|
+
onError,
|
|
3809
|
+
onVersionMismatch,
|
|
3810
|
+
idempotencyKey,
|
|
3811
|
+
input
|
|
3812
|
+
} = options;
|
|
3813
|
+
const effectiveStore = storeOption ?? getDefaultStore();
|
|
3814
|
+
let resolveIdempotencyRun;
|
|
3815
|
+
if (idempotencyKey) {
|
|
3816
|
+
const idemId = `idem:${idempotencyKey}`;
|
|
3817
|
+
const pending = pendingIdempotencyRuns.get(idemId);
|
|
3818
|
+
if (pending) {
|
|
3819
|
+
return await pending;
|
|
3820
|
+
}
|
|
3821
|
+
pendingIdempotencyRuns.set(idemId, new Promise((r) => {
|
|
3822
|
+
resolveIdempotencyRun = r;
|
|
3823
|
+
}));
|
|
3824
|
+
try {
|
|
3825
|
+
const idemSnapshot = await effectiveStore.load(idemId);
|
|
3826
|
+
if (idemSnapshot) {
|
|
3827
|
+
if (input !== void 0 && idemSnapshot.metadata?.input !== void 0) {
|
|
3828
|
+
const storedInput = JSON.stringify(idemSnapshot.metadata.input);
|
|
3829
|
+
const currentInput = JSON.stringify(input);
|
|
3830
|
+
if (storedInput !== currentInput) {
|
|
3831
|
+
const result = err({
|
|
3832
|
+
type: "IDEMPOTENCY_CONFLICT",
|
|
3833
|
+
idempotencyKey,
|
|
3834
|
+
workflowId: id,
|
|
3835
|
+
message: `Idempotency key '${idempotencyKey}' already used with different input for workflow '${id}'.`
|
|
3836
|
+
});
|
|
3837
|
+
resolveIdempotencyRun(result);
|
|
3838
|
+
pendingIdempotencyRuns.delete(idemId);
|
|
3839
|
+
resolveIdempotencyRun = void 0;
|
|
3840
|
+
return result;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
if (idemSnapshot.execution.status === "completed" && idemSnapshot.metadata?.finalResult !== void 0) {
|
|
3844
|
+
const result = idemSnapshot.metadata.finalResult;
|
|
3845
|
+
resolveIdempotencyRun(result);
|
|
3846
|
+
pendingIdempotencyRuns.delete(idemId);
|
|
3847
|
+
resolveIdempotencyRun = void 0;
|
|
3848
|
+
return result;
|
|
3849
|
+
}
|
|
3850
|
+
if (idemSnapshot.execution.status === "running") {
|
|
3851
|
+
const result = err({
|
|
3852
|
+
type: "CONCURRENT_EXECUTION",
|
|
3853
|
+
workflowId: id,
|
|
3854
|
+
message: `Workflow '${id}' with idempotency key '${idempotencyKey}' is already running.`,
|
|
3855
|
+
reason: "cross-process"
|
|
3856
|
+
});
|
|
3857
|
+
resolveIdempotencyRun(result);
|
|
3858
|
+
pendingIdempotencyRuns.delete(idemId);
|
|
3859
|
+
resolveIdempotencyRun = void 0;
|
|
3860
|
+
return result;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
} catch {
|
|
3864
|
+
}
|
|
3865
|
+
try {
|
|
3866
|
+
await effectiveStore.save(idemId, {
|
|
3867
|
+
formatVersion: 1,
|
|
3868
|
+
steps: {},
|
|
3869
|
+
execution: {
|
|
3870
|
+
status: "running",
|
|
3871
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
3872
|
+
},
|
|
3873
|
+
metadata: {
|
|
3874
|
+
workflowId: id,
|
|
3875
|
+
idempotencyKey,
|
|
3876
|
+
input
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
} catch {
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
if (!allowConcurrent && activeWorkflows.has(id)) {
|
|
3883
|
+
const error = {
|
|
3884
|
+
type: "CONCURRENT_EXECUTION",
|
|
3885
|
+
workflowId: id,
|
|
3886
|
+
message: `Workflow '${id}' is already running. Set allowConcurrent: true to allow parallel executions.`,
|
|
3887
|
+
reason: "in-process"
|
|
3888
|
+
};
|
|
3889
|
+
return err(error);
|
|
3890
|
+
}
|
|
3891
|
+
let leaseOwnerToken = null;
|
|
3892
|
+
if (!allowConcurrent && hasWorkflowLock(effectiveStore)) {
|
|
3893
|
+
let lease;
|
|
3894
|
+
try {
|
|
3895
|
+
lease = await effectiveStore.tryAcquire(id, { ttlMs: lockTtlMs });
|
|
3896
|
+
} catch (lockError) {
|
|
3897
|
+
const error = {
|
|
3898
|
+
type: "PERSISTENCE_ERROR",
|
|
3899
|
+
operation: "load",
|
|
3900
|
+
workflowId: id,
|
|
3901
|
+
cause: lockError,
|
|
3902
|
+
message: `Failed to acquire lock for workflow '${id}': ${lockError instanceof Error ? lockError.message : String(lockError)}`
|
|
3903
|
+
};
|
|
3904
|
+
return err(error);
|
|
3905
|
+
}
|
|
3906
|
+
if (lease === null) {
|
|
3907
|
+
const error = {
|
|
3908
|
+
type: "CONCURRENT_EXECUTION",
|
|
3909
|
+
workflowId: id,
|
|
3910
|
+
message: `Workflow '${id}' is already running (lease held by another process). Set allowConcurrent: true to allow parallel executions.`,
|
|
3911
|
+
reason: "cross-process"
|
|
3912
|
+
};
|
|
3913
|
+
return err(error);
|
|
3914
|
+
}
|
|
3915
|
+
leaseOwnerToken = lease.ownerToken;
|
|
3916
|
+
}
|
|
3917
|
+
let heartbeatTimer;
|
|
3918
|
+
let leaseAbortController;
|
|
3919
|
+
const lockStore = effectiveStore;
|
|
3920
|
+
if (leaseOwnerToken && typeof lockStore.renew === "function") {
|
|
3921
|
+
const heartbeatMs = heartbeatIntervalMs ?? Math.floor(lockTtlMs / 3);
|
|
3922
|
+
leaseAbortController = new AbortController();
|
|
3923
|
+
heartbeatTimer = setInterval(async () => {
|
|
3924
|
+
try {
|
|
3925
|
+
const renewed = await lockStore.renew(id, leaseOwnerToken, { ttlMs: lockTtlMs });
|
|
3926
|
+
if (!renewed) {
|
|
3927
|
+
leaseAbortController.abort(new Error("Lease expired"));
|
|
3928
|
+
}
|
|
3929
|
+
} catch {
|
|
3930
|
+
leaseAbortController.abort(new Error("Lease renewal failed"));
|
|
3931
|
+
}
|
|
3932
|
+
}, heartbeatMs);
|
|
3933
|
+
}
|
|
3934
|
+
activeWorkflows.add(id);
|
|
3935
|
+
let durableResult;
|
|
3936
|
+
try {
|
|
3937
|
+
let existingSnapshot = null;
|
|
3938
|
+
try {
|
|
3939
|
+
existingSnapshot = await effectiveStore.load(id);
|
|
3940
|
+
} catch (loadError) {
|
|
3941
|
+
const error = {
|
|
3942
|
+
type: "PERSISTENCE_ERROR",
|
|
3943
|
+
operation: "load",
|
|
3944
|
+
workflowId: id,
|
|
3945
|
+
cause: loadError,
|
|
3946
|
+
message: `Failed to load state for workflow '${id}': ${loadError instanceof Error ? loadError.message : String(loadError)}`
|
|
3947
|
+
};
|
|
3948
|
+
durableResult = err(error);
|
|
3949
|
+
return err(error);
|
|
3950
|
+
}
|
|
3951
|
+
if (existingSnapshot) {
|
|
3952
|
+
try {
|
|
3953
|
+
assertValidSnapshot(existingSnapshot);
|
|
3954
|
+
} catch (validationError) {
|
|
3955
|
+
if (validationError instanceof SnapshotFormatError) {
|
|
3956
|
+
const error = {
|
|
3957
|
+
type: "PERSISTENCE_ERROR",
|
|
3958
|
+
operation: "load",
|
|
3959
|
+
workflowId: id,
|
|
3960
|
+
cause: validationError,
|
|
3961
|
+
message: `Invalid snapshot format for workflow '${id}': ${validationError.message}`
|
|
3962
|
+
};
|
|
3963
|
+
durableResult = err(error);
|
|
3964
|
+
return err(error);
|
|
3965
|
+
}
|
|
3966
|
+
throw validationError;
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
if (existingSnapshot) {
|
|
3970
|
+
const storedVersion = typeof existingSnapshot.metadata?.version === "number" ? existingSnapshot.metadata.version : 1;
|
|
3971
|
+
if (storedVersion !== version) {
|
|
3972
|
+
const error = {
|
|
3973
|
+
type: "VERSION_MISMATCH",
|
|
3974
|
+
workflowId: id,
|
|
3975
|
+
storedVersion,
|
|
3976
|
+
requestedVersion: version,
|
|
3977
|
+
currentVersion: version,
|
|
3978
|
+
message: `Workflow '${id}' has stored state at version ${storedVersion} but this run requested version ${version}. Migrate the stored state to the new version, or clear state for this id (e.g. durable.deleteState(store, '${id}')) and re-run.`
|
|
3979
|
+
};
|
|
3980
|
+
if (!onVersionMismatch) {
|
|
3981
|
+
durableResult = err(error);
|
|
3982
|
+
return err(error);
|
|
3983
|
+
}
|
|
3984
|
+
const resolution = await Promise.resolve(
|
|
3985
|
+
onVersionMismatch({ id, storedVersion, requestedVersion: version })
|
|
3986
|
+
);
|
|
3987
|
+
if (resolution === "throw") {
|
|
3988
|
+
durableResult = err(error);
|
|
3989
|
+
return err(error);
|
|
3990
|
+
}
|
|
3991
|
+
if (resolution === "clear") {
|
|
3992
|
+
try {
|
|
3993
|
+
await effectiveStore.delete(id);
|
|
3994
|
+
} catch {
|
|
3995
|
+
}
|
|
3996
|
+
existingSnapshot = null;
|
|
3997
|
+
} else {
|
|
3998
|
+
existingSnapshot = resolution.migratedSnapshot;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
const emitDurableEvent = (event, ctx) => {
|
|
4003
|
+
if (onEvent) {
|
|
4004
|
+
onEvent(event, ctx);
|
|
4005
|
+
}
|
|
4006
|
+
};
|
|
4007
|
+
const resumeCollector = createResumeStateCollector();
|
|
4008
|
+
const workflowOptions = {
|
|
4009
|
+
// Restore from existing snapshot
|
|
4010
|
+
snapshot: existingSnapshot,
|
|
4011
|
+
// Persist after each keyed step
|
|
4012
|
+
onAfterStep: async (stepKey, result2, wfId, ctx) => {
|
|
4013
|
+
try {
|
|
4014
|
+
const collectedState = resumeCollector.getResumeState();
|
|
4015
|
+
const steps = {};
|
|
4016
|
+
for (const [key, entry] of collectedState.steps) {
|
|
4017
|
+
if (entry.result.ok) {
|
|
4018
|
+
steps[key] = { ok: true, value: entry.result.value };
|
|
4019
|
+
} else {
|
|
4020
|
+
const cause = entry.result.cause;
|
|
4021
|
+
const serializedCause = cause instanceof Error ? serializeError(cause) : serializeThrown(cause);
|
|
4022
|
+
const origin = entry.meta?.origin === "throw" ? "throw" : "result";
|
|
4023
|
+
steps[key] = {
|
|
4024
|
+
ok: false,
|
|
4025
|
+
error: entry.result.error,
|
|
4026
|
+
cause: serializedCause,
|
|
4027
|
+
meta: { origin }
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
const currentSnapshot = {
|
|
4032
|
+
formatVersion: 1,
|
|
4033
|
+
workflowName: id,
|
|
4034
|
+
steps,
|
|
4035
|
+
execution: {
|
|
4036
|
+
status: "running",
|
|
4037
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4038
|
+
currentStepId: stepKey
|
|
4039
|
+
},
|
|
4040
|
+
metadata: {
|
|
4041
|
+
...existingSnapshot?.metadata ?? {},
|
|
4042
|
+
...metadata,
|
|
4043
|
+
version,
|
|
4044
|
+
lastStepKey: stepKey
|
|
4045
|
+
}
|
|
4046
|
+
};
|
|
4047
|
+
let snapshotToSave = existingSnapshot ? mergeSnapshots(existingSnapshot, currentSnapshot) : currentSnapshot;
|
|
4048
|
+
if (snapshotToSave.warnings && snapshotToSave.warnings.length > 0) {
|
|
4049
|
+
const currentStepKeys = new Set(Object.keys(currentSnapshot.steps));
|
|
4050
|
+
const filtered = snapshotToSave.warnings.filter(
|
|
4051
|
+
(w) => !currentStepKeys.has(w.stepId)
|
|
4052
|
+
);
|
|
4053
|
+
snapshotToSave = {
|
|
4054
|
+
...snapshotToSave,
|
|
4055
|
+
warnings: filtered.length > 0 ? filtered : void 0
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
await effectiveStore.save(id, snapshotToSave);
|
|
4059
|
+
emitDurableEvent(
|
|
4060
|
+
{
|
|
4061
|
+
type: "persist_success",
|
|
4062
|
+
workflowId: wfId,
|
|
4063
|
+
stepKey,
|
|
4064
|
+
ts: Date.now(),
|
|
4065
|
+
context: ctx
|
|
4066
|
+
},
|
|
4067
|
+
ctx
|
|
4068
|
+
);
|
|
4069
|
+
} catch (persistError) {
|
|
4070
|
+
emitDurableEvent(
|
|
4071
|
+
{
|
|
4072
|
+
type: "persist_error",
|
|
4073
|
+
workflowId: wfId,
|
|
4074
|
+
stepKey,
|
|
4075
|
+
error: persistError,
|
|
4076
|
+
ts: Date.now(),
|
|
4077
|
+
context: ctx
|
|
4078
|
+
},
|
|
4079
|
+
ctx
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4082
|
+
},
|
|
4083
|
+
// Forward events and collect step results for snapshot building
|
|
4084
|
+
onEvent: (event, ctx) => {
|
|
4085
|
+
resumeCollector.handleEvent(event);
|
|
4086
|
+
emitDurableEvent(event, ctx);
|
|
4087
|
+
},
|
|
4088
|
+
onError,
|
|
4089
|
+
signal: leaseAbortController && signal ? AbortSignal.any([signal, leaseAbortController.signal]) : leaseAbortController?.signal ?? signal,
|
|
4090
|
+
createContext
|
|
4091
|
+
};
|
|
4092
|
+
let workflowInstance;
|
|
4093
|
+
try {
|
|
4094
|
+
workflowInstance = createWorkflow(id, deps, workflowOptions);
|
|
4095
|
+
} catch (createError) {
|
|
4096
|
+
if (createError instanceof SnapshotFormatError) {
|
|
4097
|
+
const error = {
|
|
4098
|
+
type: "PERSISTENCE_ERROR",
|
|
4099
|
+
operation: "load",
|
|
4100
|
+
workflowId: id,
|
|
4101
|
+
cause: createError,
|
|
4102
|
+
message: `Invalid snapshot format for workflow '${id}': ${createError.message}`
|
|
4103
|
+
};
|
|
4104
|
+
durableResult = err(error);
|
|
4105
|
+
return err(error);
|
|
4106
|
+
}
|
|
4107
|
+
throw createError;
|
|
4108
|
+
}
|
|
4109
|
+
let result;
|
|
4110
|
+
try {
|
|
4111
|
+
result = await workflowInstance.run(fn);
|
|
4112
|
+
} catch (runError) {
|
|
4113
|
+
if (runError instanceof SnapshotFormatError || runError instanceof SnapshotDecodeError) {
|
|
4114
|
+
const error = {
|
|
4115
|
+
type: "PERSISTENCE_ERROR",
|
|
4116
|
+
operation: "load",
|
|
4117
|
+
workflowId: id,
|
|
4118
|
+
cause: runError,
|
|
4119
|
+
message: `Invalid snapshot format for workflow '${id}': ${runError.message}`
|
|
4120
|
+
};
|
|
4121
|
+
durableResult = err(error);
|
|
4122
|
+
return err(error);
|
|
4123
|
+
}
|
|
4124
|
+
throw runError;
|
|
4125
|
+
}
|
|
4126
|
+
if (abortOnLeaseLoss !== false && leaseAbortController?.signal.aborted) {
|
|
4127
|
+
const leaseErr = err({
|
|
4128
|
+
type: "LEASE_EXPIRED",
|
|
4129
|
+
workflowId: id,
|
|
4130
|
+
message: `Lease expired for workflow '${id}' during execution. The workflow may have been reclaimed by another process.`
|
|
4131
|
+
});
|
|
4132
|
+
durableResult = leaseErr;
|
|
4133
|
+
return leaseErr;
|
|
4134
|
+
}
|
|
4135
|
+
if (result.ok) {
|
|
4136
|
+
try {
|
|
4137
|
+
await effectiveStore.delete(id);
|
|
4138
|
+
} catch (deleteError) {
|
|
4139
|
+
const error = {
|
|
4140
|
+
type: "PERSISTENCE_ERROR",
|
|
4141
|
+
operation: "delete",
|
|
4142
|
+
workflowId: id,
|
|
4143
|
+
cause: deleteError,
|
|
4144
|
+
message: `Failed to delete state for workflow '${id}': ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
|
|
4145
|
+
};
|
|
4146
|
+
durableResult = err(error);
|
|
4147
|
+
return err(error);
|
|
4148
|
+
}
|
|
4149
|
+
if (idempotencyKey) {
|
|
4150
|
+
const idemId = `idem:${idempotencyKey}`;
|
|
4151
|
+
try {
|
|
4152
|
+
await effectiveStore.save(idemId, {
|
|
4153
|
+
formatVersion: 1,
|
|
4154
|
+
steps: {},
|
|
4155
|
+
execution: {
|
|
4156
|
+
status: "completed",
|
|
4157
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4158
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4159
|
+
},
|
|
4160
|
+
metadata: {
|
|
4161
|
+
workflowId: id,
|
|
4162
|
+
idempotencyKey,
|
|
4163
|
+
input,
|
|
4164
|
+
finalResult: result
|
|
4165
|
+
}
|
|
4166
|
+
});
|
|
4167
|
+
} catch {
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
durableResult = result;
|
|
4172
|
+
return result;
|
|
4173
|
+
} finally {
|
|
4174
|
+
activeWorkflows.delete(id);
|
|
4175
|
+
if (resolveIdempotencyRun) {
|
|
4176
|
+
resolveIdempotencyRun(durableResult);
|
|
4177
|
+
pendingIdempotencyRuns.delete(`idem:${idempotencyKey}`);
|
|
4178
|
+
}
|
|
4179
|
+
if (heartbeatTimer) {
|
|
4180
|
+
clearInterval(heartbeatTimer);
|
|
4181
|
+
}
|
|
4182
|
+
if (leaseOwnerToken !== null && hasWorkflowLock(effectiveStore)) {
|
|
4183
|
+
try {
|
|
4184
|
+
await effectiveStore.release(id, leaseOwnerToken);
|
|
4185
|
+
} catch {
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
},
|
|
4190
|
+
/**
|
|
4191
|
+
* Clear all persisted workflow state from the store.
|
|
4192
|
+
* Use for admin/testing. If the store implements `clear()`, that is used;
|
|
4193
|
+
* otherwise clears by listing and deleting in pages.
|
|
4194
|
+
*
|
|
4195
|
+
* @param store - Snapshot store
|
|
4196
|
+
*/
|
|
4197
|
+
async clearState(store) {
|
|
4198
|
+
const storeWithClear = store;
|
|
4199
|
+
if (typeof storeWithClear.clear === "function") {
|
|
4200
|
+
await storeWithClear.clear();
|
|
4201
|
+
return;
|
|
4202
|
+
}
|
|
4203
|
+
const limit = 100;
|
|
4204
|
+
for (; ; ) {
|
|
4205
|
+
const entries = await store.list({ limit });
|
|
4206
|
+
if (entries.length === 0) break;
|
|
4207
|
+
const ids = entries.map((e) => e.id);
|
|
4208
|
+
await this.deleteStates(store, ids, { continueOnError: true });
|
|
4209
|
+
if (entries.length < limit) break;
|
|
4210
|
+
}
|
|
4211
|
+
},
|
|
4212
|
+
/**
|
|
4213
|
+
* Check if a workflow ID has persisted state (can be resumed).
|
|
4214
|
+
*
|
|
4215
|
+
* @param store - Snapshot store
|
|
4216
|
+
* @param id - Workflow execution ID
|
|
4217
|
+
* @returns `true` if state exists, `false` otherwise (including on store errors)
|
|
4218
|
+
*/
|
|
4219
|
+
async hasState(store, id) {
|
|
4220
|
+
try {
|
|
4221
|
+
const snapshot = await store.load(id);
|
|
4222
|
+
return snapshot !== null;
|
|
4223
|
+
} catch {
|
|
4224
|
+
return false;
|
|
4225
|
+
}
|
|
4226
|
+
},
|
|
4227
|
+
/**
|
|
4228
|
+
* Delete persisted state for a workflow (cancel resume capability).
|
|
4229
|
+
* Deleting is effectively an ack/reset: the workflow can no longer resume from that state.
|
|
4230
|
+
* If you delete while a run is in flight, the run continues; on success it may delete again (no-op) or save (recreating state).
|
|
4231
|
+
* For multi-worker safety, prefer deleting only when the workflow is not running or when you hold the lock.
|
|
4232
|
+
*
|
|
4233
|
+
* @param store - Snapshot store
|
|
4234
|
+
* @param id - Workflow execution ID
|
|
4235
|
+
* @returns `true` on success, `false` on store errors
|
|
4236
|
+
*/
|
|
4237
|
+
async deleteState(store, id) {
|
|
4238
|
+
try {
|
|
4239
|
+
await store.delete(id);
|
|
4240
|
+
return true;
|
|
4241
|
+
} catch {
|
|
4242
|
+
return false;
|
|
4243
|
+
}
|
|
4244
|
+
},
|
|
4245
|
+
/**
|
|
4246
|
+
* Bulk delete persisted state for multiple workflow IDs (best-effort).
|
|
4247
|
+
* Use for admin/cleanup. Deletes in a loop with optional concurrency.
|
|
4248
|
+
*
|
|
4249
|
+
* @param store - Snapshot store
|
|
4250
|
+
* @param ids - Workflow execution IDs to delete
|
|
4251
|
+
* @param options - Optional concurrency and error handling
|
|
4252
|
+
* @returns Count of deleted entries and any errors when continueOnError is true
|
|
4253
|
+
*/
|
|
4254
|
+
async deleteStates(store, ids, options = {}) {
|
|
4255
|
+
const { concurrency = 10, continueOnError = true } = options;
|
|
4256
|
+
if (ids.length === 0) {
|
|
4257
|
+
return { deleted: 0 };
|
|
4258
|
+
}
|
|
4259
|
+
const errors = [];
|
|
4260
|
+
let deleted = 0;
|
|
4261
|
+
const run2 = async (id) => {
|
|
4262
|
+
try {
|
|
4263
|
+
await store.delete(id);
|
|
4264
|
+
deleted++;
|
|
4265
|
+
} catch (error) {
|
|
4266
|
+
if (continueOnError) errors.push({ id, error });
|
|
4267
|
+
else throw error;
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
const limit = Math.max(1, concurrency);
|
|
4271
|
+
for (let i = 0; i < ids.length; i += limit) {
|
|
4272
|
+
const batch = ids.slice(i, i + limit);
|
|
4273
|
+
await Promise.all(batch.map((id) => run2(id)));
|
|
4274
|
+
}
|
|
4275
|
+
return errors.length > 0 ? { deleted, errors } : { deleted };
|
|
4276
|
+
},
|
|
4277
|
+
/**
|
|
4278
|
+
* List workflow IDs with persisted state.
|
|
4279
|
+
*
|
|
4280
|
+
* @param store - Snapshot store
|
|
4281
|
+
* @param options - Optional prefix and limit
|
|
4282
|
+
* @returns Array of { id, updatedAt } entries
|
|
4283
|
+
*/
|
|
4284
|
+
async listPending(store, options) {
|
|
4285
|
+
try {
|
|
4286
|
+
return await store.list(options);
|
|
4287
|
+
} catch {
|
|
4288
|
+
return [];
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
};
|
|
4292
|
+
|
|
4293
|
+
// src/engine/index.ts
|
|
4294
|
+
function createEngine(options) {
|
|
4295
|
+
const {
|
|
4296
|
+
store,
|
|
4297
|
+
workflows,
|
|
4298
|
+
concurrency = 5,
|
|
4299
|
+
onEvent,
|
|
4300
|
+
onError
|
|
4301
|
+
} = options;
|
|
4302
|
+
const schedules = /* @__PURE__ */ new Map();
|
|
4303
|
+
let pollTimer;
|
|
4304
|
+
let running = false;
|
|
4305
|
+
let tickInFlight = false;
|
|
4306
|
+
function emit(event) {
|
|
4307
|
+
try {
|
|
4308
|
+
onEvent?.(event);
|
|
4309
|
+
} catch {
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
async function enqueue(name, opts) {
|
|
4313
|
+
const wf = workflows[name];
|
|
4314
|
+
if (!wf) {
|
|
4315
|
+
throw new Error(`Unknown workflow: '${name}'. Registered: ${Object.keys(workflows).join(", ")}`);
|
|
4316
|
+
}
|
|
4317
|
+
const id = opts?.id ?? `${name}:${randomUUID()}`;
|
|
4318
|
+
const snapshot = {
|
|
4319
|
+
formatVersion: 1,
|
|
4320
|
+
workflowName: name,
|
|
4321
|
+
steps: {},
|
|
4322
|
+
execution: {
|
|
4323
|
+
status: "running",
|
|
4324
|
+
// "running" so durable.run can resume it
|
|
4325
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4326
|
+
},
|
|
4327
|
+
metadata: {
|
|
4328
|
+
engineState: "queued",
|
|
4329
|
+
workflowName: name,
|
|
4330
|
+
input: opts?.input ?? null,
|
|
4331
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
4332
|
+
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4333
|
+
...opts?.metadata
|
|
4334
|
+
}
|
|
4335
|
+
};
|
|
4336
|
+
await store.save(id, snapshot);
|
|
4337
|
+
emit({ type: "workflow_enqueued", workflowName: name, id, ts: Date.now() });
|
|
4338
|
+
return id;
|
|
4339
|
+
}
|
|
4340
|
+
async function tick() {
|
|
4341
|
+
if (tickInFlight) return 0;
|
|
4342
|
+
tickInFlight = true;
|
|
4343
|
+
try {
|
|
4344
|
+
const entries = await store.list({ limit: concurrency * 2 });
|
|
4345
|
+
let processed = 0;
|
|
4346
|
+
const queued = [];
|
|
4347
|
+
for (const entry of entries) {
|
|
4348
|
+
if (queued.length >= concurrency) break;
|
|
4349
|
+
const snapshot = await store.load(entry.id);
|
|
4350
|
+
if (snapshot && snapshot.metadata?.engineState === "queued" && typeof snapshot.metadata?.workflowName === "string") {
|
|
4351
|
+
queued.push({ id: entry.id, workflowName: snapshot.metadata.workflowName, snapshot });
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
const results = await Promise.allSettled(
|
|
4355
|
+
queued.map(async ({ id, workflowName, snapshot }) => {
|
|
4356
|
+
const wf = workflows[workflowName];
|
|
4357
|
+
if (!wf) return;
|
|
4358
|
+
const processingSnapshot = {
|
|
4359
|
+
...snapshot,
|
|
4360
|
+
metadata: {
|
|
4361
|
+
...snapshot.metadata,
|
|
4362
|
+
engineState: "processing"
|
|
4363
|
+
}
|
|
4364
|
+
};
|
|
4365
|
+
await store.save(id, processingSnapshot);
|
|
4366
|
+
emit({ type: "workflow_started", workflowName, id, ts: Date.now() });
|
|
4367
|
+
const result = await durable.run(wf.deps, wf.fn, {
|
|
4368
|
+
id,
|
|
4369
|
+
store,
|
|
4370
|
+
idempotencyKey: snapshot.metadata?.idempotencyKey,
|
|
4371
|
+
input: snapshot.metadata?.input,
|
|
4372
|
+
...wf.durableDefaults
|
|
4373
|
+
});
|
|
4374
|
+
if (result.ok) {
|
|
4375
|
+
emit({ type: "workflow_completed", workflowName, id, ts: Date.now() });
|
|
4376
|
+
} else {
|
|
4377
|
+
emit({ type: "workflow_failed", workflowName, id, error: result.error, ts: Date.now() });
|
|
4378
|
+
}
|
|
4379
|
+
processed++;
|
|
4380
|
+
})
|
|
4381
|
+
);
|
|
4382
|
+
for (const r of results) {
|
|
4383
|
+
if (r.status === "rejected") {
|
|
4384
|
+
try {
|
|
4385
|
+
onError?.(r.reason);
|
|
4386
|
+
} catch {
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
emit({ type: "engine_tick", ts: Date.now(), processed });
|
|
4391
|
+
return processed;
|
|
4392
|
+
} finally {
|
|
4393
|
+
tickInFlight = false;
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
function schedule(name, opts) {
|
|
4397
|
+
const scheduleId = opts?.id ? `schedule:${opts.id}` : `schedule:${randomUUID()}`;
|
|
4398
|
+
if (opts.immediate) {
|
|
4399
|
+
void enqueue(name, opts).catch((e) => {
|
|
4400
|
+
try {
|
|
4401
|
+
onError?.(e);
|
|
4402
|
+
} catch {
|
|
4403
|
+
}
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
const interval = setInterval(() => {
|
|
4407
|
+
void enqueue(name, opts).catch((e) => {
|
|
4408
|
+
try {
|
|
4409
|
+
onError?.(e);
|
|
4410
|
+
} catch {
|
|
4411
|
+
}
|
|
4412
|
+
});
|
|
4413
|
+
}, opts.intervalMs);
|
|
4414
|
+
schedules.set(scheduleId, interval);
|
|
4415
|
+
emit({ type: "schedule_created", workflowName: name, scheduleId, intervalMs: opts.intervalMs, ts: Date.now() });
|
|
4416
|
+
return scheduleId;
|
|
4417
|
+
}
|
|
4418
|
+
function unschedule(scheduleId) {
|
|
4419
|
+
const interval = schedules.get(scheduleId);
|
|
4420
|
+
if (!interval) return false;
|
|
4421
|
+
clearInterval(interval);
|
|
4422
|
+
schedules.delete(scheduleId);
|
|
4423
|
+
emit({ type: "schedule_removed", scheduleId, ts: Date.now() });
|
|
4424
|
+
return true;
|
|
4425
|
+
}
|
|
4426
|
+
function start(pollIntervalMs = 1e3) {
|
|
4427
|
+
if (running) return;
|
|
4428
|
+
running = true;
|
|
4429
|
+
emit({ type: "engine_start", ts: Date.now() });
|
|
4430
|
+
void tick().catch((e) => {
|
|
4431
|
+
try {
|
|
4432
|
+
onError?.(e);
|
|
4433
|
+
} catch {
|
|
4434
|
+
}
|
|
4435
|
+
});
|
|
4436
|
+
pollTimer = setInterval(() => {
|
|
4437
|
+
void tick().catch((e) => {
|
|
4438
|
+
try {
|
|
4439
|
+
onError?.(e);
|
|
4440
|
+
} catch {
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
}, pollIntervalMs);
|
|
4444
|
+
}
|
|
4445
|
+
async function stop() {
|
|
4446
|
+
if (!running) return;
|
|
4447
|
+
running = false;
|
|
4448
|
+
if (pollTimer) {
|
|
4449
|
+
clearInterval(pollTimer);
|
|
4450
|
+
pollTimer = void 0;
|
|
4451
|
+
}
|
|
4452
|
+
for (const [id, interval] of schedules) {
|
|
4453
|
+
clearInterval(interval);
|
|
4454
|
+
schedules.delete(id);
|
|
4455
|
+
}
|
|
4456
|
+
while (tickInFlight) {
|
|
4457
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
4458
|
+
}
|
|
4459
|
+
emit({ type: "engine_stop", ts: Date.now() });
|
|
4460
|
+
}
|
|
4461
|
+
return {
|
|
4462
|
+
enqueue,
|
|
4463
|
+
schedule,
|
|
4464
|
+
unschedule,
|
|
4465
|
+
start,
|
|
4466
|
+
stop,
|
|
4467
|
+
tick,
|
|
4468
|
+
status: () => ({ running, pendingSchedules: schedules.size })
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
export {
|
|
4472
|
+
createEngine
|
|
4473
|
+
};
|
|
4474
|
+
//# sourceMappingURL=engine.js.map
|