@dudousxd/nestjs-durable-core 0.79.0 → 0.80.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 +198 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +198 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3859,6 +3859,16 @@ function isNewerVersion(a, b) {
|
|
|
3859
3859
|
}) > 0;
|
|
3860
3860
|
}
|
|
3861
3861
|
__name(isNewerVersion, "isNewerVersion");
|
|
3862
|
+
var SETTLE_ELIGIBLE_STATUSES = [
|
|
3863
|
+
"pending",
|
|
3864
|
+
"running",
|
|
3865
|
+
"suspended",
|
|
3866
|
+
"blocked",
|
|
3867
|
+
"failed",
|
|
3868
|
+
"cancelling"
|
|
3869
|
+
];
|
|
3870
|
+
var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
|
|
3871
|
+
var RECOVERY_BATCH = 500;
|
|
3862
3872
|
var NamespaceMismatch = class NamespaceMismatch2 extends Error {
|
|
3863
3873
|
static {
|
|
3864
3874
|
__name(this, "NamespaceMismatch");
|
|
@@ -3958,6 +3968,18 @@ var WorkflowEngine = class {
|
|
|
3958
3968
|
awaitedStepFans = /* @__PURE__ */ new Map();
|
|
3959
3969
|
/** Step fans whose join barrier this instance opened, by `runId:firstSeq` — see {@link settledHere}. */
|
|
3960
3970
|
stepFansFiredHere = /* @__PURE__ */ new Map();
|
|
3971
|
+
/**
|
|
3972
|
+
* The in-process execution gate, by run id: the execution of the run currently in flight in THIS
|
|
3973
|
+
* process (`tail`), and at most one resume queued behind it (`queued`). See {@link resumeRun}.
|
|
3974
|
+
*
|
|
3975
|
+
* The run lease is keyed by engine instance, so it fences OTHER processes only: to the store, a
|
|
3976
|
+
* second execution in this same process holds the very same lease (and {@link execute} skips
|
|
3977
|
+
* `tryLockRun` when its snapshot says this instance holds it). Without this gate, a resume landing
|
|
3978
|
+
* while this process was still executing the run (a signal, a late step result, a child wake, an
|
|
3979
|
+
* explicit `resume()`) went straight past the lease and ran the body concurrently: a
|
|
3980
|
+
* not-yet-checkpointed step executed twice.
|
|
3981
|
+
*/
|
|
3982
|
+
runGates = /* @__PURE__ */ new Map();
|
|
3961
3983
|
/** The tail of each run's queue of results being handled on this instance — see {@link serializedPerRun}. */
|
|
3962
3984
|
resultChains = /* @__PURE__ */ new Map();
|
|
3963
3985
|
/** Each run's results waiting to be settled on this instance — see {@link completeRemoteResult}. */
|
|
@@ -4426,11 +4448,16 @@ var WorkflowEngine = class {
|
|
|
4426
4448
|
if (registered.singleton) {
|
|
4427
4449
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
4428
4450
|
}
|
|
4451
|
+
const startAtMs = opts?.startAt instanceof Date ? opts.startAt.getTime() : opts?.startAt;
|
|
4452
|
+
const delayed = startAtMs !== void 0 && Number.isFinite(startAtMs) && startAtMs > this.clock();
|
|
4429
4453
|
const run = {
|
|
4430
4454
|
id: runId,
|
|
4431
4455
|
workflow: name,
|
|
4432
4456
|
workflowVersion: registered.version,
|
|
4433
|
-
status: "pending",
|
|
4457
|
+
status: delayed ? "suspended" : "pending",
|
|
4458
|
+
...delayed ? {
|
|
4459
|
+
wakeAt: startAtMs
|
|
4460
|
+
} : {},
|
|
4434
4461
|
namespace: opts?.namespace ?? this.namespace,
|
|
4435
4462
|
// DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
|
|
4436
4463
|
// deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
|
|
@@ -4456,6 +4483,18 @@ var WorkflowEngine = class {
|
|
|
4456
4483
|
error: winner.error
|
|
4457
4484
|
};
|
|
4458
4485
|
}
|
|
4486
|
+
if (delayed) {
|
|
4487
|
+
this.emit({
|
|
4488
|
+
type: "run.suspended",
|
|
4489
|
+
runId,
|
|
4490
|
+
workflow: name,
|
|
4491
|
+
namespace: run.namespace
|
|
4492
|
+
});
|
|
4493
|
+
return {
|
|
4494
|
+
runId,
|
|
4495
|
+
status: "suspended"
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4459
4498
|
await this.runDispatcher.dispatch(runId);
|
|
4460
4499
|
if (this.controlPlane) {
|
|
4461
4500
|
void this.controlPlane.publishControl({
|
|
@@ -4493,7 +4532,104 @@ var WorkflowEngine = class {
|
|
|
4493
4532
|
resumeImplicit(runId) {
|
|
4494
4533
|
return this.resumeRun(runId, false);
|
|
4495
4534
|
}
|
|
4496
|
-
|
|
4535
|
+
/**
|
|
4536
|
+
* One execution of a run at a time in this process. A resume that lands while the run executes
|
|
4537
|
+
* here neither joins that execution (the double-run) nor no-ops against its held lease (a lost
|
|
4538
|
+
* wake: its checkpoint may postdate the running turn's snapshot): it is QUEUED, and re-drives the
|
|
4539
|
+
* run once the running execution has settled and released its lease, re-reading the run then
|
|
4540
|
+
* (terminal: echoed, never re-executed). Resumes landing while one is already queued coalesce
|
|
4541
|
+
* into it: it has not started yet, so it will observe every write they made. Their callers get
|
|
4542
|
+
* the queued execution's result, i.e. the run's state after their own wake was applied.
|
|
4543
|
+
*
|
|
4544
|
+
* Across processes, exclusion stays the lease's job: {@link execute} still `tryLockRun`s when it
|
|
4545
|
+
* does not hold the lease.
|
|
4546
|
+
*/
|
|
4547
|
+
resumeRun(runId, allowFailed) {
|
|
4548
|
+
const gate = this.runGates.get(runId);
|
|
4549
|
+
if (!gate) {
|
|
4550
|
+
const tail = this.resumeRunNow(runId, allowFailed);
|
|
4551
|
+
const fresh = {
|
|
4552
|
+
tail
|
|
4553
|
+
};
|
|
4554
|
+
this.runGates.set(runId, fresh);
|
|
4555
|
+
this.closeGateAfter(runId, fresh, tail);
|
|
4556
|
+
return tail;
|
|
4557
|
+
}
|
|
4558
|
+
let queued = gate.queued;
|
|
4559
|
+
if (queued) {
|
|
4560
|
+
queued.allowFailed ||= allowFailed;
|
|
4561
|
+
} else {
|
|
4562
|
+
const entry = {
|
|
4563
|
+
allowFailed
|
|
4564
|
+
};
|
|
4565
|
+
const result = gate.tail.catch(() => void 0).then(() => {
|
|
4566
|
+
if (gate.queued === entry) gate.queued = void 0;
|
|
4567
|
+
return this.resumeRunNow(runId, entry.allowFailed);
|
|
4568
|
+
});
|
|
4569
|
+
entry.result = result;
|
|
4570
|
+
queued = entry;
|
|
4571
|
+
gate.queued = queued;
|
|
4572
|
+
gate.tail = result;
|
|
4573
|
+
this.closeGateAfter(runId, gate, result);
|
|
4574
|
+
void this.track(result).catch(() => void 0);
|
|
4575
|
+
}
|
|
4576
|
+
if (currentWorkflowCtx()?.runId === runId) return this.echoCurrentStatus(runId);
|
|
4577
|
+
return queued.result;
|
|
4578
|
+
}
|
|
4579
|
+
/** Drop `gate` once `tail` settles, unless another execution queued behind it meanwhile. */
|
|
4580
|
+
closeGateAfter(runId, gate, tail) {
|
|
4581
|
+
const close = /* @__PURE__ */ __name(() => {
|
|
4582
|
+
if (this.runGates.get(runId) === gate && gate.tail === tail) this.runGates.delete(runId);
|
|
4583
|
+
}, "close");
|
|
4584
|
+
void tail.then(close, close);
|
|
4585
|
+
}
|
|
4586
|
+
/**
|
|
4587
|
+
* Release a run's lease AFTER its outcome is decided (persisted, or about to be returned). Best
|
|
4588
|
+
* effort, and that matters most on the failure path: the thing that failed the run is frequently
|
|
4589
|
+
* the very store this release talks to (a step that violated a DB constraint leaves Postgres
|
|
4590
|
+
* refusing every further statement on that connection), so the release throws too. A throwing
|
|
4591
|
+
* release in a `finally` REPLACES the run's real outcome with a lease write nobody asked about,
|
|
4592
|
+
* skips the post-turn re-drives after it, and, for a turn driven as a background resume, surfaces
|
|
4593
|
+
* as an unhandled rejection instead of a failed run.
|
|
4594
|
+
*
|
|
4595
|
+
* Letting it go is safe: the lease expires on its own and `recoverIncomplete` re-drives whatever
|
|
4596
|
+
* is still held. A lost release costs one lease window of latency; a masked error costs the
|
|
4597
|
+
* diagnosis.
|
|
4598
|
+
*/
|
|
4599
|
+
async releaseLeaseBestEffort(runId) {
|
|
4600
|
+
try {
|
|
4601
|
+
await this.store.releaseRunLock(runId);
|
|
4602
|
+
} catch (err) {
|
|
4603
|
+
console.warn(`[nestjs-durable] releasing the lease of run ${runId} failed (it expires on its own and recovery re-drives): ${err instanceof Error ? err.message : String(err)}`);
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
/**
|
|
4607
|
+
* The run's persisted state as a {@link RunResult}, without executing anything: the answer to a
|
|
4608
|
+
* resume issued from inside the run's own execution, and to a settle whose compare-and-set lost
|
|
4609
|
+
* (the path that won already emitted and notified). A run that is gone reads as `cancelled`, the
|
|
4610
|
+
* safest terminal reading, never a fabricated success.
|
|
4611
|
+
*/
|
|
4612
|
+
async echoCurrentStatus(runId) {
|
|
4613
|
+
const run = await this.store.getRun(runId);
|
|
4614
|
+
return {
|
|
4615
|
+
runId,
|
|
4616
|
+
status: run?.status ?? "cancelled",
|
|
4617
|
+
output: run?.output,
|
|
4618
|
+
error: run?.error
|
|
4619
|
+
};
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* The engine's stand-in for {@link StateStore.updateRunIf} on a store without it, called with the
|
|
4623
|
+
* store as `this`: read-then-write, which narrows the race to the gap between the read and the
|
|
4624
|
+
* write (the pre-CAS behavior, now applied to every settle) but cannot close it.
|
|
4625
|
+
*/
|
|
4626
|
+
async updateRunIfFallback(runId, expected, patch) {
|
|
4627
|
+
const current = await this.getRun(runId);
|
|
4628
|
+
if (!current || !expected.includes(current.status)) return false;
|
|
4629
|
+
await this.updateRun(runId, patch);
|
|
4630
|
+
return true;
|
|
4631
|
+
}
|
|
4632
|
+
async resumeRunNow(runId, allowFailed) {
|
|
4497
4633
|
const run = await this.store.getRun(runId);
|
|
4498
4634
|
if (!run) throw new Error(`run ${runId} not found`);
|
|
4499
4635
|
if (run.status === "cancelled" || run.status === "completed" || run.status === "dead" || run.status === "failed" && !allowFailed) {
|
|
@@ -4799,7 +4935,7 @@ var WorkflowEngine = class {
|
|
|
4799
4935
|
async recoverIncomplete(nowMs = this.clock()) {
|
|
4800
4936
|
if (this.draining) return [];
|
|
4801
4937
|
const results = [];
|
|
4802
|
-
for (const listed of await this.
|
|
4938
|
+
for (const listed of await this.orphanedRuns(nowMs)) {
|
|
4803
4939
|
if (this.draining) break;
|
|
4804
4940
|
const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
|
|
4805
4941
|
if (!acquired) continue;
|
|
@@ -4845,6 +4981,21 @@ var WorkflowEngine = class {
|
|
|
4845
4981
|
return results;
|
|
4846
4982
|
}
|
|
4847
4983
|
/**
|
|
4984
|
+
* The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
|
|
4985
|
+
* expired, never the healthy ones a live turn is renewing. Every worker runs recovery on every
|
|
4986
|
+
* timer tick, so listing every running run and lock-probing each (one doomed UPDATE per healthy
|
|
4987
|
+
* run per worker per tick) was pure database load. The store answers it directly when it can
|
|
4988
|
+
* ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
|
|
4989
|
+
* filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
|
|
4990
|
+
* recovering at once spread over the batch instead of contending for the same leases.
|
|
4991
|
+
*/
|
|
4992
|
+
async orphanedRuns(nowMs) {
|
|
4993
|
+
if (this.store.listOrphanedRuns) {
|
|
4994
|
+
return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
|
|
4995
|
+
}
|
|
4996
|
+
return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs));
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4848
4999
|
* The remote step this run dispatched and is still awaiting a result for, if any — the signature
|
|
4849
5000
|
* that tells {@link recoverIncomplete} "this run's work is ON THE TRANSPORT, not in a crashed
|
|
4850
5001
|
* process". A `pending` remote checkpoint is written BEFORE the dispatch and settled by the
|
|
@@ -6361,12 +6512,13 @@ ${run.workflowVersion}`;
|
|
|
6361
6512
|
async settleRun(run, outcome) {
|
|
6362
6513
|
const updatedAt = /* @__PURE__ */ new Date();
|
|
6363
6514
|
if (outcome.kind === "completed") {
|
|
6364
|
-
await this.store.
|
|
6515
|
+
const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
|
|
6365
6516
|
status: "completed",
|
|
6366
6517
|
output: outcome.output,
|
|
6367
6518
|
error: void 0,
|
|
6368
6519
|
updatedAt
|
|
6369
6520
|
});
|
|
6521
|
+
if (!applied2) return this.echoCurrentStatus(run.id);
|
|
6370
6522
|
this.emit({
|
|
6371
6523
|
type: "run.completed",
|
|
6372
6524
|
runId: run.id,
|
|
@@ -6388,11 +6540,12 @@ ${run.workflowVersion}`;
|
|
|
6388
6540
|
const handed = await this.routeFailure(run, outcome.error);
|
|
6389
6541
|
if (handed && handed.kind !== "failed") return this.settleRun(run, handed);
|
|
6390
6542
|
const error2 = handed?.error ?? outcome.error;
|
|
6391
|
-
await this.store.
|
|
6543
|
+
const applied2 = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SETTLE_ELIGIBLE_STATUSES, {
|
|
6392
6544
|
status: "failed",
|
|
6393
6545
|
error: error2,
|
|
6394
6546
|
updatedAt
|
|
6395
6547
|
});
|
|
6548
|
+
if (!applied2) return this.echoCurrentStatus(run.id);
|
|
6396
6549
|
this.emit({
|
|
6397
6550
|
type: "run.failed",
|
|
6398
6551
|
runId: run.id,
|
|
@@ -6408,19 +6561,12 @@ ${run.workflowVersion}`;
|
|
|
6408
6561
|
};
|
|
6409
6562
|
}
|
|
6410
6563
|
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
6411
|
-
const
|
|
6412
|
-
if (latest?.status === "cancelled") {
|
|
6413
|
-
return {
|
|
6414
|
-
runId: run.id,
|
|
6415
|
-
status: "cancelled",
|
|
6416
|
-
error: latest.error
|
|
6417
|
-
};
|
|
6418
|
-
}
|
|
6419
|
-
await this.store.updateRun(run.id, {
|
|
6564
|
+
const applied = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SUSPEND_ELIGIBLE_STATUSES, {
|
|
6420
6565
|
status: "suspended",
|
|
6421
6566
|
wakeAt: this.reconcileWakeAt(outcome.wakeAt),
|
|
6422
6567
|
updatedAt
|
|
6423
6568
|
});
|
|
6569
|
+
if (!applied) return this.echoCurrentStatus(run.id);
|
|
6424
6570
|
this.emit({
|
|
6425
6571
|
type: "run.suspended",
|
|
6426
6572
|
runId: run.id,
|
|
@@ -6467,7 +6613,7 @@ ${run.workflowVersion}`;
|
|
|
6467
6613
|
from: this.instanceId
|
|
6468
6614
|
}).catch(() => void 0);
|
|
6469
6615
|
}
|
|
6470
|
-
await this.
|
|
6616
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6471
6617
|
return {
|
|
6472
6618
|
runId: run.id,
|
|
6473
6619
|
status: "cancelled",
|
|
@@ -6497,7 +6643,7 @@ ${run.workflowVersion}`;
|
|
|
6497
6643
|
const handoff = liveHandoff(run.id, checkpoints);
|
|
6498
6644
|
if (handoff) {
|
|
6499
6645
|
const result = await this.settleRun(run, await this.followHandoff(run, handoff));
|
|
6500
|
-
await this.
|
|
6646
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6501
6647
|
return result;
|
|
6502
6648
|
}
|
|
6503
6649
|
const history = await this.remoteHistory(run.id, checkpoints);
|
|
@@ -6535,7 +6681,7 @@ ${run.workflowVersion}`;
|
|
|
6535
6681
|
decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
|
|
6536
6682
|
} catch (err) {
|
|
6537
6683
|
if (err instanceof RemoteWorkflowTimeout) {
|
|
6538
|
-
await this.
|
|
6684
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
6539
6685
|
return {
|
|
6540
6686
|
runId: run.id,
|
|
6541
6687
|
status: "running"
|
|
@@ -7206,7 +7352,7 @@ ${run.workflowVersion}`;
|
|
|
7206
7352
|
this.awaitedRemoteSteps.delete(run.id);
|
|
7207
7353
|
}
|
|
7208
7354
|
if (this.awaitedStepFans.get(run.id) === awaitedFans) this.awaitedStepFans.delete(run.id);
|
|
7209
|
-
await this.
|
|
7355
|
+
await this.releaseLeaseBestEffort(run.id);
|
|
7210
7356
|
if (suspendedOnFan) {
|
|
7211
7357
|
void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
|
|
7212
7358
|
}
|
|
@@ -7577,27 +7723,21 @@ ${run.workflowVersion}`;
|
|
|
7577
7723
|
* own `finally` also releases (the run-execution path) is safe.
|
|
7578
7724
|
*/
|
|
7579
7725
|
async parkBlocked(run, blocked) {
|
|
7580
|
-
const current = await this.store.getRun(run.id);
|
|
7581
|
-
if (current && (current.status === "cancelled" || current.status === "completed")) {
|
|
7582
|
-
await this.store.releaseRunLock(run.id);
|
|
7583
|
-
return {
|
|
7584
|
-
runId: run.id,
|
|
7585
|
-
status: current.status,
|
|
7586
|
-
output: current.output,
|
|
7587
|
-
error: current.error
|
|
7588
|
-
};
|
|
7589
|
-
}
|
|
7590
7726
|
const error2 = {
|
|
7591
7727
|
message: blocked.reason,
|
|
7592
7728
|
code: blocked.code,
|
|
7593
7729
|
retryable: true
|
|
7594
7730
|
};
|
|
7595
|
-
await this.store.
|
|
7731
|
+
const applied = await (this.store.updateRunIf ?? this.updateRunIfFallback).call(this.store, run.id, SUSPEND_ELIGIBLE_STATUSES, {
|
|
7596
7732
|
status: "blocked",
|
|
7597
7733
|
error: error2,
|
|
7598
7734
|
wakeAt: this.clock() + this.blockedPollMs,
|
|
7599
7735
|
updatedAt: /* @__PURE__ */ new Date()
|
|
7600
7736
|
});
|
|
7737
|
+
if (!applied) {
|
|
7738
|
+
await this.store.releaseRunLock(run.id);
|
|
7739
|
+
return this.echoCurrentStatus(run.id);
|
|
7740
|
+
}
|
|
7601
7741
|
this.emit({
|
|
7602
7742
|
type: "run.blocked",
|
|
7603
7743
|
runId: run.id,
|
|
@@ -8955,6 +9095,12 @@ var CodecStateStore = class {
|
|
|
8955
9095
|
/** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
|
|
8956
9096
|
* engine's per-run fallback still kicks in through the wrapper. */
|
|
8957
9097
|
deleteRuns;
|
|
9098
|
+
/** Decodes the runs like {@link listIncompleteRuns}. Bound only when the inner store has it, so
|
|
9099
|
+
* the engine's list-and-filter fallback still kicks in through the wrapper. */
|
|
9100
|
+
listOrphanedRuns;
|
|
9101
|
+
/** Encodes the patch like {@link updateRun}. Bound only when the inner store has it, so the
|
|
9102
|
+
* engine's read-then-write fallback still kicks in through the wrapper. */
|
|
9103
|
+
updateRunIf;
|
|
8958
9104
|
/** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
|
|
8959
9105
|
* plain-save fallback still kicks in through the wrapper. */
|
|
8960
9106
|
saveCheckpointIfPending;
|
|
@@ -8995,6 +9141,10 @@ var CodecStateStore = class {
|
|
|
8995
9141
|
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
8996
9142
|
const deleteRuns = inner.deleteRuns;
|
|
8997
9143
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
9144
|
+
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));
|
|
9146
|
+
const updateIf = inner.updateRunIf;
|
|
9147
|
+
if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
|
|
8998
9148
|
const saveIfPending = inner.saveCheckpointIfPending;
|
|
8999
9149
|
if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
|
|
9000
9150
|
const saveManyIfPending = inner.saveCheckpointsIfPending;
|
|
@@ -9101,12 +9251,15 @@ var CodecStateStore = class {
|
|
|
9101
9251
|
return this.inner.createRun(this.encRun(run));
|
|
9102
9252
|
}
|
|
9103
9253
|
updateRun(runId, patch) {
|
|
9254
|
+
return this.inner.updateRun(runId, this.encRunPatch(patch));
|
|
9255
|
+
}
|
|
9256
|
+
encRunPatch(patch) {
|
|
9104
9257
|
const next = {
|
|
9105
9258
|
...patch
|
|
9106
9259
|
};
|
|
9107
9260
|
if ("input" in patch) next.input = this.enc(patch.input);
|
|
9108
9261
|
if ("output" in patch) next.output = this.enc(patch.output);
|
|
9109
|
-
return
|
|
9262
|
+
return next;
|
|
9110
9263
|
}
|
|
9111
9264
|
async getRun(runId) {
|
|
9112
9265
|
return this.decRun(await this.inner.getRun(runId));
|
|
@@ -9515,6 +9668,14 @@ var InMemoryStateStore = class {
|
|
|
9515
9668
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
9516
9669
|
}
|
|
9517
9670
|
async updateRun(runId, inputPatch) {
|
|
9671
|
+
this.applyRunPatch(runId, inputPatch);
|
|
9672
|
+
}
|
|
9673
|
+
updateRunIf(runId, expectedStatuses, patch) {
|
|
9674
|
+
const existing = this.runs.get(runId);
|
|
9675
|
+
if (!existing || !expectedStatuses.includes(existing.status)) return Promise.resolve(false);
|
|
9676
|
+
return this.updateRun(runId, patch).then(() => true);
|
|
9677
|
+
}
|
|
9678
|
+
applyRunPatch(runId, inputPatch) {
|
|
9518
9679
|
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
9519
9680
|
...inputPatch,
|
|
9520
9681
|
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
@@ -9605,6 +9766,13 @@ var InMemoryStateStore = class {
|
|
|
9605
9766
|
return true;
|
|
9606
9767
|
});
|
|
9607
9768
|
}
|
|
9769
|
+
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
9770
|
+
return [
|
|
9771
|
+
...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) => ({
|
|
9773
|
+
...r
|
|
9774
|
+
}));
|
|
9775
|
+
}
|
|
9608
9776
|
async listIncompleteRuns(namespace) {
|
|
9609
9777
|
return [
|
|
9610
9778
|
...this.runs.values()
|