Files
2026-05-27 23:29:53 +01:00

8.9 KiB

OpenSpec Change: enterprise-idempotency

  • Change ID: enterprise-idempotency
  • Package: @suaveplan/idempotency
  • Category: universal
  • Target path: packages/universal/idempotency
  • Status: PROPOSED
  • Date: 2026-04-19
  • Reference spec: /mnt/s/web/suaveplan/enterprise/idempotency.md

Summary

Greenfield implementation of @suaveplan/idempotency for the genesis branch. The existing main-branch idempotency package (98.68% test coverage) provides the baseline public-API surface to preserve; this change delivers enterprise-grade idempotency key guarding and deduplication with a full state machine (pending → completed | failed), pluggable store adapters (memory, database, Redis), distributed locking via a LockProvider interface, HTTP middleware, key generation and fingerprinting, key versioning, metrics collection, injectable clock and RNG, AbortSignal support throughout, structured errors with stable IDEMPOTENCY.* codes, Zod schemas, OTel instrumentation, property-based tests, and benchmarks.

Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.

Available workspace deps on genesis

Package Available
@suaveplan/error ✅
@suaveplan/types ✅
@suaveplan/events ✅
@suaveplan/crypto-utils ✅
@suaveplan/crypto-hash ✅
@suaveplan/crypto-curve ✅
@suaveplan/crypto-cipher ✅
@suaveplan/crypto ✅
@suaveplan/crypto-keys ✅
@suaveplan/logger ✅
@suaveplan/opentelemetry ❌ not yet built — use @opentelemetry/api directly
@suaveplan/schema-registry ❌ not yet built — export Zod schemas directly
@suaveplan/streams ❌ not yet built — skip streaming features
@suaveplan/feature-flags ❌ not yet built — skip
@suaveplan/retry ❌ in progress — skip retry integration for now

External deps (via catalog):

  • zod → catalog:forms
  • @opentelemetry/api → catalog:observability
  • tslib → catalog:build

Dev deps (via catalog):

  • vitest, @vitest/coverage-istanbul → catalog:testing
  • fast-check → catalog:testing
  • mitata → catalog:testing

Preserved public API surface (from main branch)

Distributed

  • DistributedIdempotencyGuard class — distributed-safe guard backed by a LockProvider
  • LockAcquisitionError extends SuaveplanError
  • MemoryLockProvider — in-process lock provider for unit tests and single-node deployments
  • RedisLockProvider — Redis-backed lock provider with lease/refresh semantics
  • RedisLockProviderConfig — configuration type for RedisLockProvider

Guard

  • IdempotencyConflictError extends SuaveplanError — thrown when a pending record is detected for the same key
  • IdempotencyGuard class — single-node guard with state machine and pluggable store
  • IdempotencyMismatchError extends SuaveplanError — thrown when the request fingerprint differs from the stored record

Key generation

  • compositeKey(...parts: string[]) → string — assembles a namespaced key from ordered parts
  • fingerprintPayload(input: unknown, hashFn?) → string — deterministic hex fingerprint of arbitrary input
  • generateIdempotencyKey(config?: KeyGeneratorConfig) → string — generates a random, optionally prefixed key

Error codes

  • IdempotencyErrorCode — union type of all stable error code strings
  • IDEMPOTENCY_ERROR_METADATA — Record<IdempotencyErrorCode, CodeMetadata> mapping
  • registerIdempotencyErrorCodes() — registers all codes on the shared ErrorCodes singleton

Metrics

  • IdempotencyMetrics class — collects and exposes a rolling IdempotencyMetricsSnapshot

Middleware

  • createIdempotencyMiddleware<TReq, TRes>(config) → middleware handler function
  • MiddlewareResult<TRes> — discriminated union returned by the middleware (hit | miss | conflict)

Store adapters

  • IdempotencyStoreAdapter<T> — interface: get, set, delete, cleanup
  • IdempotencyDatabaseAdapter — interface for plugging in a raw database client
  • RedisClientAdapter — interface for plugging in a raw Redis client
  • MemoryIdempotencyStore — in-process TTL store, GC via cleanup()
  • DatabaseIdempotencyStore — adapter-backed persistent store
  • DatabaseIdempotencyStoreConfig — configuration type
  • RedisIdempotencyStore — Redis-backed store with atomic set-if-not-exists
  • RedisIdempotencyStoreConfig — configuration type

