@dudousxd/nestjs-durable-core 0.53.0 → 0.54.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 +161 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -14
- package/dist/index.d.ts +133 -14
- package/dist/index.js +158 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,8 @@ __export(index_exports, {
|
|
|
30
30
|
DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
|
|
31
31
|
DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
|
|
32
32
|
DurableWorkflow: () => DurableWorkflow,
|
|
33
|
+
EVENT_BUFFER_SCAN_LIMIT: () => EVENT_BUFFER_SCAN_LIMIT,
|
|
34
|
+
EVENT_TOKEN_PREFIX: () => EVENT_TOKEN_PREFIX,
|
|
33
35
|
Entities: () => Entities,
|
|
34
36
|
EventAccumulators: () => EventAccumulators,
|
|
35
37
|
FatalError: () => FatalError,
|
|
@@ -68,6 +70,7 @@ __export(index_exports, {
|
|
|
68
70
|
eventPrefix: () => eventPrefix,
|
|
69
71
|
eventToken: () => eventToken,
|
|
70
72
|
indexWaitersByRun: () => indexWaitersByRun,
|
|
73
|
+
isEventToken: () => isEventToken,
|
|
71
74
|
isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
|
|
72
75
|
matchesAttributes: () => matchesAttributes,
|
|
73
76
|
normalizeAttributeRows: () => normalizeAttributeRows,
|
|
@@ -871,10 +874,16 @@ var EventAccumulators = class {
|
|
|
871
874
|
// src/events.ts
|
|
872
875
|
var enc = /* @__PURE__ */ __name((s) => Buffer.from(s, "utf8").toString("base64"), "enc");
|
|
873
876
|
var dec = /* @__PURE__ */ __name((s) => Buffer.from(s, "base64").toString("utf8"), "dec");
|
|
877
|
+
var EVENT_BUFFER_SCAN_LIMIT = 50;
|
|
878
|
+
var EVENT_TOKEN_PREFIX = "event:";
|
|
874
879
|
function eventPrefix(name) {
|
|
875
|
-
return
|
|
880
|
+
return `${EVENT_TOKEN_PREFIX}${enc(name)}:`;
|
|
876
881
|
}
|
|
877
882
|
__name(eventPrefix, "eventPrefix");
|
|
883
|
+
function isEventToken(token) {
|
|
884
|
+
return token.startsWith(EVENT_TOKEN_PREFIX);
|
|
885
|
+
}
|
|
886
|
+
__name(isEventToken, "isEventToken");
|
|
878
887
|
function eventToken(name, match, runId, seq) {
|
|
879
888
|
return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
|
|
880
889
|
}
|
|
@@ -957,7 +966,7 @@ var RemoteWorkflowExecutor = class {
|
|
|
957
966
|
var WEBHOOK_PREFIX = "wh:";
|
|
958
967
|
var CHILD_PREFIX = "child:";
|
|
959
968
|
var BREAKPOINT_PREFIX = "bp:";
|
|
960
|
-
var EVENT_PREFIX =
|
|
969
|
+
var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
|
|
961
970
|
function classifyWaiterToken(token) {
|
|
962
971
|
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
963
972
|
on: "webhook",
|
|
@@ -1423,6 +1432,29 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1423
1432
|
value: buffered.payload
|
|
1424
1433
|
};
|
|
1425
1434
|
}, "consumeBuffered");
|
|
1435
|
+
const consumeBufferedEvent = /* @__PURE__ */ __name(async (name, match, token, seq) => {
|
|
1436
|
+
const candidates = await store.listBufferedEvents(name, EVENT_BUFFER_SCAN_LIMIT);
|
|
1437
|
+
for (const candidate of candidates) {
|
|
1438
|
+
if (!eventMatches(candidate.payload, match ?? {})) continue;
|
|
1439
|
+
if (!await store.removeBufferedEvent(candidate.id)) continue;
|
|
1440
|
+
await store.removeSignalWaiter({
|
|
1441
|
+
token,
|
|
1442
|
+
runId,
|
|
1443
|
+
seq
|
|
1444
|
+
});
|
|
1445
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1446
|
+
runId,
|
|
1447
|
+
seq,
|
|
1448
|
+
name: `signal:${token}`,
|
|
1449
|
+
kind: "signal",
|
|
1450
|
+
output: candidate.payload
|
|
1451
|
+
}));
|
|
1452
|
+
return {
|
|
1453
|
+
value: candidate.payload
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
}, "consumeBufferedEvent");
|
|
1426
1458
|
const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
|
|
1427
1459
|
const recorded = await readCheckpoint(deadlineSeq);
|
|
1428
1460
|
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
@@ -1503,6 +1535,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1503
1535
|
runId,
|
|
1504
1536
|
seq: current
|
|
1505
1537
|
});
|
|
1538
|
+
const bufferedHit2 = await consumeBufferedEvent(name, opts?.match, token2, current);
|
|
1539
|
+
if (bufferedHit2) return bufferedHit2.value;
|
|
1506
1540
|
throw new WorkflowSuspended();
|
|
1507
1541
|
}
|
|
1508
1542
|
const timeoutMs = opts.timeoutMs;
|
|
@@ -1525,6 +1559,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1525
1559
|
runId,
|
|
1526
1560
|
seq: waitSeq
|
|
1527
1561
|
});
|
|
1562
|
+
const bufferedHit = await consumeBufferedEvent(name, opts.match, token, waitSeq);
|
|
1563
|
+
if (bufferedHit) return bufferedHit.value;
|
|
1528
1564
|
throw new WorkflowSuspended(deadline);
|
|
1529
1565
|
}, "waitForEvent");
|
|
1530
1566
|
const task = /* @__PURE__ */ __name(async (name, dispatch, options) => {
|
|
@@ -1918,6 +1954,7 @@ var WorkflowEngine = class {
|
|
|
1918
1954
|
maxRecoveryAttempts;
|
|
1919
1955
|
remoteAdvanceSilenceMs;
|
|
1920
1956
|
reconcileMs;
|
|
1957
|
+
eventBufferTtlMs;
|
|
1921
1958
|
remoteRedispatchMs;
|
|
1922
1959
|
remoteRedispatchMax;
|
|
1923
1960
|
webhookUrl;
|
|
@@ -1991,6 +2028,7 @@ var WorkflowEngine = class {
|
|
|
1991
2028
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1992
2029
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1993
2030
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
2031
|
+
this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
|
|
1994
2032
|
this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
|
|
1995
2033
|
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
1996
2034
|
this.webhookUrl = deps.webhookUrl;
|
|
@@ -2664,14 +2702,16 @@ var WorkflowEngine = class {
|
|
|
2664
2702
|
* Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
|
|
2665
2703
|
* boot. A run still not due re-suspends cheaply without running new work.
|
|
2666
2704
|
*
|
|
2667
|
-
* Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
|
|
2668
|
-
* between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2669
|
-
* `takeBufferedSignal`, or
|
|
2670
|
-
* `signal`'s interleaving proof
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
2705
|
+
* Piggy-backs the signal/event-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
|
|
2706
|
+
* crash between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2707
|
+
* `takeBufferedSignal`/`listBufferedEvents`, or the other side's `takeSignalWaiter`/fan-out →
|
|
2708
|
+
* `bufferSignal`/`bufferEvent` → re-check — see `signal`'s interleaving proof, which `publishEvent`
|
|
2709
|
+
* mirrors for events) can leave both rows stranded with nothing left to pair them: the waiter's turn
|
|
2710
|
+
* already suspended, the other side that buffered it already returned. Event-wait suspends already
|
|
2711
|
+
* carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so they show up
|
|
2712
|
+
* HERE — that's what makes this due-timer pass the natural place for the sweep (and, for events, also
|
|
2713
|
+
* where an `eventBufferTtlMs` prune piggy-backs — see {@link reconcileBufferedEvent}). One bulk waiter
|
|
2714
|
+
* scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
|
|
2675
2715
|
*/
|
|
2676
2716
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
2677
2717
|
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
@@ -2681,11 +2721,15 @@ var WorkflowEngine = class {
|
|
|
2681
2721
|
}
|
|
2682
2722
|
/**
|
|
2683
2723
|
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
2684
|
-
* a registered signal waiter
|
|
2685
|
-
* directly instead of falling through to a full replay.
|
|
2686
|
-
* never buffered (
|
|
2687
|
-
*
|
|
2688
|
-
*
|
|
2724
|
+
* a registered signal OR event waiter and a matching payload is already buffered, pair and deliver
|
|
2725
|
+
* them directly instead of falling through to a full replay. Child/breakpoint waiter tokens are
|
|
2726
|
+
* never buffered (a child/breakpoint notifies via `signal` only once its counterpart is already
|
|
2727
|
+
* registered) — `takeBufferedSignal` on those is a guaranteed miss, so this only ever does real work
|
|
2728
|
+
* for a plain signal or an event waiter (classified via {@link isEventToken}).
|
|
2729
|
+
*
|
|
2730
|
+
* Also opportunistically prunes expired buffered EVENTS for this waiter's name when
|
|
2731
|
+
* {@link eventBufferTtlMs} is set (see its doc — scoped to names a registered waiter references in
|
|
2732
|
+
* this tick's batch, piggy-backing the SAME scan rather than a separate pass), even on a miss.
|
|
2689
2733
|
*
|
|
2690
2734
|
* Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
|
|
2691
2735
|
* no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
|
|
@@ -2695,6 +2739,7 @@ var WorkflowEngine = class {
|
|
|
2695
2739
|
*/
|
|
2696
2740
|
async reconcileSignalWaiter(run, waiter) {
|
|
2697
2741
|
if (!waiter) return void 0;
|
|
2742
|
+
if (isEventToken(waiter.token)) return this.reconcileBufferedEvent(run, waiter);
|
|
2698
2743
|
const buffered = await this.store.takeBufferedSignal(waiter.token);
|
|
2699
2744
|
if (!buffered) return void 0;
|
|
2700
2745
|
await this.store.removeSignalWaiter(waiter);
|
|
@@ -2705,6 +2750,34 @@ var WorkflowEngine = class {
|
|
|
2705
2750
|
};
|
|
2706
2751
|
}
|
|
2707
2752
|
/**
|
|
2753
|
+
* The EVENT half of {@link reconcileSignalWaiter}: scan this waiter's name for a matching buffered
|
|
2754
|
+
* event (same list+evaluate+claim shape as `waitForEvent`'s own consumption — the match predicate
|
|
2755
|
+
* belongs to the waiter, never the store), pruning any expired candidates it encounters along the
|
|
2756
|
+
* way when {@link eventBufferTtlMs} is configured. A pruned-but-non-matching candidate does not stop
|
|
2757
|
+
* the scan; only a MATCHING hit (or exhausting the scan window) does.
|
|
2758
|
+
*/
|
|
2759
|
+
async reconcileBufferedEvent(run, waiter) {
|
|
2760
|
+
const name = eventNameOf(waiter.token);
|
|
2761
|
+
const match = eventMatchOf(waiter.token);
|
|
2762
|
+
const candidates = await this.store.listBufferedEvents(name, EVENT_BUFFER_SCAN_LIMIT);
|
|
2763
|
+
const nowMs = this.clock();
|
|
2764
|
+
for (const candidate of candidates) {
|
|
2765
|
+
if (this.eventBufferTtlMs != null && nowMs - candidate.publishedAt > this.eventBufferTtlMs) {
|
|
2766
|
+
await this.store.removeBufferedEvent(candidate.id);
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (!eventMatches(candidate.payload, match)) continue;
|
|
2770
|
+
if (!await this.store.removeBufferedEvent(candidate.id)) continue;
|
|
2771
|
+
await this.store.removeSignalWaiter(waiter);
|
|
2772
|
+
const settled = await this.deliverSignal(waiter, candidate.payload);
|
|
2773
|
+
return settled ?? {
|
|
2774
|
+
runId: run.id,
|
|
2775
|
+
status: run.status
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
return void 0;
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2708
2781
|
* Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
|
|
2709
2782
|
* exactly one instance runs it), then resumes the body. Returns the result, or null if another
|
|
2710
2783
|
* instance holds the lease or the engine is draining. The default in-process dispatcher calls this;
|
|
@@ -2892,6 +2965,21 @@ var WorkflowEngine = class {
|
|
|
2892
2965
|
* 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
|
|
2893
2966
|
* payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
|
|
2894
2967
|
* of the same logical event (default: a fresh uuid, so each publish triggers once).
|
|
2968
|
+
*
|
|
2969
|
+
* Semantics (deliberate — mirrors `signalWithStart`'s reliability contract, so document it exactly):
|
|
2970
|
+
* a publish that resumes ≥1 live waiter, OR routes into an `eventBatch` accumulator / starts ≥1
|
|
2971
|
+
* subscriber, behaves exactly as above and is NOT buffered — fan-out stays live-only, same as
|
|
2972
|
+
* today. A publish that touches NEITHER — no live waiter matched, no subscriber exists/started —
|
|
2973
|
+
* buffers ONE copy (via {@link StateStore.bufferEvent}) unless `opts.buffer === false`, so it isn't
|
|
2974
|
+
* silently dropped just because nobody happened to be listening yet. That buffered copy is consumed
|
|
2975
|
+
* by the FIRST future `ctx.waitForEvent(name, { match })` whose match accepts its payload —
|
|
2976
|
+
* point-to-point on redelivery, by design (only one waiter ever gets it, even though the LIVE path
|
|
2977
|
+
* above is fan-out) — never by a second, later-registered onEvent subscriber (subscriber starts stay
|
|
2978
|
+
* live-only; `opts.id` dedupe applies to those only, unchanged). Right after buffering, this re-checks
|
|
2979
|
+
* `listSignalWaiters` ONCE — sandwich parity with `signal`'s own take → buffer → re-check — so a
|
|
2980
|
+
* waiter that registers in the sliver between the initial miss and the buffer write is still paired
|
|
2981
|
+
* instead of leaving both rows stranded (see `signal`'s interleaving proof; `waitForEvent`'s own
|
|
2982
|
+
* post-registration buffer scan is the mirror-image half of this same race).
|
|
2895
2983
|
*/
|
|
2896
2984
|
async publishEvent(name, payload, opts) {
|
|
2897
2985
|
let touched = 0;
|
|
@@ -2918,6 +3006,23 @@ var WorkflowEngine = class {
|
|
|
2918
3006
|
}
|
|
2919
3007
|
}
|
|
2920
3008
|
}
|
|
3009
|
+
if (touched > 0 || opts?.buffer === false) return touched;
|
|
3010
|
+
const bufferedId = globalThis.crypto.randomUUID();
|
|
3011
|
+
await this.store.bufferEvent({
|
|
3012
|
+
name,
|
|
3013
|
+
payload,
|
|
3014
|
+
id: bufferedId,
|
|
3015
|
+
publishedAt: this.clock()
|
|
3016
|
+
});
|
|
3017
|
+
const lateWaiters = await this.store.listSignalWaiters(eventPrefix(name));
|
|
3018
|
+
const lateWaiter = lateWaiters.find((w) => eventMatches(payload, eventMatchOf(w.token)));
|
|
3019
|
+
if (!lateWaiter) return touched;
|
|
3020
|
+
if (!await this.store.removeBufferedEvent(bufferedId)) return touched;
|
|
3021
|
+
const waiter = await this.store.takeSignalWaiter(lateWaiter.token);
|
|
3022
|
+
if (waiter) {
|
|
3023
|
+
const settled = await this.deliverSignal(waiter, payload);
|
|
3024
|
+
if (settled) touched += 1;
|
|
3025
|
+
}
|
|
2921
3026
|
return touched;
|
|
2922
3027
|
}
|
|
2923
3028
|
/**
|
|
@@ -4809,6 +4914,22 @@ var CodecStateStore = class {
|
|
|
4809
4914
|
payload: this.dec(buffered.payload)
|
|
4810
4915
|
};
|
|
4811
4916
|
}
|
|
4917
|
+
bufferEvent(input) {
|
|
4918
|
+
return this.inner.bufferEvent({
|
|
4919
|
+
...input,
|
|
4920
|
+
payload: this.enc(input.payload)
|
|
4921
|
+
});
|
|
4922
|
+
}
|
|
4923
|
+
async listBufferedEvents(name, limit) {
|
|
4924
|
+
const events = await this.inner.listBufferedEvents(name, limit);
|
|
4925
|
+
return events.map((e) => ({
|
|
4926
|
+
...e,
|
|
4927
|
+
payload: this.dec(e.payload)
|
|
4928
|
+
}));
|
|
4929
|
+
}
|
|
4930
|
+
removeBufferedEvent(id) {
|
|
4931
|
+
return this.inner.removeBufferedEvent(id);
|
|
4932
|
+
}
|
|
4812
4933
|
async listRuns(query) {
|
|
4813
4934
|
return (await this.inner.listRuns(query)).map((r) => this.decRun(r));
|
|
4814
4935
|
}
|
|
@@ -5281,6 +5402,28 @@ var InMemoryStateStore = class {
|
|
|
5281
5402
|
payload
|
|
5282
5403
|
};
|
|
5283
5404
|
}
|
|
5405
|
+
/** `id -> row`, so {@link removeBufferedEvent} can delete an exact row regardless of iteration
|
|
5406
|
+
* order — mirrors the SQL stores' PK-by-id delete. */
|
|
5407
|
+
bufferedEvents = /* @__PURE__ */ new Map();
|
|
5408
|
+
async bufferEvent(input) {
|
|
5409
|
+
this.bufferedEvents.set(input.id, {
|
|
5410
|
+
name: input.name,
|
|
5411
|
+
payload: input.payload,
|
|
5412
|
+
publishedAt: input.publishedAt
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5415
|
+
async listBufferedEvents(name, limit) {
|
|
5416
|
+
return [
|
|
5417
|
+
...this.bufferedEvents.entries()
|
|
5418
|
+
].filter(([, e]) => e.name === name).sort((a, b) => a[1].publishedAt - b[1].publishedAt).slice(0, limit).map(([id, e]) => ({
|
|
5419
|
+
id,
|
|
5420
|
+
payload: e.payload,
|
|
5421
|
+
publishedAt: e.publishedAt
|
|
5422
|
+
}));
|
|
5423
|
+
}
|
|
5424
|
+
async removeBufferedEvent(id) {
|
|
5425
|
+
return this.bufferedEvents.delete(id);
|
|
5426
|
+
}
|
|
5284
5427
|
/**
|
|
5285
5428
|
* Candidate run ids satisfying ONE attribute predicate, served from {@link attributeIndex} — the
|
|
5286
5429
|
* pushdown step. Numeric predicates scan only `numValue` rows under the key; string/boolean ones
|
|
@@ -5440,6 +5583,8 @@ var InMemoryTransport = class {
|
|
|
5440
5583
|
DURABLE_STEP_CONFIG,
|
|
5441
5584
|
DURABLE_STEP_NAME,
|
|
5442
5585
|
DurableWorkflow,
|
|
5586
|
+
EVENT_BUFFER_SCAN_LIMIT,
|
|
5587
|
+
EVENT_TOKEN_PREFIX,
|
|
5443
5588
|
Entities,
|
|
5444
5589
|
EventAccumulators,
|
|
5445
5590
|
FatalError,
|
|
@@ -5478,6 +5623,7 @@ var InMemoryTransport = class {
|
|
|
5478
5623
|
eventPrefix,
|
|
5479
5624
|
eventToken,
|
|
5480
5625
|
indexWaitersByRun,
|
|
5626
|
+
isEventToken,
|
|
5481
5627
|
isWorkflowControlFlowSignal,
|
|
5482
5628
|
matchesAttributes,
|
|
5483
5629
|
normalizeAttributeRows,
|