@beignet/core
Version:
Core framework primitives for Beignet
669 lines • 18.4 kB
TypeScript
/**
* @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 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 messages to claim in one batch.
*/
limit: number;
/**
* Claim timestamp.
*/
now?: Date;
/**
* Lease duration in milliseconds.
*/
leaseMs?: number;
}
/**
* 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<ClaimedOutboxMessage[]>;
/**
* 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">;
/**
* 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.
*/
eventBus?: {
publish<E extends EventPayloadDef>(event: E, payload: InferEventPayload<E>, options?: EventPublishOptions): MaybePromise<void>;
};
/**
* Job dispatcher used for job messages.
*/
jobs?: JobDispatcherPort;
/**
* Maximum messages to claim in one drain pass.
*/
batchSize?: number;
/**
* Timestamp used for claiming and state updates.
*/
now?: Date;
/**
* Claim lease duration in milliseconds.
*/
leaseMs?: number;
/**
* 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: ClaimedOutboxMessage) => MaybePromise<void>;
/**
* Observer called when a failed delivery cannot be settled as retryable or
* dead-lettered. Observer failures are ignored.
*/
onSettlementError?: (settlementError: unknown, message: ClaimedOutboxMessage, deliveryError: unknown) => 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;
}
/**
* 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 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 batch of outbox messages.
*
* This does not loop forever; production workers should call it on their own
* polling cadence. Event and job messages require matching registry entries.
* Failed messages are retried with backoff until `maxAttempts`, then
* dead-lettered.
*/
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