UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

2,902 lines 82 kB
/**
 * @beignet/core/outbox
 *
 * Durable outbox primitives for transactionally recording events and jobs that
 * should be delivered after the owning database transaction commits.
 */

import {
  type EventPayloadDef,
  type EventPublishOptions,
  type EventTransportValue,
  type InferEventPayload,
  prepareEventPayloadForTransport,
} from "../events/index.js";
import {
  getJobRetryDelayMs,
  getJobRetryMaxAttempts,
  type InferJobPayload,
  type JobDef,
  parseJobPayload,
  SINGLE_ATTEMPT_DISPATCH,
  type SingleAttemptJobDispatch,
  shouldRetryJob,
} from "../jobs/index.js";
import type { JobDispatcherPort } from "../ports/events.js";
import type { DomainEventRecorderPort } from "../ports/unit-of-work.js";
import {
  type BaseProviderInstrumentationEvent,
  createProviderInstrumentation,
  type ProviderInstrumentationTarget,
} from "../providers/index.js";
import {
  captureTraceCarrier,
  parseTraceCarrier,
  resolveTracingPort,
  runWithTracing,
  type TraceCarrier,
  type TracingPort,
} from "../tracing/index.js";

/**
 * Value or promise of that value.
 */
export type MaybePromise<T> = T | Promise<T>;

/**
 * Default lease duration for claimed outbox messages.
 */
export const DEFAULT_OUTBOX_LEASE_MS = 30_000;
/**
 * Default maximum messages handled by one bounded drain pass.
 */
export const DEFAULT_OUTBOX_BATCH_SIZE = 100;
/**
 * Default number of outbox messages delivered concurrently.
 */
export const DEFAULT_OUTBOX_CONCURRENCY = 1;
/**
 * Default maximum time Beignet renews a claim for one delivery.
 */
export const DEFAULT_OUTBOX_MAX_ACTIVE_MS = 300_000;
/**
 * Default maximum delivery attempts before a message is dead-lettered.
 */
export const DEFAULT_OUTBOX_MAX_ATTEMPTS = 3;

/**
 * Message kinds supported by the Beignet outbox.
 */
export type OutboxMessageKind = "event" | "job";

/**
 * Delivery status for an outbox message.
 */
export type OutboxMessageStatus =
  | "pending"
  | "claimed"
  | "delivered"
  | "deadLettered";

/**
 * JSON-serializable value accepted by the outbox.
 */
export type OutboxJsonValue =
  | null
  | string
  | number
  | boolean
  | readonly OutboxJsonValue[]
  | { readonly [key: string]: OutboxJsonValue };

/**
 * JSON object accepted by outbox payload helpers.
 */
export type OutboxJsonObject = { readonly [key: string]: OutboxJsonValue };

/**
 * Serialized delivery error stored on failed messages.
 */
export interface OutboxErrorInfo {
  /**
   * Error name when available.
   */
  name?: string;
  /**
   * Error message.
   */
  message: string;
  /**
   * Error stack when available.
   */
  stack?: string;
}

/**
 * Input for enqueueing a raw outbox message.
 */
export interface OutboxEnqueueInput {
  /**
   * Optional caller-provided message ID.
   */
  id?: string;
  /**
   * Message kind.
   */
  kind: OutboxMessageKind;
  /**
   * Event or job name.
   */
  name: string;
  /**
   * JSON-serializable payload.
   */
  payload: OutboxJsonValue;
  /** Versioned trace context captured when the message was recorded. */
  trace?: TraceCarrier;
  /**
   * Earliest time the message may be claimed.
   */
  availableAt?: Date;
  /**
   * Maximum delivery attempts before dead-lettering.
   */
  maxAttempts?: number;
}

/**
 * Durable outbox message record.
 */
export interface OutboxMessage {
  /**
   * Stable message ID.
   */
  id: string;
  /**
   * Message kind.
   */
  kind: OutboxMessageKind;
  /**
   * Event or job name.
   */
  name: string;
  /**
   * JSON-serializable payload.
   */
  payload: OutboxJsonValue;
  /** Versioned trace context captured when the message was recorded. */
  trace?: TraceCarrier;
  /**
   * Current delivery status.
   */
  status: OutboxMessageStatus;
  /**
   * Number of claim attempts.
   */
  attempts: number;
  /**
   * Maximum delivery attempts before dead-lettering.
   */
  maxAttempts: number;
  /**
   * Earliest time the message may be claimed.
   */
  availableAt: Date;
  /**
   * Last claim timestamp.
   */
  claimedAt: Date | null;
  /**
   * Lease expiration timestamp for claimed messages.
   */
  lockedUntil: Date | null;
  /**
   * Token required to mark a claimed message delivered or failed.
   */
  claimToken: string | null;
  /**
   * Delivery timestamp.
   */
  deliveredAt: Date | null;
  /**
   * Last delivery error.
   */
  lastError: OutboxErrorInfo | null;
  /**
   * Creation timestamp.
   */
  createdAt: Date;
  /**
   * Last update timestamp.
   */
  updatedAt: Date;
}

/**
 * Message returned from a successful outbox claim.
 */
export interface ClaimedOutboxMessage
  extends Omit<
    OutboxMessage,
    "claimToken" | "claimedAt" | "lockedUntil" | "status"
  > {
  status: "claimed";
  claimToken: string;
  claimedAt: Date;
  lockedUntil: Date;
}

/**
 * Options for leasing a batch of pending outbox messages for delivery.
 */
export interface OutboxClaimBatchOptions {
  /**
   * Maximum eligible messages to claim or reconcile in one batch.
   */
  limit: number;
  /**
   * Claim timestamp.
   */
  now?: Date;
  /**
   * Lease duration in milliseconds.
   */
  leaseMs?: number;
}

/**
 * Result of atomically selecting one bounded set of eligible messages.
 */
export interface OutboxClaimBatchResult {
  /** Messages claimed for delivery by the current worker. */
  claimed: readonly ClaimedOutboxMessage[];
  /**
   * Eligible messages moved directly to dead letter because their claim
   * attempt budget was already exhausted.
   */
  deadLettered: readonly OutboxMessage[];
}

/**
 * Input for extending one active outbox claim.
 */
export interface OutboxRenewClaimInput {
  /** Claimed message ID. */
  id: string;
  /** Claim token returned by `claimBatch(...)`. */
  claimToken: string;
  /** Renewal timestamp. */
  now?: Date;
  /** New lease duration measured from `now`. */
  leaseMs?: number;
}

/**
 * Confirmed result of extending one active outbox claim.
 */
export interface OutboxRenewClaimResult {
  /** New confirmed lease expiration timestamp. */
  lockedUntil: Date;
}

/**
 * Input for marking a claimed message delivered.
 */
export interface OutboxMarkDeliveredInput {
  /**
   * Claimed message ID.
   */
  id: string;
  /**
   * Claim token returned by `claimBatch(...)`.
   */
  claimToken: string;
  /**
   * Delivery timestamp.
   */
  now?: Date;
}

/**
 * Input for marking a claimed message failed.
 */
export interface OutboxMarkFailedInput {
  /**
   * Claimed message ID.
   */
  id: string;
  /**
   * Claim token returned by `claimBatch(...)`.
   */
  claimToken: string;
  /**
   * Delivery error.
   */
  error?: unknown;
  /**
   * Next time the message may be claimed.
   */
  retryAt?: Date;
  /**
   * Whether this failure should dead-letter the message.
   */
  deadLetter?: boolean;
  /**
   * Failure timestamp.
   */
  now?: Date;
}

/**
 * Default maximum messages returned from outbox admin list calls.
 */
export const DEFAULT_OUTBOX_ADMIN_LIST_LIMIT = 50;

/**
 * Shared filters for outbox admin read operations.
 */
export interface OutboxMessageQuery {
  /**
   * Status or statuses to include.
   */
  status?: OutboxMessageStatus | readonly OutboxMessageStatus[];
  /**
   * Message kind to include.
   */
  kind?: OutboxMessageKind;
  /**
   * Event or job name to include.
   */
  name?: string;
  /**
   * Include messages last updated before this timestamp.
   */
  updatedBefore?: Date;
  /**
   * Include delivered messages delivered before this timestamp.
   */
  deliveredBefore?: Date;
}

/**
 * Options for listing outbox messages through the admin port.
 */
export interface OutboxListMessagesOptions extends OutboxMessageQuery {
  /**
   * Maximum messages to return. Defaults to
   * `DEFAULT_OUTBOX_ADMIN_LIST_LIMIT`.
   */
  limit?: number;
}

/**
 * Options for counting outbox messages through the admin port.
 */
export interface OutboxCountMessagesOptions extends OutboxMessageQuery {}

/**
 * Input for returning a dead-lettered message to the pending queue.
 */
export interface OutboxRequeueMessageInput {
  /**
   * Dead-lettered message ID.
   */
  id: string;
  /**
   * Earliest time the message may be claimed again. Defaults to `now`.
   */
  availableAt?: Date;
  /**
   * Reset attempts to zero before requeueing. Defaults to preserving the
   * attempt count so operators can decide whether to grant a fresh retry
   * budget.
   */
  resetAttempts?: boolean;
  /**
   * Requeue timestamp.
   */
  now?: Date;
}

/**
 * Input for purging dead-lettered messages.
 */
export interface OutboxPurgeDeadLetteredInput {
  /**
   * Only purge messages last updated before this timestamp. Omit only when the
   * caller intentionally wants to purge all dead-lettered messages.
   */
  before?: Date;
  /**
   * Maximum messages to purge.
   */
  limit?: number;
}

/**
 * Input for pruning delivered messages.
 */
export interface OutboxPruneDeliveredInput {
  /**
   * Prune delivered messages delivered before this timestamp.
   */
  before: Date;
  /**
   * Maximum messages to prune.
   */
  limit?: number;
}

/**
 * Result for outbox admin delete operations.
 */
export interface OutboxDeleteResult {
  /**
   * Number of rows deleted.
   */
  deleted: number;
}

/**
 * App-facing outbox storage port.
 *
 * Durable adapters should claim messages atomically and require `claimToken`
 * for delivery/failure updates.
 */
