@deepseek-ai/dsh-atomic-write 0.1.6-alpha.1 → 0.1.7-alpha.1

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: c3ce840f79c38fb7fac404786f0e5a7e41138d57
6
- README.zh.md: 9cc1f271f70e113637b88b7e3984d431e84c647f
5
+ README.md: fa2a77c8ba11e1dd80fecef6da09fa32a608b523
6
+ README.zh.md: 020d379c876733d5de11c63887de8d3168eac3b3
package/README.md CHANGED
@@ -33,7 +33,7 @@ Use `writeFileAtomic` when a file-backed store must replace one already-rendered
33
33
  import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
34
34
 
35
35
  declare const text: string
36
- await writeFileAtomic('/home/u/.dsh/settings.yaml', text, { mode: 0o600 })
36
+ await writeFileAtomic('/home/u/.dsh/cordis.patch.yml', text, { mode: 0o600 })
37
37
  ```
38
38
 
39
39
  Parent directories are created as needed, and readers observe either the old or the new complete content. On Windows, transient replacement interference reported as `EACCES`, `EBUSY`, or `EPERM` is retried for a bounded interval; any remaining failure removes the temporary file and leaves the target untouched.
@@ -48,9 +48,9 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
48
48
  declare const render: (previous: string) => string
49
49
  declare const readCurrent: () => Promise<string>
50
50
 
51
- await withFileLock('/home/u/.dsh/settings.yaml', async () => {
51
+ await withFileLock('/home/u/.dsh/cordis.patch.yml', async () => {
52
52
  const previous = await readCurrent()
53
- await writeFileAtomic('/home/u/.dsh/settings.yaml', render(previous), { mode: 0o600 })
53
+ await writeFileAtomic('/home/u/.dsh/cordis.patch.yml', render(previous), { mode: 0o600 })
54
54
  })
55
55
  ```
56
56
 
@@ -58,6 +58,8 @@ Only writers contend — readers never take the lock — and a contender backs o
58
58
 
59
59
  ### Failures to plan for
60
60
 
61
+ Windows retries one `EPERM` when the lock cannot be observed, because its holder can release between exclusive creation and the existence check. A repeated unconfirmed `EPERM` is rethrown without running the operation.
62
+
61
63
  The lock's parent directory must already exist, so `withFileLock` rejects an invalid parent hierarchy before running the operation. A process that exits while holding the lock leaves the lock sibling behind; later writers time out, and an operator removes it only after verifying that no writer still owns it.
62
64
 
63
65
  -----
@@ -98,7 +100,7 @@ The package is built on one separation: the atomic commit owns the swap, and the
98
100
 
99
101
  Read these pages when you need the consuming stores or the family this primitive belongs to.
100
102
 
101
- - [User-settings file store](../../settings/settings-file/README.md) — the settings document every write replaces through this package.
103
+ - [Profile configuration editor](../../boot/config-editor/README.md) — the profile patch every edit replaces through this package.
102
104
  - [Credentials store](../../credentials/credentials-local/README.md) — the credentials file this package locks and replaces.
103
105
  - [util group map](../README.md) — the zero-dependency utility family this package belongs to.
104
106
 
package/README.zh.md CHANGED
@@ -33,7 +33,7 @@ kind: "package-library"
33
33
  import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
34
34
 
35
35
  declare const text: string
36
- await writeFileAtomic('/home/u/.dsh/settings.yaml', text, { mode: 0o600 })
36
+ await writeFileAtomic('/home/u/.dsh/cordis.patch.yml', text, { mode: 0o600 })
37
37
  ```
38
38
 
39
39
  父目录会按需创建,读取方只会观察到旧内容或完整的新内容。在 Windows 上,报告为 `EACCES`、`EBUSY` 或 `EPERM` 的瞬时替换干扰会在有界时间内重试;任何剩余失败都会移除临时文件,并保持目标文件不变。
@@ -48,9 +48,9 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
48
48
  declare const render: (previous: string) => string
49
49
  declare const readCurrent: () => Promise<string>
50
50
 
51
- await withFileLock('/home/u/.dsh/settings.yaml', async () => {
51
+ await withFileLock('/home/u/.dsh/cordis.patch.yml', async () => {
52
52
  const previous = await readCurrent()
53
- await writeFileAtomic('/home/u/.dsh/settings.yaml', render(previous), { mode: 0o600 })
53
+ await writeFileAtomic('/home/u/.dsh/cordis.patch.yml', render(previous), { mode: 0o600 })
54
54
  })
55
55
  ```
