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:observabilitytslib→catalog:build
Dev deps (via catalog):
vitest,@vitest/coverage-istanbul→catalog:testingfast-check→catalog:testingmitata→catalog:testing
Preserved public API surface (from main branch)
Distributed
DistributedIdempotencyGuardclass — distributed-safe guard backed by aLockProviderLockAcquisitionErrorextendsSuaveplanErrorMemoryLockProvider— in-process lock provider for unit tests and single-node deploymentsRedisLockProvider— Redis-backed lock provider with lease/refresh semanticsRedisLockProviderConfig— configuration type forRedisLockProvider
Guard
IdempotencyConflictErrorextendsSuaveplanError— thrown when a pending record is detected for the same keyIdempotencyGuardclass — single-node guard with state machine and pluggable storeIdempotencyMismatchErrorextendsSuaveplanError— thrown when the request fingerprint differs from the stored record
Key generation
compositeKey(...parts: string[]) → string— assembles a namespaced key from ordered partsfingerprintPayload(input: unknown, hashFn?) → string— deterministic hex fingerprint of arbitrary inputgenerateIdempotencyKey(config?: KeyGeneratorConfig) → string— generates a random, optionally prefixed key
Error codes
IdempotencyErrorCode— union type of all stable error code stringsIDEMPOTENCY_ERROR_METADATA—Record<IdempotencyErrorCode, CodeMetadata>mappingregisterIdempotencyErrorCodes()— registers all codes on the sharedErrorCodessingleton
Metrics
IdempotencyMetricsclass — collects and exposes a rollingIdempotencyMetricsSnapshot
Middleware
createIdempotencyMiddleware<TReq, TRes>(config)→ middleware handler functionMiddlewareResult<TRes>— discriminated union returned by the middleware (hit | miss | conflict)
Store adapters
IdempotencyStoreAdapter<T>— interface:get,set,delete,cleanupIdempotencyDatabaseAdapter— interface for plugging in a raw database clientRedisClientAdapter— interface for plugging in a raw Redis clientMemoryIdempotencyStore— in-process TTL store, GC viacleanup()DatabaseIdempotencyStore— adapter-backed persistent storeDatabaseIdempotencyStoreConfig— configuration typeRedisIdempotencyStore— Redis-backed store with atomic set-if-not-existsRedisIdempotencyStoreConfig— configuration type
Types
ConflictResolutionStrategy—'reject' | 'overwrite' | 'version'DistributedGuardConfig— configuration forDistributedIdempotencyGuardIdempotencyGuardConfig— configuration forIdempotencyGuardIdempotencyMetricsConfig— configuration forIdempotencyMetricsIdempotencyMetricsSnapshot— point-in-time metrics snapshot shapeIdempotencyMiddlewareConfig<TReq, TRes>— configuration for middlewareIdempotencyRecord<T>— stored record shape with status, response, fingerprint, timestampsIdempotencyStatus—'pending' | 'completed' | 'failed'IdempotencyStoreAdapter<T>— store interfaceKeyGeneratorConfig— key prefix + separator optionsKeyVersioningConfig— store + strategy + maxVersions optionsLockProvider— interface:acquire(resource, ttlMs),release(resource)MetricsReporter— callback type(snapshot: IdempotencyMetricsSnapshot) => voidVersionEntry<T>— versioned record snapshot
Versioning
KeyVersioningclass — multi-version record management with configurable conflict strategyVersionConflictErrorextendsSuaveplanError— thrown when version limit is exceeded
New additions for genesis
Structured errors
IdempotencyErrorbase class extendsSuaveplanErrorwith codes underIDEMPOTENCY.*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,VersionConflictErrorall extendIdempotencyError
Zod schemas
IdempotencyGuardConfigSchema,DistributedGuardConfigSchema,KeyGeneratorConfigSchema,KeyVersioningConfigSchema,IdempotencyMetricsConfigSchema,IdempotencyMiddlewareConfigSchemaIdempotencyRecordSchema,IdempotencyStatusSchema
Injectable clock + RNG
IdempotencyGuardConfig.clock?: Clock— for deterministic TTL and timeout computation (FakeClock in tests)IdempotencyGuardConfig.rng?: Rng— for deterministic key generationDistributedGuardConfig.clock?: ClockKeyGeneratorConfig.rng?: Rng
AbortSignal support
IdempotencyGuard.execute(key, fn, opts?: { signal? })— cancels pending-record wait on abortDistributedIdempotencyGuard.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)
compositeKeyoutput is always deterministic for equal inputsfingerprintPayloadis collision-resistant: distinct inputs produce distinct hashes across arbitrary payloadsgenerateIdempotencyKeynever produces duplicate outputs across N iterations with a mocked RNGIdempotencyGuard.executenever executesfnmore than once for the same key when called concurrentlyKeyVersioningnever exceedsmaxVersionsstored entries regardless of call order
Benchmarks (mitata)
MemoryIdempotencyStore.gethot path — 10 000 cached readsIdempotencyGuard.executewith cached hit — zero-execution overheadcompositeKeythroughput — 1 000 000 iterationsfingerprintPayloadthroughput — 10 000 small payloads
Notes on integration testing
RedisIdempotencyStoreandRedisLockProviderrequire a real Redis instance. Integration tests use aredis://localhost:6379connection; no mock Redis. Tests are placed intests/integration/and are guarded by aTEST_REDIS_URLenvironment variable.DistributedIdempotencyGuardis unit-tested withMemoryLockProvideronly. Integration tests covering the full distributed path with Redis are intests/integration/.DatabaseIdempotencyStoreintegration tests requireTEST_DATABASE_URLenvironment variable; unit tests useMemoryIdempotencyStoreas a stand-in.
Impact
- Adds one new package:
packages/universal/idempotency - Adds one new spec capability:
idempotency - No existing package consumers on genesis yet