@dudousxd/nestjs-durable-core 0.53.0 → 0.55.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 +204 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -14
- package/dist/index.d.ts +146 -14
- package/dist/index.js +201 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -783,10 +783,16 @@ var EventAccumulators = class {
|
|
|
783
783
|
// src/events.ts
|
|
784
784
|
var enc = /* @__PURE__ */ __name((s) => Buffer.from(s, "utf8").toString("base64"), "enc");
|
|
785
785
|
var dec = /* @__PURE__ */ __name((s) => Buffer.from(s, "base64").toString("utf8"), "dec");
|
|
786
|
+
var EVENT_BUFFER_SCAN_LIMIT = 50;
|
|
787
|
+
var EVENT_TOKEN_PREFIX = "event:";
|
|
786
788
|
function eventPrefix(name) {
|
|
787
|
-
return
|
|
789
|
+
return `${EVENT_TOKEN_PREFIX}${enc(name)}:`;
|
|
788
790
|
}
|
|
789
791
|
__name(eventPrefix, "eventPrefix");
|
|
792
|
+
function isEventToken(token) {
|
|
793
|
+
return token.startsWith(EVENT_TOKEN_PREFIX);
|
|
794
|
+
}
|
|
795
|
+
__name(isEventToken, "isEventToken");
|
|
790
796
|
function eventToken(name, match, runId, seq) {
|
|
791
797
|
return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
|
|
792
798
|
}
|
|
@@ -869,7 +875,7 @@ var RemoteWorkflowExecutor = class {
|
|
|
869
875
|
var WEBHOOK_PREFIX = "wh:";
|
|
870
876
|
var CHILD_PREFIX = "child:";
|
|
871
877
|
var BREAKPOINT_PREFIX = "bp:";
|
|
872
|
-
var EVENT_PREFIX =
|
|
878
|
+
var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
|
|
873
879
|
function classifyWaiterToken(token) {
|
|
874
880
|
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
875
881
|
on: "webhook",
|
|
@@ -1093,6 +1099,21 @@ var TransportPool = class {
|
|
|
1093
1099
|
];
|
|
1094
1100
|
}
|
|
1095
1101
|
/**
|
|
1102
|
+
* The first transport (in pool order) reporting a LIVE heartbeat for `group` — the one a
|
|
1103
|
+
* convention-routed remote workflow must dispatch on. In a mixed pool (e.g. a tenant-scoped
|
|
1104
|
+
* control plane pairing its namespaced transport with a bare-prefix one for operator-convention
|
|
1105
|
+
* tenant workers), each transport sees a DIFFERENT keyspace: dispatching on `primary` while the
|
|
1106
|
+
* live worker heartbeats on another transport parks the task on a queue nobody consumes. Returns
|
|
1107
|
+
* `undefined` when no transport reports the group (mirrors the merged `listWorkerGroups` miss).
|
|
1108
|
+
*/
|
|
1109
|
+
async transportWithLiveGroup(group) {
|
|
1110
|
+
for (const { transport } of this.transports) {
|
|
1111
|
+
if (!transport.listWorkerGroups) continue;
|
|
1112
|
+
if ((await transport.listWorkerGroups()).includes(group)) return transport;
|
|
1113
|
+
}
|
|
1114
|
+
return void 0;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1096
1117
|
* Propagate the engine's `namespace` to every transport that partitions by it — so the same
|
|
1097
1118
|
* namespace that scopes the store also scopes each transport's queues/keys. A transport that
|
|
1098
1119
|
* doesn't partition (no `useNamespace`) is skipped. Idempotent; a transport given an explicit
|
|
@@ -1335,6 +1356,29 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1335
1356
|
value: buffered.payload
|
|
1336
1357
|
};
|
|
1337
1358
|
}, "consumeBuffered");
|
|
1359
|
+
const consumeBufferedEvent = /* @__PURE__ */ __name(async (name, match, token, seq) => {
|
|
1360
|
+
const candidates = await store.listBufferedEvents(name, EVENT_BUFFER_SCAN_LIMIT);
|
|
1361
|
+
for (const candidate of candidates) {
|
|
1362
|
+
if (!eventMatches(candidate.payload, match ?? {})) continue;
|
|
1363
|
+
if (!await store.removeBufferedEvent(candidate.id)) continue;
|
|
1364
|
+
await store.removeSignalWaiter({
|
|
1365
|
+
token,
|
|
1366
|
+
runId,
|
|
1367
|
+
seq
|
|
1368
|
+
});
|
|
1369
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1370
|
+
runId,
|
|
1371
|
+
seq,
|
|
1372
|
+
name: `signal:${token}`,
|
|
1373
|
+
kind: "signal",
|
|
1374
|
+
output: candidate.payload
|
|
1375
|
+
}));
|
|
1376
|
+
return {
|
|
1377
|
+
value: candidate.payload
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
return null;
|
|
1381
|
+
}, "consumeBufferedEvent");
|
|
1338
1382
|
const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
|
|
1339
1383
|
const recorded = await readCheckpoint(deadlineSeq);
|
|
1340
1384
|
const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
|
|
@@ -1415,6 +1459,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1415
1459
|
runId,
|
|
1416
1460
|
seq: current
|
|
1417
1461
|
});
|
|
1462
|
+
const bufferedHit2 = await consumeBufferedEvent(name, opts?.match, token2, current);
|
|
1463
|
+
if (bufferedHit2) return bufferedHit2.value;
|
|
1418
1464
|
throw new WorkflowSuspended();
|
|
1419
1465
|
}
|
|
1420
1466
|
const timeoutMs = opts.timeoutMs;
|
|
@@ -1437,6 +1483,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1437
1483
|
runId,
|
|
1438
1484
|
seq: waitSeq
|
|
1439
1485
|
});
|
|
1486
|
+
const bufferedHit = await consumeBufferedEvent(name, opts.match, token, waitSeq);
|
|
1487
|
+
if (bufferedHit) return bufferedHit.value;
|
|
1440
1488
|
throw new WorkflowSuspended(deadline);
|
|
1441
1489
|
}, "waitForEvent");
|
|
1442
1490
|
const task = /* @__PURE__ */ __name(async (name, dispatch, options) => {
|
|
@@ -1830,6 +1878,7 @@ var WorkflowEngine = class {
|
|
|
1830
1878
|
maxRecoveryAttempts;
|
|
1831
1879
|
remoteAdvanceSilenceMs;
|
|
1832
1880
|
reconcileMs;
|
|
1881
|
+
eventBufferTtlMs;
|
|
1833
1882
|
remoteRedispatchMs;
|
|
1834
1883
|
remoteRedispatchMax;
|
|
1835
1884
|
webhookUrl;
|
|
@@ -1903,6 +1952,7 @@ var WorkflowEngine = class {
|
|
|
1903
1952
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1904
1953
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1905
1954
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
1955
|
+
this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
|
|
1906
1956
|
this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
|
|
1907
1957
|
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
1908
1958
|
this.webhookUrl = deps.webhookUrl;
|
|
@@ -2379,10 +2429,10 @@ var WorkflowEngine = class {
|
|
|
2379
2429
|
* liveness changes between calls, exactly as {@link synthesizeRemoteChild} does.
|
|
2380
2430
|
*/
|
|
2381
2431
|
async resolveRemoteByConvention(run) {
|
|
2382
|
-
const liveGroups = await this.pool.listWorkerGroups();
|
|
2383
2432
|
const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
|
|
2384
|
-
|
|
2385
|
-
|
|
2433
|
+
const liveTransport = await this.pool.transportWithLiveGroup(group);
|
|
2434
|
+
if (!liveTransport) return void 0;
|
|
2435
|
+
const executor = new RemoteWorkflowExecutor(liveTransport, run.workflow, run.namespace);
|
|
2386
2436
|
return {
|
|
2387
2437
|
name: run.workflow,
|
|
2388
2438
|
version: run.workflowVersion,
|
|
@@ -2576,14 +2626,16 @@ var WorkflowEngine = class {
|
|
|
2576
2626
|
* Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
|
|
2577
2627
|
* boot. A run still not due re-suspends cheaply without running new work.
|
|
2578
2628
|
*
|
|
2579
|
-
* Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
|
|
2580
|
-
* between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2581
|
-
* `takeBufferedSignal`, or
|
|
2582
|
-
* `signal`'s interleaving proof
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
2629
|
+
* Piggy-backs the signal/event-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
|
|
2630
|
+
* crash between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2631
|
+
* `takeBufferedSignal`/`listBufferedEvents`, or the other side's `takeSignalWaiter`/fan-out →
|
|
2632
|
+
* `bufferSignal`/`bufferEvent` → re-check — see `signal`'s interleaving proof, which `publishEvent`
|
|
2633
|
+
* mirrors for events) can leave both rows stranded with nothing left to pair them: the waiter's turn
|
|
2634
|
+
* already suspended, the other side that buffered it already returned. Event-wait suspends already
|
|
2635
|
+
* carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so they show up
|
|
2636
|
+
* HERE — that's what makes this due-timer pass the natural place for the sweep (and, for events, also
|
|
2637
|
+
* where an `eventBufferTtlMs` prune piggy-backs — see {@link reconcileBufferedEvent}). One bulk waiter
|
|
2638
|
+
* scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
|
|
2587
2639
|
*/
|
|
2588
2640
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
2589
2641
|
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
@@ -2593,11 +2645,15 @@ var WorkflowEngine = class {
|
|
|
2593
2645
|
}
|
|
2594
2646
|
/**
|
|
2595
2647
|
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
2596
|
-
* a registered signal waiter
|
|
2597
|
-
* directly instead of falling through to a full replay.
|
|
2598
|
-
* never buffered (
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2648
|
+
* a registered signal OR event waiter and a matching payload is already buffered, pair and deliver
|
|
2649
|
+
* them directly instead of falling through to a full replay. Child/breakpoint waiter tokens are
|
|
2650
|
+
* never buffered (a child/breakpoint notifies via `signal` only once its counterpart is already
|
|
2651
|
+
* registered) — `takeBufferedSignal` on those is a guaranteed miss, so this only ever does real work
|
|
2652
|
+
* for a plain signal or an event waiter (classified via {@link isEventToken}).
|
|
2653
|
+
*
|
|
2654
|
+
* Also opportunistically prunes expired buffered EVENTS for this waiter's name when
|
|
2655
|
+
* {@link eventBufferTtlMs} is set (see its doc — scoped to names a registered waiter references in
|
|
2656
|
+
* this tick's batch, piggy-backing the SAME scan rather than a separate pass), even on a miss.
|
|
2601
2657
|
*
|
|
2602
2658
|
* Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
|
|
2603
2659
|
* no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
|
|
@@ -2607,6 +2663,7 @@ var WorkflowEngine = class {
|
|
|
2607
2663
|
*/
|
|
2608
2664
|
async reconcileSignalWaiter(run, waiter) {
|
|
2609
2665
|
if (!waiter) return void 0;
|
|
2666
|
+
if (isEventToken(waiter.token)) return this.reconcileBufferedEvent(run, waiter);
|
|
2610
2667
|
const buffered = await this.store.takeBufferedSignal(waiter.token);
|
|
2611
2668
|
if (!buffered) return void 0;
|
|
2612
2669
|
await this.store.removeSignalWaiter(waiter);
|
|
@@ -2617,6 +2674,34 @@ var WorkflowEngine = class {
|
|
|
2617
2674
|
};
|
|
2618
2675
|
}
|
|
2619
2676
|
/**
|
|
2677
|
+
* The EVENT half of {@link reconcileSignalWaiter}: scan this waiter's name for a matching buffered
|
|
2678
|
+
* event (same list+evaluate+claim shape as `waitForEvent`'s own consumption — the match predicate
|
|
2679
|
+
* belongs to the waiter, never the store), pruning any expired candidates it encounters along the
|
|
2680
|
+
* way when {@link eventBufferTtlMs} is configured. A pruned-but-non-matching candidate does not stop
|
|
2681
|
+
* the scan; only a MATCHING hit (or exhausting the scan window) does.
|
|
2682
|
+
*/
|
|
2683
|
+
async reconcileBufferedEvent(run, waiter) {
|
|
2684
|
+
const name = eventNameOf(waiter.token);
|
|
2685
|
+
const match = eventMatchOf(waiter.token);
|
|
2686
|
+
const candidates = await this.store.listBufferedEvents(name, EVENT_BUFFER_SCAN_LIMIT);
|
|
2687
|
+
const nowMs = this.clock();
|
|
2688
|
+
for (const candidate of candidates) {
|
|
2689
|
+
if (this.eventBufferTtlMs != null && nowMs - candidate.publishedAt > this.eventBufferTtlMs) {
|
|
2690
|
+
await this.store.removeBufferedEvent(candidate.id);
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (!eventMatches(candidate.payload, match)) continue;
|
|
2694
|
+
if (!await this.store.removeBufferedEvent(candidate.id)) continue;
|
|
2695
|
+
await this.store.removeSignalWaiter(waiter);
|
|
2696
|
+
const settled = await this.deliverSignal(waiter, candidate.payload);
|
|
2697
|
+
return settled ?? {
|
|
2698
|
+
runId: run.id,
|
|
2699
|
+
status: run.status
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
return void 0;
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2620
2705
|
* Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
|
|
2621
2706
|
* exactly one instance runs it), then resumes the body. Returns the result, or null if another
|
|
2622
2707
|
* instance holds the lease or the engine is draining. The default in-process dispatcher calls this;
|
|
@@ -2804,6 +2889,21 @@ var WorkflowEngine = class {
|
|
|
2804
2889
|
* 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
|
|
2805
2890
|
* payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
|
|
2806
2891
|
* of the same logical event (default: a fresh uuid, so each publish triggers once).
|
|
2892
|
+
*
|
|
2893
|
+
* Semantics (deliberate — mirrors `signalWithStart`'s reliability contract, so document it exactly):
|
|
2894
|
+
* a publish that resumes ≥1 live waiter, OR routes into an `eventBatch` accumulator / starts ≥1
|
|
2895
|
+
* subscriber, behaves exactly as above and is NOT buffered — fan-out stays live-only, same as
|
|
2896
|
+
* today. A publish that touches NEITHER — no live waiter matched, no subscriber exists/started —
|
|
2897
|
+
* buffers ONE copy (via {@link StateStore.bufferEvent}) unless `opts.buffer === false`, so it isn't
|
|
2898
|
+
* silently dropped just because nobody happened to be listening yet. That buffered copy is consumed
|
|
2899
|
+
* by the FIRST future `ctx.waitForEvent(name, { match })` whose match accepts its payload —
|
|
2900
|
+
* point-to-point on redelivery, by design (only one waiter ever gets it, even though the LIVE path
|
|
2901
|
+
* above is fan-out) — never by a second, later-registered onEvent subscriber (subscriber starts stay
|
|
2902
|
+
* live-only; `opts.id` dedupe applies to those only, unchanged). Right after buffering, this re-checks
|
|
2903
|
+
* `listSignalWaiters` ONCE — sandwich parity with `signal`'s own take → buffer → re-check — so a
|
|
2904
|
+
* waiter that registers in the sliver between the initial miss and the buffer write is still paired
|
|
2905
|
+
* instead of leaving both rows stranded (see `signal`'s interleaving proof; `waitForEvent`'s own
|
|
2906
|
+
* post-registration buffer scan is the mirror-image half of this same race).
|
|
2807
2907
|
*/
|
|
2808
2908
|
async publishEvent(name, payload, opts) {
|
|
2809
2909
|
let touched = 0;
|
|
@@ -2830,6 +2930,23 @@ var WorkflowEngine = class {
|
|
|
2830
2930
|
}
|
|
2831
2931
|
}
|
|
2832
2932
|
}
|
|
2933
|
+
if (touched > 0 || opts?.buffer === false) return touched;
|
|
2934
|
+
const bufferedId = globalThis.crypto.randomUUID();
|
|
2935
|
+
await this.store.bufferEvent({
|
|
2936
|
+
name,
|
|
2937
|
+
payload,
|
|
2938
|
+
id: bufferedId,
|
|
2939
|
+
publishedAt: this.clock()
|
|
2940
|
+
});
|
|
2941
|
+
const lateWaiters = await this.store.listSignalWaiters(eventPrefix(name));
|
|
2942
|
+
const lateWaiter = lateWaiters.find((w) => eventMatches(payload, eventMatchOf(w.token)));
|
|
2943
|
+
if (!lateWaiter) return touched;
|
|
2944
|
+
if (!await this.store.removeBufferedEvent(bufferedId)) return touched;
|
|
2945
|
+
const waiter = await this.store.takeSignalWaiter(lateWaiter.token);
|
|
2946
|
+
if (waiter) {
|
|
2947
|
+
const settled = await this.deliverSignal(waiter, payload);
|
|
2948
|
+
if (settled) touched += 1;
|
|
2949
|
+
}
|
|
2833
2950
|
return touched;
|
|
2834
2951
|
}
|
|
2835
2952
|
/**
|
|
@@ -2934,6 +3051,27 @@ var WorkflowEngine = class {
|
|
|
2934
3051
|
void this.signal(`child:${runId}`, completion).catch(() => void 0);
|
|
2935
3052
|
}
|
|
2936
3053
|
/**
|
|
3054
|
+
* Deferred child start shared by the in-process ctx host and the remote `startChild` command.
|
|
3055
|
+
* Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
|
|
3056
|
+
* that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
|
|
3057
|
+
* failure) must NOT be swallowed: the parent is already suspended on `child:<childId>` (the waiter
|
|
3058
|
+
* is put BEFORE start on every path), so a silent drop parks it in suspended-forever, invisibly
|
|
3059
|
+
* re-attempting on every recovery wake — a misconfigured remote child looks exactly like a healthy
|
|
3060
|
+
* long wait. Instead the failure is delivered to that waiter like a failed child (notifyParent):
|
|
3061
|
+
* the parent resumes and fails loudly with the cause. For a fire-and-forget `ctx.startChild`
|
|
3062
|
+
* (spawn — no waiter) the completion is buffered; a later join by the same id consumes it and
|
|
3063
|
+
* correctly observes the failed start.
|
|
3064
|
+
*/
|
|
3065
|
+
startChildDeferred(workflow, input, childId, opts) {
|
|
3066
|
+
queueMicrotask(() => void this.start(workflow, input, childId, opts).catch((err) => {
|
|
3067
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3068
|
+
this.notifyParent(childId, {
|
|
3069
|
+
ok: false,
|
|
3070
|
+
error: `child workflow "${workflow}" failed to start: ${message}`
|
|
3071
|
+
});
|
|
3072
|
+
}));
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
2937
3075
|
* Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
|
|
2938
3076
|
* `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
|
|
2939
3077
|
* steps' compensations run in reverse (visible as `compensate:<step>` events), then it's marked
|
|
@@ -3757,9 +3895,9 @@ var WorkflowEngine = class {
|
|
|
3757
3895
|
parallelGroup: cmd.parallelGroup
|
|
3758
3896
|
});
|
|
3759
3897
|
if (!await this.store.getRun(childId)) {
|
|
3760
|
-
|
|
3898
|
+
this.startChildDeferred(cmd.workflow, cmd.input, childId, {
|
|
3761
3899
|
namespace: run.namespace
|
|
3762
|
-
})
|
|
3900
|
+
});
|
|
3763
3901
|
}
|
|
3764
3902
|
} else {
|
|
3765
3903
|
throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
|
|
@@ -4138,10 +4276,10 @@ var WorkflowEngine = class {
|
|
|
4138
4276
|
// `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
|
|
4139
4277
|
// tenant's worker pool onto the shared/dev workers.
|
|
4140
4278
|
startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
|
|
4141
|
-
|
|
4279
|
+
this.startChildDeferred(workflow, input, id, {
|
|
4142
4280
|
priority,
|
|
4143
4281
|
namespace: parentNamespace
|
|
4144
|
-
})
|
|
4282
|
+
});
|
|
4145
4283
|
}, "startChild"),
|
|
4146
4284
|
// Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
|
|
4147
4285
|
// idempotent on a terminal/cancelled run (returns its existing status without side effects), so
|
|
@@ -4721,6 +4859,22 @@ var CodecStateStore = class {
|
|
|
4721
4859
|
payload: this.dec(buffered.payload)
|
|
4722
4860
|
};
|
|
4723
4861
|
}
|
|
4862
|
+
bufferEvent(input) {
|
|
4863
|
+
return this.inner.bufferEvent({
|
|
4864
|
+
...input,
|
|
4865
|
+
payload: this.enc(input.payload)
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
async listBufferedEvents(name, limit) {
|
|
4869
|
+
const events = await this.inner.listBufferedEvents(name, limit);
|
|
4870
|
+
return events.map((e) => ({
|
|
4871
|
+
...e,
|
|
4872
|
+
payload: this.dec(e.payload)
|
|
4873
|
+
}));
|
|
4874
|
+
}
|
|
4875
|
+
removeBufferedEvent(id) {
|
|
4876
|
+
return this.inner.removeBufferedEvent(id);
|
|
4877
|
+
}
|
|
4724
4878
|
async listRuns(query) {
|
|
4725
4879
|
return (await this.inner.listRuns(query)).map((r) => this.decRun(r));
|
|
4726
4880
|
}
|
|
@@ -5193,6 +5347,28 @@ var InMemoryStateStore = class {
|
|
|
5193
5347
|
payload
|
|
5194
5348
|
};
|
|
5195
5349
|
}
|
|
5350
|
+
/** `id -> row`, so {@link removeBufferedEvent} can delete an exact row regardless of iteration
|
|
5351
|
+
* order — mirrors the SQL stores' PK-by-id delete. */
|
|
5352
|
+
bufferedEvents = /* @__PURE__ */ new Map();
|
|
5353
|
+
async bufferEvent(input) {
|
|
5354
|
+
this.bufferedEvents.set(input.id, {
|
|
5355
|
+
name: input.name,
|
|
5356
|
+
payload: input.payload,
|
|
5357
|
+
publishedAt: input.publishedAt
|
|
5358
|
+
});
|
|
5359
|
+
}
|
|
5360
|
+
async listBufferedEvents(name, limit) {
|
|
5361
|
+
return [
|
|
5362
|
+
...this.bufferedEvents.entries()
|
|
5363
|
+
].filter(([, e]) => e.name === name).sort((a, b) => a[1].publishedAt - b[1].publishedAt).slice(0, limit).map(([id, e]) => ({
|
|
5364
|
+
id,
|
|
5365
|
+
payload: e.payload,
|
|
5366
|
+
publishedAt: e.publishedAt
|
|
5367
|
+
}));
|
|
5368
|
+
}
|
|
5369
|
+
async removeBufferedEvent(id) {
|
|
5370
|
+
return this.bufferedEvents.delete(id);
|
|
5371
|
+
}
|
|
5196
5372
|
/**
|
|
5197
5373
|
* Candidate run ids satisfying ONE attribute predicate, served from {@link attributeIndex} — the
|
|
5198
5374
|
* pushdown step. Numeric predicates scan only `numValue` rows under the key; string/boolean ones
|
|
@@ -5351,6 +5527,8 @@ export {
|
|
|
5351
5527
|
DURABLE_STEP_CONFIG,
|
|
5352
5528
|
DURABLE_STEP_NAME,
|
|
5353
5529
|
DurableWorkflow,
|
|
5530
|
+
EVENT_BUFFER_SCAN_LIMIT,
|
|
5531
|
+
EVENT_TOKEN_PREFIX,
|
|
5354
5532
|
Entities,
|
|
5355
5533
|
EventAccumulators,
|
|
5356
5534
|
FatalError,
|
|
@@ -5389,6 +5567,7 @@ export {
|
|
|
5389
5567
|
eventPrefix,
|
|
5390
5568
|
eventToken,
|
|
5391
5569
|
indexWaitersByRun,
|
|
5570
|
+
isEventToken,
|
|
5392
5571
|
isWorkflowControlFlowSignal,
|
|
5393
5572
|
matchesAttributes,
|
|
5394
5573
|
normalizeAttributeRows,
|