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

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 +21 -5
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -55,9 +55,10 @@ function nodeEvolutionIo() {
55
55
  /**
56
56
  * Cross-process write lock (claw `withFileLock` parity): an O_EXCL lock file
57
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.
58
+ * over. A held lock that outlasts the retry budget fails LOUD instead of
59
+ * proceeding unlocked — two unlocked writers overlap their RMW and lose
60
+ * updates, which is exactly how the rc.65 CI-only 7≠8 occurred (a slow
61
+ * writer held the lock past the budget and a peer entered unlocked).
61
62
  */
62
63
  const withWriteLock = async (path, task) => {
63
64
  const lock = `${path}.lock`;
@@ -83,7 +84,7 @@ function nodeEvolutionIo() {
83
84
  }
84
85
  await new Promise((resolve) => setTimeout(resolve, 50));
85
86
  }
86
- return await task();
87
+ throw new Error(`could not acquire write lock for ${path} after 10 attempts`);
87
88
  };
88
89
  return {
89
90
  async readText(path) {
@@ -257,6 +258,11 @@ async function loadUsage(root, io = nodeEvolutionIo()) {
257
258
  */
258
259
  async function mutateUsage(root, io, task) {
259
260
  await transactIo(io, usageFile(root), async (current) => {
261
+ if (current !== null) try {
262
+ JSON.parse(current);
263
+ } catch {
264
+ return current;
265
+ }
260
266
  const map = parseUsage(current);
261
267
  await task(map);
262
268
  return JSON.stringify(Object.fromEntries(map.entries()), null, 2);
@@ -338,6 +344,11 @@ async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
338
344
  */
339
345
  async function updateSuppressedNames(root, io, task) {
340
346
  await transactIo(io, suppressedFile(root), async (current) => {
347
+ if (current !== null) try {
348
+ JSON.parse(current);
349
+ } catch {
350
+ return current;
351
+ }
341
352
  const names = parseSuppressed(current);
342
353
  await task(names);
343
354
  return JSON.stringify({
@@ -1703,7 +1714,12 @@ async function loadMutations(root, io = nodeEvolutionIo()) {
1703
1714
  }
1704
1715
  /** Append one record, trim to `cap`, and write atomically (versioned shape). */
1705
1716
  async function recordMutation(root, io, record, cap = 500) {
1706
- await transactIo(io, mutationsFile(root), (current) => {
1717
+ await transactIo(io, mutationsFile(root), async (current) => {
1718
+ if (current !== null) try {
1719
+ JSON.parse(current);
1720
+ } catch {
1721
+ return current;
1722
+ }
1707
1723
  const existing = parseMutationRecords(current);
1708
1724
  existing.push(record);
1709
1725
  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.65",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },