Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# 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

View File

@@ -0,0 +1,186 @@
# Capability Spec: idempotency
- **Capability**: `idempotency`
- **Package**: `@suaveplan/idempotency`
- **Change**: enterprise-idempotency
- **Date**: 2026-04-19
---
## ADDED 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.

View File

@@ -0,0 +1,132 @@
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/control-flow/idempotency.
# Tasks: enterprise-idempotency
## 1. Scaffold
- [x] 1.1 Create `packages/universal/idempotency/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
## 2. Types + errors
- [x] 2.1 Implement `src/types/idempotency.types.ts` — `IdempotencyStatus`, `IdempotencyRecord<T>`, `ConflictResolutionStrategy`, `IdempotencyStoreAdapter<T>`, `IdempotencyGuardConfig`, `DistributedGuardConfig`, `KeyGeneratorConfig`, `KeyVersioningConfig`, `IdempotencyMetricsConfig`, `IdempotencyMetricsSnapshot`, `MetricsReporter`, `IdempotencyMiddlewareConfig<TReq, TRes>`, `MiddlewareResult<TRes>`, `LockProvider`, `VersionEntry<T>`
- [x] 2.2 Implement `src/errors/idempotency-error.ts` — `IdempotencyError` base extends `SuaveplanError`; `IdempotencyConflictError`, `IdempotencyMismatchError`, `LockAcquisitionError`, `VersionConflictError` extend `IdempotencyError`
- [x] 2.3 Implement `src/error-codes.ts` — `IdempotencyErrorCode` union type, `IDEMPOTENCY_ERROR_METADATA`, `registerIdempotencyErrorCodes()`; codes: `IDEMPOTENCY.KEY_CONFLICT`, `IDEMPOTENCY.RESPONSE_MISMATCH`, `IDEMPOTENCY.STORE_ERROR`, `IDEMPOTENCY.KEY_EXPIRED`, `IDEMPOTENCY.KEY_INVALID`, `IDEMPOTENCY.LOCK_ACQUISITION_FAILED`, `IDEMPOTENCY.VERSION_CONFLICT`
- [x] 2.4 Zod schemas: `IdempotencyStatusSchema`, `IdempotencyRecordSchema`, `IdempotencyGuardConfigSchema`, `DistributedGuardConfigSchema`, `KeyGeneratorConfigSchema`, `KeyVersioningConfigSchema`, `IdempotencyMetricsConfigSchema`, `IdempotencyMiddlewareConfigSchema`
- [x] 2.5 Tests: error construction, code narrowing, field presence on each error subclass, Zod schema round-trips
- [x] 2.6 Auto-register error codes via side-effect import in `src/index.ts`
## 3. Key generation
- [x] 3.1 Implement `src/key/key-generator.ts`:
- `compositeKey(...parts: string[]) → string` — joins parts with separator, rejects empty parts
- `fingerprintPayload(input: unknown, hashFn?: (data: Uint8Array) => string) → string` — deterministic hex fingerprint; uses `@suaveplan/crypto-hash` sha256 by default, accepts injectable hash function
- `generateIdempotencyKey(config?: KeyGeneratorConfig) → string` — random key with optional prefix; accepts injectable `rng?: Rng`
- [x] 3.2 Unit tests: compositeKey separator/prefix, fingerprintPayload determinism, generateIdempotencyKey uniqueness and prefix formatting
- [x] 3.3 Property-based tests: `compositeKey` output is deterministic for equal inputs; `fingerprintPayload` produces distinct results for distinct payloads; `generateIdempotencyKey` never duplicates across N calls with mocked RNG
- [x] 3.4 Co-located `src/key/key-generator.md`
## 4. Store adapters
- [x] 4.1 Implement `src/store/adapter.types.ts` — `IdempotencyDatabaseAdapter` interface, `RedisClientAdapter` interface
- [x] 4.2 Implement `src/store/memory-store.ts` — `MemoryIdempotencyStore`: TTL-aware in-process map; `cleanup(beforeTimestamp)` removes expired records; thread-safe for single-process concurrency; `size` getter
- [x] 4.3 Implement `src/store/database-store.ts` — `DatabaseIdempotencyStore` and `DatabaseIdempotencyStoreConfig`: wraps `IdempotencyDatabaseAdapter`; maps store interface to adapter calls
- [x] 4.4 Implement `src/store/redis-store.ts` — `RedisIdempotencyStore` and `RedisIdempotencyStoreConfig`: uses `RedisClientAdapter`; atomic `SET NX PX` for `set`; JSON serialization
- [x] 4.5 Unit tests for `MemoryIdempotencyStore`: get/set/delete/cleanup, TTL expiry, concurrent sets
- [x] 4.6 Integration tests for `RedisIdempotencyStore` in `tests/integration/` guarded by `TEST_REDIS_URL`
- [x] 4.7 Integration tests for `DatabaseIdempotencyStore` in `tests/integration/` guarded by `TEST_DATABASE_URL`
- [x] 4.8 Co-located `.md` for each store implementation
## 5. Guard
- [x] 5.1 Implement `src/guard/idempotency-guard.ts` — `IdempotencyGuard` class:
- `execute<T>(key, fn, opts?: { signal?: AbortSignal }) → Promise<T>`
- State machine: `pending → completed | failed`; returns cached response on hit; throws `IdempotencyConflictError` when pending; throws `IdempotencyMismatchError` on fingerprint mismatch
- `lockTimeoutMs` governs how long a pending record blocks re-execution
- `fingerprintFn` is injectable; defaults to `fingerprintPayload`
- `clock?: Clock` for deterministic timeout computation
- [x] 5.2 Honour `AbortSignal`: cancel pending-record wait on abort; propagate signal to `fn`
- [x] 5.3 Unit tests: cache hit, cache miss, conflict (pending in-flight), mismatch, failed-record retry, TTL expiry, clock injection, signal abort, `fingerprintFn` injection
- [x] 5.4 Property-based tests: `execute` never invokes `fn` more than once for the same key under concurrent callers
- [x] 5.5 Co-located `src/guard/idempotency-guard.md`
## 6. Distributed guard
- [x] 6.1 Implement `src/distributed/distributed-guard.ts` — `DistributedIdempotencyGuard` class:
- `execute<T>(key, fn, opts?: { signal?: AbortSignal }) → Promise<T>`
- Acquires lock via `LockProvider` before checking / writing store; retries on contention up to `retryCount` with `retryBaseDelayMs` exponential back-off
- Releases lock in `finally`; marks record `failed` on `fn` error
- `clock?: Clock` for injectable back-off timing
- [x] 6.2 Implement `src/distributed/memory-lock-provider.ts` — `MemoryLockProvider`: in-process TTL-aware lock map; safe for single-node and unit tests
- [x] 6.3 Implement `src/distributed/redis-lock-provider.ts` — `RedisLockProvider` and `RedisLockProviderConfig`: acquire via `SET NX PX`, release via Lua script; no Redlock dependency
- [x] 6.4 Unit tests for `DistributedIdempotencyGuard` with `MemoryLockProvider`: acquire/release lifecycle, retry on contention, abort signal, fn-error cleanup
- [x] 6.5 Integration tests for `RedisLockProvider` and the full distributed guard path in `tests/integration/` guarded by `TEST_REDIS_URL`
- [x] 6.6 Co-located `.md` for each distributed implementation file
## 7. Middleware
- [x] 7.1 Implement `src/middleware/idempotency-middleware.ts` — `createIdempotencyMiddleware<TReq, TRes>(config)`:
- Returns a handler function `(request: TReq, next: () => Promise<TRes>) → Promise<MiddlewareResult<TRes>>`
- On key absence: calls `next()` directly (passthrough)
- On cache hit: returns `{ type: 'hit', response }`
- On cache miss: executes `next()`, stores result, returns `{ type: 'miss', response }`
- On conflict or mismatch: returns `{ type: 'conflict', error }` or rethrows per `ConflictResolutionStrategy`
- Per-operation TTL overrides via `ttlOverrides` + `extractOperation`
- Injectable `fingerprintFn` and `transformResponse`
- [x] 7.2 Unit tests: hit path, miss path, conflict path, mismatch path, missing key passthrough, TTL override, transformResponse, fingerprintFn injection
- [x] 7.3 Co-located `src/middleware/idempotency-middleware.md`
## 8. Key versioning
- [x] 8.1 Implement `src/versioning/key-versioning.ts` — `KeyVersioning` class:
- `record<T>(key, fingerprint, response?, status?) → Promise<VersionEntry<T>>`
- `get<T>(key, version?) → Promise<VersionEntry<T> | undefined>` — latest or specific version
- `list<T>(key) → Promise<VersionEntry<T>[]>` — all retained versions
- Prunes oldest entries when `maxVersions` is exceeded
- Conflict strategy: `reject` throws `VersionConflictError`; `overwrite` replaces; `version` appends
- [x] 8.2 Unit tests: reject strategy, overwrite strategy, version strategy, maxVersions pruning, get by version, list
- [x] 8.3 Property-based tests: `KeyVersioning` never exceeds `maxVersions` stored entries regardless of call order
- [x] 8.4 Co-located `src/versioning/key-versioning.md`
## 9. Metrics
- [x] 9.1 Implement `src/metrics/idempotency-metrics.ts` — `IdempotencyMetrics` class:
- `record(event: MetricsEvent)` — increments counters and records durations
- `snapshot() → IdempotencyMetricsSnapshot` — computes rolling averages over `windowMs`
- `startPeriodicReporting(reporter, intervalMs)` — calls `reporter(snapshot())` on interval; returns `stopFn`
- `reset()` — clears all counters
- Injectable `clock?: Clock`
- [x] 9.2 Unit tests: counter increments, hit rate computation, avgExecutionDurationMs, rolling window cutoff, periodic reporting via FakeClock, reset
- [x] 9.3 Co-located `src/metrics/idempotency-metrics.md`
## 10. OTel instrumentation
- [x] 10.1 Counter `idempotency.request.total` by outcome label (`hit | miss | conflict | mismatch | error`)
- [x] 10.2 Histogram `idempotency.execution.duration_ms` (guard execution latency)
- [x] 10.3 Counter `idempotency.lock.acquisition.total` by result (`acquired | failed | retried`)
- [x] 10.4 Histogram `idempotency.lock.wait_ms` (time spent acquiring a distributed lock)
- [x] 10.5 Counter `idempotency.store.operation.total` by operation (`get | set | delete | cleanup`)
- [x] 10.6 Tests: verify no-op without exporter; verify counter increments with a test meter provider
## 11. Benchmarks
- [x] 11.1 `src/__benchmarks__/memory-store.bench.ts` — `MemoryIdempotencyStore.get` hot path, 10 000 cached reads (mitata)
- [x] 11.2 `src/__benchmarks__/guard.bench.ts` — `IdempotencyGuard.execute` with cached hit, zero-execution overhead
- [x] 11.3 `src/__benchmarks__/key-generator.bench.ts` — `compositeKey` throughput 1 000 000 iterations; `fingerprintPayload` throughput 10 000 small payloads
- [x] 11.4 Publish baseline numbers in README
## 12. Docs
- [x] 12.1 Co-located `.md` for every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes)
- [x] 12.2 Rewrite `README.md` with public API table, guard state-machine diagram, store adapter comparison, middleware usage guide, distributed guard usage guide, consumer integration guide
- [x] 12.3 `bun run docs:check --filter=@suaveplan/idempotency` ≥ 90%
## 13. Verify and publish
- [x] 13.1 `bunx turbo lint typecheck test --filter=@suaveplan/idempotency` green
- [x] 13.2 `bunx turbo test:coverage --filter=@suaveplan/idempotency` at 100/100/100/100
- [x] 13.3 `bunx turbo build --filter=@suaveplan/idempotency` green
- [x] 13.4 `bun scripts/publish.ts --package @suaveplan/idempotency` to Verdaccio at 0.0.0
- [x] 13.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-idempotency/`