@ai-digital-factory/state-flow-runtime 0.0.0-stage → 0.1.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/LICENSE +18 -0
- package/README.md +146 -2
- package/dist/index.d.ts +739 -0
- package/dist/index.js +2877 -0
- package/package.json +49 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2877 @@
|
|
|
1
|
+
// src/durable/definition.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
// src/durable/errors.ts
|
|
6
|
+
var DurableRuntimeError = class extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var DurableOwnershipError = class extends DurableRuntimeError {
|
|
13
|
+
};
|
|
14
|
+
var DurableRuntimeClosedError = class extends DurableRuntimeError {
|
|
15
|
+
};
|
|
16
|
+
var DurableShutdownTimeoutError = class extends DurableRuntimeError {
|
|
17
|
+
};
|
|
18
|
+
var DurableDefinitionError = class extends DurableRuntimeError {
|
|
19
|
+
problems;
|
|
20
|
+
constructor(chartName, problems) {
|
|
21
|
+
super(
|
|
22
|
+
`Durable flow "${chartName}" is invalid:
|
|
23
|
+
- ${problems.join("\n- ")}`
|
|
24
|
+
);
|
|
25
|
+
this.problems = problems;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var DurableInputError = class extends DurableRuntimeError {
|
|
29
|
+
};
|
|
30
|
+
var DurableSchemaError = class extends DurableRuntimeError {
|
|
31
|
+
};
|
|
32
|
+
var DurableDispatchError = class extends DurableRuntimeError {
|
|
33
|
+
constructor(outcome, instanceId2, context) {
|
|
34
|
+
super(`Dispatch for instance ${instanceId2}: ${outcome}`);
|
|
35
|
+
this.outcome = outcome;
|
|
36
|
+
this.instanceId = instanceId2;
|
|
37
|
+
this.context = context;
|
|
38
|
+
}
|
|
39
|
+
outcome;
|
|
40
|
+
instanceId;
|
|
41
|
+
context;
|
|
42
|
+
phase = "dispatch";
|
|
43
|
+
};
|
|
44
|
+
var DurableOperationError = class extends DurableRuntimeError {
|
|
45
|
+
constructor(phase, cause) {
|
|
46
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
47
|
+
this.phase = phase;
|
|
48
|
+
}
|
|
49
|
+
phase;
|
|
50
|
+
outcome = "error";
|
|
51
|
+
};
|
|
52
|
+
var DurableDefinitionMismatchError = class extends DurableRuntimeError {
|
|
53
|
+
constructor(context) {
|
|
54
|
+
super(context.message);
|
|
55
|
+
this.context = context;
|
|
56
|
+
}
|
|
57
|
+
context;
|
|
58
|
+
phase = "ensure-instance";
|
|
59
|
+
outcome = "definition-mismatch";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/durable/json.ts
|
|
63
|
+
function isPlainObject(value) {
|
|
64
|
+
const prototype = Object.getPrototypeOf(value);
|
|
65
|
+
return prototype === Object.prototype || prototype === null;
|
|
66
|
+
}
|
|
67
|
+
function findNonJsonValue(value, path = "$") {
|
|
68
|
+
return findNonJson(value, path, /* @__PURE__ */ new Set());
|
|
69
|
+
}
|
|
70
|
+
function findNonJson(value, path, ancestors) {
|
|
71
|
+
if (value === null) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
switch (typeof value) {
|
|
75
|
+
case "string":
|
|
76
|
+
case "boolean":
|
|
77
|
+
return null;
|
|
78
|
+
case "number":
|
|
79
|
+
return Number.isFinite(value) ? null : `${path} is not a finite number`;
|
|
80
|
+
case "object": {
|
|
81
|
+
if (ancestors.has(value)) {
|
|
82
|
+
return `${path} is a circular reference`;
|
|
83
|
+
}
|
|
84
|
+
ancestors.add(value);
|
|
85
|
+
try {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
88
|
+
if (!(index in value)) {
|
|
89
|
+
return `${path}[${index}] is an empty array slot`;
|
|
90
|
+
}
|
|
91
|
+
const found = findNonJson(
|
|
92
|
+
value[index],
|
|
93
|
+
`${path}[${index}]`,
|
|
94
|
+
ancestors
|
|
95
|
+
);
|
|
96
|
+
if (found) {
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (!isPlainObject(value)) {
|
|
103
|
+
return `${path} is not a plain object`;
|
|
104
|
+
}
|
|
105
|
+
for (const key of Object.keys(value)) {
|
|
106
|
+
const entry = value[key];
|
|
107
|
+
if (entry === void 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const found = findNonJson(
|
|
111
|
+
entry,
|
|
112
|
+
`${path}[${JSON.stringify(key)}]`,
|
|
113
|
+
ancestors
|
|
114
|
+
);
|
|
115
|
+
if (found) {
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
} finally {
|
|
121
|
+
ancestors.delete(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
default:
|
|
125
|
+
return `${path} is ${typeof value}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function canonicalJson(value) {
|
|
129
|
+
return writeCanonical(value, /* @__PURE__ */ new Set());
|
|
130
|
+
}
|
|
131
|
+
function writeCanonical(value, ancestors) {
|
|
132
|
+
if (value === null || typeof value !== "object") {
|
|
133
|
+
const text = JSON.stringify(value);
|
|
134
|
+
if (text === void 0) {
|
|
135
|
+
throw new TypeError(`Cannot serialize ${typeof value} as JSON`);
|
|
136
|
+
}
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
if (ancestors.has(value)) {
|
|
140
|
+
throw new TypeError("Cannot serialize a circular reference as JSON");
|
|
141
|
+
}
|
|
142
|
+
ancestors.add(value);
|
|
143
|
+
try {
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
return `[${value.map((entry) => writeCanonical(entry ?? null, ancestors)).join(",")}]`;
|
|
146
|
+
}
|
|
147
|
+
const parts = [];
|
|
148
|
+
for (const key of Object.keys(value).sort()) {
|
|
149
|
+
const entry = value[key];
|
|
150
|
+
if (entry !== void 0) {
|
|
151
|
+
parts.push(
|
|
152
|
+
`${JSON.stringify(key)}:${writeCanonical(entry, ancestors)}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return `{${parts.join(",")}}`;
|
|
157
|
+
} finally {
|
|
158
|
+
ancestors.delete(value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseJson(text) {
|
|
162
|
+
return text === null ? null : JSON.parse(text);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/durable/definition.ts
|
|
166
|
+
var STRATEGIES = /* @__PURE__ */ new Set([
|
|
167
|
+
"safe-replay",
|
|
168
|
+
"reconcile",
|
|
169
|
+
"manual"
|
|
170
|
+
]);
|
|
171
|
+
var SLOT_EXPORTS = {
|
|
172
|
+
state: { onEnter: "on-enter", onExit: "on-exit" },
|
|
173
|
+
transition: { guard: "guard", onTransition: "on-transition" },
|
|
174
|
+
creation: { resolveStartState: "resolve-start-state" }
|
|
175
|
+
};
|
|
176
|
+
function isRecord(value) {
|
|
177
|
+
return typeof value === "object" && value !== null;
|
|
178
|
+
}
|
|
179
|
+
function readCallback(label, kind, value, problems) {
|
|
180
|
+
if (typeof value === "function") {
|
|
181
|
+
problems.push(
|
|
182
|
+
`${label} is a plain function; durable callbacks must declare { run, recovery }`
|
|
183
|
+
);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (!isRecord(value) || typeof value.run !== "function") {
|
|
187
|
+
problems.push(`${label} must be an object with a run function`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const recovery = value.recovery;
|
|
191
|
+
if (!isRecord(recovery) || typeof recovery.strategy !== "string") {
|
|
192
|
+
problems.push(`${label} must declare recovery.strategy`);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (!STRATEGIES.has(recovery.strategy)) {
|
|
196
|
+
problems.push(
|
|
197
|
+
`${label} has unknown recovery strategy ${JSON.stringify(recovery.strategy)}`
|
|
198
|
+
);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const strategy = recovery.strategy;
|
|
202
|
+
const hasReconcile = recovery.reconcile !== void 0;
|
|
203
|
+
if (strategy === "reconcile" && typeof recovery.reconcile !== "function") {
|
|
204
|
+
problems.push(
|
|
205
|
+
`${label} uses the reconcile strategy without a reconcile function`
|
|
206
|
+
);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
if (strategy !== "reconcile" && hasReconcile) {
|
|
210
|
+
problems.push(
|
|
211
|
+
`${label} declares a reconcile function with the ${strategy} strategy`
|
|
212
|
+
);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
kind,
|
|
217
|
+
strategy,
|
|
218
|
+
run: value.run,
|
|
219
|
+
reconcile: strategy === "reconcile" ? recovery.reconcile : null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function readSlot(slotKind, label, value, problems) {
|
|
223
|
+
if (value === null || value === void 0) {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
if (!isRecord(value)) {
|
|
227
|
+
problems.push(`${label} must be a handler module object`);
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
const exportsForKind = SLOT_EXPORTS[slotKind];
|
|
231
|
+
const result = {};
|
|
232
|
+
for (const key of Object.keys(value)) {
|
|
233
|
+
if (slotKind === "transition" && key === "payloadSchema") continue;
|
|
234
|
+
const callbackKind = exportsForKind[key];
|
|
235
|
+
if (!callbackKind) {
|
|
236
|
+
problems.push(`${label} exports unexpected member "${key}"`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const callback = readCallback(
|
|
240
|
+
`${label}.${key}`,
|
|
241
|
+
callbackKind,
|
|
242
|
+
value[key],
|
|
243
|
+
problems
|
|
244
|
+
);
|
|
245
|
+
if (callback) {
|
|
246
|
+
result[key] = callback;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
function loadFlowDefinition(module) {
|
|
252
|
+
const problems = [];
|
|
253
|
+
if (module.execution !== "durable") {
|
|
254
|
+
problems.push(
|
|
255
|
+
"compiled module is not durable; rebuild its handlers package with the current State Flow compiler"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const statesById = /* @__PURE__ */ new Map();
|
|
259
|
+
const statesByName = /* @__PURE__ */ new Map();
|
|
260
|
+
for (const state of module.states) {
|
|
261
|
+
const slot = readSlot(
|
|
262
|
+
"state",
|
|
263
|
+
`state "${state.name}"`,
|
|
264
|
+
module.hooks.states[state.id],
|
|
265
|
+
problems
|
|
266
|
+
);
|
|
267
|
+
const definition = {
|
|
268
|
+
id: state.id,
|
|
269
|
+
name: state.name,
|
|
270
|
+
onEnter: slot.onEnter ?? null,
|
|
271
|
+
onExit: slot.onExit ?? null
|
|
272
|
+
};
|
|
273
|
+
if (statesByName.has(state.name)) {
|
|
274
|
+
problems.push(`state name "${state.name}" is declared more than once`);
|
|
275
|
+
}
|
|
276
|
+
statesById.set(state.id, definition);
|
|
277
|
+
statesByName.set(state.name, definition);
|
|
278
|
+
}
|
|
279
|
+
const transitionsByName = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const transition of module.transitions) {
|
|
281
|
+
const from = statesById.get(transition.fromId);
|
|
282
|
+
const to = statesById.get(transition.toId);
|
|
283
|
+
if (!from || !to) {
|
|
284
|
+
problems.push(
|
|
285
|
+
`transition "${transition.name}" references an unknown state`
|
|
286
|
+
);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (transitionsByName.has(transition.name)) {
|
|
290
|
+
problems.push(
|
|
291
|
+
`transition name "${transition.name}" is declared more than once`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const slot = readSlot(
|
|
295
|
+
"transition",
|
|
296
|
+
`transition "${transition.name}"`,
|
|
297
|
+
module.hooks.transitions[transition.name],
|
|
298
|
+
problems
|
|
299
|
+
);
|
|
300
|
+
const payloadSchema = module.payloadSchemas?.[transition.name];
|
|
301
|
+
if (!(payloadSchema instanceof z.ZodType)) {
|
|
302
|
+
problems.push(
|
|
303
|
+
`transition "${transition.name}" must declare a Zod payloadSchema`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
transitionsByName.set(transition.name, {
|
|
307
|
+
name: transition.name,
|
|
308
|
+
payloadSchema: payloadSchema instanceof z.ZodType ? payloadSchema : z.never(),
|
|
309
|
+
from,
|
|
310
|
+
to,
|
|
311
|
+
guard: slot.guard ?? null,
|
|
312
|
+
onTransition: slot.onTransition ?? null
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const creation = readSlot(
|
|
316
|
+
"creation",
|
|
317
|
+
"creation handler",
|
|
318
|
+
module.hooks.creation,
|
|
319
|
+
problems
|
|
320
|
+
);
|
|
321
|
+
const initialState = statesById.get(module.initialStateId);
|
|
322
|
+
if (!initialState) {
|
|
323
|
+
problems.push(`initial state id "${module.initialStateId}" is unknown`);
|
|
324
|
+
}
|
|
325
|
+
if (problems.length > 0 || !initialState) {
|
|
326
|
+
throw new DurableDefinitionError(module.chartName, problems);
|
|
327
|
+
}
|
|
328
|
+
const strategyOf = (callback) => callback === null ? null : callback.strategy;
|
|
329
|
+
const identitySource = {
|
|
330
|
+
chartName: module.chartName,
|
|
331
|
+
initialStateId: module.initialStateId,
|
|
332
|
+
states: [...statesById.values()].map((state) => ({
|
|
333
|
+
id: state.id,
|
|
334
|
+
name: state.name,
|
|
335
|
+
onEnter: strategyOf(state.onEnter),
|
|
336
|
+
onExit: strategyOf(state.onExit)
|
|
337
|
+
})),
|
|
338
|
+
transitions: [...transitionsByName.values()].map((transition) => ({
|
|
339
|
+
name: transition.name,
|
|
340
|
+
from: transition.from.id,
|
|
341
|
+
to: transition.to.id,
|
|
342
|
+
guard: strategyOf(transition.guard),
|
|
343
|
+
onTransition: strategyOf(transition.onTransition)
|
|
344
|
+
})),
|
|
345
|
+
resolveStartState: strategyOf(creation.resolveStartState ?? null)
|
|
346
|
+
};
|
|
347
|
+
const definitionId = `def_${createHash("sha256").update(canonicalJson(identitySource)).digest("hex").slice(0, 32)}`;
|
|
348
|
+
return {
|
|
349
|
+
chartName: module.chartName,
|
|
350
|
+
definitionId,
|
|
351
|
+
initialState,
|
|
352
|
+
statesByName,
|
|
353
|
+
transitionsByName,
|
|
354
|
+
resolveStartState: creation.resolveStartState ?? null,
|
|
355
|
+
handlerRevision: typeof module.handlerRevision === "string" ? module.handlerRevision : null
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/durable/engine.ts
|
|
360
|
+
import { z as z2 } from "zod";
|
|
361
|
+
|
|
362
|
+
// src/durable/ids.ts
|
|
363
|
+
import { randomUUID } from "node:crypto";
|
|
364
|
+
var MAX_ID_LENGTH = 256;
|
|
365
|
+
var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
366
|
+
function validateApplicationId(kind, value) {
|
|
367
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
368
|
+
throw new DurableInputError(`${kind} must be a non-empty string`);
|
|
369
|
+
}
|
|
370
|
+
if (value.length > MAX_ID_LENGTH) {
|
|
371
|
+
throw new DurableInputError(
|
|
372
|
+
`${kind} must be at most ${MAX_ID_LENGTH} characters`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (CONTROL_CHARACTERS.test(value)) {
|
|
376
|
+
throw new DurableInputError(`${kind} must not contain control characters`);
|
|
377
|
+
}
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
function instanceId(value) {
|
|
381
|
+
return validateApplicationId("instanceId", value);
|
|
382
|
+
}
|
|
383
|
+
function requestId(value) {
|
|
384
|
+
return validateApplicationId("requestId", value);
|
|
385
|
+
}
|
|
386
|
+
function mintOperationId() {
|
|
387
|
+
return `op_${randomUUID()}`;
|
|
388
|
+
}
|
|
389
|
+
function mintAttemptId() {
|
|
390
|
+
return `att_${randomUUID()}`;
|
|
391
|
+
}
|
|
392
|
+
function mintExecutionId() {
|
|
393
|
+
return `exe_${randomUUID()}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/durable/engine.ts
|
|
397
|
+
var MAX_ATTEMPTS = 4;
|
|
398
|
+
function clampLimit(value, fallback, max) {
|
|
399
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
400
|
+
return fallback;
|
|
401
|
+
}
|
|
402
|
+
return Math.min(max, Math.max(1, Math.floor(value)));
|
|
403
|
+
}
|
|
404
|
+
var RECOVERY_DELAYS_MS = [1e3, 5e3, 3e4];
|
|
405
|
+
var BUSY_RETRY_DELAY_MS = 100;
|
|
406
|
+
var InstanceCore = class {
|
|
407
|
+
constructor(id, flow) {
|
|
408
|
+
this.id = id;
|
|
409
|
+
this.flow = flow;
|
|
410
|
+
}
|
|
411
|
+
id;
|
|
412
|
+
flow;
|
|
413
|
+
listeners = /* @__PURE__ */ new Set();
|
|
414
|
+
activeExecutionId = null;
|
|
415
|
+
timer = null;
|
|
416
|
+
infrastructureRetries = 0;
|
|
417
|
+
/**
|
|
418
|
+
* Set when an infrastructure error stopped execution in this process; shown in snapshots.
|
|
419
|
+
* `restartRecovery` or opening the instance again resumes from persisted state.
|
|
420
|
+
*/
|
|
421
|
+
stall = null;
|
|
422
|
+
get stalled() {
|
|
423
|
+
return this.stall !== null;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
var VOID_CALLBACKS = /* @__PURE__ */ new Set([
|
|
427
|
+
"on-enter",
|
|
428
|
+
"on-exit",
|
|
429
|
+
"on-transition"
|
|
430
|
+
]);
|
|
431
|
+
function errorMessage(error) {
|
|
432
|
+
if (error instanceof Error) {
|
|
433
|
+
return `${error.name}: ${error.message}`;
|
|
434
|
+
}
|
|
435
|
+
return String(error);
|
|
436
|
+
}
|
|
437
|
+
function delayAfterAttempt(attempts) {
|
|
438
|
+
return RECOVERY_DELAYS_MS[attempts - 1] ?? RECOVERY_DELAYS_MS[RECOVERY_DELAYS_MS.length - 1];
|
|
439
|
+
}
|
|
440
|
+
function validateCallbackResult(definition, kind, value) {
|
|
441
|
+
if (VOID_CALLBACKS.has(kind)) {
|
|
442
|
+
return { ok: true, result: null };
|
|
443
|
+
}
|
|
444
|
+
const schema = kind === "guard" ? z2.boolean() : z2.enum([...definition.statesByName.keys()]);
|
|
445
|
+
const parsed = schema.safeParse(value);
|
|
446
|
+
return parsed.success ? { ok: true, result: parsed.data } : {
|
|
447
|
+
ok: false,
|
|
448
|
+
message: `${kind === "guard" ? "guard result must be a boolean" : "start state must be an authored state"}: ${parsed.error.message}`
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
var RuntimeCore = class {
|
|
452
|
+
constructor(store, onObserverError, onRecoveryError) {
|
|
453
|
+
this.store = store;
|
|
454
|
+
this.onObserverError = onObserverError;
|
|
455
|
+
this.onRecoveryError = onRecoveryError;
|
|
456
|
+
}
|
|
457
|
+
store;
|
|
458
|
+
onObserverError;
|
|
459
|
+
onRecoveryError;
|
|
460
|
+
instances = /* @__PURE__ */ new Map();
|
|
461
|
+
active = /* @__PURE__ */ new Set();
|
|
462
|
+
abort = new AbortController();
|
|
463
|
+
closing = false;
|
|
464
|
+
closePromise = null;
|
|
465
|
+
assertOpen() {
|
|
466
|
+
if (this.closing) {
|
|
467
|
+
throw new DurableRuntimeClosedError("The durable runtime is closed");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Stops admission and scheduling, aborts the shared signal, waits for active callbacks to
|
|
472
|
+
* settle and checkpoint, then releases the database (ADR 0061). With `timeoutMs`, rejects with
|
|
473
|
+
* {@link DurableShutdownTimeoutError} if callbacks are still running by then; the database
|
|
474
|
+
* stays owned because those callbacks may still write (ADR 0075). Calling close again keeps waiting.
|
|
475
|
+
*/
|
|
476
|
+
close(options = {}) {
|
|
477
|
+
if (!this.closePromise) {
|
|
478
|
+
this.closing = true;
|
|
479
|
+
for (const instance of this.instances.values()) {
|
|
480
|
+
this.clearTimer(instance);
|
|
481
|
+
}
|
|
482
|
+
this.abort.abort(
|
|
483
|
+
new DurableRuntimeClosedError("The durable runtime is closing")
|
|
484
|
+
);
|
|
485
|
+
this.closePromise = (async () => {
|
|
486
|
+
while (this.active.size > 0) {
|
|
487
|
+
await Promise.allSettled([...this.active]);
|
|
488
|
+
}
|
|
489
|
+
this.store.close();
|
|
490
|
+
})();
|
|
491
|
+
}
|
|
492
|
+
const { timeoutMs } = options;
|
|
493
|
+
if (timeoutMs === void 0) {
|
|
494
|
+
return this.closePromise;
|
|
495
|
+
}
|
|
496
|
+
let timer;
|
|
497
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
498
|
+
timer = setTimeout(
|
|
499
|
+
() => reject(
|
|
500
|
+
new DurableShutdownTimeoutError(
|
|
501
|
+
`${this.active.size} callback execution(s) did not settle within ${timeoutMs} ms; the database is still owned. Exit the process to release it; interrupted work recovers on the next open.`
|
|
502
|
+
)
|
|
503
|
+
),
|
|
504
|
+
timeoutMs
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
return Promise.race([this.closePromise, timeout]).finally(
|
|
508
|
+
() => clearTimeout(timer)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
get isClosed() {
|
|
512
|
+
return this.closing;
|
|
513
|
+
}
|
|
514
|
+
/** Simulates a crash: stops scheduling and drops the connection without waiting. */
|
|
515
|
+
abandon() {
|
|
516
|
+
this.closing = true;
|
|
517
|
+
for (const instance of this.instances.values()) {
|
|
518
|
+
this.clearTimer(instance);
|
|
519
|
+
}
|
|
520
|
+
this.closePromise = Promise.resolve();
|
|
521
|
+
this.store.close();
|
|
522
|
+
}
|
|
523
|
+
// --- observation ---------------------------------------------------------
|
|
524
|
+
snapshot(instance) {
|
|
525
|
+
return this.snapshotOf(instance.id);
|
|
526
|
+
}
|
|
527
|
+
/** Snapshot read from storage; works for instances this runtime has not opened. */
|
|
528
|
+
snapshotOf(instanceId2) {
|
|
529
|
+
this.assertStoreOpen();
|
|
530
|
+
const row = this.requireInstanceRow(instanceId2);
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
instanceId: instanceId2,
|
|
533
|
+
chartName: row.chart_name,
|
|
534
|
+
state: row.state,
|
|
535
|
+
revision: row.revision,
|
|
536
|
+
status: Object.freeze(this.status(row)),
|
|
537
|
+
stall: this.instances.get(instanceId2)?.stall ?? null
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
status(row) {
|
|
541
|
+
const execution = row.current_execution_id === null ? void 0 : this.store.getExecution(row.current_execution_id);
|
|
542
|
+
switch (row.status) {
|
|
543
|
+
case "ready":
|
|
544
|
+
return { kind: "ready" };
|
|
545
|
+
case "terminated":
|
|
546
|
+
return { kind: "terminated", reason: row.status_reason ?? "" };
|
|
547
|
+
case "running":
|
|
548
|
+
return {
|
|
549
|
+
kind: "running",
|
|
550
|
+
requestId: execution?.request_id ?? null
|
|
551
|
+
};
|
|
552
|
+
case "recovering":
|
|
553
|
+
case "needs-attention": {
|
|
554
|
+
const operation = execution ? this.store.findUnresolvedOperation(execution.execution_id) : void 0;
|
|
555
|
+
if (!execution || !operation) {
|
|
556
|
+
return { kind: "running", requestId: null };
|
|
557
|
+
}
|
|
558
|
+
const work = this.work(execution, operation);
|
|
559
|
+
return row.status === "recovering" ? { kind: "recovering", work } : { kind: "needs-attention", work, reason: row.status_reason ?? "" };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
work(execution, operation) {
|
|
564
|
+
return Object.freeze({
|
|
565
|
+
operationId: operation.operation_id,
|
|
566
|
+
requestId: execution.request_id,
|
|
567
|
+
callback: Object.freeze(callbackRef(operation)),
|
|
568
|
+
strategy: operation.strategy,
|
|
569
|
+
attempts: operation.attempts,
|
|
570
|
+
lastError: operation.last_error,
|
|
571
|
+
nextAttemptAt: operation.next_attempt_at
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
currentWork(instanceId2) {
|
|
575
|
+
const row = this.requireInstanceRow(instanceId2);
|
|
576
|
+
if (row.current_execution_id === null) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const execution = this.store.getExecution(row.current_execution_id);
|
|
580
|
+
const operation = execution ? this.store.findUnresolvedOperation(execution.execution_id) : void 0;
|
|
581
|
+
return execution && operation ? this.work(execution, operation) : null;
|
|
582
|
+
}
|
|
583
|
+
assertStoreOpen() {
|
|
584
|
+
if (!this.store.isOpen) {
|
|
585
|
+
throw new DurableRuntimeClosedError("The durable runtime is closed");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
history(instanceId2, query = {}) {
|
|
589
|
+
this.assertStoreOpen();
|
|
590
|
+
const limit = clampLimit(query.limit, 100, 1e3);
|
|
591
|
+
const afterSequence = Math.max(0, Math.floor(query.afterSequence ?? 0));
|
|
592
|
+
const rows = this.store.listHistory(instanceId2, {
|
|
593
|
+
afterSequence,
|
|
594
|
+
limit: limit + 1
|
|
595
|
+
});
|
|
596
|
+
const events = rows.slice(0, limit).map(
|
|
597
|
+
(row) => Object.freeze({
|
|
598
|
+
sequence: row.sequence,
|
|
599
|
+
at: row.at,
|
|
600
|
+
type: row.type,
|
|
601
|
+
requestId: row.request_id,
|
|
602
|
+
operationId: row.operation_id,
|
|
603
|
+
attemptId: row.attempt_id,
|
|
604
|
+
data: parseJson(row.data_json)
|
|
605
|
+
})
|
|
606
|
+
);
|
|
607
|
+
return Object.freeze({
|
|
608
|
+
events: Object.freeze(events),
|
|
609
|
+
nextAfterSequence: rows.length > limit ? events[events.length - 1]?.sequence ?? null : null
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
listInstances(query = {}) {
|
|
613
|
+
this.assertStoreOpen();
|
|
614
|
+
const limit = clampLimit(query.limit, 50, 500);
|
|
615
|
+
const statuses = query.status === void 0 ? null : Array.isArray(query.status) ? query.status : [query.status];
|
|
616
|
+
const rows = this.store.listInstances({
|
|
617
|
+
chartName: query.chartName ?? null,
|
|
618
|
+
statuses,
|
|
619
|
+
afterInstanceId: query.after ?? null,
|
|
620
|
+
limit: limit + 1
|
|
621
|
+
});
|
|
622
|
+
const instances = rows.slice(0, limit).map(
|
|
623
|
+
(row) => Object.freeze({
|
|
624
|
+
instanceId: row.instance_id,
|
|
625
|
+
chartName: row.chart_name,
|
|
626
|
+
state: row.state,
|
|
627
|
+
status: row.status,
|
|
628
|
+
statusReason: row.status_reason,
|
|
629
|
+
revision: row.revision,
|
|
630
|
+
createdAt: row.created_at,
|
|
631
|
+
updatedAt: row.updated_at
|
|
632
|
+
})
|
|
633
|
+
);
|
|
634
|
+
return Object.freeze({
|
|
635
|
+
instances: Object.freeze(instances),
|
|
636
|
+
next: rows.length > limit ? instances[instances.length - 1]?.instanceId ?? null : null
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/** SQLite lock contention can clear without application intervention; other faults stay visible. */
|
|
640
|
+
isTransientInfrastructureError(error) {
|
|
641
|
+
if (typeof error !== "object" || error === null) return false;
|
|
642
|
+
const code = "code" in error ? error.code : void 0;
|
|
643
|
+
const number = "errcode" in error ? error.errcode : void 0;
|
|
644
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" || typeof number === "number" && [5, 6].includes(number & 255);
|
|
645
|
+
}
|
|
646
|
+
/** Records an infrastructure stall and tells observers, without letting a broken store throw. */
|
|
647
|
+
markStalled(instance, error) {
|
|
648
|
+
instance.stall = Object.freeze({
|
|
649
|
+
error: errorMessage(error),
|
|
650
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
651
|
+
});
|
|
652
|
+
try {
|
|
653
|
+
this.notify(instance);
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
if (!this.closing && instance.timer === null && this.isTransientInfrastructureError(error) && instance.infrastructureRetries < RECOVERY_DELAYS_MS.length) {
|
|
657
|
+
const delay = RECOVERY_DELAYS_MS[instance.infrastructureRetries++];
|
|
658
|
+
instance.timer = setTimeout(() => {
|
|
659
|
+
instance.timer = null;
|
|
660
|
+
if (this.closing || !instance.stalled) return;
|
|
661
|
+
try {
|
|
662
|
+
this.startRecovery(instance);
|
|
663
|
+
} catch (failure) {
|
|
664
|
+
this.markStalled(instance, failure);
|
|
665
|
+
this.reportRecoveryError(failure);
|
|
666
|
+
}
|
|
667
|
+
}, delay);
|
|
668
|
+
instance.timer.unref?.();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** Explicit restart after a stall (ADR 0076). Returns false when there was nothing to restart. */
|
|
672
|
+
restartRecovery(instance) {
|
|
673
|
+
this.assertOpen();
|
|
674
|
+
if (instance.stall === null || instance.activeExecutionId !== null) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
this.clearTimer(instance);
|
|
678
|
+
instance.infrastructureRetries = 0;
|
|
679
|
+
this.startRecovery(instance);
|
|
680
|
+
this.notify(instance);
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
/** Runtime-wide snapshot listeners (companion tools). */
|
|
684
|
+
runtimeListeners = /* @__PURE__ */ new Set();
|
|
685
|
+
/** Delivers the current snapshot; listener failures never affect execution (ADR 0059). */
|
|
686
|
+
notify(instance) {
|
|
687
|
+
if (instance.listeners.size === 0 && this.runtimeListeners.size === 0) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
this.store.afterCommit(() => this.deliver(instance));
|
|
691
|
+
}
|
|
692
|
+
deliver(instance) {
|
|
693
|
+
if (!this.store.isOpen) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
let snapshot;
|
|
697
|
+
try {
|
|
698
|
+
snapshot = this.snapshot(instance);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
this.reportObserverError(error);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const listeners = [...instance.listeners, ...this.runtimeListeners];
|
|
704
|
+
queueMicrotask(() => {
|
|
705
|
+
for (const listener of listeners) {
|
|
706
|
+
try {
|
|
707
|
+
const result = listener(snapshot);
|
|
708
|
+
if (isThenable(result)) {
|
|
709
|
+
result.then(
|
|
710
|
+
void 0,
|
|
711
|
+
(error) => this.reportObserverError(error)
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
} catch (error) {
|
|
715
|
+
this.reportObserverError(error);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
reportObserverError(error) {
|
|
721
|
+
try {
|
|
722
|
+
this.onObserverError(error);
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
reportRecoveryError(error) {
|
|
727
|
+
try {
|
|
728
|
+
this.onRecoveryError(error);
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
record(instanceId2, type, data, refs = {}) {
|
|
733
|
+
this.store.appendHistory({
|
|
734
|
+
instance_id: instanceId2,
|
|
735
|
+
type,
|
|
736
|
+
request_id: refs.requestId ?? null,
|
|
737
|
+
operation_id: refs.operationId ?? null,
|
|
738
|
+
attempt_id: refs.attemptId ?? null,
|
|
739
|
+
data_json: canonicalJson(data)
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
requireInstanceRow(instanceId2) {
|
|
743
|
+
const row = this.store.getInstance(instanceId2);
|
|
744
|
+
if (!row) {
|
|
745
|
+
throw new Error(`Durable instance ${instanceId2} is missing from storage`);
|
|
746
|
+
}
|
|
747
|
+
return row;
|
|
748
|
+
}
|
|
749
|
+
// --- execution tracking --------------------------------------------------
|
|
750
|
+
/** Marks the instance busy while `work` runs and lets close() wait for it. */
|
|
751
|
+
track(instance, executionId, work) {
|
|
752
|
+
instance.activeExecutionId = executionId;
|
|
753
|
+
const promise = work().then((result) => {
|
|
754
|
+
instance.infrastructureRetries = 0;
|
|
755
|
+
return result;
|
|
756
|
+
}).catch((error) => {
|
|
757
|
+
this.markStalled(instance, error);
|
|
758
|
+
throw error;
|
|
759
|
+
}).finally(() => {
|
|
760
|
+
instance.activeExecutionId = null;
|
|
761
|
+
this.active.delete(promise);
|
|
762
|
+
});
|
|
763
|
+
this.active.add(promise);
|
|
764
|
+
return promise;
|
|
765
|
+
}
|
|
766
|
+
// --- stepping ------------------------------------------------------------
|
|
767
|
+
/** Runs the execution from its checkpointed step until it finishes or suspends. */
|
|
768
|
+
async advance(instance, executionId) {
|
|
769
|
+
const { definition } = instance.flow;
|
|
770
|
+
for (; ; ) {
|
|
771
|
+
const execution = this.requireExecution(executionId);
|
|
772
|
+
if (execution.status !== "running") {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const step = execution.step;
|
|
776
|
+
switch (step) {
|
|
777
|
+
case "resolve-start-state": {
|
|
778
|
+
const input = parseJson(execution.input_json);
|
|
779
|
+
const resolver = definition.resolveStartState;
|
|
780
|
+
let target2 = input.startState ?? definition.initialState.name;
|
|
781
|
+
if (input.startState === null && resolver) {
|
|
782
|
+
const outcome = await this.runStep(
|
|
783
|
+
instance,
|
|
784
|
+
execution,
|
|
785
|
+
"resolve-start-state"
|
|
786
|
+
);
|
|
787
|
+
if (outcome.kind === "suspended") {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
target2 = outcome.result;
|
|
791
|
+
}
|
|
792
|
+
this.store.updateExecution(executionId, {
|
|
793
|
+
to_state: target2,
|
|
794
|
+
step: "commit"
|
|
795
|
+
});
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
case "commit": {
|
|
799
|
+
this.store.transaction(() => {
|
|
800
|
+
this.store.updateInstance(instance.id, {
|
|
801
|
+
state: execution.to_state ?? void 0
|
|
802
|
+
});
|
|
803
|
+
this.store.updateExecution(executionId, { step: "on-enter" });
|
|
804
|
+
this.record(
|
|
805
|
+
instance.id,
|
|
806
|
+
"state-committed",
|
|
807
|
+
{ from: execution.from_state, to: execution.to_state },
|
|
808
|
+
{ requestId: execution.request_id }
|
|
809
|
+
);
|
|
810
|
+
});
|
|
811
|
+
this.notify(instance);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
case "guard": {
|
|
815
|
+
const outcome = await this.runStep(instance, execution, "guard");
|
|
816
|
+
if (outcome.kind === "suspended") {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (outcome.result === false) {
|
|
820
|
+
this.finish(instance, execution, "blocked");
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
this.store.updateExecution(executionId, { step: "on-exit" });
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
case "on-exit":
|
|
827
|
+
case "on-transition":
|
|
828
|
+
case "on-enter": {
|
|
829
|
+
const outcome = await this.runStep(instance, execution, step);
|
|
830
|
+
if (outcome.kind === "suspended") {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
this.store.updateExecution(executionId, { step: nextStep(step) });
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
case "done":
|
|
837
|
+
this.finish(instance, execution, "applied");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
finish(instance, execution, status) {
|
|
843
|
+
this.store.transaction(() => {
|
|
844
|
+
this.store.updateExecution(execution.execution_id, { status });
|
|
845
|
+
this.store.updateInstance(instance.id, {
|
|
846
|
+
status: "ready",
|
|
847
|
+
status_reason: null,
|
|
848
|
+
current_execution_id: null
|
|
849
|
+
});
|
|
850
|
+
this.record(
|
|
851
|
+
instance.id,
|
|
852
|
+
status === "applied" ? "execution-applied" : "execution-blocked",
|
|
853
|
+
{ kind: execution.kind, transition: execution.transition },
|
|
854
|
+
{ requestId: execution.request_id }
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
this.notify(instance);
|
|
858
|
+
}
|
|
859
|
+
invocation(instance, execution, kind) {
|
|
860
|
+
const { definition, config } = instance.flow;
|
|
861
|
+
const chartName = definition.chartName;
|
|
862
|
+
if (kind === "resolve-start-state") {
|
|
863
|
+
return {
|
|
864
|
+
callback: definition.resolveStartState,
|
|
865
|
+
context: {
|
|
866
|
+
chartName,
|
|
867
|
+
initialStateName: definition.initialState.name,
|
|
868
|
+
config
|
|
869
|
+
},
|
|
870
|
+
args: []
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (execution.kind === "create") {
|
|
874
|
+
const target2 = this.requireState(definition, execution.to_state);
|
|
875
|
+
const isInitial = target2.id === definition.initialState.id;
|
|
876
|
+
return {
|
|
877
|
+
// Restoring into a non-initial state does not run onEnter (ADR 0035).
|
|
878
|
+
callback: isInitial ? target2.onEnter : null,
|
|
879
|
+
context: {
|
|
880
|
+
chartName,
|
|
881
|
+
stateName: target2.name,
|
|
882
|
+
transitionName: "default",
|
|
883
|
+
config
|
|
884
|
+
},
|
|
885
|
+
args: []
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const transition = definition.transitionsByName.get(
|
|
889
|
+
execution.transition ?? ""
|
|
890
|
+
);
|
|
891
|
+
if (!transition) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
`Transition ${execution.transition} is missing from definition ${definition.chartName}`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const payload = parseJson(execution.input_json).payload;
|
|
897
|
+
switch (kind) {
|
|
898
|
+
case "guard":
|
|
899
|
+
case "on-transition":
|
|
900
|
+
return {
|
|
901
|
+
callback: kind === "guard" ? transition.guard : transition.onTransition,
|
|
902
|
+
context: {
|
|
903
|
+
chartName,
|
|
904
|
+
fromState: transition.from.name,
|
|
905
|
+
toState: transition.to.name,
|
|
906
|
+
transitionName: transition.name,
|
|
907
|
+
config
|
|
908
|
+
},
|
|
909
|
+
args: [payload]
|
|
910
|
+
};
|
|
911
|
+
case "on-exit":
|
|
912
|
+
return {
|
|
913
|
+
callback: transition.from.onExit,
|
|
914
|
+
context: {
|
|
915
|
+
chartName,
|
|
916
|
+
stateName: transition.from.name,
|
|
917
|
+
transitionName: transition.name,
|
|
918
|
+
config
|
|
919
|
+
},
|
|
920
|
+
args: []
|
|
921
|
+
};
|
|
922
|
+
case "on-enter":
|
|
923
|
+
return {
|
|
924
|
+
callback: transition.to.onEnter,
|
|
925
|
+
context: {
|
|
926
|
+
chartName,
|
|
927
|
+
stateName: transition.to.name,
|
|
928
|
+
transitionName: transition.name,
|
|
929
|
+
config
|
|
930
|
+
},
|
|
931
|
+
args: []
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
requireState(definition, name) {
|
|
936
|
+
const state = definition.statesByName.get(name ?? "");
|
|
937
|
+
if (!state) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`State ${name} is missing from definition ${definition.chartName}`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return state;
|
|
943
|
+
}
|
|
944
|
+
/** Returns a checkpointed result, or creates the operation and runs it once. */
|
|
945
|
+
async runStep(instance, execution, kind) {
|
|
946
|
+
const invocation = this.invocation(instance, execution, kind);
|
|
947
|
+
if (!invocation.callback) {
|
|
948
|
+
return { kind: "completed", result: null };
|
|
949
|
+
}
|
|
950
|
+
const existing = this.store.findOperation(execution.execution_id, kind);
|
|
951
|
+
if (existing) {
|
|
952
|
+
return existing.status === "completed" ? { kind: "completed", result: parseJson(existing.result_json) } : { kind: "suspended" };
|
|
953
|
+
}
|
|
954
|
+
const operation = {
|
|
955
|
+
operation_id: mintOperationId(),
|
|
956
|
+
execution_id: execution.execution_id,
|
|
957
|
+
instance_id: instance.id,
|
|
958
|
+
callback_kind: kind,
|
|
959
|
+
callback_target: callbackTarget(invocation, kind),
|
|
960
|
+
strategy: invocation.callback.strategy,
|
|
961
|
+
status: "running",
|
|
962
|
+
result_json: null,
|
|
963
|
+
attempts: 0,
|
|
964
|
+
attempt_limit: MAX_ATTEMPTS,
|
|
965
|
+
last_error: null,
|
|
966
|
+
last_cause: null,
|
|
967
|
+
next_attempt_at: null
|
|
968
|
+
};
|
|
969
|
+
return this.attempt(instance, execution, operation, "run", {
|
|
970
|
+
prelude: () => this.store.insertOperation(operation)
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* One attempt at an operation. The operation and attempt are persisted in one transaction
|
|
975
|
+
* before user code runs, so a crash mid-callback is detected as interrupted on the next open
|
|
976
|
+
* (ADR 0055, 0065). The attempt's result is settled in a second single transaction.
|
|
977
|
+
*/
|
|
978
|
+
async attempt(instance, execution, operation, mode, options = {}) {
|
|
979
|
+
const invocation = this.invocation(
|
|
980
|
+
instance,
|
|
981
|
+
execution,
|
|
982
|
+
operation.callback_kind
|
|
983
|
+
);
|
|
984
|
+
const callback = invocation.callback;
|
|
985
|
+
if (!callback) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
`Operation ${operation.operation_id} has no callback in the current definition`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
const attemptNumber = operation.attempts + 1;
|
|
991
|
+
const attemptId = mintAttemptId();
|
|
992
|
+
this.store.transaction(() => {
|
|
993
|
+
options.prelude?.();
|
|
994
|
+
this.store.updateOperation(operation.operation_id, {
|
|
995
|
+
status: "running",
|
|
996
|
+
attempts: attemptNumber,
|
|
997
|
+
next_attempt_at: null
|
|
998
|
+
});
|
|
999
|
+
this.store.insertAttempt({
|
|
1000
|
+
attempt_id: attemptId,
|
|
1001
|
+
operation_id: operation.operation_id,
|
|
1002
|
+
mode,
|
|
1003
|
+
number: attemptNumber
|
|
1004
|
+
});
|
|
1005
|
+
this.record(
|
|
1006
|
+
instance.id,
|
|
1007
|
+
"callback-started",
|
|
1008
|
+
{ callback: operation.callback_kind, mode, attempt: attemptNumber },
|
|
1009
|
+
{
|
|
1010
|
+
requestId: execution.request_id,
|
|
1011
|
+
operationId: operation.operation_id,
|
|
1012
|
+
attemptId
|
|
1013
|
+
}
|
|
1014
|
+
);
|
|
1015
|
+
});
|
|
1016
|
+
const refs = {
|
|
1017
|
+
requestId: execution.request_id,
|
|
1018
|
+
operationId: operation.operation_id,
|
|
1019
|
+
attemptId
|
|
1020
|
+
};
|
|
1021
|
+
const context = {
|
|
1022
|
+
...invocation.context,
|
|
1023
|
+
instanceId: instance.id,
|
|
1024
|
+
operation: Object.freeze({
|
|
1025
|
+
operationId: operation.operation_id,
|
|
1026
|
+
attemptId,
|
|
1027
|
+
attempt: attemptNumber
|
|
1028
|
+
}),
|
|
1029
|
+
signal: this.abort.signal
|
|
1030
|
+
};
|
|
1031
|
+
const current = this.requireOperation(operation.operation_id);
|
|
1032
|
+
if (mode === "run") {
|
|
1033
|
+
let value;
|
|
1034
|
+
try {
|
|
1035
|
+
value = await callback.run(context, ...invocation.args);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
const cause = this.closing ? "interrupted" : "failed";
|
|
1038
|
+
return this.store.transaction(() => {
|
|
1039
|
+
this.store.finishAttempt(attemptId, cause, errorMessage(error));
|
|
1040
|
+
return this.failed(
|
|
1041
|
+
instance,
|
|
1042
|
+
execution,
|
|
1043
|
+
current,
|
|
1044
|
+
cause,
|
|
1045
|
+
errorMessage(error),
|
|
1046
|
+
refs
|
|
1047
|
+
);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
return this.store.transaction(() => {
|
|
1051
|
+
const validated = validateCallbackResult(
|
|
1052
|
+
instance.flow.definition,
|
|
1053
|
+
operation.callback_kind,
|
|
1054
|
+
value
|
|
1055
|
+
);
|
|
1056
|
+
if (!validated.ok) {
|
|
1057
|
+
this.store.finishAttempt(
|
|
1058
|
+
attemptId,
|
|
1059
|
+
"invalid-result",
|
|
1060
|
+
validated.message
|
|
1061
|
+
);
|
|
1062
|
+
return this.escalate(
|
|
1063
|
+
instance,
|
|
1064
|
+
execution,
|
|
1065
|
+
current,
|
|
1066
|
+
`Invalid callback result: ${validated.message}`,
|
|
1067
|
+
refs
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return this.complete(
|
|
1071
|
+
instance,
|
|
1072
|
+
execution,
|
|
1073
|
+
current,
|
|
1074
|
+
attemptId,
|
|
1075
|
+
validated.result,
|
|
1076
|
+
refs
|
|
1077
|
+
);
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
const reconcile = callback.reconcile;
|
|
1081
|
+
if (!reconcile) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`Operation ${operation.operation_id} has no reconcile function`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
let decision;
|
|
1087
|
+
try {
|
|
1088
|
+
decision = await reconcile(
|
|
1089
|
+
{
|
|
1090
|
+
...context,
|
|
1091
|
+
recovery: Object.freeze({
|
|
1092
|
+
cause: operation.last_cause ?? "failed",
|
|
1093
|
+
lastError: operation.last_error
|
|
1094
|
+
})
|
|
1095
|
+
},
|
|
1096
|
+
...invocation.args
|
|
1097
|
+
);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
return this.store.transaction(() => {
|
|
1100
|
+
this.store.finishAttempt(attemptId, "failed", errorMessage(error));
|
|
1101
|
+
this.store.updateOperation(current.operation_id, {
|
|
1102
|
+
last_error: errorMessage(error)
|
|
1103
|
+
});
|
|
1104
|
+
return this.scheduleOrEscalate(
|
|
1105
|
+
instance,
|
|
1106
|
+
execution,
|
|
1107
|
+
this.requireOperation(current.operation_id),
|
|
1108
|
+
refs,
|
|
1109
|
+
this.closing ? 0 : delayAfterAttempt(attemptNumber)
|
|
1110
|
+
);
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const settled = this.store.transaction(
|
|
1114
|
+
() => this.settleReconcile(
|
|
1115
|
+
instance,
|
|
1116
|
+
execution,
|
|
1117
|
+
current,
|
|
1118
|
+
attemptId,
|
|
1119
|
+
decision,
|
|
1120
|
+
refs
|
|
1121
|
+
)
|
|
1122
|
+
);
|
|
1123
|
+
return settled === "replay" ? this.attempt(instance, execution, current, "run") : settled;
|
|
1124
|
+
}
|
|
1125
|
+
/** Applies one reconciliation decision (ADR 0064). Runs inside a transaction. */
|
|
1126
|
+
settleReconcile(instance, execution, operation, attemptId, decision, refs) {
|
|
1127
|
+
const kind = isRecord2(decision) ? decision.decision : void 0;
|
|
1128
|
+
switch (kind) {
|
|
1129
|
+
case "completed": {
|
|
1130
|
+
const validated = validateCallbackResult(
|
|
1131
|
+
instance.flow.definition,
|
|
1132
|
+
operation.callback_kind,
|
|
1133
|
+
decision.result
|
|
1134
|
+
);
|
|
1135
|
+
if (!validated.ok) {
|
|
1136
|
+
this.store.finishAttempt(
|
|
1137
|
+
attemptId,
|
|
1138
|
+
"invalid-result",
|
|
1139
|
+
validated.message
|
|
1140
|
+
);
|
|
1141
|
+
return this.escalate(
|
|
1142
|
+
instance,
|
|
1143
|
+
execution,
|
|
1144
|
+
operation,
|
|
1145
|
+
`Invalid reconciliation result: ${validated.message}`,
|
|
1146
|
+
refs
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
return this.complete(
|
|
1150
|
+
instance,
|
|
1151
|
+
execution,
|
|
1152
|
+
operation,
|
|
1153
|
+
attemptId,
|
|
1154
|
+
validated.result,
|
|
1155
|
+
refs
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
case "safe-to-retry":
|
|
1159
|
+
this.store.finishAttempt(attemptId, "safe-to-retry", null);
|
|
1160
|
+
if (operation.attempts >= operation.attempt_limit) {
|
|
1161
|
+
return this.escalate(
|
|
1162
|
+
instance,
|
|
1163
|
+
execution,
|
|
1164
|
+
operation,
|
|
1165
|
+
exhaustedReason(operation),
|
|
1166
|
+
refs
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
return "replay";
|
|
1170
|
+
case "pending":
|
|
1171
|
+
this.store.finishAttempt(attemptId, "pending", null);
|
|
1172
|
+
return this.scheduleOrEscalate(
|
|
1173
|
+
instance,
|
|
1174
|
+
execution,
|
|
1175
|
+
operation,
|
|
1176
|
+
refs,
|
|
1177
|
+
delayAfterAttempt(operation.attempts)
|
|
1178
|
+
);
|
|
1179
|
+
case "needs-attention": {
|
|
1180
|
+
const reason = decision.reason;
|
|
1181
|
+
this.store.finishAttempt(attemptId, "needs-attention", null);
|
|
1182
|
+
return this.escalate(
|
|
1183
|
+
instance,
|
|
1184
|
+
execution,
|
|
1185
|
+
operation,
|
|
1186
|
+
typeof reason === "string" && reason.length > 0 ? reason : "Reconciliation requires attention",
|
|
1187
|
+
refs
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
default:
|
|
1191
|
+
this.store.finishAttempt(attemptId, "invalid-decision", null);
|
|
1192
|
+
return this.escalate(
|
|
1193
|
+
instance,
|
|
1194
|
+
execution,
|
|
1195
|
+
operation,
|
|
1196
|
+
"Reconciliation returned an invalid decision",
|
|
1197
|
+
refs
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
complete(instance, execution, operation, attemptId, result, refs) {
|
|
1202
|
+
this.store.transaction(() => {
|
|
1203
|
+
this.checkpoint(instance, execution, operation, result);
|
|
1204
|
+
this.store.finishAttempt(attemptId, "completed", null);
|
|
1205
|
+
this.record(
|
|
1206
|
+
instance.id,
|
|
1207
|
+
"callback-completed",
|
|
1208
|
+
{ callback: operation.callback_kind, result },
|
|
1209
|
+
refs
|
|
1210
|
+
);
|
|
1211
|
+
});
|
|
1212
|
+
return { kind: "completed", result };
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Records a callback as completed and, in the same transaction, marks a suspended execution
|
|
1216
|
+
* running again, so a crash can never leave a completed callback whose execution never resumes.
|
|
1217
|
+
* Must run inside a transaction.
|
|
1218
|
+
*/
|
|
1219
|
+
checkpoint(instance, execution, operation, result) {
|
|
1220
|
+
this.store.updateOperation(operation.operation_id, {
|
|
1221
|
+
status: "completed",
|
|
1222
|
+
result_json: JSON.stringify(result),
|
|
1223
|
+
next_attempt_at: null
|
|
1224
|
+
});
|
|
1225
|
+
if (this.requireExecution(execution.execution_id).status !== "running") {
|
|
1226
|
+
this.store.updateExecution(execution.execution_id, { status: "running" });
|
|
1227
|
+
this.store.updateInstance(instance.id, {
|
|
1228
|
+
status: "running",
|
|
1229
|
+
status_reason: null
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/** Applies the declared strategy after a failed or interrupted run (ADR 0046). */
|
|
1234
|
+
failed(instance, execution, operation, cause, message, refs) {
|
|
1235
|
+
return this.store.transaction(() => {
|
|
1236
|
+
this.store.updateOperation(operation.operation_id, {
|
|
1237
|
+
last_error: message,
|
|
1238
|
+
last_cause: cause
|
|
1239
|
+
});
|
|
1240
|
+
this.record(
|
|
1241
|
+
instance.id,
|
|
1242
|
+
cause === "interrupted" ? "recovery-interrupted" : "callback-failed",
|
|
1243
|
+
{ callback: operation.callback_kind, error: message },
|
|
1244
|
+
refs
|
|
1245
|
+
);
|
|
1246
|
+
return this.settleFailure(
|
|
1247
|
+
instance,
|
|
1248
|
+
execution,
|
|
1249
|
+
operation,
|
|
1250
|
+
cause,
|
|
1251
|
+
message,
|
|
1252
|
+
refs
|
|
1253
|
+
);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
settleFailure(instance, execution, operation, cause, message, refs) {
|
|
1257
|
+
const current = this.requireOperation(operation.operation_id);
|
|
1258
|
+
if (current.strategy === "manual") {
|
|
1259
|
+
return this.escalate(
|
|
1260
|
+
instance,
|
|
1261
|
+
execution,
|
|
1262
|
+
current,
|
|
1263
|
+
`Manual recovery required: ${message}`,
|
|
1264
|
+
refs
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return this.scheduleOrEscalate(
|
|
1268
|
+
instance,
|
|
1269
|
+
execution,
|
|
1270
|
+
current,
|
|
1271
|
+
refs,
|
|
1272
|
+
cause === "interrupted" ? 0 : delayAfterAttempt(current.attempts)
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
scheduleOrEscalate(instance, execution, operation, refs, delayMs) {
|
|
1276
|
+
if (operation.attempts >= operation.attempt_limit) {
|
|
1277
|
+
return this.escalate(
|
|
1278
|
+
instance,
|
|
1279
|
+
execution,
|
|
1280
|
+
operation,
|
|
1281
|
+
exhaustedReason(operation),
|
|
1282
|
+
refs
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const nextAttemptAt = new Date(Date.now() + delayMs).toISOString();
|
|
1286
|
+
this.store.transaction(() => {
|
|
1287
|
+
this.store.updateOperation(operation.operation_id, {
|
|
1288
|
+
status: "recovering",
|
|
1289
|
+
next_attempt_at: nextAttemptAt
|
|
1290
|
+
});
|
|
1291
|
+
this.store.updateExecution(execution.execution_id, {
|
|
1292
|
+
status: "recovering"
|
|
1293
|
+
});
|
|
1294
|
+
this.store.updateInstance(instance.id, {
|
|
1295
|
+
status: "recovering",
|
|
1296
|
+
status_reason: null
|
|
1297
|
+
});
|
|
1298
|
+
this.record(
|
|
1299
|
+
instance.id,
|
|
1300
|
+
"recovery-scheduled",
|
|
1301
|
+
{ callback: operation.callback_kind, nextAttemptAt },
|
|
1302
|
+
refs
|
|
1303
|
+
);
|
|
1304
|
+
});
|
|
1305
|
+
this.scheduleTimer(instance, delayMs);
|
|
1306
|
+
this.notify(instance);
|
|
1307
|
+
return { kind: "suspended" };
|
|
1308
|
+
}
|
|
1309
|
+
escalate(instance, execution, operation, reason, refs) {
|
|
1310
|
+
this.store.transaction(() => {
|
|
1311
|
+
this.store.updateOperation(operation.operation_id, {
|
|
1312
|
+
status: "needs-attention",
|
|
1313
|
+
next_attempt_at: null
|
|
1314
|
+
});
|
|
1315
|
+
this.store.updateExecution(execution.execution_id, {
|
|
1316
|
+
status: "needs-attention"
|
|
1317
|
+
});
|
|
1318
|
+
this.store.updateInstance(instance.id, {
|
|
1319
|
+
status: "needs-attention",
|
|
1320
|
+
status_reason: reason
|
|
1321
|
+
});
|
|
1322
|
+
this.record(
|
|
1323
|
+
instance.id,
|
|
1324
|
+
"needs-attention",
|
|
1325
|
+
{ callback: operation.callback_kind, reason },
|
|
1326
|
+
refs
|
|
1327
|
+
);
|
|
1328
|
+
});
|
|
1329
|
+
this.notify(instance);
|
|
1330
|
+
return { kind: "suspended" };
|
|
1331
|
+
}
|
|
1332
|
+
// --- recovery scheduling -------------------------------------------------
|
|
1333
|
+
scheduleTimer(instance, delayMs) {
|
|
1334
|
+
this.store.afterCommit(() => this.armTimer(instance, delayMs));
|
|
1335
|
+
}
|
|
1336
|
+
armTimer(instance, delayMs) {
|
|
1337
|
+
this.clearTimer(instance);
|
|
1338
|
+
if (this.closing) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const timer = setTimeout(() => {
|
|
1342
|
+
instance.timer = null;
|
|
1343
|
+
void this.recover(instance).catch((error) => {
|
|
1344
|
+
this.markStalled(instance, error);
|
|
1345
|
+
this.reportRecoveryError(error);
|
|
1346
|
+
});
|
|
1347
|
+
}, delayMs);
|
|
1348
|
+
timer.unref?.();
|
|
1349
|
+
instance.timer = timer;
|
|
1350
|
+
}
|
|
1351
|
+
clearTimer(instance) {
|
|
1352
|
+
if (instance.timer !== null) {
|
|
1353
|
+
clearTimeout(instance.timer);
|
|
1354
|
+
instance.timer = null;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/** One scheduled automatic recovery attempt for the instance's unresolved operation. */
|
|
1358
|
+
async recover(instance) {
|
|
1359
|
+
if (this.closing) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (instance.activeExecutionId !== null) {
|
|
1363
|
+
this.scheduleTimer(instance, BUSY_RETRY_DELAY_MS);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const row = this.requireInstanceRow(instance.id);
|
|
1367
|
+
if (row.status !== "recovering" || row.current_execution_id === null) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const execution = this.requireExecution(row.current_execution_id);
|
|
1371
|
+
const operation = this.store.findUnresolvedOperation(
|
|
1372
|
+
execution.execution_id
|
|
1373
|
+
);
|
|
1374
|
+
if (!operation || operation.status !== "recovering") {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
await this.track(
|
|
1378
|
+
instance,
|
|
1379
|
+
execution.execution_id,
|
|
1380
|
+
() => this.recoverOnce(instance, execution, operation)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* One recovery attempt following the declared strategy (reconcile, or run again for
|
|
1385
|
+
* safe-replay and manual), then the execution's remaining steps if it completed.
|
|
1386
|
+
*/
|
|
1387
|
+
async recoverOnce(instance, execution, operation, prelude) {
|
|
1388
|
+
const mode = operation.strategy === "reconcile" ? "reconcile" : "run";
|
|
1389
|
+
const outcome = await this.attempt(instance, execution, operation, mode, {
|
|
1390
|
+
prelude
|
|
1391
|
+
});
|
|
1392
|
+
if (outcome.kind === "completed") {
|
|
1393
|
+
this.notify(instance);
|
|
1394
|
+
await this.advance(instance, execution.execution_id);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Operator retry (ADR 0073): grants one more bounded attempt and commits it, the resumed
|
|
1399
|
+
* status and the audit entry together with the attempt start, so a crash cannot strand it.
|
|
1400
|
+
*/
|
|
1401
|
+
manualRetry(instance, execution, operation, audit) {
|
|
1402
|
+
return this.track(
|
|
1403
|
+
instance,
|
|
1404
|
+
execution.execution_id,
|
|
1405
|
+
() => this.recoverOnce(instance, execution, operation, () => {
|
|
1406
|
+
this.store.updateOperation(operation.operation_id, {
|
|
1407
|
+
// A reconcile check may conclude safe-to-retry, so it also gets the replay it permits.
|
|
1408
|
+
attempt_limit: operation.attempts + (operation.strategy === "reconcile" ? 2 : 1)
|
|
1409
|
+
});
|
|
1410
|
+
this.store.updateExecution(execution.execution_id, {
|
|
1411
|
+
status: "running"
|
|
1412
|
+
});
|
|
1413
|
+
this.store.updateInstance(instance.id, {
|
|
1414
|
+
status: "running",
|
|
1415
|
+
status_reason: null
|
|
1416
|
+
});
|
|
1417
|
+
audit();
|
|
1418
|
+
})
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Starts recovery for an instance loaded from storage (ADR 0050), or restarts it after a
|
|
1423
|
+
* stall. Also repairs an execution whose callback completed but which never resumed.
|
|
1424
|
+
*/
|
|
1425
|
+
startRecovery(instance) {
|
|
1426
|
+
instance.stall = null;
|
|
1427
|
+
if (instance.activeExecutionId !== null) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const row = this.requireInstanceRow(instance.id);
|
|
1431
|
+
if (row.status === "ready" || row.status === "terminated" || row.current_execution_id === null) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const execution = this.requireExecution(row.current_execution_id);
|
|
1435
|
+
const operation = this.store.findUnresolvedOperation(
|
|
1436
|
+
execution.execution_id
|
|
1437
|
+
);
|
|
1438
|
+
if (operation?.status === "running") {
|
|
1439
|
+
this.store.transaction(() => {
|
|
1440
|
+
this.store.finishInterruptedAttempts(operation.operation_id);
|
|
1441
|
+
this.failed(
|
|
1442
|
+
instance,
|
|
1443
|
+
execution,
|
|
1444
|
+
operation,
|
|
1445
|
+
"interrupted",
|
|
1446
|
+
"The process stopped while the callback was running",
|
|
1447
|
+
{
|
|
1448
|
+
requestId: execution.request_id,
|
|
1449
|
+
operationId: operation.operation_id
|
|
1450
|
+
}
|
|
1451
|
+
);
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (operation?.status === "recovering") {
|
|
1456
|
+
const due = operation.next_attempt_at ? Date.parse(operation.next_attempt_at) - Date.now() : 0;
|
|
1457
|
+
this.scheduleTimer(instance, Math.max(0, due));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (operation) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (execution.status !== "running") {
|
|
1464
|
+
this.store.transaction(() => {
|
|
1465
|
+
this.store.updateExecution(execution.execution_id, {
|
|
1466
|
+
status: "running"
|
|
1467
|
+
});
|
|
1468
|
+
this.store.updateInstance(instance.id, {
|
|
1469
|
+
status: "running",
|
|
1470
|
+
status_reason: null
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
void this.track(
|
|
1475
|
+
instance,
|
|
1476
|
+
execution.execution_id,
|
|
1477
|
+
() => this.advance(instance, execution.execution_id)
|
|
1478
|
+
).catch((error) => {
|
|
1479
|
+
this.markStalled(instance, error);
|
|
1480
|
+
this.reportRecoveryError(error);
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
// --- lookups -------------------------------------------------------------
|
|
1484
|
+
requireExecution(executionId) {
|
|
1485
|
+
const execution = this.store.getExecution(executionId);
|
|
1486
|
+
if (!execution) {
|
|
1487
|
+
throw new Error(`Execution ${executionId} is missing from storage`);
|
|
1488
|
+
}
|
|
1489
|
+
return execution;
|
|
1490
|
+
}
|
|
1491
|
+
requireOperation(operationId) {
|
|
1492
|
+
const operation = this.store.getOperation(operationId);
|
|
1493
|
+
if (!operation) {
|
|
1494
|
+
throw new Error(`Operation ${operationId} is missing from storage`);
|
|
1495
|
+
}
|
|
1496
|
+
return operation;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
function isThenable(value) {
|
|
1500
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
1501
|
+
}
|
|
1502
|
+
function isRecord2(value) {
|
|
1503
|
+
return typeof value === "object" && value !== null;
|
|
1504
|
+
}
|
|
1505
|
+
function exhaustedReason(operation) {
|
|
1506
|
+
const last = operation.last_error ? `: ${operation.last_error}` : "";
|
|
1507
|
+
return `Automatic recovery exhausted after ${operation.attempts} attempts${last}`;
|
|
1508
|
+
}
|
|
1509
|
+
function nextStep(step) {
|
|
1510
|
+
switch (step) {
|
|
1511
|
+
case "on-exit":
|
|
1512
|
+
return "on-transition";
|
|
1513
|
+
case "on-transition":
|
|
1514
|
+
return "commit";
|
|
1515
|
+
case "on-enter":
|
|
1516
|
+
return "done";
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function callbackTarget(invocation, kind) {
|
|
1520
|
+
const context = invocation.context;
|
|
1521
|
+
switch (kind) {
|
|
1522
|
+
case "resolve-start-state":
|
|
1523
|
+
return null;
|
|
1524
|
+
case "on-enter":
|
|
1525
|
+
case "on-exit":
|
|
1526
|
+
return context.stateName;
|
|
1527
|
+
case "guard":
|
|
1528
|
+
case "on-transition":
|
|
1529
|
+
return context.transitionName;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function callbackRef(operation) {
|
|
1533
|
+
const target2 = operation.callback_target ?? "";
|
|
1534
|
+
switch (operation.callback_kind) {
|
|
1535
|
+
case "resolve-start-state":
|
|
1536
|
+
return { kind: "resolve-start-state" };
|
|
1537
|
+
case "on-enter":
|
|
1538
|
+
case "on-exit":
|
|
1539
|
+
return { kind: operation.callback_kind, state: target2 };
|
|
1540
|
+
case "guard":
|
|
1541
|
+
case "on-transition":
|
|
1542
|
+
return { kind: operation.callback_kind, transition: target2 };
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/durable/resolve-input.ts
|
|
1547
|
+
import { z as z3 } from "zod";
|
|
1548
|
+
var target = {
|
|
1549
|
+
operationId: z3.string().min(1).transform((value) => value),
|
|
1550
|
+
expectedAttempts: z3.number().int().nonnegative(),
|
|
1551
|
+
reason: z3.string().refine((value) => value.trim().length > 0, "A reason is required")
|
|
1552
|
+
};
|
|
1553
|
+
var confirmation = z3.discriminatedUnion("callback", [
|
|
1554
|
+
z3.strictObject({
|
|
1555
|
+
...target,
|
|
1556
|
+
action: z3.literal("confirm-completed"),
|
|
1557
|
+
callback: z3.literal("guard"),
|
|
1558
|
+
result: z3.boolean()
|
|
1559
|
+
}),
|
|
1560
|
+
z3.strictObject({
|
|
1561
|
+
...target,
|
|
1562
|
+
action: z3.literal("confirm-completed"),
|
|
1563
|
+
callback: z3.literal("resolve-start-state"),
|
|
1564
|
+
result: z3.string()
|
|
1565
|
+
}),
|
|
1566
|
+
z3.strictObject({
|
|
1567
|
+
...target,
|
|
1568
|
+
action: z3.literal("confirm-completed"),
|
|
1569
|
+
callback: z3.enum(["on-enter", "on-exit", "on-transition"]),
|
|
1570
|
+
result: z3.null().optional()
|
|
1571
|
+
}).transform(({ result, ...input }) => {
|
|
1572
|
+
void result;
|
|
1573
|
+
return input;
|
|
1574
|
+
})
|
|
1575
|
+
]);
|
|
1576
|
+
var resolveInputSchema = z3.discriminatedUnion("action", [
|
|
1577
|
+
z3.strictObject({ ...target, action: z3.literal("retry") }),
|
|
1578
|
+
z3.strictObject({ ...target, action: z3.literal("terminate") }),
|
|
1579
|
+
confirmation
|
|
1580
|
+
]);
|
|
1581
|
+
function parseResolveInput(value) {
|
|
1582
|
+
const result = resolveInputSchema.safeParse(value);
|
|
1583
|
+
if (!result.success) {
|
|
1584
|
+
throw new DurableInputError(
|
|
1585
|
+
`Invalid resolve input: ${result.error.message}`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
return result.data;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/durable/store.ts
|
|
1592
|
+
import { DatabaseSync } from "node:sqlite";
|
|
1593
|
+
var SCHEMA_VERSION = 2;
|
|
1594
|
+
var MIGRATIONS = {
|
|
1595
|
+
// v1 (first durable runtime) lacked per-execution handler revisions (ADR 0076).
|
|
1596
|
+
1: "ALTER TABLE executions ADD COLUMN handler_revision TEXT;"
|
|
1597
|
+
};
|
|
1598
|
+
var SCHEMA = `
|
|
1599
|
+
CREATE TABLE instances (
|
|
1600
|
+
instance_id TEXT PRIMARY KEY,
|
|
1601
|
+
chart_name TEXT NOT NULL,
|
|
1602
|
+
definition_id TEXT NOT NULL,
|
|
1603
|
+
state TEXT NOT NULL,
|
|
1604
|
+
status TEXT NOT NULL,
|
|
1605
|
+
status_reason TEXT,
|
|
1606
|
+
current_execution_id TEXT,
|
|
1607
|
+
revision INTEGER NOT NULL,
|
|
1608
|
+
created_at TEXT NOT NULL,
|
|
1609
|
+
updated_at TEXT NOT NULL
|
|
1610
|
+
);
|
|
1611
|
+
CREATE TABLE executions (
|
|
1612
|
+
execution_id TEXT PRIMARY KEY,
|
|
1613
|
+
instance_id TEXT NOT NULL REFERENCES instances(instance_id),
|
|
1614
|
+
kind TEXT NOT NULL,
|
|
1615
|
+
request_id TEXT,
|
|
1616
|
+
input_json TEXT NOT NULL,
|
|
1617
|
+
from_state TEXT,
|
|
1618
|
+
transition TEXT,
|
|
1619
|
+
to_state TEXT,
|
|
1620
|
+
step TEXT NOT NULL,
|
|
1621
|
+
status TEXT NOT NULL,
|
|
1622
|
+
handler_revision TEXT,
|
|
1623
|
+
created_at TEXT NOT NULL,
|
|
1624
|
+
updated_at TEXT NOT NULL,
|
|
1625
|
+
UNIQUE (instance_id, request_id)
|
|
1626
|
+
);
|
|
1627
|
+
CREATE TABLE operations (
|
|
1628
|
+
operation_id TEXT PRIMARY KEY,
|
|
1629
|
+
execution_id TEXT NOT NULL REFERENCES executions(execution_id),
|
|
1630
|
+
instance_id TEXT NOT NULL REFERENCES instances(instance_id),
|
|
1631
|
+
callback_kind TEXT NOT NULL,
|
|
1632
|
+
callback_target TEXT,
|
|
1633
|
+
strategy TEXT NOT NULL,
|
|
1634
|
+
status TEXT NOT NULL,
|
|
1635
|
+
result_json TEXT,
|
|
1636
|
+
attempts INTEGER NOT NULL,
|
|
1637
|
+
attempt_limit INTEGER NOT NULL,
|
|
1638
|
+
last_error TEXT,
|
|
1639
|
+
last_cause TEXT,
|
|
1640
|
+
next_attempt_at TEXT,
|
|
1641
|
+
created_at TEXT NOT NULL,
|
|
1642
|
+
updated_at TEXT NOT NULL,
|
|
1643
|
+
UNIQUE (execution_id, callback_kind)
|
|
1644
|
+
);
|
|
1645
|
+
CREATE TABLE attempts (
|
|
1646
|
+
attempt_id TEXT PRIMARY KEY,
|
|
1647
|
+
operation_id TEXT NOT NULL REFERENCES operations(operation_id),
|
|
1648
|
+
mode TEXT NOT NULL,
|
|
1649
|
+
number INTEGER NOT NULL,
|
|
1650
|
+
started_at TEXT NOT NULL,
|
|
1651
|
+
finished_at TEXT,
|
|
1652
|
+
outcome TEXT,
|
|
1653
|
+
error TEXT
|
|
1654
|
+
);
|
|
1655
|
+
CREATE TABLE history (
|
|
1656
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1657
|
+
instance_id TEXT NOT NULL REFERENCES instances(instance_id),
|
|
1658
|
+
at TEXT NOT NULL,
|
|
1659
|
+
type TEXT NOT NULL,
|
|
1660
|
+
request_id TEXT,
|
|
1661
|
+
operation_id TEXT,
|
|
1662
|
+
attempt_id TEXT,
|
|
1663
|
+
data_json TEXT NOT NULL
|
|
1664
|
+
);
|
|
1665
|
+
CREATE INDEX history_by_instance ON history (instance_id, sequence);
|
|
1666
|
+
`;
|
|
1667
|
+
function nowIso() {
|
|
1668
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1669
|
+
}
|
|
1670
|
+
var DurableStore = class {
|
|
1671
|
+
db;
|
|
1672
|
+
transactionDepth = 0;
|
|
1673
|
+
pendingEffects = [];
|
|
1674
|
+
open = true;
|
|
1675
|
+
constructor(databasePath) {
|
|
1676
|
+
this.db = new DatabaseSync(databasePath, { timeout: 0 });
|
|
1677
|
+
try {
|
|
1678
|
+
this.db.exec("PRAGMA locking_mode = EXCLUSIVE;");
|
|
1679
|
+
this.db.exec("PRAGMA synchronous = FULL;");
|
|
1680
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
1681
|
+
this.db.exec("BEGIN EXCLUSIVE; COMMIT;");
|
|
1682
|
+
} catch (cause) {
|
|
1683
|
+
this.db.close();
|
|
1684
|
+
throw new DurableOwnershipError(
|
|
1685
|
+
`Another State Flow runtime owns ${databasePath}`,
|
|
1686
|
+
{ cause }
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
this.migrate();
|
|
1690
|
+
}
|
|
1691
|
+
migrate() {
|
|
1692
|
+
const { user_version: version } = this.db.prepare("PRAGMA user_version").get();
|
|
1693
|
+
if (version > SCHEMA_VERSION) {
|
|
1694
|
+
this.db.close();
|
|
1695
|
+
throw new DurableSchemaError(
|
|
1696
|
+
`Database schema version ${version} is newer than supported version ${SCHEMA_VERSION}`
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
if (version > 0 && version < SCHEMA_VERSION) {
|
|
1700
|
+
this.transaction(() => {
|
|
1701
|
+
for (let from = version; from < SCHEMA_VERSION; from += 1) {
|
|
1702
|
+
const migration = MIGRATIONS[from];
|
|
1703
|
+
if (migration === void 0) {
|
|
1704
|
+
throw new DurableSchemaError(
|
|
1705
|
+
`No migration from schema version ${from}`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
this.db.exec(migration);
|
|
1709
|
+
}
|
|
1710
|
+
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
if (version === 0) {
|
|
1714
|
+
this.transaction(() => {
|
|
1715
|
+
this.db.exec(SCHEMA);
|
|
1716
|
+
this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
get isOpen() {
|
|
1721
|
+
return this.open;
|
|
1722
|
+
}
|
|
1723
|
+
close() {
|
|
1724
|
+
if (this.open) {
|
|
1725
|
+
this.open = false;
|
|
1726
|
+
this.db.close();
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/** Runs `work` atomically. Nested calls join the outer transaction. */
|
|
1730
|
+
transaction(work) {
|
|
1731
|
+
if (this.transactionDepth > 0) {
|
|
1732
|
+
return work();
|
|
1733
|
+
}
|
|
1734
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1735
|
+
this.transactionDepth += 1;
|
|
1736
|
+
let committed = false;
|
|
1737
|
+
try {
|
|
1738
|
+
const result = work();
|
|
1739
|
+
this.db.exec("COMMIT");
|
|
1740
|
+
committed = true;
|
|
1741
|
+
return result;
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
this.db.exec("ROLLBACK");
|
|
1744
|
+
throw error;
|
|
1745
|
+
} finally {
|
|
1746
|
+
this.transactionDepth -= 1;
|
|
1747
|
+
const effects = this.pendingEffects.splice(0);
|
|
1748
|
+
if (committed) {
|
|
1749
|
+
for (const effect of effects) {
|
|
1750
|
+
effect();
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Runs `effect` once the current transaction commits (immediately outside one), and drops it
|
|
1757
|
+
* on rollback. Used for notifications and timers, which must reflect committed state only.
|
|
1758
|
+
*/
|
|
1759
|
+
afterCommit(effect) {
|
|
1760
|
+
if (this.transactionDepth > 0) {
|
|
1761
|
+
this.pendingEffects.push(effect);
|
|
1762
|
+
} else {
|
|
1763
|
+
effect();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
// --- instances -----------------------------------------------------------
|
|
1767
|
+
getInstance(instanceId2) {
|
|
1768
|
+
return this.db.prepare("SELECT * FROM instances WHERE instance_id = ?").get(instanceId2);
|
|
1769
|
+
}
|
|
1770
|
+
insertInstance(row) {
|
|
1771
|
+
const at = nowIso();
|
|
1772
|
+
this.db.prepare(
|
|
1773
|
+
`INSERT INTO instances (instance_id, chart_name, definition_id, state, status, status_reason,
|
|
1774
|
+
current_execution_id, revision, created_at, updated_at)
|
|
1775
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`
|
|
1776
|
+
).run(
|
|
1777
|
+
row.instance_id,
|
|
1778
|
+
row.chart_name,
|
|
1779
|
+
row.definition_id,
|
|
1780
|
+
row.state,
|
|
1781
|
+
row.status,
|
|
1782
|
+
row.status_reason,
|
|
1783
|
+
row.current_execution_id,
|
|
1784
|
+
at,
|
|
1785
|
+
at
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
updateInstance(instanceId2, changes) {
|
|
1789
|
+
this.update("instances", "instance_id", instanceId2, changes, {
|
|
1790
|
+
bumpRevision: true
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
// --- executions ----------------------------------------------------------
|
|
1794
|
+
getExecution(executionId) {
|
|
1795
|
+
return this.db.prepare("SELECT * FROM executions WHERE execution_id = ?").get(executionId);
|
|
1796
|
+
}
|
|
1797
|
+
findExecutionByRequest(instanceId2, requestId2) {
|
|
1798
|
+
return this.db.prepare(
|
|
1799
|
+
"SELECT * FROM executions WHERE instance_id = ? AND request_id = ?"
|
|
1800
|
+
).get(instanceId2, requestId2);
|
|
1801
|
+
}
|
|
1802
|
+
insertExecution(row) {
|
|
1803
|
+
const at = nowIso();
|
|
1804
|
+
this.db.prepare(
|
|
1805
|
+
`INSERT INTO executions (execution_id, instance_id, kind, request_id, input_json, from_state,
|
|
1806
|
+
transition, to_state, step, status, handler_revision, created_at, updated_at)
|
|
1807
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1808
|
+
).run(
|
|
1809
|
+
row.execution_id,
|
|
1810
|
+
row.instance_id,
|
|
1811
|
+
row.kind,
|
|
1812
|
+
row.request_id,
|
|
1813
|
+
row.input_json,
|
|
1814
|
+
row.from_state,
|
|
1815
|
+
row.transition,
|
|
1816
|
+
row.to_state,
|
|
1817
|
+
row.step,
|
|
1818
|
+
row.status,
|
|
1819
|
+
row.handler_revision,
|
|
1820
|
+
at,
|
|
1821
|
+
at
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
updateExecution(executionId, changes) {
|
|
1825
|
+
this.update("executions", "execution_id", executionId, changes);
|
|
1826
|
+
}
|
|
1827
|
+
// --- operations ----------------------------------------------------------
|
|
1828
|
+
getOperation(operationId) {
|
|
1829
|
+
return this.db.prepare("SELECT * FROM operations WHERE operation_id = ?").get(operationId);
|
|
1830
|
+
}
|
|
1831
|
+
findOperation(executionId, kind) {
|
|
1832
|
+
return this.db.prepare(
|
|
1833
|
+
"SELECT * FROM operations WHERE execution_id = ? AND callback_kind = ?"
|
|
1834
|
+
).get(executionId, kind);
|
|
1835
|
+
}
|
|
1836
|
+
/** The execution's operation that is not completed, if any. */
|
|
1837
|
+
findUnresolvedOperation(executionId) {
|
|
1838
|
+
return this.db.prepare(
|
|
1839
|
+
"SELECT * FROM operations WHERE execution_id = ? AND status != 'completed' ORDER BY created_at DESC LIMIT 1"
|
|
1840
|
+
).get(executionId);
|
|
1841
|
+
}
|
|
1842
|
+
insertOperation(row) {
|
|
1843
|
+
const at = nowIso();
|
|
1844
|
+
this.db.prepare(
|
|
1845
|
+
`INSERT INTO operations (operation_id, execution_id, instance_id, callback_kind, callback_target,
|
|
1846
|
+
strategy, status, attempts, attempt_limit, created_at, updated_at)
|
|
1847
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1848
|
+
).run(
|
|
1849
|
+
row.operation_id,
|
|
1850
|
+
row.execution_id,
|
|
1851
|
+
row.instance_id,
|
|
1852
|
+
row.callback_kind,
|
|
1853
|
+
row.callback_target,
|
|
1854
|
+
row.strategy,
|
|
1855
|
+
row.status,
|
|
1856
|
+
row.attempts,
|
|
1857
|
+
row.attempt_limit,
|
|
1858
|
+
at,
|
|
1859
|
+
at
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
updateOperation(operationId, changes) {
|
|
1863
|
+
this.update("operations", "operation_id", operationId, changes);
|
|
1864
|
+
}
|
|
1865
|
+
// --- attempts ------------------------------------------------------------
|
|
1866
|
+
insertAttempt(row) {
|
|
1867
|
+
this.db.prepare(
|
|
1868
|
+
"INSERT INTO attempts (attempt_id, operation_id, mode, number, started_at) VALUES (?, ?, ?, ?, ?)"
|
|
1869
|
+
).run(row.attempt_id, row.operation_id, row.mode, row.number, nowIso());
|
|
1870
|
+
}
|
|
1871
|
+
finishAttempt(attemptId, outcome, error) {
|
|
1872
|
+
this.db.prepare(
|
|
1873
|
+
"UPDATE attempts SET finished_at = ?, outcome = ?, error = ? WHERE attempt_id = ?"
|
|
1874
|
+
).run(nowIso(), outcome, error, attemptId);
|
|
1875
|
+
}
|
|
1876
|
+
/** Attempts left open by a process that stopped mid-callback. */
|
|
1877
|
+
finishInterruptedAttempts(operationId) {
|
|
1878
|
+
this.db.prepare(
|
|
1879
|
+
"UPDATE attempts SET finished_at = ?, outcome = 'interrupted' WHERE operation_id = ? AND finished_at IS NULL"
|
|
1880
|
+
).run(nowIso(), operationId);
|
|
1881
|
+
}
|
|
1882
|
+
// --- history -------------------------------------------------------------
|
|
1883
|
+
appendHistory(row) {
|
|
1884
|
+
this.db.prepare(
|
|
1885
|
+
`INSERT INTO history (instance_id, at, type, request_id, operation_id, attempt_id, data_json)
|
|
1886
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
1887
|
+
).run(
|
|
1888
|
+
row.instance_id,
|
|
1889
|
+
nowIso(),
|
|
1890
|
+
row.type,
|
|
1891
|
+
row.request_id,
|
|
1892
|
+
row.operation_id,
|
|
1893
|
+
row.attempt_id,
|
|
1894
|
+
row.data_json
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
/** One page of history in sequence order, after `afterSequence` (exclusive). */
|
|
1898
|
+
listHistory(instanceId2, page) {
|
|
1899
|
+
return this.db.prepare(
|
|
1900
|
+
"SELECT * FROM history WHERE instance_id = ? AND sequence > ? ORDER BY sequence ASC LIMIT ?"
|
|
1901
|
+
).all(instanceId2, page.afterSequence, page.limit);
|
|
1902
|
+
}
|
|
1903
|
+
/** One page of instances ordered by ID, after `afterInstanceId` (exclusive). */
|
|
1904
|
+
listInstances(query) {
|
|
1905
|
+
const where = [];
|
|
1906
|
+
const values = [];
|
|
1907
|
+
if (query.chartName !== null) {
|
|
1908
|
+
where.push("chart_name = ?");
|
|
1909
|
+
values.push(query.chartName);
|
|
1910
|
+
}
|
|
1911
|
+
if (query.statuses !== null) {
|
|
1912
|
+
if (query.statuses.length === 0) {
|
|
1913
|
+
return [];
|
|
1914
|
+
}
|
|
1915
|
+
where.push(`status IN (${query.statuses.map(() => "?").join(", ")})`);
|
|
1916
|
+
values.push(...query.statuses);
|
|
1917
|
+
}
|
|
1918
|
+
if (query.afterInstanceId !== null) {
|
|
1919
|
+
where.push("instance_id > ?");
|
|
1920
|
+
values.push(query.afterInstanceId);
|
|
1921
|
+
}
|
|
1922
|
+
const clause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
1923
|
+
return this.db.prepare(
|
|
1924
|
+
`SELECT * FROM instances ${clause} ORDER BY instance_id ASC LIMIT ?`
|
|
1925
|
+
).all(...values, query.limit);
|
|
1926
|
+
}
|
|
1927
|
+
// --- helpers -------------------------------------------------------------
|
|
1928
|
+
update(table, keyColumn, key, changes, options = {}) {
|
|
1929
|
+
const columns = Object.keys(changes).filter(
|
|
1930
|
+
(column) => changes[column] !== void 0
|
|
1931
|
+
);
|
|
1932
|
+
const assignments = columns.map((column) => `${column} = ?`);
|
|
1933
|
+
assignments.push("updated_at = ?");
|
|
1934
|
+
if (options.bumpRevision) {
|
|
1935
|
+
assignments.push("revision = revision + 1");
|
|
1936
|
+
}
|
|
1937
|
+
const values = columns.map((column) => changes[column] ?? null);
|
|
1938
|
+
this.db.prepare(
|
|
1939
|
+
`UPDATE ${table} SET ${assignments.join(", ")} WHERE ${keyColumn} = ?`
|
|
1940
|
+
).run(...values, nowIso(), key);
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
// src/durable/runtime.ts
|
|
1945
|
+
var runtimeCores = /* @__PURE__ */ new WeakMap();
|
|
1946
|
+
function createDurableRuntime(options) {
|
|
1947
|
+
const store = new DurableStore(options.databasePath);
|
|
1948
|
+
const core = new RuntimeCore(
|
|
1949
|
+
store,
|
|
1950
|
+
options.onObserverError ?? ((error) => console.error("State Flow observer failed", error)),
|
|
1951
|
+
options.onRecoveryError ?? ((error) => console.error("State Flow background recovery failed", error))
|
|
1952
|
+
);
|
|
1953
|
+
const flowsByChart = /* @__PURE__ */ new Map();
|
|
1954
|
+
function registerFlow(compiled, config, onFailure) {
|
|
1955
|
+
core.assertOpen();
|
|
1956
|
+
const definition = loadFlowDefinition(compiled);
|
|
1957
|
+
if (flowsByChart.has(definition.chartName))
|
|
1958
|
+
throw new DurableInputError(
|
|
1959
|
+
`Flow "${definition.chartName}" is already registered with this runtime`
|
|
1960
|
+
);
|
|
1961
|
+
const flow = { definition, config };
|
|
1962
|
+
const api = createFlowApi(core, flow, onFailure);
|
|
1963
|
+
flowsByChart.set(definition.chartName, flow);
|
|
1964
|
+
return api;
|
|
1965
|
+
}
|
|
1966
|
+
const runtime = {
|
|
1967
|
+
createFlow(compiled, config) {
|
|
1968
|
+
return {
|
|
1969
|
+
handleFailure(onFailure) {
|
|
1970
|
+
if (typeof onFailure !== "function")
|
|
1971
|
+
throw new DurableInputError("A flow failure handler is required");
|
|
1972
|
+
let pending;
|
|
1973
|
+
return {
|
|
1974
|
+
run() {
|
|
1975
|
+
pending ??= Promise.resolve().then(() => {
|
|
1976
|
+
try {
|
|
1977
|
+
return registerFlow(
|
|
1978
|
+
compiled,
|
|
1979
|
+
config,
|
|
1980
|
+
onFailure
|
|
1981
|
+
);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
return rejectFlowFailure(
|
|
1984
|
+
onFailure,
|
|
1985
|
+
new DurableOperationError("create-flow", error)
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
return pending;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
},
|
|
1995
|
+
flow(compiled, config) {
|
|
1996
|
+
return registerFlow(
|
|
1997
|
+
compiled,
|
|
1998
|
+
config
|
|
1999
|
+
);
|
|
2000
|
+
},
|
|
2001
|
+
close: (options2) => core.close(options2),
|
|
2002
|
+
inspector: {
|
|
2003
|
+
listInstances: (query) => core.listInstances(query),
|
|
2004
|
+
getSnapshot: (id) => {
|
|
2005
|
+
const valid = instanceId(id);
|
|
2006
|
+
return core.store.getInstance(valid) ? core.snapshotOf(valid) : null;
|
|
2007
|
+
},
|
|
2008
|
+
history: (id, query) => core.history(instanceId(id), query),
|
|
2009
|
+
subscribe(listener) {
|
|
2010
|
+
const wrapped = (snapshot) => listener(snapshot);
|
|
2011
|
+
core.runtimeListeners.add(wrapped);
|
|
2012
|
+
return () => {
|
|
2013
|
+
core.runtimeListeners.delete(wrapped);
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
},
|
|
2017
|
+
controls: {
|
|
2018
|
+
async resolve(id, untrustedInput) {
|
|
2019
|
+
core.assertOpen();
|
|
2020
|
+
const valid = instanceId(id);
|
|
2021
|
+
const input = parseResolveInput(untrustedInput);
|
|
2022
|
+
const row = core.store.getInstance(valid);
|
|
2023
|
+
if (!row) {
|
|
2024
|
+
return {
|
|
2025
|
+
outcome: "rejected",
|
|
2026
|
+
reason: "unavailable",
|
|
2027
|
+
message: `Instance ${valid} does not exist`
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
const flow = flowsByChart.get(row.chart_name);
|
|
2031
|
+
const loaded = flow ? loadInstance(core, flow, valid) : void 0;
|
|
2032
|
+
if (loaded?.kind === "opened") {
|
|
2033
|
+
return resolve(core, loaded.instance, input);
|
|
2034
|
+
}
|
|
2035
|
+
if (input.action === "terminate") {
|
|
2036
|
+
return terminateUnloadable(core, valid, input);
|
|
2037
|
+
}
|
|
2038
|
+
return {
|
|
2039
|
+
outcome: "rejected",
|
|
2040
|
+
reason: "unavailable",
|
|
2041
|
+
message: flow ? `Instance ${valid} cannot be loaded by the registered flow (${loaded?.kind}); only terminate is available` : `No flow is registered for chart "${row.chart_name}"; only terminate is available`
|
|
2042
|
+
};
|
|
2043
|
+
},
|
|
2044
|
+
restartRecovery(id) {
|
|
2045
|
+
const instance = core.instances.get(instanceId(id));
|
|
2046
|
+
return instance ? core.restartRecovery(instance) : false;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
runtimeCores.set(runtime, core);
|
|
2051
|
+
return runtime;
|
|
2052
|
+
}
|
|
2053
|
+
async function rejectFlowFailure(onFailure, failure) {
|
|
2054
|
+
await onFailure(failure);
|
|
2055
|
+
throw new DurableInputError(
|
|
2056
|
+
"The flow failure handler must throw or transfer control"
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
function createFlowApi(core, flow, onFailure) {
|
|
2060
|
+
const { definition } = flow;
|
|
2061
|
+
let dispatchFailure = onFailure ? (failure) => rejectFlowFailure(onFailure, failure) : void 0;
|
|
2062
|
+
const instanceApis = /* @__PURE__ */ new WeakMap();
|
|
2063
|
+
const apiFor = (instance) => {
|
|
2064
|
+
let api = instanceApis.get(instance);
|
|
2065
|
+
if (!api) {
|
|
2066
|
+
api = createInstanceApi(core, instance, () => dispatchFailure, onFailure);
|
|
2067
|
+
instanceApis.set(instance, api);
|
|
2068
|
+
}
|
|
2069
|
+
return api;
|
|
2070
|
+
};
|
|
2071
|
+
async function createInstance(input) {
|
|
2072
|
+
core.assertOpen();
|
|
2073
|
+
const id = instanceId(input.instanceId);
|
|
2074
|
+
if (input.startState !== void 0 && !definition.statesByName.has(input.startState)) {
|
|
2075
|
+
throw new DurableInputError(
|
|
2076
|
+
`Unknown start state ${JSON.stringify(input.startState)} for flow "${definition.chartName}"`
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
if (core.instances.has(id) || core.store.getInstance(id)) {
|
|
2080
|
+
return { kind: "already-exists", id };
|
|
2081
|
+
}
|
|
2082
|
+
const executionId = mintExecutionId();
|
|
2083
|
+
core.store.transaction(() => {
|
|
2084
|
+
core.store.insertInstance({
|
|
2085
|
+
instance_id: id,
|
|
2086
|
+
chart_name: definition.chartName,
|
|
2087
|
+
definition_id: definition.definitionId,
|
|
2088
|
+
state: input.startState ?? definition.initialState.name,
|
|
2089
|
+
status: "running",
|
|
2090
|
+
status_reason: null,
|
|
2091
|
+
current_execution_id: executionId
|
|
2092
|
+
});
|
|
2093
|
+
core.store.insertExecution({
|
|
2094
|
+
execution_id: executionId,
|
|
2095
|
+
instance_id: id,
|
|
2096
|
+
kind: "create",
|
|
2097
|
+
request_id: null,
|
|
2098
|
+
input_json: canonicalJson({
|
|
2099
|
+
startState: input.startState ?? null
|
|
2100
|
+
}),
|
|
2101
|
+
from_state: null,
|
|
2102
|
+
transition: null,
|
|
2103
|
+
to_state: null,
|
|
2104
|
+
step: "resolve-start-state",
|
|
2105
|
+
status: "running",
|
|
2106
|
+
handler_revision: definition.handlerRevision
|
|
2107
|
+
});
|
|
2108
|
+
core.record(id, "instance-created", {
|
|
2109
|
+
chartName: definition.chartName,
|
|
2110
|
+
definitionId: definition.definitionId,
|
|
2111
|
+
startState: input.startState ?? null
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
const instance = new InstanceCore(id, flow);
|
|
2115
|
+
core.instances.set(id, instance);
|
|
2116
|
+
await core.track(
|
|
2117
|
+
instance,
|
|
2118
|
+
executionId,
|
|
2119
|
+
() => core.advance(instance, executionId)
|
|
2120
|
+
);
|
|
2121
|
+
return { kind: "created", instance, executionId };
|
|
2122
|
+
}
|
|
2123
|
+
const flowApi = {
|
|
2124
|
+
chartName: definition.chartName,
|
|
2125
|
+
definitionId: definition.definitionId,
|
|
2126
|
+
ensureInstance(input) {
|
|
2127
|
+
const captured = { instanceId: input.instanceId };
|
|
2128
|
+
function prepare(override) {
|
|
2129
|
+
let pending;
|
|
2130
|
+
return {
|
|
2131
|
+
handleEnsureInstance(handlers) {
|
|
2132
|
+
assertHandlers(handlers, ["definition-mismatch"]);
|
|
2133
|
+
return prepare(handlers);
|
|
2134
|
+
},
|
|
2135
|
+
run() {
|
|
2136
|
+
pending ??= Promise.resolve().then(async () => {
|
|
2137
|
+
if (!onFailure)
|
|
2138
|
+
throw new DurableInputError(
|
|
2139
|
+
"Create this flow through createFlow(...).handleFailure(...).run() before using ensureInstance"
|
|
2140
|
+
);
|
|
2141
|
+
try {
|
|
2142
|
+
return await flowApi.handleEnsure({
|
|
2143
|
+
"definition-mismatch": (context) => {
|
|
2144
|
+
throw new DurableDefinitionMismatchError(
|
|
2145
|
+
context
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
}).ensure(captured);
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
if (error instanceof DurableDefinitionMismatchError && override) {
|
|
2151
|
+
await override["definition-mismatch"](error.context);
|
|
2152
|
+
throw new DurableInputError(
|
|
2153
|
+
"The definition-mismatch handler must throw or transfer control"
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
return rejectFlowFailure(
|
|
2157
|
+
onFailure,
|
|
2158
|
+
error instanceof DurableDefinitionMismatchError ? error : new DurableOperationError("ensure-instance", error)
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
return pending;
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
return prepare();
|
|
2167
|
+
},
|
|
2168
|
+
handleDispatch(onFailure2) {
|
|
2169
|
+
core.assertOpen();
|
|
2170
|
+
if (typeof onFailure2 !== "function")
|
|
2171
|
+
throw new DurableInputError("A dispatch failure handler is required");
|
|
2172
|
+
if (dispatchFailure)
|
|
2173
|
+
throw new DurableInputError(
|
|
2174
|
+
"This flow already has a dispatch failure handler"
|
|
2175
|
+
);
|
|
2176
|
+
dispatchFailure = onFailure2;
|
|
2177
|
+
return flowApi;
|
|
2178
|
+
},
|
|
2179
|
+
handleEnsure(handlers) {
|
|
2180
|
+
assertHandlers(handlers, ["definition-mismatch"]);
|
|
2181
|
+
return {
|
|
2182
|
+
async ensure(input) {
|
|
2183
|
+
core.assertOpen();
|
|
2184
|
+
const id = instanceId(input.instanceId);
|
|
2185
|
+
let loaded = loadInstance(core, flow, id);
|
|
2186
|
+
if (loaded.kind === "not-found") {
|
|
2187
|
+
const created = await createInstance({ instanceId: id });
|
|
2188
|
+
loaded = created.kind === "created" ? { kind: "opened", instance: created.instance } : loadInstance(core, flow, id);
|
|
2189
|
+
}
|
|
2190
|
+
if (loaded.kind === "mismatch") {
|
|
2191
|
+
await handlers["definition-mismatch"](loaded.context);
|
|
2192
|
+
throw new DurableInputError(
|
|
2193
|
+
"The definition-mismatch handler must throw or transfer control; ensure cannot return an incompatible instance"
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
if (loaded.kind === "not-found") {
|
|
2197
|
+
throw new DurableInputError(
|
|
2198
|
+
`Instance ${id} disappeared during ensure`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
return apiFor(loaded.instance);
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
},
|
|
2205
|
+
handleCreate(handlers) {
|
|
2206
|
+
assertHandlers(handlers, [
|
|
2207
|
+
"created",
|
|
2208
|
+
"already-exists",
|
|
2209
|
+
"recovery-pending",
|
|
2210
|
+
"needs-attention"
|
|
2211
|
+
]);
|
|
2212
|
+
return {
|
|
2213
|
+
async create(input) {
|
|
2214
|
+
const created = await createInstance(input);
|
|
2215
|
+
if (created.kind === "already-exists") {
|
|
2216
|
+
return handlers["already-exists"]({ instanceId: created.id });
|
|
2217
|
+
}
|
|
2218
|
+
const { instance, executionId } = created;
|
|
2219
|
+
const id = instance.id;
|
|
2220
|
+
const api = apiFor(instance);
|
|
2221
|
+
const snapshot = core.snapshot(instance);
|
|
2222
|
+
const execution = core.requireExecution(executionId);
|
|
2223
|
+
switch (execution.status) {
|
|
2224
|
+
case "recovering":
|
|
2225
|
+
return handlers["recovery-pending"]({
|
|
2226
|
+
instance: api,
|
|
2227
|
+
snapshot,
|
|
2228
|
+
work: requireWork(core, id)
|
|
2229
|
+
});
|
|
2230
|
+
case "needs-attention":
|
|
2231
|
+
return handlers["needs-attention"]({
|
|
2232
|
+
instance: api,
|
|
2233
|
+
snapshot,
|
|
2234
|
+
work: requireWork(core, id),
|
|
2235
|
+
reason: reasonOf(snapshot)
|
|
2236
|
+
});
|
|
2237
|
+
default:
|
|
2238
|
+
return handlers.created({ instance: api, snapshot });
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
},
|
|
2243
|
+
handleOpen(handlers) {
|
|
2244
|
+
assertHandlers(handlers, ["opened", "not-found", "definition-mismatch"]);
|
|
2245
|
+
return {
|
|
2246
|
+
async open(rawInstanceId) {
|
|
2247
|
+
core.assertOpen();
|
|
2248
|
+
const id = instanceId(rawInstanceId);
|
|
2249
|
+
const loaded = loadInstance(core, flow, id);
|
|
2250
|
+
switch (loaded.kind) {
|
|
2251
|
+
case "not-found":
|
|
2252
|
+
return handlers["not-found"]({ instanceId: id });
|
|
2253
|
+
case "mismatch":
|
|
2254
|
+
return handlers["definition-mismatch"](loaded.context);
|
|
2255
|
+
}
|
|
2256
|
+
const instance = loaded.instance;
|
|
2257
|
+
return handlers.opened({
|
|
2258
|
+
instance: apiFor(instance),
|
|
2259
|
+
snapshot: core.snapshot(instance)
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
return flowApi;
|
|
2266
|
+
}
|
|
2267
|
+
function loadInstance(core, flow, id) {
|
|
2268
|
+
const { definition } = flow;
|
|
2269
|
+
const row = core.store.getInstance(id);
|
|
2270
|
+
if (!row) {
|
|
2271
|
+
return { kind: "not-found" };
|
|
2272
|
+
}
|
|
2273
|
+
const cached = core.instances.get(id);
|
|
2274
|
+
if (row.chart_name !== definition.chartName || row.definition_id !== definition.definitionId || cached !== void 0 && cached.flow !== flow) {
|
|
2275
|
+
return {
|
|
2276
|
+
kind: "mismatch",
|
|
2277
|
+
context: {
|
|
2278
|
+
reason: "definition",
|
|
2279
|
+
instanceId: id,
|
|
2280
|
+
message: `Instance ${id} uses chart "${row.chart_name}" with definition ${row.definition_id}; this flow uses "${definition.chartName}" with definition ${definition.definitionId}`,
|
|
2281
|
+
storedChartName: row.chart_name,
|
|
2282
|
+
storedDefinitionId: row.definition_id,
|
|
2283
|
+
currentDefinitionId: definition.definitionId
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
const unresolved = row.current_execution_id === null ? void 0 : core.store.getExecution(row.current_execution_id);
|
|
2288
|
+
if (cached === void 0 && unresolved !== void 0 && unresolved.handler_revision !== definition.handlerRevision) {
|
|
2289
|
+
return {
|
|
2290
|
+
kind: "mismatch",
|
|
2291
|
+
context: {
|
|
2292
|
+
reason: "handler-revision",
|
|
2293
|
+
instanceId: id,
|
|
2294
|
+
message: `Instance ${id} has unresolved work from handler revision ${unresolved.handler_revision}; this build uses ${definition.handlerRevision}`,
|
|
2295
|
+
storedHandlerRevision: unresolved.handler_revision,
|
|
2296
|
+
currentHandlerRevision: definition.handlerRevision
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
if (cached) {
|
|
2301
|
+
if (cached.stalled) {
|
|
2302
|
+
core.startRecovery(cached);
|
|
2303
|
+
}
|
|
2304
|
+
return { kind: "opened", instance: cached };
|
|
2305
|
+
}
|
|
2306
|
+
const instance = new InstanceCore(id, flow);
|
|
2307
|
+
core.instances.set(id, instance);
|
|
2308
|
+
core.startRecovery(instance);
|
|
2309
|
+
return { kind: "opened", instance };
|
|
2310
|
+
}
|
|
2311
|
+
function createInstanceApi(core, instance, dispatchFailure, flowFailure) {
|
|
2312
|
+
const { definition } = instance.flow;
|
|
2313
|
+
const api = {
|
|
2314
|
+
instanceId: instance.id,
|
|
2315
|
+
getSnapshot: () => core.snapshot(instance),
|
|
2316
|
+
subscribe(listener) {
|
|
2317
|
+
const wrapped = (snapshot) => listener(snapshot);
|
|
2318
|
+
instance.listeners.add(wrapped);
|
|
2319
|
+
return () => {
|
|
2320
|
+
instance.listeners.delete(wrapped);
|
|
2321
|
+
};
|
|
2322
|
+
},
|
|
2323
|
+
history: (query) => core.history(instance.id, query),
|
|
2324
|
+
when(from) {
|
|
2325
|
+
const hasOutgoing = [...definition.transitionsByName.values()].some(
|
|
2326
|
+
(transition) => transition.from.name === from
|
|
2327
|
+
);
|
|
2328
|
+
if (!hasOutgoing) {
|
|
2329
|
+
throw new DurableInputError(
|
|
2330
|
+
`State ${JSON.stringify(from)} has no outgoing transitions in flow "${definition.chartName}"`
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
return {
|
|
2334
|
+
async dispatch(input) {
|
|
2335
|
+
const onFailure = dispatchFailure();
|
|
2336
|
+
if (!onFailure)
|
|
2337
|
+
throw new DurableInputError(
|
|
2338
|
+
"Configure flow.handleDispatch before direct dispatch, or use exhaustive handle callbacks"
|
|
2339
|
+
);
|
|
2340
|
+
const handlers = Object.fromEntries(
|
|
2341
|
+
DISPATCH_OUTCOMES.map((outcome) => [
|
|
2342
|
+
outcome,
|
|
2343
|
+
(context) => ({ outcome, context })
|
|
2344
|
+
])
|
|
2345
|
+
);
|
|
2346
|
+
let result;
|
|
2347
|
+
try {
|
|
2348
|
+
result = await dispatch(
|
|
2349
|
+
core,
|
|
2350
|
+
instance,
|
|
2351
|
+
from,
|
|
2352
|
+
handlers,
|
|
2353
|
+
input
|
|
2354
|
+
);
|
|
2355
|
+
} catch (error) {
|
|
2356
|
+
if (flowFailure)
|
|
2357
|
+
return rejectFlowFailure(
|
|
2358
|
+
flowFailure,
|
|
2359
|
+
new DurableOperationError("dispatch", error)
|
|
2360
|
+
);
|
|
2361
|
+
throw error;
|
|
2362
|
+
}
|
|
2363
|
+
if (result.outcome === "applied") return result.context;
|
|
2364
|
+
await onFailure(
|
|
2365
|
+
new DurableDispatchError(
|
|
2366
|
+
result.outcome,
|
|
2367
|
+
instance.id,
|
|
2368
|
+
result.context
|
|
2369
|
+
)
|
|
2370
|
+
);
|
|
2371
|
+
throw new DurableInputError(
|
|
2372
|
+
"The dispatch failure handler must throw or transfer control"
|
|
2373
|
+
);
|
|
2374
|
+
},
|
|
2375
|
+
handle(handlers) {
|
|
2376
|
+
assertHandlers(handlers, DISPATCH_OUTCOMES);
|
|
2377
|
+
return {
|
|
2378
|
+
dispatch: (input) => dispatch(core, instance, from, handlers, input)
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
},
|
|
2383
|
+
handleResolve(handlers) {
|
|
2384
|
+
assertHandlers(handlers, ["accepted", "rejected"]);
|
|
2385
|
+
return {
|
|
2386
|
+
resolve: async (input) => {
|
|
2387
|
+
const result = await resolve(core, instance, input);
|
|
2388
|
+
return handlers[result.outcome](result);
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
},
|
|
2392
|
+
handleRestart(handlers) {
|
|
2393
|
+
assertHandlers(handlers, ["restarted", "not-restarted"]);
|
|
2394
|
+
return {
|
|
2395
|
+
restart: () => handlers[core.restartRecovery(instance) ? "restarted" : "not-restarted"]({})
|
|
2396
|
+
};
|
|
2397
|
+
},
|
|
2398
|
+
resolve: (input) => resolve(core, instance, input),
|
|
2399
|
+
restartRecovery: () => core.restartRecovery(instance)
|
|
2400
|
+
};
|
|
2401
|
+
return api;
|
|
2402
|
+
}
|
|
2403
|
+
var DISPATCH_OUTCOMES = [
|
|
2404
|
+
"applied",
|
|
2405
|
+
"blocked",
|
|
2406
|
+
"invalid-payload",
|
|
2407
|
+
"busy",
|
|
2408
|
+
"recovery-pending",
|
|
2409
|
+
"needs-attention",
|
|
2410
|
+
"state-mismatch",
|
|
2411
|
+
"request-conflict",
|
|
2412
|
+
"terminated"
|
|
2413
|
+
];
|
|
2414
|
+
function assertHandlers(handlers, expected) {
|
|
2415
|
+
const missing = expected.filter((key) => typeof handlers[key] !== "function");
|
|
2416
|
+
const extra = Object.keys(handlers).filter((key) => !expected.includes(key));
|
|
2417
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
2418
|
+
throw new DurableInputError(
|
|
2419
|
+
`Outcome handlers must be exactly ${expected.join(", ")}` + (missing.length ? `; missing ${missing.join(", ")}` : "") + (extra.length ? `; unexpected ${extra.join(", ")}` : "")
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
function requireWork(core, id) {
|
|
2424
|
+
const work = core.currentWork(id);
|
|
2425
|
+
if (!work) {
|
|
2426
|
+
throw new Error(`Instance ${id} has no unresolved work`);
|
|
2427
|
+
}
|
|
2428
|
+
return work;
|
|
2429
|
+
}
|
|
2430
|
+
function reasonOf(snapshot) {
|
|
2431
|
+
return snapshot.status.kind === "needs-attention" || snapshot.status.kind === "terminated" ? snapshot.status.reason : "";
|
|
2432
|
+
}
|
|
2433
|
+
function recordedDispatch(core, instance, handlers, input, requestId2, inputJson) {
|
|
2434
|
+
const recorded = core.store.findExecutionByRequest(instance.id, requestId2);
|
|
2435
|
+
if (!recorded) return { found: false };
|
|
2436
|
+
if (recorded.input_json !== inputJson) {
|
|
2437
|
+
const original = parseJson(recorded.input_json);
|
|
2438
|
+
return {
|
|
2439
|
+
found: true,
|
|
2440
|
+
value: handlers["request-conflict"]({ input, recorded: original })
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
if (instance.activeExecutionId === recorded.execution_id)
|
|
2444
|
+
return {
|
|
2445
|
+
found: true,
|
|
2446
|
+
value: handlers.busy({ input, snapshot: core.snapshot(instance) })
|
|
2447
|
+
};
|
|
2448
|
+
return {
|
|
2449
|
+
found: true,
|
|
2450
|
+
value: selectRecorded(core, instance, handlers, input, recorded, true)
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async function dispatch(core, instance, from, handlers, input) {
|
|
2454
|
+
core.assertOpen();
|
|
2455
|
+
const { definition } = instance.flow;
|
|
2456
|
+
const requestId2 = requestId(input.requestId);
|
|
2457
|
+
const transition = definition.transitionsByName.get(input.transition);
|
|
2458
|
+
if (!transition || transition.from.name !== from) {
|
|
2459
|
+
throw new DurableInputError(
|
|
2460
|
+
`Transition ${JSON.stringify(input.transition)} does not leave "${from}" in flow "${definition.chartName}"`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
const payload = input.payload === void 0 ? null : input.payload;
|
|
2464
|
+
const problem = findNonJsonValue(payload);
|
|
2465
|
+
if (problem) {
|
|
2466
|
+
return handlers["invalid-payload"]({
|
|
2467
|
+
input,
|
|
2468
|
+
issues: [
|
|
2469
|
+
{ path: [], message: `Payload is not JSON-compatible: ${problem}` }
|
|
2470
|
+
]
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
const payloadJson = canonicalJson(payload);
|
|
2474
|
+
input = Object.freeze({
|
|
2475
|
+
requestId: requestId2,
|
|
2476
|
+
transition: transition.name,
|
|
2477
|
+
payload: parseJson(payloadJson)
|
|
2478
|
+
});
|
|
2479
|
+
const inputJson = canonicalJson({
|
|
2480
|
+
fromState: from,
|
|
2481
|
+
transition: transition.name,
|
|
2482
|
+
payload: input.payload
|
|
2483
|
+
});
|
|
2484
|
+
const previous = recordedDispatch(
|
|
2485
|
+
core,
|
|
2486
|
+
instance,
|
|
2487
|
+
handlers,
|
|
2488
|
+
input,
|
|
2489
|
+
requestId2,
|
|
2490
|
+
inputJson
|
|
2491
|
+
);
|
|
2492
|
+
if (previous.found) return previous.value;
|
|
2493
|
+
const parsed = await transition.payloadSchema.safeParseAsync(
|
|
2494
|
+
parseJson(payloadJson)
|
|
2495
|
+
);
|
|
2496
|
+
core.assertOpen();
|
|
2497
|
+
const concurrent = recordedDispatch(
|
|
2498
|
+
core,
|
|
2499
|
+
instance,
|
|
2500
|
+
handlers,
|
|
2501
|
+
input,
|
|
2502
|
+
requestId2,
|
|
2503
|
+
inputJson
|
|
2504
|
+
);
|
|
2505
|
+
if (concurrent.found) return concurrent.value;
|
|
2506
|
+
if (!parsed.success) {
|
|
2507
|
+
return handlers["invalid-payload"]({
|
|
2508
|
+
input,
|
|
2509
|
+
issues: parsed.error.issues.map(({ path, message }) => ({
|
|
2510
|
+
path,
|
|
2511
|
+
message
|
|
2512
|
+
}))
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
const parsedProblem = findNonJsonValue(parsed.data);
|
|
2516
|
+
if (parsedProblem || canonicalJson(parsed.data) !== payloadJson) {
|
|
2517
|
+
return handlers["invalid-payload"]({
|
|
2518
|
+
input,
|
|
2519
|
+
issues: [
|
|
2520
|
+
{
|
|
2521
|
+
path: [],
|
|
2522
|
+
message: "Payload schemas must preserve the JSON input; coercion, defaults, transforms and stripped fields are not supported"
|
|
2523
|
+
}
|
|
2524
|
+
]
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
if (instance.activeExecutionId !== null) {
|
|
2528
|
+
return handlers.busy({ input, snapshot: core.snapshot(instance) });
|
|
2529
|
+
}
|
|
2530
|
+
const row = core.requireInstanceRow(instance.id);
|
|
2531
|
+
const snapshot = core.snapshot(instance);
|
|
2532
|
+
switch (row.status) {
|
|
2533
|
+
case "terminated":
|
|
2534
|
+
return handlers.terminated({
|
|
2535
|
+
input,
|
|
2536
|
+
snapshot,
|
|
2537
|
+
reason: row.status_reason ?? ""
|
|
2538
|
+
});
|
|
2539
|
+
case "needs-attention":
|
|
2540
|
+
return handlers["needs-attention"]({
|
|
2541
|
+
input,
|
|
2542
|
+
snapshot,
|
|
2543
|
+
work: requireWork(core, instance.id),
|
|
2544
|
+
reason: row.status_reason ?? ""
|
|
2545
|
+
});
|
|
2546
|
+
case "recovering":
|
|
2547
|
+
return handlers["recovery-pending"]({
|
|
2548
|
+
input,
|
|
2549
|
+
snapshot,
|
|
2550
|
+
work: requireWork(core, instance.id)
|
|
2551
|
+
});
|
|
2552
|
+
case "running":
|
|
2553
|
+
return handlers.busy({ input, snapshot });
|
|
2554
|
+
case "ready":
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2557
|
+
if (row.state !== from) {
|
|
2558
|
+
return handlers["state-mismatch"]({
|
|
2559
|
+
input,
|
|
2560
|
+
expectedState: from,
|
|
2561
|
+
actualState: row.state,
|
|
2562
|
+
snapshot
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
const executionId = mintExecutionId();
|
|
2566
|
+
core.store.transaction(() => {
|
|
2567
|
+
core.store.insertExecution({
|
|
2568
|
+
execution_id: executionId,
|
|
2569
|
+
instance_id: instance.id,
|
|
2570
|
+
kind: "dispatch",
|
|
2571
|
+
request_id: requestId2,
|
|
2572
|
+
input_json: inputJson,
|
|
2573
|
+
from_state: from,
|
|
2574
|
+
transition: transition.name,
|
|
2575
|
+
to_state: transition.to.name,
|
|
2576
|
+
step: "guard",
|
|
2577
|
+
status: "running",
|
|
2578
|
+
handler_revision: definition.handlerRevision
|
|
2579
|
+
});
|
|
2580
|
+
core.store.updateInstance(instance.id, {
|
|
2581
|
+
status: "running",
|
|
2582
|
+
current_execution_id: executionId
|
|
2583
|
+
});
|
|
2584
|
+
core.record(
|
|
2585
|
+
instance.id,
|
|
2586
|
+
"execution-started",
|
|
2587
|
+
{ transition: transition.name, from, to: transition.to.name },
|
|
2588
|
+
{ requestId: requestId2 }
|
|
2589
|
+
);
|
|
2590
|
+
});
|
|
2591
|
+
core.notify(instance);
|
|
2592
|
+
await core.track(
|
|
2593
|
+
instance,
|
|
2594
|
+
executionId,
|
|
2595
|
+
() => core.advance(instance, executionId)
|
|
2596
|
+
);
|
|
2597
|
+
return selectRecorded(
|
|
2598
|
+
core,
|
|
2599
|
+
instance,
|
|
2600
|
+
handlers,
|
|
2601
|
+
input,
|
|
2602
|
+
core.requireExecution(executionId),
|
|
2603
|
+
false
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
function selectRecorded(core, instance, handlers, input, execution, duplicate) {
|
|
2607
|
+
const snapshot = core.snapshot(instance);
|
|
2608
|
+
switch (execution.status) {
|
|
2609
|
+
case "applied":
|
|
2610
|
+
return handlers.applied({ input, snapshot, duplicate });
|
|
2611
|
+
case "blocked":
|
|
2612
|
+
return handlers.blocked({ input, snapshot, duplicate });
|
|
2613
|
+
case "terminated":
|
|
2614
|
+
return handlers.terminated({
|
|
2615
|
+
input,
|
|
2616
|
+
snapshot,
|
|
2617
|
+
reason: reasonOf(snapshot)
|
|
2618
|
+
});
|
|
2619
|
+
case "needs-attention": {
|
|
2620
|
+
const work = core.currentWork(instance.id);
|
|
2621
|
+
if (work) {
|
|
2622
|
+
return handlers["needs-attention"]({
|
|
2623
|
+
input,
|
|
2624
|
+
snapshot,
|
|
2625
|
+
work,
|
|
2626
|
+
reason: reasonOf(snapshot)
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
return handlers.busy({ input, snapshot });
|
|
2630
|
+
}
|
|
2631
|
+
case "recovering":
|
|
2632
|
+
case "running": {
|
|
2633
|
+
const work = core.currentWork(instance.id);
|
|
2634
|
+
if (work && snapshot.status.kind === "recovering") {
|
|
2635
|
+
return handlers["recovery-pending"]({ input, snapshot, work });
|
|
2636
|
+
}
|
|
2637
|
+
return handlers.busy({ input, snapshot });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
async function resolve(core, instance, untrustedInput) {
|
|
2642
|
+
core.assertOpen();
|
|
2643
|
+
const input = parseResolveInput(untrustedInput);
|
|
2644
|
+
const row = core.requireInstanceRow(instance.id);
|
|
2645
|
+
const execution = row.current_execution_id === null ? void 0 : core.store.getExecution(row.current_execution_id);
|
|
2646
|
+
const operation = execution ? core.store.findUnresolvedOperation(execution.execution_id) : void 0;
|
|
2647
|
+
if (instance.activeExecutionId !== null || row.status !== "needs-attention" || !execution || !operation || operation.status !== "needs-attention" || operation.operation_id !== input.operationId || operation.attempts !== input.expectedAttempts) {
|
|
2648
|
+
return staleRejection(instance.id, input, operation);
|
|
2649
|
+
}
|
|
2650
|
+
const refs = {
|
|
2651
|
+
requestId: execution.request_id,
|
|
2652
|
+
operationId: operation.operation_id
|
|
2653
|
+
};
|
|
2654
|
+
const audit = (data) => core.record(
|
|
2655
|
+
instance.id,
|
|
2656
|
+
"manual-resolution",
|
|
2657
|
+
{ action: input.action, reason: input.reason, ...data },
|
|
2658
|
+
refs
|
|
2659
|
+
);
|
|
2660
|
+
switch (input.action) {
|
|
2661
|
+
case "terminate":
|
|
2662
|
+
terminate(core, instance.id, execution, operation, input.reason, audit);
|
|
2663
|
+
core.notify(instance);
|
|
2664
|
+
return { outcome: "accepted", snapshot: core.snapshot(instance) };
|
|
2665
|
+
case "retry": {
|
|
2666
|
+
await core.manualRetry(instance, execution, operation, () => audit({}));
|
|
2667
|
+
return { outcome: "accepted", snapshot: core.snapshot(instance) };
|
|
2668
|
+
}
|
|
2669
|
+
case "confirm-completed": {
|
|
2670
|
+
if (input.callback !== operation.callback_kind) {
|
|
2671
|
+
return {
|
|
2672
|
+
outcome: "rejected",
|
|
2673
|
+
reason: "callback-mismatch",
|
|
2674
|
+
message: `Operation ${operation.operation_id} is ${operation.callback_kind}, not ${input.callback}`
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
const supplied = "result" in input ? input.result : void 0;
|
|
2678
|
+
const validated = validateConfirmedResult(
|
|
2679
|
+
instance.flow.definition,
|
|
2680
|
+
operation.callback_kind,
|
|
2681
|
+
supplied
|
|
2682
|
+
);
|
|
2683
|
+
if (!validated.ok) {
|
|
2684
|
+
return {
|
|
2685
|
+
outcome: "rejected",
|
|
2686
|
+
reason: "invalid-result",
|
|
2687
|
+
message: validated.message
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
core.store.transaction(() => {
|
|
2691
|
+
core.checkpoint(instance, execution, operation, validated.result);
|
|
2692
|
+
audit({ result: validated.result });
|
|
2693
|
+
});
|
|
2694
|
+
core.notify(instance);
|
|
2695
|
+
await core.track(
|
|
2696
|
+
instance,
|
|
2697
|
+
execution.execution_id,
|
|
2698
|
+
() => core.advance(instance, execution.execution_id)
|
|
2699
|
+
);
|
|
2700
|
+
return { outcome: "accepted", snapshot: core.snapshot(instance) };
|
|
2701
|
+
}
|
|
2702
|
+
default:
|
|
2703
|
+
throw new DurableInputError(
|
|
2704
|
+
`Unknown resolve action ${JSON.stringify(input.action)}`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
function staleRejection(instanceId2, input, operation) {
|
|
2709
|
+
const detail = operation && operation.operation_id === input.operationId ? ` (it has had ${operation.attempts} attempt(s); the action expected ${input.expectedAttempts})` : "";
|
|
2710
|
+
return {
|
|
2711
|
+
outcome: "rejected",
|
|
2712
|
+
reason: "stale",
|
|
2713
|
+
message: `Operation ${input.operationId} is not the work awaiting attention on instance ${instanceId2}${detail}`
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function terminate(core, instanceId2, execution, operation, reason, audit) {
|
|
2717
|
+
const refs = {
|
|
2718
|
+
requestId: execution.request_id,
|
|
2719
|
+
operationId: operation.operation_id
|
|
2720
|
+
};
|
|
2721
|
+
core.store.transaction(() => {
|
|
2722
|
+
core.store.updateOperation(operation.operation_id, {
|
|
2723
|
+
status: "terminated"
|
|
2724
|
+
});
|
|
2725
|
+
core.store.updateExecution(execution.execution_id, {
|
|
2726
|
+
status: "terminated"
|
|
2727
|
+
});
|
|
2728
|
+
core.store.updateInstance(instanceId2, {
|
|
2729
|
+
status: "terminated",
|
|
2730
|
+
status_reason: reason
|
|
2731
|
+
});
|
|
2732
|
+
audit({});
|
|
2733
|
+
core.record(instanceId2, "instance-terminated", { reason }, refs);
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
function terminateUnloadable(core, instanceId2, input) {
|
|
2737
|
+
const row = core.requireInstanceRow(instanceId2);
|
|
2738
|
+
const cached = core.instances.get(instanceId2);
|
|
2739
|
+
const execution = row.current_execution_id === null ? void 0 : core.store.getExecution(row.current_execution_id);
|
|
2740
|
+
const operation = execution ? core.store.findUnresolvedOperation(execution.execution_id) : void 0;
|
|
2741
|
+
if (cached !== void 0 && cached.activeExecutionId !== null || row.status === "ready" || row.status === "terminated" || !execution || !operation || operation.operation_id !== input.operationId || operation.attempts !== input.expectedAttempts) {
|
|
2742
|
+
return staleRejection(instanceId2, input, operation);
|
|
2743
|
+
}
|
|
2744
|
+
if (cached) {
|
|
2745
|
+
core.clearTimer(cached);
|
|
2746
|
+
}
|
|
2747
|
+
terminate(
|
|
2748
|
+
core,
|
|
2749
|
+
instanceId2,
|
|
2750
|
+
execution,
|
|
2751
|
+
operation,
|
|
2752
|
+
input.reason,
|
|
2753
|
+
(data) => core.record(
|
|
2754
|
+
instanceId2,
|
|
2755
|
+
"manual-resolution",
|
|
2756
|
+
{ action: "terminate", reason: input.reason, unloadable: true, ...data },
|
|
2757
|
+
{ requestId: execution.request_id, operationId: operation.operation_id }
|
|
2758
|
+
)
|
|
2759
|
+
);
|
|
2760
|
+
if (cached) {
|
|
2761
|
+
core.notify(cached);
|
|
2762
|
+
}
|
|
2763
|
+
return { outcome: "accepted", snapshot: core.snapshotOf(instanceId2) };
|
|
2764
|
+
}
|
|
2765
|
+
function validateConfirmedResult(definition, kind, result) {
|
|
2766
|
+
if ((kind === "on-enter" || kind === "on-exit" || kind === "on-transition") && result !== void 0 && result !== null) {
|
|
2767
|
+
return { ok: false, message: `${kind} does not produce a result` };
|
|
2768
|
+
}
|
|
2769
|
+
return validateCallbackResult(definition, kind, result);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/durable/create-runtime.ts
|
|
2773
|
+
function createRuntime(options) {
|
|
2774
|
+
const { shutdownTimeoutMs, ...runtimeOptions } = options;
|
|
2775
|
+
const runtime = createDurableRuntime(runtimeOptions);
|
|
2776
|
+
const close = runtime.close;
|
|
2777
|
+
runtime.close = (options2) => close({ timeoutMs: options2?.timeoutMs ?? shutdownTimeoutMs });
|
|
2778
|
+
return Object.assign(runtime, {
|
|
2779
|
+
[Symbol.asyncDispose]: () => runtime.close()
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// src/durable/scoped-runtime.ts
|
|
2784
|
+
async function withDurableRuntime(options, run) {
|
|
2785
|
+
const runtime = createRuntime(options);
|
|
2786
|
+
let work;
|
|
2787
|
+
try {
|
|
2788
|
+
work = { ok: true, value: await run(runtime) };
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
work = { ok: false, error };
|
|
2791
|
+
}
|
|
2792
|
+
try {
|
|
2793
|
+
await runtime.close();
|
|
2794
|
+
} catch (error) {
|
|
2795
|
+
if (!work.ok)
|
|
2796
|
+
throw new AggregateError(
|
|
2797
|
+
[work.error, error],
|
|
2798
|
+
"Application work and State Flow shutdown both failed"
|
|
2799
|
+
);
|
|
2800
|
+
throw error;
|
|
2801
|
+
}
|
|
2802
|
+
if (!work.ok) throw work.error;
|
|
2803
|
+
return work.value;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// src/durable/recovery-target.ts
|
|
2807
|
+
function recoveryTarget(work) {
|
|
2808
|
+
const target2 = {
|
|
2809
|
+
operationId: work.operationId,
|
|
2810
|
+
expectedAttempts: work.attempts
|
|
2811
|
+
};
|
|
2812
|
+
const base = {
|
|
2813
|
+
retry: (reason) => ({ ...target2, action: "retry", reason }),
|
|
2814
|
+
terminate: (reason) => ({ ...target2, action: "terminate", reason })
|
|
2815
|
+
};
|
|
2816
|
+
switch (work.callback.kind) {
|
|
2817
|
+
case "guard":
|
|
2818
|
+
return Object.freeze({
|
|
2819
|
+
...base,
|
|
2820
|
+
callback: "guard",
|
|
2821
|
+
confirm: (result, reason) => ({
|
|
2822
|
+
...target2,
|
|
2823
|
+
action: "confirm-completed",
|
|
2824
|
+
callback: "guard",
|
|
2825
|
+
result,
|
|
2826
|
+
reason
|
|
2827
|
+
})
|
|
2828
|
+
});
|
|
2829
|
+
case "resolve-start-state":
|
|
2830
|
+
return Object.freeze({
|
|
2831
|
+
...base,
|
|
2832
|
+
callback: "resolve-start-state",
|
|
2833
|
+
confirm: (result, reason) => ({
|
|
2834
|
+
...target2,
|
|
2835
|
+
action: "confirm-completed",
|
|
2836
|
+
callback: "resolve-start-state",
|
|
2837
|
+
result,
|
|
2838
|
+
reason
|
|
2839
|
+
})
|
|
2840
|
+
});
|
|
2841
|
+
default: {
|
|
2842
|
+
const callback = work.callback.kind;
|
|
2843
|
+
return Object.freeze({
|
|
2844
|
+
...base,
|
|
2845
|
+
callback,
|
|
2846
|
+
confirm: (reason) => ({
|
|
2847
|
+
...target2,
|
|
2848
|
+
action: "confirm-completed",
|
|
2849
|
+
callback,
|
|
2850
|
+
reason
|
|
2851
|
+
})
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
export {
|
|
2857
|
+
DurableDefinitionError,
|
|
2858
|
+
DurableDefinitionMismatchError,
|
|
2859
|
+
DurableDispatchError,
|
|
2860
|
+
DurableInputError,
|
|
2861
|
+
DurableOperationError,
|
|
2862
|
+
DurableOwnershipError,
|
|
2863
|
+
DurableRuntimeClosedError,
|
|
2864
|
+
DurableRuntimeError,
|
|
2865
|
+
DurableSchemaError,
|
|
2866
|
+
DurableShutdownTimeoutError,
|
|
2867
|
+
MAX_ATTEMPTS,
|
|
2868
|
+
RECOVERY_DELAYS_MS,
|
|
2869
|
+
createDurableRuntime,
|
|
2870
|
+
createRuntime,
|
|
2871
|
+
instanceId,
|
|
2872
|
+
parseResolveInput,
|
|
2873
|
+
recoveryTarget,
|
|
2874
|
+
requestId,
|
|
2875
|
+
resolveInputSchema,
|
|
2876
|
+
withDurableRuntime
|
|
2877
|
+
};
|