UNPKG

syncguard

Version:

Functional TypeScript library for distributed locking across microservices. Prevents race conditions with Redis, PostgreSQL, Firestore, and custom backends. Features automatic lock management, timeout handling, and extensible architecture.

62 lines (61 loc) 2.88 kB
// SPDX-FileCopyrightText: 2025-present Kriasoft // SPDX-License-Identifier: MIT import { checkAborted, LockError, makeStorageKey, normalizeAndValidateKey, } from "../../common/backend.js"; import { isLive, TIME_TOLERANCE_MS } from "../../common/time-predicates.js"; import { mapFirestoreError } from "../errors.js"; /** * Creates isLocked operation for Firestore backend. * Uses transactional cleanup with safety guard when enabled. * @see ../../common/time-predicates.ts for expiration logic */ export function createIsLockedOperation(db, locksCollection, config) { return async (opts) => { try { // Check for cancellation before starting operation checkAborted(opts.signal); const normalizedKey = normalizeAndValidateKey(opts.key); const FIRESTORE_LIMIT_BYTES = 1500; const RESERVE_BYTES = 0; // No derived keys in Firestore const storageKey = makeStorageKey("", normalizedKey, FIRESTORE_LIMIT_BYTES, RESERVE_BYTES); const docRef = locksCollection.doc(storageKey); const doc = await docRef.get(); // Check for cancellation after read checkAborted(opts.signal); if (!doc.exists) { return false; } const data = doc.data(); const nowMs = Date.now(); if (!isLive(data.expiresAtMs, nowMs, TIME_TOLERANCE_MS)) { if (config.cleanupInIsLocked) { // 1s safety guard prevents race with concurrent extend operations const guardMs = 1000; if (nowMs - data.expiresAtMs > TIME_TOLERANCE_MS + guardMs) { // Fire-and-forget: non-blocking cleanup, swallow errors // Note: opts.signal intentionally not passed to background cleanup db.runTransaction(async (trx) => { const transactionDoc = await trx.get(docRef); if (transactionDoc.exists) { const transactionData = transactionDoc.data(); // Re-verify expiration in transaction to prevent races if (!isLive(transactionData.expiresAtMs, nowMs, TIME_TOLERANCE_MS + guardMs)) { await trx.delete(docRef); } } }).catch(() => { // Lock expires naturally if cleanup fails }); } } return false; } return true; } catch (error) { if (error instanceof LockError) { throw error; } throw mapFirestoreError(error); } }; }