export interface OutboxPort {
  /**
   * Enqueue a new pending message.
   */
  enqueue(input: OutboxEnqueueInput): Promise<OutboxMessage>;
  /**
   * Atomically claim eligible messages for one worker.
   */
  claimBatch(options: OutboxClaimBatchOptions): Promise<OutboxClaimBatchResult>;
  /**
   * Extend an unexpired claim owned by the supplied claim token.
   */
  renewClaim(input: OutboxRenewClaimInput): Promise<OutboxRenewClaimResult>;
  /**
   * Mark a claimed message delivered.
   */
  markDelivered(input: OutboxMarkDeliveredInput): Promise<void>;
  /**
   * Mark a claimed message failed, retryable, or dead-lettered.
   */
  markFailed(input: OutboxMarkFailedInput): Promise<void>;
}

/**
 * Operational outbox admin port.
 *
 * Keep this separate from `OutboxPort` so request and drain contexts can expose
 * only the hot-path delivery operations. Wire this port into maintenance
 * contexts for CLI commands, runbooks, and devtools.
 */
export interface OutboxAdminPort {
  /**
   * List messages ordered by newest update first.
   */
  listMessages(
    options?: OutboxListMessagesOptions,
  ): Promise<readonly OutboxMessage[]>;
  /**
   * Count messages matching admin filters.
   */
  countMessages(options?: OutboxCountMessagesOptions): Promise<number>;
  /**
   * Fetch one message by ID.
   */
  getMessage(id: string): Promise<OutboxMessage | null>;
  /**
   * Return a dead-lettered message to pending state.
   */
  requeueMessage(input: OutboxRequeueMessageInput): Promise<OutboxMessage>;
  /**
   * Delete dead-lettered messages.
   */
  purgeDeadLettered(
    input?: OutboxPurgeDeadLetteredInput,
  ): Promise<OutboxDeleteResult>;
  /**
   * Delete delivered messages older than a retention cutoff.
   */
  pruneDelivered(input: OutboxPruneDeliveredInput): Promise<OutboxDeleteResult>;
}

/**
 * In-memory outbox for tests and local examples.
 */
export interface MemoryOutboxPort extends OutboxPort, OutboxAdminPort {
  /**
   * Current message snapshots.
   */
  readonly messages: readonly OutboxMessage[];
  /**
   * Remove all messages.
   */
  clear(): void;
}

/**
 * Options for `createOutboxMessage(...)`.
 */
export interface CreateOutboxMessageOptions {
  /**
   * Generated message ID override. Wins over `input.id`.
   */
  id?: string;
  /**
   * Fallback ID factory used when neither `id` nor `input.id` is provided.
   */
  createId?: () => string;
  /**
   * Timestamp used for created/updated/available dates.
   */
  now?: Date;
}

/**
 * Options for typed event/job enqueue helpers.
 */
export interface EnqueueTypedOutboxOptions {
  /**
   * Optional caller-provided message ID.
   */
  id?: string;
  /**
   * Earliest time the message may be claimed.
   */
  availableAt?: Date;
  /**
   * Maximum delivery attempts before dead-lettering.
   */
  maxAttempts?: number;
  /** Explicit trace context to persist with this message. */
  trace?: TraceCarrier;
  /** Tracing port used to capture the active context at enqueue time. */
  tracing?: TracingPort;
}

/**
 * Registry of definitions that `drainOutbox(...)` can deliver.
 */
export interface OutboxRegistry {
  /**
   * Event definitions keyed by event name.
   */
  readonly events: ReadonlyMap<string, EventPayloadDef>;
  /**
   * Job definitions keyed by job name.
   */
  readonly jobs: ReadonlyMap<string, JobDef>;
}

/**
 * Input for defining an outbox registry.
 */
export interface DefineOutboxRegistryInput {
  /**
   * Events that may be delivered from the outbox.
   */
  events?: readonly EventPayloadDef[];
  /**
   * Jobs that may be delivered from the outbox.
   */
  jobs?: readonly JobDef[];
}

/**
 * Correlation fields attached to outbox instrumentation events.
 */
export type OutboxInstrumentationContext = Pick<
  BaseProviderInstrumentationEvent,
  "requestId" | "traceId" | "spanId" | "parentSpanId" | "traceparent"
>;

/** Wait primitive used by outbox heartbeats and bounded settlement retries. */
export type OutboxDrainWait = (
  delayMs: number,
  signal: AbortSignal,
) => Promise<void>;

/** Structured failure from a claim heartbeat or active-delivery boundary. */
export interface OutboxLeaseFailure {
  /** Underlying renewal, ownership, or duration error. */
  error: unknown;
  /** Message whose claim could not be kept active. */
  message: ClaimedOutboxMessage;
  /** Lease phase that surfaced the failure. */
  operation: "renewClaim" | "maxActiveDuration";
  /** Current lease state after handling the failure. */
  state: "recovered" | "degraded" | "lost";
  /** Whether the worker no longer has confirmed ownership. */
  confirmedLost: boolean;
}

/** Structured failure from a token-guarded outbox settlement. */
export interface OutboxSettlementFailure {
  /** Storage failure returned by the settlement operation. */
  error: unknown;
  /** Message whose final storage state is unknown. */
  message: ClaimedOutboxMessage;
  /** Settlement operation that failed. */
  operation: "markDelivered" | "markFailed";
  /** Whether the external delivery completed successfully. */
  deliverySucceeded: boolean;
  /** Original delivery error when `deliverySucceeded` is false. */
  deliveryError?: unknown;
}

/**
 * Options for draining one outbox batch.
 */
export interface DrainOutboxOptions {
  /**
   * Outbox storage port.
   */
  outbox: OutboxPort;
  /**
   * Registry used to resolve message names to event/job definitions.
   */
  registry: OutboxRegistry;
  /**
   * Event bus used for event messages. Required when the registry contains
   * events.
   */
  eventBus?: {
    publish<E extends EventPayloadDef>(
      event: E,
      payload: InferEventPayload<E>,
      options?: EventPublishOptions,
    ): MaybePromise<void>;
  };
  /**
   * Job dispatcher used for job messages. Required when the registry contains
   * jobs.
   */
  jobs?: JobDispatcherPort;
  /**
   * Maximum eligible messages to handle in one drain pass.
   */
  batchSize?: number;
  /**
   * Maximum messages delivered concurrently. Defaults to serial delivery.
   * Values greater than one do not preserve delivery order.
   */
  concurrency?: number;
  /**
   * Clock used independently for claiming, renewal, settlement, and retry
   * scheduling. Defaults to the system clock.
   */
  now?: () => Date;
  /**
   * Claim lease duration in milliseconds.
   */
  leaseMs?: number;
  /**
   * Interval between serialized claim renewals. Defaults to one third of the
   * lease duration and must remain shorter than the lease.
   */
  heartbeatMs?: number;
  /**
   * Maximum time Beignet renews a claim for one delivery. When exceeded, the
   * drain stops renewing and leaves final recovery to the last lease expiry.
   */
  maxActiveMs?: number;
  /**
   * Abort-aware wait implementation. Inject a deterministic implementation in
   * tests; production callers normally use the default timer.
   */
  wait?: OutboxDrainWait;
  /**
   * Retry delay in milliseconds or function for per-message delay.
   */
  retryDelayMs?:
    | number
    | ((args: {
        message: ClaimedOutboxMessage;
        error: unknown;
        now: Date;
      }) => number);
  /**
   * Optional instrumentation target for delivery, retry, and dead-letter
   * visibility.
   */
  instrumentation?: ProviderInstrumentationTarget;
  /**
   * Optional correlation fields attached to outbox instrumentation events.
   */
  instrumentationContext?: OutboxInstrumentationContext;
  /**
   * Observer called when delivery fails. Observer failures are ignored so the
   * original delivery failure still controls retry/dead-letter behavior.
   */
  onError?: (
    error: unknown,
    message: ClaimedOutboxMessage,
  ) => MaybePromise<void>;
  /**
   * Observer called after a failed delivery is successfully moved to the dead
   * letter state. Observer failures are ignored.
   */
  onDeadLetter?: (error: unknown, message: OutboxMessage) => MaybePromise<void>;
  /**
   * Observer called when claim renewal degrades or ownership is lost.
   * Observer failures are ignored.
   */
  onLeaseError?: (failure: OutboxLeaseFailure) => MaybePromise<void>;
  /**
   * Observer called when a delivery outcome cannot be settled durably.
   * Observer failures are ignored.
   */
  onSettlementError?: (failure: OutboxSettlementFailure) => MaybePromise<void>;
}

/**
 * Summary returned from one `drainOutbox(...)` pass.
 */
export interface DrainOutboxResult {
  /**
   * Messages claimed in this batch.
   */
  claimed: number;
  /**
   * Messages delivered successfully.
   */
  delivered: number;
  /**
   * Messages scheduled for retry.
   */
  retried: number;
  /**
   * Messages moved to dead letter state.
   */
  deadLettered: number;
  /**
   * Dead-lettered messages whose claim attempt budget was already exhausted.
   * This is a subset of `deadLettered`.
   */
  abandonedDeadLettered: number;
  /** Messages delivered or failed whose final storage state is unknown. */
  settlementFailed: number;
  /** Messages whose active claim could no longer be confirmed. */
  leaseLost: number;
}

/**
 * Error thrown when an outbox payload is not JSON serializable.
 */
export class OutboxSerializationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "OutboxSerializationError";
  }
}

/**
 * Error thrown when an outbox message cannot be resolved through the registry.
 */
export class OutboxRegistryError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "OutboxRegistryError";
  }
}

/**
 * Error thrown when a claimed message cannot be updated with the supplied token.
 */
export class OutboxClaimError extends Error {
  /**
   * Message ID involved in the claim error.
   */
  readonly id: string;

  constructor(args: { id: string; message: string }) {
    super(args.message);
    this.name = "OutboxClaimError";
    this.id = args.id;
  }
}

/**
 * Error persisted when an eligible message has exhausted its claim attempts
 * without reaching a terminal settlement.
 */
export class OutboxAbandonedClaimError extends Error {
  /** Message ID whose attempt budget was exhausted. */
  readonly id: string;
  /** Number of claims already made. */
  readonly attempts: number;
  /** Maximum permitted claim attempts. */
  readonly maxAttempts: number;

