@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.js
CHANGED
|
@@ -172,6 +172,27 @@ function currentWorkflowCtx() {
|
|
|
172
172
|
return storage.getStore();
|
|
173
173
|
}
|
|
174
174
|
__name(currentWorkflowCtx, "currentWorkflowCtx");
|
|
175
|
+
var STEP_SCOPE_KEY = Symbol.for("nestjs-durable:ambient-step-scope");
|
|
176
|
+
var stepScopeRef = globalThis;
|
|
177
|
+
if (!stepScopeRef[STEP_SCOPE_KEY]) {
|
|
178
|
+
stepScopeRef[STEP_SCOPE_KEY] = new AsyncLocalStorage();
|
|
179
|
+
}
|
|
180
|
+
var stepScopeStorage = stepScopeRef[STEP_SCOPE_KEY];
|
|
181
|
+
function runInStepScope(owner, step, fn) {
|
|
182
|
+
return stepScopeStorage.run({
|
|
183
|
+
owner,
|
|
184
|
+
step
|
|
185
|
+
}, fn);
|
|
186
|
+
}
|
|
187
|
+
__name(runInStepScope, "runInStepScope");
|
|
188
|
+
function currentStepScope() {
|
|
189
|
+
return stepScopeStorage.getStore();
|
|
190
|
+
}
|
|
191
|
+
__name(currentStepScope, "currentStepScope");
|
|
192
|
+
function runOutsideWorkflowCtx(fn) {
|
|
193
|
+
return storage.exit(() => stepScopeStorage.exit(fn));
|
|
194
|
+
}
|
|
195
|
+
__name(runOutsideWorkflowCtx, "runOutsideWorkflowCtx");
|
|
175
196
|
|
|
176
197
|
// src/ambient-step.ts
|
|
177
198
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
@@ -334,6 +355,21 @@ var NonDeterminismError = class extends Error {
|
|
|
334
355
|
this.seq = seq;
|
|
335
356
|
}
|
|
336
357
|
};
|
|
358
|
+
var NestedWorkflowCallError = class extends FatalError {
|
|
359
|
+
static {
|
|
360
|
+
__name(this, "NestedWorkflowCallError");
|
|
361
|
+
}
|
|
362
|
+
runId;
|
|
363
|
+
primitive;
|
|
364
|
+
step;
|
|
365
|
+
constructor(runId, primitive, step) {
|
|
366
|
+
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");
|
|
367
|
+
this.name = "NestedWorkflowCallError";
|
|
368
|
+
this.runId = runId;
|
|
369
|
+
this.primitive = primitive;
|
|
370
|
+
this.step = step;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
337
373
|
var GatherError = class extends Error {
|
|
338
374
|
static {
|
|
339
375
|
__name(this, "GatherError");
|
|
@@ -690,14 +726,19 @@ async function runStepHandler(task, handler) {
|
|
|
690
726
|
} : result, "withEvents");
|
|
691
727
|
try {
|
|
692
728
|
const logger = createStepLogger(events, Date.now);
|
|
693
|
-
const output = await runDurableExecution(
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
729
|
+
const output = await runDurableExecution(
|
|
730
|
+
{
|
|
731
|
+
unit: "step",
|
|
732
|
+
runId: task.runId,
|
|
733
|
+
workflow: void 0,
|
|
734
|
+
name: task.name,
|
|
735
|
+
seq: task.seq,
|
|
736
|
+
attempt: task.attempt
|
|
737
|
+
},
|
|
738
|
+
// Outside any ambient workflow ctx: an in-process transport runs the handler on the parent
|
|
739
|
+
// body's async path, but a handler is not the body (see `runOutsideWorkflowCtx`).
|
|
740
|
+
async () => runOutsideWorkflowCtx(() => runInStepLogger(logger, () => handler(task.input, logger)))
|
|
741
|
+
);
|
|
701
742
|
return withEvents({
|
|
702
743
|
...base,
|
|
703
744
|
status: "completed",
|
|
@@ -2247,6 +2288,54 @@ function errorMessage(error2) {
|
|
|
2247
2288
|
}
|
|
2248
2289
|
__name(errorMessage, "errorMessage");
|
|
2249
2290
|
|
|
2291
|
+
// src/singleton-admission.ts
|
|
2292
|
+
var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
|
|
2293
|
+
var SINGLETON_ACTIVE_STATUSES = [
|
|
2294
|
+
"pending",
|
|
2295
|
+
"running",
|
|
2296
|
+
"suspended",
|
|
2297
|
+
"blocked",
|
|
2298
|
+
"cancelling"
|
|
2299
|
+
];
|
|
2300
|
+
function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
2301
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
|
|
2302
|
+
const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
|
|
2303
|
+
const candidate = active.find((run) => run.id === runId);
|
|
2304
|
+
if (!candidate) return null;
|
|
2305
|
+
if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
|
|
2306
|
+
if (candidate.status === "cancelling") return null;
|
|
2307
|
+
const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
|
|
2308
|
+
const remaining = limit - holders.length;
|
|
2309
|
+
if (remaining <= 0) return null;
|
|
2310
|
+
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));
|
|
2311
|
+
if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
|
|
2312
|
+
return [
|
|
2313
|
+
...candidate.tags ?? [],
|
|
2314
|
+
SINGLETON_ADMITTED_TAG
|
|
2315
|
+
];
|
|
2316
|
+
}
|
|
2317
|
+
__name(singletonAdmissionTags, "singletonAdmissionTags");
|
|
2318
|
+
async function clearTerminalSingletonAdmission(runId, patch, getRun) {
|
|
2319
|
+
if (!patch.status || !TERMINAL_RUN_STATUSES.includes(patch.status)) return patch;
|
|
2320
|
+
const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
|
|
2321
|
+
return {
|
|
2322
|
+
...patch,
|
|
2323
|
+
tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
__name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
|
|
2327
|
+
function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt) {
|
|
2328
|
+
if (retryWakeAt === void 0) return void 0;
|
|
2329
|
+
const candidate = runs.find((run) => run.id === runId && run.workflow === workflow && run.tags?.includes(tag) && SINGLETON_ACTIVE_STATUSES.includes(run.status));
|
|
2330
|
+
if (!candidate || candidate.status === "cancelling") return void 0;
|
|
2331
|
+
return {
|
|
2332
|
+
status: "suspended",
|
|
2333
|
+
wakeAt: retryWakeAt,
|
|
2334
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
__name(singletonAdmissionDenialPatch, "singletonAdmissionDenialPatch");
|
|
2338
|
+
|
|
2250
2339
|
// src/singleton-gate.ts
|
|
2251
2340
|
var SINGLETON_RETRY_MS = 1e3;
|
|
2252
2341
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -2260,6 +2349,9 @@ var SingletonGate = class {
|
|
|
2260
2349
|
}
|
|
2261
2350
|
/** The tag a singleton run carries, so the gate can find others sharing its key. */
|
|
2262
2351
|
tag(cfg, input) {
|
|
2352
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2353
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2354
|
+
}
|
|
2263
2355
|
return `singleton:${cfg.key(input)}`;
|
|
2264
2356
|
}
|
|
2265
2357
|
/** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
|
|
@@ -2268,7 +2360,7 @@ var SingletonGate = class {
|
|
|
2268
2360
|
}
|
|
2269
2361
|
/**
|
|
2270
2362
|
* Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
|
|
2271
|
-
* `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2363
|
+
* `pending`/`running`/`suspended`/`blocked`/`cancelling` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2272
2364
|
* is configured.
|
|
2273
2365
|
*/
|
|
2274
2366
|
async assertCapacity(workflow, cfg, input) {
|
|
@@ -2277,34 +2369,25 @@ var SingletonGate = class {
|
|
|
2277
2369
|
const queued = await this.deps.store.listRuns({
|
|
2278
2370
|
tag: this.tag(cfg, input),
|
|
2279
2371
|
workflow,
|
|
2280
|
-
statuses:
|
|
2281
|
-
"pending",
|
|
2282
|
-
"running",
|
|
2283
|
-
"suspended",
|
|
2284
|
-
"cancelling"
|
|
2285
|
-
]
|
|
2372
|
+
statuses: SINGLETON_ACTIVE_STATUSES
|
|
2286
2373
|
});
|
|
2287
2374
|
if (queued.length >= cap) {
|
|
2288
2375
|
throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
|
|
2289
2376
|
}
|
|
2290
2377
|
}
|
|
2291
|
-
/**
|
|
2292
|
-
* Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
|
|
2293
|
-
* suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
|
|
2294
|
-
* instance the same ordering, so admission is race-free + FIFO.
|
|
2295
|
-
*/
|
|
2378
|
+
/** Preserve durable holders and atomically claim one of the remaining slots. */
|
|
2296
2379
|
async admit(run, cfg) {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
return
|
|
2380
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2381
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2382
|
+
}
|
|
2383
|
+
const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
|
|
2384
|
+
if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
2385
|
+
run.tags = [
|
|
2386
|
+
...run.tags ?? [],
|
|
2387
|
+
SINGLETON_ADMITTED_TAG
|
|
2388
|
+
];
|
|
2389
|
+
}
|
|
2390
|
+
return admitted;
|
|
2308
2391
|
}
|
|
2309
2392
|
/**
|
|
2310
2393
|
* Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
|
|
@@ -2601,10 +2684,15 @@ var Position = class Position2 {
|
|
|
2601
2684
|
rewind() {
|
|
2602
2685
|
this.seq -= 1;
|
|
2603
2686
|
}
|
|
2687
|
+
/** The position the NEXT primitive would claim, without claiming it. */
|
|
2688
|
+
peek() {
|
|
2689
|
+
return this.seq + 1;
|
|
2690
|
+
}
|
|
2604
2691
|
};
|
|
2605
2692
|
function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttributesSchema) {
|
|
2606
2693
|
const { store, replay } = host;
|
|
2607
2694
|
const pos = new Position();
|
|
2695
|
+
const scopeOwner = {};
|
|
2608
2696
|
const readCheckpoint = /* @__PURE__ */ __name((seq) => {
|
|
2609
2697
|
const hit = replay?.get(seq);
|
|
2610
2698
|
if (hit !== void 0) return hit;
|
|
@@ -2650,7 +2738,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2650
2738
|
};
|
|
2651
2739
|
const body = /* @__PURE__ */ __name(() => {
|
|
2652
2740
|
const logger = createStepLogger(events, host.clock);
|
|
2653
|
-
return runInStepLogger(logger, () => fn(logger));
|
|
2741
|
+
return runInStepScope(scopeOwner, name, () => runInStepLogger(logger, () => fn(logger)));
|
|
2654
2742
|
}, "body");
|
|
2655
2743
|
const output = host.interceptStep ? await host.interceptStep(invocation, body) : await body();
|
|
2656
2744
|
await host.completeStep({
|
|
@@ -2703,7 +2791,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2703
2791
|
}
|
|
2704
2792
|
if (existing && existing.status === "completed") return existing.output;
|
|
2705
2793
|
return store.transaction(async (tx) => {
|
|
2706
|
-
const output = await fn(tx.raw);
|
|
2794
|
+
const output = await runInStepScope(scopeOwner, name, () => fn(tx.raw));
|
|
2707
2795
|
const cp = instantCheckpoint({
|
|
2708
2796
|
runId,
|
|
2709
2797
|
seq: current,
|
|
@@ -3212,30 +3300,49 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3212
3300
|
});
|
|
3213
3301
|
}
|
|
3214
3302
|
__name(step, "step");
|
|
3303
|
+
const nestedStep = /* @__PURE__ */ __name(() => {
|
|
3304
|
+
const scope = currentStepScope();
|
|
3305
|
+
return scope !== void 0 && scope.owner === scopeOwner ? scope.step : void 0;
|
|
3306
|
+
}, "nestedStep");
|
|
3307
|
+
const guard = /* @__PURE__ */ __name((primitive, fn) => (...args) => {
|
|
3308
|
+
const stepName = nestedStep();
|
|
3309
|
+
if (stepName === void 0) return fn(...args);
|
|
3310
|
+
return (async () => {
|
|
3311
|
+
if (await readCheckpoint(pos.peek())) return fn(...args);
|
|
3312
|
+
throw new NestedWorkflowCallError(runId, primitive, stepName);
|
|
3313
|
+
})();
|
|
3314
|
+
}, "guard");
|
|
3315
|
+
const guardedWebhook = /* @__PURE__ */ __name(() => {
|
|
3316
|
+
const stepName = nestedStep();
|
|
3317
|
+
if (stepName !== void 0 && !replay?.get(pos.peek())) {
|
|
3318
|
+
throw new NestedWorkflowCallError(runId, "webhook", stepName);
|
|
3319
|
+
}
|
|
3320
|
+
return webhook();
|
|
3321
|
+
}, "guardedWebhook");
|
|
3215
3322
|
return {
|
|
3216
3323
|
runId,
|
|
3217
|
-
step,
|
|
3218
|
-
localStep,
|
|
3219
|
-
upsertSearchAttributes,
|
|
3220
|
-
transaction,
|
|
3221
|
-
callEntity,
|
|
3222
|
-
signalEntity,
|
|
3223
|
-
sleep,
|
|
3224
|
-
sleepUntil,
|
|
3324
|
+
step: guard("step", step),
|
|
3325
|
+
localStep: guard("localStep", localStep),
|
|
3326
|
+
upsertSearchAttributes: guard("upsertSearchAttributes", upsertSearchAttributes),
|
|
3327
|
+
transaction: guard("transaction", transaction),
|
|
3328
|
+
callEntity: guard("callEntity", callEntity),
|
|
3329
|
+
signalEntity: guard("signalEntity", signalEntity),
|
|
3330
|
+
sleep: guard("sleep", sleep),
|
|
3331
|
+
sleepUntil: guard("sleepUntil", sleepUntil),
|
|
3225
3332
|
continueAsNew,
|
|
3226
|
-
waitForSignal,
|
|
3227
|
-
waitForEvent,
|
|
3228
|
-
task,
|
|
3229
|
-
child,
|
|
3230
|
-
all,
|
|
3231
|
-
startChild,
|
|
3232
|
-
breakpoint,
|
|
3233
|
-
webhook,
|
|
3234
|
-
setEvent,
|
|
3235
|
-
onUpdate,
|
|
3236
|
-
patched,
|
|
3237
|
-
now,
|
|
3238
|
-
sideEffect
|
|
3333
|
+
waitForSignal: guard("waitForSignal", waitForSignal),
|
|
3334
|
+
waitForEvent: guard("waitForEvent", waitForEvent),
|
|
3335
|
+
task: guard("task", task),
|
|
3336
|
+
child: guard("child", child),
|
|
3337
|
+
all: guard("all", all),
|
|
3338
|
+
startChild: guard("startChild", startChild),
|
|
3339
|
+
breakpoint: guard("breakpoint", breakpoint),
|
|
3340
|
+
webhook: guardedWebhook,
|
|
3341
|
+
setEvent: guard("setEvent", setEvent),
|
|
3342
|
+
onUpdate: guard("onUpdate", onUpdate),
|
|
3343
|
+
patched: guard("patched", patched),
|
|
3344
|
+
now: guard("now", now),
|
|
3345
|
+
sideEffect: guard("sideEffect", sideEffect)
|
|
3239
3346
|
};
|
|
3240
3347
|
}
|
|
3241
3348
|
__name(createWorkflowCtx, "createWorkflowCtx");
|
|
@@ -3797,7 +3904,7 @@ var WorkflowEngine = class {
|
|
|
3797
3904
|
VERSION_UNDECLARED_TAG
|
|
3798
3905
|
] : []
|
|
3799
3906
|
];
|
|
3800
|
-
const tags = mergeTags(registered.tags, stampedTags);
|
|
3907
|
+
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
3801
3908
|
if (registered.singleton) {
|
|
3802
3909
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
3803
3910
|
}
|
|
@@ -3837,10 +3944,30 @@ var WorkflowEngine = class {
|
|
|
3837
3944
|
getRun(runId) {
|
|
3838
3945
|
return this.store.getRun(runId);
|
|
3839
3946
|
}
|
|
3840
|
-
|
|
3947
|
+
/**
|
|
3948
|
+
* Re-execute a run's body (replaying its checkpoints). This is the EXPLICIT entry point: besides
|
|
3949
|
+
* driving a live run, it re-drives a `failed` run — the manual retry (prefer {@link requeue}, which
|
|
3950
|
+
* also resets the failure state so replay re-attempts the failed parts). Completed / cancelled /
|
|
3951
|
+
* dead runs are never re-executed. The engine's own implicit wake-ups (signals, step results,
|
|
3952
|
+
* timers, redelivered dispatches) go through {@link resumeImplicit}, which treats `failed` as
|
|
3953
|
+
* terminal too.
|
|
3954
|
+
*/
|
|
3955
|
+
resume(runId) {
|
|
3956
|
+
return this.resumeRun(runId, true);
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* {@link resume} for every IMPLICIT wake-up — a delivered signal/child completion, a late step
|
|
3960
|
+
* result, a due timer, a redelivered dispatch, a queue-slot wake. A `failed` run is terminal to
|
|
3961
|
+
* these: only an explicit retry ({@link requeue} / {@link resume}) may re-drive it. Otherwise a
|
|
3962
|
+
* stale waiter or a late result left behind by a failed run silently re-executes its body.
|
|
3963
|
+
*/
|
|
3964
|
+
resumeImplicit(runId) {
|
|
3965
|
+
return this.resumeRun(runId, false);
|
|
3966
|
+
}
|
|
3967
|
+
async resumeRun(runId, allowFailed) {
|
|
3841
3968
|
const run = await this.store.getRun(runId);
|
|
3842
3969
|
if (!run) throw new Error(`run ${runId} not found`);
|
|
3843
|
-
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") {
|
|
3970
|
+
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead" || run.status === "failed" && !allowFailed) {
|
|
3844
3971
|
return {
|
|
3845
3972
|
runId,
|
|
3846
3973
|
status: run.status,
|
|
@@ -3852,6 +3979,12 @@ var WorkflowEngine = class {
|
|
|
3852
3979
|
await this.store.releaseRunLock(runId);
|
|
3853
3980
|
throw new NamespaceMismatch();
|
|
3854
3981
|
}
|
|
3982
|
+
if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
3983
|
+
run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
3984
|
+
await this.store.updateRun(run.id, {
|
|
3985
|
+
tags: run.tags
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3855
3988
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
3856
3989
|
if (!registered) {
|
|
3857
3990
|
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)`);
|
|
@@ -4001,7 +4134,10 @@ var WorkflowEngine = class {
|
|
|
4001
4134
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
4002
4135
|
track(p) {
|
|
4003
4136
|
this.inflight.add(p);
|
|
4004
|
-
|
|
4137
|
+
const done = /* @__PURE__ */ __name(() => {
|
|
4138
|
+
this.inflight.delete(p);
|
|
4139
|
+
}, "done");
|
|
4140
|
+
void p.then(done, done);
|
|
4005
4141
|
return p;
|
|
4006
4142
|
}
|
|
4007
4143
|
/**
|
|
@@ -4289,7 +4425,7 @@ var WorkflowEngine = class {
|
|
|
4289
4425
|
const nowMs = this.clock();
|
|
4290
4426
|
const acquired = await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
4291
4427
|
if (!acquired) return null;
|
|
4292
|
-
return this.
|
|
4428
|
+
return this.resumeImplicit(runId).catch((err) => {
|
|
4293
4429
|
if (err instanceof NamespaceMismatch) return null;
|
|
4294
4430
|
throw err;
|
|
4295
4431
|
});
|
|
@@ -4352,7 +4488,8 @@ var WorkflowEngine = class {
|
|
|
4352
4488
|
status: "pending",
|
|
4353
4489
|
error: void 0,
|
|
4354
4490
|
...resurrecting ? {
|
|
4355
|
-
recoveryAttempts: 0
|
|
4491
|
+
recoveryAttempts: 0,
|
|
4492
|
+
tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
4356
4493
|
} : {},
|
|
4357
4494
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4358
4495
|
});
|
|
@@ -4471,10 +4608,19 @@ var WorkflowEngine = class {
|
|
|
4471
4608
|
off();
|
|
4472
4609
|
resolve(toResult(run));
|
|
4473
4610
|
}, "finish");
|
|
4611
|
+
const fail = /* @__PURE__ */ __name((error2) => {
|
|
4612
|
+
if (done) return;
|
|
4613
|
+
done = true;
|
|
4614
|
+
if (timer) clearTimeout(timer);
|
|
4615
|
+
if (poller) clearInterval(poller);
|
|
4616
|
+
off();
|
|
4617
|
+
reject(error2);
|
|
4618
|
+
}, "fail");
|
|
4474
4619
|
const check = /* @__PURE__ */ __name(() => {
|
|
4620
|
+
if (done) return;
|
|
4475
4621
|
void this.store.getRun(runId).then((run) => {
|
|
4476
4622
|
if (run && isSettled(run)) finish(run);
|
|
4477
|
-
});
|
|
4623
|
+
}, fail);
|
|
4478
4624
|
}, "check");
|
|
4479
4625
|
const poller = opts?.until === "terminal" ? (() => {
|
|
4480
4626
|
const interval = setInterval(check, 250);
|
|
@@ -4510,7 +4656,7 @@ var WorkflowEngine = class {
|
|
|
4510
4656
|
if (!acquired) continue;
|
|
4511
4657
|
try {
|
|
4512
4658
|
const settled = onLocked ? await onLocked(run) : void 0;
|
|
4513
|
-
results.push(settled ?? await this.
|
|
4659
|
+
results.push(settled ?? await this.resumeImplicit(run.id));
|
|
4514
4660
|
} catch (error2) {
|
|
4515
4661
|
console.error(`[nestjs-durable] could not resume run ${run.id} (${run.workflow}@${run.workflowVersion}); leaving it for another instance:`, error2);
|
|
4516
4662
|
}
|
|
@@ -4607,7 +4753,7 @@ var WorkflowEngine = class {
|
|
|
4607
4753
|
// for an ordinary (non-fan) signal/child await.
|
|
4608
4754
|
parallelGroup: waiter.parallelGroup
|
|
4609
4755
|
}));
|
|
4610
|
-
return this.
|
|
4756
|
+
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
4611
4757
|
if (err instanceof NamespaceMismatch) return null;
|
|
4612
4758
|
throw err;
|
|
4613
4759
|
});
|
|
@@ -4723,7 +4869,7 @@ var WorkflowEngine = class {
|
|
|
4723
4869
|
async cancel(runId, opts) {
|
|
4724
4870
|
const run = await this.store.getRun(runId);
|
|
4725
4871
|
if (!run) return null;
|
|
4726
|
-
if (
|
|
4872
|
+
if (TERMINAL_RUN_STATUSES.includes(run.status)) {
|
|
4727
4873
|
return {
|
|
4728
4874
|
runId,
|
|
4729
4875
|
status: run.status,
|
|
@@ -4752,7 +4898,7 @@ var WorkflowEngine = class {
|
|
|
4752
4898
|
}).catch(() => void 0);
|
|
4753
4899
|
}
|
|
4754
4900
|
queueMicrotask(() => {
|
|
4755
|
-
void this.
|
|
4901
|
+
void this.resumeImplicit(runId).then(() => this.notifyCancelled(runId)).catch(() => void 0);
|
|
4756
4902
|
});
|
|
4757
4903
|
await this.cancelChildren(runId, opts);
|
|
4758
4904
|
return {
|
|
@@ -5257,6 +5403,26 @@ var WorkflowEngine = class {
|
|
|
5257
5403
|
durationMs: Date.now() - step.startedAt.getTime()
|
|
5258
5404
|
});
|
|
5259
5405
|
}
|
|
5406
|
+
/** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
|
|
5407
|
+
async singletonWaitResult(run) {
|
|
5408
|
+
const current = await this.store.getRun(run.id);
|
|
5409
|
+
await this.store.releaseRunLock(run.id);
|
|
5410
|
+
if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
|
|
5411
|
+
if (current.status === "suspended") {
|
|
5412
|
+
this.emit({
|
|
5413
|
+
type: "run.suspended",
|
|
5414
|
+
runId: current.id,
|
|
5415
|
+
workflow: current.workflow,
|
|
5416
|
+
namespace: current.namespace
|
|
5417
|
+
});
|
|
5418
|
+
}
|
|
5419
|
+
return {
|
|
5420
|
+
runId: current.id,
|
|
5421
|
+
status: current.status,
|
|
5422
|
+
output: current.output,
|
|
5423
|
+
error: current.error
|
|
5424
|
+
};
|
|
5425
|
+
}
|
|
5260
5426
|
async execute(run, fn) {
|
|
5261
5427
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
5262
5428
|
const lockNow = this.clock();
|
|
@@ -5426,23 +5592,7 @@ var WorkflowEngine = class {
|
|
|
5426
5592
|
});
|
|
5427
5593
|
}
|
|
5428
5594
|
if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
5429
|
-
|
|
5430
|
-
await this.store.updateRun(run.id, {
|
|
5431
|
-
status: "suspended",
|
|
5432
|
-
wakeAt,
|
|
5433
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5434
|
-
});
|
|
5435
|
-
this.emit({
|
|
5436
|
-
type: "run.suspended",
|
|
5437
|
-
runId: run.id,
|
|
5438
|
-
workflow: run.workflow,
|
|
5439
|
-
namespace: run.namespace
|
|
5440
|
-
});
|
|
5441
|
-
await this.store.releaseRunLock(run.id);
|
|
5442
|
-
return {
|
|
5443
|
-
runId: run.id,
|
|
5444
|
-
status: "suspended"
|
|
5445
|
-
};
|
|
5595
|
+
return this.singletonWaitResult(run);
|
|
5446
5596
|
}
|
|
5447
5597
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
5448
5598
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
@@ -5587,7 +5737,7 @@ var WorkflowEngine = class {
|
|
|
5587
5737
|
const checkpoint = await this.store.getCheckpoint(runId, cmd.seq);
|
|
5588
5738
|
if (checkpoint && checkpoint.status !== "pending") {
|
|
5589
5739
|
setTimeout(() => {
|
|
5590
|
-
void this.
|
|
5740
|
+
void this.resumeImplicit(runId).catch(() => void 0);
|
|
5591
5741
|
}, 0).unref?.();
|
|
5592
5742
|
return;
|
|
5593
5743
|
}
|
|
@@ -5929,7 +6079,7 @@ var WorkflowEngine = class {
|
|
|
5929
6079
|
kind: "signal",
|
|
5930
6080
|
output: payload
|
|
5931
6081
|
}));
|
|
5932
|
-
setTimeout(() => void this.
|
|
6082
|
+
setTimeout(() => void this.resumeImplicit(run.id).catch(() => void 0), 0);
|
|
5933
6083
|
}, "deliverBuffered");
|
|
5934
6084
|
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
5935
6085
|
if (buffered) {
|
|
@@ -6056,23 +6206,7 @@ var WorkflowEngine = class {
|
|
|
6056
6206
|
});
|
|
6057
6207
|
}
|
|
6058
6208
|
if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
6059
|
-
|
|
6060
|
-
await this.store.updateRun(run.id, {
|
|
6061
|
-
status: "suspended",
|
|
6062
|
-
wakeAt,
|
|
6063
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
6064
|
-
});
|
|
6065
|
-
this.emit({
|
|
6066
|
-
type: "run.suspended",
|
|
6067
|
-
runId: run.id,
|
|
6068
|
-
workflow: run.workflow,
|
|
6069
|
-
namespace: run.namespace
|
|
6070
|
-
});
|
|
6071
|
-
await this.store.releaseRunLock(run.id);
|
|
6072
|
-
return {
|
|
6073
|
-
runId: run.id,
|
|
6074
|
-
status: "suspended"
|
|
6075
|
-
};
|
|
6209
|
+
return this.singletonWaitResult(run);
|
|
6076
6210
|
}
|
|
6077
6211
|
const compensations = [];
|
|
6078
6212
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
@@ -6743,7 +6877,7 @@ var WorkflowEngine = class {
|
|
|
6743
6877
|
queueMs: startedAt.getTime() - cp.enqueuedAt.getTime(),
|
|
6744
6878
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
6745
6879
|
});
|
|
6746
|
-
await this.
|
|
6880
|
+
await this.resumeImplicit(result.runId).catch((err) => {
|
|
6747
6881
|
if (err instanceof NamespaceMismatch) return null;
|
|
6748
6882
|
throw err;
|
|
6749
6883
|
});
|
|
@@ -6844,7 +6978,7 @@ var WorkflowEngine = class {
|
|
|
6844
6978
|
...waiters
|
|
6845
6979
|
];
|
|
6846
6980
|
waiters.clear();
|
|
6847
|
-
for (const runId of runIds) void this.
|
|
6981
|
+
for (const runId of runIds) void this.resumeImplicit(runId).catch(() => void 0);
|
|
6848
6982
|
}
|
|
6849
6983
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
6850
6984
|
async releaseQueueSlot(id) {
|
|
@@ -7091,6 +7225,8 @@ var CodecStateStore = class {
|
|
|
7091
7225
|
}
|
|
7092
7226
|
inner;
|
|
7093
7227
|
codec;
|
|
7228
|
+
/** Atomic claims touch only searchable metadata and bypass payload encoding. */
|
|
7229
|
+
tryAdmitSingleton;
|
|
7094
7230
|
/**
|
|
7095
7231
|
* Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
|
|
7096
7232
|
* (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
|
|
@@ -7123,6 +7259,7 @@ var CodecStateStore = class {
|
|
|
7123
7259
|
constructor(inner, codec) {
|
|
7124
7260
|
this.inner = inner;
|
|
7125
7261
|
this.codec = codec;
|
|
7262
|
+
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
7126
7263
|
const deleteRuns = inner.deleteRuns;
|
|
7127
7264
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7128
7265
|
const prune = inner.pruneTerminalRuns;
|
|
@@ -7575,6 +7712,28 @@ var InMemoryStateStore = class {
|
|
|
7575
7712
|
byRun.set(runId, row);
|
|
7576
7713
|
}
|
|
7577
7714
|
}
|
|
7715
|
+
async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
|
|
7716
|
+
const runs = [
|
|
7717
|
+
...this.runs.values()
|
|
7718
|
+
];
|
|
7719
|
+
const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
|
|
7720
|
+
if (!tags) {
|
|
7721
|
+
const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
|
|
7722
|
+
const candidate = this.runs.get(runId);
|
|
7723
|
+
if (patch && candidate) this.runs.set(runId, {
|
|
7724
|
+
...candidate,
|
|
7725
|
+
...patch
|
|
7726
|
+
});
|
|
7727
|
+
return false;
|
|
7728
|
+
}
|
|
7729
|
+
const run = this.runs.get(runId);
|
|
7730
|
+
if (!run) return false;
|
|
7731
|
+
this.runs.set(runId, {
|
|
7732
|
+
...run,
|
|
7733
|
+
tags
|
|
7734
|
+
});
|
|
7735
|
+
return true;
|
|
7736
|
+
}
|
|
7578
7737
|
async createRun(run) {
|
|
7579
7738
|
this.runs.set(run.id, {
|
|
7580
7739
|
...run,
|
|
@@ -7582,7 +7741,11 @@ var InMemoryStateStore = class {
|
|
|
7582
7741
|
});
|
|
7583
7742
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
7584
7743
|
}
|
|
7585
|
-
async updateRun(runId,
|
|
7744
|
+
async updateRun(runId, inputPatch) {
|
|
7745
|
+
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
7746
|
+
...inputPatch,
|
|
7747
|
+
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
7748
|
+
} : inputPatch;
|
|
7586
7749
|
const existing = this.runs.get(runId);
|
|
7587
7750
|
if (!existing) throw new Error(`run ${runId} not found`);
|
|
7588
7751
|
const next = {
|
|
@@ -7996,6 +8159,7 @@ export {
|
|
|
7996
8159
|
InMemoryTransport,
|
|
7997
8160
|
LEGACY_V1_CAPABILITIES,
|
|
7998
8161
|
LEGACY_V1_PROTOCOL,
|
|
8162
|
+
NestedWorkflowCallError,
|
|
7999
8163
|
NonDeterminismError,
|
|
8000
8164
|
QueueController,
|
|
8001
8165
|
RUN_VALUE_FACET_LIMIT,
|
|
@@ -8005,6 +8169,8 @@ export {
|
|
|
8005
8169
|
RemoteWorkflowExecutor,
|
|
8006
8170
|
RemoteWorkflowTimeout,
|
|
8007
8171
|
RunGateway,
|
|
8172
|
+
SINGLETON_ACTIVE_STATUSES,
|
|
8173
|
+
SINGLETON_ADMITTED_TAG,
|
|
8008
8174
|
STATE_STORE,
|
|
8009
8175
|
STATE_STORE_CANONICAL,
|
|
8010
8176
|
ScheduleAlreadyExistsError,
|
|
@@ -8035,6 +8201,7 @@ export {
|
|
|
8035
8201
|
capableWorkers,
|
|
8036
8202
|
classifyWaiterToken,
|
|
8037
8203
|
clearDurableExecutionWrappers,
|
|
8204
|
+
clearTerminalSingletonAdmission,
|
|
8038
8205
|
collectMetrics,
|
|
8039
8206
|
concurrencyTag,
|
|
8040
8207
|
controlPlaneDescriptor,
|
|
@@ -8042,6 +8209,7 @@ export {
|
|
|
8042
8209
|
coveredTokens,
|
|
8043
8210
|
createStepLogger,
|
|
8044
8211
|
currentStep,
|
|
8212
|
+
currentStepScope,
|
|
8045
8213
|
currentWorkflowCtx,
|
|
8046
8214
|
debug,
|
|
8047
8215
|
describeWorker,
|
|
@@ -8083,12 +8251,16 @@ export {
|
|
|
8083
8251
|
resolveRunWaiting,
|
|
8084
8252
|
runDurableExecution,
|
|
8085
8253
|
runInStepLogger,
|
|
8254
|
+
runInStepScope,
|
|
8086
8255
|
runInWorkflowCtx,
|
|
8256
|
+
runOutsideWorkflowCtx,
|
|
8087
8257
|
runSchedules,
|
|
8088
8258
|
runStepHandler,
|
|
8089
8259
|
runValueFacetsFromRuns,
|
|
8090
8260
|
sanitizeQueueToken,
|
|
8091
8261
|
scheduledRunId,
|
|
8262
|
+
singletonAdmissionDenialPatch,
|
|
8263
|
+
singletonAdmissionTags,
|
|
8092
8264
|
sqlComparator,
|
|
8093
8265
|
stepConfigOf,
|
|
8094
8266
|
stepId,
|