@dudousxd/nestjs-durable-core 0.43.0 → 0.44.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 +135 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -13
- package/dist/index.d.ts +75 -13
- package/dist/index.js +135 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -780,33 +780,18 @@ var RemoteWorkflowExecutor = class {
|
|
|
780
780
|
}
|
|
781
781
|
transport;
|
|
782
782
|
group;
|
|
783
|
-
|
|
784
|
-
pending = /* @__PURE__ */ new Map();
|
|
785
|
-
subscribed = false;
|
|
786
|
-
constructor(transport, group, opts = {}) {
|
|
783
|
+
constructor(transport, group, _opts = {}) {
|
|
787
784
|
this.transport = transport;
|
|
788
785
|
this.group = group;
|
|
789
|
-
this.opts = opts;
|
|
790
|
-
}
|
|
791
|
-
ensureSubscribed() {
|
|
792
|
-
if (this.subscribed) return;
|
|
793
|
-
if (!this.transport.onDecision) {
|
|
794
|
-
throw new Error("transport does not support workflow decisions (onDecision)");
|
|
795
|
-
}
|
|
796
|
-
this.subscribed = true;
|
|
797
|
-
this.transport.onDecision(async (decision) => {
|
|
798
|
-
const resolve = this.pending.get(decision.taskId);
|
|
799
|
-
if (resolve) {
|
|
800
|
-
this.pending.delete(decision.taskId);
|
|
801
|
-
resolve(decision);
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
786
|
}
|
|
805
|
-
|
|
787
|
+
/**
|
|
788
|
+
* Dispatch one workflow turn and return its `taskId`. Does NOT await the decision — the engine
|
|
789
|
+
* suspends the run on the returned `taskId` and applies the decision durably via `onDecision`.
|
|
790
|
+
*/
|
|
791
|
+
async dispatch(run, history, pendingSignals) {
|
|
806
792
|
if (!this.transport.dispatchWorkflowTask) {
|
|
807
793
|
throw new Error("transport does not support workflow tasks (dispatchWorkflowTask)");
|
|
808
794
|
}
|
|
809
|
-
this.ensureSubscribed();
|
|
810
795
|
taskCounter += 1;
|
|
811
796
|
const taskId = `${run.id}:wf:${taskCounter}`;
|
|
812
797
|
const task = {
|
|
@@ -816,23 +801,17 @@ var RemoteWorkflowExecutor = class {
|
|
|
816
801
|
workflowVersion: run.workflowVersion,
|
|
817
802
|
input: run.input,
|
|
818
803
|
history,
|
|
804
|
+
...pendingSignals ? {
|
|
805
|
+
pendingSignals
|
|
806
|
+
} : {},
|
|
819
807
|
group: this.group,
|
|
820
808
|
priority: run.priority,
|
|
821
809
|
attempt: 1
|
|
822
810
|
};
|
|
823
|
-
const decision = new Promise((resolve, reject) => {
|
|
824
|
-
this.pending.set(taskId, resolve);
|
|
825
|
-
const { timeoutMs } = this.opts;
|
|
826
|
-
if (timeoutMs) {
|
|
827
|
-
const timer = setTimeout(() => {
|
|
828
|
-
this.pending.delete(taskId);
|
|
829
|
-
reject(new RemoteWorkflowTimeout(taskId, timeoutMs));
|
|
830
|
-
}, timeoutMs);
|
|
831
|
-
timer.unref?.();
|
|
832
|
-
}
|
|
833
|
-
});
|
|
834
811
|
await this.transport.dispatchWorkflowTask(task);
|
|
835
|
-
return
|
|
812
|
+
return {
|
|
813
|
+
taskId
|
|
814
|
+
};
|
|
836
815
|
}
|
|
837
816
|
};
|
|
838
817
|
|
|
@@ -953,13 +932,16 @@ var TransportPool = class {
|
|
|
953
932
|
/** Register the engine's result/heartbeat/step-event handlers on EVERY transport — a result can come
|
|
954
933
|
* back on whichever one delivered the task, so failover stays symmetric. `onStepEvent` is wired only
|
|
955
934
|
* on transports that carry streamed workflow step lifecycle (BullMQ); others simply skip it. */
|
|
956
|
-
bind(onResult, onHeartbeat, onStepEvent) {
|
|
935
|
+
bind(onResult, onHeartbeat, onStepEvent, onDecision) {
|
|
957
936
|
for (const { transport } of this.transports) {
|
|
958
937
|
transport.onResult(onResult);
|
|
959
938
|
transport.onHeartbeat(onHeartbeat);
|
|
960
939
|
if (onStepEvent && transport.onStepEvent) {
|
|
961
940
|
transport.onStepEvent(onStepEvent);
|
|
962
941
|
}
|
|
942
|
+
if (onDecision && transport.onDecision) {
|
|
943
|
+
transport.onDecision(onDecision);
|
|
944
|
+
}
|
|
963
945
|
}
|
|
964
946
|
}
|
|
965
947
|
/**
|
|
@@ -1717,14 +1699,27 @@ var WorkflowEngine = class {
|
|
|
1717
1699
|
},
|
|
1718
1700
|
// A heartbeat resets the liveness window for whatever it targets: an in-flight long STEP (keyed
|
|
1719
1701
|
// by stepId, see callRemote) or — when stepId is absent — the in-flight workflow TURN (keyed by
|
|
1720
|
-
// runId
|
|
1702
|
+
// runId). For an INLINE-advance turn the reset is an in-memory callback; for a DISPATCH-suspend
|
|
1703
|
+
// turn there is no in-memory waiter, so a run-scoped beat rearms the suspended turn's durable
|
|
1704
|
+
// re-drive deadline (`wakeAt`) instead, keeping a live worker's turn from being re-dispatched.
|
|
1721
1705
|
async (beat) => {
|
|
1722
|
-
this.heartbeatResets.get(beat.stepId ?? beat.runId)
|
|
1706
|
+
const reset = this.heartbeatResets.get(beat.stepId ?? beat.runId);
|
|
1707
|
+
if (reset) {
|
|
1708
|
+
reset();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (beat.stepId === void 0) await this.rearmDecisionDeadline(beat.runId);
|
|
1723
1712
|
},
|
|
1724
1713
|
// A remote workflow worker streams each local step's lifecycle (running → completed/failed) so
|
|
1725
1714
|
// it's checkpointed live, not all-at-once when the long turn ends.
|
|
1726
1715
|
async (event) => {
|
|
1727
1716
|
await this.persistStepEvent(event);
|
|
1717
|
+
},
|
|
1718
|
+
// A workflow-turn DECISION delivered over the broker — applied durably (by run id) on whatever
|
|
1719
|
+
// instance consumes it, NOT correlated to an in-memory promise on the dispatcher. This is the
|
|
1720
|
+
// multi-instance fix: a point-to-point broker may hand the decision to a non-dispatching instance.
|
|
1721
|
+
async (decision) => {
|
|
1722
|
+
await this.completeRemoteDecision(decision);
|
|
1728
1723
|
}
|
|
1729
1724
|
);
|
|
1730
1725
|
this.controlPlane?.onControl((msg) => {
|
|
@@ -2353,7 +2348,7 @@ var WorkflowEngine = class {
|
|
|
2353
2348
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
2354
2349
|
*/
|
|
2355
2350
|
waitForRun(runId, opts) {
|
|
2356
|
-
const isSettled = /* @__PURE__ */ __name((
|
|
2351
|
+
const isSettled = /* @__PURE__ */ __name((run) => run.status !== "pending" && run.status !== "running" && run.status !== "cancelling" && !(run.status === "suspended" && run.awaitingDecisionTaskId !== void 0), "isSettled");
|
|
2357
2352
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
2358
2353
|
runId,
|
|
2359
2354
|
status: run.status,
|
|
@@ -2379,7 +2374,7 @@ var WorkflowEngine = class {
|
|
|
2379
2374
|
}, "finish");
|
|
2380
2375
|
const check = /* @__PURE__ */ __name(() => {
|
|
2381
2376
|
void this.store.getRun(runId).then((run) => {
|
|
2382
|
-
if (run && isSettled(run
|
|
2377
|
+
if (run && isSettled(run)) finish(run);
|
|
2383
2378
|
});
|
|
2384
2379
|
}, "check");
|
|
2385
2380
|
off = this.subscribe((ev) => {
|
|
@@ -2986,10 +2981,10 @@ var WorkflowEngine = class {
|
|
|
2986
2981
|
});
|
|
2987
2982
|
}
|
|
2988
2983
|
if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
2989
|
-
const
|
|
2984
|
+
const wakeAt = this.singletons.retryWakeAt();
|
|
2990
2985
|
await this.store.updateRun(run.id, {
|
|
2991
2986
|
status: "suspended",
|
|
2992
|
-
wakeAt
|
|
2987
|
+
wakeAt,
|
|
2993
2988
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2994
2989
|
});
|
|
2995
2990
|
this.emit({
|
|
@@ -3004,10 +2999,45 @@ var WorkflowEngine = class {
|
|
|
3004
2999
|
};
|
|
3005
3000
|
}
|
|
3006
3001
|
const history = await this.remoteHistory(run.id);
|
|
3002
|
+
if (remote.executor.dispatch) {
|
|
3003
|
+
const { taskId } = await remote.executor.dispatch(run, history);
|
|
3004
|
+
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
3005
|
+
const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
|
|
3006
|
+
const fresh = await this.store.getRun(run.id);
|
|
3007
|
+
if (fresh && (fresh.status === "cancelled" || fresh.status === "completed" || fresh.status === "dead")) {
|
|
3008
|
+
await this.store.releaseRunLock(run.id);
|
|
3009
|
+
return {
|
|
3010
|
+
runId: run.id,
|
|
3011
|
+
status: fresh.status,
|
|
3012
|
+
output: fresh.output,
|
|
3013
|
+
error: fresh.error
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
await this.store.updateRun(run.id, {
|
|
3017
|
+
status: "suspended",
|
|
3018
|
+
awaitingDecisionTaskId: taskId,
|
|
3019
|
+
wakeAt,
|
|
3020
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3021
|
+
});
|
|
3022
|
+
this.emit({
|
|
3023
|
+
type: "run.suspended",
|
|
3024
|
+
runId: run.id,
|
|
3025
|
+
workflow: run.workflow
|
|
3026
|
+
});
|
|
3027
|
+
await this.store.releaseRunLock(run.id);
|
|
3028
|
+
return {
|
|
3029
|
+
runId: run.id,
|
|
3030
|
+
status: "suspended"
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
const advanceExecutor = remote.executor.advance;
|
|
3034
|
+
if (!advanceExecutor) {
|
|
3035
|
+
throw new Error(`workflow ${run.workflow}@${run.workflowVersion}: executor implements neither advance nor dispatch`);
|
|
3036
|
+
}
|
|
3007
3037
|
let decision;
|
|
3008
3038
|
try {
|
|
3009
3039
|
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
3010
|
-
const advance = remote.executor
|
|
3040
|
+
const advance = advanceExecutor.call(remote.executor, run, history);
|
|
3011
3041
|
decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
|
|
3012
3042
|
} catch (err) {
|
|
3013
3043
|
if (err instanceof RemoteWorkflowTimeout) {
|
|
@@ -3038,6 +3068,16 @@ var WorkflowEngine = class {
|
|
|
3038
3068
|
error
|
|
3039
3069
|
};
|
|
3040
3070
|
}
|
|
3071
|
+
return this.applyDecision(run, decision);
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Apply a remote workflow turn's {@link WorkflowDecision}: persist the local steps it ran, then
|
|
3075
|
+
* settle (completed/failed/cancelled) or dispatch its blocking ops and suspend (continue). Shared by
|
|
3076
|
+
* the INLINE-advance path ({@link runRemoteExecution}, under the held lease) and the durable
|
|
3077
|
+
* DISPATCH path ({@link completeRemoteDecision}, which holds the lease itself). Idempotent transitions
|
|
3078
|
+
* — the caller has already ensured this is the decision for the currently-awaited turn.
|
|
3079
|
+
*/
|
|
3080
|
+
async applyDecision(run, decision) {
|
|
3041
3081
|
if (decision.status === "completed") {
|
|
3042
3082
|
await this.applyCommands(run, decision.commands);
|
|
3043
3083
|
return this.settleRun(run, {
|
|
@@ -3635,6 +3675,60 @@ var WorkflowEngine = class {
|
|
|
3635
3675
|
});
|
|
3636
3676
|
}
|
|
3637
3677
|
/**
|
|
3678
|
+
* Apply a remote workflow-turn {@link WorkflowDecision} durably — runs on WHATEVER instance receives
|
|
3679
|
+
* it over the broker (the dispatching one may be gone, or a point-to-point broker handed it to a
|
|
3680
|
+
* non-dispatcher). The durable, multi-instance-safe twin of {@link completeRemoteResult} for the
|
|
3681
|
+
* workflow TURN: look the run up by `decision.runId`, verify the decision matches the run's
|
|
3682
|
+
* CURRENTLY-awaited turn (`awaitingDecisionTaskId`) — so a stale / duplicate / foreign / re-driven
|
|
3683
|
+
* decision is dropped (applied AT MOST ONCE) — lease it, clear the marker, and apply the decision.
|
|
3684
|
+
*/
|
|
3685
|
+
async completeRemoteDecision(decision) {
|
|
3686
|
+
const run = await this.store.getRun(decision.runId);
|
|
3687
|
+
if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
|
|
3688
|
+
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
|
|
3689
|
+
if (run.namespace !== void 0 && run.namespace !== this.namespace) return;
|
|
3690
|
+
const nowMs = this.clock();
|
|
3691
|
+
if (!await this.store.tryLockRun(decision.runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
const renew = setInterval(() => {
|
|
3695
|
+
void this.store.renewRunLock(decision.runId, this.instanceId, this.clock() + this.leaseMs).catch(() => void 0);
|
|
3696
|
+
}, Math.max(50, Math.floor(this.leaseMs / 2)));
|
|
3697
|
+
renew.unref?.();
|
|
3698
|
+
try {
|
|
3699
|
+
const fresh = await this.store.getRun(decision.runId);
|
|
3700
|
+
if (!fresh || fresh.awaitingDecisionTaskId !== decision.taskId) return;
|
|
3701
|
+
if (fresh.status === "cancelled" || fresh.status === "completed" || fresh.status === "dead") {
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
await this.store.updateRun(decision.runId, {
|
|
3705
|
+
awaitingDecisionTaskId: void 0,
|
|
3706
|
+
wakeAt: void 0,
|
|
3707
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3708
|
+
});
|
|
3709
|
+
fresh.awaitingDecisionTaskId = void 0;
|
|
3710
|
+
await this.applyDecision(fresh, decision);
|
|
3711
|
+
} finally {
|
|
3712
|
+
clearInterval(renew);
|
|
3713
|
+
await this.store.releaseRunLock(decision.runId);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
/**
|
|
3717
|
+
* A run-scoped heartbeat for a run suspended awaiting a workflow-turn decision (the dispatch path):
|
|
3718
|
+
* push its durable re-drive deadline (`wakeAt`) forward so a still-working worker's turn is not
|
|
3719
|
+
* re-dispatched. The durable counterpart of an inline turn's in-memory `heartbeatResets` rearm.
|
|
3720
|
+
*/
|
|
3721
|
+
async rearmDecisionDeadline(runId) {
|
|
3722
|
+
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
3723
|
+
if (silenceMs == null) return;
|
|
3724
|
+
const run = await this.store.getRun(runId);
|
|
3725
|
+
if (!run || run.status !== "suspended" || run.awaitingDecisionTaskId === void 0) return;
|
|
3726
|
+
await this.store.updateRun(runId, {
|
|
3727
|
+
wakeAt: this.clock() + silenceMs,
|
|
3728
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3638
3732
|
* A slot freed on `queue` (a fleet-wide signal): resume this instance's runs blocked on it so they
|
|
3639
3733
|
* re-contend now instead of at their retry tick. Snapshot-and-clear — a run still blocked after the
|
|
3640
3734
|
* retry re-registers itself, and one that's gone (cancelled/admitted) is dropped. Best-effort.
|