UNPKG

@shagital/atomic-lock

Version:

Universal atomic locking with pluggable drivers (Redis, SQLite, File, Memory)

239 lines (238 loc) 8.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.FileLockDriver = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); /** * File system driver implementation * Uses atomic file operations and directory-based locking */ class FileLockDriver { constructor(config) { this.lockDir = config.lockDir; this.ensureLockDir(); if (config.cleanupInterval) { this.cleanupInterval = setInterval(() => { this.cleanup().catch(console.error); }, config.cleanupInterval); } } async ensureLockDir() { try { await fs.mkdir(this.lockDir, { recursive: true }); } catch (error) { // Directory might already exist } } getLockPath(key) { // Sanitize key for filesystem const sanitized = key.replace(/[^a-zA-Z0-9-_]/g, '_'); // If the sanitized key is too long, hash it if (sanitized.length > FileLockDriver.MAX_FILENAME_LENGTH) { const hash = crypto.createHash('sha256').update(key).digest('hex'); // Include a prefix of the original key for debugging, then the hash const prefix = sanitized.substring(0, 20); const filename = `${prefix}_${hash}`; return path.join(this.lockDir, `${filename}.lock`); } return path.join(this.lockDir, `${sanitized}.lock`); } async tryAcquire(key, lockValue, expiryInSeconds) { const lockPath = this.getLockPath(key); const expiresAt = Date.now() + (expiryInSeconds * 1000); const lockData = { value: lockValue, expiresAt, createdAt: Date.now() }; try { // Use 'wx' flag for exclusive creation (atomic) await fs.writeFile(lockPath, JSON.stringify(lockData), { flag: 'wx' }); return true; } catch (error) { if (error.code === 'EEXIST') { // Lock file exists, check if expired try { const existing = await this.readLockFile(lockPath); if (existing && existing.expiresAt < Date.now()) { // Expired lock, try to remove and acquire await fs.unlink(lockPath); return this.tryAcquire(key, lockValue, expiryInSeconds); } } catch { // If we can't read the lock file, assume it's valid } return false; } throw error; } } async tryAcquireMultiple(keys, lockValue, expiryInSeconds) { const lockPaths = keys.map(key => this.getLockPath(key)); const expiresAt = Date.now() + (expiryInSeconds * 1000); const lockData = { value: lockValue, expiresAt, createdAt: Date.now() }; // Check if any locks exist for (const lockPath of lockPaths) { try { const existing = await this.readLockFile(lockPath); if (existing && existing.expiresAt >= Date.now()) { return false; // Lock is active } } catch { // File doesn't exist, which is what we want } } // Try to acquire all locks const acquired = []; try { for (const lockPath of lockPaths) { await fs.writeFile(lockPath, JSON.stringify(lockData), { flag: 'wx' }); acquired.push(lockPath); } return true; } catch (error) { // Rollback any acquired locks for (const acquiredPath of acquired) { try { await fs.unlink(acquiredPath); } catch { // Best effort cleanup } } return false; } } async release(key, lockValue) { const lockPath = this.getLockPath(key); try { const existing = await this.readLockFile(lockPath); if (existing && existing.value === lockValue) { await fs.unlink(lockPath); return true; } return false; } catch { return false; } } async releaseMultiple(keys, lockValue) { let released = 0; for (const key of keys) { const success = await this.release(key, lockValue); if (success) released++; } return released; } async exists(key) { const lockPath = this.getLockPath(key); try { const existing = await this.readLockFile(lockPath); return existing !== null && existing.expiresAt >= Date.now(); } catch { return false; } } async getLockInfo(key) { const lockPath = this.getLockPath(key); try { const fileLock = await this.readLockFile(lockPath); if (!fileLock) return null; return { key, value: fileLock.value, expiresAt: fileLock.expiresAt, createdAt: fileLock.createdAt }; } catch { return null; } } async cleanup() { try { const files = await fs.readdir(this.lockDir); const now = Date.now(); for (const file of files) { if (file.endsWith('.lock')) { const lockPath = path.join(this.lockDir, file); try { const lockData = await this.readLockFile(lockPath); if (lockData && lockData.expiresAt < now) { await fs.unlink(lockPath); } } catch { // Skip files we can't read/parse } } } } catch (error) { console.error('Error during lock cleanup:', error); } } async close() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } async readLockFile(lockPath) { try { const content = await fs.readFile(lockPath, 'utf8'); return JSON.parse(content); } catch { return null; } } } exports.FileLockDriver = FileLockDriver; FileLockDriver.MAX_FILENAME_LENGTH = 200; // Safe limit for most filesystems