  constructor(args: { id: string; attempts: number; maxAttempts: number }) {
    super(
      `Outbox message "${args.id}" exhausted ${args.maxAttempts} claim attempts without a terminal settlement.`,
    );
    this.name = "OutboxAbandonedClaimError";
    this.id = args.id;
    this.attempts = args.attempts;
    this.maxAttempts = args.maxAttempts;
  }
}

/** Error surfaced when an outbox worker no longer has a confirmed claim. */
export class OutboxLeaseLostError extends Error {
  /** Message ID whose ownership was lost. */
  readonly id: string;

  constructor(args: { id: string; message?: string; cause?: unknown }) {
    super(
      args.message ??
        `Outbox claim for message "${args.id}" is no longer active.`,
      args.cause === undefined ? undefined : { cause: args.cause },
    );
    this.name = "OutboxLeaseLostError";
    this.id = args.id;
  }
}

/** Error surfaced when delivery exceeds the configured active-claim window. */
export class OutboxMaxActiveDurationError extends Error {
  /** Message ID whose active window elapsed. */
  readonly id: string;
  /** Configured maximum active duration. */
  readonly maxActiveMs: number;

  constructor(args: { id: string; maxActiveMs: number }) {
    super(
      `Outbox delivery for message "${args.id}" exceeded the ${args.maxActiveMs}ms active-claim limit.`,
    );
    this.name = "OutboxMaxActiveDurationError";
    this.id = args.id;
    this.maxActiveMs = args.maxActiveMs;
  }
}

/**
 * Error thrown when an outbox admin operation cannot be completed safely.
 */
export class OutboxAdminError extends Error {
  /**
   * Message ID involved in the admin error, when applicable.
   */
  readonly id?: string;

  constructor(args: { message: string; id?: string }) {
    super(args.message);
    this.name = "OutboxAdminError";
    this.id = args.id;
  }
}

function assertNonEmptyString(name: string, value: string): void {
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new Error(`${name} must be a non-empty string`);
  }
}

function assertPositiveInteger(name: string, value: number): void {
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`${name} must be a positive integer`);
  }
}

function assertValidDate(name: string, value: Date): void {
  if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
    throw new Error(`${name} must be a valid Date`);
  }
}

function cloneDate(value: Date): Date {
  return new Date(value.getTime());
}

function createId(): string {
  if (!globalThis.crypto?.randomUUID) {
    throw new Error("crypto.randomUUID is required to create outbox IDs.");
  }

  return globalThis.crypto.randomUUID();
}

function assertJsonValue(
  value: unknown,
  path: readonly string[] = [],
  seen: WeakSet<object> = new WeakSet(),
): OutboxJsonValue {
  const label = path.length > 0 ? path.join(".") : "payload";

  if (value === null) return null;

  if (
    typeof value === "string" ||
    typeof value === "boolean" ||
    typeof value === "number"
  ) {
    if (typeof value === "number" && !Number.isFinite(value)) {
      throw new OutboxSerializationError(
        `Outbox ${label} must be a finite number.`,
      );
    }
    return value;
  }

  if (Array.isArray(value)) {
    if (seen.has(value)) {
      throw new OutboxSerializationError(
        `Outbox ${label} must be JSON serializable. Circular references are not supported.`,
      );
    }
    seen.add(value);
    try {
      return value.map((item, index) =>
        assertJsonValue(item, [...path, String(index)], seen),
      );
    } finally {
      seen.delete(value);
    }
  }

  if (typeof value === "object") {
    if (value instanceof Date) {
      throw new OutboxSerializationError(
        `Outbox ${label} must be JSON serializable. Convert Date values to strings before enqueueing.`,
      );
    }
    if (seen.has(value)) {
      throw new OutboxSerializationError(
        `Outbox ${label} must be JSON serializable. Circular references are not supported.`,
      );
    }

    seen.add(value);
    try {
      const record = value as Record<string, unknown>;
      const output: Record<string, OutboxJsonValue> = {};
      for (const key of Object.keys(record)) {
        const child = record[key];
        if (child === undefined) {
          throw new OutboxSerializationError(
            `Outbox ${[...path, key].join(".")} cannot be undefined.`,
          );
        }
        output[key] = assertJsonValue(child, [...path, key], seen);
      }
      return output;
    } finally {
      seen.delete(value);
    }
  }

  throw new OutboxSerializationError(
    `Outbox ${label} must be JSON serializable. Received ${typeof value}.`,
  );
}

/**
 * Convert an unknown value to an outbox-safe JSON value.
 *
 * Dates, undefined values, functions, non-finite numbers, symbols, and circular
 * references are rejected so durable adapters can store the payload safely.
 */
export function toOutboxJsonValue(value: unknown): OutboxJsonValue {
  const jsonValue = assertJsonValue(value);
  return JSON.parse(JSON.stringify(jsonValue)) as OutboxJsonValue;
}

/**
 * Serialize an unknown delivery error into outbox error metadata.
 */
export function serializeOutboxError(error: unknown): OutboxErrorInfo {
  if (error instanceof Error) {
    return {
      name: error.name,
      message: error.message,
      stack: error.stack,
    };
  }

  if (typeof error === "string") {
    return { message: error };
  }

  return { message: "Unknown outbox delivery error" };
}

function copyMessage(message: OutboxMessage): OutboxMessage {
  return {
    ...message,
    availableAt: cloneDate(message.availableAt),
    claimedAt: message.claimedAt ? cloneDate(message.claimedAt) : null,
    lockedUntil: message.lockedUntil ? cloneDate(message.lockedUntil) : null,
    deliveredAt: message.deliveredAt ? cloneDate(message.deliveredAt) : null,
    createdAt: cloneDate(message.createdAt),
    updatedAt: cloneDate(message.updatedAt),
    lastError: message.lastError ? { ...message.lastError } : null,
  };
}

function normalizeStatusFilter(
  status: OutboxMessageQuery["status"],
): ReadonlySet<OutboxMessageStatus> | undefined {
  if (status === undefined) return undefined;
  return new Set(Array.isArray(status) ? status : [status]);
}

function messageMatchesQuery(
  message: OutboxMessage,
  query: OutboxMessageQuery = {},
): boolean {
  const statuses = normalizeStatusFilter(query.status);
  if (statuses && !statuses.has(message.status)) return false;
  if (query.kind !== undefined && message.kind !== query.kind) return false;
  if (query.name !== undefined && message.name !== query.name) return false;
  if (
    query.updatedBefore !== undefined &&
    message.updatedAt.getTime() >= query.updatedBefore.getTime()
  ) {
    return false;
  }
  if (
    query.deliveredBefore !== undefined &&
    (message.deliveredAt === null ||
      message.deliveredAt.getTime() >= query.deliveredBefore.getTime())
  ) {
    return false;
  }
  return true;
}

function compareMessagesForAdminList(
  left: OutboxMessage,
  right: OutboxMessage,
): number {
  const updated = right.updatedAt.getTime() - left.updatedAt.getTime();
  if (updated !== 0) return updated;
  const created = right.createdAt.getTime() - left.createdAt.getTime();
  if (created !== 0) return created;
  return right.id.localeCompare(left.id);
}

function compareMessagesForAdminCleanup(
  left: OutboxMessage,
  right: OutboxMessage,
): number {
  const updated = left.updatedAt.getTime() - right.updatedAt.getTime();
  if (updated !== 0) return updated;
  const created = left.createdAt.getTime() - right.createdAt.getTime();
  if (created !== 0) return created;
  return left.id.localeCompare(right.id);
}

function compareDeliveredMessagesForPrune(
  left: OutboxMessage,
  right: OutboxMessage,
): number {
  const delivered =
    (left.deliveredAt?.getTime() ?? 0) - (right.deliveredAt?.getTime() ?? 0);
  if (delivered !== 0) return delivered;
  return compareMessagesForAdminCleanup(left, right);
}

function resolveAdminListLimit(limit: number | undefined): number {
  const resolved = limit ?? DEFAULT_OUTBOX_ADMIN_LIST_LIMIT;
  assertPositiveInteger("limit", resolved);
  return resolved;
}

function toClaimedMessage(message: OutboxMessage): ClaimedOutboxMessage {
  if (
    message.status !== "claimed" ||
    !message.claimToken ||
    !message.claimedAt ||
    !message.lockedUntil
  ) {
    throw new OutboxClaimError({
      id: message.id,
      message: `Outbox message "${message.id}" is not claimed.`,
    });
  }

  return {
    ...copyMessage(message),
    status: "claimed",
    claimToken: message.claimToken,
    claimedAt: cloneDate(message.claimedAt),
    lockedUntil: cloneDate(message.lockedUntil),
  };
}

/**
 * Create a validated pending outbox message.
 */
export function createOutboxMessage(
  input: OutboxEnqueueInput,
  options: CreateOutboxMessageOptions = {},
): OutboxMessage {
  assertNonEmptyString("kind", input.kind);
  assertNonEmptyString("name", input.name);
  if (input.id !== undefined) assertNonEmptyString("id", input.id);
  if (input.maxAttempts !== undefined) {
    assertPositiveInteger("maxAttempts", input.maxAttempts);
  }

  const now = options.now ?? new Date();
  const trace = parseTraceCarrier(input.trace);
  return {
    id: options.id ?? input.id ?? options.createId?.() ?? createId(),
    kind: input.kind,
    name: input.name,
    payload: toOutboxJsonValue(input.payload),
    ...(trace ? { trace } : {}),
    status: "pending",
    attempts: 0,
    maxAttempts: input.maxAttempts ?? DEFAULT_OUTBOX_MAX_ATTEMPTS,
    availableAt: input.availableAt
      ? cloneDate(input.availableAt)
      : cloneDate(now),
    claimedAt: null,
    lockedUntil: null,
    claimToken: null,
    deliveredAt: null,
    lastError: null,
    createdAt: cloneDate(now),
    updatedAt: cloneDate(now),
  };
}

function isEligible(message: OutboxMessage, now: Date): boolean {
  if (message.status === "pending") {
    return message.availableAt.getTime() <= now.getTime();
  }

  return (
    message.status === "claimed" &&
    message.lockedUntil !== null &&
    message.lockedUntil.getTime() <= now.getTime()
  );
}

