@deepseek-ai/dsh-atomic-write 0.1.7-rc.1 → 0.2.0-rc.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
@@ -1,6 +1,55 @@
1
- # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
- # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
- # after editing either side, bring the other along and re-record with:
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
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: fa2a77c8ba11e1dd80fecef6da09fa32a608b523
6
- README.zh.md: 020d379c876733d5de11c63887de8d3168eac3b3
5
+ /:
6
+ en: 1065a4fcafd0430d
7
+ zh: 40bc911b6d01288b
8
+ /deepseek-ai-dsh-atomic-write:
9
+ en: ed491d62812ad8cc
10
+ zh: 653e184dd035ec71
11
+ /deepseek-ai-dsh-atomic-write/summary:
12
+ en: 6c4c49bda04ed05f
13
+ zh: 1080ddd179d0a3d2
14
+ /deepseek-ai-dsh-atomic-write/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /deepseek-ai-dsh-atomic-write/use-this-package:
18
+ en: 9cc27a0ee5ae0808
19
+ zh: d0ee06b54452f600
20
+ /deepseek-ai-dsh-atomic-write/use-this-package/writing-a-file-atomically:
21
+ en: 7b757dc3c3b27335
22
+ zh: a097d90a2561475d
23
+ /deepseek-ai-dsh-atomic-write/use-this-package/coordinating-writers:
24
+ en: 77c865c1f78d2a0c
25
+ zh: 5a91874c352c6f18
26
+ /deepseek-ai-dsh-atomic-write/use-this-package/failures-to-plan-for:
27
+ en: 59308dc757e25273
28
+ zh: 01113ed577a413cb
29
+ /deepseek-ai-dsh-atomic-write/understand-the-implementation:
30
+ en: 93b537308f7c88c7
31
+ zh: 282e0d2042aeb1be
32
+ /deepseek-ai-dsh-atomic-write/understand-the-implementation/source-map:
33
+ en: 725509a8b621c668
34
+ zh: c11e3ffdf73af844
35
+ /deepseek-ai-dsh-atomic-write/understand-the-implementation/write-path:
36
+ en: 2e6561c9282f0c0d
37
+ zh: 7f0e76f6dbdea501
38
+ /deepseek-ai-dsh-atomic-write/understand-the-implementation/why-the-swap-stays-safe:
39
+ en: 784e985c1fb3a25e
40
+ zh: 4afda1b9bf5f29cc
41
+ /deepseek-ai-dsh-atomic-write/further-exploration:
42
+ en: 16ece9b754f7fff6
43
+ zh: 2456fe14cc5a658b
44
+ /deepseek-ai-dsh-atomic-write/model-experience:
45
+ en: 225fc1d94a284905
46
+ zh: 7319a43c3b8a884d
47
+ /deepseek-ai-dsh-atomic-write/model-experience/kv-cache-effect:
48
+ en: 815e93723a98919c
49
+ zh: 921da870f67e4033
50
+ /deepseek-ai-dsh-atomic-write/known-limitations-and-deferred-work:
51
+ en: 5d0b395da8833bbb
52
+ zh: 6191bab46d288be7
53
+ /deepseek-ai-dsh-atomic-write/known-limitations-and-deferred-work/dev-note:
54
+ en: d3461ceeaf041974
55
+ zh: f59df2adbf52fed9
package/README.md CHANGED
@@ -54,13 +54,13 @@ await withFileLock('/home/u/.dsh/cordis.patch.yml', async () => {
54
54
  })
