Files
oh-my-pi/packages/utils/src/file-lock.ts
T
can1357 455493dfad feat: introduced native file lock bindings for cross-process advisory locking
- Added native `FileLock` bindings supporting cross-process advisory locking on Linux, Unix, and Windows.
- Replaced directory-based file locking and custom stale-lock reclamation with OS-backed native locks.
- Updated TypeScript declarations, native bindings, and package documentation for the new API.
- Added comprehensive unit tests and fixtures validating single-owner constraints and process death handoff.
2026-08-03 16:09:09 +02:00

67 lines
2.0 KiB
TypeScript

/**
* Cross-process advisory lock for packages that serialize access to an
* on-disk resource. The native handle is process-owned and automatically
* released on exit: Linux uses abstract Unix sockets, Windows uses named
* mutexes, and other Unix platforms use `flock(2)` on `${filePath}.lock`.
*/
import * as path from "node:path";
import { FileLock as NativeFileLock } from "@oh-my-pi/pi-natives";
/** Controls bounded waiting when an advisory file lock is contended. */
export interface FileLockOptions {
/** Maximum acquisition attempts, including the initial attempt. */
retries?: number;
/** Delay between acquisition attempts. */
retryDelayMs?: number;
}
const DEFAULT_OPTIONS: Required<FileLockOptions> = {
retries: 50,
retryDelayMs: 100,
};
function getLockPath(filePath: string): string {
return `${path.resolve(filePath)}.lock`;
}
function tryAcquireLock(lockPath: string): NativeFileLock | null {
const lock = NativeFileLock.tryAcquire(lockPath);
return lock.acquired ? lock : null;
}
async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<NativeFileLock> {
const opts = { ...DEFAULT_OPTIONS, ...options };
const lockPath = getLockPath(filePath);
for (let attempt = 0; attempt < opts.retries; attempt++) {
const lock = tryAcquireLock(lockPath);
if (lock) return lock;
if (attempt + 1 < opts.retries) await Bun.sleep(opts.retryDelayMs);
}
throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
}
/** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
export async function withFileLock<T>(
filePath: string,
fn: () => Promise<T>,
options: FileLockOptions = {},
): Promise<T> {
const lock = await acquireLock(filePath, options);
try {
return await fn();
} finally {
lock.release();
}
}
/**
* Test-only acquisition handle for forcing ownership handoffs. This is not
* part of the supported package API.
*/
export const __internalsForTesting = {
tryAcquireLock,
getLockPath,
};