Files
suaveplan/openspec/specs/idempotency/spec.md
2026-05-27 23:29:53 +01:00

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:

  • IdempotencyStatusSchema
  • IdempotencyRecordSchema
  • IdempotencyGuardConfigSchema
  • DistributedGuardConfigSchema
  • KeyGeneratorConfigSchema
  • KeyVersioningConfigSchema
  • IdempotencyMetricsConfigSchema
  • IdempotencyMiddlewareConfigSchema

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:

  • compositeKey output is deterministic and stable for equal inputs across arbitrary string arrays.
  • fingerprintPayload produces distinct hex strings for structurally distinct payloads with overwhelming probability (collision resistance property).
  • generateIdempotencyKey never produces duplicate keys across N calls when supplied a deterministic RNG.
  • IdempotencyGuard.execute never invokes fn more than once for the same key when called concurrently from multiple coroutines.
  • KeyVersioning never retains more than maxVersions entries 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.get hot path — 10 000 cached reads.
  • IdempotencyGuard.execute with cached hit — zero-execution overhead.
  • compositeKey throughput — 1 000 000 iterations.
  • fingerprintPayload throughput — 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.