@deepseek-ai/dsh-atomic-write 0.1.0-rc.8 → 0.1.1-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 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: 4d0b55291955c9d37f4788c7d37ad8e6ce728f70
6
- README.zh.md: c2d7f0b49fa123befbb663ac43862a40b4ef19b4
5
+ README.md: 0e6501c0ae35bf26d7df67f28ce0ae24c85ecca6
6
+ README.zh.md: 377f85f5c4aab74e8c04068b616de32a8ba063d6
package/README.md CHANGED
@@ -30,6 +30,8 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
30
30
 
31
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
+ How long a contender waits is a property of the operation the holder runs, so it is stated per call through `waitMs`. The default is sized for file work alone; 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: it governs how often a contender asks, which no caller has a reason to vary.
34
+
33
35
  ## Model Experience
34
36
 
35
37
  None, as this is a pure filesystem primitive; nothing here reaches a model request.
package/README.zh.md CHANGED
@@ -30,6 +30,8 @@ await withFileLock('/home/u/.dsh/settings.yaml', async () => {
30
30
 
31
31
  `withFileLock` 跨进程串行化同一文件的写入方,服务于单靠原子提交无法保证安全的读-渲染-提交循环。锁是以 `wx` 创建的同目录 `<filename>.lock`,因此读取方从不参与竞争;等待方按指数退避,超时即失败而非无限阻塞。`EEXIST` 直接表示竞争;只有一次新的 `lstat` 确认锁路径存在时,`EPERM` 才表示竞争,从而兼容 Windows 的独占创建行为,又不掩盖无关的权限故障。竞争者绝不移除现有锁:锁龄无法区分已经崩溃的所有者与被暂停但仍存活的写入方。
32
32
 
33
+ 等待多久是持锁方所跑操作的属性,因此由每次调用经 `waitMs` 声明。默认值只按纯文件工作量级选定;若持锁方的循环包含一次网络往返——例如刷新过期 token 的凭据变更——就应声明更长的值,否则该文件的其他写入方会在这段时间内全部失败。退避节奏保持固定:它决定竞争者多久问一次,调用方没有理由改变它。
34
+
33
35
  ## 模型体验
34
36
 
35
37
  无:本包是纯文件系统原语,此处没有任何内容会到达模型请求。
package/lib/index.js CHANGED
@@ -57,14 +57,22 @@ async function isLockContention(error, lockPath) {
57
57
  }
58
58
  }
59
59
  /**
60
- * Writer-lock protocol constants. These are robustness invariants of the
61
- * cross-process write protocol, not deployment tunables: contention normally
62
- * resolves within the retry deadline, while expiry fails the contender without
63
- * guessing whether the existing lock still has an owner.
60
+ * Retry cadence for a contended lock. These stay robustness invariants of the
61
+ * cross-process write protocol rather than deployment tunables: they govern how
62
+ * often a contender asks, which no caller has a reason to vary.
64
63
  */
65
64
  const LOCK_RETRY_INITIAL_MS = 20;
66
65
  const LOCK_RETRY_MAX_MS = 200;
67
- const LOCK_TIMEOUT_MS = 2e3;
66
+ /**
67
+ * How long a contender waits when the caller states no limit — sized for the
68
+ * render-and-rename cycle every call site had when this package was written.
69
+ * Expiry fails the contender rather than guessing whether the existing lock
70
+ * still has an owner. How long is *worth* waiting is a property of the
71
+ * operation the lock holder runs, which is why {@link FileLockOptions.waitMs}
72
+ * exists; the value here is the floor for an operation that does file work
73
+ * alone.
74
+ */
75
+ const DEFAULT_LOCK_WAIT_MS = 2e3;
68
76
  /**
69
77
  * Hold the cross-process writer lock for `filename` around one operation. The
70
78
  * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
@@ -78,11 +86,12 @@ const LOCK_TIMEOUT_MS = 2e3;
78
86
  * action. The parent directory must exist.
79
87
  * @param filename - the file whose writers this lock serializes.
80
88
  * @param operation - the read-render-commit cycle to run while holding the lock.
89
+ * @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
81
90
  * @returns the operation's result; the lock releases on both outcomes.
82
91
  */
83
- async function withFileLock(filename, operation) {
92
+ async function withFileLock(filename, operation, options) {
84
93
  const lockPath = `${filename}.lock`;
85
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
94
+ const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
86
95
  let delay = LOCK_RETRY_INITIAL_MS;
87
96
  for (;;) {
88
97
  try {
@@ -41,6 +41,18 @@ export interface WriteFileAtomicOptions {
41
41
  * @param options - permission bits for the replacement inode.
42
42
  */
43
43
  export declare function writeFileAtomic(filename: string, content: string, options: WriteFileAtomicOptions): Promise<void>;
44
+ /** Options for one {@link withFileLock} acquisition. */
45
+ export interface FileLockOptions {
46
+ /**
47
+ * Maximum time to wait for the lock, in milliseconds. State one when the
48
+ * holder's operation legitimately runs longer than file work — a credential
49
+ * mutation that refreshes a token performs a network round trip while
50
+ * holding the lock, and leaving the default in place would fail every other
51
+ * writer of the same file for the duration. Waiting is productive: a
52
+ * contender that acquires the lock afterwards re-reads the committed state.
53
+ */
54
+ waitMs?: number;
55
+ }
44
56
  /**
45
57
  * Hold the cross-process writer lock for `filename` around one operation. The
46
58
  * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
@@ -54,7 +66,8 @@ export declare function writeFileAtomic(filename: string, content: string, optio
54
66
  * action. The parent directory must exist.
55
67
  * @param filename - the file whose writers this lock serializes.
56
68
  * @param operation - the read-render-commit cycle to run while holding the lock.
69
+ * @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
57
70
  * @returns the operation's result; the lock releases on both outcomes.
58
71
  */
59
- export declare function withFileLock<T>(filename: string, operation: () => Promise<T>): Promise<T>;
72
+ export declare function withFileLock<T>(filename: string, operation: () => Promise<T>, options?: FileLockOptions): Promise<T>;
60
73
  //# sourceMappingURL=index.d.ts.map
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.8",
4
+ "version": "0.1.1-rc.2",
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.8",
36
- "@deepseek-ai/cordis": "^4.0.1"
35
+ "@deepseek-ai/cordis": "^4.0.1",
36
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
40
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8"
40
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
41
41
  }
42
42
  }