first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# `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<T>(key, fn, opts?): Promise<T>`
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:<key>` 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.