@yieldstar/core 0.5.0 → 0.6.1
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 +255 -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 +1653 -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 +112 -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 +315 -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 +138 -3
- package/src/lib/effect-state.ts +254 -0
- package/src/lib/execution-dispatcher.ts +408 -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,1362 @@
|
|
|
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
|
+
filter;
|
|
1075
|
+
running = false;
|
|
1076
|
+
looping = false;
|
|
1077
|
+
timer;
|
|
1078
|
+
inFlight = new Set;
|
|
1079
|
+
claiming = 0;
|
|
1080
|
+
constructor(runtime, options) {
|
|
1081
|
+
this.runtime = runtime;
|
|
1082
|
+
this.invoker = options.invoker;
|
|
1083
|
+
this.logger = options.logger;
|
|
1084
|
+
this.pollInterval = options.pollInterval ?? 10;
|
|
1085
|
+
this.keepAlive = options.keepAlive ?? true;
|
|
1086
|
+
this.concurrency = options.concurrency ?? DEFAULT_DISPATCHER_CONCURRENCY;
|
|
1087
|
+
this.leaseDuration = options.leaseDuration ?? DEFAULT_LEASE_DURATION;
|
|
1088
|
+
this.renewInterval = options.renewInterval ?? this.leaseDuration / 3;
|
|
1089
|
+
this.abortGracePeriod = options.abortGracePeriod ?? DEFAULT_ABORT_GRACE_PERIOD;
|
|
1090
|
+
this.filter = options.filter;
|
|
1091
|
+
if (!Number.isInteger(this.concurrency) || this.concurrency < 1) {
|
|
1092
|
+
throw new RangeError("concurrency must be a positive integer");
|
|
1093
|
+
}
|
|
1094
|
+
if (!(this.leaseDuration > 0)) {
|
|
1095
|
+
throw new RangeError("leaseDuration must be positive");
|
|
1096
|
+
}
|
|
1097
|
+
if (!(this.renewInterval > 0 && this.renewInterval < this.leaseDuration)) {
|
|
1098
|
+
throw new RangeError("renewInterval must be positive and shorter than leaseDuration");
|
|
1099
|
+
}
|
|
1100
|
+
if (!(this.abortGracePeriod >= 0)) {
|
|
1101
|
+
throw new RangeError("abortGracePeriod must not be negative");
|
|
1102
|
+
}
|
|
1103
|
+
if (this.filter?.executionIds?.length === 0) {
|
|
1104
|
+
throw new RangeError("filter.executionIds must not be empty");
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
start() {
|
|
1108
|
+
if (this.running)
|
|
1109
|
+
return;
|
|
1110
|
+
this.running = true;
|
|
1111
|
+
this.loop();
|
|
1112
|
+
}
|
|
1113
|
+
async stop() {
|
|
1114
|
+
this.running = false;
|
|
1115
|
+
if (this.timer)
|
|
1116
|
+
clearTimeout(this.timer);
|
|
1117
|
+
this.timer = undefined;
|
|
1118
|
+
await this.drain();
|
|
1119
|
+
}
|
|
1120
|
+
get activeDeliveries() {
|
|
1121
|
+
return this.inFlight.size;
|
|
1122
|
+
}
|
|
1123
|
+
async tick() {
|
|
1124
|
+
const slots = this.concurrency - this.inFlight.size - this.claiming;
|
|
1125
|
+
if (slots <= 0)
|
|
1126
|
+
return 0;
|
|
1127
|
+
this.claiming += slots;
|
|
1128
|
+
let leases;
|
|
1129
|
+
try {
|
|
1130
|
+
leases = await this.runtime.executions.claimDue({
|
|
1131
|
+
limit: slots,
|
|
1132
|
+
duration: this.leaseDuration,
|
|
1133
|
+
filter: this.filter
|
|
1134
|
+
});
|
|
1135
|
+
} finally {
|
|
1136
|
+
this.claiming -= slots;
|
|
1137
|
+
}
|
|
1138
|
+
for (const lease of leases) {
|
|
1139
|
+
const task = this.dispatch(lease).finally(() => {
|
|
1140
|
+
this.inFlight.delete(task);
|
|
1141
|
+
if (this.running && !this.looping)
|
|
1142
|
+
this.schedule(0);
|
|
1143
|
+
});
|
|
1144
|
+
this.inFlight.add(task);
|
|
1145
|
+
}
|
|
1146
|
+
return leases.length;
|
|
1147
|
+
}
|
|
1148
|
+
async drain() {
|
|
1149
|
+
while (this.inFlight.size > 0) {
|
|
1150
|
+
await Promise.allSettled([...this.inFlight]);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
schedule(delay) {
|
|
1154
|
+
if (this.timer)
|
|
1155
|
+
clearTimeout(this.timer);
|
|
1156
|
+
this.timer = setTimeout(() => {
|
|
1157
|
+
this.timer = undefined;
|
|
1158
|
+
this.loop();
|
|
1159
|
+
}, delay);
|
|
1160
|
+
if (!this.keepAlive)
|
|
1161
|
+
this.timer.unref?.();
|
|
1162
|
+
}
|
|
1163
|
+
async loop() {
|
|
1164
|
+
if (!this.running || this.looping)
|
|
1165
|
+
return;
|
|
1166
|
+
this.looping = true;
|
|
1167
|
+
let claimed = 0;
|
|
1168
|
+
try {
|
|
1169
|
+
claimed = await this.tick();
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
this.logger.error({ err }, "Failed to claim due executions");
|
|
1172
|
+
} finally {
|
|
1173
|
+
this.looping = false;
|
|
1174
|
+
}
|
|
1175
|
+
if (!this.running)
|
|
1176
|
+
return;
|
|
1177
|
+
if (this.inFlight.size + this.claiming >= this.concurrency)
|
|
1178
|
+
return;
|
|
1179
|
+
this.schedule(claimed > 0 ? 0 : this.pollInterval);
|
|
1180
|
+
}
|
|
1181
|
+
async dispatch(lease) {
|
|
1182
|
+
const { executionId } = lease;
|
|
1183
|
+
const { executions } = this.runtime;
|
|
1184
|
+
const renewal = this.renew(lease);
|
|
1185
|
+
try {
|
|
1186
|
+
const record = await executions.get(executionId);
|
|
1187
|
+
if (!record) {
|
|
1188
|
+
this.logger.error({ executionId }, "Claimed execution has no record");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (isTerminalExecutionStatus(record.status)) {
|
|
1192
|
+
this.emit(executionId, record.outcome);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
let outcome;
|
|
1196
|
+
try {
|
|
1197
|
+
outcome = await this.abandonAfterAbort(this.invoker.deliver({
|
|
1198
|
+
workflowId: record.workflowId,
|
|
1199
|
+
executionId,
|
|
1200
|
+
params: record.params,
|
|
1201
|
+
context: record.context
|
|
1202
|
+
}, { lease, signal: renewal.signal }), lease, renewal.signal);
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
const current = await renewal.stop();
|
|
1205
|
+
if (!current)
|
|
1206
|
+
return;
|
|
1207
|
+
if (WorkerExitedError.is(err)) {
|
|
1208
|
+
this.logger.error({ executionId, err }, "Worker exited without replying; the lease will expire and the execution will be redelivered");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
this.logger.error({ executionId, err }, "Delivery failed without an outcome; releasing its lease for redelivery");
|
|
1212
|
+
const released = await executions.release({ lease: current });
|
|
1213
|
+
if (released.status === "lost")
|
|
1214
|
+
this.warnLost(lease);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (outcome === ABANDONED)
|
|
1218
|
+
return;
|
|
1219
|
+
const current = await renewal.stop();
|
|
1220
|
+
if (!current)
|
|
1221
|
+
return;
|
|
1222
|
+
const result = await executions.acknowledge({ lease: current, outcome });
|
|
1223
|
+
if (result.status === "lost") {
|
|
1224
|
+
this.warnLost(lease);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
this.emit(executionId, result);
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
this.logger.error({ executionId, err }, "Failed to dispatch execution; the lease will expire and the execution will be redelivered");
|
|
1230
|
+
} finally {
|
|
1231
|
+
await renewal.stop();
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
abandonAfterAbort(delivery, lease, signal) {
|
|
1235
|
+
return new Promise((resolve, reject) => {
|
|
1236
|
+
let abandoned = false;
|
|
1237
|
+
let timer;
|
|
1238
|
+
const onAbort = () => {
|
|
1239
|
+
timer = setTimeout(() => {
|
|
1240
|
+
abandoned = true;
|
|
1241
|
+
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");
|
|
1242
|
+
resolve(ABANDONED);
|
|
1243
|
+
}, this.abortGracePeriod);
|
|
1244
|
+
if (!this.keepAlive)
|
|
1245
|
+
timer.unref?.();
|
|
1246
|
+
};
|
|
1247
|
+
const settle = () => {
|
|
1248
|
+
clearTimeout(timer);
|
|
1249
|
+
signal.removeEventListener("abort", onAbort);
|
|
1250
|
+
};
|
|
1251
|
+
if (signal.aborted)
|
|
1252
|
+
onAbort();
|
|
1253
|
+
else
|
|
1254
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1255
|
+
delivery.then((value) => {
|
|
1256
|
+
settle();
|
|
1257
|
+
if (abandoned) {
|
|
1258
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token }, "Abandoned delivery settled late; its outcome is ignored");
|
|
1259
|
+
} else
|
|
1260
|
+
resolve(value);
|
|
1261
|
+
}, (err) => {
|
|
1262
|
+
settle();
|
|
1263
|
+
if (abandoned) {
|
|
1264
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token, err }, "Abandoned delivery rejected late; ignored");
|
|
1265
|
+
} else
|
|
1266
|
+
reject(err);
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
renew(lease) {
|
|
1271
|
+
const controller = new AbortController;
|
|
1272
|
+
let current = lease;
|
|
1273
|
+
let pending;
|
|
1274
|
+
let stopped = false;
|
|
1275
|
+
const { executions } = this.runtime;
|
|
1276
|
+
const timer = setInterval(() => {
|
|
1277
|
+
if (stopped || pending || !current)
|
|
1278
|
+
return;
|
|
1279
|
+
const renewing = current;
|
|
1280
|
+
pending = executions.renew({ lease: renewing, duration: this.leaseDuration }).then((result) => {
|
|
1281
|
+
if (result.status === "lost") {
|
|
1282
|
+
current = undefined;
|
|
1283
|
+
clearInterval(timer);
|
|
1284
|
+
this.warnLost(lease);
|
|
1285
|
+
controller.abort(new LeaseLostError(lease.executionId));
|
|
1286
|
+
} else if (current) {
|
|
1287
|
+
current = result.lease;
|
|
1288
|
+
}
|
|
1289
|
+
}, (err) => {
|
|
1290
|
+
this.logger.error({ executionId: lease.executionId, err }, "Failed to renew lease");
|
|
1291
|
+
}).finally(() => {
|
|
1292
|
+
pending = undefined;
|
|
1293
|
+
});
|
|
1294
|
+
}, this.renewInterval);
|
|
1295
|
+
if (!this.keepAlive)
|
|
1296
|
+
timer.unref?.();
|
|
1297
|
+
return {
|
|
1298
|
+
signal: controller.signal,
|
|
1299
|
+
async stop() {
|
|
1300
|
+
stopped = true;
|
|
1301
|
+
clearInterval(timer);
|
|
1302
|
+
await pending;
|
|
1303
|
+
return current;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
warnLost(lease) {
|
|
1308
|
+
this.logger.warn({ executionId: lease.executionId, token: lease.token }, "Lease was lost to another claim; this delivery stops without an outcome");
|
|
1309
|
+
}
|
|
1310
|
+
emit(executionId, result) {
|
|
1311
|
+
if (result.status === "completed") {
|
|
1312
|
+
this.invoker.workflowEndEmitter.emit(executionId, result.result);
|
|
1313
|
+
} else if (result.status === "failed") {
|
|
1314
|
+
this.invoker.workflowEndEmitter.emit(executionId, result.error);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
// src/lib/wait-for-outcome.ts
|
|
1319
|
+
async function waitForExecutionOutcome(executions, executionId, options) {
|
|
1320
|
+
const pollInterval = options?.pollInterval ?? 25;
|
|
1321
|
+
const emitter = options?.emitter;
|
|
1322
|
+
let wake;
|
|
1323
|
+
const nudge = () => wake?.();
|
|
1324
|
+
emitter?.on(executionId, nudge);
|
|
1325
|
+
try {
|
|
1326
|
+
while (true) {
|
|
1327
|
+
options?.signal?.throwIfAborted();
|
|
1328
|
+
const record = await executions.get(executionId);
|
|
1329
|
+
if (!record)
|
|
1330
|
+
return;
|
|
1331
|
+
if (isTerminalExecutionStatus(record.status))
|
|
1332
|
+
return record.outcome;
|
|
1333
|
+
await new Promise((resolve) => {
|
|
1334
|
+
const timer = setTimeout(resolve, pollInterval);
|
|
1335
|
+
wake = () => {
|
|
1336
|
+
clearTimeout(timer);
|
|
1337
|
+
resolve();
|
|
1338
|
+
};
|
|
1339
|
+
});
|
|
1340
|
+
wake = undefined;
|
|
1341
|
+
}
|
|
1342
|
+
} finally {
|
|
1343
|
+
emitter?.off(executionId, nudge);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1
1346
|
// src/base/store.ts
|
|
2
|
-
|
|
3
|
-
|
|
1347
|
+
class StoreValidationError extends Error {
|
|
1348
|
+
constructor(message) {
|
|
1349
|
+
super(message);
|
|
1350
|
+
this.name = "StoreValidationError";
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
registerErrorClass("StoreValidationError", StoreValidationError);
|
|
1354
|
+
function defineStore(name, schema, options) {
|
|
1355
|
+
return {
|
|
1356
|
+
name,
|
|
1357
|
+
schema,
|
|
1358
|
+
initial: options.initial
|
|
1359
|
+
};
|
|
4
1360
|
}
|
|
5
1361
|
|
|
6
1362
|
class StoreClient {
|
|
@@ -31,17 +1387,21 @@ class StoreClient {
|
|
|
31
1387
|
}
|
|
32
1388
|
|
|
33
1389
|
class CasStoreClient extends StoreClient {
|
|
1390
|
+
createInstanceId() {
|
|
1391
|
+
return crypto.randomUUID();
|
|
1392
|
+
}
|
|
1393
|
+
async getStore(params) {
|
|
1394
|
+
return this.getOrInsertStore(params.definition, params.id);
|
|
1395
|
+
}
|
|
34
1396
|
async updateStore(params) {
|
|
35
1397
|
const applied = await this.getAppliedResult(params);
|
|
36
1398
|
if (applied) {
|
|
37
1399
|
return applied.result;
|
|
38
1400
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
id: params.id
|
|
42
|
-
});
|
|
1401
|
+
const stepKey = params.stepId?.stepKey;
|
|
1402
|
+
let snapshot = await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
43
1403
|
while (true) {
|
|
44
|
-
const prepared = await prepareStoreUpdate(params.definition, snapshot, params.updater);
|
|
1404
|
+
const prepared = await prepareStoreUpdate(params.definition, snapshot, params.updater, stepKey);
|
|
45
1405
|
const result = {
|
|
46
1406
|
state: cloneStoreState(prepared.nextState),
|
|
47
1407
|
previousVersion: snapshot.version,
|
|
@@ -61,7 +1421,7 @@ class CasStoreClient extends StoreClient {
|
|
|
61
1421
|
if (committed.status === "already-applied") {
|
|
62
1422
|
return committed.result;
|
|
63
1423
|
}
|
|
64
|
-
snapshot = committed.snapshot;
|
|
1424
|
+
snapshot = committed.status === "conflict" ? existing(committed.snapshot) : await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
65
1425
|
}
|
|
66
1426
|
}
|
|
67
1427
|
async updateStoreFrom(params) {
|
|
@@ -70,14 +1430,12 @@ class CasStoreClient extends StoreClient {
|
|
|
70
1430
|
return applied.result;
|
|
71
1431
|
}
|
|
72
1432
|
assertStoreSnapshotInstanceId(params.snapshot);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
id: params.id
|
|
76
|
-
});
|
|
1433
|
+
const stepKey = params.stepId?.stepKey;
|
|
1434
|
+
const current = await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
77
1435
|
if (!sameStoreVersion(current, params.snapshot)) {
|
|
78
1436
|
return storeUpdateConflict(params.snapshot, current);
|
|
79
1437
|
}
|
|
80
|
-
const prepared = await prepareStoreUpdate(params.definition, params.snapshot, params.updater);
|
|
1438
|
+
const prepared = await prepareStoreUpdate(params.definition, params.snapshot, params.updater, stepKey);
|
|
81
1439
|
const result = {
|
|
82
1440
|
updated: true,
|
|
83
1441
|
state: cloneStoreState(prepared.nextState),
|
|
@@ -87,7 +1445,7 @@ class CasStoreClient extends StoreClient {
|
|
|
87
1445
|
const committed = await this.commitStoreMutation({
|
|
88
1446
|
definition: params.definition,
|
|
89
1447
|
id: params.id,
|
|
90
|
-
expected: params.snapshot,
|
|
1448
|
+
expected: existing(params.snapshot),
|
|
91
1449
|
nextState: prepared.nextState,
|
|
92
1450
|
changedPaths: prepared.changedPaths,
|
|
93
1451
|
stepId: params.stepId,
|
|
@@ -98,22 +1456,63 @@ class CasStoreClient extends StoreClient {
|
|
|
98
1456
|
if (committed.status === "already-applied") {
|
|
99
1457
|
return committed.result;
|
|
100
1458
|
}
|
|
101
|
-
return storeUpdateConflict(params.snapshot, committed.snapshot);
|
|
1459
|
+
return storeUpdateConflict(params.snapshot, committed.status === "conflict" ? committed.snapshot : await this.getOrInsertStore(params.definition, params.id, stepKey));
|
|
1460
|
+
}
|
|
1461
|
+
async deleteStoreFrom(params) {
|
|
1462
|
+
const applied = await this.getAppliedResult(params);
|
|
1463
|
+
if (applied)
|
|
1464
|
+
return applied.result;
|
|
1465
|
+
assertStoreSnapshotInstanceId(params.snapshot);
|
|
1466
|
+
const stepKey = params.stepId?.stepKey;
|
|
1467
|
+
let current = await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
1468
|
+
while (sameStoreVersion(current, params.snapshot)) {
|
|
1469
|
+
const result = { deleted: true };
|
|
1470
|
+
const committed = await this.commitStoreDeletion({
|
|
1471
|
+
definition: params.definition,
|
|
1472
|
+
id: params.id,
|
|
1473
|
+
expected: params.snapshot,
|
|
1474
|
+
stepId: params.stepId,
|
|
1475
|
+
result
|
|
1476
|
+
});
|
|
1477
|
+
if (committed.status === "committed")
|
|
1478
|
+
return result;
|
|
1479
|
+
if (committed.status === "already-applied") {
|
|
1480
|
+
return committed.result;
|
|
1481
|
+
}
|
|
1482
|
+
current = committed.status === "conflict" ? committed.snapshot : await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
deleted: false,
|
|
1486
|
+
reason: "conflict",
|
|
1487
|
+
expectedInstanceId: params.snapshot.instanceId,
|
|
1488
|
+
actualInstanceId: current.instanceId,
|
|
1489
|
+
expectedVersion: params.snapshot.version,
|
|
1490
|
+
actualVersion: current.version
|
|
1491
|
+
};
|
|
102
1492
|
}
|
|
103
1493
|
async takeFromStore(params) {
|
|
104
1494
|
const applied = await this.getAppliedResult(params);
|
|
105
1495
|
if (applied)
|
|
106
1496
|
return applied.result;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
id: params.id
|
|
110
|
-
});
|
|
1497
|
+
const stepKey = params.stepId?.stepKey;
|
|
1498
|
+
let snapshot = await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
111
1499
|
while (true) {
|
|
112
1500
|
const previousState = cloneStoreState(snapshot.state);
|
|
113
1501
|
const draft = cloneStoreState(previousState);
|
|
114
1502
|
const tracked = trackStoreUpdater(draft);
|
|
115
1503
|
const { result: selectedResult, readPaths } = trackStoreSelector(tracked.draft, params.selector);
|
|
116
1504
|
if (!isStoreSelectorMatch(selectedResult)) {
|
|
1505
|
+
if (snapshot.created) {
|
|
1506
|
+
const stored = await this.insertStore({
|
|
1507
|
+
definition: params.definition,
|
|
1508
|
+
id: params.id,
|
|
1509
|
+
snapshot: detach(snapshot)
|
|
1510
|
+
});
|
|
1511
|
+
if (stored.instanceId !== snapshot.instanceId) {
|
|
1512
|
+
snapshot = existing(stored);
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
117
1516
|
return {
|
|
118
1517
|
matched: false,
|
|
119
1518
|
instanceId: snapshot.instanceId,
|
|
@@ -127,8 +1526,10 @@ class CasStoreClient extends StoreClient {
|
|
|
127
1526
|
definition: params.definition,
|
|
128
1527
|
previousState,
|
|
129
1528
|
draft,
|
|
130
|
-
tracked
|
|
1529
|
+
tracked,
|
|
1530
|
+
stepKey
|
|
131
1531
|
});
|
|
1532
|
+
assertDurableStoreValue(selected, stepKey ?? "", `Store take selection for "${params.definition.name}"`);
|
|
132
1533
|
const selectedSnapshot = cloneStoreState(selected);
|
|
133
1534
|
const result = {
|
|
134
1535
|
matched: true,
|
|
@@ -150,9 +1551,31 @@ class CasStoreClient extends StoreClient {
|
|
|
150
1551
|
if (committed.status === "already-applied") {
|
|
151
1552
|
return committed.result;
|
|
152
1553
|
}
|
|
153
|
-
snapshot = committed.snapshot;
|
|
1554
|
+
snapshot = committed.status === "conflict" ? existing(committed.snapshot) : await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
154
1555
|
}
|
|
155
1556
|
}
|
|
1557
|
+
async readOrInitialize(definition, id, stepKey) {
|
|
1558
|
+
const snapshot = await this.readStore({ definition, id });
|
|
1559
|
+
if (snapshot)
|
|
1560
|
+
return existing(snapshot);
|
|
1561
|
+
const state = await validateStoreState(definition, definition.initial(id), stepKey);
|
|
1562
|
+
return {
|
|
1563
|
+
state,
|
|
1564
|
+
instanceId: this.createInstanceId(),
|
|
1565
|
+
version: 0,
|
|
1566
|
+
created: true
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
async getOrInsertStore(definition, id, stepKey) {
|
|
1570
|
+
const snapshot = await this.readOrInitialize(definition, id, stepKey);
|
|
1571
|
+
if (!snapshot.created)
|
|
1572
|
+
return detach(snapshot);
|
|
1573
|
+
return await this.insertStore({
|
|
1574
|
+
definition,
|
|
1575
|
+
id,
|
|
1576
|
+
snapshot: detach(snapshot)
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
156
1579
|
getAppliedResult(params) {
|
|
157
1580
|
return params.stepId ? this.getAppliedStoreStep({
|
|
158
1581
|
definition: params.definition,
|
|
@@ -161,7 +1584,22 @@ class CasStoreClient extends StoreClient {
|
|
|
161
1584
|
}) : Promise.resolve(undefined);
|
|
162
1585
|
}
|
|
163
1586
|
}
|
|
164
|
-
|
|
1587
|
+
function existing(snapshot) {
|
|
1588
|
+
return {
|
|
1589
|
+
state: snapshot.state,
|
|
1590
|
+
instanceId: snapshot.instanceId,
|
|
1591
|
+
version: snapshot.version,
|
|
1592
|
+
created: false
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
function detach(snapshot) {
|
|
1596
|
+
return {
|
|
1597
|
+
state: snapshot.state,
|
|
1598
|
+
instanceId: snapshot.instanceId,
|
|
1599
|
+
version: snapshot.version
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
async function prepareStoreUpdate(definition, snapshot, updater, stepKey) {
|
|
165
1603
|
const previousState = cloneStoreState(snapshot.state);
|
|
166
1604
|
const draft = cloneStoreState(previousState);
|
|
167
1605
|
const tracked = trackStoreUpdater(draft);
|
|
@@ -173,12 +1611,13 @@ async function prepareStoreUpdate(definition, snapshot, updater) {
|
|
|
173
1611
|
previousState,
|
|
174
1612
|
draft,
|
|
175
1613
|
tracked,
|
|
176
|
-
replacement: returned
|
|
1614
|
+
replacement: returned,
|
|
1615
|
+
stepKey
|
|
177
1616
|
});
|
|
178
1617
|
}
|
|
179
1618
|
async function finalizeStoreMutation(params) {
|
|
180
1619
|
const isReplacement = params.replacement !== undefined && params.replacement !== params.draft;
|
|
181
|
-
const nextState = await validateStoreState(params.definition, isReplacement ? params.replacement : params.draft);
|
|
1620
|
+
const nextState = await validateStoreState(params.definition, isReplacement ? params.replacement : params.draft, params.stepKey);
|
|
182
1621
|
const changedPaths = !isReplacement && nextState === params.draft ? params.tracked.writePaths() : diffStorePaths(params.previousState, nextState);
|
|
183
1622
|
return { nextState, changedPaths };
|
|
184
1623
|
}
|
|
@@ -212,15 +1651,42 @@ function assertSynchronousUpdater(updaterResult) {
|
|
|
212
1651
|
throw new Error("Store updaters must be synchronous");
|
|
213
1652
|
}
|
|
214
1653
|
}
|
|
215
|
-
async function validateStoreState(definition, state) {
|
|
1654
|
+
async function validateStoreState(definition, state, stepKey = "") {
|
|
216
1655
|
const result = await definition.schema["~standard"].validate(state);
|
|
217
1656
|
if ("issues" in result && result.issues) {
|
|
218
1657
|
const messages = result.issues.map((issue) => issue.message).join("; ");
|
|
219
|
-
throw new
|
|
1658
|
+
throw new StoreValidationError(`Invalid store state for "${definition.name}": ${messages}`);
|
|
220
1659
|
}
|
|
1660
|
+
assertDurableStoreValue(result.value, stepKey, `Store state for "${definition.name}"`);
|
|
221
1661
|
return result.value;
|
|
222
1662
|
}
|
|
1663
|
+
function assertDurableStoreValue(value, stepKey, label) {
|
|
1664
|
+
const encoding = durableValueCodec.encode(value);
|
|
1665
|
+
if (encoding.status === "invalid") {
|
|
1666
|
+
throw new WorkflowValueError("INVALID_STEP_VALUE", stepKey, `${label} is not a durable value: ${encoding.message}`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function encodeStoreValue(value) {
|
|
1670
|
+
const encoding = durableValueCodec.encode(value);
|
|
1671
|
+
if (encoding.status === "invalid") {
|
|
1672
|
+
throw new Error(`Store value is not a durable value: ${encoding.message}`);
|
|
1673
|
+
}
|
|
1674
|
+
return encoding.canonical;
|
|
1675
|
+
}
|
|
1676
|
+
function decodeStoreValue(canonical) {
|
|
1677
|
+
const decoding = durableValueCodec.decode(canonical);
|
|
1678
|
+
if (decoding.status === "invalid") {
|
|
1679
|
+
throw new Error(`Invalid store value encoding: ${decoding.message}`);
|
|
1680
|
+
}
|
|
1681
|
+
return decoding.value;
|
|
1682
|
+
}
|
|
223
1683
|
function cloneStoreState(state) {
|
|
1684
|
+
const encoding = durableValueCodec.encode(state);
|
|
1685
|
+
if (encoding.status === "encoded") {
|
|
1686
|
+
const decoding = durableValueCodec.decode(encoding.canonical);
|
|
1687
|
+
if (decoding.status === "decoded")
|
|
1688
|
+
return decoding.value;
|
|
1689
|
+
}
|
|
224
1690
|
if (typeof structuredClone === "function") {
|
|
225
1691
|
try {
|
|
226
1692
|
return structuredClone(state);
|
|
@@ -310,29 +1776,29 @@ function trackStoreUpdater(target) {
|
|
|
310
1776
|
if (cached)
|
|
311
1777
|
return cached;
|
|
312
1778
|
const proxy = new Proxy(value, {
|
|
313
|
-
get(
|
|
1779
|
+
get(target, property, receiver) {
|
|
314
1780
|
if (typeof property === "symbol") {
|
|
315
1781
|
if (property === TRACKING_TARGET)
|
|
316
|
-
return
|
|
317
|
-
return Reflect.get(
|
|
1782
|
+
return target;
|
|
1783
|
+
return Reflect.get(target, property, receiver);
|
|
318
1784
|
}
|
|
319
|
-
const child = Reflect.get(
|
|
1785
|
+
const child = Reflect.get(target, property);
|
|
320
1786
|
return track(child, [...path, arrayKeyToPathSegment(property)]);
|
|
321
1787
|
},
|
|
322
|
-
set(
|
|
1788
|
+
set(target, property, value) {
|
|
323
1789
|
record(path, property);
|
|
324
|
-
return Reflect.set(
|
|
1790
|
+
return Reflect.set(target, property, unwrapTrackedValue(value));
|
|
325
1791
|
},
|
|
326
|
-
deleteProperty(
|
|
1792
|
+
deleteProperty(target, property) {
|
|
327
1793
|
record(path, property);
|
|
328
|
-
return Reflect.deleteProperty(
|
|
1794
|
+
return Reflect.deleteProperty(target, property);
|
|
329
1795
|
},
|
|
330
|
-
defineProperty(
|
|
1796
|
+
defineProperty(target, property, descriptor) {
|
|
331
1797
|
record(path, property);
|
|
332
1798
|
if ("value" in descriptor) {
|
|
333
1799
|
descriptor = { ...descriptor, value: unwrapTrackedValue(descriptor.value) };
|
|
334
1800
|
}
|
|
335
|
-
return Reflect.defineProperty(
|
|
1801
|
+
return Reflect.defineProperty(target, property, descriptor);
|
|
336
1802
|
}
|
|
337
1803
|
});
|
|
338
1804
|
proxies.set(value, proxy);
|
|
@@ -391,9 +1857,12 @@ class StepResult extends StepResponse {
|
|
|
391
1857
|
static type = "step-result";
|
|
392
1858
|
type = "step-result";
|
|
393
1859
|
result;
|
|
394
|
-
|
|
1860
|
+
schema;
|
|
1861
|
+
constructor(result, schema) {
|
|
395
1862
|
super();
|
|
396
1863
|
this.result = result;
|
|
1864
|
+
if (schema !== undefined)
|
|
1865
|
+
this.schema = schema;
|
|
397
1866
|
}
|
|
398
1867
|
}
|
|
399
1868
|
|
|
@@ -410,10 +1879,10 @@ class WorkflowResult extends StepResponse {
|
|
|
410
1879
|
class WorkflowDelay extends StepResponse {
|
|
411
1880
|
static type = "workflow-delay";
|
|
412
1881
|
type = "workflow-delay";
|
|
413
|
-
|
|
414
|
-
constructor(
|
|
1882
|
+
wakeAt;
|
|
1883
|
+
constructor(wakeAt) {
|
|
415
1884
|
super();
|
|
416
|
-
this.
|
|
1885
|
+
this.wakeAt = wakeAt;
|
|
417
1886
|
}
|
|
418
1887
|
}
|
|
419
1888
|
|
|
@@ -425,6 +1894,16 @@ class StepStoreWait extends StepResponse {
|
|
|
425
1894
|
}
|
|
426
1895
|
}
|
|
427
1896
|
|
|
1897
|
+
class StepEffectWait extends StepResponse {
|
|
1898
|
+
static type = "effect-wait";
|
|
1899
|
+
type = "effect-wait";
|
|
1900
|
+
wakeAt;
|
|
1901
|
+
constructor(wakeAt) {
|
|
1902
|
+
super();
|
|
1903
|
+
this.wakeAt = wakeAt;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
428
1907
|
class WorkflowRestart extends StepResponse {
|
|
429
1908
|
static type = "workflow-restart";
|
|
430
1909
|
type = "workflow-restart";
|
|
@@ -452,54 +1931,66 @@ class StepError extends StepResponse {
|
|
|
452
1931
|
class StepDelay extends StepResponse {
|
|
453
1932
|
static type = "step-delay";
|
|
454
1933
|
type = "step-delay";
|
|
455
|
-
|
|
456
|
-
constructor(
|
|
1934
|
+
wakeAt;
|
|
1935
|
+
constructor(wakeAt) {
|
|
457
1936
|
super();
|
|
458
|
-
this.
|
|
1937
|
+
this.wakeAt = wakeAt;
|
|
459
1938
|
}
|
|
460
1939
|
}
|
|
461
1940
|
// src/lib/workflow-runner.ts
|
|
462
1941
|
class WorkflowRunner {
|
|
463
|
-
|
|
464
|
-
schedulerClient;
|
|
465
|
-
storeClient;
|
|
1942
|
+
runtime;
|
|
466
1943
|
router;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
471
|
-
this.
|
|
1944
|
+
logger;
|
|
1945
|
+
constructor(runtime, options) {
|
|
1946
|
+
this.runtime = runtime;
|
|
1947
|
+
this.router = options.router;
|
|
1948
|
+
this.logger = options.logger;
|
|
472
1949
|
}
|
|
473
|
-
run = async (event,
|
|
1950
|
+
run = async (event, options) => {
|
|
1951
|
+
const { lease, signal, logger = this.logger } = options;
|
|
474
1952
|
const workflow = this.router[event.workflowId];
|
|
475
1953
|
if (!workflow) {
|
|
476
|
-
|
|
1954
|
+
return {
|
|
1955
|
+
status: "failed",
|
|
1956
|
+
error: new Error(`No workflow registered for "${event.workflowId}"`)
|
|
1957
|
+
};
|
|
477
1958
|
}
|
|
1959
|
+
let response;
|
|
478
1960
|
try {
|
|
479
|
-
|
|
1961
|
+
response = await this.runWorkflows({
|
|
480
1962
|
workflow,
|
|
481
1963
|
logger,
|
|
482
|
-
event
|
|
1964
|
+
event,
|
|
1965
|
+
lease,
|
|
1966
|
+
signal
|
|
483
1967
|
});
|
|
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
1968
|
} catch (err) {
|
|
494
|
-
|
|
1969
|
+
if (InfrastructureError.is(err))
|
|
1970
|
+
throw err;
|
|
1971
|
+
return { status: "failed", error: toError(err) };
|
|
1972
|
+
}
|
|
1973
|
+
switch (response.type) {
|
|
1974
|
+
case "workflow-result":
|
|
1975
|
+
return { status: "completed", result: response.result };
|
|
1976
|
+
case "step-delay":
|
|
1977
|
+
case "effect-wait":
|
|
1978
|
+
return { status: "suspended", wakeAt: response.wakeAt };
|
|
1979
|
+
case "store-wait":
|
|
1980
|
+
return { status: "suspended" };
|
|
495
1981
|
}
|
|
496
1982
|
};
|
|
497
1983
|
async runWorkflows(params) {
|
|
498
|
-
const { workflow, event, logger } = params;
|
|
1984
|
+
const { workflow, event, lease, signal, logger } = params;
|
|
499
1985
|
const workflowIterator = workflow({
|
|
500
1986
|
event,
|
|
501
|
-
|
|
502
|
-
|
|
1987
|
+
lease,
|
|
1988
|
+
signal,
|
|
1989
|
+
heapClient: this.runtime.heap,
|
|
1990
|
+
storeClient: this.runtime.stores,
|
|
1991
|
+
executionClient: this.runtime.executions,
|
|
1992
|
+
effectClient: this.runtime.effects,
|
|
1993
|
+
clock: this.runtime.clock,
|
|
503
1994
|
logger
|
|
504
1995
|
});
|
|
505
1996
|
const iteratorResult = await workflowIterator.next();
|
|
@@ -508,117 +1999,107 @@ class WorkflowRunner {
|
|
|
508
1999
|
return stageResponse;
|
|
509
2000
|
}
|
|
510
2001
|
if (stageResponse instanceof StepDelay) {
|
|
511
|
-
return
|
|
2002
|
+
return stageResponse;
|
|
512
2003
|
}
|
|
513
|
-
if (stageResponse instanceof StepStoreWait) {
|
|
2004
|
+
if (stageResponse instanceof StepEffectWait || stageResponse instanceof StepStoreWait) {
|
|
514
2005
|
return stageResponse;
|
|
515
2006
|
}
|
|
516
2007
|
throw new Error("Workflow runner critical error. This is likely because your worklow and worker are referencing different versions of @yieldstar/core.");
|
|
517
2008
|
}
|
|
518
2009
|
}
|
|
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
|
-
}
|
|
2010
|
+
function toError(value) {
|
|
2011
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
594
2012
|
}
|
|
595
2013
|
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,
|
|
2014
|
+
CasStoreClient,
|
|
2015
|
+
DEFAULT_ABORT_GRACE_PERIOD,
|
|
2016
|
+
DEFAULT_DISPATCHER_CONCURRENCY,
|
|
2017
|
+
DEFAULT_LEASE_DURATION,
|
|
2018
|
+
DurableValueError,
|
|
2019
|
+
EffectClientInvariantError,
|
|
2020
|
+
EffectDeliveryError,
|
|
2021
|
+
ExecutionAdmissionError,
|
|
2022
|
+
ExecutionDispatcher,
|
|
2023
|
+
ExecutionEventEncodingError,
|
|
622
2024
|
FreezableMap,
|
|
623
|
-
|
|
2025
|
+
InfrastructureError,
|
|
2026
|
+
LeaseLostError,
|
|
2027
|
+
REDELIVERY_DELAY_BASE,
|
|
2028
|
+
REDELIVERY_DELAY_MAX,
|
|
2029
|
+
ReadOnlyMap,
|
|
2030
|
+
Runtime,
|
|
2031
|
+
StepCacheCheck,
|
|
2032
|
+
StepConflictError,
|
|
2033
|
+
StepDelay,
|
|
2034
|
+
StepEffectWait,
|
|
2035
|
+
StepError,
|
|
2036
|
+
StepInvalid,
|
|
2037
|
+
StepKey,
|
|
2038
|
+
StepResponse,
|
|
2039
|
+
StepResult,
|
|
2040
|
+
StepStoreWait,
|
|
2041
|
+
StoreClient,
|
|
2042
|
+
StoreValidationError,
|
|
2043
|
+
WORKFLOW_RESULT_STEP_KEY,
|
|
2044
|
+
WorkerExitedError,
|
|
2045
|
+
WorkflowDelay,
|
|
2046
|
+
WorkflowEffectError,
|
|
2047
|
+
WorkflowRestart,
|
|
2048
|
+
WorkflowResult,
|
|
2049
|
+
WorkflowRunner,
|
|
2050
|
+
WorkflowStartError,
|
|
2051
|
+
WorkflowValueError,
|
|
2052
|
+
acknowledgeExecution,
|
|
2053
|
+
assertSynchronousClaim,
|
|
2054
|
+
assertSynchronousUpdater,
|
|
2055
|
+
claimExecution,
|
|
2056
|
+
cloneStoreState,
|
|
2057
|
+
createEffectState,
|
|
2058
|
+
createExecutionState,
|
|
2059
|
+
createStartedExecution,
|
|
2060
|
+
decodeExecutionEvent,
|
|
2061
|
+
decodeExecutionOutcome,
|
|
2062
|
+
decodeParsedValue,
|
|
2063
|
+
decodeStoreValue,
|
|
2064
|
+
defineStore,
|
|
2065
|
+
diffStorePaths,
|
|
2066
|
+
durableValueCodec,
|
|
2067
|
+
effectNotFound,
|
|
2068
|
+
encodeExecutionEvent,
|
|
2069
|
+
encodeExecutionOutcome,
|
|
2070
|
+
encodeStoreValue,
|
|
2071
|
+
encodeThrownValue,
|
|
2072
|
+
errorWithOriginalStack,
|
|
2073
|
+
executionIdConflict,
|
|
2074
|
+
executionNotFound,
|
|
2075
|
+
executionStatusAt,
|
|
2076
|
+
isExecutionClaimable,
|
|
2077
|
+
isLeaseCurrent,
|
|
2078
|
+
isLeaseLost,
|
|
2079
|
+
isStoreSelectorMatch,
|
|
2080
|
+
isTerminalExecutionStatus,
|
|
2081
|
+
matchEffectBegin,
|
|
2082
|
+
matchStartRequest,
|
|
2083
|
+
nextClaimAt,
|
|
2084
|
+
prepareEffectBegin,
|
|
2085
|
+
prepareEffectSettle,
|
|
2086
|
+
prepareEffectWatch,
|
|
2087
|
+
prepareExecutionAdmission,
|
|
2088
|
+
prepareExecutionStart,
|
|
2089
|
+
redeliveryDelay,
|
|
2090
|
+
registerErrorClass,
|
|
2091
|
+
releaseExecutionLease,
|
|
2092
|
+
renewExecutionLease,
|
|
2093
|
+
requestExecutionWake,
|
|
2094
|
+
settleEffect,
|
|
2095
|
+
storePathsIntersect,
|
|
2096
|
+
toEffectRecord,
|
|
2097
|
+
toExecutionRecord,
|
|
2098
|
+
trackStoreSelector,
|
|
2099
|
+
trackStoreUpdater,
|
|
2100
|
+
unacknowledgedDeliveryCountAt,
|
|
2101
|
+
unwrapTrackedValue,
|
|
2102
|
+
validateEffectRequest,
|
|
2103
|
+
validateStoreState,
|
|
2104
|
+
waitForExecutionOutcome
|
|
624
2105
|
};
|