UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

418 lines 12.2 kB
/** * @beignet/core/idempotency * * Idempotency primitives for retry-safe commands, webhooks, and jobs. */ /** * Value or promise of that value. */ export type MaybePromise<T> = T | Promise<T>; /** * Primitive value accepted inside an idempotency scope object. */ export type IdempotencyScopeValue = string | number | boolean | null | undefined; /** * Logical scope for idempotency keys. * * String scopes and object scopes are normalized with type tags so adapters can * build stable storage keys without primitive or delimiter collisions. */ export type IdempotencyScope = string | Record<string, IdempotencyScopeValue>; /** * Scope mode that HTTP hooks can use when deriving an idempotency scope. */ export type IdempotencyScopeMode = "global" | "actor" | "tenant" | "actor-tenant"; /** Default lifetime for unfinished idempotency reservations. */ export declare const DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC = 300; /** * Contract metadata for idempotency-aware routes. * * This metadata is enforced at the HTTP boundary by * `createIdempotencyHooks(...)` from `@beignet/core/server`. * `runIdempotently(...)` remains the primitive for non-HTTP workflows such as * jobs, listeners, webhooks, and schedules. */ export interface IdempotencyMeta { /** * Whether this operation requires an idempotency key at the HTTP boundary. */ required?: boolean; /** * Header that carries the idempotency key. * * Default: "idempotency-key". */ header?: string; /** * How to scope idempotency keys when an HTTP hook derives the scope. * * By default the HTTP hook scopes to the actor and includes the tenant when * one is present. Use "global" explicitly only when every caller should * share one idempotency-key namespace for the operation. */ scope?: IdempotencyScopeMode; /** * Time-to-live for completed replay results. */ ttlSec?: number; /** * Time-to-live for unfinished reservations. * * @default 300 */ reservationTtlSec?: number; } /** * Input for reserving an idempotency key. */ export interface IdempotencyReserveInput { /** * Operation namespace, usually a use-case or route name. */ namespace: string; /** * Client-provided idempotency key. */ key: string; /** * Logical scope for this key. */ scope?: IdempotencyScope; /** * Fingerprint of the logical command payload. */ fingerprint: string; /** * Optional time-to-live for the completed replay result. */ ttlSec?: number; /** * Time-to-live for unfinished work before a successor may reserve the key. * * @default 300 */ reservationTtlSec?: number; } /** * Result of reserving an idempotency key. */ export type IdempotencyReservation = { status: "reserved"; namespace: string; key: string; scopeKey: string; fingerprint: string; reservationToken: string; reservedAt: Date; expiresAt: Date | null; } | { status: "replay"; namespace: string; key: string; scopeKey: string; fingerprint: string; result: unknown; reservedAt: Date; completedAt: Date; expiresAt: Date | null; } | { status: "inProgress"; namespace: string; key: string; scopeKey: string; fingerprint: string; reservedAt: Date; expiresAt: Date | null; } | { status: "conflict"; namespace: string; key: string; scopeKey: string; storedFingerprint: string; receivedFingerprint: string; reservedAt: Date; completedAt?: Date; expiresAt: Date | null; }; /** * Input for marking an idempotency key complete. */ export interface IdempotencyCompleteInput { /** * Operation namespace. */ namespace: string; /** * Client-provided idempotency key. */ key: string; /** * Logical scope for this key. */ scope?: IdempotencyScope; /** * Fingerprint that must match the reserved operation. */ fingerprint: string; /** Opaque identity returned by the matching reserved result. */ reservationToken: string; /** * Result to replay for future matching requests. */ result?: unknown; } /** * Input for releasing or marking a failed idempotency reservation. */ export interface IdempotencyFailInput { /** * Operation namespace. */ namespace: string; /** * Client-provided idempotency key. */ key: string; /** * Logical scope for this key. */ scope?: IdempotencyScope; /** * Fingerprint that must match the reserved operation. */ fingerprint: string; /** Opaque identity returned by the matching reserved result. */ reservationToken: string; /** * Error that caused the protected operation to fail. */ error?: unknown; } /** * App-facing idempotency port. */ export interface IdempotencyPort { /** * Atomically reserve a key for work, replay an already completed result, or * report that the key is in progress/conflicting. */ reserve(input: IdempotencyReserveInput): Promise<IdempotencyReservation>; /** * Mark a reserved key as complete and store the result that may be replayed. * * Implementations must reject when the fingerprint, reservation token, or * in-progress state no longer matches the current reservation. */ complete(input: IdempotencyCompleteInput): Promise<void>; /** * Release or mark a reserved key after the protected work fails. * * Implementations must reject when the fingerprint, reservation token, or * in-progress state no longer matches the current reservation. */ fail(input: IdempotencyFailInput): Promise<void>; } /** * Options for reserving an idempotency key around a protected operation. */ export interface RunIdempotentlyOptions<Result> extends IdempotencyReserveInput { /** * Protected operation to run after the key is reserved. */ run: () => MaybePromise<Result>; /** * Replay behavior for completed matching reservations. * * Defaults to returning the stored result. Use `"error"` when callers need to * distinguish replay from first execution. */ replay?: "return" | "error"; } /** * Options for `createIdempotencyFingerprint(...)`. */ export interface CreateIdempotencyFingerprintOptions { /** * Omit values from the fingerprint input. Use this for the idempotency key * itself or other request metadata that does not define the logical command. * * String paths can be top-level keys (`"idempotencyKey"`) or dotted paths * (`"metadata.requestId"`). Array paths avoid ambiguity when keys contain * dots. */ omit?: readonly (string | readonly string[])[]; } /** * In-memory idempotency store for tests and local examples. */ export interface MemoryIdempotencyStore extends IdempotencyPort { /** * Current store entries. */ readonly entries: readonly MemoryIdempotencyEntry[]; /** * Remove all entries. */ clear(): void; } /** * Snapshot entry from the memory idempotency store. */ export interface MemoryIdempotencyEntry { /** * Operation namespace. */ namespace: string; /** * Client-provided idempotency key. */ key: string; /** * Normalized scope key. */ scopeKey: string; /** * Fingerprint of the logical command payload. */ fingerprint: string; /** Opaque identity of the current in-progress reservation. */ reservationToken: string; /** * Memory store status. */ status: "in-progress" | "completed"; /** * Stored result for completed entries. */ result?: unknown; /** * Reservation timestamp. */ reservedAt: Date; /** * Completion timestamp. */ completedAt?: Date; /** * Expiration timestamp, or null for no expiration. */ expiresAt: Date | null; } /** * Error thrown when an idempotency key is reused with a different fingerprint. */ export declare class IdempotencyConflictError extends Error { readonly namespace: string; readonly key: string; readonly scopeKey: string; readonly storedFingerprint: string; readonly receivedFingerprint: string; constructor(args: { namespace: string; key: string; scopeKey: string; storedFingerprint: string; receivedFingerprint: string; }); } /** * Error thrown when an idempotency key is already reserved by in-progress work. */ export declare class IdempotencyInProgressError extends Error { readonly namespace: string; readonly key: string; readonly scopeKey: string; constructor(args: { namespace: string; key: string; scopeKey: string; }); } /** * Error thrown when replay is disabled for a completed idempotency key. */ export declare class IdempotencyReplayError extends Error { readonly namespace: string; readonly key: string; readonly scopeKey: string; constructor(args: { namespace: string; key: string; scopeKey: string; }); } /** * Error thrown when fingerprint input cannot be canonicalized. */ export declare class IdempotencyFingerprintError extends Error { constructor(message: string); } /** * Error thrown when a completion or failure mutation no longer owns the * matching in-progress reservation. */ export declare class IdempotencyMutationError extends Error { /** Mutation that failed to match the current reservation. */ readonly action: "complete" | "fail"; /** Operation namespace supplied to the failed mutation. */ readonly namespace: string; /** Client-provided idempotency key supplied to the failed mutation. */ readonly key: string; /** Normalized logical scope key supplied to the failed mutation. */ readonly scopeKey: string; constructor(args: { action: "complete" | "fail"; namespace: string; key: string; scope?: IdempotencyScope; }); } /** * Normalize an idempotency scope into a stable string. */ export declare function normalizeIdempotencyScope(scope: IdempotencyScope | undefined): string; /** * Create the stable storage key for an idempotency operation. */ export declare function createIdempotencyStorageKey(input: { namespace: string; key: string; scope?: IdempotencyScope; }): string; /** * Options for `createMemoryIdempotencyStore(...)`. */ export interface MemoryIdempotencyStoreOptions { /** * Clock used for reservation and completion timestamps. Defaults to the * system clock. */ now?: () => Date; /** Token factory used for deterministic tests. */ createReservationToken?: () => string; } /** * Create an in-memory idempotency store for tests and local examples. * * The memory store is process-local and not suitable for multi-process * production deployments. */ export declare function createMemoryIdempotencyStore(options?: MemoryIdempotencyStoreOptions): MemoryIdempotencyStore; /** * Run an operation behind an idempotency reservation. * * The flow is: reserve the key, replay completed matching results, reject * in-progress/conflicting keys, run the operation for new reservations, then * complete or fail the reservation. Callers are responsible for choosing a * namespace, scope, key, and fingerprint that match their business operation. */ export declare function runIdempotently<Result>(idempotency: IdempotencyPort, options: RunIdempotentlyOptions<Result>): Promise<Result>; /** * Create a SHA-256 fingerprint from a canonicalized value. * * Object keys are sorted, `undefined` and functions are omitted, `Date` values * become ISO strings, BigInts become strings, and circular or non-finite values * throw. Exact omit paths may be supplied as dotted strings or string arrays. */ export declare function createIdempotencyFingerprint(value: unknown, options?: CreateIdempotencyFingerprintOptions): Promise<string>; //# sourceMappingURL=index.d.ts.map