@dudousxd/nestjs-durable-core 0.72.1 → 0.73.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
@@ -919,6 +919,17 @@ interface StateStore {
919
919
  * module's `autoSchema` is on. Optional: stores that need no setup (in-memory) omit it.
920
920
  */
921
921
  ensureSchema?(): Promise<void>;
922
+ /**
923
+ * Atomically read active runs sharing workflow/tag in this store's read scope, preserve durable
924
+ * admitted holders, and claim a free slot for an eligible FIFO waiter. The claim persists the
925
+ * reserved SINGLETON_ADMITTED_TAG in tags. Execution lease expiry never releases a slot; terminal
926
+ * status does. Optional for general stores, required when using singleton workflows. Implement
927
+ * with a database transaction that serializes competing claims BEFORE reading; list/update alone
928
+ * is unsafe across engine instances. If retryWakeAt is supplied, denial must atomically suspend
929
+ * the still-active candidate with that wakeAt while the same locks are held. Never overwrite a
930
+ * cancelling or terminal run on denial. Return false for missing/terminal/out-of-scope runs.
931
+ */
932
+ tryAdmitSingleton?(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
922
933
  createRun(run: WorkflowRun): Promise<void>;
923
934
  updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
924
935
  getRun(runId: string): Promise<WorkflowRun | null>;
@@ -3441,8 +3452,8 @@ interface StartOptions {
3441
3452
  /**
3442
3453
  * Serialize runs of a workflow that share a key — a durable, FIFO mutex (e.g. one pipeline per base).
3443
3454
  * Excess runs are admitted in creation order, `limit` at a time; the rest wait (suspended) and retry
3444
- * admission on a timer until a slot frees. Race-free on a consistent store (admission order is the
3445
- * same `(createdAt, id)` view for every instance).
3455
+ * admission on a timer until a slot frees. The store atomically reserves durable slots before
3456
+ * ordering unadmitted waiters by `(createdAt, id)`, so later starts cannot displace a holder.
3446
3457
  */
3447
3458
  interface SingletonConfig {
3448
3459
  /** Derive the serialization key from the workflow input. */
@@ -3453,7 +3464,7 @@ interface SingletonConfig {
3453
3464
  * Max GATED (waiting-for-admission) runs allowed to queue behind the `limit` in-flight ones. When
3454
3465
  * set, `start` rejects with {@link SingletonQueueFullError} once in-flight + gated reaches
3455
3466
  * `limit + maxQueueDepth` — back-pressure against an unbounded same-key backlog. Omit for the
3456
- * default unbounded queue. Counts `pending`/`running`/`suspended` runs sharing the key.
3467
+ * default unbounded queue. Counts pending/running/suspended/blocked/cancelling runs sharing the key.
3457
3468
  */
3458
3469
  maxQueueDepth?: number;
3459
3470
  }
@@ -4427,6 +4438,8 @@ declare class WorkflowEngine {
4427
4438
  private completeStep;
4428
4439
  /** Checkpoint a step that failed terminally, so the failure point is visible (not just the run). */
4429
4440
  private failStep;
4441
+ /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
4442
+ private singletonWaitResult;
4430
4443
  private execute;
4431
4444
  /**
4432
4445
  * Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
@@ -5328,6 +5341,8 @@ interface PayloadCodec {
5328
5341
  declare class CodecStateStore implements StateStore {
5329
5342
  private readonly inner;
5330
5343
  private readonly codec;
5344
+ /** Atomic claims touch only searchable metadata and bypass payload encoding. */
5345
+ readonly tryAdmitSingleton?: (runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number) => Promise<boolean>;
5331
5346
  /**
5332
5347
  * Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
5333
5348
  * (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
@@ -5796,8 +5811,9 @@ declare class InMemoryStateStore implements StateStore {
5796
5811
  private key;
5797
5812
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
5798
5813
  private reindexAttributes;
5814
+ tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
5799
5815
  createRun(run: WorkflowRun): Promise<void>;
5800
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
5816
+ updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
5801
5817
  getRun(runId: string): Promise<WorkflowRun | null>;
5802
5818
  deleteRun(runId: string): Promise<void>;
5803
5819
  deleteRuns(runIds: string[]): Promise<void>;
@@ -5891,4 +5907,18 @@ declare class InMemoryTransport implements Transport, ControlPlane {
5891
5907
  onControl(handler: (msg: ControlMessage) => void): void;
5892
5908
  }
5893
5909
 
5894
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AnnouncedWorkflow, type AnnouncementEvidence, type AttributeFilter, type AttributeOp, type AttributePredicateOperand, type AttributeValue, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, CodecStateStore, type ConcurrencyConfig, ConcurrencyLimitError, type ConcurrencyQuota, ContinueAsNew, type ControlMessage, type ControlPlane, DEFAULT_CONCURRENCY_STATUSES, DEFAULT_PARTITION, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, type DynamicWorkflowCtx, ENGINE_MINTED_TAG_PREFIXES, 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 HeartbeatSighting, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NestedWorkflowCallError, NonDeterminismError, type PartitionSighting, type PayloadCodec, type QueueConfig, QueueController, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunFacetQuery, type RunFacetRow, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunScope, type RunStatus, type RunValueAxis, type RunValueFacetOptions, type RunValueFacetOrder, type RunValueFacetRow, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScalarAttributeFilter, ScheduleAlreadyExistsError, ScheduleClient, type ScheduleClientOptions, type ScheduleDescription, type ScheduleHost, type ScheduleListQuery, ScheduleNotFoundError, type ScheduleOptions, type ScheduleQuery, type ScheduleRecord, 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 StepScope, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, VERSION_UNDECLARED_TAG, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, type WorkflowDirectory, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, announcementsOf, applyAttributeQuery, assertConcurrency, attributeColumnFor, attributeOperand, attributePredicateOperands, axisIsRunColumn, bindWorkflowClass, breakpointToken, buildWorkflowDirectory, canRoute, capableWorkers, classifyWaiterToken, clearDurableExecutionWrappers, collectMetrics, concurrencyTag, controlPlaneDescriptor, countRuns, coveredTokens, createStepLogger, currentStep, currentStepScope, currentWorkflowCtx, debug, describeWorker, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, facetOrigin, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEngineMintedTag, isEventToken, isLegacyDescriptor, isNonEmptyRunScope, isWorkflowControlFlowSignal, log, matchesAttributes, mergeRunFacetRows, mergeRunValueFacetRows, missingCapabilities, negotiate, nextCronFireMs, normalizeAttributeRows, normalizeDescriptor, observedAnnouncements, parseDuration, partitionOfToken, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveConcurrency, resolveRouting, resolveRunWaiting, runDurableExecution, runInStepLogger, runInStepScope, runInWorkflowCtx, runOutsideWorkflowCtx, runSchedules, runStepHandler, runValueFacetsFromRuns, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, tenantGroup, useDurableExecution, warn, workflowName };
5910
+ /** Reserved engine metadata: a durable singleton slot, independent of the execution lease. */
5911
+ declare const SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
5912
+ declare const SINGLETON_ACTIVE_STATUSES: RunStatus[];
5913
+ /** Call only inside a store's atomic admission transaction/critical section. */
5914
+ declare function singletonAdmissionTags(runs: WorkflowRun[], runId: string, tag: string, workflow: string, limit: number): string[] | null;
5915
+ /** Terminal runs release admission, including when later resumed directly rather than requeued. */
5916
+ declare function clearTerminalSingletonAdmission(runId: string, patch: Partial<WorkflowRun>, getRun: (id: string) => Promise<WorkflowRun | null>): Promise<Partial<WorkflowRun>>;
5917
+ /** Persist denial while admission locks are held; a cancelling/terminal run must never revive. */
5918
+ declare function singletonAdmissionDenialPatch(runs: WorkflowRun[], runId: string, tag: string, workflow: string, retryWakeAt?: number): {
5919
+ status: 'suspended';
5920
+ wakeAt: number;
5921
+ updatedAt: Date;
5922
+ } | undefined;
5923
+
5924
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AnnouncedWorkflow, type AnnouncementEvidence, type AttributeFilter, type AttributeOp, type AttributePredicateOperand, type AttributeValue, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, CodecStateStore, type ConcurrencyConfig, ConcurrencyLimitError, type ConcurrencyQuota, ContinueAsNew, type ControlMessage, type ControlPlane, DEFAULT_CONCURRENCY_STATUSES, DEFAULT_PARTITION, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, type DynamicWorkflowCtx, ENGINE_MINTED_TAG_PREFIXES, 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 HeartbeatSighting, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NestedWorkflowCallError, NonDeterminismError, type PartitionSighting, type PayloadCodec, type QueueConfig, QueueController, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunFacetQuery, type RunFacetRow, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunScope, type RunStatus, type RunValueAxis, type RunValueFacetOptions, type RunValueFacetOrder, type RunValueFacetRow, type RunWaiting, SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, STATE_STORE, STATE_STORE_CANONICAL, type ScalarAttributeFilter, ScheduleAlreadyExistsError, ScheduleClient, type ScheduleClientOptions, type ScheduleDescription, type ScheduleHost, type ScheduleListQuery, ScheduleNotFoundError, type ScheduleOptions, type ScheduleQuery, type ScheduleRecord, 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 StepScope, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, VERSION_UNDECLARED_TAG, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, type WorkflowDirectory, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, announcementsOf, applyAttributeQuery, assertConcurrency, attributeColumnFor, attributeOperand, attributePredicateOperands, axisIsRunColumn, bindWorkflowClass, breakpointToken, buildWorkflowDirectory, canRoute, capableWorkers, classifyWaiterToken, clearDurableExecutionWrappers, clearTerminalSingletonAdmission, collectMetrics, concurrencyTag, controlPlaneDescriptor, countRuns, coveredTokens, createStepLogger, currentStep, currentStepScope, currentWorkflowCtx, debug, describeWorker, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, facetOrigin, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEngineMintedTag, isEventToken, isLegacyDescriptor, isNonEmptyRunScope, isWorkflowControlFlowSignal, log, matchesAttributes, mergeRunFacetRows, mergeRunValueFacetRows, missingCapabilities, negotiate, nextCronFireMs, normalizeAttributeRows, normalizeDescriptor, observedAnnouncements, parseDuration, partitionOfToken, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveConcurrency, resolveRouting, resolveRunWaiting, runDurableExecution, runInStepLogger, runInStepScope, runInWorkflowCtx, runOutsideWorkflowCtx, runSchedules, runStepHandler, runValueFacetsFromRuns, sanitizeQueueToken, scheduledRunId, singletonAdmissionDenialPatch, singletonAdmissionTags, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, tenantGroup, useDurableExecution, warn, workflowName };
package/dist/index.d.ts CHANGED
@@ -919,6 +919,17 @@ interface StateStore {
919
919
  * module's `autoSchema` is on. Optional: stores that need no setup (in-memory) omit it.
920
920
  */
921
921
  ensureSchema?(): Promise<void>;
922
+ /**
923
+ * Atomically read active runs sharing workflow/tag in this store's read scope, preserve durable
924
+ * admitted holders, and claim a free slot for an eligible FIFO waiter. The claim persists the
925
+ * reserved SINGLETON_ADMITTED_TAG in tags. Execution lease expiry never releases a slot; terminal
926
+ * status does. Optional for general stores, required when using singleton workflows. Implement
927
+ * with a database transaction that serializes competing claims BEFORE reading; list/update alone
928
+ * is unsafe across engine instances. If retryWakeAt is supplied, denial must atomically suspend
929
+ * the still-active candidate with that wakeAt while the same locks are held. Never overwrite a
930
+ * cancelling or terminal run on denial. Return false for missing/terminal/out-of-scope runs.
931
+ */
932
+ tryAdmitSingleton?(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
922
933
  createRun(run: WorkflowRun): Promise<void>;
923
934
  updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
924
935
  getRun(runId: string): Promise<WorkflowRun | null>;
@@ -3441,8 +3452,8 @@ interface StartOptions {
3441
3452
  /**
3442
3453
  * Serialize runs of a workflow that share a key — a durable, FIFO mutex (e.g. one pipeline per base).
3443
3454
  * Excess runs are admitted in creation order, `limit` at a time; the rest wait (suspended) and retry
3444
- * admission on a timer until a slot frees. Race-free on a consistent store (admission order is the
3445
- * same `(createdAt, id)` view for every instance).
3455
+ * admission on a timer until a slot frees. The store atomically reserves durable slots before
3456
+ * ordering unadmitted waiters by `(createdAt, id)`, so later starts cannot displace a holder.
3446
3457
  */
3447
3458
  interface SingletonConfig {
3448
3459
  /** Derive the serialization key from the workflow input. */
@@ -3453,7 +3464,7 @@ interface SingletonConfig {
3453
3464
  * Max GATED (waiting-for-admission) runs allowed to queue behind the `limit` in-flight ones. When
3454
3465
  * set, `start` rejects with {@link SingletonQueueFullError} once in-flight + gated reaches
3455
3466
  * `limit + maxQueueDepth` — back-pressure against an unbounded same-key backlog. Omit for the
3456
- * default unbounded queue. Counts `pending`/`running`/`suspended` runs sharing the key.
3467
+ * default unbounded queue. Counts pending/running/suspended/blocked/cancelling runs sharing the key.
3457
3468
  */
3458
3469
  maxQueueDepth?: number;
3459
3470
  }
@@ -4427,6 +4438,8 @@ declare class WorkflowEngine {
4427
4438
  private completeStep;
4428
4439
  /** Checkpoint a step that failed terminally, so the failure point is visible (not just the run). */
4429
4440
  private failStep;
4441
+ /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
4442
+ private singletonWaitResult;
4430
4443
  private execute;
4431
4444
  /**
4432
4445
  * Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
@@ -5328,6 +5341,8 @@ interface PayloadCodec {
5328
5341
  declare class CodecStateStore implements StateStore {
5329
5342
  private readonly inner;
5330
5343
  private readonly codec;
5344
+ /** Atomic claims touch only searchable metadata and bypass payload encoding. */
5345
+ readonly tryAdmitSingleton?: (runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number) => Promise<boolean>;
5331
5346
  /**
5332
5347
  * Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
5333
5348
  * (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
@@ -5796,8 +5811,9 @@ declare class InMemoryStateStore implements StateStore {
5796
5811
  private key;
5797
5812
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
5798
5813
  private reindexAttributes;
5814
+ tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
5799
5815
  createRun(run: WorkflowRun): Promise<void>;
5800
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
5816
+ updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
5801
5817
  getRun(runId: string): Promise<WorkflowRun | null>;
5802
5818
  deleteRun(runId: string): Promise<void>;
5803
5819
  deleteRuns(runIds: string[]): Promise<void>;
@@ -5891,4 +5907,18 @@ declare class InMemoryTransport implements Transport, ControlPlane {
5891
5907
  onControl(handler: (msg: ControlMessage) => void): void;
5892
5908
  }
5893
5909
 
5894
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AnnouncedWorkflow, type AnnouncementEvidence, type AttributeFilter, type AttributeOp, type AttributePredicateOperand, type AttributeValue, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, CodecStateStore, type ConcurrencyConfig, ConcurrencyLimitError, type ConcurrencyQuota, ContinueAsNew, type ControlMessage, type ControlPlane, DEFAULT_CONCURRENCY_STATUSES, DEFAULT_PARTITION, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, type DynamicWorkflowCtx, ENGINE_MINTED_TAG_PREFIXES, 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 HeartbeatSighting, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NestedWorkflowCallError, NonDeterminismError, type PartitionSighting, type PayloadCodec, type QueueConfig, QueueController, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunFacetQuery, type RunFacetRow, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunScope, type RunStatus, type RunValueAxis, type RunValueFacetOptions, type RunValueFacetOrder, type RunValueFacetRow, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScalarAttributeFilter, ScheduleAlreadyExistsError, ScheduleClient, type ScheduleClientOptions, type ScheduleDescription, type ScheduleHost, type ScheduleListQuery, ScheduleNotFoundError, type ScheduleOptions, type ScheduleQuery, type ScheduleRecord, 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 StepScope, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, VERSION_UNDECLARED_TAG, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, type WorkflowDirectory, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, announcementsOf, applyAttributeQuery, assertConcurrency, attributeColumnFor, attributeOperand, attributePredicateOperands, axisIsRunColumn, bindWorkflowClass, breakpointToken, buildWorkflowDirectory, canRoute, capableWorkers, classifyWaiterToken, clearDurableExecutionWrappers, collectMetrics, concurrencyTag, controlPlaneDescriptor, countRuns, coveredTokens, createStepLogger, currentStep, currentStepScope, currentWorkflowCtx, debug, describeWorker, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, facetOrigin, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEngineMintedTag, isEventToken, isLegacyDescriptor, isNonEmptyRunScope, isWorkflowControlFlowSignal, log, matchesAttributes, mergeRunFacetRows, mergeRunValueFacetRows, missingCapabilities, negotiate, nextCronFireMs, normalizeAttributeRows, normalizeDescriptor, observedAnnouncements, parseDuration, partitionOfToken, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveConcurrency, resolveRouting, resolveRunWaiting, runDurableExecution, runInStepLogger, runInStepScope, runInWorkflowCtx, runOutsideWorkflowCtx, runSchedules, runStepHandler, runValueFacetsFromRuns, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, tenantGroup, useDurableExecution, warn, workflowName };
5910
+ /** Reserved engine metadata: a durable singleton slot, independent of the execution lease. */
5911
+ declare const SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
5912
+ declare const SINGLETON_ACTIVE_STATUSES: RunStatus[];
5913
+ /** Call only inside a store's atomic admission transaction/critical section. */
5914
+ declare function singletonAdmissionTags(runs: WorkflowRun[], runId: string, tag: string, workflow: string, limit: number): string[] | null;
5915
+ /** Terminal runs release admission, including when later resumed directly rather than requeued. */
5916
+ declare function clearTerminalSingletonAdmission(runId: string, patch: Partial<WorkflowRun>, getRun: (id: string) => Promise<WorkflowRun | null>): Promise<Partial<WorkflowRun>>;
5917
+ /** Persist denial while admission locks are held; a cancelling/terminal run must never revive. */
5918
+ declare function singletonAdmissionDenialPatch(runs: WorkflowRun[], runId: string, tag: string, workflow: string, retryWakeAt?: number): {
5919
+ status: 'suspended';
5920
+ wakeAt: number;
5921
+ updatedAt: Date;
5922
+ } | undefined;
5923
+
5924
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AnnouncedWorkflow, type AnnouncementEvidence, type AttributeFilter, type AttributeOp, type AttributePredicateOperand, type AttributeValue, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, CodecStateStore, type ConcurrencyConfig, ConcurrencyLimitError, type ConcurrencyQuota, ContinueAsNew, type ControlMessage, type ControlPlane, DEFAULT_CONCURRENCY_STATUSES, DEFAULT_PARTITION, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, type DynamicWorkflowCtx, ENGINE_MINTED_TAG_PREFIXES, 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 HeartbeatSighting, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NestedWorkflowCallError, NonDeterminismError, type PartitionSighting, type PayloadCodec, type QueueConfig, QueueController, RUN_VALUE_FACET_LIMIT, RUN_VALUE_FACET_SCAN, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunFacetQuery, type RunFacetRow, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunScope, type RunStatus, type RunValueAxis, type RunValueFacetOptions, type RunValueFacetOrder, type RunValueFacetRow, type RunWaiting, SINGLETON_ACTIVE_STATUSES, SINGLETON_ADMITTED_TAG, STATE_STORE, STATE_STORE_CANONICAL, type ScalarAttributeFilter, ScheduleAlreadyExistsError, ScheduleClient, type ScheduleClientOptions, type ScheduleDescription, type ScheduleHost, type ScheduleListQuery, ScheduleNotFoundError, type ScheduleOptions, type ScheduleQuery, type ScheduleRecord, 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 StepScope, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, VERSION_UNDECLARED_TAG, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, type WorkflowDirectory, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, announcementsOf, applyAttributeQuery, assertConcurrency, attributeColumnFor, attributeOperand, attributePredicateOperands, axisIsRunColumn, bindWorkflowClass, breakpointToken, buildWorkflowDirectory, canRoute, capableWorkers, classifyWaiterToken, clearDurableExecutionWrappers, clearTerminalSingletonAdmission, collectMetrics, concurrencyTag, controlPlaneDescriptor, countRuns, coveredTokens, createStepLogger, currentStep, currentStepScope, currentWorkflowCtx, debug, describeWorker, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, facetOrigin, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEngineMintedTag, isEventToken, isLegacyDescriptor, isNonEmptyRunScope, isWorkflowControlFlowSignal, log, matchesAttributes, mergeRunFacetRows, mergeRunValueFacetRows, missingCapabilities, negotiate, nextCronFireMs, normalizeAttributeRows, normalizeDescriptor, observedAnnouncements, parseDuration, partitionOfToken, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveConcurrency, resolveRouting, resolveRunWaiting, runDurableExecution, runInStepLogger, runInStepScope, runInWorkflowCtx, runOutsideWorkflowCtx, runSchedules, runStepHandler, runValueFacetsFromRuns, sanitizeQueueToken, scheduledRunId, singletonAdmissionDenialPatch, singletonAdmissionTags, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, tenantGroup, useDurableExecution, warn, workflowName };
package/dist/index.js CHANGED
@@ -2288,6 +2288,54 @@ function errorMessage(error2) {
2288
2288
  }
2289
2289
  __name(errorMessage, "errorMessage");
2290
2290
 
2291
+ // src/singleton-admission.ts
2292
+ var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
2293
+ var SINGLETON_ACTIVE_STATUSES = [
2294
+ "pending",
2295
+ "running",
2296
+ "suspended",
2297
+ "blocked",
2298
+ "cancelling"
2299
+ ];
2300
+ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2301
+ if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
2302
+ const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
2303
+ const candidate = active.find((run) => run.id === runId);
2304
+ if (!candidate) return null;
2305
+ if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
2306
+ if (candidate.status === "cancelling") return null;
2307
+ const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
2308
+ const remaining = limit - holders.length;
2309
+ if (remaining <= 0) return null;
2310
+ const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
2311
+ if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
2312
+ return [
2313
+ ...candidate.tags ?? [],
2314
+ SINGLETON_ADMITTED_TAG
2315
+ ];
2316
+ }
2317
+ __name(singletonAdmissionTags, "singletonAdmissionTags");
2318
+ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
2319
+ if (!patch.status || !TERMINAL_RUN_STATUSES.includes(patch.status)) return patch;
2320
+ const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
2321
+ return {
2322
+ ...patch,
2323
+ tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
2324
+ };
2325
+ }
2326
+ __name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
2327
+ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt) {
2328
+ if (retryWakeAt === void 0) return void 0;
2329
+ const candidate = runs.find((run) => run.id === runId && run.workflow === workflow && run.tags?.includes(tag) && SINGLETON_ACTIVE_STATUSES.includes(run.status));
2330
+ if (!candidate || candidate.status === "cancelling") return void 0;
2331
+ return {
2332
+ status: "suspended",
2333
+ wakeAt: retryWakeAt,
2334
+ updatedAt: /* @__PURE__ */ new Date()
2335
+ };
2336
+ }
2337
+ __name(singletonAdmissionDenialPatch, "singletonAdmissionDenialPatch");
2338
+
2291
2339
  // src/singleton-gate.ts
2292
2340
  var SINGLETON_RETRY_MS = 1e3;
2293
2341
  var SINGLETON_RETRY_JITTER_MS = 250;
@@ -2301,6 +2349,9 @@ var SingletonGate = class {
2301
2349
  }
2302
2350
  /** The tag a singleton run carries, so the gate can find others sharing its key. */
2303
2351
  tag(cfg, input) {
2352
+ if (!this.deps.store.tryAdmitSingleton) {
2353
+ throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
2354
+ }
2304
2355
  return `singleton:${cfg.key(input)}`;
2305
2356
  }
2306
2357
  /** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
@@ -2309,7 +2360,7 @@ var SingletonGate = class {
2309
2360
  }
2310
2361
  /**
2311
2362
  * Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
2312
- * `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
2363
+ * `pending`/`running`/`suspended`/`blocked`/`cancelling` runs sharing the key in one scan). No-op when no `maxQueueDepth`
2313
2364
  * is configured.
2314
2365
  */
2315
2366
  async assertCapacity(workflow, cfg, input) {
@@ -2318,34 +2369,25 @@ var SingletonGate = class {
2318
2369
  const queued = await this.deps.store.listRuns({
2319
2370
  tag: this.tag(cfg, input),
2320
2371
  workflow,
2321
- statuses: [
2322
- "pending",
2323
- "running",
2324
- "suspended",
2325
- "cancelling"
2326
- ]
2372
+ statuses: SINGLETON_ACTIVE_STATUSES
2327
2373
  });
2328
2374
  if (queued.length >= cap) {
2329
2375
  throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
2330
2376
  }
2331
2377
  }
2332
- /**
2333
- * Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
2334
- * suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
2335
- * instance the same ordering, so admission is race-free + FIFO.
2336
- */
2378
+ /** Preserve durable holders and atomically claim one of the remaining slots. */
2337
2379
  async admit(run, cfg) {
2338
- const inflight = (await this.deps.store.listRuns({
2339
- tag: this.tag(cfg, run.input),
2340
- workflow: run.workflow,
2341
- statuses: [
2342
- "running",
2343
- "suspended",
2344
- "cancelling"
2345
- ]
2346
- })).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
2347
- const idx = inflight.findIndex((r) => r.id === run.id);
2348
- return idx >= 0 && idx < (cfg.limit ?? 1);
2380
+ if (!this.deps.store.tryAdmitSingleton) {
2381
+ throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
2382
+ }
2383
+ const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
2384
+ if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
2385
+ run.tags = [
2386
+ ...run.tags ?? [],
2387
+ SINGLETON_ADMITTED_TAG
2388
+ ];
2389
+ }
2390
+ return admitted;
2349
2391
  }
2350
2392
  /**
2351
2393
  * Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
@@ -3862,7 +3904,7 @@ var WorkflowEngine = class {
3862
3904
  VERSION_UNDECLARED_TAG
3863
3905
  ] : []
3864
3906
  ];
3865
- const tags = mergeTags(registered.tags, stampedTags);
3907
+ const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
3866
3908
  if (registered.singleton) {
3867
3909
  await this.singletons.assertCapacity(name, registered.singleton, input);
3868
3910
  }
@@ -3937,6 +3979,12 @@ var WorkflowEngine = class {
3937
3979
  await this.store.releaseRunLock(runId);
3938
3980
  throw new NamespaceMismatch();
3939
3981
  }
3982
+ if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
3983
+ run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
3984
+ await this.store.updateRun(run.id, {
3985
+ tags: run.tags
3986
+ });
3987
+ }
3940
3988
  const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
3941
3989
  if (!registered) {
3942
3990
  throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
@@ -4086,7 +4134,10 @@ var WorkflowEngine = class {
4086
4134
  /** Track an in-flight execution so {@link drain} can wait for it. */
4087
4135
  track(p) {
4088
4136
  this.inflight.add(p);
4089
- void p.finally(() => this.inflight.delete(p));
4137
+ const done = /* @__PURE__ */ __name(() => {
4138
+ this.inflight.delete(p);
4139
+ }, "done");
4140
+ void p.then(done, done);
4090
4141
  return p;
4091
4142
  }
4092
4143
  /**
@@ -4437,7 +4488,8 @@ var WorkflowEngine = class {
4437
4488
  status: "pending",
4438
4489
  error: void 0,
4439
4490
  ...resurrecting ? {
4440
- recoveryAttempts: 0
4491
+ recoveryAttempts: 0,
4492
+ tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
4441
4493
  } : {},
4442
4494
  updatedAt: /* @__PURE__ */ new Date()
4443
4495
  });
@@ -4556,10 +4608,19 @@ var WorkflowEngine = class {
4556
4608
  off();
4557
4609
  resolve(toResult(run));
4558
4610
  }, "finish");
4611
+ const fail = /* @__PURE__ */ __name((error2) => {
4612
+ if (done) return;
4613
+ done = true;
4614
+ if (timer) clearTimeout(timer);
4615
+ if (poller) clearInterval(poller);
4616
+ off();
4617
+ reject(error2);
4618
+ }, "fail");
4559
4619
  const check = /* @__PURE__ */ __name(() => {
4620
+ if (done) return;
4560
4621
  void this.store.getRun(runId).then((run) => {
4561
4622
  if (run && isSettled(run)) finish(run);
4562
- });
4623
+ }, fail);
4563
4624
  }, "check");
