nlocks/file-lock.js

109 lines
3.3 KiB
JavaScript

'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<void>}
*/
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