/**
 * Options for `createMemoryOutbox(...)`.
 */
export interface MemoryOutboxOptions {
  /**
   * Message and claim-token ID factory. Defaults to `crypto.randomUUID()`.
   */
  id?: () => string;
  /**
   * Clock used for enqueue, claim, and completion timestamps when a call does
   * not supply its own `now`. Defaults to the system clock.
   */
  now?: () => Date;
}

/**
 * Create an in-memory outbox for tests and local examples.
 *
 * The memory outbox is process-local and not durable.
 */
export function createMemoryOutbox(
  storeOptions: MemoryOutboxOptions = {},
): MemoryOutboxPort {
  const createStoreId = storeOptions.id ?? createId;
  const storeNow = storeOptions.now ?? (() => new Date());
  const messages = new Map<string, OutboxMessage>();

  function getClaimedOrThrow(id: string, claimToken: string): OutboxMessage {
    const message = messages.get(id);
    if (!message) {
      throw new OutboxClaimError({
        id,
        message: `Outbox message "${id}" does not exist.`,
      });
    }
    if (message.status !== "claimed" || message.claimToken !== claimToken) {
      throw new OutboxClaimError({
        id,
        message: `Outbox message "${id}" is not claimed by this worker.`,
      });
    }
    return message;
  }

  return {
    get messages() {
      return [...messages.values()].map(copyMessage);
    },

    async listMessages(options = {}) {
      const limit = resolveAdminListLimit(options.limit);
      if (options.updatedBefore) {
        assertValidDate("updatedBefore", options.updatedBefore);
      }
      if (options.deliveredBefore) {
        assertValidDate("deliveredBefore", options.deliveredBefore);
      }

      return [...messages.values()]
        .filter((message) => messageMatchesQuery(message, options))
        .sort(compareMessagesForAdminList)
        .slice(0, limit)
        .map(copyMessage);
    },

    async countMessages(options = {}) {
      if (options.updatedBefore) {
        assertValidDate("updatedBefore", options.updatedBefore);
      }
      if (options.deliveredBefore) {
        assertValidDate("deliveredBefore", options.deliveredBefore);
      }

      return [...messages.values()].filter((message) =>
        messageMatchesQuery(message, options),
      ).length;
    },

    async getMessage(id) {
      assertNonEmptyString("id", id);
      const message = messages.get(id);
      return message ? copyMessage(message) : null;
    },

    async requeueMessage(input) {
      assertNonEmptyString("id", input.id);
      const message = messages.get(input.id);
      if (!message) {
        throw new OutboxAdminError({
          id: input.id,
          message: `Outbox message "${input.id}" does not exist.`,
        });
      }
      if (message.status !== "deadLettered") {
        throw new OutboxAdminError({
          id: input.id,
          message: `Outbox message "${input.id}" is not dead-lettered.`,
        });
      }

      const now = input.now ?? storeNow();
      const availableAt = input.availableAt ?? now;
      assertValidDate("now", now);
      assertValidDate("availableAt", availableAt);

      message.status = "pending";
      message.availableAt = cloneDate(availableAt);
      message.claimToken = null;
      message.claimedAt = null;
      message.lockedUntil = null;
      if (input.resetAttempts) message.attempts = 0;
      message.updatedAt = cloneDate(now);

      return copyMessage(message);
    },

    async purgeDeadLettered(input = {}) {
      const limit =
        input.limit === undefined
          ? undefined
          : resolveAdminListLimit(input.limit);
      if (input.before) assertValidDate("before", input.before);

      const candidates = [...messages.values()]
        .filter(
          (message) =>
            message.status === "deadLettered" &&
            (input.before === undefined ||
              message.updatedAt.getTime() < input.before.getTime()),
        )
        .sort(compareMessagesForAdminCleanup)
        .slice(0, limit);

      for (const message of candidates) {
        messages.delete(message.id);
      }

      return { deleted: candidates.length };
    },

    async pruneDelivered(input) {
      assertValidDate("before", input.before);
      const limit =
        input.limit === undefined
          ? undefined
          : resolveAdminListLimit(input.limit);
      const candidates = [...messages.values()]
        .filter(
          (message) =>
            message.status === "delivered" &&
            message.deliveredAt !== null &&
            message.deliveredAt.getTime() < input.before.getTime(),
        )
        .sort(compareDeliveredMessagesForPrune)
        .slice(0, limit);

      for (const message of candidates) {
        messages.delete(message.id);
      }

      return { deleted: candidates.length };
    },

    async enqueue(input) {
      const message = createOutboxMessage(input, {
        createId: createStoreId,
        now: storeNow(),
      });
      if (messages.has(message.id)) {
        throw new Error(`Outbox message "${message.id}" already exists.`);
      }
      messages.set(message.id, message);
      return copyMessage(message);
    },

    async claimBatch(options) {
      assertPositiveInteger("limit", options.limit);
      const now = options.now ?? storeNow();
      const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
      assertValidDate("now", now);
      assertPositiveInteger("leaseMs", leaseMs);
      const lockedUntil = new Date(now.getTime() + leaseMs);
      const claimed: ClaimedOutboxMessage[] = [];
      const deadLettered: OutboxMessage[] = [];

      const eligible = [...messages.values()]
        .filter((message) => isEligible(message, now))
        .sort((a, b) => {
          const available = a.availableAt.getTime() - b.availableAt.getTime();
          if (available !== 0) return available;
          return a.createdAt.getTime() - b.createdAt.getTime();
        })
        .slice(0, options.limit);

      for (const message of eligible) {
        if (message.attempts >= message.maxAttempts) {
          message.status = "deadLettered";
          message.lastError = serializeOutboxError(
            new OutboxAbandonedClaimError({
              id: message.id,
              attempts: message.attempts,
              maxAttempts: message.maxAttempts,
            }),
          );
          message.claimToken = null;
          message.claimedAt = null;
          message.lockedUntil = null;
          message.updatedAt = cloneDate(now);
          deadLettered.push(copyMessage(message));
          continue;
        }

        message.status = "claimed";
        message.attempts += 1;
        message.claimToken = createStoreId();
        message.claimedAt = cloneDate(now);
        message.lockedUntil = cloneDate(lockedUntil);
        message.updatedAt = cloneDate(now);
        claimed.push(toClaimedMessage(message));
      }

      return { claimed, deadLettered };
    },

    async renewClaim(input) {
      assertNonEmptyString("id", input.id);
      assertNonEmptyString("claimToken", input.claimToken);
      const message = getClaimedOrThrow(input.id, input.claimToken);
      const now = input.now ?? storeNow();
      const leaseMs = input.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
      assertValidDate("now", now);
      assertPositiveInteger("leaseMs", leaseMs);
      if (
        message.lockedUntil === null ||
        message.lockedUntil.getTime() <= now.getTime()
      ) {
        throw new OutboxClaimError({
          id: input.id,
          message: `Outbox message "${input.id}" no longer has an active claim to renew.`,
        });
      }

      const lockedUntil = new Date(now.getTime() + leaseMs);
      message.lockedUntil = cloneDate(lockedUntil);
      message.updatedAt = cloneDate(now);
      return { lockedUntil: cloneDate(lockedUntil) };
    },

    async markDelivered(input) {
      assertNonEmptyString("id", input.id);
      assertNonEmptyString("claimToken", input.claimToken);
      const message = getClaimedOrThrow(input.id, input.claimToken);
      const now = input.now ?? storeNow();
      assertValidDate("now", now);
      if (
        message.lockedUntil === null ||
        message.lockedUntil.getTime() <= now.getTime()
      ) {
        throw new OutboxClaimError({
          id: input.id,
          message: `Outbox message "${input.id}" no longer has an active claim to settle.`,
        });
      }

      message.status = "delivered";
      message.deliveredAt = cloneDate(now);
      message.claimToken = null;
      message.claimedAt = null;
      message.lockedUntil = null;
      message.updatedAt = cloneDate(now);
    },

    async markFailed(input) {
      assertNonEmptyString("id", input.id);
      assertNonEmptyString("claimToken", input.claimToken);
      const message = getClaimedOrThrow(input.id, input.claimToken);
      const now = input.now ?? storeNow();
      assertValidDate("now", now);
      if (input.retryAt) assertValidDate("retryAt", input.retryAt);
      if (
        message.lockedUntil === null ||
        message.lockedUntil.getTime() <= now.getTime()
      ) {
        throw new OutboxClaimError({
          id: input.id,
          message: `Outbox message "${input.id}" no longer has an active claim to settle.`,
        });
      }

      message.status = input.deadLetter ? "deadLettered" : "pending";
      message.lastError = serializeOutboxError(input.error);
      message.availableAt = input.retryAt
        ? cloneDate(input.retryAt)
        : cloneDate(now);
      message.claimToken = null;
      message.claimedAt = null;
      message.lockedUntil = null;
      message.updatedAt = cloneDate(now);
    },

    clear() {
      messages.clear();
    },
  };
}

function mapDefinitions<T extends { name: string }>(
  kind: string,
  defs: readonly T[],
): ReadonlyMap<string, T> {
  const map = new Map<string, T>();
  for (const def of defs) {
    if (map.has(def.name)) {
      throw new OutboxRegistryError(
        `Duplicate ${kind} definition "${def.name}" in outbox registry.`,
      );
    }
    map.set(def.name, def);
  }
  return map;
}

/**
 * Define the events and jobs that an outbox drain worker can deliver.
 *
 * Duplicate names throw because message delivery resolves by name.
 */
export function defineOutboxRegistry(
  input: DefineOutboxRegistryInput,
): OutboxRegistry {
  return {
    events: mapDefinitions("event", input.events ?? []),
    jobs: mapDefinitions("job", input.jobs ?? []),
  };
}

/**
 * Validate an event payload and enqueue it as an outbox message.
 */
export async function enqueueEvent<E extends EventPayloadDef>(
  outbox: OutboxPort,
  event: E,
  payload: InferEventPayload<E>,
  options: EnqueueTypedOutboxOptions = {},
): Promise<OutboxMessage> {
  const prepared = await prepareEventPayloadForTransport(event, payload);
  return await enqueueTransportEvent(
    outbox,
    event,
    prepared.transportValue,
    options,
  );
}

