@dudousxd/nestjs-durable-core 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +525 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -27
- package/dist/index.d.ts +218 -27
- package/dist/index.js +521 -184
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -156,6 +156,218 @@ var InMemoryAdmissionBackend = class {
|
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
+
// src/ambient-ctx.ts
|
|
160
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
161
|
+
var STORAGE_KEY = Symbol.for("nestjs-durable:ambient-workflow-ctx");
|
|
162
|
+
var globalRef = globalThis;
|
|
163
|
+
if (!globalRef[STORAGE_KEY]) {
|
|
164
|
+
globalRef[STORAGE_KEY] = new AsyncLocalStorage();
|
|
165
|
+
}
|
|
166
|
+
var storage = globalRef[STORAGE_KEY];
|
|
167
|
+
function runInWorkflowCtx(ctx, fn) {
|
|
168
|
+
return storage.run(ctx, fn);
|
|
169
|
+
}
|
|
170
|
+
__name(runInWorkflowCtx, "runInWorkflowCtx");
|
|
171
|
+
function currentWorkflowCtx() {
|
|
172
|
+
return storage.getStore();
|
|
173
|
+
}
|
|
174
|
+
__name(currentWorkflowCtx, "currentWorkflowCtx");
|
|
175
|
+
|
|
176
|
+
// src/errors.ts
|
|
177
|
+
var FatalError = class extends Error {
|
|
178
|
+
static {
|
|
179
|
+
__name(this, "FatalError");
|
|
180
|
+
}
|
|
181
|
+
code;
|
|
182
|
+
constructor(message, code) {
|
|
183
|
+
super(message);
|
|
184
|
+
this.name = "FatalError";
|
|
185
|
+
this.code = code;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var SingletonQueueFullError = class extends Error {
|
|
189
|
+
static {
|
|
190
|
+
__name(this, "SingletonQueueFullError");
|
|
191
|
+
}
|
|
192
|
+
workflow;
|
|
193
|
+
key;
|
|
194
|
+
maxQueueDepth;
|
|
195
|
+
constructor(workflow, key, maxQueueDepth) {
|
|
196
|
+
super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
|
|
197
|
+
this.name = "SingletonQueueFullError";
|
|
198
|
+
this.workflow = workflow;
|
|
199
|
+
this.key = key;
|
|
200
|
+
this.maxQueueDepth = maxQueueDepth;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var SignalTimeoutError = class extends Error {
|
|
204
|
+
static {
|
|
205
|
+
__name(this, "SignalTimeoutError");
|
|
206
|
+
}
|
|
207
|
+
token;
|
|
208
|
+
timeoutMs;
|
|
209
|
+
constructor(token, timeoutMs) {
|
|
210
|
+
super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
|
|
211
|
+
this.name = "SignalTimeoutError";
|
|
212
|
+
this.token = token;
|
|
213
|
+
this.timeoutMs = timeoutMs;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var RemoteStepTimeout = class extends Error {
|
|
217
|
+
static {
|
|
218
|
+
__name(this, "RemoteStepTimeout");
|
|
219
|
+
}
|
|
220
|
+
stepId;
|
|
221
|
+
timeoutMs;
|
|
222
|
+
constructor(stepId2, timeoutMs) {
|
|
223
|
+
super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
|
|
224
|
+
this.name = "RemoteStepTimeout";
|
|
225
|
+
this.stepId = stepId2;
|
|
226
|
+
this.timeoutMs = timeoutMs;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var RemoteWorkflowTimeout = class extends Error {
|
|
230
|
+
static {
|
|
231
|
+
__name(this, "RemoteWorkflowTimeout");
|
|
232
|
+
}
|
|
233
|
+
taskId;
|
|
234
|
+
timeoutMs;
|
|
235
|
+
constructor(taskId, timeoutMs) {
|
|
236
|
+
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
237
|
+
this.name = "RemoteWorkflowTimeout";
|
|
238
|
+
this.taskId = taskId;
|
|
239
|
+
this.timeoutMs = timeoutMs;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var NonDeterminismError = class extends Error {
|
|
243
|
+
static {
|
|
244
|
+
__name(this, "NonDeterminismError");
|
|
245
|
+
}
|
|
246
|
+
runId;
|
|
247
|
+
seq;
|
|
248
|
+
constructor(runId, seq, expected, recorded) {
|
|
249
|
+
super(`non-determinism at ${runId}#${seq}: code expects "${expected}" but history recorded "${recorded}". The workflow changed under an in-flight run \u2014 register a new @Workflow version.`);
|
|
250
|
+
this.name = "NonDeterminismError";
|
|
251
|
+
this.runId = runId;
|
|
252
|
+
this.seq = seq;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var GatherError = class extends Error {
|
|
256
|
+
static {
|
|
257
|
+
__name(this, "GatherError");
|
|
258
|
+
}
|
|
259
|
+
failures;
|
|
260
|
+
constructor(failures, total) {
|
|
261
|
+
const ids = failures.map((f) => f.id).join(", ");
|
|
262
|
+
const denom = total ?? failures.length;
|
|
263
|
+
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
264
|
+
this.name = "GatherError";
|
|
265
|
+
this.failures = failures;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
var WorkflowSuspended = class extends Error {
|
|
269
|
+
static {
|
|
270
|
+
__name(this, "WorkflowSuspended");
|
|
271
|
+
}
|
|
272
|
+
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
273
|
+
wakeAt;
|
|
274
|
+
constructor(wakeAt) {
|
|
275
|
+
super("workflow suspended");
|
|
276
|
+
this.name = "WorkflowSuspended";
|
|
277
|
+
this.wakeAt = wakeAt;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var ContinueAsNew = class extends Error {
|
|
281
|
+
static {
|
|
282
|
+
__name(this, "ContinueAsNew");
|
|
283
|
+
}
|
|
284
|
+
input;
|
|
285
|
+
constructor(input) {
|
|
286
|
+
super("workflow continued as new"), this.input = input;
|
|
287
|
+
this.name = "ContinueAsNew";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/workflow-ref.ts
|
|
292
|
+
var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
|
|
293
|
+
function workflowName(ref) {
|
|
294
|
+
if (typeof ref === "string") return ref;
|
|
295
|
+
const name = ref[WORKFLOW_NAME_KEY];
|
|
296
|
+
if (!name) {
|
|
297
|
+
throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
|
|
298
|
+
}
|
|
299
|
+
return name;
|
|
300
|
+
}
|
|
301
|
+
__name(workflowName, "workflowName");
|
|
302
|
+
|
|
303
|
+
// src/durable-workflow.ts
|
|
304
|
+
var BINDINGS_KEY = Symbol.for("nestjs-durable:workflow-class-bindings");
|
|
305
|
+
var globalRef2 = globalThis;
|
|
306
|
+
if (!globalRef2[BINDINGS_KEY]) {
|
|
307
|
+
globalRef2[BINDINGS_KEY] = /* @__PURE__ */ new WeakMap();
|
|
308
|
+
}
|
|
309
|
+
var bindings = globalRef2[BINDINGS_KEY];
|
|
310
|
+
function bindWorkflowClass(ctor, client) {
|
|
311
|
+
bindings.set(ctor, client);
|
|
312
|
+
}
|
|
313
|
+
__name(bindWorkflowClass, "bindWorkflowClass");
|
|
314
|
+
function clientOf(ctor) {
|
|
315
|
+
const client = bindings.get(ctor);
|
|
316
|
+
if (!client) {
|
|
317
|
+
throw new Error(`${workflowName(ctor)} is not bound to a durable engine \u2014 the class-first statics only work after the module that registers this workflow has booted (or, in a store-less tenant worker, use the injected WorkflowService/engine facade instead).`);
|
|
318
|
+
}
|
|
319
|
+
return client;
|
|
320
|
+
}
|
|
321
|
+
__name(clientOf, "clientOf");
|
|
322
|
+
var DurableWorkflow = class {
|
|
323
|
+
static {
|
|
324
|
+
__name(this, "DurableWorkflow");
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Fire-and-forget start — `engine.start` outside a workflow, `ctx.startChild` (parent-linked)
|
|
328
|
+
* inside one. `opts.id` pins the run id (outside) / child id (inside) for idempotent starts.
|
|
329
|
+
*/
|
|
330
|
+
static async start(input, opts) {
|
|
331
|
+
const cls = this;
|
|
332
|
+
const ctx = currentWorkflowCtx();
|
|
333
|
+
if (ctx) {
|
|
334
|
+
const runId2 = await ctx.startChild(cls, input, opts?.id);
|
|
335
|
+
return {
|
|
336
|
+
runId: runId2,
|
|
337
|
+
status: "pending"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const runId = opts?.id ?? globalThis.crypto.randomUUID();
|
|
341
|
+
return clientOf(cls).start(workflowName(cls), input, runId, opts);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Run-and-await the typed output — `ctx.child` inside a workflow (suspends the parent, zero
|
|
345
|
+
* compute); outside, starts the run and waits until it settles terminally. Throws `FatalError`
|
|
346
|
+
* when the run does not complete. `opts.timeoutMs` bounds the OUTSIDE wait only (inside, a
|
|
347
|
+
* suspended parent costs nothing — bound the child itself with `executionTimeout` instead).
|
|
348
|
+
*/
|
|
349
|
+
static async execute(input, opts) {
|
|
350
|
+
const cls = this;
|
|
351
|
+
const ctx = currentWorkflowCtx();
|
|
352
|
+
if (ctx) return ctx.child(cls, input, opts?.id);
|
|
353
|
+
const client = clientOf(cls);
|
|
354
|
+
if (!client.waitForRun) {
|
|
355
|
+
throw new FatalError(`${workflowName(cls)}.execute() outside a workflow needs a store-backed engine to await the result \u2014 this class is bound to a store-less start facade (tenant worker). Use ${workflowName(cls)}.start() and observe the run instead.`, "execute_unsupported");
|
|
356
|
+
}
|
|
357
|
+
const runId = opts?.id ?? globalThis.crypto.randomUUID();
|
|
358
|
+
await client.start(workflowName(cls), input, runId, opts);
|
|
359
|
+
const settled = await client.waitForRun(runId, {
|
|
360
|
+
...opts?.timeoutMs != null ? {
|
|
361
|
+
timeoutMs: opts.timeoutMs
|
|
362
|
+
} : {},
|
|
363
|
+
until: "terminal"
|
|
364
|
+
});
|
|
365
|
+
if (settled.status === "completed") return settled.output;
|
|
366
|
+
const detail = settled.error ? `: ${settled.error.message}` : "";
|
|
367
|
+
throw new FatalError(`${workflowName(cls)} run ${runId} settled ${settled.status}${detail}`, settled.error?.code ?? "execute_failed");
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
159
371
|
// src/duration.ts
|
|
160
372
|
var UNIT_MS = {
|
|
161
373
|
ms: 1,
|
|
@@ -412,121 +624,6 @@ function instantCheckpoint(p) {
|
|
|
412
624
|
}
|
|
413
625
|
__name(instantCheckpoint, "instantCheckpoint");
|
|
414
626
|
|
|
415
|
-
// src/errors.ts
|
|
416
|
-
var FatalError = class extends Error {
|
|
417
|
-
static {
|
|
418
|
-
__name(this, "FatalError");
|
|
419
|
-
}
|
|
420
|
-
code;
|
|
421
|
-
constructor(message, code) {
|
|
422
|
-
super(message);
|
|
423
|
-
this.name = "FatalError";
|
|
424
|
-
this.code = code;
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
var SingletonQueueFullError = class extends Error {
|
|
428
|
-
static {
|
|
429
|
-
__name(this, "SingletonQueueFullError");
|
|
430
|
-
}
|
|
431
|
-
workflow;
|
|
432
|
-
key;
|
|
433
|
-
maxQueueDepth;
|
|
434
|
-
constructor(workflow, key, maxQueueDepth) {
|
|
435
|
-
super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
|
|
436
|
-
this.name = "SingletonQueueFullError";
|
|
437
|
-
this.workflow = workflow;
|
|
438
|
-
this.key = key;
|
|
439
|
-
this.maxQueueDepth = maxQueueDepth;
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
var SignalTimeoutError = class extends Error {
|
|
443
|
-
static {
|
|
444
|
-
__name(this, "SignalTimeoutError");
|
|
445
|
-
}
|
|
446
|
-
token;
|
|
447
|
-
timeoutMs;
|
|
448
|
-
constructor(token, timeoutMs) {
|
|
449
|
-
super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
|
|
450
|
-
this.name = "SignalTimeoutError";
|
|
451
|
-
this.token = token;
|
|
452
|
-
this.timeoutMs = timeoutMs;
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
var RemoteStepTimeout = class extends Error {
|
|
456
|
-
static {
|
|
457
|
-
__name(this, "RemoteStepTimeout");
|
|
458
|
-
}
|
|
459
|
-
stepId;
|
|
460
|
-
timeoutMs;
|
|
461
|
-
constructor(stepId2, timeoutMs) {
|
|
462
|
-
super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
|
|
463
|
-
this.name = "RemoteStepTimeout";
|
|
464
|
-
this.stepId = stepId2;
|
|
465
|
-
this.timeoutMs = timeoutMs;
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
var RemoteWorkflowTimeout = class extends Error {
|
|
469
|
-
static {
|
|
470
|
-
__name(this, "RemoteWorkflowTimeout");
|
|
471
|
-
}
|
|
472
|
-
taskId;
|
|
473
|
-
timeoutMs;
|
|
474
|
-
constructor(taskId, timeoutMs) {
|
|
475
|
-
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
476
|
-
this.name = "RemoteWorkflowTimeout";
|
|
477
|
-
this.taskId = taskId;
|
|
478
|
-
this.timeoutMs = timeoutMs;
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
var NonDeterminismError = class extends Error {
|
|
482
|
-
static {
|
|
483
|
-
__name(this, "NonDeterminismError");
|
|
484
|
-
}
|
|
485
|
-
runId;
|
|
486
|
-
seq;
|
|
487
|
-
constructor(runId, seq, expected, recorded) {
|
|
488
|
-
super(`non-determinism at ${runId}#${seq}: code expects "${expected}" but history recorded "${recorded}". The workflow changed under an in-flight run \u2014 register a new @Workflow version.`);
|
|
489
|
-
this.name = "NonDeterminismError";
|
|
490
|
-
this.runId = runId;
|
|
491
|
-
this.seq = seq;
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
var GatherError = class extends Error {
|
|
495
|
-
static {
|
|
496
|
-
__name(this, "GatherError");
|
|
497
|
-
}
|
|
498
|
-
failures;
|
|
499
|
-
constructor(failures, total) {
|
|
500
|
-
const ids = failures.map((f) => f.id).join(", ");
|
|
501
|
-
const denom = total ?? failures.length;
|
|
502
|
-
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
503
|
-
this.name = "GatherError";
|
|
504
|
-
this.failures = failures;
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
var WorkflowSuspended = class extends Error {
|
|
508
|
-
static {
|
|
509
|
-
__name(this, "WorkflowSuspended");
|
|
510
|
-
}
|
|
511
|
-
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
512
|
-
wakeAt;
|
|
513
|
-
constructor(wakeAt) {
|
|
514
|
-
super("workflow suspended");
|
|
515
|
-
this.name = "WorkflowSuspended";
|
|
516
|
-
this.wakeAt = wakeAt;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
var ContinueAsNew = class extends Error {
|
|
520
|
-
static {
|
|
521
|
-
__name(this, "ContinueAsNew");
|
|
522
|
-
}
|
|
523
|
-
input;
|
|
524
|
-
constructor(input) {
|
|
525
|
-
super("workflow continued as new"), this.input = input;
|
|
526
|
-
this.name = "ContinueAsNew";
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
627
|
// src/entities.ts
|
|
531
628
|
var ENTITY_WORKFLOW = "__entity";
|
|
532
629
|
var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
|
|
@@ -988,18 +1085,6 @@ function stepConfigOf(ref) {
|
|
|
988
1085
|
}
|
|
989
1086
|
__name(stepConfigOf, "stepConfigOf");
|
|
990
1087
|
|
|
991
|
-
// src/workflow-ref.ts
|
|
992
|
-
var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
|
|
993
|
-
function workflowName(ref) {
|
|
994
|
-
if (typeof ref === "string") return ref;
|
|
995
|
-
const name = ref[WORKFLOW_NAME_KEY];
|
|
996
|
-
if (!name) {
|
|
997
|
-
throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
|
|
998
|
-
}
|
|
999
|
-
return name;
|
|
1000
|
-
}
|
|
1001
|
-
__name(workflowName, "workflowName");
|
|
1002
|
-
|
|
1003
1088
|
// src/workflow-ctx.ts
|
|
1004
1089
|
function normalizeChildOptions(options) {
|
|
1005
1090
|
return typeof options === "string" ? {
|
|
@@ -1185,6 +1270,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1185
1270
|
value: buffered.payload
|
|
1186
1271
|
};
|
|
1187
1272
|
}, "consumeBuffered");
|
|
1273
|
+
const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
|
|
1274
|
+
const recorded = await readCheckpoint(deadlineSeq);
|
|
1275
|
+
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1276
|
+
if (!recorded) {
|
|
1277
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1278
|
+
runId,
|
|
1279
|
+
seq: deadlineSeq,
|
|
1280
|
+
name: `timeout:${label}`,
|
|
1281
|
+
kind: "sleep",
|
|
1282
|
+
wakeAt: deadline
|
|
1283
|
+
}));
|
|
1284
|
+
}
|
|
1285
|
+
return deadline;
|
|
1286
|
+
}, "stampDeadline");
|
|
1188
1287
|
const waitForSignal = /* @__PURE__ */ __name(async (token, opts) => {
|
|
1189
1288
|
if (opts?.timeoutMs == null) {
|
|
1190
1289
|
const current = pos.next();
|
|
@@ -1202,17 +1301,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1202
1301
|
const timeoutMs = opts.timeoutMs;
|
|
1203
1302
|
const deadlineSeq = pos.next();
|
|
1204
1303
|
const waitSeq = pos.next();
|
|
1205
|
-
const
|
|
1206
|
-
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1207
|
-
if (!recorded) {
|
|
1208
|
-
await writeCheckpoint(instantCheckpoint({
|
|
1209
|
-
runId,
|
|
1210
|
-
seq: deadlineSeq,
|
|
1211
|
-
name: `timeout:${token}`,
|
|
1212
|
-
kind: "sleep",
|
|
1213
|
-
wakeAt: deadline
|
|
1214
|
-
}));
|
|
1215
|
-
}
|
|
1304
|
+
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
1216
1305
|
const waited = await readCheckpoint(waitSeq);
|
|
1217
1306
|
if (waited && waited.status === "completed") return waited.output;
|
|
1218
1307
|
const buffered = await consumeBuffered(token, waitSeq);
|
|
@@ -1245,17 +1334,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1245
1334
|
const deadlineSeq = pos.next();
|
|
1246
1335
|
const waitSeq = pos.next();
|
|
1247
1336
|
const token = eventToken(name, opts.match, runId, waitSeq);
|
|
1248
|
-
const
|
|
1249
|
-
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1250
|
-
if (!recorded) {
|
|
1251
|
-
await writeCheckpoint(instantCheckpoint({
|
|
1252
|
-
runId,
|
|
1253
|
-
seq: deadlineSeq,
|
|
1254
|
-
name: `timeout:event:${name}`,
|
|
1255
|
-
kind: "sleep",
|
|
1256
|
-
wakeAt: deadline
|
|
1257
|
-
}));
|
|
1258
|
-
}
|
|
1337
|
+
const deadline = await stampDeadline(deadlineSeq, `event:${name}`, timeoutMs);
|
|
1259
1338
|
const waited = await readCheckpoint(waitSeq);
|
|
1260
1339
|
if (waited && waited.status === "completed") return waited.output;
|
|
1261
1340
|
if (host.clock() >= deadline) {
|
|
@@ -1311,6 +1390,9 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1311
1390
|
const cp = existing[i];
|
|
1312
1391
|
const c = cp?.status === "completed" ? cp.output : null;
|
|
1313
1392
|
if (c && c.ok === false) {
|
|
1393
|
+
for (let j = 0; j < inputs.length; j += 1) {
|
|
1394
|
+
if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
|
|
1395
|
+
}
|
|
1314
1396
|
throw new GatherError([
|
|
1315
1397
|
{
|
|
1316
1398
|
index: i,
|
|
@@ -1467,15 +1549,30 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1467
1549
|
const webhook = /* @__PURE__ */ __name(() => {
|
|
1468
1550
|
const current = pos.next();
|
|
1469
1551
|
const token = `wh:${runId}:${current}`;
|
|
1470
|
-
const wait = /* @__PURE__ */ __name(async () => {
|
|
1552
|
+
const wait = /* @__PURE__ */ __name(async (opts) => {
|
|
1553
|
+
const timeoutMs = opts?.timeoutMs;
|
|
1554
|
+
const deadlineSeq = timeoutMs == null ? -1 : pos.next();
|
|
1471
1555
|
const existing = await readCheckpoint(current);
|
|
1472
1556
|
if (existing && existing.status === "completed") return existing.output;
|
|
1557
|
+
if (timeoutMs == null) {
|
|
1558
|
+
await store.putSignalWaiter({
|
|
1559
|
+
token,
|
|
1560
|
+
runId,
|
|
1561
|
+
seq: current
|
|
1562
|
+
});
|
|
1563
|
+
throw new WorkflowSuspended();
|
|
1564
|
+
}
|
|
1565
|
+
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
1566
|
+
if (host.clock() >= deadline) {
|
|
1567
|
+
await store.takeSignalWaiter(token).catch(() => void 0);
|
|
1568
|
+
throw new SignalTimeoutError(token, timeoutMs);
|
|
1569
|
+
}
|
|
1473
1570
|
await store.putSignalWaiter({
|
|
1474
1571
|
token,
|
|
1475
1572
|
runId,
|
|
1476
1573
|
seq: current
|
|
1477
1574
|
});
|
|
1478
|
-
throw new WorkflowSuspended();
|
|
1575
|
+
throw new WorkflowSuspended(deadline);
|
|
1479
1576
|
}, "wait");
|
|
1480
1577
|
return {
|
|
1481
1578
|
token,
|
|
@@ -1503,6 +1600,18 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1503
1600
|
output: attrs
|
|
1504
1601
|
}));
|
|
1505
1602
|
}, "upsertSearchAttributes");
|
|
1603
|
+
function resolveCompensateDef(compensate) {
|
|
1604
|
+
const name = typeof compensate === "string" ? compensate : stepNameOf(compensate);
|
|
1605
|
+
if (name === void 0) {
|
|
1606
|
+
throw new Error("ctx.step: compensate handler is not a @Step reference (no stamped step name) \u2014 pass a @Step-decorated method or a step name string");
|
|
1607
|
+
}
|
|
1608
|
+
const config = typeof compensate === "string" ? void 0 : stepConfigOf(compensate);
|
|
1609
|
+
return {
|
|
1610
|
+
name,
|
|
1611
|
+
...resolveStepPolicy(config, void 0)
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
__name(resolveCompensateDef, "resolveCompensateDef");
|
|
1506
1615
|
function step(handlerOrName, input, opts) {
|
|
1507
1616
|
const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
|
|
1508
1617
|
if (name === void 0) {
|
|
@@ -1516,6 +1625,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1516
1625
|
return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
|
|
1517
1626
|
priority: opts?.priority,
|
|
1518
1627
|
fairnessKey: opts?.fairnessKey
|
|
1628
|
+
}).then((output) => {
|
|
1629
|
+
if (opts?.compensate) {
|
|
1630
|
+
compensations.push({
|
|
1631
|
+
name,
|
|
1632
|
+
dispatch: {
|
|
1633
|
+
def: resolveCompensateDef(opts.compensate),
|
|
1634
|
+
args: {
|
|
1635
|
+
input,
|
|
1636
|
+
output
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
return output;
|
|
1519
1642
|
});
|
|
1520
1643
|
}
|
|
1521
1644
|
__name(step, "step");
|
|
@@ -2008,7 +2131,7 @@ var WorkflowEngine = class {
|
|
|
2008
2131
|
workflow: name,
|
|
2009
2132
|
workflowVersion: "1",
|
|
2010
2133
|
status: "pending",
|
|
2011
|
-
// Stamp the namespace the REAL run will carry (
|
|
2134
|
+
// Stamp the namespace the REAL run will carry (below) so convention routing resolves the same
|
|
2012
2135
|
// tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
|
|
2013
2136
|
// `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
|
|
2014
2137
|
// not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
|
|
@@ -2409,7 +2532,13 @@ var WorkflowEngine = class {
|
|
|
2409
2532
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
2410
2533
|
*/
|
|
2411
2534
|
waitForRun(runId, opts) {
|
|
2412
|
-
const
|
|
2535
|
+
const TERMINAL = /* @__PURE__ */ new Set([
|
|
2536
|
+
"completed",
|
|
2537
|
+
"failed",
|
|
2538
|
+
"cancelled",
|
|
2539
|
+
"dead"
|
|
2540
|
+
]);
|
|
2541
|
+
const isSettled = /* @__PURE__ */ __name((run) => opts?.until === "terminal" ? TERMINAL.has(run.status) : run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
|
|
2413
2542
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
2414
2543
|
runId,
|
|
2415
2544
|
status: run.status,
|
|
@@ -2423,6 +2552,7 @@ var WorkflowEngine = class {
|
|
|
2423
2552
|
const timer = opts?.timeoutMs != null ? setTimeout(() => {
|
|
2424
2553
|
if (done) return;
|
|
2425
2554
|
done = true;
|
|
2555
|
+
if (poller) clearInterval(poller);
|
|
2426
2556
|
off();
|
|
2427
2557
|
reject(new Error(`waitForRun(${runId}) timed out after ${opts.timeoutMs}ms`));
|
|
2428
2558
|
}, opts.timeoutMs) : void 0;
|
|
@@ -2430,6 +2560,7 @@ var WorkflowEngine = class {
|
|
|
2430
2560
|
if (done) return;
|
|
2431
2561
|
done = true;
|
|
2432
2562
|
if (timer) clearTimeout(timer);
|
|
2563
|
+
if (poller) clearInterval(poller);
|
|
2433
2564
|
off();
|
|
2434
2565
|
resolve(toResult(run));
|
|
2435
2566
|
}, "finish");
|
|
@@ -2438,6 +2569,11 @@ var WorkflowEngine = class {
|
|
|
2438
2569
|
if (run && isSettled(run)) finish(run);
|
|
2439
2570
|
});
|
|
2440
2571
|
}, "check");
|
|
2572
|
+
const poller = opts?.until === "terminal" ? (() => {
|
|
2573
|
+
const interval = setInterval(check, 250);
|
|
2574
|
+
interval.unref?.();
|
|
2575
|
+
return interval;
|
|
2576
|
+
})() : void 0;
|
|
2441
2577
|
off = this.subscribe((ev) => {
|
|
2442
2578
|
if (ev.runId === runId && (ev.type === "run.completed" || ev.type === "run.failed" || ev.type === "run.suspended")) {
|
|
2443
2579
|
check();
|
|
@@ -3347,7 +3483,9 @@ var WorkflowEngine = class {
|
|
|
3347
3483
|
parallelGroup: cmd.parallelGroup
|
|
3348
3484
|
});
|
|
3349
3485
|
if (!await this.store.getRun(childId)) {
|
|
3350
|
-
queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId
|
|
3486
|
+
queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId, {
|
|
3487
|
+
namespace: run.namespace
|
|
3488
|
+
}).catch(() => void 0));
|
|
3351
3489
|
}
|
|
3352
3490
|
} else {
|
|
3353
3491
|
throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
|
|
@@ -3359,14 +3497,24 @@ var WorkflowEngine = class {
|
|
|
3359
3497
|
* Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
|
|
3360
3498
|
* exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
|
|
3361
3499
|
* suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
|
|
3362
|
-
* stop at a suspension point.
|
|
3500
|
+
* stop at a suspension point.
|
|
3501
|
+
*
|
|
3502
|
+
* A dispatched compensation still in flight (or backing off a retry) needs the run to keep waiting:
|
|
3503
|
+
* unlike the failure path (which is fine going `suspended` — a resumable state no different from an
|
|
3504
|
+
* ordinary in-flight `ctx.step`), THIS run must stay `cancelling` — `engine.cancel`'s idempotent
|
|
3505
|
+
* re-check, `waitForRun`'s non-terminal treatment, and crash recovery (`recoverIncomplete` only
|
|
3506
|
+
* re-drives `running`/`cancelling` runs — a `cancelling` one with a free lease, whatever it's
|
|
3507
|
+
* internally waiting on) all key off that status. So on a pending unwind this returns WITHOUT
|
|
3508
|
+
* touching `status` (already `cancelling`, untouched since `engine.cancel` set it) — the in-memory
|
|
3509
|
+
* cancel flag stays too, re-derived from the status on any later re-drive regardless.
|
|
3363
3510
|
*/
|
|
3364
|
-
async compensateAndCancel(run, compensations) {
|
|
3511
|
+
async compensateAndCancel(run, compensations, replay) {
|
|
3512
|
+
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
3513
|
+
if (pending) return {
|
|
3514
|
+
runId: run.id,
|
|
3515
|
+
status: "cancelling"
|
|
3516
|
+
};
|
|
3365
3517
|
this.cancelRequested.delete(run.id);
|
|
3366
|
-
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
3367
|
-
const comp = compensations[i];
|
|
3368
|
-
if (comp) await this.runCompensation(run, comp);
|
|
3369
|
-
}
|
|
3370
3518
|
const error = {
|
|
3371
3519
|
message: "cancelled"
|
|
3372
3520
|
};
|
|
@@ -3427,17 +3575,21 @@ var WorkflowEngine = class {
|
|
|
3427
3575
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
3428
3576
|
const replay = /* @__PURE__ */ new Map();
|
|
3429
3577
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
3430
|
-
const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
|
|
3578
|
+
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
|
|
3431
3579
|
try {
|
|
3432
|
-
const output = await fn(ctx, run.input);
|
|
3433
|
-
if (this.cancelRequested.has(run.id))
|
|
3580
|
+
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
3581
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3582
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3583
|
+
}
|
|
3434
3584
|
return this.settleRun(run, {
|
|
3435
3585
|
kind: "completed",
|
|
3436
3586
|
output
|
|
3437
3587
|
});
|
|
3438
3588
|
} catch (err) {
|
|
3439
3589
|
if (err instanceof ContinueAsNew) {
|
|
3440
|
-
if (this.cancelRequested.has(run.id))
|
|
3590
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3591
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3592
|
+
}
|
|
3441
3593
|
await this.store.updateRun(run.id, {
|
|
3442
3594
|
status: "completed",
|
|
3443
3595
|
output: void 0,
|
|
@@ -3462,22 +3614,26 @@ var WorkflowEngine = class {
|
|
|
3462
3614
|
};
|
|
3463
3615
|
}
|
|
3464
3616
|
if (err instanceof WorkflowSuspended) {
|
|
3465
|
-
if (this.cancelRequested.has(run.id))
|
|
3617
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3618
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3619
|
+
}
|
|
3466
3620
|
return this.settleRun(run, {
|
|
3467
3621
|
kind: "suspended",
|
|
3468
3622
|
wakeAt: err.wakeAt
|
|
3469
3623
|
});
|
|
3470
3624
|
}
|
|
3471
|
-
if (this.cancelRequested.has(run.id))
|
|
3625
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3626
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3627
|
+
}
|
|
3472
3628
|
const error = {
|
|
3473
3629
|
message: err instanceof Error ? err.message : String(err),
|
|
3474
3630
|
stack: err instanceof Error ? err.stack : void 0
|
|
3475
3631
|
};
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
}
|
|
3632
|
+
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
3633
|
+
if (pending) return this.settleRun(run, {
|
|
3634
|
+
kind: "suspended",
|
|
3635
|
+
wakeAt: pending.wakeAt
|
|
3636
|
+
});
|
|
3481
3637
|
return this.settleRun(run, {
|
|
3482
3638
|
kind: "failed",
|
|
3483
3639
|
error
|
|
@@ -3487,14 +3643,62 @@ var WorkflowEngine = class {
|
|
|
3487
3643
|
}
|
|
3488
3644
|
}
|
|
3489
3645
|
/**
|
|
3490
|
-
*
|
|
3491
|
-
*
|
|
3492
|
-
*
|
|
3646
|
+
* Undo every registered saga compensation in strict reverse order — the shared unwind both the
|
|
3647
|
+
* run-failure path (`runExecution`'s catch) and the compensating-cancel path
|
|
3648
|
+
* (`compensateAndCancel`) drive. Every entry, LOCAL or DISPATCHED, gets a NEGATIVE checkpoint seq
|
|
3649
|
+
* `-(k + 1)` for its position `k` (0, 1, 2… walking the stack in reverse) — reserved space that can
|
|
3650
|
+
* never collide with a body position (those start at 0), so a saga's undo trail composes onto the
|
|
3651
|
+
* run's ordinary checkpoint history with no store-schema change, and shows up in run detail exactly
|
|
3652
|
+
* like any other step. Deterministic because `compensations` is rebuilt identically on every replay
|
|
3653
|
+
* (see `workflow-ctx.ts`), so the same comp always lands on the same negative seq across a crash +
|
|
3654
|
+
* re-drive — the crash-safety this whole scheme rests on.
|
|
3655
|
+
*
|
|
3656
|
+
* A LOCAL entry runs in-process (`runCompensation`), which checkpoints its own outcome — completed,
|
|
3657
|
+
* or failed once `compensationRetries` are exhausted — so a crash mid-unwind SKIPS an
|
|
3658
|
+
* already-resolved one on the next pass instead of re-running it (and re-spending its retry budget)
|
|
3659
|
+
* from attempt 1. A DISPATCHED entry is an ordinary remote-step dispatch under the hood
|
|
3660
|
+
* (`dispatchCompensation`): when it needs the run to WAIT — freshly dispatched, still in flight, or
|
|
3661
|
+
* backing off a durable retry — this function returns that wait signal immediately (skipping every
|
|
3662
|
+
* comp after it in this pass); the CALLER decides how to represent "still unwinding" (see
|
|
3663
|
+
* `compensateAndCancel`'s doc comment for why that differs between the two callers). Either way, the
|
|
3664
|
+
* run resumes later exactly like any other dispatched step: `completeRemoteResult` completes the
|
|
3665
|
+
* negative-seq checkpoint and re-drives, the body replays and deterministically re-throws the same
|
|
3666
|
+
* exit it took before (the original failure, or the suspension `compensateAndCancel` resumed from),
|
|
3667
|
+
* this function runs again, the now-resolved comp is skipped, and the unwind continues from there.
|
|
3668
|
+
*
|
|
3669
|
+
* Returns `undefined` once every compensation has resolved — the caller then finalizes the run
|
|
3670
|
+
* (`failed` with the ORIGINAL body error, never masked by a comp outcome; or `cancelled`).
|
|
3671
|
+
*/
|
|
3672
|
+
async unwindCompensations(run, compensations, replay) {
|
|
3673
|
+
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
3674
|
+
const comp = compensations[i];
|
|
3675
|
+
if (!comp) continue;
|
|
3676
|
+
const seq = -(compensations.length - i);
|
|
3677
|
+
if ("fn" in comp) {
|
|
3678
|
+
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
|
|
3679
|
+
if (existing) continue;
|
|
3680
|
+
await this.runCompensation(run, comp, seq, replay);
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
const pending = await this.dispatchCompensation(run, comp, seq, replay);
|
|
3684
|
+
if (pending) return pending;
|
|
3685
|
+
}
|
|
3686
|
+
return void 0;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Run one LOCAL saga compensation, retried up to `compensationRetries`, emitting a
|
|
3690
|
+
* `compensate:<step>` step event for its outcome so a stranded undo is visible. Never throws — a
|
|
3691
|
+
* permanently-failing compensation is skipped so it can't mask the original failure. Checkpoints
|
|
3692
|
+
* its outcome at `seq` (a negative unwind position reserved by {@link unwindCompensations}) so a
|
|
3693
|
+
* crash mid-unwind skips an already-resolved undo on a later pass instead of re-running it (and
|
|
3694
|
+
* re-spending its retry budget) from attempt 1 — the checkpoint is durability/visibility only; the
|
|
3695
|
+
* runtime retry-in-process-back-to-back semantics of `comp.fn()` are unchanged.
|
|
3493
3696
|
*/
|
|
3494
|
-
async runCompensation(run, comp) {
|
|
3697
|
+
async runCompensation(run, comp, seq, replay) {
|
|
3495
3698
|
const name = `compensate:${comp.name}`;
|
|
3699
|
+
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
3496
3700
|
for (let attempt = 1; attempt <= this.compensationRetries; attempt += 1) {
|
|
3497
|
-
const startedAt = Date
|
|
3701
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
3498
3702
|
try {
|
|
3499
3703
|
await comp.fn();
|
|
3500
3704
|
this.emit({
|
|
@@ -3503,30 +3707,147 @@ var WorkflowEngine = class {
|
|
|
3503
3707
|
workflow: run.workflow,
|
|
3504
3708
|
name,
|
|
3505
3709
|
kind: "local",
|
|
3506
|
-
durationMs: Date.now() - startedAt
|
|
3710
|
+
durationMs: Date.now() - startedAt.getTime()
|
|
3507
3711
|
});
|
|
3712
|
+
const cp = stepCheckpoint({
|
|
3713
|
+
runId: run.id,
|
|
3714
|
+
seq,
|
|
3715
|
+
name,
|
|
3716
|
+
kind: "local",
|
|
3717
|
+
status: "completed",
|
|
3718
|
+
attempts: attempt,
|
|
3719
|
+
enqueuedAt,
|
|
3720
|
+
startedAt,
|
|
3721
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
3722
|
+
});
|
|
3723
|
+
await this.store.saveCheckpoint(cp);
|
|
3724
|
+
replay?.set(seq, cp);
|
|
3508
3725
|
return;
|
|
3509
3726
|
} catch (err) {
|
|
3510
3727
|
if (attempt >= this.compensationRetries) {
|
|
3728
|
+
const error = {
|
|
3729
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3730
|
+
};
|
|
3511
3731
|
this.emit({
|
|
3512
3732
|
type: "step.failed",
|
|
3513
3733
|
runId: run.id,
|
|
3514
3734
|
workflow: run.workflow,
|
|
3515
3735
|
name,
|
|
3516
3736
|
kind: "local",
|
|
3517
|
-
error
|
|
3518
|
-
|
|
3519
|
-
},
|
|
3520
|
-
durationMs: Date.now() - startedAt
|
|
3737
|
+
error,
|
|
3738
|
+
durationMs: Date.now() - startedAt.getTime()
|
|
3521
3739
|
});
|
|
3740
|
+
const cp = stepCheckpoint({
|
|
3741
|
+
runId: run.id,
|
|
3742
|
+
seq,
|
|
3743
|
+
name,
|
|
3744
|
+
kind: "local",
|
|
3745
|
+
status: "failed",
|
|
3746
|
+
attempts: attempt,
|
|
3747
|
+
error,
|
|
3748
|
+
enqueuedAt,
|
|
3749
|
+
startedAt,
|
|
3750
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
3751
|
+
});
|
|
3752
|
+
await this.store.saveCheckpoint(cp);
|
|
3753
|
+
replay?.set(seq, cp);
|
|
3522
3754
|
}
|
|
3523
3755
|
}
|
|
3524
3756
|
}
|
|
3525
3757
|
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Resolve (or advance) ONE dispatched saga compensation at its reserved negative `seq` — the undo
|
|
3760
|
+
* handler is an ordinary remote `@Step`, so this mirrors `callRemote`'s pending/failed/absent
|
|
3761
|
+
* branches (engine.ts's `callRemote`), minus the queue/admission and in-memory-`timeoutMs` paths
|
|
3762
|
+
* (V1: dispatch direct by name, always durable). Returns the wait signal when the run must pause for
|
|
3763
|
+
* this compensation (freshly dispatched, still in flight, or backing off before a durable retry);
|
|
3764
|
+
* `undefined` once it's resolved — completed (crash-safe skip), or retries exhausted/non-retryable
|
|
3765
|
+
* (already visible: `completeRemoteResult` emitted `step.failed` for `compensate:<name>` when that
|
|
3766
|
+
* checkpoint landed — never re-emitted here, and never allowed to block the rest of the unwind or
|
|
3767
|
+
* mask the run's original failure).
|
|
3768
|
+
*/
|
|
3769
|
+
async dispatchCompensation(run, comp, seq, replay) {
|
|
3770
|
+
const { def, args } = comp.dispatch;
|
|
3771
|
+
const name = `compensate:${comp.name}`;
|
|
3772
|
+
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
|
|
3773
|
+
if (existing?.status === "completed") return void 0;
|
|
3774
|
+
if (existing?.status === "pending") return {};
|
|
3775
|
+
if (existing?.status === "failed") {
|
|
3776
|
+
const maxAttempts = Math.max(1, def.retries ?? 1);
|
|
3777
|
+
const retryable = existing.error?.retryable !== false;
|
|
3778
|
+
if (!retryable || existing.attempts >= maxAttempts) return void 0;
|
|
3779
|
+
if (existing.wakeAt == null) {
|
|
3780
|
+
const wakeAt = this.clock() + backoffDelay(existing.attempts, def);
|
|
3781
|
+
const withWakeAt = {
|
|
3782
|
+
...existing,
|
|
3783
|
+
wakeAt
|
|
3784
|
+
};
|
|
3785
|
+
await this.store.saveCheckpoint(withWakeAt);
|
|
3786
|
+
replay?.set(seq, withWakeAt);
|
|
3787
|
+
return {
|
|
3788
|
+
wakeAt
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
if (this.clock() < existing.wakeAt) return {
|
|
3792
|
+
wakeAt: existing.wakeAt
|
|
3793
|
+
};
|
|
3794
|
+
await this.dispatchCompensationAttempt(run, seq, name, def, args, existing.attempts + 1, replay);
|
|
3795
|
+
return {};
|
|
3796
|
+
}
|
|
3797
|
+
await this.dispatchCompensationAttempt(run, seq, name, def, args, 1, replay);
|
|
3798
|
+
return {};
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Dispatch (or re-dispatch, on a durable retry) one compensation's undo handler — mirrors
|
|
3802
|
+
* `callRemote`'s dispatch block (persist the `pending` checkpoint BEFORE handing off to the
|
|
3803
|
+
* transport, so a fast result always finds it to complete). Two DIFFERENT names are in play: the
|
|
3804
|
+
* checkpoint's `name` is `compensate:<originalStepName>` (what makes this undo show up on the run's
|
|
3805
|
+
* timeline, and what `completeRemoteResult` emits `step.completed`/`step.failed` under); the WIRE
|
|
3806
|
+
* task's `name` is the undo handler's OWN routing name (`def.name`) — a worker dispatches to the
|
|
3807
|
+
* `@Step` it registered, not to this saga-visibility label.
|
|
3808
|
+
*/
|
|
3809
|
+
async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
|
|
3810
|
+
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
3811
|
+
const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
|
|
3812
|
+
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
3813
|
+
const cp = stepCheckpoint({
|
|
3814
|
+
runId: run.id,
|
|
3815
|
+
seq,
|
|
3816
|
+
name,
|
|
3817
|
+
kind: "remote",
|
|
3818
|
+
status: "pending",
|
|
3819
|
+
input: args,
|
|
3820
|
+
attempts: attempt,
|
|
3821
|
+
workerGroup: group,
|
|
3822
|
+
enqueuedAt,
|
|
3823
|
+
startedAt: enqueuedAt,
|
|
3824
|
+
finishedAt: enqueuedAt
|
|
3825
|
+
});
|
|
3826
|
+
await this.store.saveCheckpoint(cp);
|
|
3827
|
+
replay?.set(seq, cp);
|
|
3828
|
+
await this.pool.dispatch({
|
|
3829
|
+
runId: run.id,
|
|
3830
|
+
seq,
|
|
3831
|
+
name: def.name,
|
|
3832
|
+
stepId: cp.stepId,
|
|
3833
|
+
group,
|
|
3834
|
+
input: args,
|
|
3835
|
+
traceparent: this.traceparent?.(),
|
|
3836
|
+
context: this.context?.(),
|
|
3837
|
+
attempt
|
|
3838
|
+
});
|
|
3839
|
+
this.emit({
|
|
3840
|
+
type: "step.started",
|
|
3841
|
+
runId: run.id,
|
|
3842
|
+
seq,
|
|
3843
|
+
name,
|
|
3844
|
+
kind: "remote"
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3526
3847
|
/** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
|
|
3527
3848
|
* (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
|
|
3528
3849
|
* module and the engine stays the orchestrator. */
|
|
3529
|
-
ctxHostFor(replay) {
|
|
3850
|
+
ctxHostFor(replay, parentNamespace) {
|
|
3530
3851
|
return {
|
|
3531
3852
|
store: this.store,
|
|
3532
3853
|
replay,
|
|
@@ -3536,12 +3857,24 @@ var WorkflowEngine = class {
|
|
|
3536
3857
|
completeStep: /* @__PURE__ */ __name((s) => this.completeStep(s), "completeStep"),
|
|
3537
3858
|
failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
|
|
3538
3859
|
callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
|
|
3539
|
-
// Defer so a fast child can't reentrantly resume a still-running parent.
|
|
3860
|
+
// Defer so a fast child can't reentrantly resume a still-running parent. Stamp the child with
|
|
3861
|
+
// the PARENT run's namespace (not this engine's) so a child belongs to the tenant/partition of
|
|
3862
|
+
// the run it was spawned from — even when an operator (namespace: undefined, drives every
|
|
3863
|
+
// namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
|
|
3864
|
+
// `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
|
|
3865
|
+
// tenant's worker pool onto the shared/dev workers.
|
|
3540
3866
|
startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
|
|
3541
3867
|
queueMicrotask(() => void this.start(workflow, input, id, {
|
|
3542
|
-
priority
|
|
3868
|
+
priority,
|
|
3869
|
+
namespace: parentNamespace
|
|
3543
3870
|
}).catch(() => void 0));
|
|
3544
3871
|
}, "startChild"),
|
|
3872
|
+
// Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
|
|
3873
|
+
// idempotent on a terminal/cancelled run (returns its existing status without side effects), so
|
|
3874
|
+
// no extra guard is needed here for the failFast replay case (re-issuing the same cancel calls).
|
|
3875
|
+
cancelChild: /* @__PURE__ */ __name((childId) => {
|
|
3876
|
+
queueMicrotask(() => void this.cancel(childId).catch(() => void 0));
|
|
3877
|
+
}, "cancelChild"),
|
|
3545
3878
|
// Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
|
|
3546
3879
|
upsertSearchAttributes: /* @__PURE__ */ __name(async (runId, attrs) => {
|
|
3547
3880
|
const run = await this.store.getRun(runId);
|
|
@@ -4664,6 +4997,7 @@ export {
|
|
|
4664
4997
|
DURABLE_OPTIONS_CANONICAL,
|
|
4665
4998
|
DURABLE_STEP_CONFIG,
|
|
4666
4999
|
DURABLE_STEP_NAME,
|
|
5000
|
+
DurableWorkflow,
|
|
4667
5001
|
Entities,
|
|
4668
5002
|
EventAccumulators,
|
|
4669
5003
|
FatalError,
|
|
@@ -4690,9 +5024,11 @@ export {
|
|
|
4690
5024
|
applyAttributeQuery,
|
|
4691
5025
|
attributeColumnFor,
|
|
4692
5026
|
attributeOperand,
|
|
5027
|
+
bindWorkflowClass,
|
|
4693
5028
|
breakpointToken,
|
|
4694
5029
|
collectMetrics,
|
|
4695
5030
|
createStepLogger,
|
|
5031
|
+
currentWorkflowCtx,
|
|
4696
5032
|
eventMatchOf,
|
|
4697
5033
|
eventMatches,
|
|
4698
5034
|
eventPrefix,
|
|
@@ -4701,6 +5037,7 @@ export {
|
|
|
4701
5037
|
normalizeAttributeRows,
|
|
4702
5038
|
parseDuration,
|
|
4703
5039
|
prevCronFireMs,
|
|
5040
|
+
runInWorkflowCtx,
|
|
4704
5041
|
runSchedules,
|
|
4705
5042
|
runStepHandler,
|
|
4706
5043
|
sanitizeQueueToken,
|