@deepseek-ai/dsh-atomic-write 0.1.7-rc.1 → 0.1.7-rc.2
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 +54 -5
- package/README.md +6 -5
- package/README.zh.md +6 -5
- package/lib/index.js +73 -8
- package/lib/types/index.d.ts +13 -5
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
|
-
# Bilingual-pair consistency record (docs/i18n/README.md):
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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));
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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.
|
|
4
|
+
"version": "0.1.7-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|