async function enqueueTransportEvent<E extends EventPayloadDef>(
  outbox: OutboxPort,
  event: E,
  payload: EventTransportValue,
  options: EnqueueTypedOutboxOptions,
): Promise<OutboxMessage> {
  const trace =
    parseTraceCarrier(options.trace) ?? captureTraceCarrier(options.tracing);
  return outbox.enqueue({
    id: options.id,
    kind: "event",
    name: event.name,
    payload,
    trace,
    availableAt: options.availableAt,
    maxAttempts: options.maxAttempts,
  });
}

/**
 * Validate a job payload and enqueue it as an outbox message.
 */
export async function enqueueJob<J extends JobDef>(
  outbox: OutboxPort,
  job: J,
  payload: InferJobPayload<J>,
  options: EnqueueTypedOutboxOptions = {},
): Promise<OutboxMessage> {
  await parseJobPayload(job, payload);
  const trace =
    parseTraceCarrier(options.trace) ?? captureTraceCarrier(options.tracing);
  return outbox.enqueue({
    id: options.id,
    kind: "job",
    name: job.name,
    payload: toOutboxJsonValue(payload),
    trace,
    availableAt: options.availableAt,
    maxAttempts: options.maxAttempts ?? getJobRetryMaxAttempts(job.retry),
  });
}

/**
 * Create a domain event recorder that writes events to the outbox.
 */
export function createOutboxEventRecorder(
  outbox: OutboxPort,
  options: EnqueueTypedOutboxOptions = {},
): DomainEventRecorderPort {
  return {
    async record(event, payload, publishOptions) {
      const prepared = await prepareEventPayloadForTransport(
        event,
        payload,
        publishOptions,
      );
      await enqueueTransportEvent(outbox, event, prepared.transportValue, {
        ...options,
        trace: prepared.publishOptions.trace ?? options.trace,
      });
    },
  };
}

/**
 * Create a job dispatcher that writes jobs to the outbox.
 */
export function createOutboxJobDispatcher(
  outbox: OutboxPort,
  options: EnqueueTypedOutboxOptions = {},
): JobDispatcherPort {
  return {
    async dispatch(job, payload, dispatchOptions) {
      await enqueueJob(outbox, job, payload, {
        ...options,
        trace: dispatchOptions?.trace ?? options.trace,
      });
    },
  };
}

function resolveRetryDelayMs(
  options: DrainOutboxOptions,
  message: ClaimedOutboxMessage,
  error: unknown,
  now: Date,
): number {
  if (typeof options.retryDelayMs === "function") {
    const delay = options.retryDelayMs({ message, error, now });
    assertPositiveInteger("retryDelayMs", delay);
    return delay;
  }

  if (options.retryDelayMs !== undefined) {
    assertPositiveInteger("retryDelayMs", options.retryDelayMs);
    return options.retryDelayMs;
  }

  if (message.kind === "job") {
    const job = options.registry.jobs.get(message.name);
    if (job?.retry) {
      return getJobRetryDelayMs(job.retry, {
        attempt: message.attempts,
        error,
        jobName: message.name,
      });
    }
  }

  return Math.min(60_000, 1000 * 2 ** Math.max(0, message.attempts - 1));
}

function shouldRetryOutboxMessage(
  options: DrainOutboxOptions,
  message: ClaimedOutboxMessage,
  error: unknown,
): boolean {
  if (message.kind !== "job") {
    return message.attempts < message.maxAttempts;
  }

  const job = options.registry.jobs.get(message.name);
  return shouldRetryJob(job?.retry, {
    attempt: message.attempts,
    error,
    jobName: message.name,
    maxAttempts: message.maxAttempts,
  });
}

function outboxInstrumentationDetails(
  message: OutboxMessage,
  details?: Record<string, unknown>,
): Record<string, unknown> {
  return {
    attempt: message.attempts,
    maxAttempts: message.maxAttempts,
    messageId: message.id,
    messageKind: message.kind,
    messageName: message.name,
    ...details,
  };
}

async function deliverOutboxMessage(
  options: DrainOutboxOptions,
  message: ClaimedOutboxMessage,
  trace?: TraceCarrier,
): Promise<void> {
  if (message.kind === "event") {
    if (!options.eventBus) {
      throw new OutboxRegistryError(
        `Cannot deliver event "${message.name}" without an event bus.`,
      );
    }

    const event = options.registry.events.get(message.name);
    if (!event) {
      throw new OutboxRegistryError(
        `Outbox registry does not include event "${message.name}".`,
      );
    }

    const prepared = await prepareEventPayloadForTransport(
      event,
      message.payload,
      trace ? { trace } : undefined,
    );
    await options.eventBus.publish(
      event,
      prepared.payload,
      prepared.publishOptions,
    );
    return;
  }

  if (!options.jobs) {
    throw new OutboxRegistryError(
      `Cannot deliver job "${message.name}" without a job dispatcher.`,
    );
  }

  const job = options.registry.jobs.get(message.name);
  if (!job) {
    throw new OutboxRegistryError(
      `Outbox registry does not include job "${message.name}".`,
    );
  }

  await parseJobPayload(job, message.payload);

  // The drain owns execution retries: failed deliveries are rescheduled with
  // the job's own policy via markFailed/retryAt. When the dispatcher exposes
  // a single-attempt dispatch (the inline dispatcher does), use it so the
  // retry policy runs in exactly one layer. Durable providers do not expose
  // it — for them dispatch is an enqueue and the queue owns execution.
  const singleAttempt = (
    options.jobs as {
      [SINGLE_ATTEMPT_DISPATCH]?: SingleAttemptJobDispatch;
    }
  )[SINGLE_ATTEMPT_DISPATCH];
  if (singleAttempt) {
    await singleAttempt(job, message.payload as never, {
      attempt: message.attempts,
      maxAttempts: message.maxAttempts,
      trace,
    });
    return;
  }

  await options.jobs.dispatch(
    job,
    message.payload as never,
    trace ? { trace } : undefined,
  );
}

const MAX_TIMER_DELAY_MS = 2_147_483_647;
const OUTBOX_SETTLEMENT_ATTEMPTS = 3;
const OUTBOX_SETTLEMENT_RETRY_DELAY_MS = 100;

type ResolvedDrainRuntime = {
  batchSize: number;
  concurrency: number;
  leaseMs: number;
  heartbeatMs: number;
  maxActiveMs: number;
  now: () => Date;
  wait: OutboxDrainWait;
};

type MessageDrainOutcome = {
  delivered: number;
  retried: number;
  deadLettered: number;
  settlementFailed: number;
  leaseLost: number;
};

type ClaimHeartbeat = {
  lost: Promise<OutboxLeaseFailure>;
  stopAndExtend(): Promise<{
    lockedUntil: Date;
    renewalError?: unknown;
    renewalRecovered?: boolean;
    failure?: OutboxLeaseFailure;
  }>;
  stop(): Promise<void>;
};

type BoundedRenewalResult =
  | { kind: "succeeded"; lockedUntil: Date }
  | { kind: "failed"; error: unknown }
  | { kind: "deadline" }
  | { kind: "stopped" }
  | { kind: "waitFailed"; error: unknown };

type BoundedSettlementResult =
  | { kind: "succeeded" }
  | { kind: "failed"; error: unknown }
  | { kind: "deadline" }
  | { kind: "waitFailed"; error: unknown };

function defaultOutboxWait(
  delayMs: number,
  signal: AbortSignal,
): Promise<void> {
  return new Promise((resolve) => {
    if (signal.aborted) {
      resolve();
      return;
    }

    let timer: ReturnType<typeof setTimeout> | undefined;
    const finish = () => {
      if (timer !== undefined) clearTimeout(timer);
      signal.removeEventListener("abort", finish);
      resolve();
    };
    timer = setTimeout(finish, delayMs);
    signal.addEventListener("abort", finish, { once: true });
  });
}

function readOutboxNow(now: () => Date): Date {
  const value = now();
  assertValidDate("now", value);
  return value;
}

function assertTimerDuration(name: string, value: number): void {
  assertPositiveInteger(name, value);
  if (value > MAX_TIMER_DELAY_MS) {
    throw new Error(`${name} must be at most ${MAX_TIMER_DELAY_MS}`);
  }
}

function resolveDrainRuntime(
  options: DrainOutboxOptions,
): ResolvedDrainRuntime {
  const batchSize = options.batchSize ?? DEFAULT_OUTBOX_BATCH_SIZE;
  const concurrency = options.concurrency ?? DEFAULT_OUTBOX_CONCURRENCY;
  const leaseMs = options.leaseMs ?? DEFAULT_OUTBOX_LEASE_MS;
  const heartbeatMs =
    options.heartbeatMs ?? Math.max(1, Math.floor(leaseMs / 3));
  const maxActiveMs = options.maxActiveMs ?? DEFAULT_OUTBOX_MAX_ACTIVE_MS;

  assertPositiveInteger("batchSize", batchSize);
  assertPositiveInteger("concurrency", concurrency);
  if (concurrency > batchSize) {
    throw new Error("concurrency must be less than or equal to batchSize");
  }
  assertTimerDuration("leaseMs", leaseMs);
  if (leaseMs < 2) throw new Error("leaseMs must be at least 2");
  assertTimerDuration("heartbeatMs", heartbeatMs);
  if (heartbeatMs >= leaseMs) {
    throw new Error("heartbeatMs must be shorter than leaseMs");
  }
  assertTimerDuration("maxActiveMs", maxActiveMs);

  return {
    batchSize,
    concurrency,
    leaseMs,
    heartbeatMs,
    maxActiveMs,
    now: options.now ?? (() => new Date()),
    wait: options.wait ?? defaultOutboxWait,
  };
}

async function notifyLeaseFailure(
  options: DrainOutboxOptions,
  failure: OutboxLeaseFailure,
): Promise<void> {
  try {
    await options.onLeaseError?.(failure);
  } catch {
    // Lease observers cannot change delivery ownership or recovery behavior.
  }
}

