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.
87 lines (86 loc) • 2.87 kB
JavaScript
/* SPDX-FileCopyrightText: 2025-present Kriasoft */
/* SPDX-License-Identifier: MIT */
/**
* Error thrown when lock operations fail
*/
export class LockError extends Error {
code;
constructor(message, code) {
super(message);
this.code = code;
this.name = "LockError";
}
}
/**
* Standard error codes for lock operations
*/
export const LockErrorCodes = {
ACQUISITION_FAILED: "ACQUISITION_FAILED",
TIMEOUT: "TIMEOUT",
ALREADY_LOCKED: "ALREADY_LOCKED",
NOT_FOUND: "NOT_FOUND",
};
/**
* Creates a distributed lock function with automatic lock management
* @param backend The lock backend implementation
* @returns A function that provides both automatic and manual lock operations
*/
export function createLock(backend) {
const withLock = async (fn, config) => {
const lockResult = await backend.acquire(config);
if (!lockResult.success) {
throw new LockError(`Failed to acquire lock: ${lockResult.error}`, "ACQUISITION_FAILED");
}
try {
return await fn();
}
finally {
try {
await backend.release(lockResult.lockId);
}
catch (error) {
// Log release failure but don't throw to avoid masking the main execution result
// In production, this should be logged to your observability system
console.warn(`Failed to release lock "${config.key}" (${lockResult.lockId}): ${error instanceof Error ? error.message : error}. Lock will expire naturally after TTL.`);
}
}
};
// Attach manual operations as properties
withLock.acquire = (config) => backend.acquire(config);
withLock.release = (lockId) => backend.release(lockId);
withLock.extend = (lockId, ttl) => backend.extend(lockId, ttl);
withLock.isLocked = (key) => backend.isLocked(key);
return withLock;
}
/**
* Utility function to generate a unique lock ID using crypto.randomUUID for better uniqueness
*/
export function generateLockId() {
// Use crypto.randomUUID if available (Node.js 14.17+), fallback to timestamp + random
if (typeof crypto !== "undefined" && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback for older environments - use slice instead of deprecated substr
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}
/**
* Utility function to create a delay
*/
export function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Default configuration values
*/
export const DEFAULT_CONFIG = {
ttlMs: 30000,
retryDelayMs: 100,
maxRetries: 10,
timeoutMs: 5000,
};
/**
* Utility function to merge lock configuration with defaults
*/
export function mergeLockConfig(config) {
return { ...DEFAULT_CONFIG, ...config };
}