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

10 KiB

Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/control-flow/idempotency.

Tasks: enterprise-idempotency

1. Scaffold

  • 1.1 Create packages/universal/idempotency/ with package.json, tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, README.md skeleton
  • 1.2 Register coverage thresholds at 100/100/100/100 in vitest.config.ts; provider = istanbul
  • 1.3 Add cross-cutting guard tests in tests/: dep-graph, no-disallowed, tree-shake
  • 1.4 Add package to root CLAUDE.md, README.md, and STRUCTURE.yml

2. Types + errors

  • 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>
  • 2.2 Implement src/errors/idempotency-error.ts — IdempotencyError base extends SuaveplanError; IdempotencyConflictError, IdempotencyMismatchError, LockAcquisitionError, VersionConflictError extend IdempotencyError
  • 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
  • 2.4 Zod schemas: IdempotencyStatusSchema, IdempotencyRecordSchema, IdempotencyGuardConfigSchema, DistributedGuardConfigSchema, KeyGeneratorConfigSchema, KeyVersioningConfigSchema, IdempotencyMetricsConfigSchema, IdempotencyMiddlewareConfigSchema
  • 2.5 Tests: error construction, code narrowing, field presence on each error subclass, Zod schema round-trips
  • 2.6 Auto-register error codes via side-effect import in src/index.ts

3. Key generation

  • 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
  • 3.2 Unit tests: compositeKey separator/prefix, fingerprintPayload determinism, generateIdempotencyKey uniqueness and prefix formatting
  • 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
  • 3.4 Co-located src/key/key-generator.md

4. Store adapters

  • 4.1 Implement src/store/adapter.types.ts — IdempotencyDatabaseAdapter interface, RedisClientAdapter interface
  • 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
  • 4.3 Implement src/store/database-store.ts — DatabaseIdempotencyStore and DatabaseIdempotencyStoreConfig: wraps IdempotencyDatabaseAdapter; maps store interface to adapter calls
  • 4.4 Implement src/store/redis-store.ts — RedisIdempotencyStore and RedisIdempotencyStoreConfig: uses RedisClientAdapter; atomic SET NX PX for set; JSON serialization
  • 4.5 Unit tests for MemoryIdempotencyStore: get/set/delete/cleanup, TTL expiry, concurrent sets
  • 4.6 Integration tests for RedisIdempotencyStore in tests/integration/ guarded by TEST_REDIS_URL
  • 4.7 Integration tests for DatabaseIdempotencyStore in tests/integration/ guarded by TEST_DATABASE_URL
  • 4.8 Co-located .md for each store implementation

5. Guard

  • 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
  • 5.2 Honour AbortSignal: cancel pending-record wait on abort; propagate signal to fn
  • 5.3 Unit tests: cache hit, cache miss, conflict (pending in-flight), mismatch, failed-record retry, TTL expiry, clock injection, signal abort, fingerprintFn injection
  • 5.4 Property-based tests: execute never invokes fn more than once for the same key under concurrent callers
  • 5.5 Co-located src/guard/idempotency-guard.md

6. Distributed guard

  • 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
  • 6.2 Implement src/distributed/memory-lock-provider.ts — MemoryLockProvider: in-process TTL-aware lock map; safe for single-node and unit tests
  • 6.3 Implement src/distributed/redis-lock-provider.ts — RedisLockProvider and RedisLockProviderConfig: acquire via SET NX PX, release via Lua script; no Redlock dependency
  • 6.4 Unit tests for DistributedIdempotencyGuard with MemoryLockProvider: acquire/release lifecycle, retry on contention, abort signal, fn-error cleanup
  • 6.5 Integration tests for RedisLockProvider and the full distributed guard path in tests/integration/ guarded by TEST_REDIS_URL
  • 6.6 Co-located .md for each distributed implementation file

7. Middleware

  • 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
  • 7.2 Unit tests: hit path, miss path, conflict path, mismatch path, missing key passthrough, TTL override, transformResponse, fingerprintFn injection
  • 7.3 Co-located src/middleware/idempotency-middleware.md

8. Key versioning

  • 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
  • 8.2 Unit tests: reject strategy, overwrite strategy, version strategy, maxVersions pruning, get by version, list
  • 8.3 Property-based tests: KeyVersioning never exceeds maxVersions stored entries regardless of call order
  • 8.4 Co-located src/versioning/key-versioning.md

9. Metrics

  • 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
  • 9.2 Unit tests: counter increments, hit rate computation, avgExecutionDurationMs, rolling window cutoff, periodic reporting via FakeClock, reset
  • 9.3 Co-located src/metrics/idempotency-metrics.md

10. OTel instrumentation

  • 10.1 Counter idempotency.request.total by outcome label (hit | miss | conflict | mismatch | error)
  • 10.2 Histogram idempotency.execution.duration_ms (guard execution latency)
  • 10.3 Counter idempotency.lock.acquisition.total by result (acquired | failed | retried)
  • 10.4 Histogram idempotency.lock.wait_ms (time spent acquiring a distributed lock)
  • 10.5 Counter idempotency.store.operation.total by operation (get | set | delete | cleanup)
  • 10.6 Tests: verify no-op without exporter; verify counter increments with a test meter provider

11. Benchmarks

  • 11.1 src/__benchmarks__/memory-store.bench.ts — MemoryIdempotencyStore.get hot path, 10 000 cached reads (mitata)
  • 11.2 src/__benchmarks__/guard.bench.ts — IdempotencyGuard.execute with cached hit, zero-execution overhead
  • 11.3 src/__benchmarks__/key-generator.bench.ts — compositeKey throughput 1 000 000 iterations; fingerprintPayload throughput 10 000 small payloads
  • 11.4 Publish baseline numbers in README

12. Docs

  • 12.1 Co-located .md for every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes)
  • 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
  • 12.3 bun run docs:check --filter=@suaveplan/idempotency ≥ 90%

13. Verify and publish

  • 13.1 bunx turbo lint typecheck test --filter=@suaveplan/idempotency green
  • 13.2 bunx turbo test:coverage --filter=@suaveplan/idempotency at 100/100/100/100
  • 13.3 bunx turbo build --filter=@suaveplan/idempotency green
  • 13.4 bun scripts/publish.ts --package @suaveplan/idempotency to Verdaccio at 0.0.0
  • 13.5 Archive OpenSpec change to openspec/archive/2026-04-19-enterprise-idempotency/