UNPKG

syncguard

Version:

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

93 lines (92 loc) 3.51 kB
/* SPDX-FileCopyrightText: 2025-present Kriasoft */ /* SPDX-License-Identifier: MIT */ import { delay } from "../common/backend.js"; /** * Checks if an error is transient and should be retried */ export function isTransientError(error) { const errorMessage = error instanceof Error ? error.message : String(error); return (errorMessage.includes("UNAVAILABLE") || errorMessage.includes("DEADLINE_EXCEEDED") || errorMessage.includes("ABORTED") || errorMessage.includes("timeout") || errorMessage.includes("network")); } /** * Handles retry logic for Firestore operations * Throws on failure after exhausting retries for transient errors */ export async function withRetries(operation, config) { let attempts = 0; const maxAttempts = config.maxRetries + 1; let lastError; while (attempts < maxAttempts) { attempts++; try { return await operation(); } catch (error) { lastError = error; // If this is not a transient error, throw immediately if (!isTransientError(error)) { throw error; } // If we've exhausted all attempts for transient errors, throw the last error if (attempts === maxAttempts) { throw lastError; } // Wait before retrying transient errors await delay(config.retryDelayMs); } } // This should never be reached, but included for type safety throw lastError; } /** * Specialized retry logic for lock acquisition operations * Handles both lock contention (always retry) and system errors (transient retry only) */ export async function withAcquireRetries(operation, config, timeoutMs) { let attempts = 0; const maxAttempts = config.maxRetries + 1; const startTime = Date.now(); let lastError; while (attempts < maxAttempts) { // Check timeout before each attempt if (Date.now() - startTime > timeoutMs) { throw new Error(`Lock acquisition timeout after ${timeoutMs}ms`); } attempts++; try { const result = await operation(); // Check timeout after operation completes to ensure we don't return // success for operations that exceeded the timeout if (Date.now() - startTime > timeoutMs) { throw new Error(`Lock acquisition timeout after ${timeoutMs}ms`); } // If we got a result (success or legitimate contention), return it if (result.acquired || result.reason) { return result; } // Fallback case - treat as contention and retry if (attempts < maxAttempts) { await delay(config.retryDelayMs); } } catch (error) { lastError = error; // If this is not a transient error, throw immediately if (!isTransientError(error)) { throw error; } // If we've exhausted all attempts for transient errors, throw the last error if (attempts === maxAttempts) { throw lastError; } // Wait before retrying transient errors await delay(config.retryDelayMs); } } // If we exhausted retries due to contention, return the contention result return { acquired: false, reason: "Lock contention - max retries exceeded" }; }