async function notifySettlementFailure(
  options: DrainOutboxOptions,
  failure: OutboxSettlementFailure,
): Promise<void> {
  try {
    await options.onSettlementError?.(failure);
  } catch {
    // Settlement observers cannot replace the unknown storage outcome.
  }
}

async function runBoundedClaimRenewal(options: {
  outbox: OutboxPort;
  message: ClaimedOutboxMessage;
  runtime: ResolvedDrainRuntime;
  renewalAt: Date;
  deadline: Date;
  signal: AbortSignal;
}): Promise<BoundedRenewalResult> {
  const remainingMs = options.deadline.getTime() - options.renewalAt.getTime();
  if (remainingMs <= 0) return { kind: "deadline" };
  if (options.signal.aborted) return { kind: "stopped" };

  const waitController = new AbortController();
  const stopWaiting = () => waitController.abort();
  options.signal.addEventListener("abort", stopWaiting, { once: true });

  const renewal = Promise.resolve()
    .then(() =>
      options.outbox.renewClaim({
        id: options.message.id,
        claimToken: options.message.claimToken,
        now: options.renewalAt,
        leaseMs: options.runtime.leaseMs,
      }),
    )
    .then(
      (result) => ({ kind: "succeeded" as const, result }),
      (error: unknown) => ({ kind: "failed" as const, error }),
    );
  const deadline = Promise.resolve()
    .then(() => options.runtime.wait(remainingMs, waitController.signal))
    .then(
      () =>
        options.signal.aborted
          ? { kind: "stopped" as const }
          : { kind: "deadline" as const },
      (error: unknown) =>
        options.signal.aborted
          ? { kind: "stopped" as const }
          : { kind: "waitFailed" as const, error },
    );

  const result = await Promise.race([renewal, deadline]);
  options.signal.removeEventListener("abort", stopWaiting);
  waitController.abort();

  if (result.kind !== "succeeded") return result;
  try {
    const completedAt = readOutboxNow(options.runtime.now);
    if (completedAt.getTime() >= options.deadline.getTime()) {
      return { kind: "deadline" };
    }
    assertValidDate("lockedUntil", result.result.lockedUntil);
    if (result.result.lockedUntil.getTime() <= completedAt.getTime()) {
      return {
        kind: "failed",
        error: new Error(
          `Outbox claim renewal for message "${options.message.id}" returned an expired lease.`,
        ),
      };
    }
    return {
      kind: "succeeded",
      lockedUntil: cloneDate(result.result.lockedUntil),
    };
  } catch (error) {
    return { kind: "failed", error };
  }
}

function createClaimHeartbeat(
  options: DrainOutboxOptions,
  runtime: ResolvedDrainRuntime,
  message: ClaimedOutboxMessage,
  activeUntil: Date,
): ClaimHeartbeat {
  const controller = new AbortController();
  let stopped = false;
  let lockedUntil = cloneDate(message.lockedUntil);
  let renewing = false;
  let firstRenewalError: unknown;
  let terminalFailure: OutboxLeaseFailure | undefined;
  let resolveLost: (failure: OutboxLeaseFailure) => void = () => {};
  const lost = new Promise<OutboxLeaseFailure>((resolve) => {
    resolveLost = resolve;
  });

  const fail = (failure: OutboxLeaseFailure) => {
    if (terminalFailure) return;
    terminalFailure = failure;
    stopped = true;
    controller.abort();
    resolveLost(failure);
  };

  const run = async () => {
    try {
      let delayMs = runtime.heartbeatMs;
      while (!stopped) {
        try {
          await runtime.wait(delayMs, controller.signal);
        } catch (error) {
          if (stopped || controller.signal.aborted) return;
          fail({
            error: new OutboxLeaseLostError({
              id: message.id,
              message: `Outbox heartbeat scheduling failed for message "${message.id}".`,
              cause: error,
            }),
            message,
            operation: "renewClaim",
            state: "lost",
            confirmedLost: false,
          });
          return;
        }
        if (stopped || controller.signal.aborted) return;

        const renewalAt = readOutboxNow(runtime.now);
        const renewalDeadline = new Date(
          Math.min(lockedUntil.getTime(), activeUntil.getTime()),
        );
        const activeDeadlineEndsFirst =
          activeUntil.getTime() <= lockedUntil.getTime();
        if (renewalAt.getTime() >= renewalDeadline.getTime()) {
          fail({
            error: activeDeadlineEndsFirst
              ? new OutboxMaxActiveDurationError({
                  id: message.id,
                  maxActiveMs: runtime.maxActiveMs,
                })
              : new OutboxLeaseLostError({ id: message.id }),
            message,
            operation: activeDeadlineEndsFirst
              ? "maxActiveDuration"
              : "renewClaim",
            state: "lost",
            confirmedLost: !activeDeadlineEndsFirst,
          });
          return;
        }

        renewing = true;
        let renewal: BoundedRenewalResult;
        try {
          renewal = await runBoundedClaimRenewal({
            outbox: options.outbox,
            message,
            runtime,
            renewalAt,
            deadline: renewalDeadline,
            signal: controller.signal,
          });
        } catch (error) {
          renewal = { kind: "failed", error };
        } finally {
          renewing = false;
        }

        if (renewal.kind === "stopped") return;
        if (renewal.kind === "waitFailed") {
          fail({
            error: new OutboxLeaseLostError({
              id: message.id,
              message: `Could not enforce the renewal deadline for outbox message "${message.id}".`,
              cause: renewal.error,
            }),
            message,
            operation: "renewClaim",
            state: "lost",
            confirmedLost: false,
          });
          return;
        }
        if (renewal.kind === "deadline") {
          fail({
            error: activeDeadlineEndsFirst
              ? new OutboxMaxActiveDurationError({
                  id: message.id,
                  maxActiveMs: runtime.maxActiveMs,
                })
              : new OutboxLeaseLostError({
                  id: message.id,
                  message: `Outbox claim renewal for message "${message.id}" did not complete before the lease deadline.`,
                }),
            message,
            operation: activeDeadlineEndsFirst
              ? "maxActiveDuration"
              : "renewClaim",
            state: "lost",
            confirmedLost: false,
          });
          return;
        }
        if (renewal.kind === "succeeded") {
          lockedUntil = renewal.lockedUntil;
          delayMs = runtime.heartbeatMs;
          continue;
        }

        const error = renewal.error;
        if (error instanceof OutboxClaimError) {
          fail({
            error,
            message,
            operation: "renewClaim",
            state: "lost",
            confirmedLost: true,
          });
          return;
        }

        firstRenewalError ??= error;
        if (stopped) return;
        const retryAt = readOutboxNow(runtime.now);
        const remainingMs = renewalDeadline.getTime() - retryAt.getTime();
        if (remainingMs <= 0) {
          fail({
            error: activeDeadlineEndsFirst
              ? new OutboxMaxActiveDurationError({
                  id: message.id,
                  maxActiveMs: runtime.maxActiveMs,
                })
              : new OutboxLeaseLostError({
                  id: message.id,
                  message: `Outbox claim renewal for message "${message.id}" did not recover before the lease expired.`,
                  cause: error,
                }),
            message,
            operation: activeDeadlineEndsFirst
              ? "maxActiveDuration"
              : "renewClaim",
            state: "lost",
            confirmedLost: false,
          });
          return;
        }
        delayMs = Math.max(
          1,
          Math.min(runtime.heartbeatMs, Math.floor(remainingMs / 3)),
        );
      }
    } catch (error) {
      if (stopped || controller.signal.aborted) return;
      fail({
        error: new OutboxLeaseLostError({
          id: message.id,
          message: `Outbox claim renewal failed unexpectedly for message "${message.id}".`,
          cause: error,
        }),
        message,
        operation: "renewClaim",
        state: "lost",
        confirmedLost: false,
      });
    }
  };

  const running = run();

  const stop = async (abortRenewal = true) => {
    stopped = true;
    if (abortRenewal || !renewing) controller.abort();
    await running;
    controller.abort();
  };

  return {
    lost,
    stop: () => stop(true),
    async stopAndExtend() {
      await stop(false);
      if (terminalFailure) {
        return { lockedUntil, failure: terminalFailure };
      }

      const renewalAt = readOutboxNow(runtime.now);
      const activeDeadlineEndsFirst =
        activeUntil.getTime() <= lockedUntil.getTime();
      const finalRenewalDeadline = new Date(
        Math.min(lockedUntil.getTime(), activeUntil.getTime()),
      );
      if (renewalAt.getTime() >= finalRenewalDeadline.getTime()) {
        return {
          lockedUntil,
          failure: {
            error: activeDeadlineEndsFirst
              ? new OutboxMaxActiveDurationError({
                  id: message.id,
                  maxActiveMs: runtime.maxActiveMs,
                })
              : new OutboxLeaseLostError({ id: message.id }),
            message,
            operation: activeDeadlineEndsFirst
              ? "maxActiveDuration"
              : "renewClaim",
            state: "lost",
            confirmedLost: !activeDeadlineEndsFirst,
          },
        };
      }

      const finalController = new AbortController();
      const renewal = await runBoundedClaimRenewal({
        outbox: options.outbox,
        message,
        runtime,
        renewalAt,
        deadline: finalRenewalDeadline,
        signal: finalController.signal,
      });
      finalController.abort();

      if (renewal.kind === "succeeded") {
        return {
          lockedUntil: renewal.lockedUntil,
          renewalError: firstRenewalError,
          renewalRecovered: firstRenewalError === undefined ? undefined : true,
        };
      }
      if (renewal.kind === "failed") {
        if (!(renewal.error instanceof OutboxClaimError)) {
          const failedAt = readOutboxNow(runtime.now);
          if (
            failedAt.getTime() < lockedUntil.getTime() &&
            failedAt.getTime() < activeUntil.getTime()
          ) {
            return {
              lockedUntil,
              renewalError: renewal.error,
              renewalRecovered: false,
            };
          }
          if (
            activeDeadlineEndsFirst &&
            failedAt.getTime() >= activeUntil.getTime()
          ) {
            return {
              lockedUntil,
              failure: {
                error: new OutboxMaxActiveDurationError({
                  id: message.id,
                  maxActiveMs: runtime.maxActiveMs,
                }),
                message,
                operation: "maxActiveDuration",
                state: "lost",
                confirmedLost: false,
              },
            };
          }
        }
        return {
          lockedUntil,
          failure: {
            error: renewal.error,
            message,
            operation: "renewClaim",
            state: "lost",
            confirmedLost: renewal.error instanceof OutboxClaimError,
          },
        };
      }

      return {
        lockedUntil,
        failure: {
          error:
            renewal.kind === "waitFailed"
              ? new OutboxLeaseLostError({
                  id: message.id,
                  message: activeDeadlineEndsFirst
                    ? `Could not enforce the maximum active duration for outbox message "${message.id}".`
                    : `Could not enforce the final renewal deadline for outbox message "${message.id}".`,
                  cause: renewal.error,
                })
              : activeDeadlineEndsFirst
                ? new OutboxMaxActiveDurationError({
                    id: message.id,
                    maxActiveMs: runtime.maxActiveMs,
                  })
                : new OutboxLeaseLostError({
                    id: message.id,
                    message: `Outbox claim renewal for message "${message.id}" did not complete before settlement.`,
                  }),
          message,
          operation: activeDeadlineEndsFirst
            ? "maxActiveDuration"
            : "renewClaim",
          state: "lost",
          confirmedLost: false,
        },
      };
    },
  };
}

