# `distributed-guard` ## Purpose Provides distributed idempotency protection via a pluggable `LockProvider` that serializes concurrent access across multiple processes or nodes before consulting the idempotency store. The `DistributedIdempotencyGuard` guarantees exactly-once execution even when multiple service instances race to handle the same idempotency key simultaneously, preventing double-charge or double-write scenarios. ## Features - `DistributedIdempotencyGuard` — wraps any store + lock provider for multi-node safety - `MemoryLockProvider` — in-process lock map for unit tests and single-node deployments - Exponential back-off with jitter on lock contention - Automatic lock release in a `finally` block — never leaks locks on exception - Configurable retry count, base delay, and lock TTL - Fingerprint mismatch detection under the lock (same key, different payload) - `AbortSignal` support propagated through lock acquisition retries - Injectable `clock` for deterministic testing ## Usage: Basic Example ```ts import { DistributedIdempotencyGuard, MemoryLockProvider } from "@suaveplan/idempotency/distributed"; import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store"; const guard = new DistributedIdempotencyGuard({ store: new MemoryIdempotencyStore(), lockProvider: new MemoryLockProvider(), ttlMs: 3_600_000, lockTtlMs: 5_000, }); const result = await guard.execute("payment-42", () => chargeCard()); ``` ## Usage: Advanced Example ```ts import { DistributedIdempotencyGuard } from "@suaveplan/idempotency/distributed"; import { RedisIdempotencyStore } from "@suaveplan/idempotency/store"; import { RedisLockProvider } from "@suaveplan/idempotency/distributed"; import { LockAcquisitionError } from "@suaveplan/idempotency"; const guard = new DistributedIdempotencyGuard({ store: new RedisIdempotencyStore({ client: redisAdapter }), lockProvider: new RedisLockProvider({ client: redisAdapter, ownerId: "worker-1" }), retryCount: 5, retryBaseDelayMs: 50, lockTtlMs: 10_000, }); try { await guard.execute("order-99", () => fulfillOrder(order), { input: order }); } catch (err) { if (err instanceof LockAcquisitionError) { console.error(`Lock failed after ${err.attempts} attempts`); } } ``` ## API Reference ### `new DistributedIdempotencyGuard(config: DistributedGuardConfig)` | Option | Type | Default | Description | |--------|------|---------|-------------| | `store` | `IdempotencyStoreAdapter` | required | Record persistence | | `lockProvider` | `LockProvider` | required | Distributed lock backend | | `ttlMs` | `number` | `86_400_000` | Record TTL (24 h) | | `lockTtlMs` | `number` | `10_000` | Lock TTL (10 s) | | `retryCount` | `number` | `3` | Max lock acquisition retries | | `retryBaseDelayMs` | `number` | `100` | Base back-off delay (ms) | | `fingerprintFn` | `fn` | `fingerprintPayload` | Payload hasher | | `clock` | `() => number` | `Date.now` | Monotonic clock | ### `execute(key, fn, opts?): Promise` Acquires the distributed lock, checks the store, and executes `fn` if no cached result exists. **Throws:** `LockAcquisitionError` | `IdempotencyMismatchError` ### `MemoryLockProvider` In-memory lock map with TTL-based auto-expiration. Suitable for testing and single-process deployments. **Methods:** `acquire(resource, ttlMs)`, `release(resource)`, `isLocked(resource)`, `clear()` ## Implementation Notes - The lock key is prefixed `idempotency:lock:` to avoid collisions with store keys. - Exponential back-off delay formula: `baseDelay * 2^attempt + random(0, baseDelay)`. - The lock is always released in `finally` — even when `fn` throws or the AbortSignal fires. - `MemoryLockProvider.clock` is injectable to enable deterministic TTL expiration in tests.