export type AdvisoryLockBackendProvenance = import("./advisory-lock-reentrancy.js").AdvisoryLockBackendProvenance; /** * Provenance of one advisory-lock acquisition (STC-210 design §3): every * held lock reports the tier that actually served it, so a fallback is * visible to the callback, in logs, and in `getAdvisoryLockBackendStatus()`. * @typedef {import("./advisory-lock-reentrancy.js").AdvisoryLockBackendProvenance} AdvisoryLockBackendProvenance */ /** * Thrown when an advisory lock could not be acquired before `timeoutMs` elapsed. */ declare class AdvisoryLockTimeoutError extends Error { lockName: string; /** * Runs constructor. * @param {string} message - Error message. * @param {{name: string}} args - The advisory lock name that timed out. */ constructor(message: string, { name }: { name: string; }); } /** * Thrown when `withAdvisoryLockOrFail` finds the lock already held. */ declare class AdvisoryLockBusyError extends Error { lockName: string; /** * Runs constructor. * @param {string} message - Error message. * @param {{name: string}} args - The advisory lock name that was already held. */ constructor(message: string, { name }: { name: string; }); } /** * Advisory locks held by one async context, keyed by * `\u0000` and carrying the re-entry depth plus * the provenance of the tier that actually holds the lock. * * Re-entry is scoped to the async context, not to the process: a nested call * made inside a held callback belongs to the same logical operation and must * not contend with itself, while an unrelated concurrent request in the same * process is a different context and must still wait. That is Ruby's `Monitor` * ("re-entrant for the holding thread") expressed with the async-context * analogue of a thread. * * Without this, the four tiers disagreed on re-entry: a nested acquisition on * a `dedicatedConnection` opened a second connection and self-deadlocked, the * file tier refused it as held, and the lock-server tier received * `already-held` and treated it as contention. Applications worked around it * with hand-passed `*LockHeld` booleans, which is exactly the parameter a * monitor makes redundant. * * The storage itself belongs to the environment handler: Node owns async-context * storage, while browser and Expo runtimes have none and therefore hold without * re-entry tracking instead of pulling `node:async_hooks` into their bundles. */ /** * Thrown when a callback holds an advisory lock longer than `holdTimeoutMs`. */ declare class AdvisoryLockHoldTimeoutError extends Error { lockName: string; /** * Runs constructor. * @param {string} message - Error message. * @param {{name: string}} args - The advisory lock name whose hold timed out. */ constructor(message: string, { name }: { name: string; }); } /** * Runs advisory locks on the caller connection by default, using a dedicated * lock connection when `dedicatedConnection` is requested or when a positive * hold timeout needs separate ownership. */ export default class AdvisoryLockRunner { configuration: import("../configuration.js").default; connectionProvider: () => import("./drivers/base.js").default; databaseIdentifier: string; /** * Creates an advisory-lock runner for one model database identifier. * @param {{configuration: import("../configuration.js").default, connectionProvider: () => import("./drivers/base.js").default, databaseIdentifier: string}} args - Runner dependencies. */ constructor({ configuration, connectionProvider, databaseIdentifier }: { configuration: import("../configuration.js").default; connectionProvider: () => import("./drivers/base.js").default; databaseIdentifier: string; }); /** * Runs a callback after acquiring the advisory lock, waiting up to `timeoutMs`. * When `dedicatedConnection` is true the lock is acquired on a spawned * connection that is released after the callback finishes, while the callback * itself still runs against the caller/model connection. When a `holdTimeoutMs` * is set the callback receives a `TimeoutControl` from awaitery for cooperative * cancellation (`control.check()`, `control.signal`, `control.timedOut`, * `control.remaining()`); a dedicated connection is also used so timeout * cleanup can release the lock even if callback database work is stuck. * @template T * @param {string} name - Lock name. * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise} callback - Callback to invoke while the lock is held. * @param {{timeoutMs?: number | null, holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Lock and hold timeout options. * @returns {Promise} - Resolves with the callback result. */ withAdvisoryLock(name: string, callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise, args?: { timeoutMs?: number | null; holdTimeoutMs?: number | null; dedicatedConnection?: boolean; }): Promise; /** * Runs a callback only if the advisory lock can be acquired immediately. * When `dedicatedConnection` is true the lock is acquired on a spawned * connection that is released after the callback finishes, while the callback * itself still runs against the caller/model connection. When a `holdTimeoutMs` * is set the callback receives a `TimeoutControl` from awaitery for cooperative * cancellation. * @template T * @param {string} name - Lock name. * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise} callback - Callback to invoke while the lock is held. * @param {{holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Hold timeout options. * @returns {Promise} - Resolves with the callback result. */ withAdvisoryLockOrFail(name: string, callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise, args?: { holdTimeoutMs?: number | null; dedicatedConnection?: boolean; }): Promise; /** /** * Runs a callback if the advisory lock can be acquired without waiting; * otherwise returns a `{acquired: false}` defer result instead of throwing. * * This is the non-throwing counterpart of `withAdvisoryLockOrFail` for hot * paths: "holder busy, defer me" is a first-class outcome, so a contended * lock produces zero exception traffic and — because the probe is a single * immediate try-acquire — zero pinned connections. Callers reschedule via * their own event-driven retry machinery. The result form is identical * across tiers: the driver's `tryAcquireAdvisoryLock` maps per-tier (MySQL * `GET_LOCK(name, 0)`, PostgreSQL `pg_try_advisory_lock`, SQL Server * `sp_getapplock @LockTimeout = 0`, SQLite process-local queue, SQLite Node * file lock), so a caller never branches on the backend. * @template T * @param {string} name - Lock name. * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise} callback - Callback to invoke while the lock is held. * @param {{holdTimeoutMs?: number | null, dedicatedConnection?: boolean}} [args] - Hold timeout options. * @returns {Promise<{acquired: true, result: T} | {acquired: false, reason: "contended", lockName: string}>} - Either the held-callback result or the defer outcome. */ withAdvisoryLockOrDefer(name: string, callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise, args?: { holdTimeoutMs?: number | null; dedicatedConnection?: boolean; }): Promise<{ acquired: true; result: T; } | { acquired: false; reason: "contended"; lockName: string; }>; /** * Runs the lock holder callback and releases the lock from its owning backend. * @template T * @param {{callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise, holdTimeoutMs?: number | null, lockBackend: AdvisoryLockBackendProvenance, name: string, release: () => Promise}} args - Locked callback args. * @returns {Promise} - Resolves with the callback result. */ runLockedCallback({ callback, holdTimeoutMs, lockBackend, name, release }: { callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise; holdTimeoutMs?: number | null; lockBackend: AdvisoryLockBackendProvenance; name: string; release: () => Promise; }): Promise; /** * Returns the held entry when this async context already holds the lock. * @param {string} name - Lock name. * @returns {{depth: number, lockBackend: AdvisoryLockBackendProvenance} | null} - The held entry, or null when this context does not hold the lock. */ heldEntryFor(name: string): { depth: number; lockBackend: AdvisoryLockBackendProvenance; } | null; /** * Runs a callback as a re-entry of a lock this async context already holds. * * The re-entry adds no backend round trip and cannot fail on contention: the * outer hold already owns the lock, so mutual exclusion is unchanged. The * outer hold owns the hold timeout and the lease; a re-entry does not start a * second one, and the callback therefore receives the outer provenance without * a `control`. * @template T * @param {{callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise, entry: {depth: number, lockBackend: AdvisoryLockBackendProvenance}, name: string}} args - Re-entry request. * @returns {Promise} - Callback result. */ runReentrantCallback({ callback, entry, name }: { callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise; entry: { depth: number; lockBackend: AdvisoryLockBackendProvenance; }; name: string; }): Promise; /** * Builds the async-context storage key for one lock name. * * The database identifier is part of the key because it is part of the lock's * identity: two tenant databases reusing one name hold two different locks, * and a nested call in the other database is not a re-entry. * @param {string} name - Lock name. * @returns {string} - Storage key for the held-entry map. */ lockStorageKey(name: string): string; /** * Serves one acquisition on the lock-server tier when the lock name prefers * it (design §1/§9), falling *upward* to the driver path when the server is * unreachable and the name's minimum tier permits a DB-native hold. * * `null` means "this tier did not serve the acquisition — continue with the * next eligible tier". A returned value means the lock-server tier answered, * including a definitive busy/timeout outcome. * @template T * @param {{args: {holdTimeoutMs?: number | null, timeoutMs?: number | null}, callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise, mode: "wait" | "or-fail" | "defer", name: string}} request - Acquisition request. * @returns {Promise<{deferred: {acquired: false, reason: "contended", lockName: string}} | {value: T} | null>} - Result when this tier served it, otherwise null. */ attemptLockServer({ args, callback, mode, name }: { args: { holdTimeoutMs?: number | null; timeoutMs?: number | null; }; callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise; mode: "wait" | "or-fail" | "defer"; name: string; }): Promise<{ deferred: { acquired: false; reason: "contended"; lockName: string; }; } | { value: T; } | null>; /** * Runs the critical section under a lock-server lease and surfaces a fenced * lease as `AdvisoryLockLostError` (design §8): the section may have * overlapped another holder, so its outcome must not be trusted as exclusive. * @template T * @param {{backend: import("./advisory-lock-lock-server-backend.js").default, callback: (args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise, holdTimeoutMs?: number | null, lockBackend: AdvisoryLockBackendProvenance, name: string}} args - Hold request. * @returns {Promise} - Callback result. */ runLockServerHold({ backend, callback, holdTimeoutMs, lockBackend, name }: { backend: import("./advisory-lock-lock-server-backend.js").default; callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise; holdTimeoutMs?: number | null; lockBackend: AdvisoryLockBackendProvenance; name: string; }): Promise; /** * Reports an acquisition served below the lock name's preferred tier * (design §3): every fallback is visible on the framework-error channel, so a * silent downgrade is impossible. * @param {{lockName: string, reason: string, servedBy: import("./advisory-lock-backend-policy.js").AdvisoryLockTier}} args - Fallback details. * @returns {void} */ reportAdvisoryLockFallback({ lockName, reason, servedBy }: { lockName: string; reason: string; servedBy: import("./advisory-lock-backend-policy.js").AdvisoryLockTier; }): void; /** * Resolves the backend policy for one lock name and returns the file * backend when the name explicitly opts into the file tier * (`preferredTier: "file"` plus `advisoryLocks.backends.file.enabled: true`). * A name whose declared minimum tier is cross-host is refused loudly * (`AdvisoryLockMinTierRefusedError`) before any contact with the file * tier. A name that prefers the file tier while the backend is disabled * moves to the driver path (a stronger-or-equal tier), never the other way * around. Nothing here ever auto-enables the file tier. * @param {string} name - Lock name. * @returns {Promise} - Resolves with the file backend serving this name, or null for the driver path. */ fileBackendForName(name: string): Promise; /** * Fail-closed check on the driver path (design §9): an undeclared lock name * keeps today's behavior (any driver serves it — full backward * compatibility), but a name with an explicit policy entry is refused with * `AdvisoryLockBackendUnavailableError` when the configured driver cannot * satisfy its minimum tier (e.g. `minTier: "driver-native"` on a SQLite * driver, which has no native advisory-lock primitive). * @param {string} name - Lock name. * @param {AdvisoryLockBackendProvenance} lockBackend - Tier the driver path would serve with. * @returns {void} */ assertDriverTierEligible(name: string, lockBackend: AdvisoryLockBackendProvenance): void; /** * Resolves the wait bound for a file-tier acquisition: an explicit caller * `timeoutMs` (including an explicit `null` opt-out) wins, otherwise the * same framework default that bounds driver acquisitions applies. * @param {{timeoutMs?: number | null}} args - Lock options. * @returns {number | null} - Wait bound in milliseconds, or `null` for an indefinite wait. */ acquireTimeoutMs(args: { timeoutMs?: number | null; }): number | null; /** * Builds the acquisition provenance for a file-tier hold. The file tier * cannot carry fencing tokens, so the limitation is reported on every * acquisition rather than silently pretended away (design §8). * @param {import("./advisory-lock-file-backend.js").default} backend - Backend that served the acquisition. * @param {string} name - Lock name. * @returns {AdvisoryLockBackendProvenance} - Acquisition provenance. */ fileProvenance(backend: import("./advisory-lock-file-backend.js").default, name: string): AdvisoryLockBackendProvenance; /** * Builds the acquisition provenance for a driver-served hold. SQLite has no * native advisory-lock primitive, so its emulation tier is reported as * `process-local` rather than pretending to be database-native. * @param {import("./drivers/base.js").default} connection - Connection that served the acquisition. * @returns {AdvisoryLockBackendProvenance} - Acquisition provenance. */ driverProvenance(connection: import("./drivers/base.js").default): AdvisoryLockBackendProvenance; /** * Runs lock work on the caller connection unless `dedicatedConnection` is * requested or a positive hold timeout needs its own lock connection. * @template T * @param {{dedicatedConnection?: boolean, holdTimeoutMs?: number | null}} args - Lock connection options. * @param {(connection: import("./drivers/base.js").default) => Promise} callback - Callback receiving the connection that owns the advisory lock. * @returns {Promise} - Resolves with the callback result. */ withLockConnection(args: { dedicatedConnection?: boolean; holdTimeoutMs?: number | null; }, callback: (connection: import("./drivers/base.js").default) => Promise): Promise; /** * Spawns a dedicated lock connection and closes it after lock work completes * when the spawned driver owns the underlying physical connection. * @template T * @param {(connection: import("./drivers/base.js").default) => Promise} callback - Callback that receives the dedicated lock connection. * @returns {Promise} - Resolves with the callback result. */ withDedicatedConnection(callback: (connection: import("./drivers/base.js").default) => Promise): Promise; /** * Runs `callback`, rejecting with `AdvisoryLockHoldTimeoutError` if it has * not settled within `holdTimeoutMs`. The callback is not cancelled; callers * use a dedicated advisory-lock connection so the lock can still be released. * * The callback receives a `TimeoutControl` from awaitery, enabling cooperative * cancellation via `control.check()`, `control.signal`, `control.timedOut`, * and `control.remaining()`, plus `lockBackend` provenance for the tier that * served the acquisition. * @template T * @param {string} name - Lock name (for the error message). * @param {(args?: {control?: import("awaitery/build/timeout.js").TimeoutControl, lockBackend: AdvisoryLockBackendProvenance}) => Promise} callback - Callback holding the lock. * @param {number | null} [holdTimeoutMs] - Max hold time; falsy disables the timeout. * @param {AdvisoryLockBackendProvenance} [lockBackend] - Provenance of the acquisition serving this hold. * @returns {Promise} - Resolves with the callback result. */ static runWithAdvisoryLockHoldTimeout(name: string, callback: (args?: { control?: import("awaitery/build/timeout.js").TimeoutControl; lockBackend: AdvisoryLockBackendProvenance; }) => Promise, holdTimeoutMs?: number | null, lockBackend?: AdvisoryLockBackendProvenance): Promise; /** * Neutral provenance for direct `runWithAdvisoryLockHoldTimeout` callers that * hold a lock acquired outside this runner. * @returns {AdvisoryLockBackendProvenance} - Provenance with an unknown tier. */ static unknownBackendProvenance(): AdvisoryLockBackendProvenance; } export { AdvisoryLockBusyError, AdvisoryLockHoldTimeoutError, AdvisoryLockTimeoutError }; export { AdvisoryLockBackendUnavailableError, AdvisoryLockMinTierRefusedError } from "./advisory-lock-backend-policy.js"; export { AdvisoryLockLostError, AdvisoryLockServiceUnavailableError } from "./advisory-lock-errors.js"; //# sourceMappingURL=advisory-lock-runner.d.ts.map