56
56
 
@@ -58,6 +58,8 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
58
58
 
59
59
  ### 需要规划的失败
60
60
 
61
+ Windows 在无法观察到锁时会对 `EPERM` 重试一次,因为持锁方可能在独占创建与存在性检查之间释放锁。再次出现无法确认锁存在的 `EPERM` 时,会重新抛出错误且不运行操作。
62
+
61
63
  锁的父目录必须已经存在,因此 `withFileLock` 会在运行操作之前拒绝无效的父目录层级。持锁进程退出时会把锁文件留在原地;后续写入方超时失败,操作者只有在确认没有写入方仍持有该锁后才会移除它。
62
64
 
63
65
  -----
@@ -98,7 +100,7 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
98
100
 
99
101
  当你需要了解使用本原语的存储或它所属的工具家族时,阅读以下页面。
100
102
 
101
- - [用户设置文件存储](../../settings/settings-file/README.zh.md)——每次写入都通过本包替换的设置文档。
103
+ - [用户设置文件存储](../../boot/config-editor/README.zh.md)——每次写入都通过本包替换的设置文档。
102
104
  - [凭据存储](../../credentials/credentials-local/README.zh.md)——本包加锁并替换的凭据文件。
103
105
  - [util 组映射](../README.zh.md)——本包所属的零依赖工具家族。
104
106
 
package/lib/index.js CHANGED
@@ -109,8 +109,9 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
109
109
  * rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
110
110
  * only writers contend. `EEXIST` is contention directly; an `EPERM` is
111
111
  * contention only when a fresh `lstat` confirms the lock path exists, covering
112
- * Windows exclusive-create behavior without hiding an unrelated permission
113
- * failure. Contention backs off exponentially and fails with a timed-out error
112
+ * Windows exclusive-create behavior. Windows retries one unconfirmed EPERM
113
+ * because the holder can release before the probe; a repeated unconfirmed
114
+ * permission error is rethrown. Contention backs off exponentially and times out
114
115
  * after the deadline. The contender never removes an existing lock because
115
116
  * file age cannot prove that its owner stopped; orphan recovery is an operator
116
117
  * action. The parent directory must exist.
@@ -123,6 +124,7 @@ async function withFileLock(filename, operation, options) {
123
124
  const lockPath = `${filename}.lock`;
124
125
  const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
125
126
  let delay = LOCK_RETRY_INITIAL_MS;
127
+ let retriedUnconfirmedPermissionError = false;
126
128
  for (;;) {
127
129
  try {
128
130
  await writeFile(lockPath, `${process.pid}\n`, {
@@ -131,7 +133,10 @@ async function withFileLock(filename, operation, options) {
131
133
  });
132
134
  break;
133
135
  } catch (error) {
134
- if (!await isLockContention(error, lockPath)) throw error;
136
+ if (!await isLockContention(error, lockPath)) {
137
+ if (process.platform !== "win32" || error?.code !== "EPERM" || retriedUnconfirmedPermissionError) throw error;
138
+ retriedUnconfirmedPermissionError = true;
139
+ }
135
140
  }
136
141
  if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
137
142
  await new Promise((resolve) => setTimeout(resolve, delay));
@@ -61,8 +61,9 @@ export interface FileLockOptions {
61
61
  * rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
62
62
  * only writers contend. `EEXIST` is contention directly; an `EPERM` is
63
63
  * contention only when a fresh `lstat` confirms the lock path exists, covering
64
- * Windows exclusive-create behavior without hiding an unrelated permission
65
- * failure. Contention backs off exponentially and fails with a timed-out error
64
+ * Windows exclusive-create behavior. Windows retries one unconfirmed EPERM
65
+ * because the holder can release before the probe; a repeated unconfirmed
66
+ * permission error is rethrown. Contention backs off exponentially and times out
66
67
  * after the deadline. The contender never removes an existing lock because
67
68
  * file age cannot prove that its owner stopped; orphan recovery is an operator
68
69
  * action. The parent directory must exist.
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.6-alpha.1",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,9 +27,9 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
- "@deepseek-ai/cordis": "^4.0.2"
30
+ "@deepseek-ai/cordis": "^4.0.3"
31
31
  },
32
32
  "devDependencies": {
33
- "@deepseek-ai/cordis": "^4.0.2"
33
+ "@deepseek-ai/cordis": "^4.0.3"
34
34
  }
35
35
  }