55
55
  ```
56
56
 
57
- Only writers contend — readers never take the lock — and a contender backs off exponentially and fails with a timed-out error rather than blocking forever. How long a contender waits is stated per call through `waitMs`: the default is sized for file work alone, so a holder whose cycle includes a network round trip — a credential mutation that refreshes an expired token — states a longer one, because leaving the default would fail every other writer of that file for the duration. The retry cadence stays fixed. A contender never removes an existing lock, because file age cannot prove that its owner stopped.
57
+ Only writers contend — readers never take the lock — and a contender backs off exponentially and fails with a timed-out error rather than blocking forever. How long a contender waits is stated per call through `waitMs`: the default is sized for file work alone, so a holder whose cycle includes a network round trip — a credential mutation that refreshes an expired token — states a longer one, because leaving the default would fail every other writer of that file for the duration. The retry cadence stays fixed. A contender removes an existing lock only when the lock names a process that no longer exists; file age alone never removes a lock.
58
58
 
59
59
  ### Failures to plan for
60
60
 
61
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
62
 
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.
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, and the next writer takes it over. A lock whose record is empty or incomplete, or names a PID that a live process reused, is not taken over; later writers time out, and an operator removes it only after verifying that no writer still owns it.
64
64
 
65
65
  -----
66
66
 
@@ -83,13 +83,13 @@ The package is built on one separation: the atomic commit owns the swap, and the
83
83
 
84
84
  `writeFileAtomic` writes a random-suffix sibling opened with exclusive create (`wx`), then renames it over the target. The exclusive open refuses to follow a symlink planted at a guessable temp path; the same-directory sibling keeps the rename on one filesystem; and the rename replaces a symlinked target itself instead of writing through to its referent. A Windows retry keeps the same complete sibling and uses bounded exponential backoff, so temporary use of the target by software outside the cooperative writer lock cannot turn a safe replacement into an immediate failure; the archived [retry decision record](../../../.agents/notes/archived/bug-fix/2026-08-29-windows-atomic-replace-retry.md) documents the original rationale and rejected alternatives.
85
85
 
86
- `withFileLock` creates a `<filename>.lock` sibling with `wx`. `EEXIST` identifies contention directly; `EPERM` does so only when a fresh `lstat` confirms the lock path exists, covering Windows exclusive-create behavior without hiding an unrelated permission failure. The lock records its creator's PID and is removed by the holder in a `finally`; contention backs off exponentially and fails when the per-call `waitMs` deadline (default two seconds) passes.
86
+ `withFileLock` creates a `<filename>.lock` sibling with `wx`. `EEXIST` identifies contention directly; `EPERM` does so only when a fresh `lstat` confirms the lock path exists, covering Windows exclusive-create behavior without hiding an unrelated permission failure. The lock records its creator's PID as `<pid>\n` and is removed by the holder in a `finally`. A contender that reads a record whose PID a signal probe reports as absent (`ESRCH`) creates a `<filename>.lock.takeover-<record hash>` claim with `wx`, re-reads the lock and probes its PID again, removes it only if it still holds the same record and that PID is still absent, removes the claim, and retries at once. A holder that exists under another user (`EPERM`) and a record naming the contender's own process are waited for. Contenders that read the same record contend for one claim, and the second probe rejects a holder that reused the exited PID, so a takeover never removes a lock that another contender acquired after the exited holder's. Takeover proves only that the recorded process exited; an operation that starts other writers leaves its successor a way to find them, as the [Plugin Manager](../../boot/plugin-manager/README.md) does for its pnpm runs. Contention backs off exponentially and fails when the per-call `waitMs` deadline (default two seconds) passes; the [takeover decision record](../../../.agents/notes/implemented/bug-fix/2026-09-24-exited-holder-lock-takeover.md) owns the rationale.
87
87
 
88
88
  ### Why the swap stays safe
89
89
 
90
90
  - **Fresh inode, caller-stated mode** — the temp carries `mode` through the rename, so narrowing a wider-permission file has no chmod race. `mode` is required so the permission decision stays visible at every call site.
91
91
  - **Readers never contend** — the rename commit is atomic, so a reader needs no lock.
92
- - **A contender never deletes a lock** — age cannot distinguish a crashed owner from a paused live writer; recovery is an operator action.
92
+ - **A contender deletes only an exited holder's lock** — age cannot distinguish a crashed owner from a paused live writer, but a missing process can.
93
93
 
94
94
  </details>
95
95
 
@@ -124,7 +124,8 @@ These limits define where the package is not the right tool. They are current pa
124
124
 
125
125
  - **Atomic, not durable** — no `fsync` of the file or its directory, so after a crash the rename may be observed unwound. The file-backed stores here re-read and republish on boot, keeping durability the caller's policy.
126
126
  - **String content only** — no `Buffer` or stream form until a consumer needs one.
127
- - **Orphaned locks require operator recovery** — a process that exits while holding the lock leaves the sibling behind; later writers time out without deleting it.
127
+ - **Some orphaned locks require operator recovery** — a lock whose record is empty or incomplete, or names a PID a live process reused, stays in place; later writers time out without deleting it. A contender that ends between creating a claim and removing the lock leaves both `<filename>.lock` and `<filename>.lock.takeover-<record hash>`, and the operator removes both.
128
+ - **One host and one PID namespace** — the probe runs on the contender's host. Writers on several hosts sharing a network filesystem, or containers sharing a volume, can see a live holder as exited, take over its lock, and write at the same time as it.
128
129
 
129
130
  <a id="dev-note"></a>
130
131
  ### Dev Note
package/README.zh.md CHANGED
@@ -54,13 +54,13 @@ await withFileLock('/home/u/.dsh/cordis.patch.yml', async () => {
54
54
  })
55
55
  ```
