@dudousxd/nestjs-durable-core 0.80.1 → 0.81.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 +398 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -8
- package/dist/index.d.ts +167 -8
- package/dist/index.js +390 -98
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -928,6 +928,9 @@ async function assertConcurrency(store, workflow, quota) {
|
|
|
928
928
|
__name(assertConcurrency, "assertConcurrency");
|
|
929
929
|
|
|
930
930
|
// src/entities.ts
|
|
931
|
+
var NUDGE = {
|
|
932
|
+
nudge: true
|
|
933
|
+
};
|
|
931
934
|
var ENTITY_WORKFLOW = "__entity";
|
|
932
935
|
var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
|
|
933
936
|
var Entities = class {
|
|
@@ -935,9 +938,11 @@ var Entities = class {
|
|
|
935
938
|
__name(this, "Entities");
|
|
936
939
|
}
|
|
937
940
|
host;
|
|
941
|
+
workflowHost;
|
|
938
942
|
configs = /* @__PURE__ */ new Map();
|
|
939
|
-
constructor(host) {
|
|
943
|
+
constructor(host, workflowHost) {
|
|
940
944
|
this.host = host;
|
|
945
|
+
this.workflowHost = workflowHost;
|
|
941
946
|
host.register(ENTITY_WORKFLOW, "1", (ctx, input) => this.run(ctx, input));
|
|
942
947
|
}
|
|
943
948
|
register(name, config) {
|
|
@@ -959,6 +964,52 @@ var Entities = class {
|
|
|
959
964
|
}
|
|
960
965
|
});
|
|
961
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* The durable half of a send from INSIDE a workflow (`ctx.callEntity` / `ctx.signalEntity`): write
|
|
969
|
+
* the op to the entity's signal buffer and return once it is stored, so the caller can checkpoint
|
|
970
|
+
* after it. The op carries `id`, the caller's stable `<runId>:<seq>`, so if the caller dies between
|
|
971
|
+
* this write and its checkpoint and sends again on replay, the entity skips the second copy. The
|
|
972
|
+
* entity's run is also ensured here, awaited (`start` only enqueues, it never runs the body
|
|
973
|
+
* inline), so an op sent on first contact can't be stranded in a buffer no run will ever read if
|
|
974
|
+
* the process dies right after. The wake-up ({@link kick}) is separate and may be lost: a busy
|
|
975
|
+
* entity reads the buffer at its next wait, and a parked one is paired with it by the engine's
|
|
976
|
+
* `reconcileMs` sweep. Returns `false` on a host without a workflow host.
|
|
977
|
+
*/
|
|
978
|
+
async enqueue(name, key, op, arg, id, reply) {
|
|
979
|
+
if (!this.workflowHost) return false;
|
|
980
|
+
const message = {
|
|
981
|
+
op,
|
|
982
|
+
arg,
|
|
983
|
+
id,
|
|
984
|
+
...reply ? {
|
|
985
|
+
reply
|
|
986
|
+
} : {}
|
|
987
|
+
};
|
|
988
|
+
const runId = entityRunId(name, key);
|
|
989
|
+
await this.workflowHost.bufferSignal(runId, message);
|
|
990
|
+
await this.workflowHost.ensureStarted(ENTITY_WORKFLOW, {
|
|
991
|
+
name,
|
|
992
|
+
key
|
|
993
|
+
}, runId);
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Make the entity act on what {@link enqueue} buffered: wake its run if it is parked on its wait
|
|
998
|
+
* (the run itself was ensured by `enqueue`; ensured again here, idempotently, for a caller replay
|
|
999
|
+
* that only re-kicks). A run that is busy needs nothing: its next wait reads the buffer before
|
|
1000
|
+
* suspending. The wake carries a no-op `nudge`, never the op itself, so the op stays in
|
|
1001
|
+
* the buffer in arrival order and a lost or repeated kick costs at most one empty iteration.
|
|
1002
|
+
*/
|
|
1003
|
+
async kick(name, key) {
|
|
1004
|
+
const host = this.workflowHost;
|
|
1005
|
+
if (!host) return;
|
|
1006
|
+
const runId = entityRunId(name, key);
|
|
1007
|
+
await host.ensureStarted(ENTITY_WORKFLOW, {
|
|
1008
|
+
name,
|
|
1009
|
+
key
|
|
1010
|
+
}, runId);
|
|
1011
|
+
await host.wakeWaiter(runId, NUDGE);
|
|
1012
|
+
}
|
|
962
1013
|
async signal(name, key, op, arg) {
|
|
963
1014
|
await this.dispatch(name, key, op, arg);
|
|
964
1015
|
}
|
|
@@ -972,11 +1023,18 @@ var Entities = class {
|
|
|
972
1023
|
if (!config) throw new FatalError(`entity "${name}" is not registered`);
|
|
973
1024
|
const token = entityRunId(name, key);
|
|
974
1025
|
let state = config.initialState();
|
|
1026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
975
1027
|
for (let i = 0; ; i += 1) {
|
|
976
1028
|
const msg = await ctx.waitForSignal(token);
|
|
1029
|
+
if (msg.nudge) continue;
|
|
1030
|
+
if (msg.id !== void 0) {
|
|
1031
|
+
if (seen.has(msg.id)) continue;
|
|
1032
|
+
seen.add(msg.id);
|
|
1033
|
+
}
|
|
1034
|
+
const op = msg.op;
|
|
977
1035
|
const out = await ctx.localStep(`op:${i}`, async () => {
|
|
978
|
-
const handler = config.handlers[
|
|
979
|
-
if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${
|
|
1036
|
+
const handler = config.handlers[op];
|
|
1037
|
+
if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${op}"`);
|
|
980
1038
|
const result = await handler(state, msg.arg);
|
|
981
1039
|
return {
|
|
982
1040
|
result,
|
|
@@ -986,8 +1044,9 @@ var Entities = class {
|
|
|
986
1044
|
state = out.state;
|
|
987
1045
|
await ctx.setEvent("state", state);
|
|
988
1046
|
if (msg.reply) {
|
|
1047
|
+
const reply = msg.reply;
|
|
989
1048
|
await ctx.localStep(`reply:${i}`, async () => {
|
|
990
|
-
await this.host.signal(
|
|
1049
|
+
await this.host.signal(reply, out.result);
|
|
991
1050
|
});
|
|
992
1051
|
}
|
|
993
1052
|
}
|
|
@@ -1911,6 +1970,10 @@ function checkpointNameKind(name) {
|
|
|
1911
1970
|
return "step";
|
|
1912
1971
|
}
|
|
1913
1972
|
__name(checkpointNameKind, "checkpointNameKind");
|
|
1973
|
+
function poolNamespace(namespace) {
|
|
1974
|
+
return namespace === "" ? "default" : namespace;
|
|
1975
|
+
}
|
|
1976
|
+
__name(poolNamespace, "poolNamespace");
|
|
1914
1977
|
var TERMINAL_RUN_STATUSES = [
|
|
1915
1978
|
"completed",
|
|
1916
1979
|
"failed",
|
|
@@ -2042,6 +2105,19 @@ var WEBHOOK_PREFIX = "wh:";
|
|
|
2042
2105
|
var CHILD_PREFIX = "child:";
|
|
2043
2106
|
var BREAKPOINT_PREFIX = "bp:";
|
|
2044
2107
|
var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
|
|
2108
|
+
function parseWebhookToken(token) {
|
|
2109
|
+
if (!token.startsWith(WEBHOOK_PREFIX)) return null;
|
|
2110
|
+
const rest = token.slice(WEBHOOK_PREFIX.length);
|
|
2111
|
+
const at = rest.lastIndexOf(":");
|
|
2112
|
+
if (at <= 0) return null;
|
|
2113
|
+
const seqText = rest.slice(at + 1);
|
|
2114
|
+
if (!/^\d+$/.test(seqText)) return null;
|
|
2115
|
+
return {
|
|
2116
|
+
runId: rest.slice(0, at),
|
|
2117
|
+
seq: Number(seqText)
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
__name(parseWebhookToken, "parseWebhookToken");
|
|
2045
2121
|
function classifyWaiterToken(token) {
|
|
2046
2122
|
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
2047
2123
|
on: "webhook",
|
|
@@ -2579,6 +2655,11 @@ __name(errorMessage, "errorMessage");
|
|
|
2579
2655
|
|
|
2580
2656
|
// src/singleton-admission.ts
|
|
2581
2657
|
var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
|
|
2658
|
+
var SINGLETON_TRACKED_TAG = "durable:singleton:tracked";
|
|
2659
|
+
var SINGLETON_RESERVED_TAGS = [
|
|
2660
|
+
SINGLETON_ADMITTED_TAG,
|
|
2661
|
+
SINGLETON_TRACKED_TAG
|
|
2662
|
+
];
|
|
2582
2663
|
var SINGLETON_ACTIVE_STATUSES = [
|
|
2583
2664
|
"pending",
|
|
2584
2665
|
"running",
|
|
@@ -2586,6 +2667,33 @@ var SINGLETON_ACTIVE_STATUSES = [
|
|
|
2586
2667
|
"blocked",
|
|
2587
2668
|
"cancelling"
|
|
2588
2669
|
];
|
|
2670
|
+
var LEGACY_SCAN_STATUSES = [
|
|
2671
|
+
"running",
|
|
2672
|
+
"suspended",
|
|
2673
|
+
"cancelling"
|
|
2674
|
+
];
|
|
2675
|
+
var byAdmissionOrder = /* @__PURE__ */ __name((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id), "byAdmissionOrder");
|
|
2676
|
+
function withoutSingletonMarkers(tags) {
|
|
2677
|
+
return tags?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
|
|
2678
|
+
}
|
|
2679
|
+
__name(withoutSingletonMarkers, "withoutSingletonMarkers");
|
|
2680
|
+
function withSingletonTracked(tags) {
|
|
2681
|
+
const current = tags ?? [];
|
|
2682
|
+
if (current.includes(SINGLETON_ADMITTED_TAG) || current.includes(SINGLETON_TRACKED_TAG)) {
|
|
2683
|
+
return current;
|
|
2684
|
+
}
|
|
2685
|
+
return [
|
|
2686
|
+
...current,
|
|
2687
|
+
SINGLETON_TRACKED_TAG
|
|
2688
|
+
];
|
|
2689
|
+
}
|
|
2690
|
+
__name(withSingletonTracked, "withSingletonTracked");
|
|
2691
|
+
function legacySingletonHolders(active, runId, limit) {
|
|
2692
|
+
const scanned = active.filter((run) => LEGACY_SCAN_STATUSES.includes(run.status)).sort(byAdmissionOrder);
|
|
2693
|
+
const window = new Set(scanned.slice(0, limit).map((run) => run.id));
|
|
2694
|
+
return scanned.filter((run) => run.id !== runId && !run.tags?.includes(SINGLETON_ADMITTED_TAG) && (!run.tags?.includes(SINGLETON_TRACKED_TAG) || window.has(run.id)));
|
|
2695
|
+
}
|
|
2696
|
+
__name(legacySingletonHolders, "legacySingletonHolders");
|
|
2589
2697
|
function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
2590
2698
|
if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
|
|
2591
2699
|
const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
|
|
@@ -2594,12 +2702,13 @@ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
|
2594
2702
|
if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
|
|
2595
2703
|
if (candidate.status === "cancelling") return null;
|
|
2596
2704
|
const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
|
|
2597
|
-
const
|
|
2705
|
+
const legacy = new Set(legacySingletonHolders(active, runId, limit).map((run) => run.id));
|
|
2706
|
+
const remaining = limit - holders.length - legacy.size;
|
|
2598
2707
|
if (remaining <= 0) return null;
|
|
2599
|
-
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)
|
|
2708
|
+
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG) && !legacy.has(run.id)).sort(byAdmissionOrder);
|
|
2600
2709
|
if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
|
|
2601
2710
|
return [
|
|
2602
|
-
...candidate.tags ?? [],
|
|
2711
|
+
...(candidate.tags ?? []).filter((t) => t !== SINGLETON_TRACKED_TAG),
|
|
2603
2712
|
SINGLETON_ADMITTED_TAG
|
|
2604
2713
|
];
|
|
2605
2714
|
}
|
|
@@ -2609,7 +2718,7 @@ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
|
|
|
2609
2718
|
const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
|
|
2610
2719
|
return {
|
|
2611
2720
|
...patch,
|
|
2612
|
-
tags: tags
|
|
2721
|
+
tags: withoutSingletonMarkers(tags)
|
|
2613
2722
|
};
|
|
2614
2723
|
}
|
|
2615
2724
|
__name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
|
|
@@ -2620,6 +2729,7 @@ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt)
|
|
|
2620
2729
|
return {
|
|
2621
2730
|
status: "suspended",
|
|
2622
2731
|
wakeAt: retryWakeAt,
|
|
2732
|
+
tags: withSingletonTracked(candidate.tags),
|
|
2623
2733
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2624
2734
|
};
|
|
2625
2735
|
}
|
|
@@ -2672,7 +2782,7 @@ var SingletonGate = class {
|
|
|
2672
2782
|
const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
|
|
2673
2783
|
if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
2674
2784
|
run.tags = [
|
|
2675
|
-
...run.tags ?? [],
|
|
2785
|
+
...(run.tags ?? []).filter((tag) => tag !== SINGLETON_TRACKED_TAG),
|
|
2676
2786
|
SINGLETON_ADMITTED_TAG
|
|
2677
2787
|
];
|
|
2678
2788
|
}
|
|
@@ -3567,30 +3677,82 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3567
3677
|
}));
|
|
3568
3678
|
return id;
|
|
3569
3679
|
}, "startChild");
|
|
3680
|
+
const sendEntityOp = /* @__PURE__ */ __name(async (name, key, op, arg, seq, reply) => await host.enqueueEntityOp?.(name, key, op, arg, `${runId}:${seq}`, reply) ?? false, "sendEntityOp");
|
|
3570
3681
|
const callEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
|
|
3571
3682
|
const current = pos.next();
|
|
3572
|
-
|
|
3683
|
+
let existing = await readCheckpoint(current);
|
|
3573
3684
|
if (existing && existing.status === "completed") return existing.output;
|
|
3574
3685
|
const reply = `entityreply:${runId}:${current}`;
|
|
3686
|
+
const intentName = `signal:${reply}`;
|
|
3687
|
+
if (existing && existing.name !== intentName) {
|
|
3688
|
+
throw new NonDeterminismError(runId, current, intentName, existing.name);
|
|
3689
|
+
}
|
|
3690
|
+
if (existing && replay?.has(current)) {
|
|
3691
|
+
existing = await store.getCheckpoint(runId, current) ?? existing;
|
|
3692
|
+
if (existing.status === "completed") return existing.output;
|
|
3693
|
+
}
|
|
3694
|
+
const markSent = /* @__PURE__ */ __name(() => store.saveCheckpointIfPending?.(instantCheckpoint({
|
|
3695
|
+
runId,
|
|
3696
|
+
seq: current,
|
|
3697
|
+
name: intentName,
|
|
3698
|
+
kind: "signal",
|
|
3699
|
+
status: "pending",
|
|
3700
|
+
output: {
|
|
3701
|
+
sent: true
|
|
3702
|
+
}
|
|
3703
|
+
})) ?? Promise.resolve(false), "markSent");
|
|
3704
|
+
if (existing) {
|
|
3705
|
+
const buffered = await consumeBuffered(reply, current);
|
|
3706
|
+
if (buffered) {
|
|
3707
|
+
await store.removeSignalWaiter({
|
|
3708
|
+
token: reply,
|
|
3709
|
+
runId,
|
|
3710
|
+
seq: current
|
|
3711
|
+
});
|
|
3712
|
+
return buffered.value;
|
|
3713
|
+
}
|
|
3714
|
+
if (existing.output?.sent !== true) {
|
|
3715
|
+
if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
|
|
3716
|
+
}
|
|
3717
|
+
host.kickEntity?.(name, key);
|
|
3718
|
+
throw new WorkflowSuspended();
|
|
3719
|
+
}
|
|
3575
3720
|
await store.putSignalWaiter({
|
|
3576
3721
|
token: reply,
|
|
3577
3722
|
runId,
|
|
3578
3723
|
seq: current
|
|
3579
3724
|
});
|
|
3580
|
-
host.
|
|
3725
|
+
if (!host.enqueueEntityOp) {
|
|
3726
|
+
host.signalEntity?.(name, key, op, arg, reply);
|
|
3727
|
+
throw new WorkflowSuspended();
|
|
3728
|
+
}
|
|
3729
|
+
await writeCheckpoint(instantCheckpoint({
|
|
3730
|
+
runId,
|
|
3731
|
+
seq: current,
|
|
3732
|
+
name: intentName,
|
|
3733
|
+
kind: "signal",
|
|
3734
|
+
status: "pending",
|
|
3735
|
+
output: {
|
|
3736
|
+
sent: false
|
|
3737
|
+
}
|
|
3738
|
+
}));
|
|
3739
|
+
if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
|
|
3740
|
+
host.kickEntity?.(name, key);
|
|
3581
3741
|
throw new WorkflowSuspended();
|
|
3582
3742
|
}, "callEntity");
|
|
3583
3743
|
const signalEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
|
|
3584
3744
|
const current = pos.next();
|
|
3585
3745
|
const existing = await readCheckpoint(current);
|
|
3586
3746
|
if (existing && existing.status === "completed") return;
|
|
3587
|
-
|
|
3747
|
+
const durable = await sendEntityOp(name, key, op, arg, current);
|
|
3748
|
+
if (!durable) host.signalEntity?.(name, key, op, arg);
|
|
3588
3749
|
await writeCheckpoint(instantCheckpoint({
|
|
3589
3750
|
runId,
|
|
3590
3751
|
seq: current,
|
|
3591
3752
|
name: `entitysig:${name}:${key}`,
|
|
3592
3753
|
kind: "local"
|
|
3593
3754
|
}));
|
|
3755
|
+
if (durable) host.kickEntity?.(name, key);
|
|
3594
3756
|
}, "signalEntity");
|
|
3595
3757
|
const breakpoint = /* @__PURE__ */ __name(async (label) => {
|
|
3596
3758
|
const current = pos.next();
|
|
@@ -3658,17 +3820,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3658
3820
|
const deadlineSeq = timeoutMs == null ? -1 : pos.next();
|
|
3659
3821
|
const existing = await readCheckpoint(current);
|
|
3660
3822
|
if (existing && existing.status === "completed") return existing.output;
|
|
3661
|
-
|
|
3662
|
-
|
|
3823
|
+
const buffered = await consumeBuffered(token, current);
|
|
3824
|
+
if (buffered) return buffered.value;
|
|
3825
|
+
const deadline = timeoutMs == null ? void 0 : await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
3826
|
+
if (deadline !== void 0 && host.clock() >= deadline) {
|
|
3827
|
+
await store.removeSignalWaiter({
|
|
3663
3828
|
token,
|
|
3664
3829
|
runId,
|
|
3665
3830
|
seq: current
|
|
3666
|
-
});
|
|
3667
|
-
throw new WorkflowSuspended();
|
|
3668
|
-
}
|
|
3669
|
-
const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
|
|
3670
|
-
if (host.clock() >= deadline) {
|
|
3671
|
-
await store.takeSignalWaiter(token).catch(() => void 0);
|
|
3831
|
+
}).catch(() => void 0);
|
|
3672
3832
|
throw new SignalTimeoutError(token, timeoutMs);
|
|
3673
3833
|
}
|
|
3674
3834
|
await store.putSignalWaiter({
|
|
@@ -3676,6 +3836,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3676
3836
|
runId,
|
|
3677
3837
|
seq: current
|
|
3678
3838
|
});
|
|
3839
|
+
const lateBuffered = await consumeBuffered(token, current);
|
|
3840
|
+
if (lateBuffered) {
|
|
3841
|
+
await store.removeSignalWaiter({
|
|
3842
|
+
token,
|
|
3843
|
+
runId,
|
|
3844
|
+
seq: current
|
|
3845
|
+
});
|
|
3846
|
+
return lateBuffered.value;
|
|
3847
|
+
}
|
|
3679
3848
|
throw new WorkflowSuspended(deadline);
|
|
3680
3849
|
}, "wait");
|
|
3681
3850
|
return {
|
|
@@ -3867,6 +4036,12 @@ var SETTLE_ELIGIBLE_STATUSES = [
|
|
|
3867
4036
|
"failed",
|
|
3868
4037
|
"cancelling"
|
|
3869
4038
|
];
|
|
4039
|
+
var TIMEOUT_SWEPT_STATUSES = [
|
|
4040
|
+
"pending",
|
|
4041
|
+
"running",
|
|
4042
|
+
"suspended",
|
|
4043
|
+
"blocked"
|
|
4044
|
+
];
|
|
3870
4045
|
var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
|
|
3871
4046
|
var RECOVERY_BATCH = 500;
|
|
3872
4047
|
var NamespaceMismatch = class NamespaceMismatch2 extends Error {
|
|
@@ -3889,6 +4064,12 @@ var WorkflowEngine = class {
|
|
|
3889
4064
|
instanceId;
|
|
3890
4065
|
namespace;
|
|
3891
4066
|
leaseMs;
|
|
4067
|
+
/**
|
|
4068
|
+
* Leases held by this `instanceId` that expire strictly before this epoch ms were taken by a
|
|
4069
|
+
* PREVIOUS process with the same id (every lease this one takes or renews expires at
|
|
4070
|
+
* `clock() + leaseMs`, so at or after it). See {@link orphanedRuns}.
|
|
4071
|
+
*/
|
|
4072
|
+
ownLeaseCutoffMs;
|
|
3892
4073
|
maxRecoveryAttempts;
|
|
3893
4074
|
remoteAdvanceSilenceMs;
|
|
3894
4075
|
reconcileMs;
|
|
@@ -4010,8 +4191,9 @@ var WorkflowEngine = class {
|
|
|
4010
4191
|
this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
|
|
4011
4192
|
this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
|
|
4012
4193
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
4013
|
-
this.namespace = deps.namespace;
|
|
4194
|
+
this.namespace = poolNamespace(deps.namespace);
|
|
4014
4195
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
4196
|
+
this.ownLeaseCutoffMs = this.clock() + this.leaseMs;
|
|
4015
4197
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
4016
4198
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
4017
4199
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
@@ -4131,7 +4313,11 @@ var WorkflowEngine = class {
|
|
|
4131
4313
|
}
|
|
4132
4314
|
});
|
|
4133
4315
|
this.accumulators = new EventAccumulators(this);
|
|
4134
|
-
this.entities = new Entities(this
|
|
4316
|
+
this.entities = new Entities(this, {
|
|
4317
|
+
bufferSignal: /* @__PURE__ */ __name((token, payload) => this.store.bufferSignal(token, payload), "bufferSignal"),
|
|
4318
|
+
wakeWaiter: /* @__PURE__ */ __name((token, payload) => this.wakeSignalWaiter(token, payload), "wakeWaiter"),
|
|
4319
|
+
ensureStarted: /* @__PURE__ */ __name((workflow, input, runId) => this.start(workflow, input, runId), "ensureStarted")
|
|
4320
|
+
});
|
|
4135
4321
|
}
|
|
4136
4322
|
/**
|
|
4137
4323
|
* Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
|
|
@@ -4407,7 +4593,7 @@ var WorkflowEngine = class {
|
|
|
4407
4593
|
// tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
|
|
4408
4594
|
// `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
|
|
4409
4595
|
// not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
|
|
4410
|
-
namespace: opts?.namespace ?? this.namespace,
|
|
4596
|
+
namespace: poolNamespace(opts?.namespace) ?? this.namespace,
|
|
4411
4597
|
input,
|
|
4412
4598
|
createdAt: /* @__PURE__ */ new Date(),
|
|
4413
4599
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -4444,7 +4630,7 @@ var WorkflowEngine = class {
|
|
|
4444
4630
|
VERSION_UNDECLARED_TAG
|
|
4445
4631
|
] : []
|
|
4446
4632
|
];
|
|
4447
|
-
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag
|
|
4633
|
+
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
|
|
4448
4634
|
if (registered.singleton) {
|
|
4449
4635
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
4450
4636
|
}
|
|
@@ -4458,7 +4644,7 @@ var WorkflowEngine = class {
|
|
|
4458
4644
|
...delayed ? {
|
|
4459
4645
|
wakeAt: startAtMs
|
|
4460
4646
|
} : {},
|
|
4461
|
-
namespace: opts?.namespace ?? this.namespace,
|
|
4647
|
+
namespace: poolNamespace(opts?.namespace) ?? this.namespace,
|
|
4462
4648
|
// DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
|
|
4463
4649
|
// deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
|
|
4464
4650
|
// `undefined` when the registration carried none (remote/convention/synthesized child, or a
|
|
@@ -4640,12 +4826,12 @@ var WorkflowEngine = class {
|
|
|
4640
4826
|
error: run.error
|
|
4641
4827
|
};
|
|
4642
4828
|
}
|
|
4643
|
-
if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
|
|
4829
|
+
if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
|
|
4644
4830
|
await this.store.releaseRunLock(runId);
|
|
4645
4831
|
throw new NamespaceMismatch();
|
|
4646
4832
|
}
|
|
4647
|
-
if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
4648
|
-
run.tags = run.tags
|
|
4833
|
+
if (run.status === "failed" && (run.tags?.includes(SINGLETON_ADMITTED_TAG) || run.tags?.includes(SINGLETON_TRACKED_TAG))) {
|
|
4834
|
+
run.tags = withoutSingletonMarkers(run.tags);
|
|
4649
4835
|
await this.store.updateRun(run.id, {
|
|
4650
4836
|
tags: run.tags
|
|
4651
4837
|
});
|
|
@@ -4864,6 +5050,15 @@ var WorkflowEngine = class {
|
|
|
4864
5050
|
* `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
|
|
4865
5051
|
* are cascaded exactly as an explicit {@link cancel} cascades them.
|
|
4866
5052
|
*
|
|
5053
|
+
* WHICH RUNS. Every run not yet finished, counted from `createdAt`: `pending` and `blocked` as well
|
|
5054
|
+
* as `running` and `suspended`. A run nobody picks up (no worker on its queue) or nobody can run (no
|
|
5055
|
+
* worker with its capabilities) is as stuck as one that hangs mid-body, and the cap is the backstop
|
|
5056
|
+
* for exactly that — leaving them out meant a run that never started lived forever while one that
|
|
5057
|
+
* started and hung was cut off. Cancelling one a worker is picking up at that moment is safe: the
|
|
5058
|
+
* worker's settle only writes over a live status ({@link SETTLE_ELIGIBLE_STATUSES}), so it can't
|
|
5059
|
+
* bring the run back. `cancelling` is left alone on purpose — a compensating cancel's saga undo is
|
|
5060
|
+
* running, it ends `cancelled` by itself, and cutting it short would skip the undo.
|
|
5061
|
+
*
|
|
4867
5062
|
* WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
|
|
4868
5063
|
* the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
|
|
4869
5064
|
* a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
|
|
@@ -4876,8 +5071,8 @@ var WorkflowEngine = class {
|
|
|
4876
5071
|
* children kept running with the parent gone and nothing pointing at them — a silent orphan you
|
|
4877
5072
|
* could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
|
|
4878
5073
|
* walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
|
|
4879
|
-
* `cancel` returns early on a run that is already
|
|
4880
|
-
*
|
|
5074
|
+
* `cancel` returns early on a run that is already terminal (a child that beat the sweep to the
|
|
5075
|
+
* finish is never clobbered, and a re-cancel is a no-op), and it writes the
|
|
4881
5076
|
* parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
|
|
4882
5077
|
* second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
|
|
4883
5078
|
* the same terminal state twice and cascade into already-cancelled children — idempotent, not
|
|
@@ -4893,18 +5088,11 @@ var WorkflowEngine = class {
|
|
|
4893
5088
|
if (this.draining) return;
|
|
4894
5089
|
if (reg.executionTimeoutMs == null) continue;
|
|
4895
5090
|
const deadline = now - reg.executionTimeoutMs;
|
|
4896
|
-
const inflight =
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
}),
|
|
4902
|
-
...await this.store.listRuns({
|
|
4903
|
-
workflow: reg.name,
|
|
4904
|
-
status: "suspended",
|
|
4905
|
-
namespace: this.namespace
|
|
4906
|
-
})
|
|
4907
|
-
];
|
|
5091
|
+
const inflight = await this.store.listRuns({
|
|
5092
|
+
workflow: reg.name,
|
|
5093
|
+
statuses: TIMEOUT_SWEPT_STATUSES,
|
|
5094
|
+
namespace: this.namespace
|
|
5095
|
+
});
|
|
4908
5096
|
for (const run of inflight) {
|
|
4909
5097
|
if (this.draining) return;
|
|
4910
5098
|
if (run.createdAt.getTime() > deadline) continue;
|
|
@@ -4937,48 +5125,103 @@ var WorkflowEngine = class {
|
|
|
4937
5125
|
const results = [];
|
|
4938
5126
|
for (const listed of await this.orphanedRuns(nowMs)) {
|
|
4939
5127
|
if (this.draining) break;
|
|
4940
|
-
const
|
|
4941
|
-
if (!
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
5128
|
+
const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
|
|
5129
|
+
if (!outcome) continue;
|
|
5130
|
+
if (outcome.dispatch) await this.runDispatcher.dispatch(outcome.result.runId);
|
|
5131
|
+
results.push(outcome.result);
|
|
5132
|
+
}
|
|
5133
|
+
return results;
|
|
5134
|
+
}
|
|
5135
|
+
/** A run whose still-live lease is held by this very `instanceId` (see {@link reclaimOwnLease}). */
|
|
5136
|
+
isOwnLiveLease(run, nowMs) {
|
|
5137
|
+
return run.lockedBy === this.instanceId && (run.lockedUntil ?? 0) > nowMs;
|
|
5138
|
+
}
|
|
5139
|
+
/**
|
|
5140
|
+
* Recover a run whose lease is still live but held by THIS `instanceId` and predates this process
|
|
5141
|
+
* (listed by {@link orphanedRuns}): the previous process with this stable id died without a
|
|
5142
|
+
* graceful shutdown (OOM, SIGKILL), so nothing is renewing that lease and waiting for it to expire
|
|
5143
|
+
* only delays the run. The lease is taken back with `renewRunLock` (a compare-and-set on the
|
|
5144
|
+
* owner; `tryLockRun` refuses a live lease), so the lease alone cannot fence a live execution in
|
|
5145
|
+
* THIS process from its own recovery: the in-process run gate does. A run with a
|
|
5146
|
+
* gate is executing (or about to) here and is skipped; otherwise a gate is held while the run is
|
|
5147
|
+
* recovered, so a resume landing meanwhile queues behind the recovery instead of running beside it.
|
|
5148
|
+
* The gate is released before the re-dispatch, which may resume the run on this instance.
|
|
5149
|
+
*/
|
|
5150
|
+
async reclaimOwnLease(listed, nowMs) {
|
|
5151
|
+
if (this.runGates.has(listed.id)) return void 0;
|
|
5152
|
+
let release;
|
|
5153
|
+
const tail = new Promise((resolve) => {
|
|
5154
|
+
release = resolve;
|
|
5155
|
+
});
|
|
5156
|
+
const gate = {
|
|
5157
|
+
tail
|
|
5158
|
+
};
|
|
5159
|
+
this.runGates.set(listed.id, gate);
|
|
5160
|
+
this.closeGateAfter(listed.id, gate, tail);
|
|
5161
|
+
let outcome;
|
|
5162
|
+
try {
|
|
5163
|
+
const fresh = await this.store.getRun(listed.id);
|
|
5164
|
+
const liveHere = fresh?.lockedBy === this.instanceId && (fresh.lockedUntil ?? 0) > nowMs && (fresh.lockedUntil ?? 0) >= this.ownLeaseCutoffMs;
|
|
5165
|
+
if (fresh && !liveHere) outcome = await this.recoverOrphan(listed, nowMs, true);
|
|
5166
|
+
} finally {
|
|
5167
|
+
release(outcome?.result ?? {
|
|
5168
|
+
runId: listed.id,
|
|
5169
|
+
status: listed.status
|
|
5170
|
+
});
|
|
5171
|
+
}
|
|
5172
|
+
return outcome;
|
|
5173
|
+
}
|
|
5174
|
+
/**
|
|
5175
|
+
* One run of a {@link recoverIncomplete} pass: take its lease and decide on it. Resolves the
|
|
5176
|
+
* pass's result for it and whether it must be re-dispatched, or `undefined` when it was not ours.
|
|
5177
|
+
* `ownLease`: the run's live lease is this instance id's own, left by a previous process (see
|
|
5178
|
+
* {@link reclaimOwnLease}), so it is taken back by owner rather than acquired as a free one.
|
|
5179
|
+
*/
|
|
5180
|
+
async recoverOrphan(listed, nowMs, ownLease = false) {
|
|
5181
|
+
const acquired = ownLease ? await this.store.renewRunLock(listed.id, this.instanceId, nowMs + this.leaseMs) : await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
5182
|
+
if (!acquired) return void 0;
|
|
5183
|
+
const run = await this.store.getRun(listed.id);
|
|
5184
|
+
if (!run || run.status !== "running" && run.status !== "cancelling") {
|
|
5185
|
+
await this.store.releaseRunLock(listed.id);
|
|
5186
|
+
return void 0;
|
|
5187
|
+
}
|
|
5188
|
+
if (run.status === "running") {
|
|
5189
|
+
const dispatched = await this.inFlightRemoteStep(run.id);
|
|
5190
|
+
if (dispatched) {
|
|
4970
5191
|
await this.store.updateRun(run.id, {
|
|
4971
|
-
status: "
|
|
5192
|
+
status: "suspended",
|
|
5193
|
+
wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
|
|
4972
5194
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4973
5195
|
});
|
|
5196
|
+
await this.store.releaseRunLock(run.id);
|
|
5197
|
+
return {
|
|
5198
|
+
result: {
|
|
5199
|
+
runId: run.id,
|
|
5200
|
+
status: "suspended"
|
|
5201
|
+
},
|
|
5202
|
+
dispatch: false
|
|
5203
|
+
};
|
|
4974
5204
|
}
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5205
|
+
}
|
|
5206
|
+
const settled = await this.countRecovery(run);
|
|
5207
|
+
if (settled) return {
|
|
5208
|
+
result: settled,
|
|
5209
|
+
dispatch: false
|
|
5210
|
+
};
|
|
5211
|
+
await this.store.releaseRunLock(run.id);
|
|
5212
|
+
if (run.status !== "cancelling") {
|
|
5213
|
+
await this.store.updateRun(run.id, {
|
|
5214
|
+
status: "pending",
|
|
5215
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4979
5216
|
});
|
|
4980
5217
|
}
|
|
4981
|
-
return
|
|
5218
|
+
return {
|
|
5219
|
+
result: {
|
|
5220
|
+
runId: run.id,
|
|
5221
|
+
status: run.status === "cancelling" ? "cancelling" : "pending"
|
|
5222
|
+
},
|
|
5223
|
+
dispatch: true
|
|
5224
|
+
};
|
|
4982
5225
|
}
|
|
4983
5226
|
/**
|
|
4984
5227
|
* The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
|
|
@@ -4988,12 +5231,20 @@ var WorkflowEngine = class {
|
|
|
4988
5231
|
* ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
|
|
4989
5232
|
* filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
|
|
4990
5233
|
* recovering at once spread over the batch instead of contending for the same leases.
|
|
5234
|
+
*
|
|
5235
|
+
* Also listed: runs still leased by THIS `instanceId` with a lease taken before this process
|
|
5236
|
+
* booted ({@link ownLeaseCutoffMs}). With a stable `instanceId` (pinned to the pod identity), that
|
|
5237
|
+
* is the previous process, killed without a graceful shutdown; see {@link reclaimOwnLease}.
|
|
4991
5238
|
*/
|
|
4992
5239
|
async orphanedRuns(nowMs) {
|
|
5240
|
+
const ownedBy = {
|
|
5241
|
+
owner: this.instanceId,
|
|
5242
|
+
lockedBefore: this.ownLeaseCutoffMs
|
|
5243
|
+
};
|
|
4993
5244
|
if (this.store.listOrphanedRuns) {
|
|
4994
|
-
return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
|
|
5245
|
+
return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace, ownedBy));
|
|
4995
5246
|
}
|
|
4996
|
-
return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
|
|
5247
|
+
return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs || run.lockedBy === ownedBy.owner && run.lockedUntil < ownedBy.lockedBefore).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
|
|
4997
5248
|
}
|
|
4998
5249
|
/**
|
|
4999
5250
|
* The remote step this run dispatched and is still awaiting a result for, if any — the signature
|
|
@@ -5036,6 +5287,7 @@ var WorkflowEngine = class {
|
|
|
5036
5287
|
error: error2,
|
|
5037
5288
|
recoveryAttempts: attempts
|
|
5038
5289
|
});
|
|
5290
|
+
this.notifyParent(run.id, failedCompletion(error2));
|
|
5039
5291
|
return {
|
|
5040
5292
|
runId: run.id,
|
|
5041
5293
|
status: "dead",
|
|
@@ -5242,7 +5494,7 @@ var WorkflowEngine = class {
|
|
|
5242
5494
|
error: void 0,
|
|
5243
5495
|
...resurrecting ? {
|
|
5244
5496
|
recoveryAttempts: 0,
|
|
5245
|
-
tags: run.tags
|
|
5497
|
+
tags: withoutSingletonMarkers(run.tags)
|
|
5246
5498
|
} : {},
|
|
5247
5499
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5248
5500
|
});
|
|
@@ -5654,6 +5906,16 @@ var WorkflowEngine = class {
|
|
|
5654
5906
|
return this.deliverSignal(lateWaiter, reclaimed.payload);
|
|
5655
5907
|
}
|
|
5656
5908
|
/**
|
|
5909
|
+
* Wake the run parked on `token` with `payload`, if there is one; otherwise do nothing — unlike
|
|
5910
|
+
* {@link signal}, it never buffers. For a wake-up whose real payload is ALREADY buffered on the
|
|
5911
|
+
* token (an entity op, see `Entities.kick`): a run that isn't parked reads that buffer at its next
|
|
5912
|
+
* wait, so there is nothing to keep.
|
|
5913
|
+
*/
|
|
5914
|
+
async wakeSignalWaiter(token, payload) {
|
|
5915
|
+
const waiter = await this.store.takeSignalWaiter(token);
|
|
5916
|
+
return waiter ? this.deliverSignal(waiter, payload) : null;
|
|
5917
|
+
}
|
|
5918
|
+
/**
|
|
5657
5919
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
5658
5920
|
* signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
|
|
5659
5921
|
* consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
|
|
@@ -6249,6 +6511,25 @@ var WorkflowEngine = class {
|
|
|
6249
6511
|
});
|
|
6250
6512
|
}
|
|
6251
6513
|
/** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
|
|
6514
|
+
/**
|
|
6515
|
+
* A run's first execution: `pending` -> `running`. A singleton run not yet admitted is stamped
|
|
6516
|
+
* {@link SINGLETON_TRACKED_TAG} in the same write, BEFORE it competes for a slot, so a run this
|
|
6517
|
+
* replica is about to admit or deny is never mistaken for a holder admitted by a replica older
|
|
6518
|
+
* than 0.73 (see `legacySingletonHolders`).
|
|
6519
|
+
*/
|
|
6520
|
+
async markStarted(run, singleton) {
|
|
6521
|
+
const tags = singleton ? withSingletonTracked(run.tags) : run.tags;
|
|
6522
|
+
const retag = tags !== run.tags;
|
|
6523
|
+
await this.store.updateRun(run.id, {
|
|
6524
|
+
status: "running",
|
|
6525
|
+
...retag ? {
|
|
6526
|
+
tags
|
|
6527
|
+
} : {},
|
|
6528
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
6529
|
+
});
|
|
6530
|
+
run.status = "running";
|
|
6531
|
+
if (retag) run.tags = tags;
|
|
6532
|
+
}
|
|
6252
6533
|
async singletonWaitResult(run) {
|
|
6253
6534
|
const current = await this.store.getRun(run.id);
|
|
6254
6535
|
await this.store.releaseRunLock(run.id);
|
|
@@ -6622,11 +6903,7 @@ ${run.workflowVersion}`;
|
|
|
6622
6903
|
}
|
|
6623
6904
|
const remote = registered.remote;
|
|
6624
6905
|
if (run.status === "pending") {
|
|
6625
|
-
await this.
|
|
6626
|
-
status: "running",
|
|
6627
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
6628
|
-
});
|
|
6629
|
-
run.status = "running";
|
|
6906
|
+
await this.markStarted(run, registered?.singleton !== void 0);
|
|
6630
6907
|
this.emit({
|
|
6631
6908
|
type: "run.started",
|
|
6632
6909
|
runId: run.id,
|
|
@@ -7242,11 +7519,7 @@ ${run.workflowVersion}`;
|
|
|
7242
7519
|
async runExecutionTurn(run, fn, registered) {
|
|
7243
7520
|
if (run.status === "cancelling") this.cancelRequested.add(run.id);
|
|
7244
7521
|
if (run.status === "pending") {
|
|
7245
|
-
await this.
|
|
7246
|
-
status: "running",
|
|
7247
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
7248
|
-
});
|
|
7249
|
-
run.status = "running";
|
|
7522
|
+
await this.markStarted(run, registered?.singleton !== void 0);
|
|
7250
7523
|
this.emit({
|
|
7251
7524
|
type: "run.started",
|
|
7252
7525
|
runId: run.id,
|
|
@@ -7622,6 +7895,17 @@ ${run.workflowVersion}`;
|
|
|
7622
7895
|
signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
|
|
7623
7896
|
queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
|
|
7624
7897
|
}, "signalEntity"),
|
|
7898
|
+
// The durable half of a workflow-side entity send: awaited inside the turn (a store write
|
|
7899
|
+
// only, never an inline entity turn), so the ctx can checkpoint after it. See Entities.enqueue.
|
|
7900
|
+
enqueueEntityOp: /* @__PURE__ */ __name((name, key, op, arg, id, reply) => this.entities.enqueue(name, key, op, arg, id, reply), "enqueueEntityOp"),
|
|
7901
|
+
// Start/wake the entity for what enqueueEntityOp buffered — deferred like signalEntity above,
|
|
7902
|
+
// so it never re-enters a still-executing turn. A failed wake loses nothing: the op stays
|
|
7903
|
+
// buffered for the entity's next wait (and recovery's buffered-entity wake).
|
|
7904
|
+
kickEntity: /* @__PURE__ */ __name((name, key) => {
|
|
7905
|
+
queueMicrotask(() => void this.entities.kick(name, key).catch((err) => {
|
|
7906
|
+
console.warn(`[nestjs-durable] waking entity "${name}" key "${key}" failed (its op stays buffered for the next wake):`, err);
|
|
7907
|
+
}));
|
|
7908
|
+
}, "kickEntity"),
|
|
7625
7909
|
interceptStep: /* @__PURE__ */ __name((invocation, body) => this.interceptStep(invocation, body), "interceptStep")
|
|
7626
7910
|
};
|
|
7627
7911
|
}
|
|
@@ -7763,7 +8047,7 @@ ${run.workflowVersion}`;
|
|
|
7763
8047
|
"blocked"
|
|
7764
8048
|
]
|
|
7765
8049
|
});
|
|
7766
|
-
return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
|
|
8050
|
+
return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || poolNamespace(r.namespace) === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
|
|
7767
8051
|
}
|
|
7768
8052
|
async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
|
|
7769
8053
|
const group = stepGroup(step, routing?.namespace);
|
|
@@ -8302,7 +8586,7 @@ ${run.workflowVersion}`;
|
|
|
8302
8586
|
const run = await this.store.getRun(decision.runId);
|
|
8303
8587
|
if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
|
|
8304
8588
|
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
|
|
8305
|
-
if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
|
|
8589
|
+
if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
|
|
8306
8590
|
return;
|
|
8307
8591
|
}
|
|
8308
8592
|
const nowMs = this.clock();
|
|
@@ -9142,7 +9426,7 @@ var CodecStateStore = class {
|
|
|
9142
9426
|
const deleteRuns = inner.deleteRuns;
|
|
9143
9427
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
9144
9428
|
const orphans = inner.listOrphanedRuns;
|
|
9145
|
-
if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
|
|
9429
|
+
if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace, ownedBy) => (await orphans.call(inner, nowMs, limit, namespace, ownedBy)).map((r) => this.decRun(r));
|
|
9146
9430
|
const updateIf = inner.updateRunIf;
|
|
9147
9431
|
if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
|
|
9148
9432
|
const saveIfPending = inner.saveCheckpointIfPending;
|
|
@@ -9663,7 +9947,7 @@ var InMemoryStateStore = class {
|
|
|
9663
9947
|
async createRun(run) {
|
|
9664
9948
|
this.runs.set(run.id, {
|
|
9665
9949
|
...run,
|
|
9666
|
-
namespace: run.namespace
|
|
9950
|
+
namespace: run.namespace || "default"
|
|
9667
9951
|
});
|
|
9668
9952
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
9669
9953
|
}
|
|
@@ -9678,7 +9962,7 @@ var InMemoryStateStore = class {
|
|
|
9678
9962
|
applyRunPatch(runId, inputPatch) {
|
|
9679
9963
|
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
9680
9964
|
...inputPatch,
|
|
9681
|
-
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
|
|
9965
|
+
tags: withoutSingletonMarkers("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
|
|
9682
9966
|
} : inputPatch;
|
|
9683
9967
|
const existing = this.runs.get(runId);
|
|
9684
9968
|
if (!existing) throw new Error(`run ${runId} not found`);
|
|
@@ -9766,10 +10050,10 @@ var InMemoryStateStore = class {
|
|
|
9766
10050
|
return true;
|
|
9767
10051
|
});
|
|
9768
10052
|
}
|
|
9769
|
-
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
10053
|
+
async listOrphanedRuns(nowMs, limit, namespace, ownedBy) {
|
|
9770
10054
|
return [
|
|
9771
10055
|
...this.runs.values()
|
|
9772
|
-
].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
|
|
10056
|
+
].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs || ownedBy !== void 0 && r.lockedBy === ownedBy.owner && r.lockedUntil < ownedBy.lockedBefore) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
|
|
9773
10057
|
...r
|
|
9774
10058
|
}));
|
|
9775
10059
|
}
|
|
@@ -10161,6 +10445,7 @@ export {
|
|
|
10161
10445
|
DURABLE_STEP_NAME,
|
|
10162
10446
|
DurableWorkflow,
|
|
10163
10447
|
ENGINE_MINTED_TAG_PREFIXES,
|
|
10448
|
+
ENTITY_WORKFLOW,
|
|
10164
10449
|
EVENT_BUFFER_SCAN_LIMIT,
|
|
10165
10450
|
EVENT_TOKEN_PREFIX,
|
|
10166
10451
|
Entities,
|
|
@@ -10186,6 +10471,8 @@ export {
|
|
|
10186
10471
|
RunGateway,
|
|
10187
10472
|
SINGLETON_ACTIVE_STATUSES,
|
|
10188
10473
|
SINGLETON_ADMITTED_TAG,
|
|
10474
|
+
SINGLETON_RESERVED_TAGS,
|
|
10475
|
+
SINGLETON_TRACKED_TAG,
|
|
10189
10476
|
STATE_STORE,
|
|
10190
10477
|
STATE_STORE_CANONICAL,
|
|
10191
10478
|
SUMMARY_CHILD_LIMIT,
|
|
@@ -10261,6 +10548,7 @@ export {
|
|
|
10261
10548
|
isLegacyDescriptor,
|
|
10262
10549
|
isNonEmptyRunScope,
|
|
10263
10550
|
isWorkflowControlFlowSignal,
|
|
10551
|
+
legacySingletonHolders,
|
|
10264
10552
|
listSignalWaitersOfRuns,
|
|
10265
10553
|
liveHandoff,
|
|
10266
10554
|
log,
|
|
@@ -10278,8 +10566,10 @@ export {
|
|
|
10278
10566
|
pageFanFromStore,
|
|
10279
10567
|
parentRunIdOf,
|
|
10280
10568
|
parseDuration,
|
|
10569
|
+
parseWebhookToken,
|
|
10281
10570
|
partitionOfToken,
|
|
10282
10571
|
planDispatch,
|
|
10572
|
+
poolNamespace,
|
|
10283
10573
|
prevCronFireMs,
|
|
10284
10574
|
queryCheckpointsOf,
|
|
10285
10575
|
readSearchAttributes,
|
|
@@ -10315,6 +10605,8 @@ export {
|
|
|
10315
10605
|
tenantGroup,
|
|
10316
10606
|
useDurableExecution,
|
|
10317
10607
|
warn,
|
|
10608
|
+
withSingletonTracked,
|
|
10609
|
+
withoutSingletonMarkers,
|
|
10318
10610
|
workflowName
|
|
10319
10611
|
};
|
|
10320
10612
|
//# sourceMappingURL=index.js.map
|