@dudousxd/nestjs-durable-core 0.46.1 → 0.48.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 +511 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -28
- package/dist/index.d.ts +225 -28
- package/dist/index.js +507 -178
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
DURABLE_OPTIONS_CANONICAL: () => DURABLE_OPTIONS_CANONICAL,
|
|
29
29
|
DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
|
|
30
30
|
DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
|
|
31
|
+
DurableWorkflow: () => DurableWorkflow,
|
|
31
32
|
Entities: () => Entities,
|
|
32
33
|
EventAccumulators: () => EventAccumulators,
|
|
33
34
|
FatalError: () => FatalError,
|
|
@@ -54,9 +55,11 @@ __export(index_exports, {
|
|
|
54
55
|
applyAttributeQuery: () => applyAttributeQuery,
|
|
55
56
|
attributeColumnFor: () => attributeColumnFor,
|
|
56
57
|
attributeOperand: () => attributeOperand,
|
|
58
|
+
bindWorkflowClass: () => bindWorkflowClass,
|
|
57
59
|
breakpointToken: () => breakpointToken,
|
|
58
60
|
collectMetrics: () => collectMetrics,
|
|
59
61
|
createStepLogger: () => createStepLogger,
|
|
62
|
+
currentWorkflowCtx: () => currentWorkflowCtx,
|
|
60
63
|
eventMatchOf: () => eventMatchOf,
|
|
61
64
|
eventMatches: () => eventMatches,
|
|
62
65
|
eventPrefix: () => eventPrefix,
|
|
@@ -65,6 +68,7 @@ __export(index_exports, {
|
|
|
65
68
|
normalizeAttributeRows: () => normalizeAttributeRows,
|
|
66
69
|
parseDuration: () => parseDuration,
|
|
67
70
|
prevCronFireMs: () => prevCronFireMs,
|
|
71
|
+
runInWorkflowCtx: () => runInWorkflowCtx,
|
|
68
72
|
runSchedules: () => runSchedules,
|
|
69
73
|
runStepHandler: () => runStepHandler,
|
|
70
74
|
sanitizeQueueToken: () => sanitizeQueueToken,
|
|
@@ -233,6 +237,218 @@ var InMemoryAdmissionBackend = class {
|
|
|
233
237
|
}
|
|
234
238
|
};
|
|
235
239
|
|
|
240
|
+
// src/ambient-ctx.ts
|
|
241
|
+
var import_node_async_hooks = require("async_hooks");
|
|
242
|
+
var STORAGE_KEY = Symbol.for("nestjs-durable:ambient-workflow-ctx");
|
|
243
|
+
var globalRef = globalThis;
|
|
244
|
+
if (!globalRef[STORAGE_KEY]) {
|
|
245
|
+
globalRef[STORAGE_KEY] = new import_node_async_hooks.AsyncLocalStorage();
|
|
246
|
+
}
|
|
247
|
+
var storage = globalRef[STORAGE_KEY];
|
|
248
|
+
function runInWorkflowCtx(ctx, fn) {
|
|
249
|
+
return storage.run(ctx, fn);
|
|
250
|
+
}
|
|
251
|
+
__name(runInWorkflowCtx, "runInWorkflowCtx");
|
|
252
|
+
function currentWorkflowCtx() {
|
|
253
|
+
return storage.getStore();
|
|
254
|
+
}
|
|
255
|
+
__name(currentWorkflowCtx, "currentWorkflowCtx");
|
|
256
|
+
|
|
257
|
+
// src/errors.ts
|
|
258
|
+
var FatalError = class extends Error {
|
|
259
|
+
static {
|
|
260
|
+
__name(this, "FatalError");
|
|
261
|
+
}
|
|
262
|
+
code;
|
|
263
|
+
constructor(message, code) {
|
|
264
|
+
super(message);
|
|
265
|
+
this.name = "FatalError";
|
|
266
|
+
this.code = code;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var SingletonQueueFullError = class extends Error {
|
|
270
|
+
static {
|
|
271
|
+
__name(this, "SingletonQueueFullError");
|
|
272
|
+
}
|
|
273
|
+
workflow;
|
|
274
|
+
key;
|
|
275
|
+
maxQueueDepth;
|
|
276
|
+
constructor(workflow, key, maxQueueDepth) {
|
|
277
|
+
super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
|
|
278
|
+
this.name = "SingletonQueueFullError";
|
|
279
|
+
this.workflow = workflow;
|
|
280
|
+
this.key = key;
|
|
281
|
+
this.maxQueueDepth = maxQueueDepth;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var SignalTimeoutError = class extends Error {
|
|
285
|
+
static {
|
|
286
|
+
__name(this, "SignalTimeoutError");
|
|
287
|
+
}
|
|
288
|
+
token;
|
|
289
|
+
timeoutMs;
|
|
290
|
+
constructor(token, timeoutMs) {
|
|
291
|
+
super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
|
|
292
|
+
this.name = "SignalTimeoutError";
|
|
293
|
+
this.token = token;
|
|
294
|
+
this.timeoutMs = timeoutMs;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var RemoteStepTimeout = class extends Error {
|
|
298
|
+
static {
|
|
299
|
+
__name(this, "RemoteStepTimeout");
|
|
300
|
+
}
|
|
301
|
+
stepId;
|
|
302
|
+
timeoutMs;
|
|
303
|
+
constructor(stepId2, timeoutMs) {
|
|
304
|
+
super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
|
|
305
|
+
this.name = "RemoteStepTimeout";
|
|
306
|
+
this.stepId = stepId2;
|
|
307
|
+
this.timeoutMs = timeoutMs;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var RemoteWorkflowTimeout = class extends Error {
|
|
311
|
+
static {
|
|
312
|
+
__name(this, "RemoteWorkflowTimeout");
|
|
313
|
+
}
|
|
314
|
+
taskId;
|
|
315
|
+
timeoutMs;
|
|
316
|
+
constructor(taskId, timeoutMs) {
|
|
317
|
+
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
318
|
+
this.name = "RemoteWorkflowTimeout";
|
|
319
|
+
this.taskId = taskId;
|
|
320
|
+
this.timeoutMs = timeoutMs;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var NonDeterminismError = class extends Error {
|
|
324
|
+
static {
|
|
325
|
+
__name(this, "NonDeterminismError");
|
|
326
|
+
}
|
|
327
|
+
runId;
|
|
328
|
+
seq;
|
|
329
|
+
constructor(runId, seq, expected, recorded) {
|
|
330
|
+
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.`);
|
|
331
|
+
this.name = "NonDeterminismError";
|
|
332
|
+
this.runId = runId;
|
|
333
|
+
this.seq = seq;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var GatherError = class extends Error {
|
|
337
|
+
static {
|
|
338
|
+
__name(this, "GatherError");
|
|
339
|
+
}
|
|
340
|
+
failures;
|
|
341
|
+
constructor(failures, total) {
|
|
342
|
+
const ids = failures.map((f) => f.id).join(", ");
|
|
343
|
+
const denom = total ?? failures.length;
|
|
344
|
+
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
345
|
+
this.name = "GatherError";
|
|
346
|
+
this.failures = failures;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var WorkflowSuspended = class extends Error {
|
|
350
|
+
static {
|
|
351
|
+
__name(this, "WorkflowSuspended");
|
|
352
|
+
}
|
|
353
|
+
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
354
|
+
wakeAt;
|
|
355
|
+
constructor(wakeAt) {
|
|
356
|
+
super("workflow suspended");
|
|
357
|
+
this.name = "WorkflowSuspended";
|
|
358
|
+
this.wakeAt = wakeAt;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
var ContinueAsNew = class extends Error {
|
|
362
|
+
static {
|
|
363
|
+
__name(this, "ContinueAsNew");
|
|
364
|
+
}
|
|
365
|
+
input;
|
|
366
|
+
constructor(input) {
|
|
367
|
+
super("workflow continued as new"), this.input = input;
|
|
368
|
+
this.name = "ContinueAsNew";
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/workflow-ref.ts
|
|
373
|
+
var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
|
|
374
|
+
function workflowName(ref) {
|
|
375
|
+
if (typeof ref === "string") return ref;
|
|
376
|
+
const name = ref[WORKFLOW_NAME_KEY];
|
|
377
|
+
if (!name) {
|
|
378
|
+
throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
|
|
379
|
+
}
|
|
380
|
+
return name;
|
|
381
|
+
}
|
|
382
|
+
__name(workflowName, "workflowName");
|
|
383
|
+
|
|
384
|
+
// src/durable-workflow.ts
|
|
385
|
+
var BINDINGS_KEY = Symbol.for("nestjs-durable:workflow-class-bindings");
|
|
386
|
+
var globalRef2 = globalThis;
|
|
387
|
+
if (!globalRef2[BINDINGS_KEY]) {
|
|
388
|
+
globalRef2[BINDINGS_KEY] = /* @__PURE__ */ new WeakMap();
|
|
389
|
+
}
|
|
390
|
+
var bindings = globalRef2[BINDINGS_KEY];
|
|
391
|
+
function bindWorkflowClass(ctor, client) {
|
|
392
|
+
bindings.set(ctor, client);
|
|
393
|
+
}
|
|
394
|
+
__name(bindWorkflowClass, "bindWorkflowClass");
|
|
395
|
+
function clientOf(ctor) {
|
|
396
|
+
const client = bindings.get(ctor);
|
|
397
|
+
if (!client) {
|
|
398
|
+
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).`);
|
|
399
|
+
}
|
|
400
|
+
return client;
|
|
401
|
+
}
|
|
402
|
+
__name(clientOf, "clientOf");
|
|
403
|
+
var DurableWorkflow = class {
|
|
404
|
+
static {
|
|
405
|
+
__name(this, "DurableWorkflow");
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Fire-and-forget start — `engine.start` outside a workflow, `ctx.startChild` (parent-linked)
|
|
409
|
+
* inside one. `opts.id` pins the run id (outside) / child id (inside) for idempotent starts.
|
|
410
|
+
*/
|
|
411
|
+
static async start(input, opts) {
|
|
412
|
+
const cls = this;
|
|
413
|
+
const ctx = currentWorkflowCtx();
|
|
414
|
+
if (ctx) {
|
|
415
|
+
const runId2 = await ctx.startChild(cls, input, opts?.id);
|
|
416
|
+
return {
|
|
417
|
+
runId: runId2,
|
|
418
|
+
status: "pending"
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const runId = opts?.id ?? globalThis.crypto.randomUUID();
|
|
422
|
+
return clientOf(cls).start(workflowName(cls), input, runId, opts);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Run-and-await the typed output — `ctx.child` inside a workflow (suspends the parent, zero
|
|
426
|
+
* compute); outside, starts the run and waits until it settles terminally. Throws `FatalError`
|
|
427
|
+
* when the run does not complete. `opts.timeoutMs` bounds the OUTSIDE wait only (inside, a
|
|
428
|
+
* suspended parent costs nothing — bound the child itself with `executionTimeout` instead).
|
|
429
|
+
*/
|
|
430
|
+
static async execute(input, opts) {
|
|
431
|
+
const cls = this;
|
|
432
|
+
const ctx = currentWorkflowCtx();
|
|
433
|
+
if (ctx) return ctx.child(cls, input, opts?.id);
|
|
434
|
+
const client = clientOf(cls);
|
|
435
|
+
if (!client.waitForRun) {
|
|
436
|
+
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");
|
|
437
|
+
}
|
|
438
|
+
const runId = opts?.id ?? globalThis.crypto.randomUUID();
|
|
439
|
+
await client.start(workflowName(cls), input, runId, opts);
|
|
440
|
+
const settled = await client.waitForRun(runId, {
|
|
441
|
+
...opts?.timeoutMs != null ? {
|
|
442
|
+
timeoutMs: opts.timeoutMs
|
|
443
|
+
} : {},
|
|
444
|
+
until: "terminal"
|
|
445
|
+
});
|
|
446
|
+
if (settled.status === "completed") return settled.output;
|
|
447
|
+
const detail = settled.error ? `: ${settled.error.message}` : "";
|
|
448
|
+
throw new FatalError(`${workflowName(cls)} run ${runId} settled ${settled.status}${detail}`, settled.error?.code ?? "execute_failed");
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
236
452
|
// src/duration.ts
|
|
237
453
|
var UNIT_MS = {
|
|
238
454
|
ms: 1,
|
|
@@ -489,121 +705,6 @@ function instantCheckpoint(p) {
|
|
|
489
705
|
}
|
|
490
706
|
__name(instantCheckpoint, "instantCheckpoint");
|
|
491
707
|
|
|
492
|
-
// src/errors.ts
|
|
493
|
-
var FatalError = class extends Error {
|
|
494
|
-
static {
|
|
495
|
-
__name(this, "FatalError");
|
|
496
|
-
}
|
|
497
|
-
code;
|
|
498
|
-
constructor(message, code) {
|
|
499
|
-
super(message);
|
|
500
|
-
this.name = "FatalError";
|
|
501
|
-
this.code = code;
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
var SingletonQueueFullError = class extends Error {
|
|
505
|
-
static {
|
|
506
|
-
__name(this, "SingletonQueueFullError");
|
|
507
|
-
}
|
|
508
|
-
workflow;
|
|
509
|
-
key;
|
|
510
|
-
maxQueueDepth;
|
|
511
|
-
constructor(workflow, key, maxQueueDepth) {
|
|
512
|
-
super(`singleton queue for ${workflow} key "${key}" is full (maxQueueDepth=${maxQueueDepth}); retry later`);
|
|
513
|
-
this.name = "SingletonQueueFullError";
|
|
514
|
-
this.workflow = workflow;
|
|
515
|
-
this.key = key;
|
|
516
|
-
this.maxQueueDepth = maxQueueDepth;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
var SignalTimeoutError = class extends Error {
|
|
520
|
-
static {
|
|
521
|
-
__name(this, "SignalTimeoutError");
|
|
522
|
-
}
|
|
523
|
-
token;
|
|
524
|
-
timeoutMs;
|
|
525
|
-
constructor(token, timeoutMs) {
|
|
526
|
-
super(`timed out after ${timeoutMs}ms waiting for signal "${token}"`);
|
|
527
|
-
this.name = "SignalTimeoutError";
|
|
528
|
-
this.token = token;
|
|
529
|
-
this.timeoutMs = timeoutMs;
|
|
530
|
-
}
|
|
531
|
-
};
|
|
532
|
-
var RemoteStepTimeout = class extends Error {
|
|
533
|
-
static {
|
|
534
|
-
__name(this, "RemoteStepTimeout");
|
|
535
|
-
}
|
|
536
|
-
stepId;
|
|
537
|
-
timeoutMs;
|
|
538
|
-
constructor(stepId2, timeoutMs) {
|
|
539
|
-
super(`remote step ${stepId2} produced no result/heartbeat within ${timeoutMs}ms`);
|
|
540
|
-
this.name = "RemoteStepTimeout";
|
|
541
|
-
this.stepId = stepId2;
|
|
542
|
-
this.timeoutMs = timeoutMs;
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
var RemoteWorkflowTimeout = class extends Error {
|
|
546
|
-
static {
|
|
547
|
-
__name(this, "RemoteWorkflowTimeout");
|
|
548
|
-
}
|
|
549
|
-
taskId;
|
|
550
|
-
timeoutMs;
|
|
551
|
-
constructor(taskId, timeoutMs) {
|
|
552
|
-
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
553
|
-
this.name = "RemoteWorkflowTimeout";
|
|
554
|
-
this.taskId = taskId;
|
|
555
|
-
this.timeoutMs = timeoutMs;
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
var NonDeterminismError = class extends Error {
|
|
559
|
-
static {
|
|
560
|
-
__name(this, "NonDeterminismError");
|
|
561
|
-
}
|
|
562
|
-
runId;
|
|
563
|
-
seq;
|
|
564
|
-
constructor(runId, seq, expected, recorded) {
|
|
565
|
-
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.`);
|
|
566
|
-
this.name = "NonDeterminismError";
|
|
567
|
-
this.runId = runId;
|
|
568
|
-
this.seq = seq;
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
var GatherError = class extends Error {
|
|
572
|
-
static {
|
|
573
|
-
__name(this, "GatherError");
|
|
574
|
-
}
|
|
575
|
-
failures;
|
|
576
|
-
constructor(failures, total) {
|
|
577
|
-
const ids = failures.map((f) => f.id).join(", ");
|
|
578
|
-
const denom = total ?? failures.length;
|
|
579
|
-
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
580
|
-
this.name = "GatherError";
|
|
581
|
-
this.failures = failures;
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
var WorkflowSuspended = class extends Error {
|
|
585
|
-
static {
|
|
586
|
-
__name(this, "WorkflowSuspended");
|
|
587
|
-
}
|
|
588
|
-
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
589
|
-
wakeAt;
|
|
590
|
-
constructor(wakeAt) {
|
|
591
|
-
super("workflow suspended");
|
|
592
|
-
this.name = "WorkflowSuspended";
|
|
593
|
-
this.wakeAt = wakeAt;
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
var ContinueAsNew = class extends Error {
|
|
597
|
-
static {
|
|
598
|
-
__name(this, "ContinueAsNew");
|
|
599
|
-
}
|
|
600
|
-
input;
|
|
601
|
-
constructor(input) {
|
|
602
|
-
super("workflow continued as new"), this.input = input;
|
|
603
|
-
this.name = "ContinueAsNew";
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
708
|
// src/entities.ts
|
|
608
709
|
var ENTITY_WORKFLOW = "__entity";
|
|
609
710
|
var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
|
|
@@ -1065,18 +1166,6 @@ function stepConfigOf(ref) {
|
|
|
1065
1166
|
}
|
|
1066
1167
|
__name(stepConfigOf, "stepConfigOf");
|
|
1067
1168
|
|
|
1068
|
-
// src/workflow-ref.ts
|
|
1069
|
-
var WORKFLOW_NAME_KEY = Symbol.for("nestjs-durable:workflow-name");
|
|
1070
|
-
function workflowName(ref) {
|
|
1071
|
-
if (typeof ref === "string") return ref;
|
|
1072
|
-
const name = ref[WORKFLOW_NAME_KEY];
|
|
1073
|
-
if (!name) {
|
|
1074
|
-
throw new Error(`workflow class ${ref.name} has no registered name \u2014 is it decorated with @Workflow({ name })?`);
|
|
1075
|
-
}
|
|
1076
|
-
return name;
|
|
1077
|
-
}
|
|
1078
|
-
__name(workflowName, "workflowName");
|
|
1079
|
-
|
|
1080
1169
|
// src/workflow-ctx.ts
|
|
1081
1170
|
function normalizeChildOptions(options) {
|
|
1082
1171
|
return typeof options === "string" ? {
|
|
@@ -1262,6 +1351,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1262
1351
|
value: buffered.payload
|
|
1263
1352
|
};
|
|
1264
1353
|
}, "consumeBuffered");
|
|
1354
|
+
const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
|
|
1355
|
+
const recorded = await readCheckpoint(deadlineSeq);
|
|
1356
|
+
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1357
|
+
if (!recorded) {
|
|
1358
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1359
|
+
runId,
|
|
1360
|
+
seq: deadlineSeq,
|
|
1361
|
+
name: `timeout:${label}`,
|
|
1362
|
+
kind: "sleep",
|
|
1363
|
+
wakeAt: deadline
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
return deadline;
|
|
1367
|
+
}, "stampDeadline");
|
|
1265
1368
|
const waitForSignal = /* @__PURE__ */ __name(async (token, opts) => {
|
|
1266
1369
|
if (opts?.timeoutMs == null) {
|
|
1267
1370
|
const current = pos.next();
|
|
@@ -1279,17 +1382,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1279
1382
|
const timeoutMs = opts.timeoutMs;
|
|
1280
1383
|
const deadlineSeq = pos.next();
|
|
1281
1384
|
const waitSeq = pos.next();
|
|
1282
|
-
const
|
|
1283
|
-
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1284
|
-
if (!recorded) {
|
|
1285
|
-
await writeCheckpoint(instantCheckpoint({
|
|
1286
|
-
runId,
|
|
1287
|
-
seq: deadlineSeq,
|
|
1288
|
-
name: `timeout:${token}`,
|
|
1289
|
-
kind: "sleep",
|
|
1290
|
-
wakeAt: deadline
|
|
1291
|
-
}));
|
|
1292
|
-
}
|
|
1385
|
+
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
1293
1386
|
const waited = await readCheckpoint(waitSeq);
|
|
1294
1387
|
if (waited && waited.status === "completed") return waited.output;
|
|
1295
1388
|
const buffered = await consumeBuffered(token, waitSeq);
|
|
@@ -1322,17 +1415,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1322
1415
|
const deadlineSeq = pos.next();
|
|
1323
1416
|
const waitSeq = pos.next();
|
|
1324
1417
|
const token = eventToken(name, opts.match, runId, waitSeq);
|
|
1325
|
-
const
|
|
1326
|
-
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
1327
|
-
if (!recorded) {
|
|
1328
|
-
await writeCheckpoint(instantCheckpoint({
|
|
1329
|
-
runId,
|
|
1330
|
-
seq: deadlineSeq,
|
|
1331
|
-
name: `timeout:event:${name}`,
|
|
1332
|
-
kind: "sleep",
|
|
1333
|
-
wakeAt: deadline
|
|
1334
|
-
}));
|
|
1335
|
-
}
|
|
1418
|
+
const deadline = await stampDeadline(deadlineSeq, `event:${name}`, timeoutMs);
|
|
1336
1419
|
const waited = await readCheckpoint(waitSeq);
|
|
1337
1420
|
if (waited && waited.status === "completed") return waited.output;
|
|
1338
1421
|
if (host.clock() >= deadline) {
|
|
@@ -1388,6 +1471,9 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1388
1471
|
const cp = existing[i];
|
|
1389
1472
|
const c = cp?.status === "completed" ? cp.output : null;
|
|
1390
1473
|
if (c && c.ok === false) {
|
|
1474
|
+
for (let j = 0; j < inputs.length; j += 1) {
|
|
1475
|
+
if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
|
|
1476
|
+
}
|
|
1391
1477
|
throw new GatherError([
|
|
1392
1478
|
{
|
|
1393
1479
|
index: i,
|
|
@@ -1544,15 +1630,30 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1544
1630
|
const webhook = /* @__PURE__ */ __name(() => {
|
|
1545
1631
|
const current = pos.next();
|
|
1546
1632
|
const token = `wh:${runId}:${current}`;
|
|
1547
|
-
const wait = /* @__PURE__ */ __name(async () => {
|
|
1633
|
+
const wait = /* @__PURE__ */ __name(async (opts) => {
|
|
1634
|
+
const timeoutMs = opts?.timeoutMs;
|
|
1635
|
+
const deadlineSeq = timeoutMs == null ? -1 : pos.next();
|
|
1548
1636
|
const existing = await readCheckpoint(current);
|
|
1549
1637
|
if (existing && existing.status === "completed") return existing.output;
|
|
1638
|
+
if (timeoutMs == null) {
|
|
1639
|
+
await store.putSignalWaiter({
|
|
1640
|
+
token,
|
|
1641
|
+
runId,
|
|
1642
|
+
seq: current
|
|
1643
|
+
});
|
|
1644
|
+
throw new WorkflowSuspended();
|
|
1645
|
+
}
|
|
1646
|
+
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
1647
|
+
if (host.clock() >= deadline) {
|
|
1648
|
+
await store.takeSignalWaiter(token).catch(() => void 0);
|
|
1649
|
+
throw new SignalTimeoutError(token, timeoutMs);
|
|
1650
|
+
}
|
|
1550
1651
|
await store.putSignalWaiter({
|
|
1551
1652
|
token,
|
|
1552
1653
|
runId,
|
|
1553
1654
|
seq: current
|
|
1554
1655
|
});
|
|
1555
|
-
throw new WorkflowSuspended();
|
|
1656
|
+
throw new WorkflowSuspended(deadline);
|
|
1556
1657
|
}, "wait");
|
|
1557
1658
|
return {
|
|
1558
1659
|
token,
|
|
@@ -1580,6 +1681,18 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1580
1681
|
output: attrs
|
|
1581
1682
|
}));
|
|
1582
1683
|
}, "upsertSearchAttributes");
|
|
1684
|
+
function resolveCompensateDef(compensate) {
|
|
1685
|
+
const name = typeof compensate === "string" ? compensate : stepNameOf(compensate);
|
|
1686
|
+
if (name === void 0) {
|
|
1687
|
+
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");
|
|
1688
|
+
}
|
|
1689
|
+
const config = typeof compensate === "string" ? void 0 : stepConfigOf(compensate);
|
|
1690
|
+
return {
|
|
1691
|
+
name,
|
|
1692
|
+
...resolveStepPolicy(config, void 0)
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
__name(resolveCompensateDef, "resolveCompensateDef");
|
|
1583
1696
|
function step(handlerOrName, input, opts) {
|
|
1584
1697
|
const name = typeof handlerOrName === "string" ? handlerOrName : stepNameOf(handlerOrName);
|
|
1585
1698
|
if (name === void 0) {
|
|
@@ -1593,6 +1706,20 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1593
1706
|
return host.callRemote(runId, pos.next(), def, input, opts?.queue, opts?.transport, {
|
|
1594
1707
|
priority: opts?.priority,
|
|
1595
1708
|
fairnessKey: opts?.fairnessKey
|
|
1709
|
+
}).then((output) => {
|
|
1710
|
+
if (opts?.compensate) {
|
|
1711
|
+
compensations.push({
|
|
1712
|
+
name,
|
|
1713
|
+
dispatch: {
|
|
1714
|
+
def: resolveCompensateDef(opts.compensate),
|
|
1715
|
+
args: {
|
|
1716
|
+
input,
|
|
1717
|
+
output
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return output;
|
|
1596
1723
|
});
|
|
1597
1724
|
}
|
|
1598
1725
|
__name(step, "step");
|
|
@@ -2486,7 +2613,13 @@ var WorkflowEngine = class {
|
|
|
2486
2613
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
2487
2614
|
*/
|
|
2488
2615
|
waitForRun(runId, opts) {
|
|
2489
|
-
const
|
|
2616
|
+
const TERMINAL = /* @__PURE__ */ new Set([
|
|
2617
|
+
"completed",
|
|
2618
|
+
"failed",
|
|
2619
|
+
"cancelled",
|
|
2620
|
+
"dead"
|
|
2621
|
+
]);
|
|
2622
|
+
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");
|
|
2490
2623
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
2491
2624
|
runId,
|
|
2492
2625
|
status: run.status,
|
|
@@ -2500,6 +2633,7 @@ var WorkflowEngine = class {
|
|
|
2500
2633
|
const timer = opts?.timeoutMs != null ? setTimeout(() => {
|
|
2501
2634
|
if (done) return;
|
|
2502
2635
|
done = true;
|
|
2636
|
+
if (poller) clearInterval(poller);
|
|
2503
2637
|
off();
|
|
2504
2638
|
reject(new Error(`waitForRun(${runId}) timed out after ${opts.timeoutMs}ms`));
|
|
2505
2639
|
}, opts.timeoutMs) : void 0;
|
|
@@ -2507,6 +2641,7 @@ var WorkflowEngine = class {
|
|
|
2507
2641
|
if (done) return;
|
|
2508
2642
|
done = true;
|
|
2509
2643
|
if (timer) clearTimeout(timer);
|
|
2644
|
+
if (poller) clearInterval(poller);
|
|
2510
2645
|
off();
|
|
2511
2646
|
resolve(toResult(run));
|
|
2512
2647
|
}, "finish");
|
|
@@ -2515,6 +2650,11 @@ var WorkflowEngine = class {
|
|
|
2515
2650
|
if (run && isSettled(run)) finish(run);
|
|
2516
2651
|
});
|
|
2517
2652
|
}, "check");
|
|
2653
|
+
const poller = opts?.until === "terminal" ? (() => {
|
|
2654
|
+
const interval = setInterval(check, 250);
|
|
2655
|
+
interval.unref?.();
|
|
2656
|
+
return interval;
|
|
2657
|
+
})() : void 0;
|
|
2518
2658
|
off = this.subscribe((ev) => {
|
|
2519
2659
|
if (ev.runId === runId && (ev.type === "run.completed" || ev.type === "run.failed" || ev.type === "run.suspended")) {
|
|
2520
2660
|
check();
|
|
@@ -3438,14 +3578,24 @@ var WorkflowEngine = class {
|
|
|
3438
3578
|
* Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
|
|
3439
3579
|
* exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
|
|
3440
3580
|
* suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
|
|
3441
|
-
* stop at a suspension point.
|
|
3581
|
+
* stop at a suspension point.
|
|
3582
|
+
*
|
|
3583
|
+
* A dispatched compensation still in flight (or backing off a retry) needs the run to keep waiting:
|
|
3584
|
+
* unlike the failure path (which is fine going `suspended` — a resumable state no different from an
|
|
3585
|
+
* ordinary in-flight `ctx.step`), THIS run must stay `cancelling` — `engine.cancel`'s idempotent
|
|
3586
|
+
* re-check, `waitForRun`'s non-terminal treatment, and crash recovery (`recoverIncomplete` only
|
|
3587
|
+
* re-drives `running`/`cancelling` runs — a `cancelling` one with a free lease, whatever it's
|
|
3588
|
+
* internally waiting on) all key off that status. So on a pending unwind this returns WITHOUT
|
|
3589
|
+
* touching `status` (already `cancelling`, untouched since `engine.cancel` set it) — the in-memory
|
|
3590
|
+
* cancel flag stays too, re-derived from the status on any later re-drive regardless.
|
|
3442
3591
|
*/
|
|
3443
|
-
async compensateAndCancel(run, compensations) {
|
|
3592
|
+
async compensateAndCancel(run, compensations, replay) {
|
|
3593
|
+
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
3594
|
+
if (pending) return {
|
|
3595
|
+
runId: run.id,
|
|
3596
|
+
status: "cancelling"
|
|
3597
|
+
};
|
|
3444
3598
|
this.cancelRequested.delete(run.id);
|
|
3445
|
-
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
3446
|
-
const comp = compensations[i];
|
|
3447
|
-
if (comp) await this.runCompensation(run, comp);
|
|
3448
|
-
}
|
|
3449
3599
|
const error = {
|
|
3450
3600
|
message: "cancelled"
|
|
3451
3601
|
};
|
|
@@ -3508,15 +3658,19 @@ var WorkflowEngine = class {
|
|
|
3508
3658
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
3509
3659
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow);
|
|
3510
3660
|
try {
|
|
3511
|
-
const output = await fn(ctx, run.input);
|
|
3512
|
-
if (this.cancelRequested.has(run.id))
|
|
3661
|
+
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
3662
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3663
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3664
|
+
}
|
|
3513
3665
|
return this.settleRun(run, {
|
|
3514
3666
|
kind: "completed",
|
|
3515
3667
|
output
|
|
3516
3668
|
});
|
|
3517
3669
|
} catch (err) {
|
|
3518
3670
|
if (err instanceof ContinueAsNew) {
|
|
3519
|
-
if (this.cancelRequested.has(run.id))
|
|
3671
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3672
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3673
|
+
}
|
|
3520
3674
|
await this.store.updateRun(run.id, {
|
|
3521
3675
|
status: "completed",
|
|
3522
3676
|
output: void 0,
|
|
@@ -3541,22 +3695,26 @@ var WorkflowEngine = class {
|
|
|
3541
3695
|
};
|
|
3542
3696
|
}
|
|
3543
3697
|
if (err instanceof WorkflowSuspended) {
|
|
3544
|
-
if (this.cancelRequested.has(run.id))
|
|
3698
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3699
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3700
|
+
}
|
|
3545
3701
|
return this.settleRun(run, {
|
|
3546
3702
|
kind: "suspended",
|
|
3547
3703
|
wakeAt: err.wakeAt
|
|
3548
3704
|
});
|
|
3549
3705
|
}
|
|
3550
|
-
if (this.cancelRequested.has(run.id))
|
|
3706
|
+
if (this.cancelRequested.has(run.id)) {
|
|
3707
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
3708
|
+
}
|
|
3551
3709
|
const error = {
|
|
3552
3710
|
message: err instanceof Error ? err.message : String(err),
|
|
3553
3711
|
stack: err instanceof Error ? err.stack : void 0
|
|
3554
3712
|
};
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
}
|
|
3713
|
+
const pending = await this.unwindCompensations(run, compensations, replay);
|
|
3714
|
+
if (pending) return this.settleRun(run, {
|
|
3715
|
+
kind: "suspended",
|
|
3716
|
+
wakeAt: pending.wakeAt
|
|
3717
|
+
});
|
|
3560
3718
|
return this.settleRun(run, {
|
|
3561
3719
|
kind: "failed",
|
|
3562
3720
|
error
|
|
@@ -3566,14 +3724,62 @@ var WorkflowEngine = class {
|
|
|
3566
3724
|
}
|
|
3567
3725
|
}
|
|
3568
3726
|
/**
|
|
3569
|
-
*
|
|
3570
|
-
*
|
|
3571
|
-
*
|
|
3727
|
+
* Undo every registered saga compensation in strict reverse order — the shared unwind both the
|
|
3728
|
+
* run-failure path (`runExecution`'s catch) and the compensating-cancel path
|
|
3729
|
+
* (`compensateAndCancel`) drive. Every entry, LOCAL or DISPATCHED, gets a NEGATIVE checkpoint seq
|
|
3730
|
+
* `-(k + 1)` for its position `k` (0, 1, 2… walking the stack in reverse) — reserved space that can
|
|
3731
|
+
* never collide with a body position (those start at 0), so a saga's undo trail composes onto the
|
|
3732
|
+
* run's ordinary checkpoint history with no store-schema change, and shows up in run detail exactly
|
|
3733
|
+
* like any other step. Deterministic because `compensations` is rebuilt identically on every replay
|
|
3734
|
+
* (see `workflow-ctx.ts`), so the same comp always lands on the same negative seq across a crash +
|
|
3735
|
+
* re-drive — the crash-safety this whole scheme rests on.
|
|
3736
|
+
*
|
|
3737
|
+
* A LOCAL entry runs in-process (`runCompensation`), which checkpoints its own outcome — completed,
|
|
3738
|
+
* or failed once `compensationRetries` are exhausted — so a crash mid-unwind SKIPS an
|
|
3739
|
+
* already-resolved one on the next pass instead of re-running it (and re-spending its retry budget)
|
|
3740
|
+
* from attempt 1. A DISPATCHED entry is an ordinary remote-step dispatch under the hood
|
|
3741
|
+
* (`dispatchCompensation`): when it needs the run to WAIT — freshly dispatched, still in flight, or
|
|
3742
|
+
* backing off a durable retry — this function returns that wait signal immediately (skipping every
|
|
3743
|
+
* comp after it in this pass); the CALLER decides how to represent "still unwinding" (see
|
|
3744
|
+
* `compensateAndCancel`'s doc comment for why that differs between the two callers). Either way, the
|
|
3745
|
+
* run resumes later exactly like any other dispatched step: `completeRemoteResult` completes the
|
|
3746
|
+
* negative-seq checkpoint and re-drives, the body replays and deterministically re-throws the same
|
|
3747
|
+
* exit it took before (the original failure, or the suspension `compensateAndCancel` resumed from),
|
|
3748
|
+
* this function runs again, the now-resolved comp is skipped, and the unwind continues from there.
|
|
3749
|
+
*
|
|
3750
|
+
* Returns `undefined` once every compensation has resolved — the caller then finalizes the run
|
|
3751
|
+
* (`failed` with the ORIGINAL body error, never masked by a comp outcome; or `cancelled`).
|
|
3752
|
+
*/
|
|
3753
|
+
async unwindCompensations(run, compensations, replay) {
|
|
3754
|
+
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
3755
|
+
const comp = compensations[i];
|
|
3756
|
+
if (!comp) continue;
|
|
3757
|
+
const seq = -(compensations.length - i);
|
|
3758
|
+
if ("fn" in comp) {
|
|
3759
|
+
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
|
|
3760
|
+
if (existing) continue;
|
|
3761
|
+
await this.runCompensation(run, comp, seq, replay);
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
const pending = await this.dispatchCompensation(run, comp, seq, replay);
|
|
3765
|
+
if (pending) return pending;
|
|
3766
|
+
}
|
|
3767
|
+
return void 0;
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* Run one LOCAL saga compensation, retried up to `compensationRetries`, emitting a
|
|
3771
|
+
* `compensate:<step>` step event for its outcome so a stranded undo is visible. Never throws — a
|
|
3772
|
+
* permanently-failing compensation is skipped so it can't mask the original failure. Checkpoints
|
|
3773
|
+
* its outcome at `seq` (a negative unwind position reserved by {@link unwindCompensations}) so a
|
|
3774
|
+
* crash mid-unwind skips an already-resolved undo on a later pass instead of re-running it (and
|
|
3775
|
+
* re-spending its retry budget) from attempt 1 — the checkpoint is durability/visibility only; the
|
|
3776
|
+
* runtime retry-in-process-back-to-back semantics of `comp.fn()` are unchanged.
|
|
3572
3777
|
*/
|
|
3573
|
-
async runCompensation(run, comp) {
|
|
3778
|
+
async runCompensation(run, comp, seq, replay) {
|
|
3574
3779
|
const name = `compensate:${comp.name}`;
|
|
3780
|
+
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
3575
3781
|
for (let attempt = 1; attempt <= this.compensationRetries; attempt += 1) {
|
|
3576
|
-
const startedAt = Date
|
|
3782
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
3577
3783
|
try {
|
|
3578
3784
|
await comp.fn();
|
|
3579
3785
|
this.emit({
|
|
@@ -3582,26 +3788,143 @@ var WorkflowEngine = class {
|
|
|
3582
3788
|
workflow: run.workflow,
|
|
3583
3789
|
name,
|
|
3584
3790
|
kind: "local",
|
|
3585
|
-
durationMs: Date.now() - startedAt
|
|
3791
|
+
durationMs: Date.now() - startedAt.getTime()
|
|
3586
3792
|
});
|
|
3793
|
+
const cp = stepCheckpoint({
|
|
3794
|
+
runId: run.id,
|
|
3795
|
+
seq,
|
|
3796
|
+
name,
|
|
3797
|
+
kind: "local",
|
|
3798
|
+
status: "completed",
|
|
3799
|
+
attempts: attempt,
|
|
3800
|
+
enqueuedAt,
|
|
3801
|
+
startedAt,
|
|
3802
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
3803
|
+
});
|
|
3804
|
+
await this.store.saveCheckpoint(cp);
|
|
3805
|
+
replay?.set(seq, cp);
|
|
3587
3806
|
return;
|
|
3588
3807
|
} catch (err) {
|
|
3589
3808
|
if (attempt >= this.compensationRetries) {
|
|
3809
|
+
const error = {
|
|
3810
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3811
|
+
};
|
|
3590
3812
|
this.emit({
|
|
3591
3813
|
type: "step.failed",
|
|
3592
3814
|
runId: run.id,
|
|
3593
3815
|
workflow: run.workflow,
|
|
3594
3816
|
name,
|
|
3595
3817
|
kind: "local",
|
|
3596
|
-
error
|
|
3597
|
-
|
|
3598
|
-
},
|
|
3599
|
-
durationMs: Date.now() - startedAt
|
|
3818
|
+
error,
|
|
3819
|
+
durationMs: Date.now() - startedAt.getTime()
|
|
3600
3820
|
});
|
|
3821
|
+
const cp = stepCheckpoint({
|
|
3822
|
+
runId: run.id,
|
|
3823
|
+
seq,
|
|
3824
|
+
name,
|
|
3825
|
+
kind: "local",
|
|
3826
|
+
status: "failed",
|
|
3827
|
+
attempts: attempt,
|
|
3828
|
+
error,
|
|
3829
|
+
enqueuedAt,
|
|
3830
|
+
startedAt,
|
|
3831
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
3832
|
+
});
|
|
3833
|
+
await this.store.saveCheckpoint(cp);
|
|
3834
|
+
replay?.set(seq, cp);
|
|
3601
3835
|
}
|
|
3602
3836
|
}
|
|
3603
3837
|
}
|
|
3604
3838
|
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Resolve (or advance) ONE dispatched saga compensation at its reserved negative `seq` — the undo
|
|
3841
|
+
* handler is an ordinary remote `@Step`, so this mirrors `callRemote`'s pending/failed/absent
|
|
3842
|
+
* branches (engine.ts's `callRemote`), minus the queue/admission and in-memory-`timeoutMs` paths
|
|
3843
|
+
* (V1: dispatch direct by name, always durable). Returns the wait signal when the run must pause for
|
|
3844
|
+
* this compensation (freshly dispatched, still in flight, or backing off before a durable retry);
|
|
3845
|
+
* `undefined` once it's resolved — completed (crash-safe skip), or retries exhausted/non-retryable
|
|
3846
|
+
* (already visible: `completeRemoteResult` emitted `step.failed` for `compensate:<name>` when that
|
|
3847
|
+
* checkpoint landed — never re-emitted here, and never allowed to block the rest of the unwind or
|
|
3848
|
+
* mask the run's original failure).
|
|
3849
|
+
*/
|
|
3850
|
+
async dispatchCompensation(run, comp, seq, replay) {
|
|
3851
|
+
const { def, args } = comp.dispatch;
|
|
3852
|
+
const name = `compensate:${comp.name}`;
|
|
3853
|
+
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(run.id, seq);
|
|
3854
|
+
if (existing?.status === "completed") return void 0;
|
|
3855
|
+
if (existing?.status === "pending") return {};
|
|
3856
|
+
if (existing?.status === "failed") {
|
|
3857
|
+
const maxAttempts = Math.max(1, def.retries ?? 1);
|
|
3858
|
+
const retryable = existing.error?.retryable !== false;
|
|
3859
|
+
if (!retryable || existing.attempts >= maxAttempts) return void 0;
|
|
3860
|
+
if (existing.wakeAt == null) {
|
|
3861
|
+
const wakeAt = this.clock() + backoffDelay(existing.attempts, def);
|
|
3862
|
+
const withWakeAt = {
|
|
3863
|
+
...existing,
|
|
3864
|
+
wakeAt
|
|
3865
|
+
};
|
|
3866
|
+
await this.store.saveCheckpoint(withWakeAt);
|
|
3867
|
+
replay?.set(seq, withWakeAt);
|
|
3868
|
+
return {
|
|
3869
|
+
wakeAt
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
if (this.clock() < existing.wakeAt) return {
|
|
3873
|
+
wakeAt: existing.wakeAt
|
|
3874
|
+
};
|
|
3875
|
+
await this.dispatchCompensationAttempt(run, seq, name, def, args, existing.attempts + 1, replay);
|
|
3876
|
+
return {};
|
|
3877
|
+
}
|
|
3878
|
+
await this.dispatchCompensationAttempt(run, seq, name, def, args, 1, replay);
|
|
3879
|
+
return {};
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Dispatch (or re-dispatch, on a durable retry) one compensation's undo handler — mirrors
|
|
3883
|
+
* `callRemote`'s dispatch block (persist the `pending` checkpoint BEFORE handing off to the
|
|
3884
|
+
* transport, so a fast result always finds it to complete). Two DIFFERENT names are in play: the
|
|
3885
|
+
* checkpoint's `name` is `compensate:<originalStepName>` (what makes this undo show up on the run's
|
|
3886
|
+
* timeline, and what `completeRemoteResult` emits `step.completed`/`step.failed` under); the WIRE
|
|
3887
|
+
* task's `name` is the undo handler's OWN routing name (`def.name`) — a worker dispatches to the
|
|
3888
|
+
* `@Step` it registered, not to this saga-visibility label.
|
|
3889
|
+
*/
|
|
3890
|
+
async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
|
|
3891
|
+
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
3892
|
+
const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
|
|
3893
|
+
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
3894
|
+
const cp = stepCheckpoint({
|
|
3895
|
+
runId: run.id,
|
|
3896
|
+
seq,
|
|
3897
|
+
name,
|
|
3898
|
+
kind: "remote",
|
|
3899
|
+
status: "pending",
|
|
3900
|
+
input: args,
|
|
3901
|
+
attempts: attempt,
|
|
3902
|
+
workerGroup: group,
|
|
3903
|
+
enqueuedAt,
|
|
3904
|
+
startedAt: enqueuedAt,
|
|
3905
|
+
finishedAt: enqueuedAt
|
|
3906
|
+
});
|
|
3907
|
+
await this.store.saveCheckpoint(cp);
|
|
3908
|
+
replay?.set(seq, cp);
|
|
3909
|
+
await this.pool.dispatch({
|
|
3910
|
+
runId: run.id,
|
|
3911
|
+
seq,
|
|
3912
|
+
name: def.name,
|
|
3913
|
+
stepId: cp.stepId,
|
|
3914
|
+
group,
|
|
3915
|
+
input: args,
|
|
3916
|
+
traceparent: this.traceparent?.(),
|
|
3917
|
+
context: this.context?.(),
|
|
3918
|
+
attempt
|
|
3919
|
+
});
|
|
3920
|
+
this.emit({
|
|
3921
|
+
type: "step.started",
|
|
3922
|
+
runId: run.id,
|
|
3923
|
+
seq,
|
|
3924
|
+
name,
|
|
3925
|
+
kind: "remote"
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3605
3928
|
/** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
|
|
3606
3929
|
* (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
|
|
3607
3930
|
* module and the engine stays the orchestrator. */
|
|
@@ -3627,6 +3950,12 @@ var WorkflowEngine = class {
|
|
|
3627
3950
|
namespace: parentNamespace
|
|
3628
3951
|
}).catch(() => void 0));
|
|
3629
3952
|
}, "startChild"),
|
|
3953
|
+
// Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
|
|
3954
|
+
// idempotent on a terminal/cancelled run (returns its existing status without side effects), so
|
|
3955
|
+
// no extra guard is needed here for the failFast replay case (re-issuing the same cancel calls).
|
|
3956
|
+
cancelChild: /* @__PURE__ */ __name((childId) => {
|
|
3957
|
+
queueMicrotask(() => void this.cancel(childId).catch(() => void 0));
|
|
3958
|
+
}, "cancelChild"),
|
|
3630
3959
|
// Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
|
|
3631
3960
|
upsertSearchAttributes: /* @__PURE__ */ __name(async (runId, attrs) => {
|
|
3632
3961
|
const run = await this.store.getRun(runId);
|
|
@@ -4750,6 +5079,7 @@ var InMemoryTransport = class {
|
|
|
4750
5079
|
DURABLE_OPTIONS_CANONICAL,
|
|
4751
5080
|
DURABLE_STEP_CONFIG,
|
|
4752
5081
|
DURABLE_STEP_NAME,
|
|
5082
|
+
DurableWorkflow,
|
|
4753
5083
|
Entities,
|
|
4754
5084
|
EventAccumulators,
|
|
4755
5085
|
FatalError,
|
|
@@ -4776,9 +5106,11 @@ var InMemoryTransport = class {
|
|
|
4776
5106
|
applyAttributeQuery,
|
|
4777
5107
|
attributeColumnFor,
|
|
4778
5108
|
attributeOperand,
|
|
5109
|
+
bindWorkflowClass,
|
|
4779
5110
|
breakpointToken,
|
|
4780
5111
|
collectMetrics,
|
|
4781
5112
|
createStepLogger,
|
|
5113
|
+
currentWorkflowCtx,
|
|
4782
5114
|
eventMatchOf,
|
|
4783
5115
|
eventMatches,
|
|
4784
5116
|
eventPrefix,
|
|
@@ -4787,6 +5119,7 @@ var InMemoryTransport = class {
|
|
|
4787
5119
|
normalizeAttributeRows,
|
|
4788
5120
|
parseDuration,
|
|
4789
5121
|
prevCronFireMs,
|
|
5122
|
+
runInWorkflowCtx,
|
|
4790
5123
|
runSchedules,
|
|
4791
5124
|
runStepHandler,
|
|
4792
5125
|
sanitizeQueueToken,
|