56
56
 
57
- 只有写入方会竞争——读取方从不取锁——竞争者按指数退避,超时后报错,而不是无限阻塞。竞争者等待多久由每次调用经 `waitMs` 声明:默认值只按纯文件工作量级选定,因此持锁方循环若包含一次网络往返——例如刷新过期 token 的凭据变更——就应声明更长的值,否则该文件的其他写入方在这段时间内都会失败。退避节奏保持固定。竞争者绝不移除已有锁,因为文件存续时间无法证明其持有者已经停止。
57
+ 只有写入方会竞争——读取方从不取锁——竞争者按指数退避,超时后报错,而不是无限阻塞。竞争者等待多久由每次调用经 `waitMs` 声明:默认值只按纯文件工作量级选定,因此持锁方循环若包含一次网络往返——例如刷新过期 token 的凭据变更——就应声明更长的值,否则该文件的其他写入方在这段时间内都会失败。退避节奏保持固定。只有当锁记录的进程已不存在时,竞争者才会移除已有锁;单凭文件存续时间绝不会移除锁。
58
58
 
59
59
  ### 需要规划的失败
60
60
 
61
61
  Windows 在无法观察到锁时会对 `EPERM` 重试一次,因为持锁方可能在独占创建与存在性检查之间释放锁。再次出现无法确认锁存在的 `EPERM` 时,会重新抛出错误且不运行操作。
62
62
 
63
- 锁的父目录必须已经存在,因此 `withFileLock` 会在运行操作之前拒绝无效的父目录层级。持锁进程退出时会把锁文件留在原地;后续写入方超时失败,操作者只有在确认没有写入方仍持有该锁后才会移除它。
63
+ 锁的父目录必须已经存在,因此 `withFileLock` 会在运行操作之前拒绝无效的父目录层级。持锁进程退出时会把锁文件留在原地,下一个写入方会接管它。锁记录为空或不完整,或其 PID 已被存活进程复用时,锁不会被接管;后续写入方超时失败,操作者只有在确认没有写入方仍持有该锁后才会移除它。
64
64
 
65
65
  -----
66
66
 
@@ -83,13 +83,13 @@ Windows 在无法观察到锁时会对 `EPERM` 重试一次,因为持锁方可
83
83
 
84
84
  `writeFileAtomic` 先以独占创建(`wx`)打开一个随机后缀的同级文件并写入内容,然后 rename 到目标上。独占打开拒绝跟随预先埋在可猜测临时路径上的符号链接;同目录兄弟文件保证 rename 落在同一文件系统上;rename 替换的是目标位置的符号链接本身,绝不写穿到该链接指向的文件。Windows 重试会保留同一份完整的兄弟文件,并采用有界指数退避,因此协作式写锁之外的软件瞬时占用目标时,不会让安全替换立即失败;已归档的[重试决策记录](../../../.agents/notes/archived/bug-fix/2026-08-29-windows-atomic-replace-retry.md)记录了最初的理由与被拒绝的替代方案。
