@dudousxd/nestjs-durable-core 0.51.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.d.cts CHANGED
@@ -628,6 +628,16 @@ interface StateStore {
628
628
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
629
629
  /** List waiters whose `token` starts with `prefix` — used to fan out an event to its subscribers. */
630
630
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
631
+ /**
632
+ * Delete the EXACT waiter row — `token` AND `runId` AND `seq` must all match — no-op if absent.
633
+ * Unlike {@link takeSignalWaiter}, which deletes ANY row for `token` (fine when the caller just won
634
+ * the race to consume it), this is for a caller removing its OWN registration after resolving the
635
+ * wait some other way (a buffered hit, a timeout): blind `takeSignalWaiter(token)` there could steal
636
+ * a DIFFERENT run's waiter that has since claimed the same token (`token` is the store's primary key,
637
+ * so a later `putSignalWaiter` for the same token replaces the row). The exact-match variant only
638
+ * ever removes the row this caller itself put there.
639
+ */
640
+ removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
631
641
  /**
632
642
  * Buffer a signal whose waiter hasn't arrived yet, so the next `waitForSignal(token)` consumes it
633
643
  * instead of it being lost (FIFO per token). Makes signals reliable regardless of timing and
@@ -1749,6 +1759,59 @@ declare function runInWorkflowCtx<T>(ctx: WorkflowCtx, fn: () => T): T;
1749
1759
  */
1750
1760
  declare function currentWorkflowCtx(): WorkflowCtx | undefined;
1751
1761
 
1762
+ /**
1763
+ * Cross-package, cross-runtime marker for engine CONTROL-FLOW signals — {@link WorkflowSuspended}
1764
+ * and {@link ContinueAsNew} (this package), plus the thin worker's {@link
1765
+ * import('@dudousxd/durable-worker').Suspend} (`@dudousxd/durable-worker`, the BullMQ
1766
+ * route-by-handler runtime). These are thrown by `WorkflowCtx`/`WorkflowContext` ops to unwind the
1767
+ * current turn — never real failures — so a workflow's `catch` block MUST rethrow them untouched.
1768
+ * Running a failure/cleanup path on a control-flow signal corrupts the run: it emits extra
1769
+ * `recordStep` commands into history that a subsequent replay didn't produce the first time,
1770
+ * surfacing as a `NonDeterminismError`/`NondeterminismError` on resume.
1771
+ *
1772
+ * `Symbol.for` (the GLOBAL registry), NOT a plain `Symbol()` — so a duplicate copy of this module
1773
+ * (pnpm peer-dependency multiplexing, a dual ESM/CJS load, or the SEPARATE `@dudousxd/durable-worker`
1774
+ * package declaring the same `Symbol.for` key locally) still collapses onto the SAME symbol. Same
1775
+ * rationale as `DURABLE_STEP_NAME` in `./step-name-symbol.ts`.
1776
+ *
1777
+ * Deliberately checked as a stamped property, NOT `instanceof` — `instanceof` only recognizes a
1778
+ * signal thrown by the SAME class the checking code imported, which fails across the in-process
1779
+ * engine (`@dudousxd/nestjs-durable-core`'s `WorkflowSuspended`/`ContinueAsNew`) and the thin worker
1780
+ * (`@dudousxd/durable-worker`'s `Suspend`) — two different classes for what is, from a workflow
1781
+ * author's catch block, the same "this isn't a real error, let it through" event.
1782
+ *
1783
+ * NOT designed to survive a serialization boundary (e.g. JSON over the wire) — these signals are
1784
+ * always thrown and caught in-process within a single replay turn, never (de)serialized, so a
1785
+ * stamped-but-not-cloneable property is sufficient.
1786
+ */
1787
+ declare const CONTROL_FLOW_SIGNAL: unique symbol;
1788
+ /**
1789
+ * True for any engine control-flow signal (currently: {@link WorkflowSuspended}, {@link
1790
+ * ContinueAsNew}, and the thin worker's `Suspend`) — regardless of which runtime or module instance
1791
+ * threw it. Workflow `catch` blocks MUST rethrow these untouched; running a failure/cleanup path on
1792
+ * one corrupts the run's history (extra commands recorded during what replay will later see as a
1793
+ * suspend, not a failure) and the resumed replay dies with a non-determinism error.
1794
+ *
1795
+ * Deliberately NOT control-flow (do not add to this predicate):
1796
+ * - `Cancelled` (worker) / a cancelled run — a TERMINAL outcome the consumer may legitimately want
1797
+ * to observe and react to (e.g. release a lock), not a mid-turn unwind to rethrow untouched.
1798
+ * - `StepFailed` (worker) / an awaited step rejection reaching the engine — a REAL failure a
1799
+ * workflow's `catch` is meant to compensate for; misclassifying it as control-flow would swallow
1800
+ * genuine errors instead of protecting them.
1801
+ *
1802
+ * @example
1803
+ * ```ts
1804
+ * try {
1805
+ * await ctx.step('chargeCard', input);
1806
+ * } catch (error) {
1807
+ * if (isWorkflowControlFlowSignal(error)) throw error; // suspend/continue-as-new: rethrow as-is
1808
+ * await ctx.step('refund', input); // a REAL failure — safe to run cleanup here
1809
+ * throw error;
1810
+ * }
1811
+ * ```
1812
+ */
1813
+ declare function isWorkflowControlFlowSignal(error: unknown): boolean;
1814
+
1752
1815
  /**
1753
1816
  * Back-compat alias of {@link WorkflowCtx}. `localStep` — the checkpointed in-process step primitive
1754
1817
  * used for saga compensation — is now part of the public {@link WorkflowCtx}, so this type adds
@@ -2320,8 +2383,32 @@ declare class WorkflowEngine {
2320
2383
  /**
2321
2384
  * Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
2322
2385
  * boot. A run still not due re-suspends cheaply without running new work.
2386
+ *
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.
2323
2395
  */
2324
2396
  resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
2397
+ /**
2398
+ * 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.
2404
+ *
2405
+ * Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
2406
+ * no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
2407
+ * via {@link deliverSignal}, and even a `NamespaceMismatch` there is reported as a real (non-nullish)
2408
+ * result — returning `null` would read as "not settled" to `resumeLeased`'s `??` fallback and
2409
+ * trigger a second, redundant `resume` call on the same run.
2410
+ */
2411
+ private reconcileSignalWaiter;
2325
2412
  /**
2326
2413
  * Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
2327
2414
  * exactly one instance runs it), then resumes the body. Returns the result, or null if another
@@ -2394,6 +2481,37 @@ declare class WorkflowEngine {
2394
2481
  publishEvent(name: string, payload: unknown, opts?: {
2395
2482
  id?: string;
2396
2483
  }): Promise<number>;
2484
+ /**
2485
+ * Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
2486
+ * resume the run. Shared by {@link signal}'s direct hit, its post-buffer re-check reclaim, and the
2487
+ * due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
2488
+ * same way.
2489
+ */
2490
+ private deliverSignal;
2491
+ /**
2492
+ * Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
2493
+ * the run result — or null if no run is (or ends up) waiting for it.
2494
+ *
2495
+ * Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check, same shape in
2496
+ * the remote `waitSignal` command): a waiter can register in the sliver between this method's
2497
+ * initial `takeSignalWaiter` miss and its `bufferSignal` write, and the naive old flow (buffer and
2498
+ * return) would then leave BOTH a buffered payload and a registered waiter sitting in the store with
2499
+ * nothing left to pair them — a lost wake, forever suspended.
2500
+ *
2501
+ * The interleaving proof: the waiter side does check → put → re-check, this side does take → buffer
2502
+ * → re-check. Whichever side's SECOND look runs last is guaranteed to observe the other side's
2503
+ * write, because it always follows that write in real time:
2504
+ * - If the waiter's `put` lands before this method's first `take`, the take just finds it directly
2505
+ * (the pre-existing, non-racy path).
2506
+ * - If the waiter's `put` lands after this method's `bufferSignal`, our re-check below either sees
2507
+ * it (if `put` also lands before the re-check) or the waiter's OWN post-put re-check sees our
2508
+ * buffer (if `put` lands after our re-check but the waiter hasn't re-checked yet either — its
2509
+ * re-check strictly follows its own put).
2510
+ * - If the waiter's `put` lands between this method's `take` and its re-`take`, the re-check below
2511
+ * catches it.
2512
+ * Either way, the destructive `take*` ops (on the waiter row, on the buffer) arbitrate: whichever
2513
+ * side actually removes the row is the one that delivers, so a signal is never delivered twice.
2514
+ */
2397
2515
  signal(token: string, payload: unknown): Promise<RunResult | null>;
2398
2516
  /**
2399
2517
  * Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
@@ -2875,6 +2993,8 @@ declare class GatherError extends Error {
2875
2993
  declare class WorkflowSuspended extends Error {
2876
2994
  /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
2877
2995
  readonly wakeAt?: number | undefined;
2996
+ /** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
2997
+ readonly [CONTROL_FLOW_SIGNAL] = true;
2878
2998
  constructor(wakeAt?: number);
2879
2999
  }
2880
3000
  /**
@@ -2884,6 +3004,8 @@ declare class WorkflowSuspended extends Error {
2884
3004
  */
2885
3005
  declare class ContinueAsNew extends Error {
2886
3006
  readonly input: unknown;
3007
+ /** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
3008
+ readonly [CONTROL_FLOW_SIGNAL] = true;
2887
3009
  constructor(input: unknown);
2888
3010
  }
2889
3011
 
@@ -3098,6 +3220,7 @@ declare class CodecStateStore implements StateStore {
3098
3220
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
3099
3221
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
3100
3222
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
3223
+ removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
3101
3224
  bufferSignal(token: string, payload: unknown): Promise<void>;
3102
3225
  takeBufferedSignal(token: string): Promise<{
3103
3226
  payload: unknown;
@@ -3465,6 +3588,7 @@ declare class InMemoryStateStore implements StateStore {
3465
3588
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
3466
3589
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
3467
3590
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
3591
+ removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
3468
3592
  transaction<T>(work: (tx: {
3469
3593
  raw: unknown;
3470
3594
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
@@ -3512,4 +3636,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
3512
3636
  onControl(handler: (msg: ControlMessage) => void): void;
3513
3637
  }
3514
3638
 
3515
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, 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, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, readSearchAttributes, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
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 };
package/dist/index.d.ts CHANGED
@@ -628,6 +628,16 @@ interface StateStore {
628
628
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
629
629
  /** List waiters whose `token` starts with `prefix` — used to fan out an event to its subscribers. */
630
630
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
631
+ /**
632
+ * Delete the EXACT waiter row — `token` AND `runId` AND `seq` must all match — no-op if absent.
633
+ * Unlike {@link takeSignalWaiter}, which deletes ANY row for `token` (fine when the caller just won
634
+ * the race to consume it), this is for a caller removing its OWN registration after resolving the
635
+ * wait some other way (a buffered hit, a timeout): blind `takeSignalWaiter(token)` there could steal
636
+ * a DIFFERENT run's waiter that has since claimed the same token (`token` is the store's primary key,
637
+ * so a later `putSignalWaiter` for the same token replaces the row). The exact-match variant only
638
+ * ever removes the row this caller itself put there.
639
+ */
640
+ removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
631
641
  /**
632
642
  * Buffer a signal whose waiter hasn't arrived yet, so the next `waitForSignal(token)` consumes it
633
643
  * instead of it being lost (FIFO per token). Makes signals reliable regardless of timing and
@@ -1749,6 +1759,59 @@ declare function runInWorkflowCtx<T>(ctx: WorkflowCtx, fn: () => T): T;
1749
1759
  */
1750
1760
  declare function currentWorkflowCtx(): WorkflowCtx | undefined;
1751
1761
 
1762
+ /**
1763
+ * Cross-package, cross-runtime marker for engine CONTROL-FLOW signals — {@link WorkflowSuspended}
1764
+ * and {@link ContinueAsNew} (this package), plus the thin worker's {@link
1765
+ * import('@dudousxd/durable-worker').Suspend} (`@dudousxd/durable-worker`, the BullMQ
1766
+ * route-by-handler runtime). These are thrown by `WorkflowCtx`/`WorkflowContext` ops to unwind the
1767
+ * current turn — never real failures — so a workflow's `catch` block MUST rethrow them untouched.
1768
+ * Running a failure/cleanup path on a control-flow signal corrupts the run: it emits extra
1769
+ * `recordStep` commands into history that a subsequent replay didn't produce the first time,
1770
+ * surfacing as a `NonDeterminismError`/`NondeterminismError` on resume.
1771
+ *
1772
+ * `Symbol.for` (the GLOBAL registry), NOT a plain `Symbol()` — so a duplicate copy of this module
1773
+ * (pnpm peer-dependency multiplexing, a dual ESM/CJS load, or the SEPARATE `@dudousxd/durable-worker`
1774
+ * package declaring the same `Symbol.for` key locally) still collapses onto the SAME symbol. Same
1775
+ * rationale as `DURABLE_STEP_NAME` in `./step-name-symbol.ts`.
1776
+ *
1777
+ * Deliberately checked as a stamped property, NOT `instanceof` — `instanceof` only recognizes a
1778
+ * signal thrown by the SAME class the checking code imported, which fails across the in-process
1779
+ * engine (`@dudousxd/nestjs-durable-core`'s `WorkflowSuspended`/`ContinueAsNew`) and the thin worker
1780
+ * (`@dudousxd/durable-worker`'s `Suspend`) — two different classes for what is, from a workflow
1781
+ * author's catch block, the same "this isn't a real error, let it through" event.
1782
+ *
1783
+ * NOT designed to survive a serialization boundary (e.g. JSON over the wire) — these signals are
1784
+ * always thrown and caught in-process within a single replay turn, never (de)serialized, so a
1785
+ * stamped-but-not-cloneable property is sufficient.
1786
+ */
1787
+ declare const CONTROL_FLOW_SIGNAL: unique symbol;
1788
+ /**
1789
+ * True for any engine control-flow signal (currently: {@link WorkflowSuspended}, {@link
1790
+ * ContinueAsNew}, and the thin worker's `Suspend`) — regardless of which runtime or module instance
1791
+ * threw it. Workflow `catch` blocks MUST rethrow these untouched; running a failure/cleanup path on
1792
+ * one corrupts the run's history (extra commands recorded during what replay will later see as a
1793
+ * suspend, not a failure) and the resumed replay dies with a non-determinism error.
1794
+ *
1795
+ * Deliberately NOT control-flow (do not add to this predicate):
1796
+ * - `Cancelled` (worker) / a cancelled run — a TERMINAL outcome the consumer may legitimately want
1797
+ * to observe and react to (e.g. release a lock), not a mid-turn unwind to rethrow untouched.
1798
+ * - `StepFailed` (worker) / an awaited step rejection reaching the engine — a REAL failure a
1799
+ * workflow's `catch` is meant to compensate for; misclassifying it as control-flow would swallow
1800
+ * genuine errors instead of protecting them.
1801
+ *
1802
+ * @example
1803
+ * ```ts
1804
+ * try {
1805
+ * await ctx.step('chargeCard', input);
1806
+ * } catch (error) {
1807
+ * if (isWorkflowControlFlowSignal(error)) throw error; // suspend/continue-as-new: rethrow as-is
1808
+ * await ctx.step('refund', input); // a REAL failure — safe to run cleanup here
1809
+ * throw error;
1810
+ * }
1811
+ * ```
1812
+ */
1813
+ declare function isWorkflowControlFlowSignal(error: unknown): boolean;
1814
+
1752
1815
  /**
1753
1816
  * Back-compat alias of {@link WorkflowCtx}. `localStep` — the checkpointed in-process step primitive
1754
1817
  * used for saga compensation — is now part of the public {@link WorkflowCtx}, so this type adds
@@ -2320,8 +2383,32 @@ declare class WorkflowEngine {
2320
2383
  /**
2321
2384
  * Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
2322
2385
  * boot. A run still not due re-suspends cheaply without running new work.
2386
+ *
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.
2323
2395
  */
2324
2396
  resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
2397
+ /**
2398
+ * 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.
2404
+ *
2405
+ * Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
2406
+ * no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
2407
+ * via {@link deliverSignal}, and even a `NamespaceMismatch` there is reported as a real (non-nullish)
2408
+ * result — returning `null` would read as "not settled" to `resumeLeased`'s `??` fallback and
2409
+ * trigger a second, redundant `resume` call on the same run.
2410
+ */
2411
+ private reconcileSignalWaiter;
2325
2412
  /**
2326
2413
  * Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
2327
2414
  * exactly one instance runs it), then resumes the body. Returns the result, or null if another
@@ -2394,6 +2481,37 @@ declare class WorkflowEngine {
2394
2481
  publishEvent(name: string, payload: unknown, opts?: {
2395
2482
  id?: string;
2396
2483
  }): Promise<number>;
2484
+ /**
2485
+ * Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
2486
+ * resume the run. Shared by {@link signal}'s direct hit, its post-buffer re-check reclaim, and the
2487
+ * due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
2488
+ * same way.
2489
+ */
2490
+ private deliverSignal;
2491
+ /**
2492
+ * Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
2493
+ * the run result — or null if no run is (or ends up) waiting for it.
2494
+ *
2495
+ * Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check, same shape in
2496
+ * the remote `waitSignal` command): a waiter can register in the sliver between this method's
2497
+ * initial `takeSignalWaiter` miss and its `bufferSignal` write, and the naive old flow (buffer and
2498
+ * return) would then leave BOTH a buffered payload and a registered waiter sitting in the store with
2499
+ * nothing left to pair them — a lost wake, forever suspended.
2500
+ *
2501
+ * The interleaving proof: the waiter side does check → put → re-check, this side does take → buffer
2502
+ * → re-check. Whichever side's SECOND look runs last is guaranteed to observe the other side's
2503
+ * write, because it always follows that write in real time:
2504
+ * - If the waiter's `put` lands before this method's first `take`, the take just finds it directly
2505
+ * (the pre-existing, non-racy path).
2506
+ * - If the waiter's `put` lands after this method's `bufferSignal`, our re-check below either sees
2507
+ * it (if `put` also lands before the re-check) or the waiter's OWN post-put re-check sees our
2508
+ * buffer (if `put` lands after our re-check but the waiter hasn't re-checked yet either — its
2509
+ * re-check strictly follows its own put).
2510
+ * - If the waiter's `put` lands between this method's `take` and its re-`take`, the re-check below
2511
+ * catches it.
2512
+ * Either way, the destructive `take*` ops (on the waiter row, on the buffer) arbitrate: whichever
2513
+ * side actually removes the row is the one that delivers, so a signal is never delivered twice.
2514
+ */
2397
2515
  signal(token: string, payload: unknown): Promise<RunResult | null>;
2398
2516
  /**
2399
2517
  * Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
@@ -2875,6 +2993,8 @@ declare class GatherError extends Error {
2875
2993
  declare class WorkflowSuspended extends Error {
2876
2994
  /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
2877
2995
  readonly wakeAt?: number | undefined;
2996
+ /** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
2997
+ readonly [CONTROL_FLOW_SIGNAL] = true;
2878
2998
  constructor(wakeAt?: number);
2879
2999
  }
2880
3000
  /**
@@ -2884,6 +3004,8 @@ declare class WorkflowSuspended extends Error {
2884
3004
  */
2885
3005
  declare class ContinueAsNew extends Error {
2886
3006
  readonly input: unknown;
3007
+ /** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
3008
+ readonly [CONTROL_FLOW_SIGNAL] = true;
2887
3009
  constructor(input: unknown);
2888
3010
  }
2889
3011
 
@@ -3098,6 +3220,7 @@ declare class CodecStateStore implements StateStore {
3098
3220
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
3099
3221
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
3100
3222
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
3223
+ removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
3101
3224
  bufferSignal(token: string, payload: unknown): Promise<void>;
3102
3225
  takeBufferedSignal(token: string): Promise<{
3103
3226
  payload: unknown;
@@ -3465,6 +3588,7 @@ declare class InMemoryStateStore implements StateStore {
3465
3588
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
3466
3589
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
3467
3590
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
3591
+ removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
3468
3592
  transaction<T>(work: (tx: {
3469
3593
  raw: unknown;
3470
3594
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
@@ -3512,4 +3636,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
3512
3636
  onControl(handler: (msg: ControlMessage) => void): void;
3513
3637
  }
3514
3638
 
3515
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, 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, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, readSearchAttributes, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
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 };