Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user