@deepseek-ai/dsh-atomic-write 0.0.1-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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
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:
4
+ # pnpm run verify-translation-pairing --write packages/util/atomic-write/README.md
5
+ README.md: 2ff4abb6ac10d8b592ccd2056b4f1f92cc8518b0
6
+ README.zh.md: 570a5243aefbe6870c1991786f711adc77faacae
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # dsh-atomic-write
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Zero-dependency atomic file replacement shared by file-backed stores that must never leave partial, symlink-hijacked, or wider-than-intended content on disk — the user-settings document (`dsh-settings-local`) and the credentials store (`dsh-credentials-local`).
6
+
7
+ ## Surface
8
+
9
+ ```ts
10
+ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
11
+
12
+ declare const text: string
13
+ declare const render: (previous: string) => string
14
+
15
+ await writeFileAtomic('/home/u/.dsh/settings.yaml', text, { mode: 0o600 })
16
+
17
+ // Read-modify-write against the same file from several processes.
18
+ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
19
+ await writeFileAtomic('/home/u/.dsh/settings.yaml', render(text), { mode: 0o600 })
20
+ })
21
+ ```
22
+
23
+ `writeFileAtomic` commits one already-rendered string. The contract, in the order failures would exploit it:
24
+
25
+ - **Exclusive-create temp** (`wx`, random suffix): the open refuses to follow a symlink planted at a guessable temp path.
26
+ - **The fresh inode carries `mode` through the rename**: replacing a wider-permission file narrows it without a chmod race. `mode` is required so the permission decision stays visible at every call site (subject to the process umask, like every fresh inode).
27
+ - **`rename` replaces a symlinked target itself**, never writing through to its referent.
28
+ - **Same-directory sibling** keeps the rename on one filesystem, so the swap stays atomic.
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
+
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.
32
+
33
+ ## Model Experience
34
+
35
+ None, as this is a pure filesystem primitive; nothing here reaches a model request.
36
+
37
+ #### KV Cache effect
38
+
39
+ None; nothing here enters a request prefix.
40
+
41
+ ## Known Limitations and Deferred Work
42
+
43
+ - **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.
44
+ - **String content only** — no `Buffer` or stream form until a consumer needs one.
45
+ - **Orphaned locks require operator recovery** — a process that exits while holding the lock can leave the sibling behind. Later writers time out without deleting it; an operator removes it only after verifying that no writer still owns it. File age alone is not safe evidence of abandonment.
package/README.zh.md ADDED
@@ -0,0 +1,45 @@
1
+ # dsh-atomic-write
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 零依赖的原子文件替换,供绝不允许在磁盘上留下不完整、被符号链接劫持或权限过宽内容的文件型存储共用:用户设置文档(`dsh-settings-local`)与凭据存储(`dsh-credentials-local`)。
6
+
7
+ ## 接口面
8
+
9
+ ```ts
10
+ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
11
+
12
+ declare const text: string
13
+ declare const render: (previous: string) => string
14
+
15
+ await writeFileAtomic('/home/u/.dsh/settings.yaml', text, { mode: 0o600 })
16
+
17
+ // Read-modify-write against the same file from several processes.
18
+ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
19
+ await writeFileAtomic('/home/u/.dsh/settings.yaml', render(text), { mode: 0o600 })
20
+ })
21
+ ```
22
+
23
+ `writeFileAtomic` 提交一份已经渲染好的字符串。约定按故障利用它的先后顺序列出:
24
+
25
+ - **独占创建临时文件**(`wx` + 随机后缀):open 拒绝跟随预先埋在可猜测临时路径上的符号链接。
26
+ - **全新 inode 携带 `mode` 走完 rename**:替换权限过宽的旧文件时直接收窄,不存在 chmod 竞态。`mode` 为必填,让权限决策始终可见于每个调用点(与所有新建 inode 一样受进程 umask 影响)。
27
+ - **`rename` 替换的是符号链接目标本身**,绝不写穿到其指向的文件。
28
+ - **同目录兄弟文件**保证 rename 落在同一文件系统上,交换保持原子。
29
+ - 自动创建父目录;任何失败都会移除临时文件并重新抛出该失败;读取方只会观察到旧内容或完整的新内容。
30
+
31
+ `withFileLock` 跨进程串行化同一文件的写入方,服务于单靠原子提交无法保证安全的读-渲染-提交循环。锁是以 `wx` 创建的同目录 `<filename>.lock`,因此读取方从不参与竞争;等待方按指数退避,超时即失败而非无限阻塞。竞争者绝不移除现有锁:锁龄无法区分已经崩溃的所有者与被暂停但仍存活的写入方。
32
+
33
+ ## 模型体验
34
+
35
+ 无:本包是纯文件系统原语,此处没有任何内容会到达模型请求。
36
+
37
+ #### KV Cache 影响
38
+
39
+ 无;此处没有任何内容会进入请求前缀。
40
+
41
+ ## 已知限制与暂缓事项
42
+
43
+ - **原子但不保证持久**——不对文件或其所在目录做 `fsync`,因此崩溃后可能观察到 rename 被回退。此处的文件型存储在启动时重新读取并重新发布,把持久性留作调用方的策略。
44
+ - **仅支持字符串内容**——在有消费方需要之前,不提供 `Buffer` 或流式形态。
45
+ - **遗留锁需要操作者恢复**——进程持锁退出时可能留下同级锁文件。后续写入方超时也不会删除它;操作者只有在确认没有写入方仍拥有该锁后才会移除。文件存续时间本身不能安全证明它已无人持有。
package/lib/index.js ADDED
@@ -0,0 +1,97 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ //#region lib/types/index.js
5
+ /**
6
+ * Zero-dependency atomic file replacement and writer coordination.
7
+ * `writeFileAtomic` writes a random-suffix sibling with exclusive create and
8
+ * the caller's permission bits, then renames it over the target, so readers
9
+ * observe either the old or the new complete content and a replaced file ends
10
+ * up with exactly the stated mode. `withFileLock` serializes cross-process
11
+ * writers of one file through a `wx`-created `<file>.lock` sibling, so a
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.
14
+ * @module @deepseek-ai/dsh-atomic-write
15
+ */
16
+ /**
17
+ * Replace `filename` with `content` in one atomic step, creating parent
18
+ * directories. The content is first written to a random-suffix sibling opened
19
+ * with exclusive create (`wx`): the open refuses to follow a symlink planted
20
+ * at the temp path, and the fresh inode carries `options.mode` through the
21
+ * rename, so replacing a wider-permission file narrows it without a chmod
22
+ * race. The rename also replaces a symlinked target itself instead of writing
23
+ * through to its referent, and the same-directory sibling keeps the rename on
24
+ * one filesystem. On any failure the temp file is removed and the failure
25
+ * rethrown. Crash durability (fsync) is out of scope.
26
+ * @param filename - final path receiving the content.
27
+ * @param content - complete next file content.
28
+ * @param options - permission bits for the replacement inode.
29
+ */
30
+ async function writeFileAtomic(filename, content, options) {
31
+ await mkdir(dirname(filename), {
32
+ recursive: true,
33
+ ...options.dirMode === void 0 ? {} : { mode: options.dirMode }
34
+ });
35
+ const temp = `${filename}.${randomBytes(6).toString("hex")}.tmp`;
36
+ try {
37
+ await writeFile(temp, content, {
38
+ mode: options.mode,
39
+ flag: "wx"
40
+ });
41
+ await rename(temp, filename);
42
+ } catch (error) {
43
+ await rm(temp, { force: true });
44
+ throw error;
45
+ }
46
+ }
47
+ /** Whether an exclusive create failed because the path already exists. */
48
+ function isEEXIST(error) {
49
+ return error?.code === "EEXIST";
50
+ }
51
+ /**
52
+ * Writer-lock protocol constants. These are robustness invariants of the
53
+ * cross-process write protocol, not deployment tunables: contention normally
54
+ * resolves within the retry deadline, while expiry fails the contender without
55
+ * guessing whether the existing lock still has an owner.
56
+ */
57
+ const LOCK_RETRY_INITIAL_MS = 20;
58
+ const LOCK_RETRY_MAX_MS = 200;
59
+ const LOCK_TIMEOUT_MS = 2e3;
60
+ /**
61
+ * Hold the cross-process writer lock for `filename` around one operation. The
62
+ * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
63
+ * 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.
68
+ * @param filename - the file whose writers this lock serializes.
69
+ * @param operation - the read-render-commit cycle to run while holding the lock.
70
+ * @returns the operation's result; the lock releases on both outcomes.
71
+ */
72
+ async function withFileLock(filename, operation) {
73
+ const lockPath = `${filename}.lock`;
74
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
75
+ let delay = LOCK_RETRY_INITIAL_MS;
76
+ for (;;) {
77
+ try {
78
+ await writeFile(lockPath, `${process.pid}\n`, {
79
+ mode: 384,
80
+ flag: "wx"
81
+ });
82
+ break;
83
+ } catch (error) {
84
+ if (!isEEXIST(error)) throw error;
85
+ }
86
+ if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
87
+ await new Promise((resolve) => setTimeout(resolve, delay));
88
+ delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS);
89
+ }
90
+ try {
91
+ return await operation();
92
+ } finally {
93
+ await rm(lockPath, { force: true });
94
+ }
95
+ }
96
+ //#endregion
97
+ export { withFileLock, writeFileAtomic };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-atomic-write`.
4
+ * @module @deepseek-ai/dsh-atomic-write/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-atomic-write";
7
+ /** Cordis companion plugin name. */
8
+ const name = "atomic-write-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this pure filesystem primitive owns no event stream or mutable runtime
13
+ * data; its replacement contract is enforced by unit tests.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Zero-dependency atomic file replacement and writer coordination.
3
+ * `writeFileAtomic` writes a random-suffix sibling with exclusive create and
4
+ * the caller's permission bits, then renames it over the target, so readers
5
+ * observe either the old or the new complete content and a replaced file ends
6
+ * up with exactly the stated mode. `withFileLock` serializes cross-process
7
+ * writers of one file through a `wx`-created `<file>.lock` sibling, so a
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.
10
+ * @module @deepseek-ai/dsh-atomic-write
11
+ */
12
+ /**
13
+ * Filesystem options for {@link writeFileAtomic}; `mode` is required so the
14
+ * permission decision stays visible at every call site.
15
+ */
16
+ export interface WriteFileAtomicOptions {
17
+ /**
18
+ * Permission bits stamped on the fresh temp inode and carried through the
19
+ * rename (subject to the process umask, like every fresh inode).
20
+ */
21
+ mode: number;
22
+ /**
23
+ * Permission bits for parent directories this call creates (subject to the
24
+ * umask; existing directories keep their mode). Omission uses the mkdir
25
+ * default — pass `0o700` when the tree holds user-private data.
26
+ */
27
+ dirMode?: number;
28
+ }
29
+ /**
30
+ * Replace `filename` with `content` in one atomic step, creating parent
31
+ * directories. The content is first written to a random-suffix sibling opened
32
+ * with exclusive create (`wx`): the open refuses to follow a symlink planted
33
+ * at the temp path, and the fresh inode carries `options.mode` through the
34
+ * rename, so replacing a wider-permission file narrows it without a chmod
35
+ * race. The rename also replaces a symlinked target itself instead of writing
36
+ * through to its referent, and the same-directory sibling keeps the rename on
37
+ * one filesystem. On any failure the temp file is removed and the failure
38
+ * rethrown. Crash durability (fsync) is out of scope.
39
+ * @param filename - final path receiving the content.
40
+ * @param content - complete next file content.
41
+ * @param options - permission bits for the replacement inode.
42
+ */
43
+ export declare function writeFileAtomic(filename: string, content: string, options: WriteFileAtomicOptions): Promise<void>;
44
+ /**
45
+ * Hold the cross-process writer lock for `filename` around one operation. The
46
+ * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
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.
52
+ * @param filename - the file whose writers this lock serializes.
53
+ * @param operation - the read-render-commit cycle to run while holding the lock.
54
+ * @returns the operation's result; the lock releases on both outcomes.
55
+ */
56
+ export declare function withFileLock<T>(filename: string, operation: () => Promise<T>): Promise<T>;
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-atomic-write`.
3
+ * @module @deepseek-ai/dsh-atomic-write/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "atomic-write-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-atomic-write",
3
+ "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/util/atomic-write"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
36
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1"
37
+ },
38
+ "devDependencies": {
39
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
40
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
41
+ }
42
+ }