@dudousxd/nestjs-durable-core 0.52.0 → 0.53.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 +183 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +183 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -953,6 +953,47 @@ var RemoteWorkflowExecutor = class {
|
|
|
953
953
|
}
|
|
954
954
|
};
|
|
955
955
|
|
|
956
|
+
// src/run-waiting.ts
|
|
957
|
+
var WEBHOOK_PREFIX = "wh:";
|
|
958
|
+
var CHILD_PREFIX = "child:";
|
|
959
|
+
var BREAKPOINT_PREFIX = "bp:";
|
|
960
|
+
var EVENT_PREFIX = "event:";
|
|
961
|
+
function classifyWaiterToken(token) {
|
|
962
|
+
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
963
|
+
on: "webhook",
|
|
964
|
+
name: token
|
|
965
|
+
};
|
|
966
|
+
if (token.startsWith(BREAKPOINT_PREFIX)) return {
|
|
967
|
+
on: "breakpoint",
|
|
968
|
+
name: "breakpoint"
|
|
969
|
+
};
|
|
970
|
+
if (token.startsWith(CHILD_PREFIX)) return {
|
|
971
|
+
on: "child",
|
|
972
|
+
name: token.slice(CHILD_PREFIX.length)
|
|
973
|
+
};
|
|
974
|
+
if (token.startsWith(EVENT_PREFIX)) return {
|
|
975
|
+
on: "signal",
|
|
976
|
+
name: eventNameOf(token)
|
|
977
|
+
};
|
|
978
|
+
return {
|
|
979
|
+
on: "signal",
|
|
980
|
+
name: token
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
__name(classifyWaiterToken, "classifyWaiterToken");
|
|
984
|
+
function resolveRunWaiting(run, waiterByRun) {
|
|
985
|
+
if (run.status !== "suspended") return void 0;
|
|
986
|
+
const waiter = waiterByRun.get(run.id);
|
|
987
|
+
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
988
|
+
}
|
|
989
|
+
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
990
|
+
function indexWaitersByRun(waiters) {
|
|
991
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
992
|
+
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
993
|
+
return byRun;
|
|
994
|
+
}
|
|
995
|
+
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
996
|
+
|
|
956
997
|
// src/singleton-gate.ts
|
|
957
998
|
var SINGLETON_RETRY_MS = 1e3;
|
|
958
999
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -1408,6 +1449,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1408
1449
|
runId,
|
|
1409
1450
|
seq: current
|
|
1410
1451
|
});
|
|
1452
|
+
const lateBuffered2 = await consumeBuffered(token, current);
|
|
1453
|
+
if (lateBuffered2) {
|
|
1454
|
+
await store.removeSignalWaiter({
|
|
1455
|
+
token,
|
|
1456
|
+
runId,
|
|
1457
|
+
seq: current
|
|
1458
|
+
});
|
|
1459
|
+
return lateBuffered2.value;
|
|
1460
|
+
}
|
|
1411
1461
|
throw new WorkflowSuspended();
|
|
1412
1462
|
}
|
|
1413
1463
|
const timeoutMs = opts.timeoutMs;
|
|
@@ -1419,7 +1469,11 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1419
1469
|
const buffered = await consumeBuffered(token, waitSeq);
|
|
1420
1470
|
if (buffered) return buffered.value;
|
|
1421
1471
|
if (host.clock() >= deadline) {
|
|
1422
|
-
await store.
|
|
1472
|
+
await store.removeSignalWaiter({
|
|
1473
|
+
token,
|
|
1474
|
+
runId,
|
|
1475
|
+
seq: waitSeq
|
|
1476
|
+
}).catch(() => void 0);
|
|
1423
1477
|
throw new SignalTimeoutError(token, timeoutMs);
|
|
1424
1478
|
}
|
|
1425
1479
|
await store.putSignalWaiter({
|
|
@@ -1427,6 +1481,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1427
1481
|
runId,
|
|
1428
1482
|
seq: waitSeq
|
|
1429
1483
|
});
|
|
1484
|
+
const lateBuffered = await consumeBuffered(token, waitSeq);
|
|
1485
|
+
if (lateBuffered) {
|
|
1486
|
+
await store.removeSignalWaiter({
|
|
1487
|
+
token,
|
|
1488
|
+
runId,
|
|
1489
|
+
seq: waitSeq
|
|
1490
|
+
});
|
|
1491
|
+
return lateBuffered.value;
|
|
1492
|
+
}
|
|
1430
1493
|
throw new WorkflowSuspended(deadline);
|
|
1431
1494
|
}, "waitForSignal");
|
|
1432
1495
|
const waitForEvent = /* @__PURE__ */ __name(async (name, opts) => {
|
|
@@ -1450,7 +1513,11 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1450
1513
|
const waited = await readCheckpoint(waitSeq);
|
|
1451
1514
|
if (waited && waited.status === "completed") return waited.output;
|
|
1452
1515
|
if (host.clock() >= deadline) {
|
|
1453
|
-
await store.
|
|
1516
|
+
await store.removeSignalWaiter({
|
|
1517
|
+
token,
|
|
1518
|
+
runId,
|
|
1519
|
+
seq: waitSeq
|
|
1520
|
+
}).catch(() => void 0);
|
|
1454
1521
|
throw new SignalTimeoutError(`event:${name}`, timeoutMs);
|
|
1455
1522
|
}
|
|
1456
1523
|
await store.putSignalWaiter({
|
|
@@ -2596,9 +2663,46 @@ var WorkflowEngine = class {
|
|
|
2596
2663
|
/**
|
|
2597
2664
|
* Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
|
|
2598
2665
|
* boot. A run still not due re-suspends cheaply without running new work.
|
|
2666
|
+
*
|
|
2667
|
+
* Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
|
|
2668
|
+
* between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2669
|
+
* `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
|
|
2670
|
+
* `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
|
|
2671
|
+
* waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
|
|
2672
|
+
* suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
|
|
2673
|
+
* they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
|
|
2674
|
+
* bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
|
|
2599
2675
|
*/
|
|
2600
2676
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
2601
|
-
|
|
2677
|
+
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
2678
|
+
if (due.length === 0) return [];
|
|
2679
|
+
const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
|
|
2680
|
+
return this.resumeLeased(due, nowMs, (run) => this.reconcileSignalWaiter(run, waiterByRun.get(run.id)));
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
2684
|
+
* a registered signal waiter AND its token already has a buffered payload, pair and deliver them
|
|
2685
|
+
* directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
|
|
2686
|
+
* never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
|
|
2687
|
+
* once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
|
|
2688
|
+
* miss, and this is naturally signal-only without needing to classify the token.
|
|
2689
|
+
*
|
|
2690
|
+
* Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
|
|
2691
|
+
* no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
|
|
2692
|
+
* via {@link deliverSignal}, and even a `NamespaceMismatch` there is reported as a real (non-nullish)
|
|
2693
|
+
* result — returning `null` would read as "not settled" to `resumeLeased`'s `??` fallback and
|
|
2694
|
+
* trigger a second, redundant `resume` call on the same run.
|
|
2695
|
+
*/
|
|
2696
|
+
async reconcileSignalWaiter(run, waiter) {
|
|
2697
|
+
if (!waiter) return void 0;
|
|
2698
|
+
const buffered = await this.store.takeBufferedSignal(waiter.token);
|
|
2699
|
+
if (!buffered) return void 0;
|
|
2700
|
+
await this.store.removeSignalWaiter(waiter);
|
|
2701
|
+
const settled = await this.deliverSignal(waiter, buffered.payload);
|
|
2702
|
+
return settled ?? {
|
|
2703
|
+
runId: run.id,
|
|
2704
|
+
status: run.status
|
|
2705
|
+
};
|
|
2602
2706
|
}
|
|
2603
2707
|
/**
|
|
2604
2708
|
* Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
|
|
@@ -2816,16 +2920,17 @@ var WorkflowEngine = class {
|
|
|
2816
2920
|
}
|
|
2817
2921
|
return touched;
|
|
2818
2922
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2923
|
+
/**
|
|
2924
|
+
* Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
|
|
2925
|
+
* resume the run. Shared by {@link signal}'s direct hit, its post-buffer re-check reclaim, and the
|
|
2926
|
+
* due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
|
|
2927
|
+
* same way.
|
|
2928
|
+
*/
|
|
2929
|
+
async deliverSignal(waiter, payload) {
|
|
2825
2930
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
2826
2931
|
runId: waiter.runId,
|
|
2827
2932
|
seq: waiter.seq,
|
|
2828
|
-
name: `signal:${token}`,
|
|
2933
|
+
name: `signal:${waiter.token}`,
|
|
2829
2934
|
kind: "signal",
|
|
2830
2935
|
output: payload,
|
|
2831
2936
|
// Carry the awaiting command's fan group (set when a `ctx.gather_children`/`ctx.all` fan-out
|
|
@@ -2840,6 +2945,42 @@ var WorkflowEngine = class {
|
|
|
2840
2945
|
});
|
|
2841
2946
|
}
|
|
2842
2947
|
/**
|
|
2948
|
+
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
2949
|
+
* the run result — or null if no run is (or ends up) waiting for it.
|
|
2950
|
+
*
|
|
2951
|
+
* Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check, same shape in
|
|
2952
|
+
* the remote `waitSignal` command): a waiter can register in the sliver between this method's
|
|
2953
|
+
* initial `takeSignalWaiter` miss and its `bufferSignal` write, and the naive old flow (buffer and
|
|
2954
|
+
* return) would then leave BOTH a buffered payload and a registered waiter sitting in the store with
|
|
2955
|
+
* nothing left to pair them — a lost wake, forever suspended.
|
|
2956
|
+
*
|
|
2957
|
+
* The interleaving proof: the waiter side does check → put → re-check, this side does take → buffer
|
|
2958
|
+
* → re-check. Whichever side's SECOND look runs last is guaranteed to observe the other side's
|
|
2959
|
+
* write, because it always follows that write in real time:
|
|
2960
|
+
* - If the waiter's `put` lands before this method's first `take`, the take just finds it directly
|
|
2961
|
+
* (the pre-existing, non-racy path).
|
|
2962
|
+
* - If the waiter's `put` lands after this method's `bufferSignal`, our re-check below either sees
|
|
2963
|
+
* it (if `put` also lands before the re-check) or the waiter's OWN post-put re-check sees our
|
|
2964
|
+
* buffer (if `put` lands after our re-check but the waiter hasn't re-checked yet either — its
|
|
2965
|
+
* re-check strictly follows its own put).
|
|
2966
|
+
* - If the waiter's `put` lands between this method's `take` and its re-`take`, the re-check below
|
|
2967
|
+
* catches it.
|
|
2968
|
+
* Either way, the destructive `take*` ops (on the waiter row, on the buffer) arbitrate: whichever
|
|
2969
|
+
* side actually removes the row is the one that delivers, so a signal is never delivered twice.
|
|
2970
|
+
*/
|
|
2971
|
+
async signal(token, payload) {
|
|
2972
|
+
const waiter = await this.store.takeSignalWaiter(token);
|
|
2973
|
+
if (waiter) return this.deliverSignal(waiter, payload);
|
|
2974
|
+
await this.store.bufferSignal(token, payload);
|
|
2975
|
+
const lateWaiter = await this.store.takeSignalWaiter(token);
|
|
2976
|
+
if (!lateWaiter) return null;
|
|
2977
|
+
const reclaimed = await this.store.takeBufferedSignal(token);
|
|
2978
|
+
if (!reclaimed) {
|
|
2979
|
+
return null;
|
|
2980
|
+
}
|
|
2981
|
+
return this.deliverSignal(lateWaiter, reclaimed.payload);
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2843
2984
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
2844
2985
|
* signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
|
|
2845
2986
|
* consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
|
|
@@ -3312,6 +3453,14 @@ var WorkflowEngine = class {
|
|
|
3312
3453
|
error: outcome.error
|
|
3313
3454
|
};
|
|
3314
3455
|
}
|
|
3456
|
+
const latest = await this.store.getRun(run.id);
|
|
3457
|
+
if (latest?.status === "cancelled") {
|
|
3458
|
+
return {
|
|
3459
|
+
runId: run.id,
|
|
3460
|
+
status: "cancelled",
|
|
3461
|
+
error: latest.error
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3315
3464
|
await this.store.updateRun(run.id, {
|
|
3316
3465
|
status: "suspended",
|
|
3317
3466
|
wakeAt: this.reconcileWakeAt(outcome.wakeAt),
|
|
@@ -3654,22 +3803,34 @@ var WorkflowEngine = class {
|
|
|
3654
3803
|
}));
|
|
3655
3804
|
wakeAt = wakeAt == null ? deadline : Math.min(wakeAt, deadline);
|
|
3656
3805
|
} else if (cmd.kind === "waitSignal") {
|
|
3657
|
-
const
|
|
3658
|
-
if (buffered) {
|
|
3806
|
+
const deliverBuffered = /* @__PURE__ */ __name(async (payload) => {
|
|
3659
3807
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
3660
3808
|
runId: run.id,
|
|
3661
3809
|
seq: cmd.seq,
|
|
3662
3810
|
name: `signal:${cmd.signal}`,
|
|
3663
3811
|
kind: "signal",
|
|
3664
|
-
output:
|
|
3812
|
+
output: payload
|
|
3665
3813
|
}));
|
|
3666
3814
|
setTimeout(() => void this.resume(run.id).catch(() => void 0), 0);
|
|
3815
|
+
}, "deliverBuffered");
|
|
3816
|
+
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
3817
|
+
if (buffered) {
|
|
3818
|
+
await deliverBuffered(buffered.payload);
|
|
3667
3819
|
} else {
|
|
3668
3820
|
await this.store.putSignalWaiter({
|
|
3669
3821
|
token: cmd.signal,
|
|
3670
3822
|
runId: run.id,
|
|
3671
3823
|
seq: cmd.seq
|
|
3672
3824
|
});
|
|
3825
|
+
const lateBuffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
3826
|
+
if (lateBuffered) {
|
|
3827
|
+
await this.store.removeSignalWaiter({
|
|
3828
|
+
token: cmd.signal,
|
|
3829
|
+
runId: run.id,
|
|
3830
|
+
seq: cmd.seq
|
|
3831
|
+
});
|
|
3832
|
+
await deliverBuffered(lateBuffered.payload);
|
|
3833
|
+
}
|
|
3673
3834
|
}
|
|
3674
3835
|
} else if (cmd.kind === "startChild") {
|
|
3675
3836
|
const childId = `${run.id}.child.${cmd.seq}`;
|
|
@@ -4529,47 +4690,6 @@ var TERMINAL_RUN_STATUSES = [
|
|
|
4529
4690
|
"dead"
|
|
4530
4691
|
];
|
|
4531
4692
|
|
|
4532
|
-
// src/run-waiting.ts
|
|
4533
|
-
var WEBHOOK_PREFIX = "wh:";
|
|
4534
|
-
var CHILD_PREFIX = "child:";
|
|
4535
|
-
var BREAKPOINT_PREFIX = "bp:";
|
|
4536
|
-
var EVENT_PREFIX = "event:";
|
|
4537
|
-
function classifyWaiterToken(token) {
|
|
4538
|
-
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
4539
|
-
on: "webhook",
|
|
4540
|
-
name: token
|
|
4541
|
-
};
|
|
4542
|
-
if (token.startsWith(BREAKPOINT_PREFIX)) return {
|
|
4543
|
-
on: "breakpoint",
|
|
4544
|
-
name: "breakpoint"
|
|
4545
|
-
};
|
|
4546
|
-
if (token.startsWith(CHILD_PREFIX)) return {
|
|
4547
|
-
on: "child",
|
|
4548
|
-
name: token.slice(CHILD_PREFIX.length)
|
|
4549
|
-
};
|
|
4550
|
-
if (token.startsWith(EVENT_PREFIX)) return {
|
|
4551
|
-
on: "signal",
|
|
4552
|
-
name: eventNameOf(token)
|
|
4553
|
-
};
|
|
4554
|
-
return {
|
|
4555
|
-
on: "signal",
|
|
4556
|
-
name: token
|
|
4557
|
-
};
|
|
4558
|
-
}
|
|
4559
|
-
__name(classifyWaiterToken, "classifyWaiterToken");
|
|
4560
|
-
function resolveRunWaiting(run, waiterByRun) {
|
|
4561
|
-
if (run.status !== "suspended") return void 0;
|
|
4562
|
-
const waiter = waiterByRun.get(run.id);
|
|
4563
|
-
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
4564
|
-
}
|
|
4565
|
-
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
4566
|
-
function indexWaitersByRun(waiters) {
|
|
4567
|
-
const byRun = /* @__PURE__ */ new Map();
|
|
4568
|
-
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
4569
|
-
return byRun;
|
|
4570
|
-
}
|
|
4571
|
-
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
4572
|
-
|
|
4573
4693
|
// src/codec-state-store.ts
|
|
4574
4694
|
var CodecStateStore = class {
|
|
4575
4695
|
static {
|
|
@@ -4677,6 +4797,9 @@ var CodecStateStore = class {
|
|
|
4677
4797
|
listSignalWaiters(prefix) {
|
|
4678
4798
|
return this.inner.listSignalWaiters(prefix);
|
|
4679
4799
|
}
|
|
4800
|
+
removeSignalWaiter(waiter) {
|
|
4801
|
+
return this.inner.removeSignalWaiter(waiter);
|
|
4802
|
+
}
|
|
4680
4803
|
bufferSignal(token, payload) {
|
|
4681
4804
|
return this.inner.bufferSignal(token, this.enc(payload));
|
|
4682
4805
|
}
|
|
@@ -5129,6 +5252,12 @@ var InMemoryStateStore = class {
|
|
|
5129
5252
|
...w
|
|
5130
5253
|
}));
|
|
5131
5254
|
}
|
|
5255
|
+
async removeSignalWaiter(waiter) {
|
|
5256
|
+
const current = this.signalWaiters.get(waiter.token);
|
|
5257
|
+
if (current && current.runId === waiter.runId && current.seq === waiter.seq) {
|
|
5258
|
+
this.signalWaiters.delete(waiter.token);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5132
5261
|
// No real DB transaction (there's no DB) — run the work and save the checkpoint. Exactly-once
|
|
5133
5262
|
// across a crash needs a SQL store; this keeps `ctx.transaction` usable in tests / local dev.
|
|
5134
5263
|
async transaction(work) {
|