@beignet/core
Version:
Core framework primitives for Beignet
418 lines • 12.2 kB
TypeScript
/**
* @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