9.5 KiB
idempotency
Purpose
Idempotency key guard and dedup for the Suaveplan monorepo — pluggable store adapters (in-memory, database, Redis), distributed locking via LockProvider, request middleware, key generation utilities (compositeKey, fingerprintPayload), key versioning, and OTel instrumentation. All errors flow through IdempotencyError extending SuaveplanError, with Zod schemas for every public configuration type and AbortSignal threaded through every async path.
Requirements
All requirements in this section are new for genesis. The main-branch package provides the baseline public-API surface; everything below is either a carry-over preserved verbatim or a genesis-specific enhancement.
REQ-IDMP-001 — Structured error hierarchy
The package MUST define an IdempotencyError base class that extends SuaveplanError from @suaveplan/error. All error types thrown by the package MUST extend IdempotencyError. Error codes MUST follow the IDEMPOTENCY.* dotted namespace and be registered with defineCodes / registerIdempotencyErrorCodes() on the shared ErrorCodes singleton.
Required codes:
| Code | HTTP | Severity | Retryable |
|---|---|---|---|
IDEMPOTENCY.KEY_CONFLICT |
409 | warning | false |
IDEMPOTENCY.RESPONSE_MISMATCH |
409 | error | false |
IDEMPOTENCY.STORE_ERROR |
503 | error | true (exponential, max 3) |
IDEMPOTENCY.KEY_EXPIRED |
410 | warning | false |
IDEMPOTENCY.KEY_INVALID |
400 | warning | false |
IDEMPOTENCY.LOCK_ACQUISITION_FAILED |
503 | error | true |
IDEMPOTENCY.VERSION_CONFLICT |
409 | warning | false |
REQ-IDMP-002 — Zod schemas for all public config types
Every public configuration and record type MUST have a corresponding Zod schema exported from the package. Schemas must be usable for runtime validation at the boundary (e.g. HTTP middleware deserialization). Schema exports:
IdempotencyStatusSchemaIdempotencyRecordSchemaIdempotencyGuardConfigSchemaDistributedGuardConfigSchemaKeyGeneratorConfigSchemaKeyVersioningConfigSchemaIdempotencyMetricsConfigSchemaIdempotencyMiddlewareConfigSchema
REQ-IDMP-003 — Injectable clock and RNG
No public API may call Date.now() or Math.random() directly. Every time-dependent and random-dependent operation MUST accept an optional clock?: Clock and rng?: Rng (from @suaveplan/types) so tests can run deterministically with FakeClock and a seeded RNG. Affected components: IdempotencyGuard, DistributedIdempotencyGuard, IdempotencyMetrics, generateIdempotencyKey.
REQ-IDMP-004 — AbortSignal support throughout
Every async public method that performs IO or waits on an internal lock MUST accept an optional { signal?: AbortSignal } option. On abort, pending waits MUST reject with a DOMException named AbortError (or an IdempotencyError wrapping it). The signal MUST be propagated to the user-supplied fn where possible. Affected: IdempotencyGuard.execute, DistributedIdempotencyGuard.execute.
REQ-IDMP-005 — OTel instrumentation via @opentelemetry/api
The package MUST instrument all externally-visible operations using @opentelemetry/api meters. Instrumentation MUST be a no-op when no MeterProvider is registered. Required instruments:
| Instrument | Type | Labels |
|---|---|---|
idempotency.request.total |
Counter | outcome: hit | miss | conflict | mismatch | error |
idempotency.execution.duration_ms |
Histogram | — |
idempotency.lock.acquisition.total |
Counter | result: acquired | failed | retried |
idempotency.lock.wait_ms |
Histogram | — |
idempotency.store.operation.total |
Counter | operation: get | set | delete | cleanup |
REQ-IDMP-006 — Property-based tests with fast-check
The following properties MUST be verified with fast-check arbitraries:
compositeKeyoutput is deterministic and stable for equal inputs across arbitrary string arrays.fingerprintPayloadproduces distinct hex strings for structurally distinct payloads with overwhelming probability (collision resistance property).generateIdempotencyKeynever produces duplicate keys across N calls when supplied a deterministic RNG.IdempotencyGuard.executenever invokesfnmore than once for the same key when called concurrently from multiple coroutines.KeyVersioningnever retains more thanmaxVersionsentries regardless of call ordering.
REQ-IDMP-007 — Benchmarks with mitata
A benchmark suite MUST be maintained under src/__benchmarks__/. Baseline numbers MUST be published in README.md. Required benchmarks:
MemoryIdempotencyStore.gethot path — 10 000 cached reads.IdempotencyGuard.executewith cached hit — zero-execution overhead.compositeKeythroughput — 1 000 000 iterations.fingerprintPayloadthroughput — 10 000 small payloads.
REQ-IDMP-008 — Store adapter extensibility
IdempotencyStoreAdapter<T> MUST remain a structural interface (not a class) so that downstream packages can provide custom implementations without inheriting from a base class. The four required methods are: get(key) → Promise<IdempotencyRecord<T> | undefined>, set(key, record) → Promise<void>, delete(key) → Promise<boolean>, cleanup(beforeTimestamp) → Promise<number>.
Similarly, LockProvider MUST remain a structural interface with acquire(resource, ttlMs) → Promise<boolean> and release(resource) → Promise<void>. Implementations (memory, Redis) are provided by the package as conveniences, not as required base classes.
REQ-IDMP-009 — Real services for integration tests; no mock stores in unit tests
MemoryIdempotencyStore and MemoryLockProvider are full real implementations and MUST be used directly in unit tests — not as mocks. RedisIdempotencyStore, RedisLockProvider, and DatabaseIdempotencyStore MUST be tested against real services. Integration tests MUST be placed in tests/integration/ and MUST be guarded by environment variables (TEST_REDIS_URL, TEST_DATABASE_URL). No vi.mock or jest.mock of store or lock provider internals is permitted.
REQ-IDMP-010 — 100% test coverage (lines / statements / branches / functions)
As a Layer 1 foundation package, @suaveplan/idempotency MUST achieve 100/100/100/100 coverage (Istanbul provider). The coverage threshold MUST be enforced in vitest.config.ts. The bunx turbo test:coverage --filter=@suaveplan/idempotency command MUST exit non-zero if any threshold is not met.
Public Exports
Classes
| Export | Module |
|---|---|
IdempotencyGuard |
src/guard/idempotency-guard |
DistributedIdempotencyGuard |
src/distributed/distributed-guard |
MemoryLockProvider |
src/distributed/memory-lock-provider |
RedisLockProvider |
src/distributed/redis-lock-provider |
MemoryIdempotencyStore |
src/store/memory-store |
DatabaseIdempotencyStore |
src/store/database-store |
RedisIdempotencyStore |
src/store/redis-store |
KeyVersioning |
src/versioning/key-versioning |
IdempotencyMetrics |
src/metrics/idempotency-metrics |
Error classes
| Export | Extends |
|---|---|
IdempotencyError |
SuaveplanError |
IdempotencyConflictError |
IdempotencyError |
IdempotencyMismatchError |
IdempotencyError |
LockAcquisitionError |
IdempotencyError |
VersionConflictError |
IdempotencyError |
Functions
| Export | Signature |
|---|---|
compositeKey |
(...parts: string[]) → string |
fingerprintPayload |
(input: unknown, hashFn?: (data: Uint8Array) => string) → string |
generateIdempotencyKey |
(config?: KeyGeneratorConfig) → string |
createIdempotencyMiddleware |
<TReq, TRes>(config: IdempotencyMiddlewareConfig<TReq, TRes>) → MiddlewareHandler |
registerIdempotencyErrorCodes |
() → void |
Types
| Export | Kind |
|---|---|
IdempotencyStatus |
type alias |
ConflictResolutionStrategy |
type alias |
IdempotencyRecord<T> |
interface |
IdempotencyStoreAdapter<T> |
interface |
IdempotencyDatabaseAdapter |
interface |
RedisClientAdapter |
interface |
LockProvider |
interface |
IdempotencyGuardConfig |
interface |
DistributedGuardConfig |
interface |
DatabaseIdempotencyStoreConfig |
interface |
RedisIdempotencyStoreConfig |
interface |
RedisLockProviderConfig |
interface |
KeyGeneratorConfig |
interface |
KeyVersioningConfig |
interface |
IdempotencyMetricsConfig |
interface |
IdempotencyMetricsSnapshot |
interface |
MetricsReporter |
type alias |
MiddlewareResult<TRes> |
type alias (discriminated union) |
VersionEntry<T> |
interface |
IdempotencyErrorCode |
type alias (union) |
Constants / registries
| Export | Kind |
|---|---|
IDEMPOTENCY_ERROR_METADATA |
Readonly<Record<IdempotencyErrorCode, CodeMetadata>> |
Zod schemas
| Export | Validates |
|---|---|
IdempotencyStatusSchema |
IdempotencyStatus |
IdempotencyRecordSchema |
IdempotencyRecord |
IdempotencyGuardConfigSchema |
IdempotencyGuardConfig |
DistributedGuardConfigSchema |
DistributedGuardConfig |
KeyGeneratorConfigSchema |
KeyGeneratorConfig |
KeyVersioningConfigSchema |
KeyVersioningConfig |
IdempotencyMetricsConfigSchema |
IdempotencyMetricsConfig |
IdempotencyMiddlewareConfigSchema |
IdempotencyMiddlewareConfig |
MODIFIED Requirements
None — this is a greenfield package on genesis; there are no prior spec entries to modify.
REMOVED Requirements
None.