64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
/**
|
|
* Redis-backed distributed lock provider using atomic `SET NX` operations.
|
|
*
|
|
* Implements the `LockProvider` interface with owner-based release semantics:
|
|
* a lock is only released if the requesting instance owns it, preventing
|
|
* accidental release of another process's lock after TTL expiry.
|
|
*
|
|
* @module redis-lock-provider
|
|
*/
|
|
|
|
import type { RedisClientAdapter } from "../store/adapter.types.js";
|
|
import type { LockProvider } from "../types/idempotency.types.js";
|
|
|
|
/** Configuration for `RedisLockProvider`. */
|
|
export interface RedisLockProviderConfig {
|
|
/** Redis client adapter. */
|
|
readonly client: RedisClientAdapter;
|
|
/** Key prefix for lock keys. Default: `'lock:'`. */
|
|
readonly prefix?: string;
|
|
/** Unique identifier for this lock owner (e.g. worker ID). */
|
|
readonly ownerId?: string;
|
|
}
|
|
|
|
/**
|
|
* Redis-backed distributed lock using `SET NX` with TTL.
|
|
*
|
|
* Owner-verified release ensures that only the process that acquired the
|
|
* lock can release it — safe for use in distributed multi-process deployments.
|
|
*/
|
|
export class RedisLockProvider implements LockProvider {
|
|
protected readonly client: RedisClientAdapter;
|
|
protected readonly prefix: string;
|
|
protected readonly ownerId: string;
|
|
|
|
constructor(config: RedisLockProviderConfig) {
|
|
this.client = config.client;
|
|
this.prefix = config.prefix ?? "lock:";
|
|
this.ownerId =
|
|
config.ownerId ??
|
|
`owner-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
|
|
/**
|
|
* Attempt to acquire the lock for `resource` with the given TTL.
|
|
* Returns `true` if the lock was acquired, `false` if already held.
|
|
*/
|
|
async acquire(resource: string, ttlMs: number): Promise<boolean> {
|
|
const key = `${this.prefix}${resource}`;
|
|
return this.client.setNx(key, this.ownerId, ttlMs);
|
|
}
|
|
|
|
/**
|
|
* Release the lock for `resource` if this instance owns it.
|
|
* No-ops if the lock is not held or was taken by another owner.
|
|
*/
|
|
async release(resource: string): Promise<void> {
|
|
const key = `${this.prefix}${resource}`;
|
|
const value = await this.client.get(key);
|
|
if (value === this.ownerId) {
|
|
await this.client.del(key);
|
|
}
|
|
}
|
|
}
|