85
85
 
86
- `withFileLock` 以 `wx` 创建 `<filename>.lock` 同级文件。`EEXIST` 直接表示竞争;只有一次新的 `lstat` 确认锁路径存在时,`EPERM` 才表示竞争,从而兼容 Windows 的独占创建行为,又不掩盖无关的权限故障。锁记录创建者的 PID,由持有者在 `finally` 中移除;竞争按指数退避,在每次调用声明的 `waitMs` 期限(默认两秒)过后失败。
86
+ `withFileLock` 以 `wx` 创建 `<filename>.lock` 同级文件。`EEXIST` 直接表示竞争;只有一次新的 `lstat` 确认锁路径存在时,`EPERM` 才表示竞争,从而兼容 Windows 的独占创建行为,又不掩盖无关的权限故障。锁以 `<pid>\n` 记录创建者的 PID,由持有者在 `finally` 中移除。竞争者读到的记录若经信号探测报告该 PID 不存在(`ESRCH`),就以 `wx` 创建 `<filename>.lock.takeover-<记录哈希>` 认领文件,重新读取锁并再次探测其 PID,仅当锁仍是同一条记录且该 PID 仍不存在时才移除它,随后移除认领文件并立即重试。以其他用户身份存在的持有者(`EPERM`)和指向竞争者自身进程的记录会继续等待。读到同一条记录的竞争者争用同一个认领文件,第二次探测会排除复用了已退出 PID 的持有者,因此接管绝不会移除另一个竞争者在已退出持有者之后获得的锁。接管只能证明记录中的进程已退出;启动了其他写入者的操作要为后继者留下找到它们的途径,[Plugin Manager](../../boot/plugin-manager/README.zh.md) 对其 pnpm 运行就是这样做的。竞争按指数退避,在每次调用声明的 `waitMs` 期限(默认两秒)过后失败;[接管决策记录](../../../.agents/notes/implemented/bug-fix/2026-09-24-exited-holder-lock-takeover.zh.md)负责说明理由。
87
87
 
88
88
  ### 交换为何安全
89
89
 
90
90
  - **全新 inode,调用方声明的权限位**——临时文件带着 `mode` 走完 rename,因此收窄权限过宽的文件没有 chmod 竞态。`mode` 为必填,让权限决策始终可见于每个调用点。
91
91
  - **读取方从不竞争**——rename 提交是原子的,读取方无需加锁。
92
- - **竞争者绝不移除锁**——文件存续时间无法区分已崩溃的所有者与暂停但仍存活的写入方;恢复是操作者的动作。
92
+ - **竞争者只移除已退出持有者的锁**——文件存续时间无法区分已崩溃的所有者与暂停但仍存活的写入方,但进程已不存在可以区分。
93
93
 
94
94
  </details>
95
95
 
@@ -124,7 +124,8 @@ Windows 在无法观察到锁时会对 `EPERM` 重试一次,因为持锁方可
124
124
 
125
125
  - **原子但不保证持久**——不对文件或其所在目录做 `fsync`,因此崩溃后可能观察到 rename 被回退。此处的文件型存储在启动时重新读取并重新发布,把持久性留作调用方的策略。
126
126
  - **仅支持字符串内容**——在有消费方需要之前,不提供 `Buffer` 或流式形态。
127
- - **遗留锁需要操作者恢复**——持锁进程退出时可能留下同级锁文件;后续写入方超时也不会删除它。
127
+ - **部分遗留锁需要操作者恢复**——锁记录为空或不完整,或其 PID 已被存活进程复用时,锁会留在原地;后续写入方超时也不会删除它。竞争者若在创建认领文件与移除锁之间结束,会同时留下 `<filename>.lock` 和 `<filename>.lock.takeover-<记录哈希>`,由操作者删除两者。
128
+ - **单一主机与单一 PID 命名空间**——探测在竞争者所在主机上进行。多台主机通过网络文件系统共享、或多个容器共享同一卷时,写入方可能把存活持有者视为已退出,接管其锁并与之同时写入。
128
129
 
