10 KiB
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/withpackage.json,tsconfig.json,tsconfig.lib.json,vite.config.ts,vitest.config.ts,README.mdskeleton - 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, andSTRUCTURE.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—IdempotencyErrorbase extendsSuaveplanError;IdempotencyConflictError,IdempotencyMismatchError,LockAcquisitionError,VersionConflictErrorextendIdempotencyError - 2.3 Implement
src/error-codes.ts—IdempotencyErrorCodeunion 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 partsfingerprintPayload(input: unknown, hashFn?: (data: Uint8Array) => string) → string— deterministic hex fingerprint; uses@suaveplan/crypto-hashsha256 by default, accepts injectable hash functiongenerateIdempotencyKey(config?: KeyGeneratorConfig) → string— random key with optional prefix; accepts injectablerng?: Rng
- 3.2 Unit tests: compositeKey separator/prefix, fingerprintPayload determinism, generateIdempotencyKey uniqueness and prefix formatting
- 3.3 Property-based tests:
compositeKeyoutput is deterministic for equal inputs;fingerprintPayloadproduces distinct results for distinct payloads;generateIdempotencyKeynever 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—IdempotencyDatabaseAdapterinterface,RedisClientAdapterinterface - 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;sizegetter - 4.3 Implement
src/store/database-store.ts—DatabaseIdempotencyStoreandDatabaseIdempotencyStoreConfig: wrapsIdempotencyDatabaseAdapter; maps store interface to adapter calls - 4.4 Implement
src/store/redis-store.ts—RedisIdempotencyStoreandRedisIdempotencyStoreConfig: usesRedisClientAdapter; atomicSET NX PXforset; JSON serialization - 4.5 Unit tests for
MemoryIdempotencyStore: get/set/delete/cleanup, TTL expiry, concurrent sets - 4.6 Integration tests for
RedisIdempotencyStoreintests/integration/guarded byTEST_REDIS_URL - 4.7 Integration tests for
DatabaseIdempotencyStoreintests/integration/guarded byTEST_DATABASE_URL - 4.8 Co-located
.mdfor each store implementation
5. Guard
- 5.1 Implement
src/guard/idempotency-guard.ts—IdempotencyGuardclass:execute<T>(key, fn, opts?: { signal?: AbortSignal }) → Promise<T>- State machine:
pending → completed | failed; returns cached response on hit; throwsIdempotencyConflictErrorwhen pending; throwsIdempotencyMismatchErroron fingerprint mismatch lockTimeoutMsgoverns how long a pending record blocks re-executionfingerprintFnis injectable; defaults tofingerprintPayloadclock?: Clockfor deterministic timeout computation
- 5.2 Honour
AbortSignal: cancel pending-record wait on abort; propagate signal tofn - 5.3 Unit tests: cache hit, cache miss, conflict (pending in-flight), mismatch, failed-record retry, TTL expiry, clock injection, signal abort,
fingerprintFninjection - 5.4 Property-based tests:
executenever invokesfnmore 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—DistributedIdempotencyGuardclass:execute<T>(key, fn, opts?: { signal?: AbortSignal }) → Promise<T>- Acquires lock via
LockProviderbefore checking / writing store; retries on contention up toretryCountwithretryBaseDelayMsexponential back-off - Releases lock in
finally; marks recordfailedonfnerror clock?: Clockfor 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—RedisLockProviderandRedisLockProviderConfig: acquire viaSET NX PX, release via Lua script; no Redlock dependency - 6.4 Unit tests for
DistributedIdempotencyGuardwithMemoryLockProvider: acquire/release lifecycle, retry on contention, abort signal, fn-error cleanup - 6.5 Integration tests for
RedisLockProviderand the full distributed guard path intests/integration/guarded byTEST_REDIS_URL - 6.6 Co-located
.mdfor 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 perConflictResolutionStrategy - Per-operation TTL overrides via
ttlOverrides+extractOperation - Injectable
fingerprintFnandtransformResponse
- Returns a handler function
- 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—KeyVersioningclass:record<T>(key, fingerprint, response?, status?) → Promise<VersionEntry<T>>get<T>(key, version?) → Promise<VersionEntry<T> | undefined>— latest or specific versionlist<T>(key) → Promise<VersionEntry<T>[]>— all retained versions- Prunes oldest entries when
maxVersionsis exceeded - Conflict strategy:
rejectthrowsVersionConflictError;overwritereplaces;versionappends
- 8.2 Unit tests: reject strategy, overwrite strategy, version strategy, maxVersions pruning, get by version, list
- 8.3 Property-based tests:
KeyVersioningnever exceedsmaxVersionsstored entries regardless of call order - 8.4 Co-located
src/versioning/key-versioning.md
9. Metrics
- 9.1 Implement
src/metrics/idempotency-metrics.ts—IdempotencyMetricsclass:record(event: MetricsEvent)— increments counters and records durationssnapshot() → IdempotencyMetricsSnapshot— computes rolling averages overwindowMsstartPeriodicReporting(reporter, intervalMs)— callsreporter(snapshot())on interval; returnsstopFnreset()— 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.totalby outcome label (hit | miss | conflict | mismatch | error) - 10.2 Histogram
idempotency.execution.duration_ms(guard execution latency) - 10.3 Counter
idempotency.lock.acquisition.totalby result (acquired | failed | retried) - 10.4 Histogram
idempotency.lock.wait_ms(time spent acquiring a distributed lock) - 10.5 Counter
idempotency.store.operation.totalby 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.gethot path, 10 000 cached reads (mitata) - 11.2
src/__benchmarks__/guard.bench.ts—IdempotencyGuard.executewith cached hit, zero-execution overhead - 11.3
src/__benchmarks__/key-generator.bench.ts—compositeKeythroughput 1 000 000 iterations;fingerprintPayloadthroughput 10 000 small payloads - 11.4 Publish baseline numbers in README
12. Docs
- 12.1 Co-located
.mdfor every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes) - 12.2 Rewrite
README.mdwith 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/idempotencygreen - 13.2
bunx turbo test:coverage --filter=@suaveplan/idempotencyat 100/100/100/100 - 13.3
bunx turbo build --filter=@suaveplan/idempotencygreen - 13.4
bun scripts/publish.ts --package @suaveplan/idempotencyto Verdaccio at 0.0.0 - 13.5 Archive OpenSpec change to
openspec/archive/2026-04-19-enterprise-idempotency/