@yieldstar/core 0.5.0 → 0.6.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/base/durable-value.d.ts +71 -0
- package/dist/base/durable-value.d.ts.map +1 -0
- package/dist/base/durable-value.test.d.ts +2 -0
- package/dist/base/durable-value.test.d.ts.map +1 -0
- package/dist/base/effect.d.ts +136 -0
- package/dist/base/effect.d.ts.map +1 -0
- package/dist/base/errors.d.ts +23 -0
- package/dist/base/errors.d.ts.map +1 -0
- package/dist/base/errors.test.d.ts +2 -0
- package/dist/base/errors.test.d.ts.map +1 -0
- package/dist/base/event.d.ts +6 -1
- package/dist/base/event.d.ts.map +1 -1
- package/dist/base/execution.d.ts +244 -0
- package/dist/base/execution.d.ts.map +1 -0
- package/dist/base/heap.d.ts +15 -3
- package/dist/base/heap.d.ts.map +1 -1
- package/dist/base/invoker.d.ts +47 -2
- package/dist/base/invoker.d.ts.map +1 -1
- package/dist/base/runtime.d.ts +27 -4
- package/dist/base/runtime.d.ts.map +1 -1
- package/dist/base/step.d.ts +21 -5
- package/dist/base/step.d.ts.map +1 -1
- package/dist/base/store.d.ts +135 -21
- package/dist/base/store.d.ts.map +1 -1
- package/dist/base/workflow.d.ts +15 -0
- package/dist/base/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1647 -172
- package/dist/lib/effect-state.d.ts +78 -0
- package/dist/lib/effect-state.d.ts.map +1 -0
- package/dist/lib/execution-dispatcher.d.ts +102 -0
- package/dist/lib/execution-dispatcher.d.ts.map +1 -0
- package/dist/lib/execution-state.d.ts +248 -0
- package/dist/lib/execution-state.d.ts.map +1 -0
- package/dist/lib/execution-state.test.d.ts +2 -0
- package/dist/lib/execution-state.test.d.ts.map +1 -0
- package/dist/lib/wait-for-outcome.d.ts +15 -0
- package/dist/lib/wait-for-outcome.d.ts.map +1 -0
- package/dist/lib/workflow-runner.d.ts +14 -9
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/base/durable-value.test.ts +285 -0
- package/src/base/durable-value.ts +442 -0
- package/src/base/effect.ts +165 -0
- package/src/base/errors.test.ts +210 -0
- package/src/base/errors.ts +72 -0
- package/src/base/event.ts +6 -1
- package/src/base/execution.ts +303 -0
- package/src/base/heap.ts +17 -3
- package/src/base/invoker.ts +60 -2
- package/src/base/runtime.ts +28 -7
- package/src/base/step.ts +29 -7
- package/src/base/store.ts +339 -49
- package/src/base/workflow.ts +15 -0
- package/src/index.ts +137 -3
- package/src/lib/effect-state.ts +254 -0
- package/src/lib/execution-dispatcher.ts +393 -0
- package/src/lib/execution-state.test.ts +249 -0
- package/src/lib/execution-state.ts +650 -0
- package/src/lib/wait-for-outcome.ts +40 -0
- package/src/lib/workflow-runner.ts +75 -43
- package/src/base/scheduler.ts +0 -8
- package/tsconfig.tsbuildinfo +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,1356 @@
|
|
|
1
|
+
// src/base/invoker.ts
|
|
2
|
+
var WORKER_EXITED = Symbol.for("yieldstar.workerExited");
|
|
3
|
+
|
|
4
|
+
class WorkerExitedError extends Error {
|
|
5
|
+
[WORKER_EXITED] = true;
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "WorkerExitedError";
|
|
9
|
+
}
|
|
10
|
+
static is(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && value[WORKER_EXITED] === true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// src/base/runtime.ts
|
|
15
|
+
class Runtime {
|
|
16
|
+
}
|
|
17
|
+
// src/base/execution.ts
|
|
18
|
+
function isLeaseLost(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && value.status === "lost";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class LeaseLostError extends Error {
|
|
23
|
+
executionId;
|
|
24
|
+
constructor(executionId) {
|
|
25
|
+
super(`Lease on execution "${executionId}" was lost`);
|
|
26
|
+
this.name = "LeaseLostError";
|
|
27
|
+
this.executionId = executionId;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class StepConflictError extends Error {
|
|
32
|
+
executionId;
|
|
33
|
+
stepKey;
|
|
34
|
+
constructor(executionId, stepKey) {
|
|
35
|
+
super(`Step "${stepKey}" of execution "${executionId}" is already recorded with a different value`);
|
|
36
|
+
this.name = "StepConflictError";
|
|
37
|
+
this.executionId = executionId;
|
|
38
|
+
this.stepKey = stepKey;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ExecutionAdmissionError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "ExecutionAdmissionError";
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class WorkflowStartError extends Error {
|
|
52
|
+
code;
|
|
53
|
+
constructor(code, message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "WorkflowStartError";
|
|
56
|
+
this.code = code;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
var INFRASTRUCTURE_ERROR = Symbol.for("yieldstar.infrastructureError");
|
|
60
|
+
|
|
61
|
+
class InfrastructureError extends Error {
|
|
62
|
+
[INFRASTRUCTURE_ERROR] = true;
|
|
63
|
+
constructor(cause) {
|
|
64
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
65
|
+
this.name = "InfrastructureError";
|
|
66
|
+
}
|
|
67
|
+
static is(value) {
|
|
68
|
+
return typeof value === "object" && value !== null && value[INFRASTRUCTURE_ERROR] === true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function isTerminalExecutionStatus(status) {
|
|
72
|
+
return status === "completed" || status === "failed";
|
|
73
|
+
}
|
|
74
|
+
// src/base/effect.ts
|
|
75
|
+
class EffectClientInvariantError extends Error {
|
|
76
|
+
code = "EFFECT_RUNTIME_INVARIANT";
|
|
77
|
+
constructor(message, options) {
|
|
78
|
+
super(message, options);
|
|
79
|
+
this.name = "EffectClientInvariantError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class EffectDeliveryError extends Error {
|
|
84
|
+
code;
|
|
85
|
+
effectId;
|
|
86
|
+
stepKey;
|
|
87
|
+
constructor(code, params, options) {
|
|
88
|
+
super(params.message, options);
|
|
89
|
+
this.name = "EffectDeliveryError";
|
|
90
|
+
this.code = code;
|
|
91
|
+
this.effectId = params.effectId;
|
|
92
|
+
this.stepKey = params.stepKey;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class WorkflowEffectError extends Error {
|
|
97
|
+
code;
|
|
98
|
+
constructor(code, message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "WorkflowEffectError";
|
|
101
|
+
this.code = code;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// src/base/errors.ts
|
|
105
|
+
var WORKFLOW_RESULT_STEP_KEY = "$$workflow-result$$";
|
|
106
|
+
|
|
107
|
+
class WorkflowValueError extends Error {
|
|
108
|
+
code;
|
|
109
|
+
stepKey;
|
|
110
|
+
constructor(code, stepKey, message) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.name = "WorkflowValueError";
|
|
113
|
+
this.code = code;
|
|
114
|
+
this.stepKey = stepKey;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
var errorClasses = new Map;
|
|
118
|
+
function registerErrorClass(name, errorClass) {
|
|
119
|
+
if (typeof name !== "string" || name === "") {
|
|
120
|
+
throw new TypeError("Error class name must be a non-empty string");
|
|
121
|
+
}
|
|
122
|
+
if (typeof errorClass !== "function" || !(errorClass.prototype instanceof Error || errorClass === Error)) {
|
|
123
|
+
throw new TypeError(`Error class "${name}" must extend Error`);
|
|
124
|
+
}
|
|
125
|
+
errorClasses.set(name, errorClass);
|
|
126
|
+
}
|
|
127
|
+
function registeredErrorClass(name) {
|
|
128
|
+
return errorClasses.get(name);
|
|
129
|
+
}
|
|
130
|
+
var defaults = [
|
|
131
|
+
["TypeError", TypeError],
|
|
132
|
+
["RangeError", RangeError],
|
|
133
|
+
["SyntaxError", SyntaxError],
|
|
134
|
+
["ReferenceError", ReferenceError],
|
|
135
|
+
["EvalError", EvalError],
|
|
136
|
+
["URIError", URIError],
|
|
137
|
+
["AggregateError", AggregateError],
|
|
138
|
+
["WorkflowStartError", WorkflowStartError],
|
|
139
|
+
["WorkflowValueError", WorkflowValueError],
|
|
140
|
+
["WorkflowEffectError", WorkflowEffectError],
|
|
141
|
+
["EffectClientInvariantError", EffectClientInvariantError],
|
|
142
|
+
["EffectDeliveryError", EffectDeliveryError]
|
|
143
|
+
];
|
|
144
|
+
for (const [name, errorClass] of defaults)
|
|
145
|
+
registerErrorClass(name, errorClass);
|
|
146
|
+
|
|
147
|
+
// src/base/durable-value.ts
|
|
148
|
+
class DurableValueError extends Error {
|
|
149
|
+
}
|
|
150
|
+
var durableValueCodec = {
|
|
151
|
+
encode(value) {
|
|
152
|
+
try {
|
|
153
|
+
return { status: "encoded", canonical: encodeValue(value, "$", new Set) };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (!(err instanceof DurableValueError))
|
|
156
|
+
throw err;
|
|
157
|
+
return { status: "invalid", message: err.message };
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
decode(canonical) {
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(canonical);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return { status: "invalid", message: `not JSON: ${err.message}` };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return { status: "decoded", value: decodeParsedValue(parsed) };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (!(err instanceof DurableValueError))
|
|
171
|
+
throw err;
|
|
172
|
+
return { status: "invalid", message: err.message };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
function encodeThrownValue(value) {
|
|
177
|
+
const encoding = durableValueCodec.encode(value);
|
|
178
|
+
if (encoding.status === "encoded")
|
|
179
|
+
return encoding.canonical;
|
|
180
|
+
let message;
|
|
181
|
+
try {
|
|
182
|
+
message = String(value);
|
|
183
|
+
} catch {
|
|
184
|
+
message = "Thrown value is not a durable value";
|
|
185
|
+
}
|
|
186
|
+
return encodeValue(new Error(message), "$", new Set);
|
|
187
|
+
}
|
|
188
|
+
function isPlainObject(value) {
|
|
189
|
+
if (typeof value !== "object" || value === null)
|
|
190
|
+
return false;
|
|
191
|
+
const proto = Object.getPrototypeOf(value);
|
|
192
|
+
return proto === Object.prototype || proto === null;
|
|
193
|
+
}
|
|
194
|
+
var UNDEFINED_TAG = '{"$type":"undefined"}';
|
|
195
|
+
var ERROR_KEYS = new Set(["name", "message", "stack", "code", "cause"]);
|
|
196
|
+
function encodeValue(value, path, ancestors) {
|
|
197
|
+
if (value === null)
|
|
198
|
+
return "null";
|
|
199
|
+
switch (typeof value) {
|
|
200
|
+
case "undefined":
|
|
201
|
+
return UNDEFINED_TAG;
|
|
202
|
+
case "boolean":
|
|
203
|
+
return value ? "true" : "false";
|
|
204
|
+
case "string":
|
|
205
|
+
return JSON.stringify(value);
|
|
206
|
+
case "number":
|
|
207
|
+
if (!Number.isFinite(value)) {
|
|
208
|
+
throw new DurableValueError(`${path}: ${value} is not a finite number`);
|
|
209
|
+
}
|
|
210
|
+
return JSON.stringify(value);
|
|
211
|
+
case "object":
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
throw new DurableValueError(`${path}: ${typeof value} is not a durable value`);
|
|
215
|
+
}
|
|
216
|
+
if (ancestors.has(value)) {
|
|
217
|
+
throw new DurableValueError(`${path}: reference cycle`);
|
|
218
|
+
}
|
|
219
|
+
ancestors.add(value);
|
|
220
|
+
try {
|
|
221
|
+
if (Array.isArray(value))
|
|
222
|
+
return encodeArray(value, path, ancestors);
|
|
223
|
+
if (value instanceof Error)
|
|
224
|
+
return encodeError(value, path, ancestors);
|
|
225
|
+
return encodeObject(value, path, ancestors);
|
|
226
|
+
} finally {
|
|
227
|
+
ancestors.delete(value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function encodeArray(value, path, ancestors) {
|
|
231
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
232
|
+
throw new DurableValueError(`${path}: arrays must not be subclassed`);
|
|
233
|
+
}
|
|
234
|
+
const items = [];
|
|
235
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
236
|
+
if (key === "length")
|
|
237
|
+
continue;
|
|
238
|
+
if (typeof key === "symbol") {
|
|
239
|
+
throw new DurableValueError(`${path}: symbol properties are not durable`);
|
|
240
|
+
}
|
|
241
|
+
if (!isArrayIndex(key, value.length)) {
|
|
242
|
+
throw new DurableValueError(`${path}.${key}: arrays must only have items`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (let i = 0;i < value.length; i++) {
|
|
246
|
+
const itemPath = `${path}[${i}]`;
|
|
247
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, i);
|
|
248
|
+
if (!descriptor) {
|
|
249
|
+
throw new DurableValueError(`${itemPath}: missing array item`);
|
|
250
|
+
}
|
|
251
|
+
items.push(encodeValue(dataValue(descriptor, itemPath), itemPath, ancestors));
|
|
252
|
+
}
|
|
253
|
+
return `[${items.join(",")}]`;
|
|
254
|
+
}
|
|
255
|
+
function encodeObject(value, path, ancestors) {
|
|
256
|
+
if (!isPlainObject(value)) {
|
|
257
|
+
const name = Object.getPrototypeOf(value)?.constructor?.name ?? "object";
|
|
258
|
+
throw new DurableValueError(`${path}: ${name} instances are not plain objects`);
|
|
259
|
+
}
|
|
260
|
+
const entries = [];
|
|
261
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
262
|
+
if (typeof key === "symbol") {
|
|
263
|
+
throw new DurableValueError(`${path}: symbol properties are not durable`);
|
|
264
|
+
}
|
|
265
|
+
const keyPath = `${path}[${JSON.stringify(key)}]`;
|
|
266
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
267
|
+
entries.push([key, encodeValue(dataValue(descriptor, keyPath), keyPath, ancestors)]);
|
|
268
|
+
}
|
|
269
|
+
return objectLiteral(entries);
|
|
270
|
+
}
|
|
271
|
+
function objectLiteral(entries) {
|
|
272
|
+
const literal = sortedLiteral(entries);
|
|
273
|
+
return entries.some(([key]) => key === "$type") ? `{"$type":"object","value":${literal}}` : literal;
|
|
274
|
+
}
|
|
275
|
+
function sortedLiteral(entries) {
|
|
276
|
+
const sorted = [...entries].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
277
|
+
return `{${sorted.map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(",")}}`;
|
|
278
|
+
}
|
|
279
|
+
function encodeError(error, path, ancestors) {
|
|
280
|
+
const name = readProperty(error, "name");
|
|
281
|
+
const message = readProperty(error, "message");
|
|
282
|
+
const stack = readProperty(error, "stack");
|
|
283
|
+
const code = readProperty(error, "code");
|
|
284
|
+
const tag = [
|
|
285
|
+
["$type", '"error"'],
|
|
286
|
+
["name", JSON.stringify(typeof name === "string" ? name : "Error")],
|
|
287
|
+
["message", JSON.stringify(typeof message === "string" ? message : "")]
|
|
288
|
+
];
|
|
289
|
+
if (typeof stack === "string")
|
|
290
|
+
tag.push(["stack", JSON.stringify(stack)]);
|
|
291
|
+
if (typeof code === "string")
|
|
292
|
+
tag.push(["code", JSON.stringify(code)]);
|
|
293
|
+
if (Object.hasOwn(error, "cause")) {
|
|
294
|
+
const cause = encodeOptional(readProperty(error, "cause"), `${path}.cause`, ancestors);
|
|
295
|
+
if (cause !== undefined)
|
|
296
|
+
tag.push(["cause", cause]);
|
|
297
|
+
}
|
|
298
|
+
const errors = Object.getOwnPropertyDescriptor(error, "errors");
|
|
299
|
+
if (errors && !errors.enumerable && Array.isArray(errors.value)) {
|
|
300
|
+
const encoded = encodeOptional(errors.value, `${path}.errors`, ancestors);
|
|
301
|
+
if (encoded !== undefined)
|
|
302
|
+
tag.push(["errors", encoded]);
|
|
303
|
+
}
|
|
304
|
+
const fields = [];
|
|
305
|
+
for (const key of Object.keys(error)) {
|
|
306
|
+
if (ERROR_KEYS.has(key))
|
|
307
|
+
continue;
|
|
308
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
309
|
+
if (!("value" in descriptor))
|
|
310
|
+
continue;
|
|
311
|
+
const encoded = encodeOptional(descriptor.value, `${path}.${key}`, ancestors);
|
|
312
|
+
if (encoded !== undefined)
|
|
313
|
+
fields.push([key, encoded]);
|
|
314
|
+
}
|
|
315
|
+
if (fields.length > 0)
|
|
316
|
+
tag.push(["fields", objectLiteral(fields)]);
|
|
317
|
+
return sortedLiteral(tag);
|
|
318
|
+
}
|
|
319
|
+
function encodeOptional(value, path, ancestors) {
|
|
320
|
+
try {
|
|
321
|
+
return encodeValue(value, path, ancestors);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (!(err instanceof DurableValueError))
|
|
324
|
+
throw err;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function readProperty(error, key) {
|
|
329
|
+
try {
|
|
330
|
+
return error[key];
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function dataValue(descriptor, path) {
|
|
336
|
+
if (!("value" in descriptor)) {
|
|
337
|
+
throw new DurableValueError(`${path}: accessor properties are not durable`);
|
|
338
|
+
}
|
|
339
|
+
if (!descriptor.enumerable) {
|
|
340
|
+
throw new DurableValueError(`${path}: non-enumerable properties are not durable`);
|
|
341
|
+
}
|
|
342
|
+
return descriptor.value;
|
|
343
|
+
}
|
|
344
|
+
function isArrayIndex(key, length) {
|
|
345
|
+
const index = Number(key);
|
|
346
|
+
return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
|
|
347
|
+
}
|
|
348
|
+
function decodeParsedValue(parsed, path = "$") {
|
|
349
|
+
if (parsed === null)
|
|
350
|
+
return null;
|
|
351
|
+
switch (typeof parsed) {
|
|
352
|
+
case "boolean":
|
|
353
|
+
case "string":
|
|
354
|
+
return parsed;
|
|
355
|
+
case "number":
|
|
356
|
+
if (!Number.isFinite(parsed)) {
|
|
357
|
+
throw new DurableValueError(`${path}: ${parsed} is not a finite number`);
|
|
358
|
+
}
|
|
359
|
+
return parsed;
|
|
360
|
+
case "object":
|
|
361
|
+
break;
|
|
362
|
+
default:
|
|
363
|
+
throw new DurableValueError(`${path}: ${typeof parsed} is not a durable value`);
|
|
364
|
+
}
|
|
365
|
+
if (Array.isArray(parsed)) {
|
|
366
|
+
return parsed.map((item, i) => decodeParsedValue(item, `${path}[${i}]`));
|
|
367
|
+
}
|
|
368
|
+
const object = parsed;
|
|
369
|
+
if (!Object.hasOwn(object, "$type"))
|
|
370
|
+
return decodeObject(object, path);
|
|
371
|
+
switch (object.$type) {
|
|
372
|
+
case "undefined":
|
|
373
|
+
expectKeys(object, path, ["$type"], []);
|
|
374
|
+
return;
|
|
375
|
+
case "object": {
|
|
376
|
+
expectKeys(object, path, ["$type", "value"], []);
|
|
377
|
+
const value = object.value;
|
|
378
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
379
|
+
throw new DurableValueError(`${path}.value: a wrapped object must be an object`);
|
|
380
|
+
}
|
|
381
|
+
return decodeObject(value, `${path}.value`);
|
|
382
|
+
}
|
|
383
|
+
case "error":
|
|
384
|
+
return decodeError(object, path);
|
|
385
|
+
default:
|
|
386
|
+
throw new DurableValueError(`${path}: unknown $type ${JSON.stringify(object.$type)}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function decodeObject(object, path) {
|
|
390
|
+
const decoded = {};
|
|
391
|
+
for (const key of Object.keys(object)) {
|
|
392
|
+
Object.defineProperty(decoded, key, {
|
|
393
|
+
value: decodeParsedValue(object[key], `${path}[${JSON.stringify(key)}]`),
|
|
394
|
+
enumerable: true,
|
|
395
|
+
writable: true,
|
|
396
|
+
configurable: true
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return decoded;
|
|
400
|
+
}
|
|
401
|
+
function decodeError(tag, path) {
|
|
402
|
+
expectKeys(tag, path, ["$type", "name", "message"], ["stack", "code", "cause", "errors", "fields"]);
|
|
403
|
+
const { name, message, stack, code } = tag;
|
|
404
|
+
for (const [key, value, optional] of [
|
|
405
|
+
["name", name, false],
|
|
406
|
+
["message", message, false],
|
|
407
|
+
["stack", stack, true],
|
|
408
|
+
["code", code, true]
|
|
409
|
+
]) {
|
|
410
|
+
if (typeof value !== "string" && !(optional && value === undefined)) {
|
|
411
|
+
throw new DurableValueError(`${path}.${key}: must be a string`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const errorClass = registeredErrorClass(name) ?? Error;
|
|
415
|
+
const error = Reflect.construct(Error, [], errorClass);
|
|
416
|
+
const hidden = (key, value) => Object.defineProperty(error, key, {
|
|
417
|
+
value,
|
|
418
|
+
enumerable: false,
|
|
419
|
+
writable: true,
|
|
420
|
+
configurable: true
|
|
421
|
+
});
|
|
422
|
+
const visible = (key, value) => Object.defineProperty(error, key, {
|
|
423
|
+
value,
|
|
424
|
+
enumerable: true,
|
|
425
|
+
writable: true,
|
|
426
|
+
configurable: true
|
|
427
|
+
});
|
|
428
|
+
if (error.name !== name)
|
|
429
|
+
visible("name", name);
|
|
430
|
+
hidden("message", message);
|
|
431
|
+
if (stack !== undefined)
|
|
432
|
+
hidden("stack", stack);
|
|
433
|
+
else
|
|
434
|
+
delete error.stack;
|
|
435
|
+
if (code !== undefined)
|
|
436
|
+
visible("code", code);
|
|
437
|
+
if (Object.hasOwn(tag, "cause")) {
|
|
438
|
+
hidden("cause", decodeParsedValue(tag.cause, `${path}.cause`));
|
|
439
|
+
}
|
|
440
|
+
if (Object.hasOwn(tag, "errors")) {
|
|
441
|
+
if (!Array.isArray(tag.errors)) {
|
|
442
|
+
throw new DurableValueError(`${path}.errors: must be an array`);
|
|
443
|
+
}
|
|
444
|
+
hidden("errors", decodeParsedValue(tag.errors, `${path}.errors`));
|
|
445
|
+
}
|
|
446
|
+
if (Object.hasOwn(tag, "fields")) {
|
|
447
|
+
const fields = decodeParsedValue(tag.fields, `${path}.fields`);
|
|
448
|
+
if (!isPlainObject(fields)) {
|
|
449
|
+
throw new DurableValueError(`${path}.fields: must be an object`);
|
|
450
|
+
}
|
|
451
|
+
for (const key of Object.keys(fields))
|
|
452
|
+
visible(key, fields[key]);
|
|
453
|
+
}
|
|
454
|
+
return error;
|
|
455
|
+
}
|
|
456
|
+
function expectKeys(object, path, required, optional) {
|
|
457
|
+
for (const key of required) {
|
|
458
|
+
if (!Object.hasOwn(object, key)) {
|
|
459
|
+
throw new DurableValueError(`${path}: tag is missing ${key}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const key of Object.keys(object)) {
|
|
463
|
+
if (!required.includes(key) && !optional.includes(key)) {
|
|
464
|
+
throw new DurableValueError(`${path}: unexpected tag key ${JSON.stringify(key)}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/lib/effect-state.ts
|
|
470
|
+
function validateEffectRequest(request) {
|
|
471
|
+
if (!isNonEmptyString(request.effectId)) {
|
|
472
|
+
return { ok: false, message: "effectId must be a non-empty string" };
|
|
473
|
+
}
|
|
474
|
+
if (!isNonEmptyString(request.operation)) {
|
|
475
|
+
return { ok: false, message: "operation must be a non-empty string" };
|
|
476
|
+
}
|
|
477
|
+
if (!isPlainObject(request.input)) {
|
|
478
|
+
return { ok: false, message: "input must be a plain object" };
|
|
479
|
+
}
|
|
480
|
+
const encoding = durableValueCodec.encode(request.input);
|
|
481
|
+
if (encoding.status === "invalid") {
|
|
482
|
+
return { ok: false, message: `input: ${encoding.message}` };
|
|
483
|
+
}
|
|
484
|
+
return { ok: true, input: encoding.canonical };
|
|
485
|
+
}
|
|
486
|
+
function prepareEffectBegin(request) {
|
|
487
|
+
const validated = validateEffectRequest(request);
|
|
488
|
+
if (!validated.ok)
|
|
489
|
+
throw invariant(`Invalid effect request: ${validated.message}`);
|
|
490
|
+
assertStepId(request);
|
|
491
|
+
return {
|
|
492
|
+
effectId: request.effectId,
|
|
493
|
+
operation: request.operation,
|
|
494
|
+
input: validated.input,
|
|
495
|
+
executionId: request.executionId,
|
|
496
|
+
stepKey: request.stepKey
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function prepareEffectSettle(request) {
|
|
500
|
+
if (!isNonEmptyString(request.effectId)) {
|
|
501
|
+
throw invariant("Invalid effect settlement: effectId must be a non-empty string");
|
|
502
|
+
}
|
|
503
|
+
if (!isNonEmptyString(request.operation)) {
|
|
504
|
+
throw invariant("Invalid effect settlement: operation must be a non-empty string");
|
|
505
|
+
}
|
|
506
|
+
const encoding = durableValueCodec.encode(request.result);
|
|
507
|
+
if (encoding.status === "invalid") {
|
|
508
|
+
throw invariant(`Invalid effect settlement: result: ${encoding.message}`);
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
effectId: request.effectId,
|
|
512
|
+
operation: request.operation,
|
|
513
|
+
result: encoding.canonical
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function prepareEffectWatch(request) {
|
|
517
|
+
if (!isNonEmptyString(request.effectId)) {
|
|
518
|
+
throw invariant("Invalid effect watch: effectId must be a non-empty string");
|
|
519
|
+
}
|
|
520
|
+
assertStepId(request);
|
|
521
|
+
return {
|
|
522
|
+
effectId: request.effectId,
|
|
523
|
+
executionId: request.executionId,
|
|
524
|
+
stepKey: request.stepKey
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function createEffectState(begin) {
|
|
528
|
+
return {
|
|
529
|
+
effectId: begin.effectId,
|
|
530
|
+
operation: begin.operation,
|
|
531
|
+
input: begin.input,
|
|
532
|
+
status: "pending",
|
|
533
|
+
conflicts: []
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function matchEffectBegin(state, begin) {
|
|
537
|
+
if (state.operation !== begin.operation || state.input !== begin.input) {
|
|
538
|
+
return { status: "rejected", code: "EFFECT_REQUEST_MISMATCH" };
|
|
539
|
+
}
|
|
540
|
+
if (state.status === "pending")
|
|
541
|
+
return { status: "pending" };
|
|
542
|
+
return { status: "settled", result: decodeSettledResult(state) };
|
|
543
|
+
}
|
|
544
|
+
function settleEffect(state, settle, now) {
|
|
545
|
+
if (state.operation !== settle.operation) {
|
|
546
|
+
throw invariant(`Cannot settle effect "${state.effectId}" as operation "${settle.operation}": it was begun as "${state.operation}"`);
|
|
547
|
+
}
|
|
548
|
+
if (state.status === "pending") {
|
|
549
|
+
return {
|
|
550
|
+
state: { ...state, status: "settled", result: settle.result },
|
|
551
|
+
result: { status: "settled", result: decodeValue(settle.result, state.effectId) },
|
|
552
|
+
wake: true
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const result = decodeSettledResult(state);
|
|
556
|
+
if (state.result === settle.result) {
|
|
557
|
+
return { state, result: { status: "settled", result }, wake: false };
|
|
558
|
+
}
|
|
559
|
+
const conflictingResult = decodeValue(settle.result, state.effectId);
|
|
560
|
+
const reported = state.conflicts.some((conflict) => conflict.conflictingResult === settle.result);
|
|
561
|
+
return {
|
|
562
|
+
state: reported ? state : {
|
|
563
|
+
...state,
|
|
564
|
+
conflicts: [
|
|
565
|
+
...state.conflicts,
|
|
566
|
+
{ result: state.result, conflictingResult: settle.result, reportedAt: now }
|
|
567
|
+
]
|
|
568
|
+
},
|
|
569
|
+
result: { status: "conflict", result, conflictingResult },
|
|
570
|
+
wake: false
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function toEffectRecord(state) {
|
|
574
|
+
const record = {
|
|
575
|
+
effectId: state.effectId,
|
|
576
|
+
operation: state.operation,
|
|
577
|
+
input: decodeInput(state),
|
|
578
|
+
status: state.status,
|
|
579
|
+
conflicts: state.conflicts.map((conflict) => ({
|
|
580
|
+
result: decodeValue(conflict.result, state.effectId),
|
|
581
|
+
conflictingResult: decodeValue(conflict.conflictingResult, state.effectId),
|
|
582
|
+
reportedAt: conflict.reportedAt
|
|
583
|
+
}))
|
|
584
|
+
};
|
|
585
|
+
if (state.status === "settled")
|
|
586
|
+
record.result = decodeSettledResult(state);
|
|
587
|
+
return record;
|
|
588
|
+
}
|
|
589
|
+
function effectNotFound(effectId) {
|
|
590
|
+
return invariant(`Effect "${effectId}" does not exist: begin it first`);
|
|
591
|
+
}
|
|
592
|
+
function decodeSettledResult(state) {
|
|
593
|
+
if (state.result === undefined) {
|
|
594
|
+
throw invariant(`Settled effect "${state.effectId}" has no stored result`);
|
|
595
|
+
}
|
|
596
|
+
return decodeValue(state.result, state.effectId);
|
|
597
|
+
}
|
|
598
|
+
function decodeInput(state) {
|
|
599
|
+
const input = decodeValue(state.input, state.effectId);
|
|
600
|
+
if (!isPlainObject(input)) {
|
|
601
|
+
throw invariant(`Stored input of effect "${state.effectId}" is not an object`);
|
|
602
|
+
}
|
|
603
|
+
return input;
|
|
604
|
+
}
|
|
605
|
+
function decodeValue(canonical, effectId) {
|
|
606
|
+
const decoded = durableValueCodec.decode(canonical);
|
|
607
|
+
if (decoded.status === "invalid") {
|
|
608
|
+
throw invariant(`Stored value of effect "${effectId}" is invalid: ${decoded.message}`);
|
|
609
|
+
}
|
|
610
|
+
return decoded.value;
|
|
611
|
+
}
|
|
612
|
+
function assertStepId(request) {
|
|
613
|
+
if (!isNonEmptyString(request.executionId)) {
|
|
614
|
+
throw invariant("Invalid effect request: executionId must be a non-empty string");
|
|
615
|
+
}
|
|
616
|
+
if (!isNonEmptyString(request.stepKey)) {
|
|
617
|
+
throw invariant("Invalid effect request: stepKey must be a non-empty string");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function isNonEmptyString(value) {
|
|
621
|
+
return typeof value === "string" && value !== "";
|
|
622
|
+
}
|
|
623
|
+
function invariant(message) {
|
|
624
|
+
return new EffectClientInvariantError(message);
|
|
625
|
+
}
|
|
626
|
+
// src/utils/error.ts
|
|
627
|
+
function errorWithOriginalStack(error, fn) {
|
|
628
|
+
const err = new Error(error.message);
|
|
629
|
+
Error.captureStackTrace(err, fn);
|
|
630
|
+
err.stack = error.stack;
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/utils/map.ts
|
|
635
|
+
class ReadOnlyMap {
|
|
636
|
+
sourceMap;
|
|
637
|
+
constructor(sourceMap) {
|
|
638
|
+
if (sourceMap instanceof Map) {
|
|
639
|
+
this.sourceMap = sourceMap;
|
|
640
|
+
} else if (typeof sourceMap === "object" && sourceMap !== null) {
|
|
641
|
+
this.sourceMap = new Map(Object.entries(sourceMap));
|
|
642
|
+
} else {
|
|
643
|
+
this.sourceMap = new Map;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
get(key) {
|
|
647
|
+
return this.sourceMap.get(key);
|
|
648
|
+
}
|
|
649
|
+
has(key) {
|
|
650
|
+
return this.sourceMap.has(key);
|
|
651
|
+
}
|
|
652
|
+
get size() {
|
|
653
|
+
return this.sourceMap.size;
|
|
654
|
+
}
|
|
655
|
+
entries() {
|
|
656
|
+
return this.sourceMap.entries();
|
|
657
|
+
}
|
|
658
|
+
keys() {
|
|
659
|
+
return this.sourceMap.keys();
|
|
660
|
+
}
|
|
661
|
+
values() {
|
|
662
|
+
return this.sourceMap.values();
|
|
663
|
+
}
|
|
664
|
+
forEach(callbackfn, thisArg) {
|
|
665
|
+
return this.sourceMap.forEach(callbackfn, thisArg);
|
|
666
|
+
}
|
|
667
|
+
[Symbol.iterator]() {
|
|
668
|
+
return this.sourceMap[Symbol.iterator]();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
class FreezableMap extends Map {
|
|
673
|
+
frozen = false;
|
|
674
|
+
freeze() {
|
|
675
|
+
this.frozen = true;
|
|
676
|
+
}
|
|
677
|
+
isFrozen() {
|
|
678
|
+
return this.frozen;
|
|
679
|
+
}
|
|
680
|
+
set(key, value) {
|
|
681
|
+
if (this.frozen) {
|
|
682
|
+
const error = new Error("Cannot call set on a frozen map");
|
|
683
|
+
throw errorWithOriginalStack(error, this.set);
|
|
684
|
+
}
|
|
685
|
+
return super.set(key, value);
|
|
686
|
+
}
|
|
687
|
+
delete(key) {
|
|
688
|
+
if (this.frozen) {
|
|
689
|
+
const error = new Error("Cannot call delete on a frozen map");
|
|
690
|
+
throw errorWithOriginalStack(error, this.delete);
|
|
691
|
+
}
|
|
692
|
+
return super.delete(key);
|
|
693
|
+
}
|
|
694
|
+
clear() {
|
|
695
|
+
if (this.frozen) {
|
|
696
|
+
const error = new Error("Cannot call clear on a frozen map");
|
|
697
|
+
throw errorWithOriginalStack(error, this.clear);
|
|
698
|
+
}
|
|
699
|
+
return super.clear();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/lib/execution-state.ts
|
|
704
|
+
var DEFAULT_LEASE_DURATION = 30000;
|
|
705
|
+
var REDELIVERY_DELAY_BASE = 1000;
|
|
706
|
+
var REDELIVERY_DELAY_MAX = 300000;
|
|
707
|
+
function redeliveryDelay(unacknowledgedDeliveryCount) {
|
|
708
|
+
if (unacknowledgedDeliveryCount <= 0)
|
|
709
|
+
return 0;
|
|
710
|
+
const exponent = Math.min(unacknowledgedDeliveryCount - 1, 30);
|
|
711
|
+
return Math.min(REDELIVERY_DELAY_BASE * 2 ** exponent, REDELIVERY_DELAY_MAX);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
class ExecutionEventEncodingError extends Error {
|
|
715
|
+
constructor(message) {
|
|
716
|
+
super(message);
|
|
717
|
+
this.name = "ExecutionEventEncodingError";
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function encodeExecutionEvent(event) {
|
|
721
|
+
const params = durableValueCodec.encode(event.params);
|
|
722
|
+
if (params.status === "invalid") {
|
|
723
|
+
throw new ExecutionEventEncodingError(`params${params.message.slice(1)}`);
|
|
724
|
+
}
|
|
725
|
+
const entries = [];
|
|
726
|
+
for (const [key, value] of event.context?.entries() ?? []) {
|
|
727
|
+
if (typeof key !== "string") {
|
|
728
|
+
throw new ExecutionEventEncodingError(`context: keys must be strings, got ${typeof key}`);
|
|
729
|
+
}
|
|
730
|
+
entries.push([key, value]);
|
|
731
|
+
}
|
|
732
|
+
const context = durableValueCodec.encode(entries);
|
|
733
|
+
if (context.status === "invalid") {
|
|
734
|
+
throw new ExecutionEventEncodingError(`context${context.message.slice(1)}`);
|
|
735
|
+
}
|
|
736
|
+
return { params: params.canonical, context: context.canonical };
|
|
737
|
+
}
|
|
738
|
+
function decodeExecutionEvent(encoded) {
|
|
739
|
+
const entries = decodeOrThrow(encoded.context, "context");
|
|
740
|
+
if (!Array.isArray(entries) || !entries.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string")) {
|
|
741
|
+
throw new Error("Invalid execution context encoding: expected [key, value] entries");
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
params: decodeOrThrow(encoded.params, "params"),
|
|
745
|
+
context: new ReadOnlyMap(new Map(entries))
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function encodeExecutionOutcome(outcome) {
|
|
749
|
+
if (outcome.status === "completed") {
|
|
750
|
+
const result = durableValueCodec.encode(outcome.result);
|
|
751
|
+
if (result.status === "encoded") {
|
|
752
|
+
return `{"result":${result.canonical},"status":"completed"}`;
|
|
753
|
+
}
|
|
754
|
+
return encodeFailedOutcome(new WorkflowValueError("INVALID_STEP_VALUE", WORKFLOW_RESULT_STEP_KEY, `Workflow result is not a durable value: ${result.message}`));
|
|
755
|
+
}
|
|
756
|
+
return encodeFailedOutcome(outcome.error);
|
|
757
|
+
}
|
|
758
|
+
function encodeFailedOutcome(error) {
|
|
759
|
+
const encoded = encodeThrownValue(error instanceof Error ? error : new Error(safeString(error)));
|
|
760
|
+
return `{"error":${encoded},"status":"failed"}`;
|
|
761
|
+
}
|
|
762
|
+
function decodeExecutionOutcome(encoded) {
|
|
763
|
+
const parsed = JSON.parse(encoded);
|
|
764
|
+
if (parsed.status === "completed") {
|
|
765
|
+
return { status: "completed", result: decodeParsedValue(parsed.result, "result") };
|
|
766
|
+
}
|
|
767
|
+
const error = decodeParsedValue(parsed.error, "error");
|
|
768
|
+
if (!(error instanceof Error)) {
|
|
769
|
+
throw new Error("Invalid failed outcome encoding: expected an error");
|
|
770
|
+
}
|
|
771
|
+
return { status: "failed", error };
|
|
772
|
+
}
|
|
773
|
+
function decodeOrThrow(canonical, label) {
|
|
774
|
+
const decoding = durableValueCodec.decode(canonical);
|
|
775
|
+
if (decoding.status === "invalid") {
|
|
776
|
+
throw new Error(`Invalid ${label} encoding: ${decoding.message}`);
|
|
777
|
+
}
|
|
778
|
+
return decoding.value;
|
|
779
|
+
}
|
|
780
|
+
function safeString(value) {
|
|
781
|
+
try {
|
|
782
|
+
return String(value);
|
|
783
|
+
} catch {
|
|
784
|
+
return "Thrown value is not a durable value";
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function createExecutionState(params) {
|
|
788
|
+
return {
|
|
789
|
+
executionId: params.executionId,
|
|
790
|
+
workflowId: params.workflowId,
|
|
791
|
+
params: params.encoded.params,
|
|
792
|
+
context: params.encoded.context,
|
|
793
|
+
status: "admitted",
|
|
794
|
+
outcome: null,
|
|
795
|
+
admittedAt: params.now,
|
|
796
|
+
updatedAt: params.now,
|
|
797
|
+
dueAt: params.now,
|
|
798
|
+
claimedDueAt: null,
|
|
799
|
+
leaseToken: 0,
|
|
800
|
+
leaseExpiresAt: null,
|
|
801
|
+
unacknowledgedDeliveryCount: 0,
|
|
802
|
+
redeliverAt: null,
|
|
803
|
+
parent: params.parent ?? null
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function hasExpiredLease(state, now) {
|
|
807
|
+
return state.leaseExpiresAt !== null && state.leaseExpiresAt <= now;
|
|
808
|
+
}
|
|
809
|
+
function executionStatusAt(state, now) {
|
|
810
|
+
if (state.leaseExpiresAt !== null && state.leaseExpiresAt > now) {
|
|
811
|
+
return "running";
|
|
812
|
+
}
|
|
813
|
+
return state.status;
|
|
814
|
+
}
|
|
815
|
+
function unacknowledgedDeliveryCountAt(state, now) {
|
|
816
|
+
return state.unacknowledgedDeliveryCount + (hasExpiredLease(state, now) ? 1 : 0);
|
|
817
|
+
}
|
|
818
|
+
function toExecutionRecord(state, now) {
|
|
819
|
+
const event = decodeExecutionEvent(state);
|
|
820
|
+
return {
|
|
821
|
+
executionId: state.executionId,
|
|
822
|
+
workflowId: state.workflowId,
|
|
823
|
+
params: event.params,
|
|
824
|
+
context: event.context,
|
|
825
|
+
status: executionStatusAt(state, now),
|
|
826
|
+
...state.outcome !== null ? { outcome: decodeExecutionOutcome(state.outcome) } : {},
|
|
827
|
+
admittedAt: state.admittedAt,
|
|
828
|
+
updatedAt: state.updatedAt,
|
|
829
|
+
unacknowledgedDeliveryCount: unacknowledgedDeliveryCountAt(state, now),
|
|
830
|
+
...state.parent !== null ? { parent: { ...state.parent } } : {}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function nextClaimAt(state) {
|
|
834
|
+
if (isTerminalExecutionStatus(state.status))
|
|
835
|
+
return null;
|
|
836
|
+
if (state.leaseExpiresAt !== null) {
|
|
837
|
+
const due = earliest(state.dueAt, state.claimedDueAt);
|
|
838
|
+
if (due === null)
|
|
839
|
+
return null;
|
|
840
|
+
return Math.max(due, state.leaseExpiresAt + redeliveryDelay(state.unacknowledgedDeliveryCount + 1));
|
|
841
|
+
}
|
|
842
|
+
if (state.dueAt === null)
|
|
843
|
+
return null;
|
|
844
|
+
return state.redeliverAt === null ? state.dueAt : Math.max(state.dueAt, state.redeliverAt);
|
|
845
|
+
}
|
|
846
|
+
function isExecutionClaimable(state, now) {
|
|
847
|
+
const at = nextClaimAt(state);
|
|
848
|
+
return at !== null && at <= now;
|
|
849
|
+
}
|
|
850
|
+
function isLeaseCurrent(state, lease) {
|
|
851
|
+
return state.executionId === lease.executionId && state.leaseExpiresAt !== null && state.leaseToken === lease.token;
|
|
852
|
+
}
|
|
853
|
+
var LOST = { status: "lost" };
|
|
854
|
+
function requestExecutionWake(state, wakeAt, now) {
|
|
855
|
+
if (isTerminalExecutionStatus(state.status))
|
|
856
|
+
return null;
|
|
857
|
+
return { ...state, dueAt: earliest(state.dueAt, wakeAt), updatedAt: now };
|
|
858
|
+
}
|
|
859
|
+
function claimExecution(state, params) {
|
|
860
|
+
const lease = {
|
|
861
|
+
executionId: state.executionId,
|
|
862
|
+
token: state.leaseToken + 1,
|
|
863
|
+
expiresAt: params.now + params.duration
|
|
864
|
+
};
|
|
865
|
+
return {
|
|
866
|
+
state: {
|
|
867
|
+
...state,
|
|
868
|
+
unacknowledgedDeliveryCount: state.unacknowledgedDeliveryCount + (state.leaseExpiresAt !== null ? 1 : 0),
|
|
869
|
+
claimedDueAt: earliest(state.dueAt, state.claimedDueAt),
|
|
870
|
+
dueAt: null,
|
|
871
|
+
leaseToken: lease.token,
|
|
872
|
+
leaseExpiresAt: lease.expiresAt,
|
|
873
|
+
redeliverAt: null
|
|
874
|
+
},
|
|
875
|
+
lease
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function renewExecutionLease(state, params) {
|
|
879
|
+
if (!isLeaseCurrent(state, params.lease))
|
|
880
|
+
return { state, result: LOST };
|
|
881
|
+
const lease = {
|
|
882
|
+
...params.lease,
|
|
883
|
+
expiresAt: params.now + params.duration
|
|
884
|
+
};
|
|
885
|
+
return {
|
|
886
|
+
state: { ...state, leaseExpiresAt: lease.expiresAt },
|
|
887
|
+
result: { status: "renewed", lease }
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function releaseExecutionLease(state, params) {
|
|
891
|
+
if (!isLeaseCurrent(state, params.lease))
|
|
892
|
+
return { state, result: LOST };
|
|
893
|
+
const unacknowledgedDeliveryCount = state.unacknowledgedDeliveryCount + 1;
|
|
894
|
+
return {
|
|
895
|
+
state: {
|
|
896
|
+
...state,
|
|
897
|
+
dueAt: earliest(state.dueAt, state.claimedDueAt),
|
|
898
|
+
claimedDueAt: null,
|
|
899
|
+
leaseExpiresAt: null,
|
|
900
|
+
unacknowledgedDeliveryCount,
|
|
901
|
+
redeliverAt: params.now + redeliveryDelay(unacknowledgedDeliveryCount)
|
|
902
|
+
},
|
|
903
|
+
result: { status: "released" }
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function acknowledgeExecution(state, params) {
|
|
907
|
+
if (!isLeaseCurrent(state, params.lease))
|
|
908
|
+
return { state, result: LOST };
|
|
909
|
+
if (isTerminalExecutionStatus(state.status)) {
|
|
910
|
+
return { state, result: decodeExecutionOutcome(state.outcome) };
|
|
911
|
+
}
|
|
912
|
+
const { outcome, now } = params;
|
|
913
|
+
const ended = {
|
|
914
|
+
claimedDueAt: null,
|
|
915
|
+
leaseExpiresAt: null,
|
|
916
|
+
unacknowledgedDeliveryCount: 0,
|
|
917
|
+
redeliverAt: null,
|
|
918
|
+
updatedAt: now
|
|
919
|
+
};
|
|
920
|
+
if (outcome.status !== "suspended") {
|
|
921
|
+
const encoded = encodeExecutionOutcome(outcome.status === "completed" ? { status: "completed", result: outcome.result } : { status: "failed", error: outcome.error });
|
|
922
|
+
const terminal = decodeExecutionOutcome(encoded);
|
|
923
|
+
return {
|
|
924
|
+
state: {
|
|
925
|
+
...state,
|
|
926
|
+
...ended,
|
|
927
|
+
status: terminal.status,
|
|
928
|
+
outcome: encoded,
|
|
929
|
+
dueAt: null
|
|
930
|
+
},
|
|
931
|
+
result: terminal
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
state: {
|
|
936
|
+
...state,
|
|
937
|
+
...ended,
|
|
938
|
+
status: "suspended",
|
|
939
|
+
dueAt: earliest(state.dueAt, outcome.wakeAt ?? null)
|
|
940
|
+
},
|
|
941
|
+
result: { status: "suspended" }
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function earliest(a, b) {
|
|
945
|
+
if (a === null)
|
|
946
|
+
return b;
|
|
947
|
+
if (b === null)
|
|
948
|
+
return a;
|
|
949
|
+
return Math.min(a, b);
|
|
950
|
+
}
|
|
951
|
+
function prepareExecutionAdmission(request) {
|
|
952
|
+
if (typeof request.workflowId !== "string" || request.workflowId === "") {
|
|
953
|
+
return {
|
|
954
|
+
ok: false,
|
|
955
|
+
code: "INVALID_EXECUTION_EVENT",
|
|
956
|
+
message: "workflowId must be a non-empty string"
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (request.executionId !== undefined && (typeof request.executionId !== "string" || request.executionId === "")) {
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
code: "INVALID_EXECUTION_EVENT",
|
|
963
|
+
message: "executionId must be a non-empty string"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
return {
|
|
968
|
+
ok: true,
|
|
969
|
+
executionId: request.executionId ?? crypto.randomUUID(),
|
|
970
|
+
encoded: encodeExecutionEvent(request)
|
|
971
|
+
};
|
|
972
|
+
} catch (err) {
|
|
973
|
+
if (!(err instanceof ExecutionEventEncodingError))
|
|
974
|
+
throw err;
|
|
975
|
+
return { ok: false, code: "INVALID_EXECUTION_EVENT", message: err.message };
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function executionIdConflict(executionId) {
|
|
979
|
+
return {
|
|
980
|
+
status: "rejected",
|
|
981
|
+
code: "EXECUTION_ID_CONFLICT",
|
|
982
|
+
message: `Execution "${executionId}" already exists`
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function executionNotFound(executionId) {
|
|
986
|
+
return {
|
|
987
|
+
status: "rejected",
|
|
988
|
+
code: "EXECUTION_NOT_FOUND",
|
|
989
|
+
message: `Execution "${executionId}" does not exist`
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function prepareExecutionStart(request) {
|
|
993
|
+
const invalid = (message) => ({
|
|
994
|
+
ok: false,
|
|
995
|
+
result: {
|
|
996
|
+
status: "rejected",
|
|
997
|
+
code: "INVALID_START_PARAMETERS",
|
|
998
|
+
message
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
if (typeof request.workflowId !== "string" || request.workflowId === "") {
|
|
1002
|
+
return invalid("workflowId must be a non-empty string");
|
|
1003
|
+
}
|
|
1004
|
+
let params = null;
|
|
1005
|
+
if (request.params !== undefined) {
|
|
1006
|
+
if (!isPlainObject(request.params)) {
|
|
1007
|
+
return invalid("params must be a plain object");
|
|
1008
|
+
}
|
|
1009
|
+
const encoding = durableValueCodec.encode(request.params);
|
|
1010
|
+
if (encoding.status === "invalid") {
|
|
1011
|
+
return invalid(`params: ${encoding.message}`);
|
|
1012
|
+
}
|
|
1013
|
+
params = encoding.canonical;
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
ok: true,
|
|
1017
|
+
start: {
|
|
1018
|
+
callerExecutionId: request.requestId.executionId,
|
|
1019
|
+
stepKey: request.requestId.stepKey,
|
|
1020
|
+
workflowId: request.workflowId,
|
|
1021
|
+
params
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function matchStartRequest(recorded, start) {
|
|
1026
|
+
if (recorded.workflowId === start.workflowId && recorded.params === start.params) {
|
|
1027
|
+
return { status: "accepted", executionId: recorded.executionId };
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
status: "rejected",
|
|
1031
|
+
code: "START_REQUEST_MISMATCH",
|
|
1032
|
+
message: `Step "${start.stepKey}" of execution "${start.callerExecutionId}" already started workflow "${recorded.workflowId}" with different parameters or workflow ID`
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function createStartedExecution(params) {
|
|
1036
|
+
const { start, caller, now } = params;
|
|
1037
|
+
const executionId = params.executionId ?? crypto.randomUUID();
|
|
1038
|
+
const target = createExecutionState({
|
|
1039
|
+
executionId,
|
|
1040
|
+
workflowId: start.workflowId,
|
|
1041
|
+
encoded: {
|
|
1042
|
+
params: start.params ?? encodeExecutionEvent({ params: undefined, context: undefined }).params,
|
|
1043
|
+
context: caller.context
|
|
1044
|
+
},
|
|
1045
|
+
now,
|
|
1046
|
+
parent: { executionId: start.callerExecutionId, stepKey: start.stepKey }
|
|
1047
|
+
});
|
|
1048
|
+
return {
|
|
1049
|
+
target,
|
|
1050
|
+
startRequest: {
|
|
1051
|
+
callerExecutionId: start.callerExecutionId,
|
|
1052
|
+
stepKey: start.stepKey,
|
|
1053
|
+
workflowId: start.workflowId,
|
|
1054
|
+
params: start.params,
|
|
1055
|
+
executionId
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
// src/lib/execution-dispatcher.ts
|
|
1060
|
+
var DEFAULT_DISPATCHER_CONCURRENCY = 10;
|
|
1061
|
+
var DEFAULT_ABORT_GRACE_PERIOD = 1000;
|
|
1062
|
+
var ABANDONED = Symbol("abandoned");
|
|
1063
|
+
|
|
1064
|
+
class ExecutionDispatcher {
|
|
1065
|
+
runtime;
|
|
1066
|
+
invoker;
|
|
1067
|
+
logger;
|
|
1068
|
+
pollInterval;
|
|
1069
|
+
keepAlive;
|
|
1070
|
+
concurrency;
|
|
1071
|
+
leaseDuration;
|
|
1072
|
+
renewInterval;
|
|
1073
|
+
abortGracePeriod;
|
|
1074
|
+
running = false;
|
|
1075
|
+
looping = false;
|
|
1076
|
+
timer;
|
|
1077
|
+
inFlight = new Set;
|
|
1078
|
+
claiming = 0;
|
|
1079
|
+
constructor(runtime, options) {
|
|
1080
|
+
this.runtime = runtime;
|
|
1081
|
+
this.invoker = options.invoker;
|
|
1082
|
+
this.logger = options.logger;
|
|
1083
|
+
this.pollInterval = options.pollInterval ?? 10;
|
|
1084
|
+
this.keepAlive = options.keepAlive ?? true;
|
|
1085
|
+
this.concurrency = options.concurrency ?? DEFAULT_DISPATCHER_CONCURRENCY;
|
|
1086
|
+
this.leaseDuration = options.leaseDuration ?? DEFAULT_LEASE_DURATION;
|
|
1087
|
+
this.renewInterval = options.renewInterval ?? this.leaseDuration / 3;
|
|
1088
|
+
this.abortGracePeriod = options.abortGracePeriod ?? DEFAULT_ABORT_GRACE_PERIOD;
|
|
1089
|
+
if (!Number.isInteger(this.concurrency) || this.concurrency < 1) {
|
|
1090
|
+
throw new RangeError("concurrency must be a positive integer");
|
|
1091
|
+
}
|
|
1092
|
+
if (!(this.leaseDuration > 0)) {
|
|
1093
|
+
throw new RangeError("leaseDuration must be positive");
|
|
1094
|
+
}
|
|
1095
|
+
if (!(this.renewInterval > 0 && this.renewInterval < this.leaseDuration)) {
|
|
1096
|
+
throw new RangeError("renewInterval must be positive and shorter than leaseDuration");
|
|
1097
|
+
}
|
|
1098
|
+
if (!(this.abortGracePeriod >= 0)) {
|
|
1099
|
+
throw new RangeError("abortGracePeriod must not be negative");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
start() {
|
|
1103
|
+
if (this.running)
|
|
1104
|
+
return;
|
|
1105
|
+
this.running = true;
|
|
1106
|
+
this.loop();
|
|
1107
|
+
}
|
|
1108
|
+
async stop() {
|
|
1109
|
+
this.running = false;
|
|
1110
|
+
if (this.timer)
|
|
1111
|
+
clearTimeout(this.timer);
|
|
1112
|
+
this.timer = undefined;
|
|
1113
|
+
await this.drain();
|
|
1114
|
+
}
|
|
1115
|
+
get activeDeliveries() {
|
|
1116
|
+
return this.inFlight.size;
|
|
1117
|
+
}
|
|
1118
|
+
async tick() {
|
|
1119
|
+
const slots = this.concurrency - this.inFlight.size - this.claiming;
|
|
1120
|
+
if (slots <= 0)
|
|
1121
|
+
return 0;
|
|
1122
|
+
this.claiming += slots;
|
|
1123
|
+
let leases;
|
|
1124
|
+
try {
|
|
1125
|
+
leases = await this.runtime.executions.claimDue({
|
|
1126
|
+
limit: slots,
|
|
1127
|
+
duration: this.leaseDuration
|
|
1128
|
+
});
|
|
1129
|
+
} finally {
|
|
1130
|
+
this.claiming -= slots;
|
|
1131
|
+
}
|
|
1132
|
+
for (const lease of leases) {
|
|
1133
|
+
const task = this.dispatch(lease).finally(() => {
|
|
1134
|
+
this.inFlight.delete(task);
|
|
1135
|
+
if (this.running && !this.looping)
|
|
1136
|
+
this.schedule(0);
|
|
1137
|
+
});
|
|
1138
|
+
this.inFlight.add(task);
|
|
1139
|
+
}
|
|
1140
|
+
return leases.length;
|
|
1141
|
+
}
|
|
1142
|
+
async drain() {
|
|
1143
|
+
while (this.inFlight.size > 0) {
|
|
1144
|
+
await Promise.allSettled([...this.inFlight]);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
schedule(delay) {
|
|
1148
|
+
if (this.timer)
|
|
1149
|
+
clearTimeout(this.timer);
|
|
1150
|
+
this.timer = setTimeout(() => {
|
|
1151
|
+
this.timer = undefined;
|
|
1152
|
+
this.loop();
|
|
1153
|
+
}, delay);
|
|
1154
|
+
if (!this.keepAlive)
|
|
1155
|
+
this.timer.unref?.();
|
|
1156
|
+
}
|
|
1157
|
+
async loop() {
|
|
1158
|
+
if (!this.running || this.looping)
|
|
1159
|
+
return;
|
|
1160
|
+
this.looping = true;
|
|
1161
|
+
let claimed = 0;
|
|
1162
|
+
try {
|
|
1163
|
+
claimed = await this.tick();
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
this.logger.error({ err }, "Failed to claim due executions");
|
|
1166
|
+
} finally {
|
|
1167
|
+
this.looping = false;
|
|
1168
|
+
}
|
|
1169
|
+
if (!this.running)
|
|
1170
|
+
return;
|
|
1171
|
+
if (this.inFlight.size + this.claiming >= this.concurrency)
|
|
1172
|
+
return;
|
|
1173
|
+
this.schedule(claimed > 0 ? 0 : this.pollInterval);
|
|
1174
|
+
}
|
|
1175
|
+
async dispatch(lease) {
|
|
1176
|
+
const { executionId } = lease;
|
|
1177
|
+
const { executions } = this.runtime;
|
|
1178
|
+
const renewal = this.renew(lease);
|
|
1179
|
+
try {
|
|
1180
|
+
const record = await executions.get(executionId);
|
|
1181
|
+
if (!record) {
|
|
1182
|
+
this.logger.error({ executionId }, "Claimed execution has no record");
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (isTerminalExecutionStatus(record.status)) {
|
|
1186
|
+
this.emit(executionId, record.outcome);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
let outcome;
|
|
1190
|
+
try {
|
|
1191
|
+
outcome = await this.abandonAfterAbort(this.invoker.deliver({
|
|
1192
|
+
workflowId: record.workflowId,
|
|
1193
|
+
executionId,
|
|
1194
|
+
params: record.params,
|
|
1195
|
+
context: record.context
|
|
1196
|
+
}, { lease, signal: renewal.signal }), lease, renewal.signal);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
const current = await renewal.stop();
|
|
1199
|
+
if (!current)
|
|
1200
|
+
return;
|
|
1201
|
+
if (WorkerExitedError.is(err)) {
|
|
1202
|
+
this.logger.error({ executionId, err }, "Worker exited without replying; the lease will expire and the execution will be redelivered");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
this.logger.error({ executionId, err }, "Delivery failed without an outcome; releasing its lease for redelivery");
|
|
1206
|
+
const released = await executions.release({ lease: current });
|
|
1207
|
+
if (released.status === "lost")
|
|
1208
|
+
this.warnLost(lease);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (outcome === ABANDONED)
|
|
1212
|
+
return;
|
|
1213
|
+
const current = await renewal.stop();
|
|
1214
|
+
if (!current)
|
|
1215
|
+
return;
|
|
1216
|
+
const result = await executions.acknowledge({ lease: current, outcome });
|
|
1217
|
+
if (result.status === "lost") {
|
|
1218
|
+
this.warnLost(lease);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
this.emit(executionId, result);
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
this.logger.error({ executionId, err }, "Failed to dispatch execution; the lease will expire and the execution will be redelivered");
|
|
1224
|
+
} finally {
|
|
1225
|
+
await renewal.stop();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
abandonAfterAbort(delivery, lease, signal) {
|
|
1229
|
+
return new Promise((resolve, reject) => {
|
|
1230
|
+
let abandoned = false;
|
|
1231
|
+
let timer;
|
|
1232
|
+
const onAbort = () => {
|
|
1233
|
+
timer = setTimeout(() => {
|
|
1234
|
+
abandoned = true;
|
|
1235
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token }, "Delivery did not stop within the abort grace period after its lease was lost; abandoning it");
|
|
1236
|
+
resolve(ABANDONED);
|
|
1237
|
+
}, this.abortGracePeriod);
|
|
1238
|
+
if (!this.keepAlive)
|
|
1239
|
+
timer.unref?.();
|
|
1240
|
+
};
|
|
1241
|
+
const settle = () => {
|
|
1242
|
+
clearTimeout(timer);
|
|
1243
|
+
signal.removeEventListener("abort", onAbort);
|
|
1244
|
+
};
|
|
1245
|
+
if (signal.aborted)
|
|
1246
|
+
onAbort();
|
|
1247
|
+
else
|
|
1248
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1249
|
+
delivery.then((value) => {
|
|
1250
|
+
settle();
|
|
1251
|
+
if (abandoned) {
|
|
1252
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token }, "Abandoned delivery settled late; its outcome is ignored");
|
|
1253
|
+
} else
|
|
1254
|
+
resolve(value);
|
|
1255
|
+
}, (err) => {
|
|
1256
|
+
settle();
|
|
1257
|
+
if (abandoned) {
|
|
1258
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token, err }, "Abandoned delivery rejected late; ignored");
|
|
1259
|
+
} else
|
|
1260
|
+
reject(err);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
renew(lease) {
|
|
1265
|
+
const controller = new AbortController;
|
|
1266
|
+
let current = lease;
|
|
1267
|
+
let pending;
|
|
1268
|
+
let stopped = false;
|
|
1269
|
+
const { executions } = this.runtime;
|
|
1270
|
+
const timer = setInterval(() => {
|
|
1271
|
+
if (stopped || pending || !current)
|
|
1272
|
+
return;
|
|
1273
|
+
const renewing = current;
|
|
1274
|
+
pending = executions.renew({ lease: renewing, duration: this.leaseDuration }).then((result) => {
|
|
1275
|
+
if (result.status === "lost") {
|
|
1276
|
+
current = undefined;
|
|
1277
|
+
clearInterval(timer);
|
|
1278
|
+
this.warnLost(lease);
|
|
1279
|
+
controller.abort(new LeaseLostError(lease.executionId));
|
|
1280
|
+
} else if (current) {
|
|
1281
|
+
current = result.lease;
|
|
1282
|
+
}
|
|
1283
|
+
}, (err) => {
|
|
1284
|
+
this.logger.error({ executionId: lease.executionId, err }, "Failed to renew lease");
|
|
1285
|
+
}).finally(() => {
|
|
1286
|
+
pending = undefined;
|
|
1287
|
+
});
|
|
1288
|
+
}, this.renewInterval);
|
|
1289
|
+
if (!this.keepAlive)
|
|
1290
|
+
timer.unref?.();
|
|
1291
|
+
return {
|
|
1292
|
+
signal: controller.signal,
|
|
1293
|
+
async stop() {
|
|
1294
|
+
stopped = true;
|
|
1295
|
+
clearInterval(timer);
|
|
1296
|
+
await pending;
|
|
1297
|
+
return current;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
warnLost(lease) {
|
|
1302
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token }, "Lease was lost to another claim; this delivery stops without an outcome");
|
|
1303
|
+
}
|
|
1304
|
+
emit(executionId, result) {
|
|
1305
|
+
if (result.status === "completed") {
|
|
1306
|
+
this.invoker.workflowEndEmitter.emit(executionId, result.result);
|
|
1307
|
+
} else if (result.status === "failed") {
|
|
1308
|
+
this.invoker.workflowEndEmitter.emit(executionId, result.error);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
// src/lib/wait-for-outcome.ts
|
|
1313
|
+
async function waitForExecutionOutcome(executions, executionId, options) {
|
|
1314
|
+
const pollInterval = options?.pollInterval ?? 25;
|
|
1315
|
+
const emitter = options?.emitter;
|
|
1316
|
+
let wake;
|
|
1317
|
+
const nudge = () => wake?.();
|
|
1318
|
+
emitter?.on(executionId, nudge);
|
|
1319
|
+
try {
|
|
1320
|
+
while (true) {
|
|
1321
|
+
options?.signal?.throwIfAborted();
|
|
1322
|
+
const record = await executions.get(executionId);
|
|
1323
|
+
if (!record)
|
|
1324
|
+
return;
|
|
1325
|
+
if (isTerminalExecutionStatus(record.status))
|
|
1326
|
+
return record.outcome;
|
|
1327
|
+
await new Promise((resolve) => {
|
|
1328
|
+
const timer = setTimeout(resolve, pollInterval);
|
|
1329
|
+
wake = () => {
|
|
1330
|
+
clearTimeout(timer);
|
|
1331
|
+
resolve();
|
|
1332
|
+
};
|
|
1333
|
+
});
|
|
1334
|
+
wake = undefined;
|
|
1335
|
+
}
|
|
1336
|
+
} finally {
|
|
1337
|
+
emitter?.off(executionId, nudge);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1
1340
|
// src/base/store.ts
|
|
2
|
-
|
|
3
|
-
|
|
1341
|
+
class StoreValidationError extends Error {
|
|
1342
|
+
constructor(message) {
|
|
1343
|
+
super(message);
|
|
1344
|
+
this.name = "StoreValidationError";
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
registerErrorClass("StoreValidationError", StoreValidationError);
|
|
1348
|
+
function defineStore(name, schema, options) {
|
|
1349
|
+
return {
|
|
1350
|
+
name,
|
|
1351
|
+
schema,
|
|
1352
|
+
initial: options.initial
|
|
1353
|
+
};
|
|
4
1354
|
}
|
|
5
1355
|
|
|
6
1356
|
class StoreClient {
|
|
@@ -31,17 +1381,21 @@ class StoreClient {
|
|
|
31
1381
|
}
|
|
32
1382
|
|
|
33
1383
|
class CasStoreClient extends StoreClient {
|
|
1384
|
+
createInstanceId() {
|
|
1385
|
+
return crypto.randomUUID();
|
|
1386
|
+
}
|
|
1387
|
+
async getStore(params) {
|
|
1388
|
+
return this.getOrInsertStore(params.definition, params.id);
|
|
1389
|
+
}
|
|
34
1390
|
async updateStore(params) {
|
|
35
1391
|
const applied = await this.getAppliedResult(params);
|
|
36
1392
|
if (applied) {
|
|
37
1393
|
return applied.result;
|
|
38
1394
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
id: params.id
|
|
42
|
-
});
|
|
1395
|
+
const stepKey = params.stepId?.stepKey;
|
|
1396
|
+
let snapshot = await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
43
1397
|
while (true) {
|
|
44
|
-
const prepared = await prepareStoreUpdate(params.definition, snapshot, params.updater);
|
|
1398
|
+
const prepared = await prepareStoreUpdate(params.definition, snapshot, params.updater, stepKey);
|
|
45
1399
|
const result = {
|
|
46
1400
|
state: cloneStoreState(prepared.nextState),
|
|
47
1401
|
previousVersion: snapshot.version,
|
|
@@ -61,7 +1415,7 @@ class CasStoreClient extends StoreClient {
|
|
|
61
1415
|
if (committed.status === "already-applied") {
|
|
62
1416
|
return committed.result;
|
|
63
1417
|
}
|
|
64
|
-
snapshot = committed.snapshot;
|
|
1418
|
+
snapshot = committed.status === "conflict" ? existing(committed.snapshot) : await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
65
1419
|
}
|
|
66
1420
|
}
|
|
67
1421
|
async updateStoreFrom(params) {
|
|
@@ -70,14 +1424,12 @@ class CasStoreClient extends StoreClient {
|
|
|
70
1424
|
return applied.result;
|
|
71
1425
|
}
|
|
72
1426
|
assertStoreSnapshotInstanceId(params.snapshot);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
id: params.id
|
|
76
|
-
});
|
|
1427
|
+
const stepKey = params.stepId?.stepKey;
|
|
1428
|
+
const current = await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
77
1429
|
if (!sameStoreVersion(current, params.snapshot)) {
|
|
78
1430
|
return storeUpdateConflict(params.snapshot, current);
|
|
79
1431
|
}
|
|
80
|
-
const prepared = await prepareStoreUpdate(params.definition, params.snapshot, params.updater);
|
|
1432
|
+
const prepared = await prepareStoreUpdate(params.definition, params.snapshot, params.updater, stepKey);
|
|
81
1433
|
const result = {
|
|
82
1434
|
updated: true,
|
|
83
1435
|
state: cloneStoreState(prepared.nextState),
|
|
@@ -87,7 +1439,7 @@ class CasStoreClient extends StoreClient {
|
|
|
87
1439
|
const committed = await this.commitStoreMutation({
|
|
88
1440
|
definition: params.definition,
|
|
89
1441
|
id: params.id,
|
|
90
|
-
expected: params.snapshot,
|
|
1442
|
+
expected: existing(params.snapshot),
|
|
91
1443
|
nextState: prepared.nextState,
|
|
92
1444
|
changedPaths: prepared.changedPaths,
|
|
93
1445
|
stepId: params.stepId,
|
|
@@ -98,22 +1450,63 @@ class CasStoreClient extends StoreClient {
|
|
|
98
1450
|
if (committed.status === "already-applied") {
|
|
99
1451
|
return committed.result;
|
|
100
1452
|
}
|
|
101
|
-
return storeUpdateConflict(params.snapshot, committed.snapshot);
|
|
1453
|
+
return storeUpdateConflict(params.snapshot, committed.status === "conflict" ? committed.snapshot : await this.getOrInsertStore(params.definition, params.id, stepKey));
|
|
1454
|
+
}
|
|
1455
|
+
async deleteStoreFrom(params) {
|
|
1456
|
+
const applied = await this.getAppliedResult(params);
|
|
1457
|
+
if (applied)
|
|
1458
|
+
return applied.result;
|
|
1459
|
+
assertStoreSnapshotInstanceId(params.snapshot);
|
|
1460
|
+
const stepKey = params.stepId?.stepKey;
|
|
1461
|
+
let current = await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
1462
|
+
while (sameStoreVersion(current, params.snapshot)) {
|
|
1463
|
+
const result = { deleted: true };
|
|
1464
|
+
const committed = await this.commitStoreDeletion({
|
|
1465
|
+
definition: params.definition,
|
|
1466
|
+
id: params.id,
|
|
1467
|
+
expected: params.snapshot,
|
|
1468
|
+
stepId: params.stepId,
|
|
1469
|
+
result
|
|
1470
|
+
});
|
|
1471
|
+
if (committed.status === "committed")
|
|
1472
|
+
return result;
|
|
1473
|
+
if (committed.status === "already-applied") {
|
|
1474
|
+
return committed.result;
|
|
1475
|
+
}
|
|
1476
|
+
current = committed.status === "conflict" ? committed.snapshot : await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
deleted: false,
|
|
1480
|
+
reason: "conflict",
|
|
1481
|
+
expectedInstanceId: params.snapshot.instanceId,
|
|
1482
|
+
actualInstanceId: current.instanceId,
|
|
1483
|
+
expectedVersion: params.snapshot.version,
|
|
1484
|
+
actualVersion: current.version
|
|
1485
|
+
};
|
|
102
1486
|
}
|
|
103
1487
|
async takeFromStore(params) {
|
|
104
1488
|
const applied = await this.getAppliedResult(params);
|
|
105
1489
|
if (applied)
|
|
106
1490
|
return applied.result;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
id: params.id
|
|
110
|
-
});
|
|
1491
|
+
const stepKey = params.stepId?.stepKey;
|
|
1492
|
+
let snapshot = await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
111
1493
|
while (true) {
|
|
112
1494
|
const previousState = cloneStoreState(snapshot.state);
|
|
113
1495
|
const draft = cloneStoreState(previousState);
|
|
114
1496
|
const tracked = trackStoreUpdater(draft);
|
|
115
1497
|
const { result: selectedResult, readPaths } = trackStoreSelector(tracked.draft, params.selector);
|
|
116
1498
|
if (!isStoreSelectorMatch(selectedResult)) {
|
|
1499
|
+
if (snapshot.created) {
|
|
1500
|
+
const stored = await this.insertStore({
|
|
1501
|
+
definition: params.definition,
|
|
1502
|
+
id: params.id,
|
|
1503
|
+
snapshot: detach(snapshot)
|
|
1504
|
+
});
|
|
1505
|
+
if (stored.instanceId !== snapshot.instanceId) {
|
|
1506
|
+
snapshot = existing(stored);
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
117
1510
|
return {
|
|
118
1511
|
matched: false,
|
|
119
1512
|
instanceId: snapshot.instanceId,
|
|
@@ -127,8 +1520,10 @@ class CasStoreClient extends StoreClient {
|
|
|
127
1520
|
definition: params.definition,
|
|
128
1521
|
previousState,
|
|
129
1522
|
draft,
|
|
130
|
-
tracked
|
|
1523
|
+
tracked,
|
|
1524
|
+
stepKey
|
|
131
1525
|
});
|
|
1526
|
+
assertDurableStoreValue(selected, stepKey ?? "", `Store take selection for "${params.definition.name}"`);
|
|
132
1527
|
const selectedSnapshot = cloneStoreState(selected);
|
|
133
1528
|
const result = {
|
|
134
1529
|
matched: true,
|
|
@@ -150,9 +1545,31 @@ class CasStoreClient extends StoreClient {
|
|
|
150
1545
|
if (committed.status === "already-applied") {
|
|
151
1546
|
return committed.result;
|
|
152
1547
|
}
|
|
153
|
-
snapshot = committed.snapshot;
|
|
1548
|
+
snapshot = committed.status === "conflict" ? existing(committed.snapshot) : await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
154
1549
|
}
|
|
155
1550
|
}
|
|
1551
|
+
async readOrInitialize(definition, id, stepKey) {
|
|
1552
|
+
const snapshot = await this.readStore({ definition, id });
|
|
1553
|
+
if (snapshot)
|
|
1554
|
+
return existing(snapshot);
|
|
1555
|
+
const state = await validateStoreState(definition, definition.initial(id), stepKey);
|
|
1556
|
+
return {
|
|
1557
|
+
state,
|
|
1558
|
+
instanceId: this.createInstanceId(),
|
|
1559
|
+
version: 0,
|
|
1560
|
+
created: true
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
async getOrInsertStore(definition, id, stepKey) {
|
|
1564
|
+
const snapshot = await this.readOrInitialize(definition, id, stepKey);
|
|
1565
|
+
if (!snapshot.created)
|
|
1566
|
+
return detach(snapshot);
|
|
1567
|
+
return await this.insertStore({
|
|
1568
|
+
definition,
|
|
1569
|
+
id,
|
|
1570
|
+
snapshot: detach(snapshot)
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
156
1573
|
getAppliedResult(params) {
|
|
157
1574
|
return params.stepId ? this.getAppliedStoreStep({
|
|
158
1575
|
definition: params.definition,
|
|
@@ -161,7 +1578,22 @@ class CasStoreClient extends StoreClient {
|
|
|
161
1578
|
}) : Promise.resolve(undefined);
|
|
162
1579
|
}
|
|
163
1580
|
}
|
|
164
|
-
|
|
1581
|
+
function existing(snapshot) {
|
|
1582
|
+
return {
|
|
1583
|
+
state: snapshot.state,
|
|
1584
|
+
instanceId: snapshot.instanceId,
|
|
1585
|
+
version: snapshot.version,
|
|
1586
|
+
created: false
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function detach(snapshot) {
|
|
1590
|
+
return {
|
|
1591
|
+
state: snapshot.state,
|
|
1592
|
+
instanceId: snapshot.instanceId,
|
|
1593
|
+
version: snapshot.version
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
async function prepareStoreUpdate(definition, snapshot, updater, stepKey) {
|
|
165
1597
|
const previousState = cloneStoreState(snapshot.state);
|
|
166
1598
|
const draft = cloneStoreState(previousState);
|
|
167
1599
|
const tracked = trackStoreUpdater(draft);
|
|
@@ -173,12 +1605,13 @@ async function prepareStoreUpdate(definition, snapshot, updater) {
|
|
|
173
1605
|
previousState,
|
|
174
1606
|
draft,
|
|
175
1607
|
tracked,
|
|
176
|
-
replacement: returned
|
|
1608
|
+
replacement: returned,
|
|
1609
|
+
stepKey
|
|
177
1610
|
});
|
|
178
1611
|
}
|
|
179
1612
|
async function finalizeStoreMutation(params) {
|
|
180
1613
|
const isReplacement = params.replacement !== undefined && params.replacement !== params.draft;
|
|
181
|
-
const nextState = await validateStoreState(params.definition, isReplacement ? params.replacement : params.draft);
|
|
1614
|
+
const nextState = await validateStoreState(params.definition, isReplacement ? params.replacement : params.draft, params.stepKey);
|
|
182
1615
|
const changedPaths = !isReplacement && nextState === params.draft ? params.tracked.writePaths() : diffStorePaths(params.previousState, nextState);
|
|
183
1616
|
return { nextState, changedPaths };
|
|
184
1617
|
}
|
|
@@ -212,15 +1645,42 @@ function assertSynchronousUpdater(updaterResult) {
|
|
|
212
1645
|
throw new Error("Store updaters must be synchronous");
|
|
213
1646
|
}
|
|
214
1647
|
}
|
|
215
|
-
async function validateStoreState(definition, state) {
|
|
1648
|
+
async function validateStoreState(definition, state, stepKey = "") {
|
|
216
1649
|
const result = await definition.schema["~standard"].validate(state);
|
|
217
1650
|
if ("issues" in result && result.issues) {
|
|
218
1651
|
const messages = result.issues.map((issue) => issue.message).join("; ");
|
|
219
|
-
throw new
|
|
1652
|
+
throw new StoreValidationError(`Invalid store state for "${definition.name}": ${messages}`);
|
|
220
1653
|
}
|
|
1654
|
+
assertDurableStoreValue(result.value, stepKey, `Store state for "${definition.name}"`);
|
|
221
1655
|
return result.value;
|
|
222
1656
|
}
|
|
1657
|
+
function assertDurableStoreValue(value, stepKey, label) {
|
|
1658
|
+
const encoding = durableValueCodec.encode(value);
|
|
1659
|
+
if (encoding.status === "invalid") {
|
|
1660
|
+
throw new WorkflowValueError("INVALID_STEP_VALUE", stepKey, `${label} is not a durable value: ${encoding.message}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function encodeStoreValue(value) {
|
|
1664
|
+
const encoding = durableValueCodec.encode(value);
|
|
1665
|
+
if (encoding.status === "invalid") {
|
|
1666
|
+
throw new Error(`Store value is not a durable value: ${encoding.message}`);
|
|
1667
|
+
}
|
|
1668
|
+
return encoding.canonical;
|
|
1669
|
+
}
|
|
1670
|
+
function decodeStoreValue(canonical) {
|
|
1671
|
+
const decoding = durableValueCodec.decode(canonical);
|
|
1672
|
+
if (decoding.status === "invalid") {
|
|
1673
|
+
throw new Error(`Invalid store value encoding: ${decoding.message}`);
|
|
1674
|
+
}
|
|
1675
|
+
return decoding.value;
|
|
1676
|
+
}
|
|
223
1677
|
function cloneStoreState(state) {
|
|
1678
|
+
const encoding = durableValueCodec.encode(state);
|
|
1679
|
+
if (encoding.status === "encoded") {
|
|
1680
|
+
const decoding = durableValueCodec.decode(encoding.canonical);
|
|
1681
|
+
if (decoding.status === "decoded")
|
|
1682
|
+
return decoding.value;
|
|
1683
|
+
}
|
|
224
1684
|
if (typeof structuredClone === "function") {
|
|
225
1685
|
try {
|
|
226
1686
|
return structuredClone(state);
|
|
@@ -310,29 +1770,29 @@ function trackStoreUpdater(target) {
|
|
|
310
1770
|
if (cached)
|
|
311
1771
|
return cached;
|
|
312
1772
|
const proxy = new Proxy(value, {
|
|
313
|
-
get(
|
|
1773
|
+
get(target, property, receiver) {
|
|
314
1774
|
if (typeof property === "symbol") {
|
|
315
1775
|
if (property === TRACKING_TARGET)
|
|
316
|
-
return
|
|
317
|
-
return Reflect.get(
|
|
1776
|
+
return target;
|
|
1777
|
+
return Reflect.get(target, property, receiver);
|
|
318
1778
|
}
|
|
319
|
-
const child = Reflect.get(
|
|
1779
|
+
const child = Reflect.get(target, property);
|
|
320
1780
|
return track(child, [...path, arrayKeyToPathSegment(property)]);
|
|
321
1781
|
},
|
|
322
|
-
set(
|
|
1782
|
+
set(target, property, value) {
|
|
323
1783
|
record(path, property);
|
|
324
|
-
return Reflect.set(
|
|
1784
|
+
return Reflect.set(target, property, unwrapTrackedValue(value));
|
|
325
1785
|
},
|
|
326
|
-
deleteProperty(
|
|
1786
|
+
deleteProperty(target, property) {
|
|
327
1787
|
record(path, property);
|
|
328
|
-
return Reflect.deleteProperty(
|
|
1788
|
+
return Reflect.deleteProperty(target, property);
|
|
329
1789
|
},
|
|
330
|
-
defineProperty(
|
|
1790
|
+
defineProperty(target, property, descriptor) {
|
|
331
1791
|
record(path, property);
|
|
332
1792
|
if ("value" in descriptor) {
|
|
333
1793
|
descriptor = { ...descriptor, value: unwrapTrackedValue(descriptor.value) };
|
|
334
1794
|
}
|
|
335
|
-
return Reflect.defineProperty(
|
|
1795
|
+
return Reflect.defineProperty(target, property, descriptor);
|
|
336
1796
|
}
|
|
337
1797
|
});
|
|
338
1798
|
proxies.set(value, proxy);
|
|
@@ -391,9 +1851,12 @@ class StepResult extends StepResponse {
|
|
|
391
1851
|
static type = "step-result";
|
|
392
1852
|
type = "step-result";
|
|
393
1853
|
result;
|
|
394
|
-
|
|
1854
|
+
schema;
|
|
1855
|
+
constructor(result, schema) {
|
|
395
1856
|
super();
|
|
396
1857
|
this.result = result;
|
|
1858
|
+
if (schema !== undefined)
|
|
1859
|
+
this.schema = schema;
|
|
397
1860
|
}
|
|
398
1861
|
}
|
|
399
1862
|
|
|
@@ -410,10 +1873,10 @@ class WorkflowResult extends StepResponse {
|
|
|
410
1873
|
class WorkflowDelay extends StepResponse {
|
|
411
1874
|
static type = "workflow-delay";
|
|
412
1875
|
type = "workflow-delay";
|
|
413
|
-
|
|
414
|
-
constructor(
|
|
1876
|
+
wakeAt;
|
|
1877
|
+
constructor(wakeAt) {
|
|
415
1878
|
super();
|
|
416
|
-
this.
|
|
1879
|
+
this.wakeAt = wakeAt;
|
|
417
1880
|
}
|
|
418
1881
|
}
|
|
419
1882
|
|
|
@@ -425,6 +1888,16 @@ class StepStoreWait extends StepResponse {
|
|
|
425
1888
|
}
|
|
426
1889
|
}
|
|
427
1890
|
|
|
1891
|
+
class StepEffectWait extends StepResponse {
|
|
1892
|
+
static type = "effect-wait";
|
|
1893
|
+
type = "effect-wait";
|
|
1894
|
+
wakeAt;
|
|
1895
|
+
constructor(wakeAt) {
|
|
1896
|
+
super();
|
|
1897
|
+
this.wakeAt = wakeAt;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
428
1901
|
class WorkflowRestart extends StepResponse {
|
|
429
1902
|
static type = "workflow-restart";
|
|
430
1903
|
type = "workflow-restart";
|
|
@@ -452,54 +1925,66 @@ class StepError extends StepResponse {
|
|
|
452
1925
|
class StepDelay extends StepResponse {
|
|
453
1926
|
static type = "step-delay";
|
|
454
1927
|
type = "step-delay";
|
|
455
|
-
|
|
456
|
-
constructor(
|
|
1928
|
+
wakeAt;
|
|
1929
|
+
constructor(wakeAt) {
|
|
457
1930
|
super();
|
|
458
|
-
this.
|
|
1931
|
+
this.wakeAt = wakeAt;
|
|
459
1932
|
}
|
|
460
1933
|
}
|
|
461
1934
|
// src/lib/workflow-runner.ts
|
|
462
1935
|
class WorkflowRunner {
|
|
463
|
-
|
|
464
|
-
schedulerClient;
|
|
465
|
-
storeClient;
|
|
1936
|
+
runtime;
|
|
466
1937
|
router;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
471
|
-
this.
|
|
1938
|
+
logger;
|
|
1939
|
+
constructor(runtime, options) {
|
|
1940
|
+
this.runtime = runtime;
|
|
1941
|
+
this.router = options.router;
|
|
1942
|
+
this.logger = options.logger;
|
|
472
1943
|
}
|
|
473
|
-
run = async (event,
|
|
1944
|
+
run = async (event, options) => {
|
|
1945
|
+
const { lease, signal, logger = this.logger } = options;
|
|
474
1946
|
const workflow = this.router[event.workflowId];
|
|
475
1947
|
if (!workflow) {
|
|
476
|
-
|
|
1948
|
+
return {
|
|
1949
|
+
status: "failed",
|
|
1950
|
+
error: new Error(`No workflow registered for "${event.workflowId}"`)
|
|
1951
|
+
};
|
|
477
1952
|
}
|
|
1953
|
+
let response;
|
|
478
1954
|
try {
|
|
479
|
-
|
|
1955
|
+
response = await this.runWorkflows({
|
|
480
1956
|
workflow,
|
|
481
1957
|
logger,
|
|
482
|
-
event
|
|
1958
|
+
event,
|
|
1959
|
+
lease,
|
|
1960
|
+
signal
|
|
483
1961
|
});
|
|
484
|
-
switch (response.type) {
|
|
485
|
-
case "workflow-result":
|
|
486
|
-
return response;
|
|
487
|
-
case "workflow-delay":
|
|
488
|
-
this.schedulerClient.requestWakeUp(event, response.resumeIn);
|
|
489
|
-
break;
|
|
490
|
-
case "store-wait":
|
|
491
|
-
break;
|
|
492
|
-
}
|
|
493
1962
|
} catch (err) {
|
|
494
|
-
|
|
1963
|
+
if (InfrastructureError.is(err))
|
|
1964
|
+
throw err;
|
|
1965
|
+
return { status: "failed", error: toError(err) };
|
|
1966
|
+
}
|
|
1967
|
+
switch (response.type) {
|
|
1968
|
+
case "workflow-result":
|
|
1969
|
+
return { status: "completed", result: response.result };
|
|
1970
|
+
case "step-delay":
|
|
1971
|
+
case "effect-wait":
|
|
1972
|
+
return { status: "suspended", wakeAt: response.wakeAt };
|
|
1973
|
+
case "store-wait":
|
|
1974
|
+
return { status: "suspended" };
|
|
495
1975
|
}
|
|
496
1976
|
};
|
|
497
1977
|
async runWorkflows(params) {
|
|
498
|
-
const { workflow, event, logger } = params;
|
|
1978
|
+
const { workflow, event, lease, signal, logger } = params;
|
|
499
1979
|
const workflowIterator = workflow({
|
|
500
1980
|
event,
|
|
501
|
-
|
|
502
|
-
|
|
1981
|
+
lease,
|
|
1982
|
+
signal,
|
|
1983
|
+
heapClient: this.runtime.heap,
|
|
1984
|
+
storeClient: this.runtime.stores,
|
|
1985
|
+
executionClient: this.runtime.executions,
|
|
1986
|
+
effectClient: this.runtime.effects,
|
|
1987
|
+
clock: this.runtime.clock,
|
|
503
1988
|
logger
|
|
504
1989
|
});
|
|
505
1990
|
const iteratorResult = await workflowIterator.next();
|
|
@@ -508,117 +1993,107 @@ class WorkflowRunner {
|
|
|
508
1993
|
return stageResponse;
|
|
509
1994
|
}
|
|
510
1995
|
if (stageResponse instanceof StepDelay) {
|
|
511
|
-
return
|
|
1996
|
+
return stageResponse;
|
|
512
1997
|
}
|
|
513
|
-
if (stageResponse instanceof StepStoreWait) {
|
|
1998
|
+
if (stageResponse instanceof StepEffectWait || stageResponse instanceof StepStoreWait) {
|
|
514
1999
|
return stageResponse;
|
|
515
2000
|
}
|
|
516
2001
|
throw new Error("Workflow runner critical error. This is likely because your worklow and worker are referencing different versions of @yieldstar/core.");
|
|
517
2002
|
}
|
|
518
2003
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const err = new Error(error.message);
|
|
522
|
-
Error.captureStackTrace(err, fn);
|
|
523
|
-
err.stack = error.stack;
|
|
524
|
-
throw err;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// src/utils/map.ts
|
|
528
|
-
class ReadOnlyMap {
|
|
529
|
-
sourceMap;
|
|
530
|
-
constructor(sourceMap) {
|
|
531
|
-
if (sourceMap instanceof Map) {
|
|
532
|
-
this.sourceMap = sourceMap;
|
|
533
|
-
} else if (typeof sourceMap === "object" && sourceMap !== null) {
|
|
534
|
-
this.sourceMap = new Map(Object.entries(sourceMap));
|
|
535
|
-
} else {
|
|
536
|
-
this.sourceMap = new Map;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
get(key) {
|
|
540
|
-
return this.sourceMap.get(key);
|
|
541
|
-
}
|
|
542
|
-
has(key) {
|
|
543
|
-
return this.sourceMap.has(key);
|
|
544
|
-
}
|
|
545
|
-
get size() {
|
|
546
|
-
return this.sourceMap.size;
|
|
547
|
-
}
|
|
548
|
-
entries() {
|
|
549
|
-
return this.sourceMap.entries();
|
|
550
|
-
}
|
|
551
|
-
keys() {
|
|
552
|
-
return this.sourceMap.keys();
|
|
553
|
-
}
|
|
554
|
-
values() {
|
|
555
|
-
return this.sourceMap.values();
|
|
556
|
-
}
|
|
557
|
-
forEach(callbackfn, thisArg) {
|
|
558
|
-
return this.sourceMap.forEach(callbackfn, thisArg);
|
|
559
|
-
}
|
|
560
|
-
[Symbol.iterator]() {
|
|
561
|
-
return this.sourceMap[Symbol.iterator]();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
class FreezableMap extends Map {
|
|
566
|
-
frozen = false;
|
|
567
|
-
freeze() {
|
|
568
|
-
this.frozen = true;
|
|
569
|
-
}
|
|
570
|
-
isFrozen() {
|
|
571
|
-
return this.frozen;
|
|
572
|
-
}
|
|
573
|
-
set(key, value) {
|
|
574
|
-
if (this.frozen) {
|
|
575
|
-
const error = new Error("Cannot call set on a frozen map");
|
|
576
|
-
throw errorWithOriginalStack(error, this.set);
|
|
577
|
-
}
|
|
578
|
-
return super.set(key, value);
|
|
579
|
-
}
|
|
580
|
-
delete(key) {
|
|
581
|
-
if (this.frozen) {
|
|
582
|
-
const error = new Error("Cannot call delete on a frozen map");
|
|
583
|
-
throw errorWithOriginalStack(error, this.delete);
|
|
584
|
-
}
|
|
585
|
-
return super.delete(key);
|
|
586
|
-
}
|
|
587
|
-
clear() {
|
|
588
|
-
if (this.frozen) {
|
|
589
|
-
const error = new Error("Cannot call clear on a frozen map");
|
|
590
|
-
throw errorWithOriginalStack(error, this.clear);
|
|
591
|
-
}
|
|
592
|
-
return super.clear();
|
|
593
|
-
}
|
|
2004
|
+
function toError(value) {
|
|
2005
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
594
2006
|
}
|
|
595
2007
|
export {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
assertSynchronousUpdater,
|
|
607
|
-
assertSynchronousClaim,
|
|
608
|
-
WorkflowRunner,
|
|
609
|
-
WorkflowResult,
|
|
610
|
-
WorkflowRestart,
|
|
611
|
-
WorkflowDelay,
|
|
612
|
-
StoreClient,
|
|
613
|
-
StepStoreWait,
|
|
614
|
-
StepResult,
|
|
615
|
-
StepResponse,
|
|
616
|
-
StepKey,
|
|
617
|
-
StepInvalid,
|
|
618
|
-
StepError,
|
|
619
|
-
StepDelay,
|
|
620
|
-
StepCacheCheck,
|
|
621
|
-
ReadOnlyMap,
|
|
2008
|
+
CasStoreClient,
|
|
2009
|
+
DEFAULT_ABORT_GRACE_PERIOD,
|
|
2010
|
+
DEFAULT_DISPATCHER_CONCURRENCY,
|
|
2011
|
+
DEFAULT_LEASE_DURATION,
|
|
2012
|
+
DurableValueError,
|
|
2013
|
+
EffectClientInvariantError,
|
|
2014
|
+
EffectDeliveryError,
|
|
2015
|
+
ExecutionAdmissionError,
|
|
2016
|
+
ExecutionDispatcher,
|
|
2017
|
+
ExecutionEventEncodingError,
|
|
622
2018
|
FreezableMap,
|
|
623
|
-
|
|
2019
|
+
InfrastructureError,
|
|
2020
|
+
LeaseLostError,
|
|
2021
|
+
REDELIVERY_DELAY_BASE,
|
|
2022
|
+
REDELIVERY_DELAY_MAX,
|
|
2023
|
+
ReadOnlyMap,
|
|
2024
|
+
Runtime,
|
|
2025
|
+
StepCacheCheck,
|
|
2026
|
+
StepConflictError,
|
|
2027
|
+
StepDelay,
|
|
2028
|
+
StepEffectWait,
|
|
2029
|
+
StepError,
|
|
2030
|
+
StepInvalid,
|
|
2031
|
+
StepKey,
|
|
2032
|
+
StepResponse,
|
|
2033
|
+
StepResult,
|
|
2034
|
+
StepStoreWait,
|
|
2035
|
+
StoreClient,
|
|
2036
|
+
StoreValidationError,
|
|
2037
|
+
WORKFLOW_RESULT_STEP_KEY,
|
|
2038
|
+
WorkerExitedError,
|
|
2039
|
+
WorkflowDelay,
|
|
2040
|
+
WorkflowEffectError,
|
|
2041
|
+
WorkflowRestart,
|
|
2042
|
+
WorkflowResult,
|
|
2043
|
+
WorkflowRunner,
|
|
2044
|
+
WorkflowStartError,
|
|
2045
|
+
WorkflowValueError,
|
|
2046
|
+
acknowledgeExecution,
|
|
2047
|
+
assertSynchronousClaim,
|
|
2048
|
+
assertSynchronousUpdater,
|
|
2049
|
+
claimExecution,
|
|
2050
|
+
cloneStoreState,
|
|
2051
|
+
createEffectState,
|
|
2052
|
+
createExecutionState,
|
|
2053
|
+
createStartedExecution,
|
|
2054
|
+
decodeExecutionEvent,
|
|
2055
|
+
decodeExecutionOutcome,
|
|
2056
|
+
decodeParsedValue,
|
|
2057
|
+
decodeStoreValue,
|
|
2058
|
+
defineStore,
|
|
2059
|
+
diffStorePaths,
|
|
2060
|
+
durableValueCodec,
|
|
2061
|
+
effectNotFound,
|
|
2062
|
+
encodeExecutionEvent,
|
|
2063
|
+
encodeExecutionOutcome,
|
|
2064
|
+
encodeStoreValue,
|
|
2065
|
+
encodeThrownValue,
|
|
2066
|
+
errorWithOriginalStack,
|
|
2067
|
+
executionIdConflict,
|
|
2068
|
+
executionNotFound,
|
|
2069
|
+
executionStatusAt,
|
|
2070
|
+
isExecutionClaimable,
|
|
2071
|
+
isLeaseCurrent,
|
|
2072
|
+
isLeaseLost,
|
|
2073
|
+
isStoreSelectorMatch,
|
|
2074
|
+
isTerminalExecutionStatus,
|
|
2075
|
+
matchEffectBegin,
|
|
2076
|
+
matchStartRequest,
|
|
2077
|
+
nextClaimAt,
|
|
2078
|
+
prepareEffectBegin,
|
|
2079
|
+
prepareEffectSettle,
|
|
2080
|
+
prepareEffectWatch,
|
|
2081
|
+
prepareExecutionAdmission,
|
|
2082
|
+
prepareExecutionStart,
|
|
2083
|
+
redeliveryDelay,
|
|
2084
|
+
registerErrorClass,
|
|
2085
|
+
releaseExecutionLease,
|
|
2086
|
+
renewExecutionLease,
|
|
2087
|
+
requestExecutionWake,
|
|
2088
|
+
settleEffect,
|
|
2089
|
+
storePathsIntersect,
|
|
2090
|
+
toEffectRecord,
|
|
2091
|
+
toExecutionRecord,
|
|
2092
|
+
trackStoreSelector,
|
|
2093
|
+
trackStoreUpdater,
|
|
2094
|
+
unacknowledgedDeliveryCountAt,
|
|
2095
|
+
unwrapTrackedValue,
|
|
2096
|
+
validateEffectRequest,
|
|
2097
|
+
validateStoreState,
|
|
2098
|
+
waitForExecutionOutcome
|
|
624
2099
|
};
|