Read/Write Lockfile Utility
rwlockfile is a Node.js utility that provides a file-based readers-writers lock mechanism, allowing multiple readers or a single writer to access a resource. It is currently at version 2.0.25 and appears to be actively maintained as needed. A key differentiator is its explicit support for the standard Readers-Writers Lock design pattern, which the author claims is unique among Node.js packages. Unlike simpler lockfile solutions, rwlockfile allows for flexible, nested locking logic on a single `RWLockfile` instance, where `add()` and `remove()` methods manage an internal counter to ensure the physical lock is only held when needed and released only when all nested calls are complete. This makes it suitable for complex asynchronous workflows requiring fine-grained concurrency control over shared resources.
Common errors
-
Error: ETIMEDOUT: acquire lock timeout
cause The lock could not be acquired within the specified `timeout` period, likely because another process held the lock for too long or a stale lockfile was present.fixIncrease the `timeout` option when initializing `RWLockfile` if the wait time is acceptable, or investigate the cause of long-held locks. Ensure `remove()` is always called in `finally` blocks. Manually clean up any stale lockfiles from crashed processes if necessary. -
TypeError: RWLockfile is not a constructor
cause Incorrect import statement, attempting to use `require('rwlockfile')` or `import RWLockfile from 'rwlockfile'` directly as the constructor without destructuring.fixUse `const { RWLockfile } = require('rwlockfile');` for CommonJS or `import { RWLockfile } from 'rwlockfile';` for ESM, as `RWLockfile` is a named export.
Warnings
- gotcha The `add()` and `remove()` methods manage an internal counter on the `RWLockfile` instance itself, not directly reflecting the global lock status for each call. A physical lock is acquired or released only when this internal counter crosses the 0/1 threshold. This design enables nested function calls to acquire locks without releasing the global lock prematurely, but it can be counter-intuitive if expecting a one-to-one correspondence between method calls and file lock state changes.
- gotcha If the process holding a lock crashes abruptly without calling `remove()`, the lockfile may become stale, preventing other processes from acquiring the lock. While the library supports timeouts for acquiring locks, it doesn't automatically detect and clean up stale locks from crashed processes.
- gotcha The `retryInterval` option, which specifies the minimum time between lock checks, can significantly impact performance or the responsiveness of lock acquisition. A very low interval can lead to high CPU usage due to frequent file system polling, while a high interval can make lock acquisition slow and unresponsive.
Install
-
npm install rwlockfile -
yarn add rwlockfile -
pnpm add rwlockfile
Imports
- RWLockfile
import RWLockfile from 'rwlockfile';
import { RWLockfile } from 'rwlockfile'; - RWLockfile
const RWLockfile = require('rwlockfile');const { RWLockfile } = require('rwlockfile'); - RWLockfileOptions
import { RWLockfileOptions } from 'rwlockfile';import type { RWLockfileOptions } from 'rwlockfile';
Quickstart
import { RWLockfile } from 'rwlockfile';
async function runLockExample() {
// 'lockfile-target' is the base path; '.lock' will be appended for the actual lock file.
const lock = new RWLockfile('lockfile-target', {
timeout: 5000, // Wait up to 5 seconds for a lock
retryInterval: 50 // Check every 50ms
});
try {
console.log('Attempting to acquire a write lock...');
await lock.add('write');
console.log('Write lock acquired. Performing critical write operation...');
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate work
console.log('Write operation complete. Releasing lock.');
} finally {
await lock.remove('write');
console.log('Write lock released.');
}
console.log('\nAttempting to acquire a read lock...');
await lock.add('read');
try {
console.log('Read lock acquired. Performing read operation...');
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate work
console.log('Read operation complete. Releasing lock.');
} finally {
await lock.remove('read');
console.log('Read lock released.');
}
// Demonstrate nested locking (instance-specific counting)
async function nestedWrite() {
await lock.add('write');
try {
console.log(' Nested write lock added by inner function.');
await new Promise(resolve => setTimeout(resolve, 200));
} finally {
await lock.remove('write');
console.log(' Nested write lock removed by inner function.');
}
}
try {
console.log('\nAttempting top-level write lock for nesting...');
await lock.add('write');
console.log('Top-level write lock acquired.');
await nestedWrite(); // Call a function that also adds/removes a lock on the same instance
console.log('Back in top-level. Main write operation continuing.');
await new Promise(resolve => setTimeout(resolve, 300));
} finally {
await lock.remove('write');
console.log('Top-level write lock finally released after all nested calls.');
}
}
runLockExample().catch(console.error);