@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.d.cts CHANGED
@@ -648,6 +648,39 @@ interface StateStore {
648
648
  takeBufferedSignal(token: string): Promise<{
649
649
  payload: unknown;
650
650
  } | null>;
651
+ /**
652
+ * Buffer a published event that matched NO live waiter, so a LATER `ctx.waitForEvent(name, { match })`
653
+ * still consumes it instead of it being silently dropped — the events analog of {@link bufferSignal}'s
654
+ * reliability contract for signals, but MATCH-based rather than token-based: an event's buffer is keyed
655
+ * by `name` alone (many waiters can share a name with different `match` criteria), so consumption is
656
+ * list ({@link listBufferedEvents}) + evaluate the WAITER's own match predicate + claim
657
+ * ({@link removeBufferedEvent}) — never a blind take, because the store has no way to know which
658
+ * candidate a given waiter wants. `input.id` is minted by the caller (engine.publishEvent) so
659
+ * {@link removeBufferedEvent} can later target this exact row.
660
+ */
661
+ bufferEvent(input: {
662
+ name: string;
663
+ payload: unknown;
664
+ id: string;
665
+ publishedAt: number;
666
+ }): Promise<void>;
667
+ /**
668
+ * Buffered events for `name`, OLDEST (`publishedAt`) first, capped at `limit`. A waiter scans these,
669
+ * evaluates its own `eventMatches(payload, match)` locally, and claims the one it wants via
670
+ * {@link removeBufferedEvent} — the match predicate belongs to the WAITER, never the store.
671
+ */
672
+ listBufferedEvents(name: string, limit: number): Promise<Array<{
673
+ id: string;
674
+ payload: unknown;
675
+ publishedAt: number;
676
+ }>>;
677
+ /**
678
+ * Atomically delete the buffered event `id`. Returns `true` iff a row was actually deleted, `false`
679
+ * if it was already gone (claimed by a concurrent waiter, reclaimed by `engine.publishEvent`'s own
680
+ * late re-check, or pruned as expired) — the arbiter under concurrency: whichever caller's delete
681
+ * returns `true` is the one that gets to deliver the payload; every other caller backs off.
682
+ */
683
+ removeBufferedEvent(id: string): Promise<boolean>;
651
684
  /**
652
685
  * Run `work` in a SINGLE store transaction — giving it the store-native transaction handle (`raw`)
653
686
  * for the caller's own DB writes plus a `saveCheckpoint` that commits IN THE SAME transaction, so a
@@ -2007,6 +2040,19 @@ interface WorkflowEngineDeps {
2007
2040
  * for a genuinely-orphaned run. Default 5min; set `0` to disable (the prior wake-forever behavior).
2008
2041
  */
2009
2042
  reconcileMs?: number | undefined;
2043
+ /**
2044
+ * Retention for a BUFFERED event that no `waitForEvent` ever claims (see
2045
+ * {@link WorkflowEngine.publishEvent}'s buffering — a publish matching no live waiter keeps ONE
2046
+ * copy around for a later matching wait). Default `undefined`: keep it until consumed, same as
2047
+ * {@link StateStore.bufferSignal}'s buffered signals — no silent expiry. Set a duration (ms) to
2048
+ * have the {@link resumeDueTimers} reconcile pass prune rows past `publishedAt + eventBufferTtlMs`
2049
+ * as it encounters them. Scoped to event NAMES the current reconcile tick's registered waiters
2050
+ * reference (the sweep has no reason to otherwise enumerate every buffered name) — a name with a
2051
+ * stale buffered event but zero waiters ever registered against it is not visited by this pass; that
2052
+ * is an accepted gap given `listBufferedEvents` is name-scoped by design (see interfaces.ts), not a
2053
+ * bug to route around here.
2054
+ */
2055
+ eventBufferTtlMs?: number | undefined;
2010
2056
  /**
2011
2057
  * Opt-in self-heal window (ms) for a remote step with NO `timeoutMs` whose dispatched job was LOST —
2012
2058
  * the worker crashed mid-step leaving no result, or the transport dropped the job (a Redis
@@ -2104,6 +2150,7 @@ declare class WorkflowEngine {
2104
2150
  private readonly maxRecoveryAttempts?;
2105
2151
  private readonly remoteAdvanceSilenceMs?;
2106
2152
  private readonly reconcileMs;
2153
+ private readonly eventBufferTtlMs;
2107
2154
  private readonly remoteRedispatchMs;
2108
2155
  private readonly remoteRedispatchMax;
2109
2156
  private readonly webhookUrl?;
@@ -2384,23 +2431,29 @@ declare class WorkflowEngine {
2384
2431
  * Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
2385
2432
  * boot. A run still not due re-suspends cheaply without running new work.
2386
2433
  *
2387
- * Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
2388
- * between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
2389
- * `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
2390
- * `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
2391
- * waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
2392
- * suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
2393
- * they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
2394
- * bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
2434
+ * Piggy-backs the signal/event-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
2435
+ * crash between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
2436
+ * `takeBufferedSignal`/`listBufferedEvents`, or the other side's `takeSignalWaiter`/fan-out →
2437
+ * `bufferSignal`/`bufferEvent` → re-check — see `signal`'s interleaving proof, which `publishEvent`
2438
+ * mirrors for events) can leave both rows stranded with nothing left to pair them: the waiter's turn
2439
+ * already suspended, the other side that buffered it already returned. Event-wait suspends already
2440
+ * carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so they show up
2441
+ * HERE — that's what makes this due-timer pass the natural place for the sweep (and, for events, also
2442
+ * where an `eventBufferTtlMs` prune piggy-backs — see {@link reconcileBufferedEvent}). One bulk waiter
2443
+ * scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
2395
2444
  */
2396
2445
  resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
2397
2446
  /**
2398
2447
  * The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
2399
- * a registered signal waiter AND its token already has a buffered payload, pair and deliver them
2400
- * directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
2401
- * never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
2402
- * once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
2403
- * miss, and this is naturally signal-only without needing to classify the token.
2448
+ * a registered signal OR event waiter and a matching payload is already buffered, pair and deliver
2449
+ * them directly instead of falling through to a full replay. Child/breakpoint waiter tokens are
2450
+ * never buffered (a child/breakpoint notifies via `signal` only once its counterpart is already
2451
+ * registered) — `takeBufferedSignal` on those is a guaranteed miss, so this only ever does real work
2452
+ * for a plain signal or an event waiter (classified via {@link isEventToken}).
2453
+ *
2454
+ * Also opportunistically prunes expired buffered EVENTS for this waiter's name when
2455
+ * {@link eventBufferTtlMs} is set (see its doc — scoped to names a registered waiter references in
2456
+ * this tick's batch, piggy-backing the SAME scan rather than a separate pass), even on a miss.
2404
2457
  *
2405
2458
  * Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
2406
2459
  * no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
@@ -2409,6 +2462,14 @@ declare class WorkflowEngine {
2409
2462
  * trigger a second, redundant `resume` call on the same run.
2410
2463
  */
2411
2464
  private reconcileSignalWaiter;
2465
+ /**
2466
+ * The EVENT half of {@link reconcileSignalWaiter}: scan this waiter's name for a matching buffered
2467
+ * event (same list+evaluate+claim shape as `waitForEvent`'s own consumption — the match predicate
2468
+ * belongs to the waiter, never the store), pruning any expired candidates it encounters along the
2469
+ * way when {@link eventBufferTtlMs} is configured. A pruned-but-non-matching candidate does not stop
2470
+ * the scan; only a MATCHING hit (or exhausting the scan window) does.
2471
+ */
2472
+ private reconcileBufferedEvent;
2412
2473
  /**
2413
2474
  * Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
2414
2475
  * exactly one instance runs it), then resumes the body. Returns the result, or null if another
@@ -2477,9 +2538,25 @@ declare class WorkflowEngine {
2477
2538
  * 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
2478
2539
  * payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
2479
2540
  * of the same logical event (default: a fresh uuid, so each publish triggers once).
2541
+ *
2542
+ * Semantics (deliberate — mirrors `signalWithStart`'s reliability contract, so document it exactly):
2543
+ * a publish that resumes ≥1 live waiter, OR routes into an `eventBatch` accumulator / starts ≥1
2544
+ * subscriber, behaves exactly as above and is NOT buffered — fan-out stays live-only, same as
2545
+ * today. A publish that touches NEITHER — no live waiter matched, no subscriber exists/started —
2546
+ * buffers ONE copy (via {@link StateStore.bufferEvent}) unless `opts.buffer === false`, so it isn't
2547
+ * silently dropped just because nobody happened to be listening yet. That buffered copy is consumed
2548
+ * by the FIRST future `ctx.waitForEvent(name, { match })` whose match accepts its payload —
2549
+ * point-to-point on redelivery, by design (only one waiter ever gets it, even though the LIVE path
2550
+ * above is fan-out) — never by a second, later-registered onEvent subscriber (subscriber starts stay
2551
+ * live-only; `opts.id` dedupe applies to those only, unchanged). Right after buffering, this re-checks
2552
+ * `listSignalWaiters` ONCE — sandwich parity with `signal`'s own take → buffer → re-check — so a
2553
+ * waiter that registers in the sliver between the initial miss and the buffer write is still paired
2554
+ * instead of leaving both rows stranded (see `signal`'s interleaving proof; `waitForEvent`'s own
2555
+ * post-registration buffer scan is the mirror-image half of this same race).
2480
2556
  */
2481
2557
  publishEvent(name: string, payload: unknown, opts?: {
2482
2558
  id?: string;
2559
+ buffer?: boolean;
2483
2560
  }): Promise<number>;
2484
2561
  /**
2485
2562
  * Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
@@ -3225,14 +3302,41 @@ declare class CodecStateStore implements StateStore {
3225
3302
  takeBufferedSignal(token: string): Promise<{
3226
3303
  payload: unknown;
3227
3304
  } | null>;
3305
+ bufferEvent(input: {
3306
+ name: string;
3307
+ payload: unknown;
3308
+ id: string;
3309
+ publishedAt: number;
3310
+ }): Promise<void>;
3311
+ listBufferedEvents(name: string, limit: number): Promise<Array<{
3312
+ id: string;
3313
+ payload: unknown;
3314
+ publishedAt: number;
3315
+ }>>;
3316
+ removeBufferedEvent(id: string): Promise<boolean>;
3228
3317
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
3229
3318
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
3230
3319
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
3231
3320
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
3232
3321
  }
3233
3322
 
3323
+ /**
3324
+ * How many candidates a buffered-event scan considers (`listBufferedEvents(name, limit)`) before
3325
+ * giving up. An event's buffer is keyed by `name` only (see {@link eventMatches}'s match-based
3326
+ * consumption), so a hot name with many distinct payloads sitting unclaimed could in principle
3327
+ * accumulate an unbounded backlog; this caps the per-scan cost regardless. Shared by the workflow-ctx
3328
+ * consumer and the engine's reconcile sweep so both see the same window.
3329
+ */
3330
+ declare const EVENT_BUFFER_SCAN_LIMIT = 50;
3331
+ /** The literal prefix EVERY event-waiter token starts with, regardless of `name` — `eventPrefix(name)`
3332
+ * always begins with this; exported so a caller that only needs to CLASSIFY a token (not fan out a
3333
+ * specific name) doesn't have to re-derive or duplicate the literal (see run-waiting.ts's own copy,
3334
+ * kept in sync with this one, and engine.ts's reconcile sweep). */
3335
+ declare const EVENT_TOKEN_PREFIX = "event:";
3234
3336
  /** Token prefix for every waiter on `name` — used by `listSignalWaiters` to fan an event out. */
3235
3337
  declare function eventPrefix(name: string): string;
3338
+ /** Whether `token` is an event-waiter token (vs. a plain signal / child / webhook / breakpoint one). */
3339
+ declare function isEventToken(token: string): boolean;
3236
3340
  /** Build the unique waiter token for one `waitForEvent` call (name + match + run position). */
3237
3341
  declare function eventToken(name: string, match: Record<string, unknown> | undefined, runId: string, seq: number): string;
3238
3342
  /** The event name embedded in an event waiter token (the 2nd `:`-segment is the base64 name), so a
@@ -3598,6 +3702,21 @@ declare class InMemoryStateStore implements StateStore {
3598
3702
  takeBufferedSignal(token: string): Promise<{
3599
3703
  payload: unknown;
3600
3704
  } | null>;
3705
+ /** `id -> row`, so {@link removeBufferedEvent} can delete an exact row regardless of iteration
3706
+ * order — mirrors the SQL stores' PK-by-id delete. */
3707
+ private readonly bufferedEvents;
3708
+ bufferEvent(input: {
3709
+ name: string;
3710
+ payload: unknown;
3711
+ id: string;
3712
+ publishedAt: number;
3713
+ }): Promise<void>;
3714
+ listBufferedEvents(name: string, limit: number): Promise<Array<{
3715
+ id: string;
3716
+ payload: unknown;
3717
+ publishedAt: number;
3718
+ }>>;
3719
+ removeBufferedEvent(id: string): Promise<boolean>;
3601
3720
  /**
3602
3721
  * Candidate run ids satisfying ONE attribute predicate, served from {@link attributeIndex} — the
3603
3722
  * pushdown step. Numeric predicates scan only `numValue` rows under the key; string/boolean ones
@@ -3636,4 +3755,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
3636
3755
  onControl(handler: (msg: ControlMessage) => void): void;
3637
3756
  }
3638
3757
 
3639
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, CONTROL_FLOW_SIGNAL, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, classifyWaiterToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, indexWaitersByRun, isWorkflowControlFlowSignal, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, readSearchAttributes, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
3758
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, CONTROL_FLOW_SIGNAL, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, EVENT_BUFFER_SCAN_LIMIT, EVENT_TOKEN_PREFIX, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, classifyWaiterToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, indexWaitersByRun, isEventToken, isWorkflowControlFlowSignal, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, readSearchAttributes, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
package/dist/index.d.ts CHANGED
@@ -648,6 +648,39 @@ interface StateStore {
648
648
  takeBufferedSignal(token: string): Promise<{
649
649
  payload: unknown;
650
650
  } | null>;
651
+ /**
652
+ * Buffer a published event that matched NO live waiter, so a LATER `ctx.waitForEvent(name, { match })`
653
+ * still consumes it instead of it being silently dropped — the events analog of {@link bufferSignal}'s
654
+ * reliability contract for signals, but MATCH-based rather than token-based: an event's buffer is keyed
655
+ * by `name` alone (many waiters can share a name with different `match` criteria), so consumption is
656
+ * list ({@link listBufferedEvents}) + evaluate the WAITER's own match predicate + claim
657
+ * ({@link removeBufferedEvent}) — never a blind take, because the store has no way to know which
658
+ * candidate a given waiter wants. `input.id` is minted by the caller (engine.publishEvent) so
659
+ * {@link removeBufferedEvent} can later target this exact row.
660
+ */
661
+ bufferEvent(input: {
662
+ name: string;
663
+ payload: unknown;
664
+ id: string;
665
+ publishedAt: number;
666
+ }): Promise<void>;
667
+ /**
668
+ * Buffered events for `name`, OLDEST (`publishedAt`) first, capped at `limit`. A waiter scans these,
669
+ * evaluates its own `eventMatches(payload, match)` locally, and claims the one it wants via
670
+ * {@link removeBufferedEvent} — the match predicate belongs to the WAITER, never the store.
671
+ */
672
+ listBufferedEvents(name: string, limit: number): Promise<Array<{
673
+ id: string;
674
+ payload: unknown;
675
+ publishedAt: number;
676
+ }>>;
677
+ /**
678
+ * Atomically delete the buffered event `id`. Returns `true` iff a row was actually deleted, `false`
679
+ * if it was already gone (claimed by a concurrent waiter, reclaimed by `engine.publishEvent`'s own
680
+ * late re-check, or pruned as expired) — the arbiter under concurrency: whichever caller's delete
681
+ * returns `true` is the one that gets to deliver the payload; every other caller backs off.
682
+ */
683
+ removeBufferedEvent(id: string): Promise<boolean>;
651
684
  /**
652
685
  * Run `work` in a SINGLE store transaction — giving it the store-native transaction handle (`raw`)
653
686
  * for the caller's own DB writes plus a `saveCheckpoint` that commits IN THE SAME transaction, so a
@@ -2007,6 +2040,19 @@ interface WorkflowEngineDeps {
2007
2040
  * for a genuinely-orphaned run. Default 5min; set `0` to disable (the prior wake-forever behavior).
2008
2041
  */
2009
2042
  reconcileMs?: number | undefined;
2043
+ /**
2044
+ * Retention for a BUFFERED event that no `waitForEvent` ever claims (see
2045
+ * {@link WorkflowEngine.publishEvent}'s buffering — a publish matching no live waiter keeps ONE
2046
+ * copy around for a later matching wait). Default `undefined`: keep it until consumed, same as
2047
+ * {@link StateStore.bufferSignal}'s buffered signals — no silent expiry. Set a duration (ms) to
2048
+ * have the {@link resumeDueTimers} reconcile pass prune rows past `publishedAt + eventBufferTtlMs`
2049
+ * as it encounters them. Scoped to event NAMES the current reconcile tick's registered waiters
2050
+ * reference (the sweep has no reason to otherwise enumerate every buffered name) — a name with a
2051
+ * stale buffered event but zero waiters ever registered against it is not visited by this pass; that
2052
+ * is an accepted gap given `listBufferedEvents` is name-scoped by design (see interfaces.ts), not a
2053
+ * bug to route around here.
2054
+ */
2055
+ eventBufferTtlMs?: number | undefined;
2010
2056
  /**
2011
2057
  * Opt-in self-heal window (ms) for a remote step with NO `timeoutMs` whose dispatched job was LOST —
2012
2058
  * the worker crashed mid-step leaving no result, or the transport dropped the job (a Redis
@@ -2104,6 +2150,7 @@ declare class WorkflowEngine {
2104
2150
  private readonly maxRecoveryAttempts?;
2105
2151
  private readonly remoteAdvanceSilenceMs?;
2106
2152
  private readonly reconcileMs;
2153
+ private readonly eventBufferTtlMs;
2107
2154
  private readonly remoteRedispatchMs;
2108
2155
  private readonly remoteRedispatchMax;
2109
2156
  private readonly webhookUrl?;
@@ -2384,23 +2431,29 @@ declare class WorkflowEngine {
2384
2431
  * Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
2385
2432
  * boot. A run still not due re-suspends cheaply without running new work.
2386
2433
  *
2387
- * Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
2388
- * between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
2389
- * `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
2390
- * `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
2391
- * waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
2392
- * suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
2393
- * they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
2394
- * bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
2434
+ * Piggy-backs the signal/event-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
2435
+ * crash between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
2436
+ * `takeBufferedSignal`/`listBufferedEvents`, or the other side's `takeSignalWaiter`/fan-out →
2437
+ * `bufferSignal`/`bufferEvent` → re-check — see `signal`'s interleaving proof, which `publishEvent`
2438
+ * mirrors for events) can leave both rows stranded with nothing left to pair them: the waiter's turn
2439
+ * already suspended, the other side that buffered it already returned. Event-wait suspends already
2440
+ * carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so they show up
2441
+ * HERE — that's what makes this due-timer pass the natural place for the sweep (and, for events, also
2442
+ * where an `eventBufferTtlMs` prune piggy-backs — see {@link reconcileBufferedEvent}). One bulk waiter
2443
+ * scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
2395
2444
  */
2396
2445
  resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
2397
2446
  /**
2398
2447
  * The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
2399
- * a registered signal waiter AND its token already has a buffered payload, pair and deliver them
2400
- * directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
2401
- * never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
2402
- * once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
2403
- * miss, and this is naturally signal-only without needing to classify the token.
2448
+ * a registered signal OR event waiter and a matching payload is already buffered, pair and deliver
2449
+ * them directly instead of falling through to a full replay. Child/breakpoint waiter tokens are
2450
+ * never buffered (a child/breakpoint notifies via `signal` only once its counterpart is already
2451
+ * registered) — `takeBufferedSignal` on those is a guaranteed miss, so this only ever does real work
2452
+ * for a plain signal or an event waiter (classified via {@link isEventToken}).
2453
+ *
2454
+ * Also opportunistically prunes expired buffered EVENTS for this waiter's name when
2455
+ * {@link eventBufferTtlMs} is set (see its doc — scoped to names a registered waiter references in
2456
+ * this tick's batch, piggy-backing the SAME scan rather than a separate pass), even on a miss.
2404
2457
  *
2405
2458
  * Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
2406
2459
  * no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
@@ -2409,6 +2462,14 @@ declare class WorkflowEngine {
2409
2462
  * trigger a second, redundant `resume` call on the same run.
2410
2463
  */
2411
2464
  private reconcileSignalWaiter;
2465
+ /**
2466
+ * The EVENT half of {@link reconcileSignalWaiter}: scan this waiter's name for a matching buffered
2467
+ * event (same list+evaluate+claim shape as `waitForEvent`'s own consumption — the match predicate
2468
+ * belongs to the waiter, never the store), pruning any expired candidates it encounters along the
2469
+ * way when {@link eventBufferTtlMs} is configured. A pruned-but-non-matching candidate does not stop
2470
+ * the scan; only a MATCHING hit (or exhausting the scan window) does.
2471
+ */
2472
+ private reconcileBufferedEvent;
2412
2473
  /**
2413
2474
  * Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
2414
2475
  * exactly one instance runs it), then resumes the body. Returns the result, or null if another
@@ -2477,9 +2538,25 @@ declare class WorkflowEngine {
2477
2538
  * 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
2478
2539
  * payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
2479
2540
  * of the same logical event (default: a fresh uuid, so each publish triggers once).
2541
+ *
2542
+ * Semantics (deliberate — mirrors `signalWithStart`'s reliability contract, so document it exactly):
2543
+ * a publish that resumes ≥1 live waiter, OR routes into an `eventBatch` accumulator / starts ≥1
2544
+ * subscriber, behaves exactly as above and is NOT buffered — fan-out stays live-only, same as
2545
+ * today. A publish that touches NEITHER — no live waiter matched, no subscriber exists/started —
2546
+ * buffers ONE copy (via {@link StateStore.bufferEvent}) unless `opts.buffer === false`, so it isn't
2547
+ * silently dropped just because nobody happened to be listening yet. That buffered copy is consumed
2548
+ * by the FIRST future `ctx.waitForEvent(name, { match })` whose match accepts its payload —
2549
+ * point-to-point on redelivery, by design (only one waiter ever gets it, even though the LIVE path
2550
+ * above is fan-out) — never by a second, later-registered onEvent subscriber (subscriber starts stay
2551
+ * live-only; `opts.id` dedupe applies to those only, unchanged). Right after buffering, this re-checks
2552
+ * `listSignalWaiters` ONCE — sandwich parity with `signal`'s own take → buffer → re-check — so a
2553
+ * waiter that registers in the sliver between the initial miss and the buffer write is still paired
2554
+ * instead of leaving both rows stranded (see `signal`'s interleaving proof; `waitForEvent`'s own
2555
+ * post-registration buffer scan is the mirror-image half of this same race).
2480
2556
  */
2481
2557
  publishEvent(name: string, payload: unknown, opts?: {
2482
2558
  id?: string;
2559
+ buffer?: boolean;
2483
2560
  }): Promise<number>;
2484
2561
  /**
2485
2562
  * Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
@@ -3225,14 +3302,41 @@ declare class CodecStateStore implements StateStore {
3225
3302
  takeBufferedSignal(token: string): Promise<{
3226
3303
  payload: unknown;
3227
3304
  } | null>;
3305
+ bufferEvent(input: {
3306
+ name: string;
3307
+ payload: unknown;
3308
+ id: string;
3309
+ publishedAt: number;
3310
+ }): Promise<void>;
3311
+ listBufferedEvents(name: string, limit: number): Promise<Array<{
3312
+ id: string;
3313
+ payload: unknown;
3314
+ publishedAt: number;
3315
+ }>>;
3316
+ removeBufferedEvent(id: string): Promise<boolean>;
3228
3317
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
3229
3318
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
3230
3319
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
3231
3320
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
3232
3321
  }
3233
3322
 
3323
+ /**
3324
+ * How many candidates a buffered-event scan considers (`listBufferedEvents(name, limit)`) before
3325
+ * giving up. An event's buffer is keyed by `name` only (see {@link eventMatches}'s match-based
3326
+ * consumption), so a hot name with many distinct payloads sitting unclaimed could in principle
3327
+ * accumulate an unbounded backlog; this caps the per-scan cost regardless. Shared by the workflow-ctx
3328
+ * consumer and the engine's reconcile sweep so both see the same window.
3329
+ */
3330
+ declare const EVENT_BUFFER_SCAN_LIMIT = 50;
3331
+ /** The literal prefix EVERY event-waiter token starts with, regardless of `name` — `eventPrefix(name)`
3332
+ * always begins with this; exported so a caller that only needs to CLASSIFY a token (not fan out a
3333
+ * specific name) doesn't have to re-derive or duplicate the literal (see run-waiting.ts's own copy,
3334
+ * kept in sync with this one, and engine.ts's reconcile sweep). */
3335
+ declare const EVENT_TOKEN_PREFIX = "event:";
3234
3336
  /** Token prefix for every waiter on `name` — used by `listSignalWaiters` to fan an event out. */
3235
3337
  declare function eventPrefix(name: string): string;
3338
+ /** Whether `token` is an event-waiter token (vs. a plain signal / child / webhook / breakpoint one). */
3339
+ declare function isEventToken(token: string): boolean;
3236
3340
  /** Build the unique waiter token for one `waitForEvent` call (name + match + run position). */
3237
3341
  declare function eventToken(name: string, match: Record<string, unknown> | undefined, runId: string, seq: number): string;
3238
3342
  /** The event name embedded in an event waiter token (the 2nd `:`-segment is the base64 name), so a
@@ -3598,6 +3702,21 @@ declare class InMemoryStateStore implements StateStore {
3598
3702
  takeBufferedSignal(token: string): Promise<{
3599
3703
  payload: unknown;
3600
3704
  } | null>;
3705
+ /** `id -> row`, so {@link removeBufferedEvent} can delete an exact row regardless of iteration
3706
+ * order — mirrors the SQL stores' PK-by-id delete. */
3707
+ private readonly bufferedEvents;
3708
+ bufferEvent(input: {
3709
+ name: string;
3710
+ payload: unknown;
3711
+ id: string;
3712
+ publishedAt: number;
3713
+ }): Promise<void>;
3714
+ listBufferedEvents(name: string, limit: number): Promise<Array<{
3715
+ id: string;
3716
+ payload: unknown;
3717
+ publishedAt: number;
3718
+ }>>;
3719
+ removeBufferedEvent(id: string): Promise<boolean>;
3601
3720
  /**
3602
3721
  * Candidate run ids satisfying ONE attribute predicate, served from {@link attributeIndex} — the
3603
3722
  * pushdown step. Numeric predicates scan only `numValue` rows under the key; string/boolean ones
@@ -3636,4 +3755,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
3636
3755
  onControl(handler: (msg: ControlMessage) => void): void;
3637
3756
  }
3638
3757
 
3639
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, CONTROL_FLOW_SIGNAL, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, classifyWaiterToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, indexWaitersByRun, isWorkflowControlFlowSignal, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, readSearchAttributes, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
3758
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, CONTROL_FLOW_SIGNAL, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, EVENT_BUFFER_SCAN_LIMIT, EVENT_TOKEN_PREFIX, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, classifyWaiterToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, indexWaitersByRun, isEventToken, isWorkflowControlFlowSignal, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, readSearchAttributes, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };