3.7 KiB
3.7 KiB
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 safetyMemoryLockProvider— in-process lock map for unit tests and single-node deployments- Exponential back-off with jitter on lock contention
- Automatic lock release in a
finallyblock — never leaks locks on exception - Configurable retry count, base delay, and lock TTL
- Fingerprint mismatch detection under the lock (same key, different payload)
AbortSignalsupport propagated through lock acquisition retries- Injectable
clockfor deterministic testing
Usage: Basic Example
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
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 whenfnthrows or the AbortSignal fires. MemoryLockProvider.clockis injectable to enable deterministic TTL expiration in tests.