@dudousxd/nestjs-durable-core 0.33.0 → 0.35.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 +136 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -7
- package/dist/index.d.ts +51 -7
- package/dist/index.js +136 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -804,7 +804,8 @@ var SingletonGate = class {
|
|
|
804
804
|
statuses: [
|
|
805
805
|
"pending",
|
|
806
806
|
"running",
|
|
807
|
-
"suspended"
|
|
807
|
+
"suspended",
|
|
808
|
+
"cancelling"
|
|
808
809
|
]
|
|
809
810
|
});
|
|
810
811
|
if (queued.length >= cap) {
|
|
@@ -822,7 +823,8 @@ var SingletonGate = class {
|
|
|
822
823
|
workflow: run.workflow,
|
|
823
824
|
statuses: [
|
|
824
825
|
"running",
|
|
825
|
-
"suspended"
|
|
826
|
+
"suspended",
|
|
827
|
+
"cancelling"
|
|
826
828
|
]
|
|
827
829
|
})).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
828
830
|
const idx = inflight.findIndex((r) => r.id === run.id);
|
|
@@ -1502,6 +1504,7 @@ var WorkflowEngine = class {
|
|
|
1502
1504
|
instanceId;
|
|
1503
1505
|
leaseMs;
|
|
1504
1506
|
maxRecoveryAttempts;
|
|
1507
|
+
remoteAdvanceSilenceMs;
|
|
1505
1508
|
webhookUrl;
|
|
1506
1509
|
traceparent;
|
|
1507
1510
|
context;
|
|
@@ -1526,7 +1529,9 @@ var WorkflowEngine = class {
|
|
|
1526
1529
|
accumulators;
|
|
1527
1530
|
/** In-flight remote steps awaiting a worker result, keyed by stepId. */
|
|
1528
1531
|
pending = /* @__PURE__ */ new Map();
|
|
1529
|
-
/**
|
|
1532
|
+
/** "Reset the liveness timer" callbacks keyed by the beat's target — a stepId (in-flight remote step)
|
|
1533
|
+
* or a runId (in-flight remote workflow turn) — invoked when a matching heartbeat arrives. The two id
|
|
1534
|
+
* shapes don't collide in practice: a stepId is `${runId}:${seq}`, a run key is the bare runId. */
|
|
1530
1535
|
heartbeatResets = /* @__PURE__ */ new Map();
|
|
1531
1536
|
listeners = /* @__PURE__ */ new Set();
|
|
1532
1537
|
/** Step interceptors (onion middleware around real local-step execution), first = outermost. */
|
|
@@ -1565,6 +1570,7 @@ var WorkflowEngine = class {
|
|
|
1565
1570
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
1566
1571
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
1567
1572
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1573
|
+
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1568
1574
|
this.webhookUrl = deps.webhookUrl;
|
|
1569
1575
|
this.traceparent = deps.traceparent;
|
|
1570
1576
|
this.context = deps.context;
|
|
@@ -1601,9 +1607,11 @@ var WorkflowEngine = class {
|
|
|
1601
1607
|
}
|
|
1602
1608
|
await this.completeRemoteResult(result);
|
|
1603
1609
|
},
|
|
1604
|
-
// A heartbeat for an in-flight long
|
|
1610
|
+
// A heartbeat resets the liveness window for whatever it targets: an in-flight long STEP (keyed
|
|
1611
|
+
// by stepId, see callRemote) or — when stepId is absent — the in-flight workflow TURN (keyed by
|
|
1612
|
+
// runId, see runRemoteExecution's heartbeat-rearmed advance).
|
|
1605
1613
|
async (beat) => {
|
|
1606
|
-
this.heartbeatResets.get(beat.stepId)?.();
|
|
1614
|
+
this.heartbeatResets.get(beat.stepId ?? beat.runId)?.();
|
|
1607
1615
|
},
|
|
1608
1616
|
// A remote workflow worker streams each local step's lifecycle (running → completed/failed) so
|
|
1609
1617
|
// it's checkpointed live, not all-at-once when the long turn ends.
|
|
@@ -1965,14 +1973,16 @@ var WorkflowEngine = class {
|
|
|
1965
1973
|
continue;
|
|
1966
1974
|
}
|
|
1967
1975
|
await this.store.releaseRunLock(run.id);
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1976
|
+
if (run.status !== "cancelling") {
|
|
1977
|
+
await this.store.updateRun(run.id, {
|
|
1978
|
+
status: "pending",
|
|
1979
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1972
1982
|
await this.runDispatcher.dispatch(run.id);
|
|
1973
1983
|
results.push({
|
|
1974
1984
|
runId: run.id,
|
|
1975
|
-
status: "pending"
|
|
1985
|
+
status: run.status === "cancelling" ? "cancelling" : "pending"
|
|
1976
1986
|
});
|
|
1977
1987
|
}
|
|
1978
1988
|
return results;
|
|
@@ -2091,7 +2101,7 @@ var WorkflowEngine = class {
|
|
|
2091
2101
|
* `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
|
|
2092
2102
|
*/
|
|
2093
2103
|
waitForRun(runId, opts) {
|
|
2094
|
-
const isSettled = /* @__PURE__ */ __name((s) => s !== "pending" && s !== "running", "isSettled");
|
|
2104
|
+
const isSettled = /* @__PURE__ */ __name((s) => s !== "pending" && s !== "running" && s !== "cancelling", "isSettled");
|
|
2095
2105
|
const toResult = /* @__PURE__ */ __name((run) => ({
|
|
2096
2106
|
runId,
|
|
2097
2107
|
status: run.status,
|
|
@@ -2255,8 +2265,19 @@ var WorkflowEngine = class {
|
|
|
2255
2265
|
error: run.error
|
|
2256
2266
|
};
|
|
2257
2267
|
}
|
|
2268
|
+
if (run.status === "cancelling") {
|
|
2269
|
+
return {
|
|
2270
|
+
runId,
|
|
2271
|
+
status: "cancelling"
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2258
2274
|
if (opts?.compensate && (run.status === "suspended" || run.status === "running")) {
|
|
2259
2275
|
this.cancelRequested.add(runId);
|
|
2276
|
+
await this.store.updateRun(runId, {
|
|
2277
|
+
status: "cancelling",
|
|
2278
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2279
|
+
});
|
|
2280
|
+
run.status = "cancelling";
|
|
2260
2281
|
if (this.controlPlane) {
|
|
2261
2282
|
void this.controlPlane.publishControl({
|
|
2262
2283
|
kind: "cancel",
|
|
@@ -2270,7 +2291,7 @@ var WorkflowEngine = class {
|
|
|
2270
2291
|
await this.cancelChildren(runId, opts);
|
|
2271
2292
|
return {
|
|
2272
2293
|
runId,
|
|
2273
|
-
status:
|
|
2294
|
+
status: "cancelling"
|
|
2274
2295
|
};
|
|
2275
2296
|
}
|
|
2276
2297
|
const error = {
|
|
@@ -2661,6 +2682,36 @@ var WorkflowEngine = class {
|
|
|
2661
2682
|
};
|
|
2662
2683
|
}
|
|
2663
2684
|
async runRemoteExecution(run, registered) {
|
|
2685
|
+
if (run.status === "cancelling") {
|
|
2686
|
+
const error = {
|
|
2687
|
+
message: "cancelled"
|
|
2688
|
+
};
|
|
2689
|
+
await this.store.updateRun(run.id, {
|
|
2690
|
+
status: "cancelled",
|
|
2691
|
+
error,
|
|
2692
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2693
|
+
});
|
|
2694
|
+
this.emit({
|
|
2695
|
+
type: "run.failed",
|
|
2696
|
+
runId: run.id,
|
|
2697
|
+
workflow: run.workflow,
|
|
2698
|
+
error
|
|
2699
|
+
});
|
|
2700
|
+
this.notifyCancelled(run.id);
|
|
2701
|
+
if (this.controlPlane) {
|
|
2702
|
+
void this.controlPlane.publishControl({
|
|
2703
|
+
kind: "cancel",
|
|
2704
|
+
runId: run.id,
|
|
2705
|
+
from: this.instanceId
|
|
2706
|
+
}).catch(() => void 0);
|
|
2707
|
+
}
|
|
2708
|
+
await this.store.releaseRunLock(run.id);
|
|
2709
|
+
return {
|
|
2710
|
+
runId: run.id,
|
|
2711
|
+
status: "cancelled",
|
|
2712
|
+
error
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2664
2715
|
const remote = registered.remote;
|
|
2665
2716
|
if (run.status === "pending") {
|
|
2666
2717
|
await this.store.updateRun(run.id, {
|
|
@@ -2695,7 +2746,9 @@ var WorkflowEngine = class {
|
|
|
2695
2746
|
const history = await this.remoteHistory(run.id);
|
|
2696
2747
|
let decision;
|
|
2697
2748
|
try {
|
|
2698
|
-
|
|
2749
|
+
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
2750
|
+
const advance = remote.executor.advance(run, history);
|
|
2751
|
+
decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
|
|
2699
2752
|
} catch (err) {
|
|
2700
2753
|
if (err instanceof RemoteWorkflowTimeout) {
|
|
2701
2754
|
await this.store.releaseRunLock(run.id);
|
|
@@ -2752,6 +2805,15 @@ var WorkflowEngine = class {
|
|
|
2752
2805
|
status: "cancelled"
|
|
2753
2806
|
};
|
|
2754
2807
|
}
|
|
2808
|
+
const fresh = await this.store.getRun(run.id);
|
|
2809
|
+
if (fresh && (fresh.status === "cancelled" || fresh.status === "completed" || fresh.status === "dead")) {
|
|
2810
|
+
return {
|
|
2811
|
+
runId: run.id,
|
|
2812
|
+
status: fresh.status,
|
|
2813
|
+
output: fresh.output,
|
|
2814
|
+
error: fresh.error
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2755
2817
|
const wakeAt = await this.applyCommands(run, decision.commands);
|
|
2756
2818
|
return this.settleRun(run, {
|
|
2757
2819
|
kind: "suspended",
|
|
@@ -2919,8 +2981,41 @@ var WorkflowEngine = class {
|
|
|
2919
2981
|
}
|
|
2920
2982
|
return wakeAt;
|
|
2921
2983
|
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
|
|
2986
|
+
* exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
|
|
2987
|
+
* suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
|
|
2988
|
+
* stop at a suspension point. Clears the in-memory cancel flag.
|
|
2989
|
+
*/
|
|
2990
|
+
async compensateAndCancel(run, compensations) {
|
|
2991
|
+
this.cancelRequested.delete(run.id);
|
|
2992
|
+
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
2993
|
+
const comp = compensations[i];
|
|
2994
|
+
if (comp) await this.runCompensation(run, comp);
|
|
2995
|
+
}
|
|
2996
|
+
const error = {
|
|
2997
|
+
message: "cancelled"
|
|
2998
|
+
};
|
|
2999
|
+
await this.store.updateRun(run.id, {
|
|
3000
|
+
status: "cancelled",
|
|
3001
|
+
error,
|
|
3002
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3003
|
+
});
|
|
3004
|
+
this.emit({
|
|
3005
|
+
type: "run.failed",
|
|
3006
|
+
runId: run.id,
|
|
3007
|
+
workflow: run.workflow,
|
|
3008
|
+
error
|
|
3009
|
+
});
|
|
3010
|
+
return {
|
|
3011
|
+
runId: run.id,
|
|
3012
|
+
status: "cancelled",
|
|
3013
|
+
error
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
2922
3016
|
/** The run body, lease held + renewed by {@link execute}. */
|
|
2923
3017
|
async runExecution(run, fn, registered) {
|
|
3018
|
+
if (run.status === "cancelling") this.cancelRequested.add(run.id);
|
|
2924
3019
|
if (run.status === "pending") {
|
|
2925
3020
|
await this.store.updateRun(run.id, {
|
|
2926
3021
|
status: "running",
|
|
@@ -2933,7 +3028,7 @@ var WorkflowEngine = class {
|
|
|
2933
3028
|
workflow: run.workflow
|
|
2934
3029
|
});
|
|
2935
3030
|
}
|
|
2936
|
-
if (registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
3031
|
+
if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
2937
3032
|
const wakeAt = this.singletons.retryWakeAt();
|
|
2938
3033
|
await this.store.updateRun(run.id, {
|
|
2939
3034
|
status: "suspended",
|
|
@@ -2958,12 +3053,14 @@ var WorkflowEngine = class {
|
|
|
2958
3053
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
|
|
2959
3054
|
try {
|
|
2960
3055
|
const output = await fn(ctx, run.input);
|
|
3056
|
+
if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
|
|
2961
3057
|
return this.settleRun(run, {
|
|
2962
3058
|
kind: "completed",
|
|
2963
3059
|
output
|
|
2964
3060
|
});
|
|
2965
3061
|
} catch (err) {
|
|
2966
3062
|
if (err instanceof ContinueAsNew) {
|
|
3063
|
+
if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
|
|
2967
3064
|
await this.store.updateRun(run.id, {
|
|
2968
3065
|
status: "completed",
|
|
2969
3066
|
output: void 0,
|
|
@@ -2987,37 +3084,13 @@ var WorkflowEngine = class {
|
|
|
2987
3084
|
};
|
|
2988
3085
|
}
|
|
2989
3086
|
if (err instanceof WorkflowSuspended) {
|
|
2990
|
-
if (this.cancelRequested.has(run.id))
|
|
2991
|
-
this.cancelRequested.delete(run.id);
|
|
2992
|
-
for (let i = compensations.length - 1; i >= 0; i -= 1) {
|
|
2993
|
-
const comp = compensations[i];
|
|
2994
|
-
if (comp) await this.runCompensation(run, comp);
|
|
2995
|
-
}
|
|
2996
|
-
const error2 = {
|
|
2997
|
-
message: "cancelled"
|
|
2998
|
-
};
|
|
2999
|
-
await this.store.updateRun(run.id, {
|
|
3000
|
-
status: "cancelled",
|
|
3001
|
-
error: error2,
|
|
3002
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
3003
|
-
});
|
|
3004
|
-
this.emit({
|
|
3005
|
-
type: "run.failed",
|
|
3006
|
-
runId: run.id,
|
|
3007
|
-
workflow: run.workflow,
|
|
3008
|
-
error: error2
|
|
3009
|
-
});
|
|
3010
|
-
return {
|
|
3011
|
-
runId: run.id,
|
|
3012
|
-
status: "cancelled",
|
|
3013
|
-
error: error2
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3087
|
+
if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
|
|
3016
3088
|
return this.settleRun(run, {
|
|
3017
3089
|
kind: "suspended",
|
|
3018
3090
|
wakeAt: err.wakeAt
|
|
3019
3091
|
});
|
|
3020
3092
|
}
|
|
3093
|
+
if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
|
|
3021
3094
|
const error = {
|
|
3022
3095
|
message: err instanceof Error ? err.message : String(err),
|
|
3023
3096
|
stack: err instanceof Error ? err.stack : void 0
|
|
@@ -3356,11 +3429,13 @@ var WorkflowEngine = class {
|
|
|
3356
3429
|
}
|
|
3357
3430
|
}
|
|
3358
3431
|
/**
|
|
3359
|
-
* Await
|
|
3360
|
-
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat
|
|
3361
|
-
* window, so a worker that keeps beating stays alive past
|
|
3432
|
+
* Await `resultPromise`, but reject with `onTimeout()` if neither it settles nor a heartbeat for `id`
|
|
3433
|
+
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`, looked up in
|
|
3434
|
+
* `heartbeatResets` by `id`) REARMS the window, so a worker that keeps beating stays alive past
|
|
3435
|
+
* `timeoutMs`. Shared by the remote-STEP liveness (keyed by stepId) and the remote-WORKFLOW advance
|
|
3436
|
+
* liveness (keyed by runId); the caller supplies the timeout error and any extra cleanup via `onTimeout`.
|
|
3362
3437
|
*/
|
|
3363
|
-
|
|
3438
|
+
awaitWithLivenessDeadline(id, resultPromise, timeoutMs, onTimeout) {
|
|
3364
3439
|
return new Promise((resolve, reject) => {
|
|
3365
3440
|
let timer;
|
|
3366
3441
|
const cleanup = /* @__PURE__ */ __name(() => {
|
|
@@ -3370,8 +3445,7 @@ var WorkflowEngine = class {
|
|
|
3370
3445
|
const arm = /* @__PURE__ */ __name(() => {
|
|
3371
3446
|
timer = setTimeout(() => {
|
|
3372
3447
|
cleanup();
|
|
3373
|
-
|
|
3374
|
-
reject(new RemoteStepTimeout(id, timeoutMs));
|
|
3448
|
+
reject(onTimeout());
|
|
3375
3449
|
}, timeoutMs);
|
|
3376
3450
|
timer.unref?.();
|
|
3377
3451
|
}, "arm");
|
|
@@ -3389,6 +3463,14 @@ var WorkflowEngine = class {
|
|
|
3389
3463
|
});
|
|
3390
3464
|
});
|
|
3391
3465
|
}
|
|
3466
|
+
/** Remote-STEP liveness: await a worker result, timing out (with the step's pending-map cleanup) to
|
|
3467
|
+
* `RemoteStepTimeout` if it neither lands nor beats within `timeoutMs`. See {@link awaitWithLivenessDeadline}. */
|
|
3468
|
+
awaitWithHeartbeat(id, resultPromise, timeoutMs) {
|
|
3469
|
+
return this.awaitWithLivenessDeadline(id, resultPromise, timeoutMs, () => {
|
|
3470
|
+
this.pending.delete(id);
|
|
3471
|
+
return new RemoteStepTimeout(id, timeoutMs);
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3392
3474
|
};
|
|
3393
3475
|
var RemoteStepError = class extends Error {
|
|
3394
3476
|
static {
|
|
@@ -3962,7 +4044,7 @@ var InMemoryStateStore = class {
|
|
|
3962
4044
|
async listIncompleteRuns() {
|
|
3963
4045
|
return [
|
|
3964
4046
|
...this.runs.values()
|
|
3965
|
-
].filter((r) => r.status === "running").map((r) => ({
|
|
4047
|
+
].filter((r) => r.status === "running" || r.status === "cancelling").map((r) => ({
|
|
3966
4048
|
...r
|
|
3967
4049
|
}));
|
|
3968
4050
|
}
|
|
@@ -4154,6 +4236,7 @@ var InMemoryTransport = class {
|
|
|
4154
4236
|
}
|
|
4155
4237
|
handlers = /* @__PURE__ */ new Map();
|
|
4156
4238
|
resultHandler;
|
|
4239
|
+
heartbeatHandler;
|
|
4157
4240
|
stepEventHandler;
|
|
4158
4241
|
controlHandlers = /* @__PURE__ */ new Set();
|
|
4159
4242
|
/** Register a fake worker handler for a step name. */
|
|
@@ -4168,7 +4251,12 @@ var InMemoryTransport = class {
|
|
|
4168
4251
|
onResult(handler) {
|
|
4169
4252
|
this.resultHandler = handler;
|
|
4170
4253
|
}
|
|
4171
|
-
onHeartbeat(
|
|
4254
|
+
onHeartbeat(handler) {
|
|
4255
|
+
this.heartbeatHandler = handler;
|
|
4256
|
+
}
|
|
4257
|
+
/** Test hook: deliver a liveness heartbeat to the engine, as a real broker would on its channel. */
|
|
4258
|
+
async emitHeartbeat(beat) {
|
|
4259
|
+
await this.heartbeatHandler?.(beat);
|
|
4172
4260
|
}
|
|
4173
4261
|
/** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
|
|
4174
4262
|
async dispatchStepEvent(event) {
|