UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

820 lines 24 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 InferEventPayload } from "../events/index.js"; import { type InferJobPayload, type JobDef } from "../jobs/index.js"; import type { JobDispatcherPort } from "../ports/events.js"; import type { DomainEventRecorderPort } from "../ports/unit-of-work.js"; import { type BaseProviderInstrumentationEvent, type ProviderInstrumentationTarget } from "../providers/index.js"; import { 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 declare const DEFAULT_OUTBOX_LEASE_MS = 30000; /** * Default maximum messages handled by one bounded drain pass. */ export declare const DEFAULT_OUTBOX_BATCH_SIZE = 100; /** * Default number of outbox messages delivered concurrently. */ export declare const DEFAULT_OUTBOX_CONCURRENCY = 1; /** * Default maximum time Beignet renews a claim for one delivery. */ export declare const DEFAULT_OUTBOX_MAX_ACTIVE_MS = 300000; /** * Default maximum delivery attempts before a message is dead-lettered. */ export declare 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 declare 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 declare class OutboxSerializationError extends Error { constructor(message: string); } /** * Error thrown when an outbox message cannot be resolved through the registry. */ export declare class OutboxRegistryError extends Error { constructor(message: string); } /** * Error thrown when a claimed message cannot be updated with the supplied token. */ export declare class OutboxClaimError extends Error { /** * Message ID involved in the claim error. */ readonly id: string; constructor(args: { id: string; message: string; }); } /** * Error persisted when an eligible message has exhausted its claim attempts * without reaching a terminal settlement. */ export declare 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; }); } /** Error surfaced when an outbox worker no longer has a confirmed claim. */ export declare class OutboxLeaseLostError extends Error { /** Message ID whose ownership was lost. */ readonly id: string; constructor(args: { id: string; message?: string; cause?: unknown; }); } /** Error surfaced when delivery exceeds the configured active-claim window. */ export declare 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; }); } /** * Error thrown when an outbox admin operation cannot be completed safely. */ export declare class OutboxAdminError extends Error { /** * Message ID involved in the admin error, when applicable. */ readonly id?: string; constructor(args: { message: string; id?: string; }); } /** * 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 declare function toOutboxJsonValue(value: unknown): OutboxJsonValue; /** * Serialize an unknown delivery error into outbox error metadata. */ export declare function serializeOutboxError(error: unknown): OutboxErrorInfo; /** * Create a validated pending outbox message. */ export declare function createOutboxMessage(input: OutboxEnqueueInput, options?: CreateOutboxMessageOptions): OutboxMessage; /** * 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 declare function createMemoryOutbox(storeOptions?: MemoryOutboxOptions): MemoryOutboxPort; /** * Define the events and jobs that an outbox drain worker can deliver. * * Duplicate names throw because message delivery resolves by name. */ export declare function defineOutboxRegistry(input: DefineOutboxRegistryInput): OutboxRegistry; /** * Validate an event payload and enqueue it as an outbox message. */ export declare function enqueueEvent<E extends EventPayloadDef>(outbox: OutboxPort, event: E, payload: InferEventPayload<E>, options?: EnqueueTypedOutboxOptions): Promise<OutboxMessage>; /** * Validate a job payload and enqueue it as an outbox message. */ export declare function enqueueJob<J extends JobDef>(outbox: OutboxPort, job: J, payload: InferJobPayload<J>, options?: EnqueueTypedOutboxOptions): Promise<OutboxMessage>; /** * Create a domain event recorder that writes events to the outbox. */ export declare function createOutboxEventRecorder(outbox: OutboxPort, options?: EnqueueTypedOutboxOptions): DomainEventRecorderPort; /** * Create a job dispatcher that writes jobs to the outbox. */ export declare function createOutboxJobDispatcher(outbox: OutboxPort, options?: EnqueueTypedOutboxOptions): JobDispatcherPort; /** * 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 declare function drainOutbox(options: DrainOutboxOptions): Promise<DrainOutboxResult>; /** * Domain event recorder port re-exported for outbox integrations. */ export type { DomainEventRecorderPort }; //# sourceMappingURL=index.d.ts.map