@dudousxd/nestjs-durable-core 0.72.0 → 0.73.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 +285 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -5
- package/dist/index.d.ts +97 -5
- package/dist/index.js +276 -104
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -46,6 +46,7 @@ __export(index_exports, {
|
|
|
46
46
|
InMemoryTransport: () => InMemoryTransport,
|
|
47
47
|
LEGACY_V1_CAPABILITIES: () => LEGACY_V1_CAPABILITIES,
|
|
48
48
|
LEGACY_V1_PROTOCOL: () => LEGACY_V1_PROTOCOL,
|
|
49
|
+
NestedWorkflowCallError: () => NestedWorkflowCallError,
|
|
49
50
|
NonDeterminismError: () => NonDeterminismError,
|
|
50
51
|
QueueController: () => QueueController,
|
|
51
52
|
RUN_VALUE_FACET_LIMIT: () => RUN_VALUE_FACET_LIMIT,
|
|
@@ -55,6 +56,8 @@ __export(index_exports, {
|
|
|
55
56
|
RemoteWorkflowExecutor: () => RemoteWorkflowExecutor,
|
|
56
57
|
RemoteWorkflowTimeout: () => RemoteWorkflowTimeout,
|
|
57
58
|
RunGateway: () => RunGateway,
|
|
59
|
+
SINGLETON_ACTIVE_STATUSES: () => SINGLETON_ACTIVE_STATUSES,
|
|
60
|
+
SINGLETON_ADMITTED_TAG: () => SINGLETON_ADMITTED_TAG,
|
|
58
61
|
STATE_STORE: () => STATE_STORE,
|
|
59
62
|
STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
|
|
60
63
|
ScheduleAlreadyExistsError: () => ScheduleAlreadyExistsError,
|
|
@@ -85,6 +88,7 @@ __export(index_exports, {
|
|
|
85
88
|
capableWorkers: () => capableWorkers,
|
|
86
89
|
classifyWaiterToken: () => classifyWaiterToken,
|
|
87
90
|
clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
|
|
91
|
+
clearTerminalSingletonAdmission: () => clearTerminalSingletonAdmission,
|
|
88
92
|
collectMetrics: () => collectMetrics,
|
|
89
93
|
concurrencyTag: () => concurrencyTag,
|
|
90
94
|
controlPlaneDescriptor: () => controlPlaneDescriptor,
|
|
@@ -92,6 +96,7 @@ __export(index_exports, {
|
|
|
92
96
|
coveredTokens: () => coveredTokens,
|
|
93
97
|
createStepLogger: () => createStepLogger,
|
|
94
98
|
currentStep: () => currentStep,
|
|
99
|
+
currentStepScope: () => currentStepScope,
|
|
95
100
|
currentWorkflowCtx: () => currentWorkflowCtx,
|
|
96
101
|
debug: () => debug,
|
|
97
102
|
describeWorker: () => describeWorker,
|
|
@@ -133,12 +138,16 @@ __export(index_exports, {
|
|
|
133
138
|
resolveRunWaiting: () => resolveRunWaiting,
|
|
134
139
|
runDurableExecution: () => runDurableExecution,
|
|
135
140
|
runInStepLogger: () => runInStepLogger,
|
|
141
|
+
runInStepScope: () => runInStepScope,
|
|
136
142
|
runInWorkflowCtx: () => runInWorkflowCtx,
|
|
143
|
+
runOutsideWorkflowCtx: () => runOutsideWorkflowCtx,
|
|
137
144
|
runSchedules: () => runSchedules,
|
|
138
145
|
runStepHandler: () => runStepHandler,
|
|
139
146
|
runValueFacetsFromRuns: () => runValueFacetsFromRuns,
|
|
140
147
|
sanitizeQueueToken: () => sanitizeQueueToken,
|
|
141
148
|
scheduledRunId: () => scheduledRunId,
|
|
149
|
+
singletonAdmissionDenialPatch: () => singletonAdmissionDenialPatch,
|
|
150
|
+
singletonAdmissionTags: () => singletonAdmissionTags,
|
|
142
151
|
sqlComparator: () => sqlComparator,
|
|
143
152
|
stepConfigOf: () => stepConfigOf,
|
|
144
153
|
stepId: () => stepId,
|
|
@@ -324,6 +333,27 @@ function currentWorkflowCtx() {
|
|
|
324
333
|
return storage.getStore();
|
|
325
334
|
}
|
|
326
335
|
__name(currentWorkflowCtx, "currentWorkflowCtx");
|
|
336
|
+
var STEP_SCOPE_KEY = Symbol.for("nestjs-durable:ambient-step-scope");
|
|
337
|
+
var stepScopeRef = globalThis;
|
|
338
|
+
if (!stepScopeRef[STEP_SCOPE_KEY]) {
|
|
339
|
+
stepScopeRef[STEP_SCOPE_KEY] = new import_node_async_hooks.AsyncLocalStorage();
|
|
340
|
+
}
|
|
341
|
+
var stepScopeStorage = stepScopeRef[STEP_SCOPE_KEY];
|
|
342
|
+
function runInStepScope(owner, step, fn) {
|
|
343
|
+
return stepScopeStorage.run({
|
|
344
|
+
owner,
|
|
345
|
+
step
|
|
346
|
+
}, fn);
|
|
347
|
+
}
|
|
348
|
+
__name(runInStepScope, "runInStepScope");
|
|
349
|
+
function currentStepScope() {
|
|
350
|
+
return stepScopeStorage.getStore();
|
|
351
|
+
}
|
|
352
|
+
__name(currentStepScope, "currentStepScope");
|
|
353
|
+
function runOutsideWorkflowCtx(fn) {
|
|
354
|
+
return storage.exit(() => stepScopeStorage.exit(fn));
|
|
355
|
+
}
|
|
356
|
+
__name(runOutsideWorkflowCtx, "runOutsideWorkflowCtx");
|
|
327
357
|
|
|
328
358
|
// src/ambient-step.ts
|
|
329
359
|
var import_node_async_hooks2 = require("async_hooks");
|
|
@@ -486,6 +516,21 @@ var NonDeterminismError = class extends Error {
|
|
|
486
516
|
this.seq = seq;
|
|
487
517
|
}
|
|
488
518
|
};
|
|
519
|
+
var NestedWorkflowCallError = class extends FatalError {
|
|
520
|
+
static {
|
|
521
|
+
__name(this, "NestedWorkflowCallError");
|
|
522
|
+
}
|
|
523
|
+
runId;
|
|
524
|
+
primitive;
|
|
525
|
+
step;
|
|
526
|
+
constructor(runId, primitive, step) {
|
|
527
|
+
super(`ctx.${primitive}() was called inside the body of step "${step}" (run ${runId}). Workflow primitives (child/startChild/step/sleep/signals/sideEffect/\u2026, and MyWorkflow.start()/.execute() via the ambient ctx) must be called from the workflow body, never from inside a step body: the step's checkpoint replays without re-running it, so the nested call would shift every later journal position and corrupt the run on replay. Move the call out of the step.`, "nested_workflow_call");
|
|
528
|
+
this.name = "NestedWorkflowCallError";
|
|
529
|
+
this.runId = runId;
|
|
530
|
+
this.primitive = primitive;
|
|
531
|
+
this.step = step;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
489
534
|
var GatherError = class extends Error {
|
|
490
535
|
static {
|
|
491
536
|
__name(this, "GatherError");
|
|
@@ -842,14 +887,19 @@ async function runStepHandler(task, handler) {
|
|
|
842
887
|
} : result, "withEvents");
|
|
843
888
|
try {
|
|
844
889
|
const logger = createStepLogger(events, Date.now);
|
|
845
|
-
const output = await runDurableExecution(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
890
|
+
const output = await runDurableExecution(
|
|
891
|
+
{
|
|
892
|
+
unit: "step",
|
|
893
|
+
runId: task.runId,
|
|
894
|
+
workflow: void 0,
|
|
895
|
+
name: task.name,
|
|
896
|
+
seq: task.seq,
|
|
897
|
+
attempt: task.attempt
|
|
898
|
+
},
|
|
899
|
+
// Outside any ambient workflow ctx: an in-process transport runs the handler on the parent
|
|
900
|
+
// body's async path, but a handler is not the body (see `runOutsideWorkflowCtx`).
|
|
901
|
+
async () => runOutsideWorkflowCtx(() => runInStepLogger(logger, () => handler(task.input, logger)))
|
|
902
|
+
);
|
|
853
903
|
return withEvents({
|
|
854
904
|
...base,
|
|
855
905
|
status: "completed",
|
|
@@ -2399,6 +2449,54 @@ function errorMessage(error2) {
|
|
|
2399
2449
|
}
|
|
2400
2450
|
__name(errorMessage, "errorMessage");
|
|
2401
2451
|
|
|
2452
|
+
// src/singleton-admission.ts
|
|
2453
|
+
var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
|
|
2454
|
+
var SINGLETON_ACTIVE_STATUSES = [
|
|
2455
|
+
"pending",
|
|
2456
|
+
"running",
|
|
2457
|
+
"suspended",
|
|
2458
|
+
"blocked",
|
|
2459
|
+
"cancelling"
|
|
2460
|
+
];
|
|
2461
|
+
function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
2462
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
|
|
2463
|
+
const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
|
|
2464
|
+
const candidate = active.find((run) => run.id === runId);
|
|
2465
|
+
if (!candidate) return null;
|
|
2466
|
+
if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
|
|
2467
|
+
if (candidate.status === "cancelling") return null;
|
|
2468
|
+
const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
|
|
2469
|
+
const remaining = limit - holders.length;
|
|
2470
|
+
if (remaining <= 0) return null;
|
|
2471
|
+
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
2472
|
+
if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
|
|
2473
|
+
return [
|
|
2474
|
+
...candidate.tags ?? [],
|
|
2475
|
+
SINGLETON_ADMITTED_TAG
|
|
2476
|
+
];
|
|
2477
|
+
}
|
|
2478
|
+
__name(singletonAdmissionTags, "singletonAdmissionTags");
|
|
2479
|
+
async function clearTerminalSingletonAdmission(runId, patch, getRun) {
|
|
2480
|
+
if (!patch.status || !TERMINAL_RUN_STATUSES.includes(patch.status)) return patch;
|
|
2481
|
+
const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
|
|
2482
|
+
return {
|
|
2483
|
+
...patch,
|
|
2484
|
+
tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
__name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
|
|
2488
|
+
function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt) {
|
|
2489
|
+
if (retryWakeAt === void 0) return void 0;
|
|
2490
|
+
const candidate = runs.find((run) => run.id === runId && run.workflow === workflow && run.tags?.includes(tag) && SINGLETON_ACTIVE_STATUSES.includes(run.status));
|
|
2491
|
+
if (!candidate || candidate.status === "cancelling") return void 0;
|
|
2492
|
+
return {
|
|
2493
|
+
status: "suspended",
|
|
2494
|
+
wakeAt: retryWakeAt,
|
|
2495
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
__name(singletonAdmissionDenialPatch, "singletonAdmissionDenialPatch");
|
|
2499
|
+
|
|
2402
2500
|
// src/singleton-gate.ts
|
|
2403
2501
|
var SINGLETON_RETRY_MS = 1e3;
|
|
2404
2502
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -2412,6 +2510,9 @@ var SingletonGate = class {
|
|
|
2412
2510
|
}
|
|
2413
2511
|
/** The tag a singleton run carries, so the gate can find others sharing its key. */
|
|
2414
2512
|
tag(cfg, input) {
|
|
2513
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2514
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2515
|
+
}
|
|
2415
2516
|
return `singleton:${cfg.key(input)}`;
|
|
2416
2517
|
}
|
|
2417
2518
|
/** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
|
|
@@ -2420,7 +2521,7 @@ var SingletonGate = class {
|
|
|
2420
2521
|
}
|
|
2421
2522
|
/**
|
|
2422
2523
|
* Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
|
|
2423
|
-
* `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2524
|
+
* `pending`/`running`/`suspended`/`blocked`/`cancelling` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2424
2525
|
* is configured.
|
|
2425
2526
|
*/
|
|
2426
2527
|
async assertCapacity(workflow, cfg, input) {
|
|
@@ -2429,34 +2530,25 @@ var SingletonGate = class {
|
|
|
2429
2530
|
const queued = await this.deps.store.listRuns({
|
|
2430
2531
|
tag: this.tag(cfg, input),
|
|
2431
2532
|
workflow,
|
|
2432
|
-
statuses:
|
|
2433
|
-
"pending",
|
|
2434
|
-
"running",
|
|
2435
|
-
"suspended",
|
|
2436
|
-
"cancelling"
|
|
2437
|
-
]
|
|
2533
|
+
statuses: SINGLETON_ACTIVE_STATUSES
|
|
2438
2534
|
});
|
|
2439
2535
|
if (queued.length >= cap) {
|
|
2440
2536
|
throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
|
|
2441
2537
|
}
|
|
2442
2538
|
}
|
|
2443
|
-
/**
|
|
2444
|
-
* Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
|
|
2445
|
-
* suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
|
|
2446
|
-
* instance the same ordering, so admission is race-free + FIFO.
|
|
2447
|
-
*/
|
|
2539
|
+
/** Preserve durable holders and atomically claim one of the remaining slots. */
|
|
2448
2540
|
async admit(run, cfg) {
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
return
|
|
2541
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2542
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2543
|
+
}
|
|
2544
|
+
const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
|
|
2545
|
+
if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
2546
|
+
run.tags = [
|
|
2547
|
+
...run.tags ?? [],
|
|
2548
|
+
SINGLETON_ADMITTED_TAG
|
|
2549
|
+
];
|
|
2550
|
+
}
|
|
2551
|
+
return admitted;
|
|
2460
2552
|
}
|
|
2461
2553
|
/**
|
|
2462
2554
|
* Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
|
|
@@ -2753,10 +2845,15 @@ var Position = class Position2 {
|
|
|
2753
2845
|
rewind() {
|
|
2754
2846
|
this.seq -= 1;
|
|
2755
2847
|
}
|
|
2848
|
+
/** The position the NEXT primitive would claim, without claiming it. */
|
|
2849
|
+
peek() {
|
|
2850
|
+
return this.seq + 1;
|
|
2851
|
+
}
|
|
2756
2852
|
};
|
|
2757
2853
|
function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttributesSchema) {
|
|
2758
2854
|
const { store, replay } = host;
|
|
2759
2855
|
const pos = new Position();
|
|
2856
|
+
const scopeOwner = {};
|
|
2760
2857
|
const readCheckpoint = /* @__PURE__ */ __name((seq) => {
|
|
2761
2858
|
const hit = replay?.get(seq);
|
|
2762
2859
|
if (hit !== void 0) return hit;
|
|
@@ -2802,7 +2899,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2802
2899
|
};
|
|
2803
2900
|
const body = /* @__PURE__ */ __name(() => {
|
|
2804
2901
|
const logger = createStepLogger(events, host.clock);
|
|
2805
|
-
return runInStepLogger(logger, () => fn(logger));
|
|
2902
|
+
return runInStepScope(scopeOwner, name, () => runInStepLogger(logger, () => fn(logger)));
|
|
2806
2903
|
}, "body");
|
|
2807
2904
|
const output = host.interceptStep ? await host.interceptStep(invocation, body) : await body();
|
|
2808
2905
|
await host.completeStep({
|
|
@@ -2855,7 +2952,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2855
2952
|
}
|
|
2856
2953
|
if (existing && existing.status === "completed") return existing.output;
|
|
2857
2954
|
return store.transaction(async (tx) => {
|
|
2858
|
-
const output = await fn(tx.raw);
|
|
2955
|
+
const output = await runInStepScope(scopeOwner, name, () => fn(tx.raw));
|
|
2859
2956
|
const cp = instantCheckpoint({
|
|
2860
2957
|
runId,
|
|
2861
2958
|
seq: current,
|
|
@@ -3364,30 +3461,49 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3364
3461
|
});
|
|
3365
3462
|
}
|
|
3366
3463
|
__name(step, "step");
|
|
3464
|
+
const nestedStep = /* @__PURE__ */ __name(() => {
|
|
3465
|
+
const scope = currentStepScope();
|
|
3466
|
+
return scope !== void 0 && scope.owner === scopeOwner ? scope.step : void 0;
|
|
3467
|
+
}, "nestedStep");
|
|
3468
|
+
const guard = /* @__PURE__ */ __name((primitive, fn) => (...args) => {
|
|
3469
|
+
const stepName = nestedStep();
|
|
3470
|
+
if (stepName === void 0) return fn(...args);
|
|
3471
|
+
return (async () => {
|
|
3472
|
+
if (await readCheckpoint(pos.peek())) return fn(...args);
|
|
3473
|
+
throw new NestedWorkflowCallError(runId, primitive, stepName);
|
|
3474
|
+
})();
|
|
3475
|
+
}, "guard");
|
|
3476
|
+
const guardedWebhook = /* @__PURE__ */ __name(() => {
|
|
3477
|
+
const stepName = nestedStep();
|
|
3478
|
+
if (stepName !== void 0 && !replay?.get(pos.peek())) {
|
|
3479
|
+
throw new NestedWorkflowCallError(runId, "webhook", stepName);
|
|
3480
|
+
}
|
|
3481
|
+
return webhook();
|
|
3482
|
+
}, "guardedWebhook");
|
|
3367
3483
|
return {
|
|
3368
3484
|
runId,
|
|
3369
|
-
step,
|
|
3370
|
-
localStep,
|
|
3371
|
-
upsertSearchAttributes,
|
|
3372
|
-
transaction,
|
|
3373
|
-
callEntity,
|
|
3374
|
-
signalEntity,
|
|
3375
|
-
sleep,
|
|
3376
|
-
sleepUntil,
|
|
3485
|
+
step: guard("step", step),
|
|
3486
|
+
localStep: guard("localStep", localStep),
|
|
3487
|
+
upsertSearchAttributes: guard("upsertSearchAttributes", upsertSearchAttributes),
|
|
3488
|
+
transaction: guard("transaction", transaction),
|
|
3489
|
+
callEntity: guard("callEntity", callEntity),
|
|
3490
|
+
signalEntity: guard("signalEntity", signalEntity),
|
|
3491
|
+
sleep: guard("sleep", sleep),
|
|
3492
|
+
sleepUntil: guard("sleepUntil", sleepUntil),
|
|
3377
3493
|
continueAsNew,
|
|
3378
|
-
waitForSignal,
|
|
3379
|
-
waitForEvent,
|
|
3380
|
-
task,
|
|
3381
|
-
child,
|
|
3382
|
-
all,
|
|
3383
|
-
startChild,
|
|
3384
|
-
breakpoint,
|
|
3385
|
-
webhook,
|
|
3386
|
-
setEvent,
|
|
3387
|
-
onUpdate,
|
|
3388
|
-
patched,
|
|
3389
|
-
now,
|
|
3390
|
-
sideEffect
|
|
3494
|
+
waitForSignal: guard("waitForSignal", waitForSignal),
|
|
3495
|
+
waitForEvent: guard("waitForEvent", waitForEvent),
|
|
3496
|
+
task: guard("task", task),
|
|
3497
|
+
child: guard("child", child),
|
|
3498
|
+
all: guard("all", all),
|
|
3499
|
+
startChild: guard("startChild", startChild),
|
|
3500
|
+
breakpoint: guard("breakpoint", breakpoint),
|
|
3501
|
+
webhook: guardedWebhook,
|
|
3502
|
+
setEvent: guard("setEvent", setEvent),
|
|
3503
|
+
onUpdate: guard("onUpdate", onUpdate),
|
|
3504
|
+
patched: guard("patched", patched),
|
|
3505
|
+
now: guard("now", now),
|
|
3506
|
+
sideEffect: guard("sideEffect", sideEffect)
|
|
3391
3507
|
};
|
|
3392
3508
|
}
|
|
3393
3509
|
__name(createWorkflowCtx, "createWorkflowCtx");
|
|
@@ -3949,7 +4065,7 @@ var WorkflowEngine = class {
|
|
|
3949
4065
|
VERSION_UNDECLARED_TAG
|
|
3950
4066
|
] : []
|
|
3951
4067
|
];
|
|
3952
|
-
const tags = mergeTags(registered.tags, stampedTags);
|
|
4068
|
+
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
3953
4069
|
if (registered.singleton) {
|
|
3954
4070
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
3955
4071
|
}
|
|
@@ -3989,10 +4105,30 @@ var WorkflowEngine = class {
|
|
|
3989
4105
|
getRun(runId) {
|
|
3990
4106
|
return this.store.getRun(runId);
|
|
3991
4107
|
}
|
|
3992
|
-
|
|
4108
|
+
/**
|
|
4109
|
+
* Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
|
|
4110
|
+
* driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
|
|
4111
|
+
* also resets the failure state so replay re-attempts the failed parts). Completed / cancelled /
|
|
4112
|
+
* dead runs are never re-executed. The engine's own implicit wake-ups (signals, step results,
|
|
4113
|
+
* timers, redelivered dispatches) go through {@link resumeImplicit}, which treats `failed` as
|
|
4114
|
+
* terminal too.
|
|
4115
|
+
*/
|
|
4116
|
+
resume(runId) {
|
|
4117
|
+
return this.resumeRun(runId, true);
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* {@link resume} for every IMPLICIT wake-up — a delivered signal/child completion, a late step
|
|
4121
|
+
* result, a due timer, a redelivered dispatch, a queue-slot wake. A `failed` run is terminal to
|
|
4122
|
+
* these: only an explicit retry ({@link requeue} / {@link resume}) may re-drive it. Otherwise a
|
|
4123
|
+
* stale waiter or a late result left behind by a failed run silently re-executes its body.
|
|
4124
|
+
*/
|
|
4125
|
+
resumeImplicit(runId) {
|
|
4126
|
+
return this.resumeRun(runId, false);
|
|
4127
|
+
}
|
|
4128
|
+
async resumeRun(runId, allowFailed) {
|
|
3993
4129
|
const run = await this.store.getRun(runId);
|
|
3994
4130
|
if (!run) throw new Error(`run ${runId} not found`);
|
|
3995
|
-
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") {
|
|
4131
|
+
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead" || run.status === "failed" && !allowFailed) {
|
|
3996
4132
|
return {
|
|
3997
4133
|
runId,
|
|
3998
4134
|
status: run.status,
|
|
@@ -4004,6 +4140,12 @@ var WorkflowEngine = class {
|
|
|
4004
4140
|
await this.store.releaseRunLock(runId);
|
|
4005
4141
|
throw new NamespaceMismatch();
|
|
4006
4142
|
}
|
|
4143
|
+
if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
4144
|
+
run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
4145
|
+
await this.store.updateRun(run.id, {
|
|
4146
|
+
tags: run.tags
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4007
4149
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
4008
4150
|
if (!registered) {
|
|
4009
4151
|
throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
|
|
@@ -4153,7 +4295,10 @@ var WorkflowEngine = class {
|
|
|
4153
4295
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
4154
4296
|
track(p) {
|
|
4155
4297
|
this.inflight.add(p);
|
|
4156
|
-
|
|
4298
|
+
const done = /* @__PURE__ */ __name(() => {
|
|
4299
|
+
this.inflight.delete(p);
|
|
4300
|
+
}, "done");
|
|
4301
|
+
void p.then(done, done);
|
|
4157
4302
|
return p;
|
|
4158
4303
|
}
|
|
4159
4304
|
/**
|
|
@@ -4441,7 +4586,7 @@ var WorkflowEngine = class {
|
|
|
4441
4586
|
const nowMs = this.clock();
|
|
4442
4587
|
const acquired = await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
4443
4588
|
if (!acquired) return null;
|
|
4444
|
-
return this.
|
|
4589
|
+
return this.resumeImplicit(runId).catch((err) => {
|
|
4445
4590
|
if (err instanceof NamespaceMismatch) return null;
|
|
4446
4591
|
throw err;
|
|
4447
4592
|
});
|
|
@@ -4504,7 +4649,8 @@ var WorkflowEngine = class {
|
|
|
4504
4649
|
status: "pending",
|
|
4505
4650
|
error: void 0,
|
|
4506
4651
|
...resurrecting ? {
|
|
4507
|
-
recoveryAttempts: 0
|
|
4652
|
+
recoveryAttempts: 0,
|
|
4653
|
+
tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
4508
4654
|
} : {},
|
|
4509
4655
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4510
4656
|
});
|
|
@@ -4623,10 +4769,19 @@ var WorkflowEngine = class {
|
|
|
4623
4769
|
off();
|
|
4624
4770
|
resolve(toResult(run));
|
|
4625
4771
|
}, "finish");
|
|
4772
|
+
const fail = /* @__PURE__ */ __name((error2) => {
|
|
4773
|
+
if (done) return;
|
|
4774
|
+
done = true;
|
|
4775
|
+
if (timer) clearTimeout(timer);
|
|
4776
|
+
if (poller) clearInterval(poller);
|
|
4777
|
+
off();
|
|
4778
|
+
reject(error2);
|
|
4779
|
+
}, "fail");
|
|
4626
4780
|
const check = /* @__PURE__ */ __name(() => {
|
|
4781
|
+
if (done) return;
|
|
4627
4782
|
void this.store.getRun(runId).then((run) => {
|
|
4628
4783
|
if (run && isSettled(run)) finish(run);
|
|
4629
|
-
});
|
|
4784
|
+
}, fail);
|
|
4630
4785
|
}, "check");
|
|
4631
4786
|
const poller = opts?.until === "terminal" ? (() => {
|
|
4632
4787
|
const interval = setInterval(check, 250);
|
|
@@ -4662,7 +4817,7 @@ var WorkflowEngine = class {
|
|
|
4662
4817
|
if (!acquired) continue;
|
|
4663
4818
|
try {
|
|
4664
4819
|
const settled = onLocked ? await onLocked(run) : void 0;
|
|
4665
|
-
results.push(settled ?? await this.
|
|
4820
|
+
results.push(settled ?? await this.resumeImplicit(run.id));
|
|
4666
4821
|
} catch (error2) {
|
|
4667
4822
|
console.error(`[nestjs-durable] could not resume run ${run.id} (${run.workflow}@${run.workflowVersion}); leaving it for another instance:`, error2);
|
|
4668
4823
|
}
|
|
@@ -4759,7 +4914,7 @@ var WorkflowEngine = class {
|
|
|
4759
4914
|
// for an ordinary (non-fan) signal/child await.
|
|
4760
4915
|
parallelGroup: waiter.parallelGroup
|
|
4761
4916
|
}));
|
|
4762
|
-
return this.
|
|
4917
|
+
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
4763
4918
|
if (err instanceof NamespaceMismatch) return null;
|
|
4764
4919
|
throw err;
|
|
4765
4920
|
});
|
|
@@ -4875,7 +5030,7 @@ var WorkflowEngine = class {
|
|
|
4875
5030
|
async cancel(runId, opts) {
|
|
4876
5031
|
const run = await this.store.getRun(runId);
|
|
4877
5032
|
if (!run) return null;
|
|
4878
|
-
if (
|
|
5033
|
+
if (TERMINAL_RUN_STATUSES.includes(run.status)) {
|
|
4879
5034
|
return {
|
|
4880
5035
|
runId,
|
|
4881
5036
|
status: run.status,
|
|
@@ -4904,7 +5059,7 @@ var WorkflowEngine = class {
|
|
|
4904
5059
|
}).catch(() => void 0);
|
|
4905
5060
|
}
|
|
4906
5061
|
queueMicrotask(() => {
|
|
4907
|
-
void this.
|
|
5062
|
+
void this.resumeImplicit(runId).then(() => this.notifyCancelled(runId)).catch(() => void 0);
|
|
4908
5063
|
});
|
|
4909
5064
|
await this.cancelChildren(runId, opts);
|
|
4910
5065
|
return {
|
|
@@ -5409,6 +5564,26 @@ var WorkflowEngine = class {
|
|
|
5409
5564
|
durationMs: Date.now() - step.startedAt.getTime()
|
|
5410
5565
|
});
|
|
5411
5566
|
}
|
|
5567
|
+
/** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
|
|
5568
|
+
async singletonWaitResult(run) {
|
|
5569
|
+
const current = await this.store.getRun(run.id);
|
|
5570
|
+
await this.store.releaseRunLock(run.id);
|
|
5571
|
+
if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
|
|
5572
|
+
if (current.status === "suspended") {
|
|
5573
|
+
this.emit({
|
|
5574
|
+
type: "run.suspended",
|
|
5575
|
+
runId: current.id,
|
|
5576
|
+
workflow: current.workflow,
|
|
5577
|
+
namespace: current.namespace
|
|
5578
|
+
});
|
|
5579
|
+
}
|
|
5580
|
+
return {
|
|
5581
|
+
runId: current.id,
|
|
5582
|
+
status: current.status,
|
|
5583
|
+
output: current.output,
|
|
5584
|
+
error: current.error
|
|
5585
|
+
};
|
|
5586
|
+
}
|
|
5412
5587
|
async execute(run, fn) {
|
|
5413
5588
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
5414
5589
|
const lockNow = this.clock();
|
|
@@ -5578,23 +5753,7 @@ var WorkflowEngine = class {
|
|
|
5578
5753
|
});
|
|
5579
5754
|
}
|
|
5580
5755
|
if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
5581
|
-
|
|
5582
|
-
await this.store.updateRun(run.id, {
|
|
5583
|
-
status: "suspended",
|
|
5584
|
-
wakeAt,
|
|
5585
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5586
|
-
});
|
|
5587
|
-
this.emit({
|
|
5588
|
-
type: "run.suspended",
|
|
5589
|
-
runId: run.id,
|
|
5590
|
-
workflow: run.workflow,
|
|
5591
|
-
namespace: run.namespace
|
|
5592
|
-
});
|
|
5593
|
-
await this.store.releaseRunLock(run.id);
|
|
5594
|
-
return {
|
|
5595
|
-
runId: run.id,
|
|
5596
|
-
status: "suspended"
|
|
5597
|
-
};
|
|
5756
|
+
return this.singletonWaitResult(run);
|
|
5598
5757
|
}
|
|
5599
5758
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
5600
5759
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
@@ -5739,7 +5898,7 @@ var WorkflowEngine = class {
|
|
|
5739
5898
|
const checkpoint = await this.store.getCheckpoint(runId, cmd.seq);
|
|
5740
5899
|
if (checkpoint && checkpoint.status !== "pending") {
|
|
5741
5900
|
setTimeout(() => {
|
|
5742
|
-
void this.
|
|
5901
|
+
void this.resumeImplicit(runId).catch(() => void 0);
|
|
5743
5902
|
}, 0).unref?.();
|
|
5744
5903
|
return;
|
|
5745
5904
|
}
|
|
@@ -6081,7 +6240,7 @@ var WorkflowEngine = class {
|
|
|
6081
6240
|
kind: "signal",
|
|
6082
6241
|
output: payload
|
|
6083
6242
|
}));
|
|
6084
|
-
setTimeout(() => void this.
|
|
6243
|
+
setTimeout(() => void this.resumeImplicit(run.id).catch(() => void 0), 0);
|
|
6085
6244
|
}, "deliverBuffered");
|
|
6086
6245
|
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
6087
6246
|
if (buffered) {
|
|
@@ -6208,23 +6367,7 @@ var WorkflowEngine = class {
|
|
|
6208
6367
|
});
|
|
6209
6368
|
}
|
|
6210
6369
|
if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
6211
|
-
|
|
6212
|
-
await this.store.updateRun(run.id, {
|
|
6213
|
-
status: "suspended",
|
|
6214
|
-
wakeAt,
|
|
6215
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
6216
|
-
});
|
|
6217
|
-
this.emit({
|
|
6218
|
-
type: "run.suspended",
|
|
6219
|
-
runId: run.id,
|
|
6220
|
-
workflow: run.workflow,
|
|
6221
|
-
namespace: run.namespace
|
|
6222
|
-
});
|
|
6223
|
-
await this.store.releaseRunLock(run.id);
|
|
6224
|
-
return {
|
|
6225
|
-
runId: run.id,
|
|
6226
|
-
status: "suspended"
|
|
6227
|
-
};
|
|
6370
|
+
return this.singletonWaitResult(run);
|
|
6228
6371
|
}
|
|
6229
6372
|
const compensations = [];
|
|
6230
6373
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
@@ -6895,7 +7038,7 @@ var WorkflowEngine = class {
|
|
|
6895
7038
|
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
6896
7039
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
6897
7040
|
});
|
|
6898
|
-
await this.
|
|
7041
|
+
await this.resumeImplicit(result.runId).catch((err) => {
|
|
6899
7042
|
if (err instanceof NamespaceMismatch) return null;
|
|
6900
7043
|
throw err;
|
|
6901
7044
|
});
|
|
@@ -6996,7 +7139,7 @@ var WorkflowEngine = class {
|
|
|
6996
7139
|
...waiters
|
|
6997
7140
|
];
|
|
6998
7141
|
waiters.clear();
|
|
6999
|
-
for (const runId of runIds) void this.
|
|
7142
|
+
for (const runId of runIds) void this.resumeImplicit(runId).catch(() => void 0);
|
|
7000
7143
|
}
|
|
7001
7144
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
7002
7145
|
async releaseQueueSlot(id) {
|
|
@@ -7243,6 +7386,8 @@ var CodecStateStore = class {
|
|
|
7243
7386
|
}
|
|
7244
7387
|
inner;
|
|
7245
7388
|
codec;
|
|
7389
|
+
/** Atomic claims touch only searchable metadata and bypass payload encoding. */
|
|
7390
|
+
tryAdmitSingleton;
|
|
7246
7391
|
/**
|
|
7247
7392
|
* Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
|
|
7248
7393
|
* (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
|
|
@@ -7275,6 +7420,7 @@ var CodecStateStore = class {
|
|
|
7275
7420
|
constructor(inner, codec) {
|
|
7276
7421
|
this.inner = inner;
|
|
7277
7422
|
this.codec = codec;
|
|
7423
|
+
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
7278
7424
|
const deleteRuns = inner.deleteRuns;
|
|
7279
7425
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7280
7426
|
const prune = inner.pruneTerminalRuns;
|
|
@@ -7727,6 +7873,28 @@ var InMemoryStateStore = class {
|
|
|
7727
7873
|
byRun.set(runId, row);
|
|
7728
7874
|
}
|
|
7729
7875
|
}
|
|
7876
|
+
async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
|
|
7877
|
+
const runs = [
|
|
7878
|
+
...this.runs.values()
|
|
7879
|
+
];
|
|
7880
|
+
const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
|
|
7881
|
+
if (!tags) {
|
|
7882
|
+
const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
|
|
7883
|
+
const candidate = this.runs.get(runId);
|
|
7884
|
+
if (patch && candidate) this.runs.set(runId, {
|
|
7885
|
+
...candidate,
|
|
7886
|
+
...patch
|
|
7887
|
+
});
|
|
7888
|
+
return false;
|
|
7889
|
+
}
|
|
7890
|
+
const run = this.runs.get(runId);
|
|
7891
|
+
if (!run) return false;
|
|
7892
|
+
this.runs.set(runId, {
|
|
7893
|
+
...run,
|
|
7894
|
+
tags
|
|
7895
|
+
});
|
|
7896
|
+
return true;
|
|
7897
|
+
}
|
|
7730
7898
|
async createRun(run) {
|
|
7731
7899
|
this.runs.set(run.id, {
|
|
7732
7900
|
...run,
|
|
@@ -7734,7 +7902,11 @@ var InMemoryStateStore = class {
|
|
|
7734
7902
|
});
|
|
7735
7903
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
7736
7904
|
}
|
|
7737
|
-
async updateRun(runId,
|
|
7905
|
+
async updateRun(runId, inputPatch) {
|
|
7906
|
+
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
7907
|
+
...inputPatch,
|
|
7908
|
+
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
7909
|
+
} : inputPatch;
|
|
7738
7910
|
const existing = this.runs.get(runId);
|
|
7739
7911
|
if (!existing) throw new Error(`run ${runId} not found`);
|
|
7740
7912
|
const next = {
|
|
@@ -8149,6 +8321,7 @@ var InMemoryTransport = class {
|
|
|
8149
8321
|
InMemoryTransport,
|
|
8150
8322
|
LEGACY_V1_CAPABILITIES,
|
|
8151
8323
|
LEGACY_V1_PROTOCOL,
|
|
8324
|
+
NestedWorkflowCallError,
|
|
8152
8325
|
NonDeterminismError,
|
|
8153
8326
|
QueueController,
|
|
8154
8327
|
RUN_VALUE_FACET_LIMIT,
|
|
@@ -8158,6 +8331,8 @@ var InMemoryTransport = class {
|
|
|
8158
8331
|
RemoteWorkflowExecutor,
|
|
8159
8332
|
RemoteWorkflowTimeout,
|
|
8160
8333
|
RunGateway,
|
|
8334
|
+
SINGLETON_ACTIVE_STATUSES,
|
|
8335
|
+
SINGLETON_ADMITTED_TAG,
|
|
8161
8336
|
STATE_STORE,
|
|
8162
8337
|
STATE_STORE_CANONICAL,
|
|
8163
8338
|
ScheduleAlreadyExistsError,
|
|
@@ -8188,6 +8363,7 @@ var InMemoryTransport = class {
|
|
|
8188
8363
|
capableWorkers,
|
|
8189
8364
|
classifyWaiterToken,
|
|
8190
8365
|
clearDurableExecutionWrappers,
|
|
8366
|
+
clearTerminalSingletonAdmission,
|
|
8191
8367
|
collectMetrics,
|
|
8192
8368
|
concurrencyTag,
|
|
8193
8369
|
controlPlaneDescriptor,
|
|
@@ -8195,6 +8371,7 @@ var InMemoryTransport = class {
|
|
|
8195
8371
|
coveredTokens,
|
|
8196
8372
|
createStepLogger,
|
|
8197
8373
|
currentStep,
|
|
8374
|
+
currentStepScope,
|
|
8198
8375
|
currentWorkflowCtx,
|
|
8199
8376
|
debug,
|
|
8200
8377
|
describeWorker,
|
|
@@ -8236,12 +8413,16 @@ var InMemoryTransport = class {
|
|
|
8236
8413
|
resolveRunWaiting,
|
|
8237
8414
|
runDurableExecution,
|
|
8238
8415
|
runInStepLogger,
|
|
8416
|
+
runInStepScope,
|
|
8239
8417
|
runInWorkflowCtx,
|
|
8418
|
+
runOutsideWorkflowCtx,
|
|
8240
8419
|
runSchedules,
|
|
8241
8420
|
runStepHandler,
|
|
8242
8421
|
runValueFacetsFromRuns,
|
|
8243
8422
|
sanitizeQueueToken,
|
|
8244
8423
|
scheduledRunId,
|
|
8424
|
+
singletonAdmissionDenialPatch,
|
|
8425
|
+
singletonAdmissionTags,
|
|
8245
8426
|
sqlComparator,
|
|
8246
8427
|
stepConfigOf,
|
|
8247
8428
|
stepId,
|