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,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.