4564
4625
  const poller = opts?.until === "terminal" ? (() => {
4565
4626
  const interval = setInterval(check, 250);
@@ -5342,6 +5403,26 @@ var WorkflowEngine = class {
5342
5403
  durationMs: Date.now() - step.startedAt.getTime()
5343
5404
  });
5344
5405
  }
5406
+ /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
5407
+ async singletonWaitResult(run) {
5408
+ const current = await this.store.getRun(run.id);
5409
+ await this.store.releaseRunLock(run.id);
5410
+ if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
5411
+ if (current.status === "suspended") {
5412
+ this.emit({
5413
+ type: "run.suspended",
5414
+ runId: current.id,
5415
+ workflow: current.workflow,
5416
+ namespace: current.namespace
5417
+ });
5418
+ }
5419
+ return {
5420
+ runId: current.id,
5421
+ status: current.status,
5422
+ output: current.output,
5423
+ error: current.error
5424
+ };
5425
+ }
5345
5426
  async execute(run, fn) {
5346
5427
  const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
5347
5428
  const lockNow = this.clock();
@@ -5511,23 +5592,7 @@ var WorkflowEngine = class {
5511
5592
  });
5512
5593
  }
5513
5594
  if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
5514
- const wakeAt = this.singletons.retryWakeAt();
5515
- await this.store.updateRun(run.id, {
5516
- status: "suspended",
5517
- wakeAt,
5518
- updatedAt: /* @__PURE__ */ new Date()
5519
- });
5520
- this.emit({
5521
- type: "run.suspended",
5522
- runId: run.id,
5523
- workflow: run.workflow,
5524
- namespace: run.namespace
5525
- });
5526
- await this.store.releaseRunLock(run.id);
5527
- return {
5528
- runId: run.id,
5529
- status: "suspended"
5530
- };
5595
+ return this.singletonWaitResult(run);
5531
5596
  }