129
130
  <a id="dev-note"></a>
130
131
  ### 开发备注
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { randomBytes } from "node:crypto";
2
- import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  //#region lib/types/index.js
5
5
  /**
@@ -10,7 +10,8 @@ import { dirname } from "node:path";
10
10
  * up with exactly the stated mode. `withFileLock` serializes cross-process
11
11
  * writers of one file through a `wx`-created `<file>.lock` sibling, so a
12
12
  * read-modify-write cycle can never resurrect a state another writer just
13
- * replaced; readers stay lock-free because the rename commit is atomic.
13
+ * replaced; readers stay lock-free because the rename commit is atomic. A lock
14
+ * whose recorded holder process no longer exists is taken over.
14
15
  * @module @deepseek-ai/dsh-atomic-write
15
16
  */
16
17
  const WINDOWS_TRANSIENT_RENAME_ERRORS = new Set([
@@ -86,6 +87,63 @@ async function isLockContention(error, lockPath) {
86
87
  return false;
87
88
  }
88
89
  }
90
+ /** Whether the holder a `<pid>\n` record names is proven gone: a signal probe finds no such process. */
91
+ function holderExited(record) {
92
+ if (!/^\d+\n$/.test(record)) return false;
93
+ const pid = Number(record.trim());
94
+ if (pid === 0 || pid > 2147483647) return false;
95
+ if (pid === process.pid) return false;
96
+ try {
97
+ process.kill(pid, 0);
98
+ return false;
99
+ } catch (error) {
100
+ return error.code === "ESRCH";
101
+ }
102
+ }
103
+ /** The lock file's content, or undefined when it cannot be read. */
104
+ async function readLockRecord(lockPath) {
105
+ try {
106
+ return await readFile(lockPath, "utf8");
107
+ } catch (error) {
108
+ return;
109
+ }
110
+ }
111
+ /**
112
+ * Remove the lock when its recorded holder exited. Contenders that read the
113
+ * same record serialize on a claim file named after it. Under the claim, the
114
+ * claimant re-reads the lock and probes its PID again, and removes it only
115
+ * when it still holds that record and that PID is still gone: the record's
116
+ * holder can no longer release it, and no other contender can remove it
117
+ * without the claim, so a removal never deletes a lock another contender
118
+ * acquired after the dead holder's, including one whose holder reused the PID.
119
+ * @returns Whether this call removed the dead holder's lock.
120
+ */
121
+ async function takeOverExitedLock(lockPath) {
122
+ const record = await readLockRecord(lockPath);
123
+ if (record === void 0 || !holderExited(record)) return false;
124
+ const claim = `${lockPath}.takeover-${createHash("sha256").update(record).digest("hex").slice(0, 16)}`;
125
+ try {
126
+ await writeFile(claim, `${process.pid}\n`, {
127
+ mode: 384,
128
+ flag: "wx"
129
+ });
130
+ } catch (error) {
131
+ const code = error.code;
132
+ if (code === "EEXIST" || code === "EPERM") return false;
133
+ throw error;
134
+ }
135
+ try {
136
+ if (await readLockRecord(lockPath) !== record || !holderExited(record)) return false;
137
+ try {
138
+ await rm(lockPath, { force: true });
139
+ } catch (error) {
140
+ return false;
141
+ }
142
+ return true;
143
+ } finally {
144
+ await rm(claim, { force: true }).catch((error) => {});
145
+ }
146
+ }
89
147
  /**
90
148
  * Retry cadence for a contended lock. These stay robustness invariants of the
91
149
  * cross-process write protocol rather than deployment tunables: they govern how
@@ -111,10 +169,17 @@ const DEFAULT_LOCK_WAIT_MS = 2e3;
111
169
  * contention only when a fresh `lstat` confirms the lock path exists, covering
112
170
  * Windows exclusive-create behavior. Windows retries one unconfirmed EPERM
113
171
  * because the holder can release before the probe; a repeated unconfirmed
114
- * permission error is rethrown. Contention backs off exponentially and times out
115
- * after the deadline. The contender never removes an existing lock because
116
- * file age cannot prove that its owner stopped; orphan recovery is an operator
117
- * action. The parent directory must exist.
172
+ * permission error is rethrown. The lock records its holder's PID. A contender
173
+ * removes the lock and retries at once when no process with that PID exists
174
+ * (`ESRCH`); any other lock, including one whose holder exists under another
175
+ * user (`EPERM`) or whose record is incomplete, is waited for. Contention backs
176
+ * off exponentially and times out after the deadline. A holder whose PID a
177
+ * live process reused keeps its lock until an operator removes it. Takeover
178
+ * proves only that the recorded process exited: an operation that starts other
179
+ * writers must stop them with it or leave its successor a way to find them.
180
+ * PIDs are compared on the contender's host, so writers on other hosts or in
181
+ * other PID namespaces sharing the file are unsupported and could both hold
182
+ * the lock. The parent directory must exist.
118
183
  * @param filename - the file whose writers this lock serializes.
119
184
  * @param operation - the read-render-commit cycle to run while holding the lock.
120
185
  * @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
@@ -136,7 +201,7 @@ async function withFileLock(filename, operation, options) {
136
201
  if (!await isLockContention(error, lockPath)) {
137
202
  if (process.platform !== "win32" || error?.code !== "EPERM" || retriedUnconfirmedPermissionError) throw error;
138
203
  retriedUnconfirmedPermissionError = true;
139
- }
204
+ } else if (await takeOverExitedLock(lockPath)) continue;
140
205
  }
141
206
  if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
142
207
  await new Promise((resolve) => setTimeout(resolve, delay));
@@ -6,7 +6,8 @@
6
6
  * up with exactly the stated mode. `withFileLock` serializes cross-process
7
7
  * writers of one file through a `wx`-created `<file>.lock` sibling, so a
8
8
  * read-modify-write cycle can never resurrect a state another writer just
9
- * replaced; readers stay lock-free because the rename commit is atomic.
9
+ * replaced; readers stay lock-free because the rename commit is atomic. A lock
10
+ * whose recorded holder process no longer exists is taken over.
10
11
  * @module @deepseek-ai/dsh-atomic-write
11
12
  */
12
13
  /**
@@ -63,10 +64,17 @@ export interface FileLockOptions {
63
64
  * contention only when a fresh `lstat` confirms the lock path exists, covering
64
65
  * Windows exclusive-create behavior. Windows retries one unconfirmed EPERM
65
66
  * because the holder can release before the probe; a repeated unconfirmed
66
- * permission error is rethrown. Contention backs off exponentially and times out
67
- * after the deadline. The contender never removes an existing lock because
68
- * file age cannot prove that its owner stopped; orphan recovery is an operator
69
- * action. The parent directory must exist.
67
+ * permission error is rethrown. The lock records its holder's PID. A contender
68
+ * removes the lock and retries at once when no process with that PID exists
69
+ * (`ESRCH`); any other lock, including one whose holder exists under another
70
+ * user (`EPERM`) or whose record is incomplete, is waited for. Contention backs
71
+ * off exponentially and times out after the deadline. A holder whose PID a
72
+ * live process reused keeps its lock until an operator removes it. Takeover
73
+ * proves only that the recorded process exited: an operation that starts other
74
+ * writers must stop them with it or leave its successor a way to find them.
75
+ * PIDs are compared on the contender's host, so writers on other hosts or in
76
+ * other PID namespaces sharing the file are unsupported and could both hold
77
+ * the lock. The parent directory must exist.
70
78
  * @param filename - the file whose writers this lock serializes.
71
79
  * @param operation - the read-render-commit cycle to run while holding the lock.
72
80
  * @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
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.7-rc.1",
4
+ "version": "0.2.0-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },