@bybrave/proper-lockfile2
Maintained fork of proper-lockfile — an inter-process and inter-machine lockfile utility that works on a local or network file system.
Why this fork
The original proper-lockfile has not been released since 2021. This fork is a drop-in replacement that fixes a real double-lock race and ships the most requested features:
| Change | Upstream issue |
|---|---|
| Stale-lock reclaim race fixed: two processes could both "reclaim" the same stale lock and end up holding it simultaneously | #121, root cause of many #92 reports |
onReclaimed callback: know when your lock was acquired by reclaiming a stale one (e.g. to run crash recovery exactly once) | #105 |
ELOCKED error message now hints at the retries option — the most common source of confusion | #92 |
Ctrl-C and SIGTERM keep working: the exit handler is installed on the first acquired lock, not on require | #111, #110 |
No Cannot read properties of undefined (reading 'updateTimeout') crash when a lock is released while an mtime update is being retried | #113 |
onCompromised: undefined / null keeps the default handler instead of crashing | #117 |
| mtime precision is detected per device, so locks on filesystems with different precision work in one process | #104 |
| TypeScript type definitions included | — |
| Updated dependencies, CI on Node 18/20/22, original test suite preserved (76 tests) + regression tests for every fix | — |
The race, in short
Upstream reclaims a stale lock with rmdir + mkdir. If process A detects a stale lock but stalls before its rmdir, process B can reclaim the lock and create a fresh one — which A's late rmdir then silently deletes, and both processes believe they own the lock. This fork claims stale locks by atomically renaming them to a unique path (only one process can win a rename) and verifies the claimed directory is still the stale one it saw. A reproduction script and regression tests are in test/fork.test.js.
Install
npm install @bybrave/proper-lockfile2
Usage
const lockfile = require('@bybrave/proper-lockfile2');
const release = await lockfile.lock('some/file');
// do your critical work...
await release();
Wait for a held lock instead of failing:
const release = await lockfile.lock('some/file', {
retries: { retries: 5, maxTimeout: 1000 },
});
Run crash recovery when a stale lock (left by a dead process) is reclaimed:
const release = await lockfile.lock('some/file', {
onReclaimed: () => runRecovery(),
});
Sync API: lockSync, unlockSync, checkSync (retries are not supported in sync mode).
API
lock(file, [options]) → Promise<release>
Acquires a lock on file, resolving with a release() function. Rejects with ELOCKED if the lock is held (and not stale).
| Option | Default | Description |
|---|---|---|
stale | 10000 | Ms after which a non-updated lock is considered stale (min 2000) |
update | stale/2 | Interval of lockfile mtime refresh (min 1000, max stale/2) |
retries | 0 | Number of retries (or a node-retry options object) while acquiring a held lock |
realpath | true | Resolve symlinks; set false to lock the symlink itself (also allows locking non-existing paths) |
onCompromised | (err) => { throw err; } | Called when the lock can no longer be guaranteed |
onReclaimed | — | Called when the lock was acquired by reclaiming a stale lockfile |
lockfilePath | ${file}.lock | Custom lockfile location |
fs | graceful-fs | Custom fs implementation |
Locking a file that does not exist yet
With the default realpath: true the file must exist, because realpath resolves symlinks of the full path. This also shows up when locking a not-yet-created file under a symlinked directory such as /tmp on macOS (it points to /private/tmp), with ENOENT (#100, #99).
Either create the file first, or pass realpath: false. The trade-off of realpath: false: the lock is keyed by the path as written, so two processes that reach the same file through different symlinks (/tmp/foo and /private/tmp/foo) will not see each other's lock. Use one canonical path in all processes, e.g. fs.realpathSync(dir) + '/file' for a directory that exists.
unlock(file, [options]) / check(file, [options]) → Promise
Same semantics as the original: unlock releases a lock acquired in this process, check resolves with whether the file is currently locked. Options: stale (check only), realpath, lockfilePath, fs.
Full conceptual documentation (how mtime-based locking works, its guarantees and caveats) is in the original README — the design is unchanged.
Migrating from proper-lockfile
It is a drop-in replacement:
- const lockfile = require('proper-lockfile');
+ const lockfile = require('@bybrave/proper-lockfile2');
No API was removed or changed; onReclaimed is the only new option. Unlike the original, requiring the module no longer subscribes to process signals until a lock is acquired.
Support
If this package saves you time, you can support maintenance:
Bitcoin (BTC): bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q
Credits & license
MIT. Based on proper-lockfile by André Cruz / MOXY.