@deepseek-ai/dsh-atomic-write 0.1.0-rc.6 → 0.1.0-rc.8

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/util/atomic-write/README.md
5
- README.md: a767f24064c368b60d85fed6fa1d88349cab9587
6
- README.zh.md: 6388e264898e0025fb6586acecab450be3eb9e55
5
+ README.md: 4d0b55291955c9d37f4788c7d37ad8e6ce728f70
6
+ README.zh.md: c2d7f0b49fa123befbb663ac43862a40b4ef19b4
package/README.md CHANGED
@@ -28,7 +28,7 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
28
28
  - **Same-directory sibling** keeps the rename on one filesystem, so the swap stays atomic.
29
29
  - Parent directories are created; on any failure the temp is removed and the failure rethrown; readers observe either the old or the new complete content.
30
30
 
31
- `withFileLock` serializes the writers of one file across processes, for the read-render-commit cycles a bare atomic commit cannot make safe on its own. The lock is a `wx`-created `<filename>.lock` sibling, so readers never contend; waiters back off exponentially and fail with a timeout rather than block forever. A contender never removes the existing lock: age cannot distinguish a crashed owner from a paused live writer.
31
+ `withFileLock` serializes the writers of one file across processes, for the read-render-commit cycles a bare atomic commit cannot make safe on its own. The lock is a `wx`-created `<filename>.lock` sibling, so readers never contend; waiters back off exponentially and fail with a timeout rather than block forever. `EEXIST` identifies contention directly; `EPERM` does so only when a fresh `lstat` confirms that the lock path exists, covering Windows exclusive-create behavior without hiding an unrelated permission failure. A contender never removes the existing lock: age cannot distinguish a crashed owner from a paused live writer.
32
32
 
33
33
  ## Model Experience
34
34
 
package/README.zh.md CHANGED
@@ -28,7 +28,7 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
28
28
  - **同目录兄弟文件**保证 rename 落在同一文件系统上,交换保持原子。
29
29
  - 自动创建父目录;任何失败都会移除临时文件并重新抛出该失败;读取方只会观察到旧内容或完整的新内容。
30
30
 
31
- `withFileLock` 跨进程串行化同一文件的写入方,服务于单靠原子提交无法保证安全的读-渲染-提交循环。锁是以 `wx` 创建的同目录 `<filename>.lock`,因此读取方从不参与竞争;等待方按指数退避,超时即失败而非无限阻塞。竞争者绝不移除现有锁:锁龄无法区分已经崩溃的所有者与被暂停但仍存活的写入方。
31
+ `withFileLock` 跨进程串行化同一文件的写入方,服务于单靠原子提交无法保证安全的读-渲染-提交循环。锁是以 `wx` 创建的同目录 `<filename>.lock`,因此读取方从不参与竞争;等待方按指数退避,超时即失败而非无限阻塞。`EEXIST` 直接表示竞争;只有一次新的 `lstat` 确认锁路径存在时,`EPERM` 才表示竞争,从而兼容 Windows 的独占创建行为,又不掩盖无关的权限故障。竞争者绝不移除现有锁:锁龄无法区分已经崩溃的所有者与被暂停但仍存活的写入方。
32
32
 
33
33
  ## 模型体验
34
34
 
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { mkdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  //#region lib/types/index.js
5
5
  /**
@@ -44,9 +44,17 @@ async function writeFileAtomic(filename, content, options) {
44
44
  throw error;
45
45
  }
46
46
  }
47
- /** Whether an exclusive create failed because the path already exists. */
48
- function isEEXIST(error) {
49
- return error?.code === "EEXIST";
47
+ /** Whether an exclusive create found an existing lock. */
48
+ async function isLockContention(error, lockPath) {
49
+ const code = error?.code;
50
+ if (code === "EEXIST") return true;
51
+ if (code !== "EPERM") return false;
52
+ try {
53
+ await lstat(lockPath);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
50
58
  }
51
59
  /**
52
60
  * Writer-lock protocol constants. These are robustness invariants of the
@@ -61,10 +69,13 @@ const LOCK_TIMEOUT_MS = 2e3;
61
69
  * Hold the cross-process writer lock for `filename` around one operation. The
62
70
  * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
63
71
  * rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
64
- * only writers contend. Contention backs off exponentially and fails with a
65
- * timed-out error after the deadline. The contender never removes an existing
66
- * lock because file age cannot prove that its owner stopped; orphan recovery
67
- * is an operator action. The parent directory must exist.
72
+ * only writers contend. `EEXIST` is contention directly; an `EPERM` is
73
+ * contention only when a fresh `lstat` confirms the lock path exists, covering
74
+ * Windows exclusive-create behavior without hiding an unrelated permission
75
+ * failure. Contention backs off exponentially and fails with a timed-out error
76
+ * after the deadline. The contender never removes an existing lock because
77
+ * file age cannot prove that its owner stopped; orphan recovery is an operator
78
+ * action. The parent directory must exist.
68
79
  * @param filename - the file whose writers this lock serializes.
69
80
  * @param operation - the read-render-commit cycle to run while holding the lock.
70
81
  * @returns the operation's result; the lock releases on both outcomes.
@@ -81,7 +92,7 @@ async function withFileLock(filename, operation) {
81
92
  });
82
93
  break;
83
94
  } catch (error) {
84
- if (!isEEXIST(error)) throw error;
95
+ if (!await isLockContention(error, lockPath)) throw error;
85
96
  }
86
97
  if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
87
98
  await new Promise((resolve) => setTimeout(resolve, delay));
@@ -45,10 +45,13 @@ export declare function writeFileAtomic(filename: string, content: string, optio
45
45
  * Hold the cross-process writer lock for `filename` around one operation. The
46
46
  * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
47
47
  * rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
48
- * only writers contend. Contention backs off exponentially and fails with a
49
- * timed-out error after the deadline. The contender never removes an existing
50
- * lock because file age cannot prove that its owner stopped; orphan recovery
51
- * is an operator action. The parent directory must exist.
48
+ * only writers contend. `EEXIST` is contention directly; an `EPERM` is
49
+ * contention only when a fresh `lstat` confirms the lock path exists, covering
50
+ * Windows exclusive-create behavior without hiding an unrelated permission
51
+ * failure. Contention backs off exponentially and fails with a timed-out error
52
+ * after the deadline. The contender never removes an existing lock because
53
+ * file age cannot prove that its owner stopped; orphan recovery is an operator
54
+ * action. The parent directory must exist.
52
55
  * @param filename - the file whose writers this lock serializes.
53
56
  * @param operation - the read-render-commit cycle to run while holding the lock.
54
57
  * @returns the operation's result; the lock releases on both outcomes.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-atomic-write",
3
3
  "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)",
4
- "version": "0.1.0-rc.6",
4
+ "version": "0.1.0-rc.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,11 +32,11 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
35
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
36
36
  "@deepseek-ai/cordis": "^4.0.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
40
- "@deepseek-ai/cordis": "^4.0.1"
39
+ "@deepseek-ai/cordis": "^4.0.1",
40
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8"
41
41
  }
42
42
  }