import { DEFAULT_LOCK_KEY, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_TTL_MS, } from "../../constants/index.js"; import type { DistributedLockAdapter, DistributedLockConfig, MigrationLockAdapter, } from "../../types/migration.types.js"; /** * In-memory implementation of the distributed lock adapter interface. * Suitable for testing without a real Redis or database backend. */ export class InMemoryDistributedLockAdapter implements DistributedLockAdapter { protected readonly locks: Map = new Map(); async acquire(key: string, ttl: number): Promise { const now = Date.now(); const existing = this.locks.get(key); if (existing !== undefined && existing > now) { return false; } this.locks.set(key, now + ttl); return true; } async release(key: string): Promise { this.locks.delete(key); } } /** * Distributed advisory lock that delegates acquisition and release to a * `DistributedLockAdapter`. Supports timeout-based retry. */ export class DistributedMigrationLock implements MigrationLockAdapter { protected readonly adapter: DistributedLockAdapter; protected readonly key: string; protected readonly ttl: number; constructor(config: DistributedLockConfig) { this.adapter = config.adapter; this.key = config.key ?? DEFAULT_LOCK_KEY; this.ttl = config.ttl ?? DEFAULT_LOCK_TTL_MS; } async acquire(timeoutMs = DEFAULT_LOCK_TIMEOUT_MS): Promise { const deadline = Date.now() + timeoutMs; let delay = 50; while (Date.now() < deadline) { const acquired = await this.adapter.acquire(this.key, this.ttl); if (acquired) return true; await sleep(Math.min(delay, deadline - Date.now())); delay = Math.min(delay * 2, 1000); } return false; } async release(): Promise { await this.adapter.release(this.key); } async isLocked(): Promise { // Try to acquire with zero ttl — if it fails, something holds the lock const canAcquire = await this.adapter.acquire(`${this.key}__probe`, 0); if (canAcquire) { await this.adapter.release(`${this.key}__probe`); return false; } return true; } } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms))); }