@lmzhen/dsh-evolution-core 0.1.0-rc.64 → 0.1.0-rc.66

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.
Files changed (2) hide show
  1. package/lib/index.js +38 -10
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -52,12 +52,22 @@ function nodeEvolutionIo() {
52
52
  const code = error?.code;
53
53
  return code === "ENOENT" || code === "ENOTDIR";
54
54
  };
55
+ /** True when the pid is alive (EPERM = alive but unowned; ESRCH = gone). */
56
+ const isAlive = (pid) => {
57
+ try {
58
+ process.kill(pid, 0);
59
+ return true;
60
+ } catch (error) {
61
+ return error?.code === "EPERM";
62
+ }
63
+ };
55
64
  /**
56
65
  * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
57
- * guards the atomic write; a >5s-old lock is treated as stale and taken
58
- * over. After the retry budget the write proceeds unlocked — the lock is a
59
- * best-effort accommodation for multi-process deployments, never a read of
60
- * availability.
66
+ * guards the atomic write. A >5s-old lock is taken over ONLY after probing
67
+ * the holder pid it carries (rc.66): a LIVE holder is never stolen, so a
68
+ * slow writer no longer loses its lock to a peer at the 5s mark (the
69
+ * takeover is the only best-effort surface; the retry budget fails loud —
70
+ * rc.65 — instead of ever proceeding unlocked).
61
71
  */
62
72
  const withWriteLock = async (path, task) => {
63
73
  const lock = `${path}.lock`;
@@ -73,17 +83,20 @@ function nodeEvolutionIo() {
73
83
  try {
74
84
  const st = await stat(lock);
75
85
  if (Date.now() - st.mtimeMs > 5e3) {
76
- try {
77
- await rm(lock, { force: true });
78
- } catch {}
79
- continue;
86
+ const holder = Number(await readFile(lock, "utf8").catch(() => ""));
87
+ if (!(Number.isInteger(holder) && holder > 0 && isAlive(holder))) {
88
+ try {
89
+ await rm(lock, { force: true });
90
+ } catch {}
91
+ continue;
92
+ }
80
93
  }
81
94
  } catch {
82
95
  continue;
83
96
  }
84
97
  await new Promise((resolve) => setTimeout(resolve, 50));
85
98
  }
86
- return await task();
99
+ throw new Error(`could not acquire write lock for ${path} after 10 attempts`);
87
100
  };
88
101
  return {
89
102
  async readText(path) {
@@ -257,6 +270,11 @@ async function loadUsage(root, io = nodeEvolutionIo()) {
257
270
  */
258
271
  async function mutateUsage(root, io, task) {
259
272
  await transactIo(io, usageFile(root), async (current) => {
273
+ if (current !== null) try {
274
+ JSON.parse(current);
275
+ } catch {
276
+ return current;
277
+ }
260
278
  const map = parseUsage(current);
261
279
  await task(map);
262
280
  return JSON.stringify(Object.fromEntries(map.entries()), null, 2);
@@ -338,6 +356,11 @@ async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
338
356
  */
339
357
  async function updateSuppressedNames(root, io, task) {
340
358
  await transactIo(io, suppressedFile(root), async (current) => {
359
+ if (current !== null) try {
360
+ JSON.parse(current);
361
+ } catch {
362
+ return current;
363
+ }
341
364
  const names = parseSuppressed(current);
342
365
  await task(names);
343
366
  return JSON.stringify({
@@ -1703,7 +1726,12 @@ async function loadMutations(root, io = nodeEvolutionIo()) {
1703
1726
  }
1704
1727
  /** Append one record, trim to `cap`, and write atomically (versioned shape). */
1705
1728
  async function recordMutation(root, io, record, cap = 500) {
1706
- await transactIo(io, mutationsFile(root), (current) => {
1729
+ await transactIo(io, mutationsFile(root), async (current) => {
1730
+ if (current !== null) try {
1731
+ JSON.parse(current);
1732
+ } catch {
1733
+ return current;
1734
+ }
1707
1735
  const existing = parseMutationRecords(current);
1708
1736
  existing.push(record);
1709
1737
  const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.1.0-rc.64",
4
+ "version": "0.1.0-rc.66",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },