# 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` mapping - `registerIdempotencyErrorCodes()` — registers all codes on the shared `ErrorCodes` singleton ### Metrics - `IdempotencyMetrics` class — collects and exposes a rolling `IdempotencyMetricsSnapshot` ### Middleware - `createIdempotencyMiddleware(config)` → middleware handler function - `MiddlewareResult` — discriminated union returned by the middleware (`hit | miss | conflict`) ### Store adapters - `IdempotencyStoreAdapter` — 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` — configuration for middleware - `IdempotencyRecord` — stored record shape with status, response, fingerprint, timestamps - `IdempotencyStatus` — `'pending' | 'completed' | 'failed'` - `IdempotencyStoreAdapter` — 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` — 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