'use strict' const fs = require('fs') const path = require('path') class FileLock { constructor() { this.lockedFiles = new Map() } /** * Acquire a file-based lock * @param {string} filePath - The path of the file to lock * @returns {Promise} */ acquire(filePath) { return new Promise((resolve, reject) => { const lockFilePath = this._getLockFilePath(filePath) // Try to acquire lock immediately if not locked if (!this.lockedFiles.has(filePath)) { this._createLockFile(lockFilePath, filePath, resolve, reject) } else { // Add to queue if already locked const queue = this.lockedFiles.get(filePath).queue queue.push({ resolve, reject }) } }) } /** * Release a file-based lock * @param {string} filePath - The path of the file to unlock */ release(filePath) { if (!this.lockedFiles.has(filePath)) { // Releasing an unlocked file is a no-op return } const lockInfo = this.lockedFiles.get(filePath) const lockFilePath = this._getLockFilePath(filePath) // Remove lock file try { if (fs.existsSync(lockFilePath)) { fs.unlinkSync(lockFilePath) } } catch (err) { // Ignore errors during unlock } // Check if there are queued requests if (lockInfo.queue.length > 0) { // Process the next queued request const nextRequest = lockInfo.queue.shift() this._createLockFile(lockFilePath, filePath, nextRequest.resolve, nextRequest.reject) } else { // No more queued requests, remove from locked files this.lockedFiles.delete(filePath) } } /** * Create a lock file * @private */ _createLockFile(lockFilePath, filePath, resolve, reject) { try { // Use wx flag to atomically create the lock file // If the file already exists, this will throw an error fs.writeFileSync(lockFilePath, process.pid.toString(), { flag: 'wx' }) // Initialize queue for this file if not exists if (!this.lockedFiles.has(filePath)) { this.lockedFiles.set(filePath, { lockFilePath: lockFilePath, queue: [] }) } resolve() } catch (err) { if (err.code === 'EEXIST') { // Lock file already exists, add to queue if (!this.lockedFiles.has(filePath)) { this.lockedFiles.set(filePath, { lockFilePath: lockFilePath, queue: [] }) } this.lockedFiles.get(filePath).queue.push({ resolve, reject }) } else { reject(err) } } } /** * Get lock file path from original file path * @private */ _getLockFilePath(filePath) { const resolvedPath = path.resolve(filePath) return `${resolvedPath}.lock` } } module.exports = FileLock