nextfile
Atomic file-based locking with retry, TTL, heartbeat, PID tracking, stale detection, and multi-strategy support.

Installation
npm install nextfile
Quick Start
import { withLock, lock, lockSync } from 'nextfile';
await withLock('/tmp/my-resource', async () => {
});
const handle = await lock('/tmp/my-resource');
try {
} finally {
await handle.release();
}
const { release } = lockSync('/tmp/my-resource');
release();
Features
| Atomic writes | Uses hard-link strategy — no partial writes |
| Multiple adapters | fs, atomic, mkdir, symlink |
| Multiple strategies | exclusive, shared, advisory, optimistic |
| TTL | Locks auto-expire after a configurable duration |
| Heartbeat | Refreshes lock timestamps to prevent false stale detection |
| PID tracking | Detects orphaned locks from dead processes |
| Stale detection | By age, PID liveness, or TTL expiry |
| Retry + backoff | linear, exponential, fibonacci, jitter |
| AbortSignal | Cancel acquisition at any point |
| Sync API | lockSync, unlockSync, checkSync |
| Events | Typed event system for monitoring |
| Hooks | Lifecycle hooks: beforeLock, afterLock, etc. |
| Middleware | Compose option transformers |
| CLI | lock, unlock, status, list, cleanup |
| TypeScript | 100% typed, strict mode |
| ESM + CJS | Dual package |
API
lock(path, options?) → Promise<LockHandle>
Acquire a lock. Retries with backoff on conflict.
const handle = await lock('/tmp/resource', {
retries: 10,
retryBackoff: 'exponential',
ttl: 30_000,
heartbeatInterval: 5_000,
removeStale: true,
});
await handle.release();
withLock(path, fn, options?) → Promise<T>
Acquire, run fn, release — even on error.
const result = await withLock('/tmp/resource', async () => {
return computeSomething();
});
lockSync(path, options?) → { lockPath, release }
Synchronous lock (no retry, no heartbeat).
checkLock(path) → Promise<LockFile>
Inspect a lock without acquiring it.
isLocked(path) → Promise<boolean>
Returns true if a non-stale lock exists.
cleanupLocks(dir, options?) → Promise<CleanupResult>
Remove stale lock files from a directory.
Options
retries | number | 5 | Max retry attempts |
retryDelay | number | 100 | Base delay in ms |
retryBackoff | string | 'exponential' | Backoff strategy |
retryMaxDelay | number | 5000 | Max delay cap in ms |
timeout | number | 60000 | Total acquisition timeout |
ttl | number | null | Lock TTL in ms |
heartbeatInterval | number | 5000 | Heartbeat refresh interval |
staleAge | number | 30000 | Age threshold for stale detection |
removeStale | boolean | false | Auto-remove stale locks |
strategy | string | 'exclusive' | Lock strategy |
adapter | string | 'atomic' | File-system adapter |
signal | AbortSignal | — | Cancellation signal |
meta | object | {} | Arbitrary metadata stored in lock file |
CLI
nextfile lock /tmp/resource --ttl 30000
nextfile unlock /tmp/resource
nextfile status /tmp/resource --json
nextfile list --dir /tmp --recursive --table
nextfile cleanup --dir /tmp --stale-age 60000 --dry-run
Adapters
atomic (default) | ✅ | ✅ | Hard-link strategy |
fs | ❌ | ✅ | Simple wx flag write |
mkdir | ✅ | ✅ | mkdir is atomic on most FS |
symlink | ✅ | ⚠️ Unix only | Symlink as lock token |
Events
import { globalEmitter } from 'nextfile';
globalEmitter.on('acquired', (event) => {
console.log('Lock acquired:', event.lockPath, 'after', event.elapsed, 'ms');
});
globalEmitter.on('stale', (event) => {
console.warn('Stale lock detected:', event.lockPath);
});
Hooks
import { addBeforeLockHook, addAfterLockHook } from 'nextfile';
addBeforeLockHook(async (path, lockPath, options) => {
console.log(`About to lock ${path}`);
});
addAfterLockHook(async (handle) => {
console.log(`Acquired lock on ${handle.path}, PID ${handle.data.pid}`);
});
Middleware
import { withLoggingMiddleware, withRetryMiddleware, composeMiddleware } from 'nextfile';
const middleware = composeMiddleware(withLoggingMiddleware, withRetryMiddleware);
const handle = await lock('/tmp/resource', middleware({}));
License
MIT © nextfile contributors