> 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`, `ConflictResolutionStrategy`, `IdempotencyStoreAdapter`, `IdempotencyGuardConfig`, `DistributedGuardConfig`, `KeyGeneratorConfig`, `KeyVersioningConfig`, `IdempotencyMetricsConfig`, `IdempotencyMetricsSnapshot`, `MetricsReporter`, `IdempotencyMiddlewareConfig`, `MiddlewareResult`, `LockProvider`, `VersionEntry` - [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(key, fn, opts?: { signal?: AbortSignal }) → Promise` - 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(key, fn, opts?: { signal?: AbortSignal }) → Promise` - 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(config)`: - Returns a handler function `(request: TReq, next: () => Promise) → Promise>` - 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(key, fingerprint, response?, status?) → Promise>` - `get(key, version?) → Promise | undefined>` — latest or specific version - `list(key) → Promise[]>` — 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/`