@yieldstar/core 0.4.6 → 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 +26 -5
- package/dist/base/step.d.ts.map +1 -1
- package/dist/base/store.d.ts +431 -0
- package/dist/base/store.d.ts.map +1 -0
- package/dist/base/store.test.d.ts +2 -0
- package/dist/base/store.test.d.ts.map +1 -0
- package/dist/base/workflow.d.ts +17 -0
- package/dist/base/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +24 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1986 -111
- 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 -7
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/utils/map.d.ts +1 -1
- package/dist/utils/map.d.ts.map +1 -1
- package/package.json +13 -3
- 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 +37 -7
- package/src/base/store.test.ts +176 -0
- package/src/base/store.ts +1123 -0
- package/src/base/workflow.ts +17 -0
- package/src/index.ts +174 -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 +78 -34
- package/src/utils/map.test.ts +1 -1
- package/src/utils/map.ts +8 -2
- package/dist/base/map.d.ts +0 -21
- package/dist/base/map.d.ts.map +0 -1
- package/dist/base/map.test.d.ts +0 -2
- package/dist/base/map.test.d.ts.map +0 -1
- package/dist/base/schedule.d.ts +0 -71
- package/dist/base/schedule.d.ts.map +0 -1
- package/dist/base/types.d.ts +0 -24
- package/dist/base/types.d.ts.map +0 -1
- package/src/base/scheduler.ts +0 -8
- package/tsconfig.tsbuildinfo +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,1828 @@
|
|
|
1
|
-
//
|
|
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
|
+
}
|
|
1340
|
+
// src/base/store.ts
|
|
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
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
class StoreClient {
|
|
1357
|
+
store(definition, id) {
|
|
1358
|
+
return {
|
|
1359
|
+
get: () => this.getStore({
|
|
1360
|
+
definition,
|
|
1361
|
+
id
|
|
1362
|
+
}),
|
|
1363
|
+
update: (updater) => this.updateStore({
|
|
1364
|
+
definition,
|
|
1365
|
+
id,
|
|
1366
|
+
updater
|
|
1367
|
+
}),
|
|
1368
|
+
updateFrom: (snapshot, updater) => this.updateStoreFrom({
|
|
1369
|
+
definition,
|
|
1370
|
+
id,
|
|
1371
|
+
snapshot,
|
|
1372
|
+
updater
|
|
1373
|
+
}),
|
|
1374
|
+
deleteFrom: (snapshot) => this.deleteStoreFrom({
|
|
1375
|
+
definition,
|
|
1376
|
+
id,
|
|
1377
|
+
snapshot
|
|
1378
|
+
})
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
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
|
+
}
|
|
1390
|
+
async updateStore(params) {
|
|
1391
|
+
const applied = await this.getAppliedResult(params);
|
|
1392
|
+
if (applied) {
|
|
1393
|
+
return applied.result;
|
|
1394
|
+
}
|
|
1395
|
+
const stepKey = params.stepId?.stepKey;
|
|
1396
|
+
let snapshot = await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
1397
|
+
while (true) {
|
|
1398
|
+
const prepared = await prepareStoreUpdate(params.definition, snapshot, params.updater, stepKey);
|
|
1399
|
+
const result = {
|
|
1400
|
+
state: cloneStoreState(prepared.nextState),
|
|
1401
|
+
previousVersion: snapshot.version,
|
|
1402
|
+
version: snapshot.version + 1
|
|
1403
|
+
};
|
|
1404
|
+
const committed = await this.commitStoreMutation({
|
|
1405
|
+
definition: params.definition,
|
|
1406
|
+
id: params.id,
|
|
1407
|
+
expected: snapshot,
|
|
1408
|
+
nextState: prepared.nextState,
|
|
1409
|
+
changedPaths: prepared.changedPaths,
|
|
1410
|
+
stepId: params.stepId,
|
|
1411
|
+
result
|
|
1412
|
+
});
|
|
1413
|
+
if (committed.status === "committed")
|
|
1414
|
+
return result;
|
|
1415
|
+
if (committed.status === "already-applied") {
|
|
1416
|
+
return committed.result;
|
|
1417
|
+
}
|
|
1418
|
+
snapshot = committed.status === "conflict" ? existing(committed.snapshot) : await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async updateStoreFrom(params) {
|
|
1422
|
+
const applied = await this.getAppliedResult(params);
|
|
1423
|
+
if (applied) {
|
|
1424
|
+
return applied.result;
|
|
1425
|
+
}
|
|
1426
|
+
assertStoreSnapshotInstanceId(params.snapshot);
|
|
1427
|
+
const stepKey = params.stepId?.stepKey;
|
|
1428
|
+
const current = await this.getOrInsertStore(params.definition, params.id, stepKey);
|
|
1429
|
+
if (!sameStoreVersion(current, params.snapshot)) {
|
|
1430
|
+
return storeUpdateConflict(params.snapshot, current);
|
|
1431
|
+
}
|
|
1432
|
+
const prepared = await prepareStoreUpdate(params.definition, params.snapshot, params.updater, stepKey);
|
|
1433
|
+
const result = {
|
|
1434
|
+
updated: true,
|
|
1435
|
+
state: cloneStoreState(prepared.nextState),
|
|
1436
|
+
previousVersion: params.snapshot.version,
|
|
1437
|
+
version: params.snapshot.version + 1
|
|
1438
|
+
};
|
|
1439
|
+
const committed = await this.commitStoreMutation({
|
|
1440
|
+
definition: params.definition,
|
|
1441
|
+
id: params.id,
|
|
1442
|
+
expected: existing(params.snapshot),
|
|
1443
|
+
nextState: prepared.nextState,
|
|
1444
|
+
changedPaths: prepared.changedPaths,
|
|
1445
|
+
stepId: params.stepId,
|
|
1446
|
+
result
|
|
1447
|
+
});
|
|
1448
|
+
if (committed.status === "committed")
|
|
1449
|
+
return result;
|
|
1450
|
+
if (committed.status === "already-applied") {
|
|
1451
|
+
return committed.result;
|
|
1452
|
+
}
|
|
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
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
async takeFromStore(params) {
|
|
1488
|
+
const applied = await this.getAppliedResult(params);
|
|
1489
|
+
if (applied)
|
|
1490
|
+
return applied.result;
|
|
1491
|
+
const stepKey = params.stepId?.stepKey;
|
|
1492
|
+
let snapshot = await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
1493
|
+
while (true) {
|
|
1494
|
+
const previousState = cloneStoreState(snapshot.state);
|
|
1495
|
+
const draft = cloneStoreState(previousState);
|
|
1496
|
+
const tracked = trackStoreUpdater(draft);
|
|
1497
|
+
const { result: selectedResult, readPaths } = trackStoreSelector(tracked.draft, params.selector);
|
|
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
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
matched: false,
|
|
1512
|
+
instanceId: snapshot.instanceId,
|
|
1513
|
+
version: snapshot.version,
|
|
1514
|
+
readPaths
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
const selected = unwrapTrackedValue(selectedResult);
|
|
1518
|
+
assertSynchronousClaim(params.claim(tracked.draft, selected));
|
|
1519
|
+
const prepared = await finalizeStoreMutation({
|
|
1520
|
+
definition: params.definition,
|
|
1521
|
+
previousState,
|
|
1522
|
+
draft,
|
|
1523
|
+
tracked,
|
|
1524
|
+
stepKey
|
|
1525
|
+
});
|
|
1526
|
+
assertDurableStoreValue(selected, stepKey ?? "", `Store take selection for "${params.definition.name}"`);
|
|
1527
|
+
const selectedSnapshot = cloneStoreState(selected);
|
|
1528
|
+
const result = {
|
|
1529
|
+
matched: true,
|
|
1530
|
+
selected: selectedSnapshot,
|
|
1531
|
+
instanceId: snapshot.instanceId,
|
|
1532
|
+
version: snapshot.version + 1
|
|
1533
|
+
};
|
|
1534
|
+
const committed = await this.commitStoreMutation({
|
|
1535
|
+
definition: params.definition,
|
|
1536
|
+
id: params.id,
|
|
1537
|
+
expected: snapshot,
|
|
1538
|
+
nextState: prepared.nextState,
|
|
1539
|
+
changedPaths: prepared.changedPaths,
|
|
1540
|
+
stepId: params.stepId,
|
|
1541
|
+
result
|
|
1542
|
+
});
|
|
1543
|
+
if (committed.status === "committed")
|
|
1544
|
+
return result;
|
|
1545
|
+
if (committed.status === "already-applied") {
|
|
1546
|
+
return committed.result;
|
|
1547
|
+
}
|
|
1548
|
+
snapshot = committed.status === "conflict" ? existing(committed.snapshot) : await this.readOrInitialize(params.definition, params.id, stepKey);
|
|
1549
|
+
}
|
|
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
|
+
}
|
|
1573
|
+
getAppliedResult(params) {
|
|
1574
|
+
return params.stepId ? this.getAppliedStoreStep({
|
|
1575
|
+
definition: params.definition,
|
|
1576
|
+
id: params.id,
|
|
1577
|
+
stepId: params.stepId
|
|
1578
|
+
}) : Promise.resolve(undefined);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
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) {
|
|
1597
|
+
const previousState = cloneStoreState(snapshot.state);
|
|
1598
|
+
const draft = cloneStoreState(previousState);
|
|
1599
|
+
const tracked = trackStoreUpdater(draft);
|
|
1600
|
+
const updated = updater(tracked.draft);
|
|
1601
|
+
assertSynchronousUpdater(updated);
|
|
1602
|
+
const returned = updated === undefined ? undefined : unwrapTrackedValue(updated);
|
|
1603
|
+
return finalizeStoreMutation({
|
|
1604
|
+
definition,
|
|
1605
|
+
previousState,
|
|
1606
|
+
draft,
|
|
1607
|
+
tracked,
|
|
1608
|
+
replacement: returned,
|
|
1609
|
+
stepKey
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
async function finalizeStoreMutation(params) {
|
|
1613
|
+
const isReplacement = params.replacement !== undefined && params.replacement !== params.draft;
|
|
1614
|
+
const nextState = await validateStoreState(params.definition, isReplacement ? params.replacement : params.draft, params.stepKey);
|
|
1615
|
+
const changedPaths = !isReplacement && nextState === params.draft ? params.tracked.writePaths() : diffStorePaths(params.previousState, nextState);
|
|
1616
|
+
return { nextState, changedPaths };
|
|
1617
|
+
}
|
|
1618
|
+
function sameStoreVersion(left, right) {
|
|
1619
|
+
return left.instanceId === right.instanceId && left.version === right.version;
|
|
1620
|
+
}
|
|
1621
|
+
function assertStoreSnapshotInstanceId(snapshot) {
|
|
1622
|
+
if (!snapshot.instanceId) {
|
|
1623
|
+
throw new Error("Store snapshot is missing instanceId; read a fresh snapshot");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function storeUpdateConflict(expected, actual) {
|
|
1627
|
+
return {
|
|
1628
|
+
updated: false,
|
|
1629
|
+
expectedInstanceId: expected.instanceId,
|
|
1630
|
+
actualInstanceId: actual.instanceId,
|
|
1631
|
+
expectedVersion: expected.version,
|
|
1632
|
+
actualVersion: actual.version
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function isStoreSelectorMatch(result) {
|
|
1636
|
+
return result !== undefined && result !== null && result !== false;
|
|
1637
|
+
}
|
|
1638
|
+
function assertSynchronousClaim(claimResult) {
|
|
1639
|
+
if (claimResult && typeof claimResult.then === "function") {
|
|
1640
|
+
throw new Error("Store take claims must be synchronous");
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function assertSynchronousUpdater(updaterResult) {
|
|
1644
|
+
if (updaterResult && typeof updaterResult.then === "function") {
|
|
1645
|
+
throw new Error("Store updaters must be synchronous");
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
async function validateStoreState(definition, state, stepKey = "") {
|
|
1649
|
+
const result = await definition.schema["~standard"].validate(state);
|
|
1650
|
+
if ("issues" in result && result.issues) {
|
|
1651
|
+
const messages = result.issues.map((issue) => issue.message).join("; ");
|
|
1652
|
+
throw new StoreValidationError(`Invalid store state for "${definition.name}": ${messages}`);
|
|
1653
|
+
}
|
|
1654
|
+
assertDurableStoreValue(result.value, stepKey, `Store state for "${definition.name}"`);
|
|
1655
|
+
return result.value;
|
|
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
|
+
}
|
|
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
|
+
}
|
|
1684
|
+
if (typeof structuredClone === "function") {
|
|
1685
|
+
try {
|
|
1686
|
+
return structuredClone(state);
|
|
1687
|
+
} catch {}
|
|
1688
|
+
}
|
|
1689
|
+
return JSON.parse(JSON.stringify(state));
|
|
1690
|
+
}
|
|
1691
|
+
function diffStorePaths(previous, next) {
|
|
1692
|
+
const paths = [];
|
|
1693
|
+
function visit(left, right, path) {
|
|
1694
|
+
if (Object.is(left, right))
|
|
1695
|
+
return;
|
|
1696
|
+
if (!isDiffableObject(left) || !isDiffableObject(right)) {
|
|
1697
|
+
paths.push(path);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
1701
|
+
for (const key of keys) {
|
|
1702
|
+
visit(left[key], right[key], [...path, arrayKeyToPathSegment(key)]);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
visit(previous, next, []);
|
|
1706
|
+
return collapseStorePaths(paths);
|
|
1707
|
+
}
|
|
1708
|
+
var TRACKING_TARGET = Symbol("yieldstar.trackingTarget");
|
|
1709
|
+
function unwrapTrackedValue(value) {
|
|
1710
|
+
if (isDiffableObject(value)) {
|
|
1711
|
+
const target = value[TRACKING_TARGET];
|
|
1712
|
+
if (target !== undefined)
|
|
1713
|
+
return target;
|
|
1714
|
+
}
|
|
1715
|
+
return value;
|
|
1716
|
+
}
|
|
1717
|
+
function trackStoreSelector(state, selector) {
|
|
1718
|
+
const paths = [];
|
|
1719
|
+
const proxies = new WeakMap;
|
|
1720
|
+
function track(value, path) {
|
|
1721
|
+
if (!isDiffableObject(value))
|
|
1722
|
+
return value;
|
|
1723
|
+
const cached = proxies.get(value);
|
|
1724
|
+
if (cached)
|
|
1725
|
+
return cached;
|
|
1726
|
+
const proxy = new Proxy(value, {
|
|
1727
|
+
get(target, property, receiver) {
|
|
1728
|
+
if (typeof property === "symbol") {
|
|
1729
|
+
if (property === TRACKING_TARGET)
|
|
1730
|
+
return target;
|
|
1731
|
+
return Reflect.get(target, property, receiver);
|
|
1732
|
+
}
|
|
1733
|
+
const nextPath = [...path, arrayKeyToPathSegment(property)];
|
|
1734
|
+
if (nextPath.length > 0) {
|
|
1735
|
+
paths.push(nextPath);
|
|
1736
|
+
}
|
|
1737
|
+
const child = Reflect.get(target, property, receiver);
|
|
1738
|
+
return track(child, nextPath);
|
|
1739
|
+
},
|
|
1740
|
+
set() {
|
|
1741
|
+
throw new Error("Store selectors must not mutate state");
|
|
1742
|
+
},
|
|
1743
|
+
deleteProperty() {
|
|
1744
|
+
throw new Error("Store selectors must not mutate state");
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
proxies.set(value, proxy);
|
|
1748
|
+
return proxy;
|
|
1749
|
+
}
|
|
1750
|
+
const result = selector(track(state, []));
|
|
1751
|
+
return {
|
|
1752
|
+
result,
|
|
1753
|
+
readPaths: collapseStorePaths(paths)
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function trackStoreUpdater(target) {
|
|
1757
|
+
const paths = [];
|
|
1758
|
+
const proxies = new WeakMap;
|
|
1759
|
+
function record(path, property) {
|
|
1760
|
+
if (typeof property === "symbol") {
|
|
1761
|
+
paths.push(path);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
paths.push([...path, arrayKeyToPathSegment(String(property))]);
|
|
1765
|
+
}
|
|
1766
|
+
function track(value, path) {
|
|
1767
|
+
if (!isDiffableObject(value))
|
|
1768
|
+
return value;
|
|
1769
|
+
const cached = proxies.get(value);
|
|
1770
|
+
if (cached)
|
|
1771
|
+
return cached;
|
|
1772
|
+
const proxy = new Proxy(value, {
|
|
1773
|
+
get(target, property, receiver) {
|
|
1774
|
+
if (typeof property === "symbol") {
|
|
1775
|
+
if (property === TRACKING_TARGET)
|
|
1776
|
+
return target;
|
|
1777
|
+
return Reflect.get(target, property, receiver);
|
|
1778
|
+
}
|
|
1779
|
+
const child = Reflect.get(target, property);
|
|
1780
|
+
return track(child, [...path, arrayKeyToPathSegment(property)]);
|
|
1781
|
+
},
|
|
1782
|
+
set(target, property, value) {
|
|
1783
|
+
record(path, property);
|
|
1784
|
+
return Reflect.set(target, property, unwrapTrackedValue(value));
|
|
1785
|
+
},
|
|
1786
|
+
deleteProperty(target, property) {
|
|
1787
|
+
record(path, property);
|
|
1788
|
+
return Reflect.deleteProperty(target, property);
|
|
1789
|
+
},
|
|
1790
|
+
defineProperty(target, property, descriptor) {
|
|
1791
|
+
record(path, property);
|
|
1792
|
+
if ("value" in descriptor) {
|
|
1793
|
+
descriptor = { ...descriptor, value: unwrapTrackedValue(descriptor.value) };
|
|
1794
|
+
}
|
|
1795
|
+
return Reflect.defineProperty(target, property, descriptor);
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
proxies.set(value, proxy);
|
|
1799
|
+
return proxy;
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
draft: track(target, []),
|
|
1803
|
+
writePaths: () => collapseStorePaths(paths)
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
function storePathsIntersect(readPaths, writePaths) {
|
|
1807
|
+
return readPaths.some((readPath) => writePaths.some((writePath) => isPathPrefix(readPath, writePath) || isPathPrefix(writePath, readPath)));
|
|
1808
|
+
}
|
|
1809
|
+
function collapseStorePaths(paths) {
|
|
1810
|
+
const unique = new Map(paths.map((path) => [path.join("\x00"), path]));
|
|
1811
|
+
return [...unique.values()];
|
|
1812
|
+
}
|
|
1813
|
+
function isPathPrefix(prefix, path) {
|
|
1814
|
+
if (prefix.length > path.length)
|
|
1815
|
+
return false;
|
|
1816
|
+
return prefix.every((segment, index) => segment === path[index]);
|
|
1817
|
+
}
|
|
1818
|
+
function isDiffableObject(value) {
|
|
1819
|
+
return typeof value === "object" && value !== null;
|
|
1820
|
+
}
|
|
1821
|
+
function arrayKeyToPathSegment(key) {
|
|
1822
|
+
if (/^(0|[1-9]\d*)$/.test(key))
|
|
1823
|
+
return Number(key);
|
|
1824
|
+
return key;
|
|
1825
|
+
}
|
|
2
1826
|
// src/base/step.ts
|
|
3
1827
|
class StepResponse {
|
|
4
1828
|
}
|
|
@@ -27,9 +1851,12 @@ class StepResult extends StepResponse {
|
|
|
27
1851
|
static type = "step-result";
|
|
28
1852
|
type = "step-result";
|
|
29
1853
|
result;
|
|
30
|
-
|
|
1854
|
+
schema;
|
|
1855
|
+
constructor(result, schema) {
|
|
31
1856
|
super();
|
|
32
1857
|
this.result = result;
|
|
1858
|
+
if (schema !== undefined)
|
|
1859
|
+
this.schema = schema;
|
|
33
1860
|
}
|
|
34
1861
|
}
|
|
35
1862
|
|
|
@@ -46,10 +1873,28 @@ class WorkflowResult extends StepResponse {
|
|
|
46
1873
|
class WorkflowDelay extends StepResponse {
|
|
47
1874
|
static type = "workflow-delay";
|
|
48
1875
|
type = "workflow-delay";
|
|
49
|
-
|
|
50
|
-
constructor(
|
|
1876
|
+
wakeAt;
|
|
1877
|
+
constructor(wakeAt) {
|
|
1878
|
+
super();
|
|
1879
|
+
this.wakeAt = wakeAt;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
class StepStoreWait extends StepResponse {
|
|
1884
|
+
static type = "store-wait";
|
|
1885
|
+
type = "store-wait";
|
|
1886
|
+
constructor() {
|
|
1887
|
+
super();
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
class StepEffectWait extends StepResponse {
|
|
1892
|
+
static type = "effect-wait";
|
|
1893
|
+
type = "effect-wait";
|
|
1894
|
+
wakeAt;
|
|
1895
|
+
constructor(wakeAt) {
|
|
51
1896
|
super();
|
|
52
|
-
this.
|
|
1897
|
+
this.wakeAt = wakeAt;
|
|
53
1898
|
}
|
|
54
1899
|
}
|
|
55
1900
|
|
|
@@ -80,49 +1925,66 @@ class StepError extends StepResponse {
|
|
|
80
1925
|
class StepDelay extends StepResponse {
|
|
81
1926
|
static type = "step-delay";
|
|
82
1927
|
type = "step-delay";
|
|
83
|
-
|
|
84
|
-
constructor(
|
|
1928
|
+
wakeAt;
|
|
1929
|
+
constructor(wakeAt) {
|
|
85
1930
|
super();
|
|
86
|
-
this.
|
|
1931
|
+
this.wakeAt = wakeAt;
|
|
87
1932
|
}
|
|
88
1933
|
}
|
|
89
1934
|
// src/lib/workflow-runner.ts
|
|
90
1935
|
class WorkflowRunner {
|
|
91
|
-
|
|
92
|
-
schedulerClient;
|
|
1936
|
+
runtime;
|
|
93
1937
|
router;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
this.router =
|
|
1938
|
+
logger;
|
|
1939
|
+
constructor(runtime, options) {
|
|
1940
|
+
this.runtime = runtime;
|
|
1941
|
+
this.router = options.router;
|
|
1942
|
+
this.logger = options.logger;
|
|
98
1943
|
}
|
|
99
|
-
run = async (event,
|
|
1944
|
+
run = async (event, options) => {
|
|
1945
|
+
const { lease, signal, logger = this.logger } = options;
|
|
100
1946
|
const workflow = this.router[event.workflowId];
|
|
101
1947
|
if (!workflow) {
|
|
102
|
-
|
|
1948
|
+
return {
|
|
1949
|
+
status: "failed",
|
|
1950
|
+
error: new Error(`No workflow registered for "${event.workflowId}"`)
|
|
1951
|
+
};
|
|
103
1952
|
}
|
|
1953
|
+
let response;
|
|
104
1954
|
try {
|
|
105
|
-
|
|
1955
|
+
response = await this.runWorkflows({
|
|
106
1956
|
workflow,
|
|
107
1957
|
logger,
|
|
108
|
-
event
|
|
1958
|
+
event,
|
|
1959
|
+
lease,
|
|
1960
|
+
signal
|
|
109
1961
|
});
|
|
110
|
-
switch (response.type) {
|
|
111
|
-
case "workflow-result":
|
|
112
|
-
return response;
|
|
113
|
-
case "workflow-delay":
|
|
114
|
-
this.schedulerClient.requestWakeUp(event, response.resumeIn);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
1962
|
} catch (err) {
|
|
118
|
-
|
|
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" };
|
|
119
1975
|
}
|
|
120
1976
|
};
|
|
121
1977
|
async runWorkflows(params) {
|
|
122
|
-
const { workflow, event, logger } = params;
|
|
1978
|
+
const { workflow, event, lease, signal, logger } = params;
|
|
123
1979
|
const workflowIterator = workflow({
|
|
124
1980
|
event,
|
|
125
|
-
|
|
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,
|
|
126
1988
|
logger
|
|
127
1989
|
});
|
|
128
1990
|
const iteratorResult = await workflowIterator.next();
|
|
@@ -131,94 +1993,107 @@ class WorkflowRunner {
|
|
|
131
1993
|
return stageResponse;
|
|
132
1994
|
}
|
|
133
1995
|
if (stageResponse instanceof StepDelay) {
|
|
134
|
-
return
|
|
1996
|
+
return stageResponse;
|
|
1997
|
+
}
|
|
1998
|
+
if (stageResponse instanceof StepEffectWait || stageResponse instanceof StepStoreWait) {
|
|
1999
|
+
return stageResponse;
|
|
135
2000
|
}
|
|
136
2001
|
throw new Error("Workflow runner critical error. This is likely because your worklow and worker are referencing different versions of @yieldstar/core.");
|
|
137
2002
|
}
|
|
138
2003
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const err = new Error(error.message);
|
|
142
|
-
Error.captureStackTrace(err, fn);
|
|
143
|
-
err.stack = error.stack;
|
|
144
|
-
throw err;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// src/utils/map.ts
|
|
148
|
-
class ReadOnlyMap {
|
|
149
|
-
sourceMap;
|
|
150
|
-
constructor(sourceMap) {
|
|
151
|
-
this.sourceMap = sourceMap;
|
|
152
|
-
}
|
|
153
|
-
get(key) {
|
|
154
|
-
return this.sourceMap.get(key);
|
|
155
|
-
}
|
|
156
|
-
has(key) {
|
|
157
|
-
return this.sourceMap.has(key);
|
|
158
|
-
}
|
|
159
|
-
get size() {
|
|
160
|
-
return this.sourceMap.size;
|
|
161
|
-
}
|
|
162
|
-
entries() {
|
|
163
|
-
return this.sourceMap.entries();
|
|
164
|
-
}
|
|
165
|
-
keys() {
|
|
166
|
-
return this.sourceMap.keys();
|
|
167
|
-
}
|
|
168
|
-
values() {
|
|
169
|
-
return this.sourceMap.values();
|
|
170
|
-
}
|
|
171
|
-
forEach(callbackfn, thisArg) {
|
|
172
|
-
return this.sourceMap.forEach(callbackfn, thisArg);
|
|
173
|
-
}
|
|
174
|
-
[Symbol.iterator]() {
|
|
175
|
-
return this.sourceMap[Symbol.iterator]();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
class FreezableMap extends Map {
|
|
180
|
-
frozen = false;
|
|
181
|
-
freeze() {
|
|
182
|
-
this.frozen = true;
|
|
183
|
-
}
|
|
184
|
-
isFrozen() {
|
|
185
|
-
return this.frozen;
|
|
186
|
-
}
|
|
187
|
-
set(key, value) {
|
|
188
|
-
if (this.frozen) {
|
|
189
|
-
const error = new Error("Cannot call set on a frozen map");
|
|
190
|
-
throw errorWithOriginalStack(error, this.set);
|
|
191
|
-
}
|
|
192
|
-
return super.set(key, value);
|
|
193
|
-
}
|
|
194
|
-
delete(key) {
|
|
195
|
-
if (this.frozen) {
|
|
196
|
-
const error = new Error("Cannot call delete on a frozen map");
|
|
197
|
-
throw errorWithOriginalStack(error, this.delete);
|
|
198
|
-
}
|
|
199
|
-
return super.delete(key);
|
|
200
|
-
}
|
|
201
|
-
clear() {
|
|
202
|
-
if (this.frozen) {
|
|
203
|
-
const error = new Error("Cannot call clear on a frozen map");
|
|
204
|
-
throw errorWithOriginalStack(error, this.clear);
|
|
205
|
-
}
|
|
206
|
-
return super.clear();
|
|
207
|
-
}
|
|
2004
|
+
function toError(value) {
|
|
2005
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
208
2006
|
}
|
|
209
2007
|
export {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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,
|
|
2018
|
+
FreezableMap,
|
|
2019
|
+
InfrastructureError,
|
|
2020
|
+
LeaseLostError,
|
|
2021
|
+
REDELIVERY_DELAY_BASE,
|
|
2022
|
+
REDELIVERY_DELAY_MAX,
|
|
222
2023
|
ReadOnlyMap,
|
|
223
|
-
|
|
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
|
|
224
2099
|
};
|