/** * AsyncDisposable support for automatic lock cleanup with `await using` syntax. * * Provides RAII (Resource Acquisition Is Initialization) pattern for locks, * ensuring cleanup on all code paths including early returns and exceptions. * * ## Default Error Handling * * **NEW**: Disposal errors are now observable by default to prevent silent failures. * - Development (NODE_ENV !== 'production'): Logs to console.error * - Production: Silent unless SYNCGUARD_DEBUG=true * - Security: Omits sensitive data (key, lockId) from default logs * * **Production Best Practice**: Override with custom callback integrated with your * logging/metrics infrastructure: * * ```typescript * const backend = createRedisBackend(redis, { * onReleaseError: (err, ctx) => { * logger.error('Lock disposal failed', { err, ...ctx }); * metrics.increment('syncguard.disposal.error'); * }, * }); * ``` * * ## Configuration Patterns * * There are two independent ways to configure error callbacks, serving different APIs: * * ### Pattern A: Backend-level (for low-level `await using` API) * Configure once at backend creation for all acquisitions: * ```typescript * const backend = createRedisBackend(redis, { * onReleaseError: (err, ctx) => logger.error("Disposal error", err, ctx), * disposeTimeoutMs: 5000 // Optional: timeout disposal after 5s * }); * * await using lock = await backend.acquire({ key, ttlMs }); * // Disposal errors automatically route to backend's onReleaseError * ``` * * ### Pattern B: Lock-level (for high-level `lock()` helper) * Configure per-call for fine-grained control: * ```typescript * const backend = createRedisBackend(redis); // Uses default callback * * await lock(backend, { * key, * onReleaseError: (err, ctx) => logger.error("Lock error", err, ctx), * async fn(handle) { ... } * }); * ``` * * **Note**: These are independent configurations for different usage patterns. * Choose the pattern that matches your API usage - you typically won't mix them. * * ## Disposal Timeout Behavior * * When `disposeTimeoutMs` is configured, Symbol.asyncDispose races the release operation * against a hard ceiling timer. If timeout elapses before release completes: * - AbortSignal is triggered to signal cancellation to the backend * - Disposal returns immediately (Symbol.asyncDispose never blocks) * - Backend-specific behavior depends on signal handling: * - Redis/PostgreSQL: Network socket timeouts provide additional safety * - Firestore: Note that AbortSignal cannot interrupt in-flight gRPC calls; timeout * signals cancellation intent but may not stop the RPC. The in-flight call may * continue in the background. Timeout errors are routed to onReleaseError for observability. * * **Important**: disposeTimeoutMs bounds the async disposal wait time, not the actual * backend cleanup. Use for responsiveness guarantees in high-reliability contexts. * * @see docs/specs/interface.md#resource-management - Normative specification * @see docs/adr/015-async-raii-locks.md - Async RAII for Locks * @see docs/adr/016-disposal-timeout.md - Opt-In Disposal Timeout */ import type { AcquireOk, AcquireResult, BackendCapabilities, DecoratedAcquireResult, ExtendResult, KeyOp, LockBackend, OnReleaseError, ReleaseResult } from "./types.js"; export type { OnReleaseError } from "./types.js"; /** * Lock handle with resource management methods. * Extends acquire result with release/extend operations and async disposal. */ export interface DisposableLockHandle { /** * Manually release the lock. Idempotent - safe to call multiple times. * Returns { ok: false } if lock was already released or absent. * * **Error handling**: Throws on system errors (network failures, auth errors) * for consistency with backend API. Only automatic disposal (via `await using`) * swallows errors and routes them to onReleaseError callback. * * @param signal Optional AbortSignal to cancel the release operation * @throws Error on system failures (network timeouts, service unavailable) */ release(signal?: AbortSignal): Promise; /** * Extend lock TTL. Returns { ok: false } if lock was already released or absent. * @param ttlMs New TTL in milliseconds (resets expiration to now + ttlMs) * @param signal Optional AbortSignal to cancel the extend operation * @throws Error on system failures (network timeouts, service unavailable) */ extend(ttlMs: number, signal?: AbortSignal): Promise; /** * Automatic cleanup on scope exit (used by `await using` syntax). * Never throws - errors are swallowed and optionally routed to onReleaseError callback. */ [Symbol.asyncDispose](): Promise; } /** * Successful acquisition with automatic cleanup support. * Use with `await using` for automatic lock release on scope exit. * * @example * ```typescript * await using lock = await backend.acquire({ key, ttlMs: 15_000 }); * if (!lock.ok) throw new Error("Failed to acquire lock"); * // TypeScript narrows lock to AsyncLock after ok check * await doWork(lock.fence); * // Lock automatically released on scope exit * ``` */ export type AsyncLock = AcquireOk & DisposableLockHandle; /** * Creates a disposable lock handle from a successful acquisition. * Internal utility - use decorateAcquireResult() for public API. * * @param backend Backend operations (release, extend) * @param result Successful acquisition result * @param key Original normalized key for error context * @param onReleaseError Error callback for disposal failures (defaults to defaultDisposalErrorHandler) * @param disposeTimeoutMs Optional timeout for disposal operations in ms * @returns AsyncLock with disposal support */ export declare function createDisposableHandle(backend: Pick, "release" | "extend">, result: AcquireOk, key: string, onReleaseError?: OnReleaseError, disposeTimeoutMs?: number): AsyncLock; /** * Decorates an acquire result with async disposal support. * This is the main integration point for backends. * * - If acquisition succeeded (ok: true): Returns AsyncLock with disposal methods * - If acquisition failed (ok: false): Returns result with no-op disposal * * **Error Handling**: Disposal errors are routed to the onReleaseError callback. * If not provided, uses defaultDisposalErrorHandler which logs in development * and is silent in production (unless SYNCGUARD_DEBUG=true). * * @param backend Backend instance for release/extend operations * @param result Raw acquisition result from backend * @param key Original normalized key for error context * @param onReleaseError Callback for disposal errors (defaults to defaultDisposalErrorHandler) * @param disposeTimeoutMs Optional timeout for disposal operations in ms * @returns Decorated result with disposal support * * @example * ```typescript * // In backend implementation: * const backend = { * acquire: async (opts) => { * const normalizedKey = normalizeAndValidateKey(opts.key); * const result = await acquireCore(opts); * return decorateAcquireResult( * backend, * result, * normalizedKey, * config.onReleaseError, // Pass through user config or use default * config.disposeTimeoutMs * ); * }, * // ... * }; * ``` */ export declare function decorateAcquireResult(backend: Pick, "release" | "extend">, result: AcquireResult, key: string, onReleaseError?: OnReleaseError, disposeTimeoutMs?: number): DecoratedAcquireResult; /** * Optional sugar for fully-typed RAII without manual type narrowing. * Calls backend.acquire() and returns typed handle or failure. * * @param backend Backend instance * @param opts Acquisition options * @returns AsyncLock or failure - no type narrowing needed * * @example * ```typescript * // Option A: Standard (uses TypeScript's built-in narrowing) * await using lock = await backend.acquire({ key, ttlMs }); * if (!lock.ok) return; * await lock.extend(5000); // TypeScript knows this is AsyncLock after ok check * * // Option B: Sugar (same narrowing, different API) * await using lock = await acquireHandle(backend, { key, ttlMs }); * if (!lock.ok) return; * await lock.extend(5000); // Same narrowing behavior * ``` */ export declare function acquireHandle(backend: LockBackend, opts: KeyOp & { ttlMs: number; }): Promise | { ok: false; reason: "locked"; }>;