async function settleClaim(options: {
  operation: OutboxSettlementFailure["operation"];
  message: ClaimedOutboxMessage;
  lockedUntil: Date;
  activeUntil: Date;
  runtime: ResolvedDrainRuntime;
  settle(now: Date): Promise<void>;
}): Promise<{ ok: true } | { ok: false; error: unknown; claimLost: boolean }> {
  let lastError: unknown;

  for (let attempt = 1; attempt <= OUTBOX_SETTLEMENT_ATTEMPTS; attempt += 1) {
    const settlementAt = readOutboxNow(options.runtime.now);
    const settlementDeadline = new Date(
      Math.min(options.lockedUntil.getTime(), options.activeUntil.getTime()),
    );
    const settlement = await runBoundedSettlement({
      runtime: options.runtime,
      settlementAt,
      deadline: settlementDeadline,
      settle: options.settle,
    });
    if (settlement.kind === "succeeded") {
      return { ok: true };
    }
    if (settlement.kind === "deadline") {
      return {
        ok: false,
        error: new OutboxLeaseLostError({
          id: options.message.id,
          message: `Outbox ${options.operation} for message "${options.message.id}" did not complete before its settlement deadline.`,
        }),
        claimLost: true,
      };
    }
    if (settlement.kind === "waitFailed") {
      return { ok: false, error: settlement.error, claimLost: false };
    }

    const error = settlement.error;
    lastError = error;
    if (error instanceof OutboxClaimError) {
      return { ok: false, error, claimLost: true };
    }

    const remainingMs =
      settlementDeadline.getTime() -
      readOutboxNow(options.runtime.now).getTime();
    if (attempt >= OUTBOX_SETTLEMENT_ATTEMPTS || remainingMs <= 0) break;

    const waitController = new AbortController();
    try {
      await options.runtime.wait(
        Math.max(
          1,
          Math.min(
            OUTBOX_SETTLEMENT_RETRY_DELAY_MS * 2 ** (attempt - 1),
            remainingMs,
          ),
        ),
        waitController.signal,
      );
    } catch (error) {
      return { ok: false, error, claimLost: false };
    }
  }

  return { ok: false, error: lastError, claimLost: false };
}

async function runBoundedSettlement(options: {
  runtime: ResolvedDrainRuntime;
  settlementAt: Date;
  deadline: Date;
  settle(now: Date): Promise<void>;
}): Promise<BoundedSettlementResult> {
  const remainingMs =
    options.deadline.getTime() - options.settlementAt.getTime();
  if (remainingMs <= 0) return { kind: "deadline" };

  const waitController = new AbortController();
  const settlement = Promise.resolve()
    .then(() => options.settle(options.settlementAt))
    .then(
      () => ({ kind: "succeeded" as const }),
      (error: unknown) => ({ kind: "failed" as const, error }),
    );
  const deadline = Promise.resolve()
    .then(() => options.runtime.wait(remainingMs, waitController.signal))
    .then(
      () => ({ kind: "deadline" as const }),
      (error: unknown) => ({ kind: "waitFailed" as const, error }),
    );

  const result = await Promise.race([settlement, deadline]);
  waitController.abort();
  return result;
}

async function recordDeadLetter(
  options: DrainOutboxOptions,
  instrumentation: ReturnType<typeof createProviderInstrumentation>,
  jobInstrumentation: ReturnType<typeof createProviderInstrumentation>,
  message: OutboxMessage,
  error: unknown,
  details: Record<string, unknown> = {},
): Promise<void> {
  try {
    await options.onDeadLetter?.(error, message);
  } catch {
    // Dead-letter observers must not change the settled message state.
  }
  instrumentation.record({
    type: "outbox",
    ...options.instrumentationContext,
    messageId: message.id,
    messageKind: message.kind,
    messageName: message.name,
    status: "deadLettered",
    details: outboxInstrumentationDetails(message, {
      ...details,
      error: serializeOutboxError(error),
    }),
  });
  if (message.kind === "job") {
    jobInstrumentation.record({
      type: "job",
      ...options.instrumentationContext,
      jobName: message.name,
      status: "deadLettered",
      details: outboxInstrumentationDetails(message, {
        ...details,
        error: serializeOutboxError(error),
      }),
    });
  }
}

