@n-seiji/nuthatch 0.1.5 → 0.2.0

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.
@@ -0,0 +1,124 @@
1
+ import {
2
+ EXIT_SAFE_REJECTION,
3
+ fail
4
+ } from "./cli-77yanq37.js";
5
+
6
+ // src/infra/lock.ts
7
+ import { randomUUID } from "node:crypto";
8
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+
11
+ // src/domain/lock-policy.ts
12
+ var canReclaimLock = (input) => {
13
+ if (input.processAlive !== false) {
14
+ return false;
15
+ }
16
+ return input.nowMs - input.startedAtMs > input.ttlMs;
17
+ };
18
+
19
+ // src/infra/lock.ts
20
+ var LOCK_DIR_NAME = "nuthatch-lock";
21
+ var LOCK_INFO_FILE = "info.json";
22
+ var DEFAULT_TTL_MS = 30000;
23
+ var HEARTBEAT_INTERVAL_MS = 5000;
24
+
25
+ class LockHeldError extends Error {
26
+ info;
27
+ constructor(info) {
28
+ super(`Repository is locked by another nuthatch process (pid ${info.pid})`);
29
+ this.name = "LockHeldError";
30
+ this.info = info;
31
+ }
32
+ }
33
+ var isProcessAlive = (pid) => {
34
+ try {
35
+ process.kill(pid, 0);
36
+ return true;
37
+ } catch (error) {
38
+ const { code } = error;
39
+ if (code === "ESRCH") {
40
+ return false;
41
+ }
42
+ if (code === "EPERM") {
43
+ return true;
44
+ }
45
+ return "unknown";
46
+ }
47
+ };
48
+ var readLockInfo = async (lockDir) => {
49
+ try {
50
+ const raw = await readFile(join(lockDir, LOCK_INFO_FILE), "utf8");
51
+ return JSON.parse(raw);
52
+ } catch {
53
+ return null;
54
+ }
55
+ };
56
+ var writeLockInfo = async (lockDir, info) => {
57
+ await writeFile(join(lockDir, LOCK_INFO_FILE), JSON.stringify(info), "utf8");
58
+ };
59
+ var acquireRepoLock = async (commonDir, ttlMs = DEFAULT_TTL_MS) => {
60
+ const lockDir = join(commonDir, LOCK_DIR_NAME);
61
+ for (;; ) {
62
+ try {
63
+ await mkdir(lockDir);
64
+ break;
65
+ } catch (error) {
66
+ if (error.code !== "EEXIST") {
67
+ throw error;
68
+ }
69
+ const existing = await readLockInfo(lockDir);
70
+ if (existing === null) {
71
+ throw new LockHeldError({ pid: -1, startedAtMs: 0, token: "unknown" });
72
+ }
73
+ const reclaimable = canReclaimLock({
74
+ processAlive: isProcessAlive(existing.pid),
75
+ startedAtMs: existing.startedAtMs,
76
+ nowMs: Date.now(),
77
+ ttlMs
78
+ });
79
+ if (!reclaimable) {
80
+ throw new LockHeldError(existing);
81
+ }
82
+ await rm(lockDir, { recursive: true, force: true });
83
+ }
84
+ }
85
+ const info = {
86
+ pid: process.pid,
87
+ startedAtMs: Date.now(),
88
+ token: randomUUID()
89
+ };
90
+ await writeLockInfo(lockDir, info);
91
+ const heartbeat = setInterval(() => {
92
+ writeLockInfo(lockDir, { ...info, startedAtMs: Date.now() });
93
+ }, HEARTBEAT_INTERVAL_MS);
94
+ heartbeat.unref();
95
+ return {
96
+ async release() {
97
+ clearInterval(heartbeat);
98
+ await rm(lockDir, { recursive: true, force: true });
99
+ }
100
+ };
101
+ };
102
+ var acquireUnlessHeld = async (commonDir) => {
103
+ try {
104
+ return await acquireRepoLock(commonDir);
105
+ } catch (error) {
106
+ if (error instanceof LockHeldError) {
107
+ return error;
108
+ }
109
+ throw error;
110
+ }
111
+ };
112
+ var withRepoLock = async (commonDir, mutate) => {
113
+ const lock = await acquireUnlessHeld(commonDir);
114
+ if (lock instanceof LockHeldError) {
115
+ return fail(EXIT_SAFE_REJECTION, lock.message);
116
+ }
117
+ try {
118
+ return await mutate();
119
+ } finally {
120
+ await lock.release();
121
+ }
122
+ };
123
+
124
+ export { withRepoLock };