@dudousxd/nestjs-durable-core 0.75.2 → 0.76.1
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 +67 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +67 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -969,6 +969,16 @@ interface StateStore {
|
|
|
969
969
|
* being a single transaction; stores without transactions cannot give the strong guarantee.
|
|
970
970
|
*/
|
|
971
971
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
972
|
+
/**
|
|
973
|
+
* Overwrite the checkpoint at `(runId, seq)` with `checkpoint`, but only while the stored row is
|
|
974
|
+
* still `pending`; resolves whether it wrote. The engine writes a dispatched step's row from two
|
|
975
|
+
* places at once (a turn stamping its redispatch deadline, and the worker's result settling it),
|
|
976
|
+
* so a blind {@link saveCheckpoint} from a row read a moment earlier can put a `completed` step
|
|
977
|
+
* back to `pending`. This is the compare-and-set that prevents it.
|
|
978
|
+
*
|
|
979
|
+
* Optional: a store that omits it falls back to {@link saveCheckpoint}, which keeps that race.
|
|
980
|
+
*/
|
|
981
|
+
saveCheckpointIfPending?(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
972
982
|
/** Used by recovery on boot to find runs to resume (crashed, left `running`). */
|
|
973
983
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
974
984
|
/** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
|
|
@@ -3796,6 +3806,12 @@ declare class WorkflowEngine {
|
|
|
3796
3806
|
* {@link redriveSettledSteps}).
|
|
3797
3807
|
*/
|
|
3798
3808
|
private readonly awaitedRemoteSteps;
|
|
3809
|
+
/**
|
|
3810
|
+
* When THIS instance settled a remote step from its result, by `runId:seq`. That result's resume
|
|
3811
|
+
* runs here, and `execute` lets it into a lease this instance holds, so its wake is never the
|
|
3812
|
+
* dropped one {@link redriveSettledSteps} recovers. Pruned by age (see {@link noteSettledHere}).
|
|
3813
|
+
*/
|
|
3814
|
+
private readonly settledHere;
|
|
3799
3815
|
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
3800
3816
|
private readonly admission;
|
|
3801
3817
|
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
@@ -4833,6 +4849,10 @@ declare class WorkflowEngine {
|
|
|
4833
4849
|
* capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
|
|
4834
4850
|
private dueBlockedRuns;
|
|
4835
4851
|
private callRemote;
|
|
4852
|
+
/** Remember a step settled here, forgetting entries older than a lease so the map stays small. */
|
|
4853
|
+
private noteSettledHere;
|
|
4854
|
+
/** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
|
|
4855
|
+
private savePendingCheckpoint;
|
|
4836
4856
|
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
4837
4857
|
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
4838
4858
|
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
@@ -5508,6 +5528,9 @@ declare class CodecStateStore implements StateStore {
|
|
|
5508
5528
|
/** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
|
|
5509
5529
|
* engine's per-run fallback still kicks in through the wrapper. */
|
|
5510
5530
|
readonly deleteRuns?: (runIds: string[]) => Promise<void>;
|
|
5531
|
+
/** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
|
|
5532
|
+
* plain-save fallback still kicks in through the wrapper. */
|
|
5533
|
+
readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
|
|
5511
5534
|
/** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
|
|
5512
5535
|
* when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
|
|
5513
5536
|
readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
|
|
@@ -5977,6 +6000,7 @@ declare class InMemoryStateStore implements StateStore {
|
|
|
5977
6000
|
pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
|
|
5978
6001
|
getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
|
|
5979
6002
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
6003
|
+
saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
5980
6004
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
5981
6005
|
listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
5982
6006
|
listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -969,6 +969,16 @@ interface StateStore {
|
|
|
969
969
|
* being a single transaction; stores without transactions cannot give the strong guarantee.
|
|
970
970
|
*/
|
|
971
971
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
972
|
+
/**
|
|
973
|
+
* Overwrite the checkpoint at `(runId, seq)` with `checkpoint`, but only while the stored row is
|
|
974
|
+
* still `pending`; resolves whether it wrote. The engine writes a dispatched step's row from two
|
|
975
|
+
* places at once (a turn stamping its redispatch deadline, and the worker's result settling it),
|
|
976
|
+
* so a blind {@link saveCheckpoint} from a row read a moment earlier can put a `completed` step
|
|
977
|
+
* back to `pending`. This is the compare-and-set that prevents it.
|
|
978
|
+
*
|
|
979
|
+
* Optional: a store that omits it falls back to {@link saveCheckpoint}, which keeps that race.
|
|
980
|
+
*/
|
|
981
|
+
saveCheckpointIfPending?(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
972
982
|
/** Used by recovery on boot to find runs to resume (crashed, left `running`). */
|
|
973
983
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
974
984
|
/** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
|
|
@@ -3796,6 +3806,12 @@ declare class WorkflowEngine {
|
|
|
3796
3806
|
* {@link redriveSettledSteps}).
|
|
3797
3807
|
*/
|
|
3798
3808
|
private readonly awaitedRemoteSteps;
|
|
3809
|
+
/**
|
|
3810
|
+
* When THIS instance settled a remote step from its result, by `runId:seq`. That result's resume
|
|
3811
|
+
* runs here, and `execute` lets it into a lease this instance holds, so its wake is never the
|
|
3812
|
+
* dropped one {@link redriveSettledSteps} recovers. Pruned by age (see {@link noteSettledHere}).
|
|
3813
|
+
*/
|
|
3814
|
+
private readonly settledHere;
|
|
3799
3815
|
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
3800
3816
|
private readonly admission;
|
|
3801
3817
|
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
@@ -4833,6 +4849,10 @@ declare class WorkflowEngine {
|
|
|
4833
4849
|
* capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
|
|
4834
4850
|
private dueBlockedRuns;
|
|
4835
4851
|
private callRemote;
|
|
4852
|
+
/** Remember a step settled here, forgetting entries older than a lease so the map stays small. */
|
|
4853
|
+
private noteSettledHere;
|
|
4854
|
+
/** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
|
|
4855
|
+
private savePendingCheckpoint;
|
|
4836
4856
|
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
4837
4857
|
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
4838
4858
|
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
@@ -5508,6 +5528,9 @@ declare class CodecStateStore implements StateStore {
|
|
|
5508
5528
|
/** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
|
|
5509
5529
|
* engine's per-run fallback still kicks in through the wrapper. */
|
|
5510
5530
|
readonly deleteRuns?: (runIds: string[]) => Promise<void>;
|
|
5531
|
+
/** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
|
|
5532
|
+
* plain-save fallback still kicks in through the wrapper. */
|
|
5533
|
+
readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
|
|
5511
5534
|
/** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
|
|
5512
5535
|
* when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
|
|
5513
5536
|
readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
|
|
@@ -5977,6 +6000,7 @@ declare class InMemoryStateStore implements StateStore {
|
|
|
5977
6000
|
pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
|
|
5978
6001
|
getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
|
|
5979
6002
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
6003
|
+
saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
5980
6004
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
5981
6005
|
listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
5982
6006
|
listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
|
package/dist/index.js
CHANGED
|
@@ -3704,6 +3704,12 @@ var WorkflowEngine = class {
|
|
|
3704
3704
|
* {@link redriveSettledSteps}).
|
|
3705
3705
|
*/
|
|
3706
3706
|
awaitedRemoteSteps = /* @__PURE__ */ new Map();
|
|
3707
|
+
/**
|
|
3708
|
+
* When THIS instance settled a remote step from its result, by `runId:seq`. That result's resume
|
|
3709
|
+
* runs here, and `execute` lets it into a lease this instance holds, so its wake is never the
|
|
3710
|
+
* dropped one {@link redriveSettledSteps} recovers. Pruned by age (see {@link noteSettledHere}).
|
|
3711
|
+
*/
|
|
3712
|
+
settledHere = /* @__PURE__ */ new Map();
|
|
3707
3713
|
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
3708
3714
|
admission;
|
|
3709
3715
|
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
@@ -4178,7 +4184,18 @@ var WorkflowEngine = class {
|
|
|
4178
4184
|
createdAt: now,
|
|
4179
4185
|
updatedAt: now
|
|
4180
4186
|
};
|
|
4181
|
-
|
|
4187
|
+
try {
|
|
4188
|
+
await this.store.createRun(run);
|
|
4189
|
+
} catch (err) {
|
|
4190
|
+
const winner = await this.store.getRun(runId).catch(() => null);
|
|
4191
|
+
if (!winner) throw err;
|
|
4192
|
+
return {
|
|
4193
|
+
runId,
|
|
4194
|
+
status: winner.status,
|
|
4195
|
+
output: winner.output,
|
|
4196
|
+
error: winner.error
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4182
4199
|
await this.runDispatcher.dispatch(runId);
|
|
4183
4200
|
if (this.controlPlane) {
|
|
4184
4201
|
void this.controlPlane.publishControl({
|
|
@@ -5120,6 +5137,16 @@ var WorkflowEngine = class {
|
|
|
5120
5137
|
for (const seq of seqs) {
|
|
5121
5138
|
const cp = await this.store.getCheckpoint(runId, seq);
|
|
5122
5139
|
if (cp && (cp.status === "completed" || cp.status === "failed")) {
|
|
5140
|
+
if (this.settledHere.has(`${runId}:${seq}`)) return;
|
|
5141
|
+
const nowMs = this.clock();
|
|
5142
|
+
if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
const run = await this.store.getRun(runId);
|
|
5146
|
+
if (run?.status !== "suspended") {
|
|
5147
|
+
await this.store.releaseRunLock(runId);
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5123
5150
|
await this.resumeImplicit(runId);
|
|
5124
5151
|
return;
|
|
5125
5152
|
}
|
|
@@ -7091,10 +7118,12 @@ var WorkflowEngine = class {
|
|
|
7091
7118
|
if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
|
|
7092
7119
|
if (existing.wakeAt == null) {
|
|
7093
7120
|
const wakeAt = this.clock() + this.remoteRedispatchMs;
|
|
7094
|
-
await this.
|
|
7121
|
+
if (!await this.savePendingCheckpoint({
|
|
7095
7122
|
...existing,
|
|
7096
7123
|
wakeAt
|
|
7097
|
-
})
|
|
7124
|
+
})) {
|
|
7125
|
+
return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
|
|
7126
|
+
}
|
|
7098
7127
|
throw new WorkflowSuspended(wakeAt);
|
|
7099
7128
|
}
|
|
7100
7129
|
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
@@ -7107,7 +7136,7 @@ var WorkflowEngine = class {
|
|
|
7107
7136
|
const reAttempt = existing.attempts + 1;
|
|
7108
7137
|
const nextDeadline = this.clock() + this.remoteRedispatchMs;
|
|
7109
7138
|
const reEnqueuedAt = /* @__PURE__ */ new Date();
|
|
7110
|
-
await this.
|
|
7139
|
+
const claimed = await this.savePendingCheckpoint({
|
|
7111
7140
|
...existing,
|
|
7112
7141
|
attempts: reAttempt,
|
|
7113
7142
|
wakeAt: nextDeadline,
|
|
@@ -7121,6 +7150,9 @@ var WorkflowEngine = class {
|
|
|
7121
7150
|
lostStepEvent(this.clock(), reAttempt, "redispatched")
|
|
7122
7151
|
]
|
|
7123
7152
|
});
|
|
7153
|
+
if (!claimed) {
|
|
7154
|
+
return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
|
|
7155
|
+
}
|
|
7124
7156
|
await this.dispatchRemoteTask({
|
|
7125
7157
|
runId,
|
|
7126
7158
|
seq,
|
|
@@ -7211,6 +7243,21 @@ var WorkflowEngine = class {
|
|
|
7211
7243
|
this.awaitedRemoteSteps.get(runId)?.add(seq);
|
|
7212
7244
|
throw new WorkflowSuspended();
|
|
7213
7245
|
}
|
|
7246
|
+
/** Remember a step settled here, forgetting entries older than a lease so the map stays small. */
|
|
7247
|
+
noteSettledHere(key) {
|
|
7248
|
+
const now = this.clock();
|
|
7249
|
+
this.settledHere.set(key, now);
|
|
7250
|
+
if (this.settledHere.size < 1024) return;
|
|
7251
|
+
for (const [k, at] of this.settledHere) {
|
|
7252
|
+
if (now - at > this.leaseMs) this.settledHere.delete(k);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
/** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
|
|
7256
|
+
async savePendingCheckpoint(checkpoint) {
|
|
7257
|
+
if (this.store.saveCheckpointIfPending) return this.store.saveCheckpointIfPending(checkpoint);
|
|
7258
|
+
await this.store.saveCheckpoint(checkpoint);
|
|
7259
|
+
return true;
|
|
7260
|
+
}
|
|
7214
7261
|
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
7215
7262
|
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
7216
7263
|
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
@@ -7241,7 +7288,7 @@ var WorkflowEngine = class {
|
|
|
7241
7288
|
if (run && (run.status === "cancelled" || run.status === "completed")) return;
|
|
7242
7289
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
7243
7290
|
const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
|
|
7244
|
-
await this.
|
|
7291
|
+
const claimed = await this.savePendingCheckpoint({
|
|
7245
7292
|
...cp,
|
|
7246
7293
|
status: result.status,
|
|
7247
7294
|
output: result.status === "completed" ? result.output : cp.output,
|
|
@@ -7250,6 +7297,8 @@ var WorkflowEngine = class {
|
|
|
7250
7297
|
startedAt,
|
|
7251
7298
|
finishedAt
|
|
7252
7299
|
});
|
|
7300
|
+
if (!claimed) return;
|
|
7301
|
+
this.noteSettledHere(`${result.runId}:${result.seq}`);
|
|
7253
7302
|
this.emit({
|
|
7254
7303
|
type: result.status === "completed" ? "step.completed" : "step.failed",
|
|
7255
7304
|
runId: result.runId,
|
|
@@ -7642,6 +7691,9 @@ var CodecStateStore = class {
|
|
|
7642
7691
|
/** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
|
|
7643
7692
|
* engine's per-run fallback still kicks in through the wrapper. */
|
|
7644
7693
|
deleteRuns;
|
|
7694
|
+
/** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
|
|
7695
|
+
* plain-save fallback still kicks in through the wrapper. */
|
|
7696
|
+
saveCheckpointIfPending;
|
|
7645
7697
|
/** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
|
|
7646
7698
|
* when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
|
|
7647
7699
|
pruneTerminalRuns;
|
|
@@ -7669,6 +7721,8 @@ var CodecStateStore = class {
|
|
|
7669
7721
|
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
7670
7722
|
const deleteRuns = inner.deleteRuns;
|
|
7671
7723
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7724
|
+
const saveIfPending = inner.saveCheckpointIfPending;
|
|
7725
|
+
if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
|
|
7672
7726
|
const prune = inner.pruneTerminalRuns;
|
|
7673
7727
|
if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
|
|
7674
7728
|
const count = inner.countRuns;
|
|
@@ -8232,6 +8286,14 @@ var InMemoryStateStore = class {
|
|
|
8232
8286
|
...checkpoint
|
|
8233
8287
|
});
|
|
8234
8288
|
}
|
|
8289
|
+
async saveCheckpointIfPending(checkpoint) {
|
|
8290
|
+
const key = this.key(checkpoint.runId, checkpoint.seq);
|
|
8291
|
+
if (this.checkpoints.get(key)?.status !== "pending") return false;
|
|
8292
|
+
this.checkpoints.set(key, {
|
|
8293
|
+
...checkpoint
|
|
8294
|
+
});
|
|
8295
|
+
return true;
|
|
8296
|
+
}
|
|
8235
8297
|
async listIncompleteRuns(namespace) {
|
|
8236
8298
|
return [
|
|
8237
8299
|
...this.runs.values()
|