async function processClaimedMessage(
  options: DrainOutboxOptions,
  runtime: ResolvedDrainRuntime,
  instrumentation: ReturnType<typeof createProviderInstrumentation>,
  jobInstrumentation: ReturnType<typeof createProviderInstrumentation>,
  tracing: TracingPort | undefined,
  message: ClaimedOutboxMessage,
): Promise<MessageDrainOutcome> {
  const emptyOutcome = (): MessageDrainOutcome => ({
    delivered: 0,
    retried: 0,
    deadLettered: 0,
    settlementFailed: 0,
    leaseLost: 0,
  });
  const outcome = emptyOutcome();
  const startedAt = readOutboxNow(runtime.now);
  const activeUntil = new Date(startedAt.getTime() + runtime.maxActiveMs);
  const heartbeat = createClaimHeartbeat(
    options,
    runtime,
    message,
    activeUntil,
  );
  const lifetimeController = new AbortController();

  const parentTrace = parseTraceCarrier(message.trace);
  const traceAttributes = {
    "beignet.outbox.message_kind": message.kind,
    "beignet.outbox.message_name": message.name,
  } as const;
  const delivery = Promise.resolve()
    .then(() =>
      runWithTracing(
        tracing,
        {
          name: `beignet.outbox deliver ${message.name}`,
          type: "outbox",
          kind: "consumer",
          parent: parentTrace,
          attributes: traceAttributes,
          metricAttributes: traceAttributes,
        },
        (span) =>
          deliverOutboxMessage(
            options,
            message,
            captureTraceCarrier(span?.context ?? parentTrace),
          ),
      ),
    )
    .then(
      () => ({ kind: "succeeded" as const }),
      (error: unknown) => ({ kind: "failed" as const, error }),
    );
  const maximumActive = Promise.resolve()
    .then(() => runtime.wait(runtime.maxActiveMs, lifetimeController.signal))
    .then(
      () => ({ kind: "maxActive" as const }),
      (error: unknown) => ({ kind: "waitFailed" as const, error }),
    );
  const leaseLost = heartbeat.lost.then((failure) => ({
    kind: "leaseLost" as const,
    failure,
  }));

  const deliveryOutcome = await Promise.race([
    delivery,
    maximumActive,
    leaseLost,
  ]);
  lifetimeController.abort();

  if (deliveryOutcome.kind === "maxActive") {
    await heartbeat.stop();
    const failure: OutboxLeaseFailure = {
      error: new OutboxMaxActiveDurationError({
        id: message.id,
        maxActiveMs: runtime.maxActiveMs,
      }),
      message,
      operation: "maxActiveDuration",
      state: "lost",
      confirmedLost: false,
    };
    await notifyLeaseFailure(options, failure);
    instrumentation.custom({
      name: "outbox.lease.lost",
      label: "Outbox claim no longer confirmed",
      summary: `Stopped renewing ${message.kind} "${message.name}" after its active-delivery limit`,
      details: outboxInstrumentationDetails(message, {
        operation: failure.operation,
        state: failure.state,
        error: serializeOutboxError(failure.error),
      }),
    });
    outcome.leaseLost = 1;
    return outcome;
  }

  if (deliveryOutcome.kind === "waitFailed") {
    await heartbeat.stop();
    const failure: OutboxLeaseFailure = {
      error: new OutboxLeaseLostError({
        id: message.id,
        message: `Could not enforce the active-delivery limit for outbox message "${message.id}".`,
        cause: deliveryOutcome.error,
      }),
      message,
      operation: "maxActiveDuration",
      state: "lost",
      confirmedLost: false,
    };
    await notifyLeaseFailure(options, failure);
    instrumentation.custom({
      name: "outbox.lease.lost",
      label: "Outbox claim no longer confirmed",
      summary: `Could not enforce the active-delivery limit for ${message.kind} "${message.name}"`,
      details: outboxInstrumentationDetails(message, {
        operation: failure.operation,
        state: failure.state,
        error: serializeOutboxError(failure.error),
      }),
    });
    outcome.leaseLost = 1;
    return outcome;
  }

  if (deliveryOutcome.kind === "leaseLost") {
    await heartbeat.stop();
    await notifyLeaseFailure(options, deliveryOutcome.failure);
    instrumentation.custom({
      name: "outbox.lease.lost",
      label: "Outbox claim lost",
      summary: `Could not keep the claim for ${message.kind} "${message.name}" active`,
      details: outboxInstrumentationDetails(message, {
        operation: deliveryOutcome.failure.operation,
        state: deliveryOutcome.failure.state,
        confirmedLost: deliveryOutcome.failure.confirmedLost,
        error: serializeOutboxError(deliveryOutcome.failure.error),
      }),
    });
    outcome.leaseLost = 1;
    return outcome;
  }

  const lease = await heartbeat.stopAndExtend();
  if (lease.failure) {
    await notifyLeaseFailure(options, lease.failure);
    instrumentation.custom({
      name: "outbox.lease.lost",
      label: "Outbox claim lost",
      summary: `Could not confirm the claim for ${message.kind} "${message.name}" before settlement`,
      details: outboxInstrumentationDetails(message, {
        operation: lease.failure.operation,
        state: lease.failure.state,
        confirmedLost: lease.failure.confirmedLost,
        error: serializeOutboxError(lease.failure.error),
      }),
    });
    outcome.leaseLost = 1;
    return outcome;
  }

  const reportRenewalOutcome = async () => {
    if (lease.renewalError === undefined) return;
    const failure: OutboxLeaseFailure = {
      error: lease.renewalError,
      message,
      operation: "renewClaim",
      state: lease.renewalRecovered ? "recovered" : "degraded",
      confirmedLost: false,
    };
    await notifyLeaseFailure(options, failure);
    instrumentation.custom({
      name: lease.renewalRecovered
        ? "outbox.lease.renewal.recovered"
        : "outbox.lease.renewal.degraded",
      label: lease.renewalRecovered
        ? "Outbox claim renewal recovered"
        : "Outbox claim renewal degraded",
      summary: lease.renewalRecovered
        ? `Recovered claim renewal for ${message.kind} "${message.name}"`
        : `Continued ${message.kind} "${message.name}" settlement under its last confirmed lease`,
      details: outboxInstrumentationDetails(message, {
        state: failure.state,
        error: serializeOutboxError(lease.renewalError),
      }),
    });
  };

  if (deliveryOutcome.kind === "succeeded") {
    const settlement = await settleClaim({
      operation: "markDelivered",
      message,
      lockedUntil: lease.lockedUntil,
      activeUntil,
      runtime,
      settle: (now) =>
        options.outbox.markDelivered({
          id: message.id,
          claimToken: message.claimToken,
          now,
        }),
    });
    await reportRenewalOutcome();
    if (!settlement.ok) {
      const failure: OutboxSettlementFailure = {
        error: settlement.error,
        message,
        operation: "markDelivered",
        deliverySucceeded: true,
      };
      await notifySettlementFailure(options, failure);
      instrumentation.custom({
        name: "outbox.settlement.failed",
        label: "Outbox settlement failed",
        summary: `Delivered ${message.kind} "${message.name}", but could not confirm its durable acknowledgement`,
        details: outboxInstrumentationDetails(message, {
          operation: failure.operation,
          deliverySucceeded: true,
          settlementError: serializeOutboxError(settlement.error),
        }),
      });
      outcome.settlementFailed = 1;
      if (settlement.claimLost) outcome.leaseLost = 1;
      return outcome;
    }

    instrumentation.record({
      type: "outbox",
      ...options.instrumentationContext,
      messageId: message.id,
      messageKind: message.kind,
      messageName: message.name,
      status: "delivered",
      details: outboxInstrumentationDetails(message),
    });
    outcome.delivered = 1;
    return outcome;
  }

  const deliveryError = deliveryOutcome.error;
  const failedAt = readOutboxNow(runtime.now);
  const shouldRetry = shouldRetryOutboxMessage(options, message, deliveryError);
  const deadLetter = !shouldRetry;
  const retryDelayMs = deadLetter
    ? 0
    : resolveRetryDelayMs(options, message, deliveryError, failedAt);
  const retryAt = deadLetter
    ? undefined
    : new Date(failedAt.getTime() + retryDelayMs);
  const settlement = await settleClaim({
    operation: "markFailed",
    message,
    lockedUntil: lease.lockedUntil,
    activeUntil,
    runtime,
    settle: (now) =>
      options.outbox.markFailed({
        id: message.id,
        claimToken: message.claimToken,
        error: deliveryError,
        deadLetter,
        now,
        retryAt,
      }),
  });

  await reportRenewalOutcome();

  try {
    await options.onError?.(deliveryError, message);
  } catch {
    // Delivery observers cannot change retry, dead-letter, or recovery state.
  }

  if (!settlement.ok) {
    const failure: OutboxSettlementFailure = {
      error: settlement.error,
      message,
      operation: "markFailed",
      deliverySucceeded: false,
      deliveryError,
    };
    await notifySettlementFailure(options, failure);
    instrumentation.custom({
      name: "outbox.settlement.failed",
      label: "Outbox settlement failed",
      summary: `Could not settle failed ${message.kind} "${message.name}"`,
      details: outboxInstrumentationDetails(message, {
        operation: failure.operation,
        deliverySucceeded: false,
        deliveryError: serializeOutboxError(deliveryError),
        settlementError: serializeOutboxError(settlement.error),
      }),
    });
    outcome.settlementFailed = 1;
    if (settlement.claimLost) outcome.leaseLost = 1;
    return outcome;
  }

  if (deadLetter) {
    await recordDeadLetter(
      options,
      instrumentation,
      jobInstrumentation,
      message,
      deliveryError,
    );
    outcome.deadLettered = 1;
    return outcome;
  }

  instrumentation.record({
    type: "outbox",
    ...options.instrumentationContext,
    messageId: message.id,
    messageKind: message.kind,
    messageName: message.name,
    status: "retryScheduled",
    details: outboxInstrumentationDetails(message, {
      retryDelayMs,
      retryAt: retryAt?.toISOString(),
      error: serializeOutboxError(deliveryError),
    }),
  });
  if (message.kind === "job") {
    jobInstrumentation.record({
      type: "job",
      ...options.instrumentationContext,
      jobName: message.name,
      status: "retryScheduled",
      details: outboxInstrumentationDetails(message, {
        retryDelayMs,
        retryAt: retryAt?.toISOString(),
        error: serializeOutboxError(deliveryError),
      }),
    });
  }
  outcome.retried = 1;
  return outcome;
}

/**
 * Claim and deliver one bounded set of outbox messages.
 *
 * The drain claims only enough messages to fill active delivery slots and
 * renews each active claim until delivery settles or reaches its configured
 * maximum duration. It remains an at-least-once transport: a process can still
 * terminate after the external effect succeeds but before acknowledgement.
 */
export async function drainOutbox(
  options: DrainOutboxOptions,
): Promise<DrainOutboxResult> {
  const runtime = resolveDrainRuntime(options);
  assertOutboxDrainPort(options.outbox);
  assertOutboxDeliveryCapabilities(options);
  const instrumentation = createProviderInstrumentation(
    options.instrumentation,
    {
      providerName: "outbox",
      watcher: "outbox",
    },
  );
  const jobInstrumentation = createProviderInstrumentation(
    options.instrumentation,
    {
      providerName: "outbox",
      watcher: "jobs",
    },
  );
  const tracing = resolveTracingPort(options.instrumentation);
  const result: DrainOutboxResult = {
    claimed: 0,
    delivered: 0,
    retried: 0,
    deadLettered: 0,
    abandonedDeadLettered: 0,
    settlementFailed: 0,
    leaseLost: 0,
  };
  let remaining = runtime.batchSize;
  let sourceExhausted = false;

  while (remaining > 0 && !sourceExhausted) {
    const active: ClaimedOutboxMessage[] = [];
    const abandoned: OutboxMessage[] = [];

    while (active.length < runtime.concurrency && remaining > 0) {
      const limit = Math.min(runtime.concurrency - active.length, remaining);
      const selected = await options.outbox.claimBatch({
        limit,
        now: readOutboxNow(runtime.now),
        leaseMs: runtime.leaseMs,
      });
      const selectedCount =
        selected.claimed.length + selected.deadLettered.length;
      if (selectedCount === 0) {
        sourceExhausted = true;
        break;
      }
      if (selectedCount > limit) {
        throw new Error(
          `Outbox claimBatch returned ${selectedCount} messages for a limit of ${limit}.`,
        );
      }

      remaining -= selectedCount;
      result.claimed += selected.claimed.length;
      active.push(...selected.claimed);
      abandoned.push(...selected.deadLettered);
    }

    // Construct active delivery promises first so their heartbeats protect
    // freshly claimed rows while abandoned-message observers run.
    const outcomePromises = active.map((message) =>
      processClaimedMessage(
        options,
        runtime,
        instrumentation,
        jobInstrumentation,
        tracing,
        message,
      ),
    );
    const abandonedPromises = abandoned.map(async (message) => {
      const error = new OutboxAbandonedClaimError({
        id: message.id,
        attempts: message.attempts,
        maxAttempts: message.maxAttempts,
      });
      await recordDeadLetter(
        options,
        instrumentation,
        jobInstrumentation,
        message,
        error,
        { abandoned: true },
      );
    });
    const [outcomes] = await Promise.all([
      Promise.all(outcomePromises),
      Promise.all(abandonedPromises),
    ]);
    result.deadLettered += abandoned.length;
    result.abandonedDeadLettered += abandoned.length;

    for (const outcome of outcomes) {
      result.delivered += outcome.delivered;
      result.retried += outcome.retried;
      result.deadLettered += outcome.deadLettered;
      result.settlementFailed += outcome.settlementFailed;
      result.leaseLost += outcome.leaseLost;
    }
  }

  return result;
}

function assertOutboxDrainPort(outbox: OutboxPort): void {
  const candidate = outbox as unknown as Record<string, unknown>;
  const missing = [
    "claimBatch",
    "renewClaim",
    "markDelivered",
    "markFailed",
  ].filter((method) => typeof candidate[method] !== "function");
  if (missing.length > 0) {
    throw new Error(
      `Cannot drain this outbox: the outbox port is missing ${missing
        .map((method) => `${method}()`)
        .join(", ")}.`,
    );
  }
}

function assertOutboxDeliveryCapabilities(options: DrainOutboxOptions): void {
  const missing: string[] = [];

  if (options.registry.events.size > 0 && !options.eventBus) {
    missing.push("events require an event bus");
  }
  if (options.registry.jobs.size > 0 && !options.jobs) {
    missing.push("jobs require a job dispatcher");
  }

  if (missing.length > 0) {
    throw new OutboxRegistryError(
      `Cannot drain this outbox registry: ${missing.join("; ")}.`,
    );
  }
}

/**
 * Domain event recorder port re-exported for outbox integrations.
 */
export type { DomainEventRecorderPort };