@oh-my-pi/pi-utils 17.2.5 → 17.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.6] - 2026-08-03
6
+
7
+ ### Added
8
+
9
+ - Added a shared `file-lock` utility backed by process-owned native OS locks with automatic crash release and bounded asynchronous retry.
10
+
5
11
  ## [17.2.5] - 2026-08-03
6
12
 
7
13
  ### Added
@@ -0,0 +1,21 @@
1
+ import { FileLock as NativeFileLock } from "@oh-my-pi/pi-natives";
2
+ /** Controls bounded waiting when an advisory file lock is contended. */
3
+ export interface FileLockOptions {
4
+ /** Maximum acquisition attempts, including the initial attempt. */
5
+ retries?: number;
6
+ /** Delay between acquisition attempts. */
7
+ retryDelayMs?: number;
8
+ }
9
+ declare function getLockPath(filePath: string): string;
10
+ declare function tryAcquireLock(lockPath: string): NativeFileLock | null;
11
+ /** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
12
+ export declare function withFileLock<T>(filePath: string, fn: () => Promise<T>, options?: FileLockOptions): Promise<T>;
13
+ /**
14
+ * Test-only acquisition handle for forcing ownership handoffs. This is not
15
+ * part of the supported package API.
16
+ */
17
+ export declare const __internalsForTesting: {
18
+ tryAcquireLock: typeof tryAcquireLock;
19
+ getLockPath: typeof getLockPath;
20
+ };
21
+ export {};
@@ -5,6 +5,7 @@ export * from "./color.js";
5
5
  export * from "./dirs.js";
6
6
  export * from "./env.js";
7
7
  export * from "./fetch-retry.js";
8
+ export * from "./file-lock.js";
8
9
  export * from "./format.js";
9
10
  export * from "./frontmatter.js";
10
11
  export * from "./fs-error.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "17.2.5",
4
+ "version": "17.2.7",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "17.2.5",
34
+ "@oh-my-pi/pi-natives": "17.2.7",
35
35
  "handlebars": "^4.7.9",
36
36
  "winston": "^3.19.0",
37
37
  "winston-daily-rotate-file": "5.0.0"
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cross-process advisory lock for packages that serialize access to an
3
+ * on-disk resource. The native handle is process-owned and automatically
4
+ * released on exit: Linux uses abstract Unix sockets, Windows uses named
5
+ * mutexes, and other Unix platforms use `flock(2)` on `${filePath}.lock`.
6
+ */
7
+ import * as path from "node:path";
8
+ import { FileLock as NativeFileLock } from "@oh-my-pi/pi-natives";
9
+
10
+ /** Controls bounded waiting when an advisory file lock is contended. */
11
+ export interface FileLockOptions {
12
+ /** Maximum acquisition attempts, including the initial attempt. */
13
+ retries?: number;
14
+ /** Delay between acquisition attempts. */
15
+ retryDelayMs?: number;
16
+ }
17
+
18
+ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
19
+ retries: 50,
20
+ retryDelayMs: 100,
21
+ };
22
+
23
+ function getLockPath(filePath: string): string {
24
+ return `${path.resolve(filePath)}.lock`;
25
+ }
26
+
27
+ function tryAcquireLock(lockPath: string): NativeFileLock | null {
28
+ const lock = NativeFileLock.tryAcquire(lockPath);
29
+ return lock.acquired ? lock : null;
30
+ }
31
+
32
+ async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<NativeFileLock> {
33
+ const opts = { ...DEFAULT_OPTIONS, ...options };
34
+ const lockPath = getLockPath(filePath);
35
+
36
+ for (let attempt = 0; attempt < opts.retries; attempt++) {
37
+ const lock = tryAcquireLock(lockPath);
38
+ if (lock) return lock;
39
+ if (attempt + 1 < opts.retries) await Bun.sleep(opts.retryDelayMs);
40
+ }
41
+
42
+ throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
43
+ }
44
+
45
+ /** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
46
+ export async function withFileLock<T>(
47
+ filePath: string,
48
+ fn: () => Promise<T>,
49
+ options: FileLockOptions = {},
50
+ ): Promise<T> {
51
+ const lock = await acquireLock(filePath, options);
52
+ try {
53
+ return await fn();
54
+ } finally {
55
+ lock.release();
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Test-only acquisition handle for forcing ownership handoffs. This is not
61
+ * part of the supported package API.
62
+ */
63
+ export const __internalsForTesting = {
64
+ tryAcquireLock,
65
+ getLockPath,
66
+ };
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./color";
5
5
  export * from "./dirs";
6
6
  export * from "./env";
7
7
  export * from "./fetch-retry";
8
+ export * from "./file-lock";
8
9
  export * from "./format";
9
10
  export * from "./frontmatter";
10
11
  export * from "./fs-error";