5532
5597
  const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
5533
5598
  if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
@@ -6141,23 +6206,7 @@ var WorkflowEngine = class {
6141
6206
  });
6142
6207
  }
6143
6208
  if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
6144
- const wakeAt = this.singletons.retryWakeAt();
6145
- await this.store.updateRun(run.id, {
6146
- status: "suspended",
6147
- wakeAt,
6148
- updatedAt: /* @__PURE__ */ new Date()
6149
- });
6150
- this.emit({
6151
- type: "run.suspended",
6152
- runId: run.id,
6153
- workflow: run.workflow,
6154
- namespace: run.namespace
6155
- });
6156
- await this.store.releaseRunLock(run.id);
6157
- return {
6158
- runId: run.id,
6159
- status: "suspended"
6160
- };
6209
+ return this.singletonWaitResult(run);
6161
6210
  }
6162
6211
  const compensations = [];
6163
6212
  const snapshot = await this.store.listCheckpoints(run.id);
@@ -7176,6 +7225,8 @@ var CodecStateStore = class {
7176
7225
  }
7177
7226
  inner;
7178
7227
  codec;
7228
+ /** Atomic claims touch only searchable metadata and bypass payload encoding. */
7229
+ tryAdmitSingleton;
7179
7230
  /**
7180
7231
  * Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
7181
7232
  * (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
@@ -7208,6 +7259,7 @@ var CodecStateStore = class {
7208
7259
  constructor(inner, codec) {
7209
7260
  this.inner = inner;
7210
7261
  this.codec = codec;
7262
+ if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
7211
7263
  const deleteRuns = inner.deleteRuns;
7212
7264
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7213
7265
  const prune = inner.pruneTerminalRuns;
@@ -7660,6 +7712,28 @@ var InMemoryStateStore = class {
7660
7712
  byRun.set(runId, row);
7661
7713
  }
7662
7714
  }
7715
+ async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
7716
+ const runs = [
7717
+ ...this.runs.values()
7718
+ ];
7719
+ const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
7720
+ if (!tags) {
7721
+ const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
7722
+ const candidate = this.runs.get(runId);
7723
+ if (patch && candidate) this.runs.set(runId, {
7724
+ ...candidate,
7725
+ ...patch
7726
+ });
7727
+ return false;
7728
+ }
7729
+ const run = this.runs.get(runId);
7730
+ if (!run) return false;
7731
+ this.runs.set(runId, {
7732
+ ...run,
7733
+ tags
7734
+ });
7735
+ return true;
7736
+ }
7663
7737
  async createRun(run) {
7664
7738
  this.runs.set(run.id, {
7665
7739
  ...run,
@@ -7667,7 +7741,11 @@ var InMemoryStateStore = class {
7667
7741
  });
7668
7742
  this.reindexAttributes(run.id, run.searchAttributes);
7669
7743
  }
7670
- async updateRun(runId, patch) {
7744
+ async updateRun(runId, inputPatch) {
7745
+ const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
7746
+ ...inputPatch,
7747
+ tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
7748
+ } : inputPatch;
7671
7749
  const existing = this.runs.get(runId);
7672
7750
  if (!existing) throw new Error(`run ${runId} not found`);
7673
7751
  const next = {
@@ -8091,6 +8169,8 @@ export {
8091
8169
  RemoteWorkflowExecutor,
8092
8170
  RemoteWorkflowTimeout,
8093
8171
  RunGateway,
8172
+ SINGLETON_ACTIVE_STATUSES,
8173
+ SINGLETON_ADMITTED_TAG,
8094
8174
  STATE_STORE,
8095
8175
  STATE_STORE_CANONICAL,
8096
8176
  ScheduleAlreadyExistsError,
@@ -8121,6 +8201,7 @@ export {
8121
8201
  capableWorkers,
8122
8202
  classifyWaiterToken,
8123
8203
  clearDurableExecutionWrappers,
8204
+ clearTerminalSingletonAdmission,
8124
8205
  collectMetrics,
8125
8206
  concurrencyTag,
8126
8207
  controlPlaneDescriptor,
@@ -8178,6 +8259,8 @@ export {
8178
8259
  runValueFacetsFromRuns,
8179
8260
  sanitizeQueueToken,
8180
8261
  scheduledRunId,
8262
+ singletonAdmissionDenialPatch,
8263
+ singletonAdmissionTags,
8181
8264
  sqlComparator,
8182
8265
  stepConfigOf,
8183
8266
  stepId,