455493dfad
- 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.
67 lines
2.0 KiB
TypeScript
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,
|
|
};
|