Types

  • ConflictResolutionStrategy — 'reject' | 'overwrite' | 'version'
  • DistributedGuardConfig — configuration for DistributedIdempotencyGuard
  • IdempotencyGuardConfig — configuration for IdempotencyGuard
  • IdempotencyMetricsConfig — configuration for IdempotencyMetrics
  • IdempotencyMetricsSnapshot — point-in-time metrics snapshot shape
  • IdempotencyMiddlewareConfig<TReq, TRes> — configuration for middleware
  • IdempotencyRecord<T> — stored record shape with status, response, fingerprint, timestamps
  • IdempotencyStatus — 'pending' | 'completed' | 'failed'
  • IdempotencyStoreAdapter<T> — store interface
  • KeyGeneratorConfig — key prefix + separator options
  • KeyVersioningConfig — store + strategy + maxVersions options
  • LockProvider — interface: acquire(resource, ttlMs), release(resource)
  • MetricsReporter — callback type (snapshot: IdempotencyMetricsSnapshot) => void
  • VersionEntry<T> — versioned record snapshot

Versioning

  • KeyVersioning class — multi-version record management with configurable conflict strategy
  • VersionConflictError extends SuaveplanError — thrown when version limit is exceeded

New additions for genesis

Structured errors

  • IdempotencyError base class extends SuaveplanError with codes under IDEMPOTENCY.* namespace
  • Codes: IDEMPOTENCY.KEY_CONFLICT, IDEMPOTENCY.RESPONSE_MISMATCH, IDEMPOTENCY.STORE_ERROR, IDEMPOTENCY.KEY_EXPIRED, IDEMPOTENCY.KEY_INVALID, IDEMPOTENCY.LOCK_ACQUISITION_FAILED, IDEMPOTENCY.VERSION_CONFLICT
  • IdempotencyConflictError, IdempotencyMismatchError, LockAcquisitionError, VersionConflictError all extend IdempotencyError

Zod schemas

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

Injectable clock + RNG

  • IdempotencyGuardConfig.clock?: Clock — for deterministic TTL and timeout computation (FakeClock in tests)
  • IdempotencyGuardConfig.rng?: Rng — for deterministic key generation
  • DistributedGuardConfig.clock?: Clock
  • KeyGeneratorConfig.rng?: Rng

AbortSignal support

  • IdempotencyGuard.execute(key, fn, opts?: { signal? }) — cancels pending-record wait on abort
  • DistributedIdempotencyGuard.execute(key, fn, opts?: { signal? }) — propagates signal through lock acquisition retries and fn invocation

OTel instrumentation

  • Counter: idempotency.request.total (by outcome label: hit | miss | conflict | mismatch | error)
  • Histogram: idempotency.execution.duration_ms (guard execution latency)
  • Counter: idempotency.lock.acquisition.total (by result: acquired | failed | retried)
  • Histogram: idempotency.lock.wait_ms (time spent acquiring a distributed lock)
  • Counter: idempotency.store.operation.total (by operation: get | set | delete | cleanup)

Property-based tests (fast-check)

  • compositeKey output is always deterministic for equal inputs
  • fingerprintPayload is collision-resistant: distinct inputs produce distinct hashes across arbitrary payloads
  • generateIdempotencyKey never produces duplicate outputs across N iterations with a mocked RNG
  • IdempotencyGuard.execute never executes fn more than once for the same key when called concurrently
  • KeyVersioning never exceeds maxVersions stored entries regardless of call order

Benchmarks (mitata)

  • 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

Notes on integration testing

  • RedisIdempotencyStore and RedisLockProvider require a real Redis instance. Integration tests use a redis://localhost:6379 connection; no mock Redis. Tests are placed in tests/integration/ and are guarded by a TEST_REDIS_URL environment variable.
  • DistributedIdempotencyGuard is unit-tested with MemoryLockProvider only. Integration tests covering the full distributed path with Redis are in tests/integration/.
  • DatabaseIdempotencyStore integration tests require TEST_DATABASE_URL environment variable; unit tests use MemoryIdempotencyStore as a stand-in.

Impact

  • Adds one new package: packages/universal/idempotency
  • Adds one new spec capability: idempotency
  • No existing package consumers on genesis yet