@demlik/tea 0.21.0 → 0.22.0

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.
Files changed (126) hide show
  1. package/dist/agent/index.d.ts +32 -19
  2. package/dist/agent/index.js +12 -10
  3. package/dist/{chunk-VM7PMFV5.js → chunk-2A5XTKQQ.js} +142 -19
  4. package/dist/chunk-2A5XTKQQ.js.map +1 -0
  5. package/dist/{chunk-EB5OMOXX.js → chunk-7K7SEYCH.js} +16 -15
  6. package/dist/chunk-7K7SEYCH.js.map +1 -0
  7. package/dist/{chunk-GAVLND5C.js → chunk-A3QPMX5M.js} +17 -10
  8. package/dist/chunk-A3QPMX5M.js.map +1 -0
  9. package/dist/{chunk-VHKDUBXC.js → chunk-BVFS3ICC.js} +3 -3
  10. package/dist/{chunk-VHKDUBXC.js.map → chunk-BVFS3ICC.js.map} +1 -1
  11. package/dist/{chunk-4I2LCQKB.js → chunk-CPE5ZQXQ.js} +3 -3
  12. package/dist/{chunk-4I2LCQKB.js.map → chunk-CPE5ZQXQ.js.map} +1 -1
  13. package/dist/chunk-DEAPNMKO.js +11 -0
  14. package/dist/chunk-DEAPNMKO.js.map +1 -0
  15. package/dist/{chunk-BFSZ4L6Y.js → chunk-ETRNSJCR.js} +4 -4
  16. package/dist/{chunk-BFSZ4L6Y.js.map → chunk-ETRNSJCR.js.map} +1 -1
  17. package/dist/chunk-FXWDDNGD.js +98 -0
  18. package/dist/chunk-FXWDDNGD.js.map +1 -0
  19. package/dist/chunk-GFGOQIEC.js +10 -0
  20. package/dist/chunk-GFGOQIEC.js.map +1 -0
  21. package/dist/{chunk-YDF6P74H.js → chunk-IAHEYPYJ.js} +3 -3
  22. package/dist/{chunk-YDF6P74H.js.map → chunk-IAHEYPYJ.js.map} +1 -1
  23. package/dist/{chunk-YCJPLPQQ.js → chunk-J5JNLXO7.js} +3 -3
  24. package/dist/{chunk-YCJPLPQQ.js.map → chunk-J5JNLXO7.js.map} +1 -1
  25. package/dist/{chunk-MBQR3LFW.js → chunk-NFOGYDRS.js} +5 -5
  26. package/dist/{chunk-MBQR3LFW.js.map → chunk-NFOGYDRS.js.map} +1 -1
  27. package/dist/{chunk-Z7IL735U.js → chunk-R2YNDRGL.js} +3 -3
  28. package/dist/{chunk-Z7IL735U.js.map → chunk-R2YNDRGL.js.map} +1 -1
  29. package/dist/{chunk-ZV4WGW74.js → chunk-R5NXOPL2.js} +3 -3
  30. package/dist/{chunk-ZV4WGW74.js.map → chunk-R5NXOPL2.js.map} +1 -1
  31. package/dist/chunk-RMBZ7YH7.js +52 -0
  32. package/dist/chunk-RMBZ7YH7.js.map +1 -0
  33. package/dist/{chunk-HU3Y2AZY.js → chunk-SHXUSTHM.js} +62 -15
  34. package/dist/chunk-SHXUSTHM.js.map +1 -0
  35. package/dist/{chunk-QIE4Y6BK.js → chunk-UGTC33VH.js} +4 -4
  36. package/dist/{chunk-QIE4Y6BK.js.map → chunk-UGTC33VH.js.map} +1 -1
  37. package/dist/{chunk-6NIQIUGR.js → chunk-VH5MYJ2K.js} +4 -4
  38. package/dist/{chunk-6NIQIUGR.js.map → chunk-VH5MYJ2K.js.map} +1 -1
  39. package/dist/{chunk-W6XWI6QE.js → chunk-WZEDIUWR.js} +8 -7
  40. package/dist/chunk-WZEDIUWR.js.map +1 -0
  41. package/dist/{chunk-TMYUDZLE.js → chunk-ZRRET3RP.js} +11 -11
  42. package/dist/chunk-ZRRET3RP.js.map +1 -0
  43. package/dist/{core-BKJW1Kmq.d.ts → core-DNDAEhO2.d.ts} +7 -10
  44. package/dist/devtools/index.d.ts +4 -4
  45. package/dist/do/index.d.ts +98 -35
  46. package/dist/do/index.js +40 -21
  47. package/dist/do/index.js.map +1 -1
  48. package/dist/{drive-loop-Dc6Z8Vbs.d.ts → drive-loop-DlOEv49c.d.ts} +2 -2
  49. package/dist/{durable-effects-CgrLNccA.d.ts → durable-effects-wLWEdy9H.d.ts} +1 -1
  50. package/dist/effect/index.d.ts +52 -26
  51. package/dist/effect/index.js +7 -31
  52. package/dist/effect/index.js.map +1 -1
  53. package/dist/extension/index.d.ts +3 -3
  54. package/dist/extension/index.js +2 -2
  55. package/dist/flow/index.d.ts +13 -13
  56. package/dist/flow/index.js +13 -11
  57. package/dist/flow/index.js.map +1 -1
  58. package/dist/idempotency/index.d.ts +3 -3
  59. package/dist/idempotency/index.js +3 -3
  60. package/dist/{index-1hit3dJ9.d.ts → index-9P1PaXIV.d.ts} +2 -2
  61. package/dist/{index-Cuy1_xTh.d.ts → index-BBLI2WWN.d.ts} +1 -1
  62. package/dist/{index-Z6Uw7C9t.d.ts → index-C1WhBZ6a.d.ts} +4 -5
  63. package/dist/{index-B-VC4qjx.d.ts → index-C92SgNWz.d.ts} +2 -2
  64. package/dist/{index-CYrUWi1c.d.ts → index-CAsUE_d1.d.ts} +4 -4
  65. package/dist/{index-BjwRcjBi.d.ts → index-CO34esWT.d.ts} +2 -2
  66. package/dist/{index-DB8Yn0-K.d.ts → index-COeX9Bmz.d.ts} +11 -9
  67. package/dist/{index-Diyo8WBO.d.ts → index-Cg0QXnzf.d.ts} +2 -2
  68. package/dist/index.d.ts +5 -5
  69. package/dist/index.js +2 -2
  70. package/dist/jev/index.d.ts +8 -8
  71. package/dist/jev/index.js +5 -5
  72. package/dist/laws-DTtCDDlU.d.ts +135 -0
  73. package/dist/loop-DReKGV1X.d.ts +70 -0
  74. package/dist/{machine-BuPTS1QH.d.ts → machine-C0WhOAyB.d.ts} +5 -5
  75. package/dist/machine-viz/index.d.ts +1 -1
  76. package/dist/machine-viz/index.js +2 -2
  77. package/dist/mem/index.d.ts +4 -3
  78. package/dist/mem/index.js +2 -2
  79. package/dist/mem/index.js.map +1 -1
  80. package/dist/node/index.d.ts +57 -9
  81. package/dist/node/index.js +175 -68
  82. package/dist/node/index.js.map +1 -1
  83. package/dist/otel/index.d.ts +28 -12
  84. package/dist/otel/index.js +25 -21
  85. package/dist/otel/index.js.map +1 -1
  86. package/dist/paginate/index.d.ts +4 -4
  87. package/dist/paginate/index.js +5 -5
  88. package/dist/parity/index.d.ts +11 -15
  89. package/dist/parity/index.js +4 -9
  90. package/dist/parity/index.js.map +1 -1
  91. package/dist/pbt/index.d.ts +279 -67
  92. package/dist/pbt/index.js +299 -58
  93. package/dist/pbt/index.js.map +1 -1
  94. package/dist/persistence/index.d.ts +10 -12
  95. package/dist/persistence/index.js +4 -4
  96. package/dist/promise/index.d.ts +95 -13
  97. package/dist/promise/index.js +6 -4
  98. package/dist/react/index.d.ts +34 -22
  99. package/dist/react/index.js +271 -42
  100. package/dist/react/index.js.map +1 -1
  101. package/dist/resilience/index.d.ts +6 -6
  102. package/dist/resilience/index.js +5 -5
  103. package/dist/{run-D0sxbyno.d.ts → run-Cnw4qXj2.d.ts} +86 -17
  104. package/dist/{runtime-types-BPKp2bGo.d.ts → runtime-types-CP-ykwgf.d.ts} +120 -31
  105. package/dist/testing/effect/index.d.ts +162 -9
  106. package/dist/testing/effect/index.js +110 -10
  107. package/dist/testing/effect/index.js.map +1 -1
  108. package/dist/testing/index.d.ts +6 -8
  109. package/dist/testing/index.js +4 -24
  110. package/dist/testing/index.js.map +1 -1
  111. package/dist/testing/promise/index.d.ts +4 -4
  112. package/dist/testing/promise/index.js +5 -5
  113. package/dist/timing/index.d.ts +4 -4
  114. package/dist/timing/index.js +4 -4
  115. package/dist/{types-BiXj7k_k.d.ts → types-S53WDy_h.d.ts} +1 -1
  116. package/dist/work-queue/index.d.ts +4 -4
  117. package/dist/work-queue/index.js +2 -2
  118. package/package.json +18 -18
  119. package/dist/chunk-7Z2IDZ7R.js +0 -34
  120. package/dist/chunk-7Z2IDZ7R.js.map +0 -1
  121. package/dist/chunk-EB5OMOXX.js.map +0 -1
  122. package/dist/chunk-GAVLND5C.js.map +0 -1
  123. package/dist/chunk-HU3Y2AZY.js.map +0 -1
  124. package/dist/chunk-TMYUDZLE.js.map +0 -1
  125. package/dist/chunk-VM7PMFV5.js.map +0 -1
  126. package/dist/chunk-W6XWI6QE.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'ws';
2
- import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-BPKp2bGo.js';
3
- import { c as Sub, D as Dispose } from '../core-BKJW1Kmq.js';
2
+ import { M as Migrated, D as DeletableStore, F as FencedStore } from '../runtime-types-CP-ykwgf.js';
3
+ import { c as Sub, D as Dispose } from '../core-DNDAEhO2.js';
4
4
  import '@standard-schema/spec';
5
5
 
6
6
  /**
@@ -79,6 +79,30 @@ interface Journal<R> {
79
79
  withLock<A>(stream: string, fx: () => Promise<A>): Promise<A>;
80
80
  }
81
81
 
82
+ /**
83
+ * Why a writer could not take a lock:
84
+ *
85
+ * - `another-host`: the lock names another host, which may still be writing.
86
+ * - `live-holder`: its process is alive on this machine, or its old-format id
87
+ * names a live process here.
88
+ * - `unwritten`: the lock is empty and too young to count as a crash.
89
+ * - `unreadable`: its contents are not a lock this version can read.
90
+ * - `contended`: no one holder was to blame; other writers kept taking it.
91
+ */
92
+ type FileLockBlock = "another-host" | "live-holder" | "unwritten" | "unreadable" | "contended";
93
+ /**
94
+ * A write gave up waiting for a lock file. `lockPath` names the file to clear
95
+ * and `blockedBy` says why it could not be taken. The write did not happen.
96
+ */
97
+ declare class FileLockTimeoutError extends Error {
98
+ readonly lockPath: string;
99
+ readonly waitedMs: number;
100
+ readonly blockedBy: FileLockBlock;
101
+ readonly name = "FileLockTimeoutError";
102
+ readonly _tag: "file_lock_timeout";
103
+ constructor(lockPath: string, waitedMs: number, blockedBy: FileLockBlock);
104
+ }
105
+
82
106
  /**
83
107
  * @packageDocumentation
84
108
  * @demlik/tea/node — Node host adapter for `@demlik/tea`.
@@ -158,6 +182,25 @@ interface FileStoreOptions {
158
182
  * that started from the same version is refused with a `StoreConflictError`
159
183
  * at its next save. Omit it and the store is exactly as it was — the last
160
184
  * writer wins, and single-writer is the caller's precondition to keep.
185
+ *
186
+ * A run on a fenced store must be given an `onError`, or it does not compile
187
+ * (see `StoreAndOnError`). A lost fence can surface with no caller to reject
188
+ * at, under `"stop-save"` or `"follow-up"`, and the default sink would rethrow
189
+ * it and end the process. Treat a `StoreConflictError` there as this run having
190
+ * moved to another process: stop that run, and leave the others alone.
191
+ *
192
+ * Each save and `delete()` takes a lock file, `<path>.fence.lock`, and a
193
+ * writer that dies holding it leaves it behind. The next write steals a lock
194
+ * whose holder is proven gone: an empty one older than a short grace period,
195
+ * one from an earlier boot of this machine, or one whose process is dead.
196
+ *
197
+ * Any other lock is waited on for up to 10 seconds, and then the write rejects
198
+ * with a {@link FileLockTimeoutError} naming the file. That includes a lock
199
+ * from another host, which is never stolen because that host may be live on a
200
+ * shared volume. So a folder restored onto a new machine can need one clear
201
+ * step: once the old machine is known to be gone, delete the file. Hosts are
202
+ * told apart by `os.hostname()`, so give two live hosts on one volume two
203
+ * names.
161
204
  */
162
205
  readonly fenced?: true;
163
206
  }
@@ -176,12 +219,17 @@ interface FileStoreOptions {
176
219
  * O(n) in the stream's length; a durable record log a machine replays is read
177
220
  * whole anyway, so the atomic-rewrite guarantee is worth the cost.
178
221
  *
179
- * The ordering key is taken under a `wx` lock file (`<file>.lock`) holding the
180
- * writer's PID: a second writer racing the same stream blocks on the lock, so
181
- * `seq` is a single total order across processes. A crashed writer's lock is
182
- * stolen once its PID proves dead (`process.kill(pid, 0)` → `ESRCH`), so a
183
- * crash never wedges the stream. This is the lock phoenix spike #6673 proved:
184
- * its only job is an honest order key, not the win.
222
+ * The ordering key is taken under a `wx` lock file (`<file>.lock`) naming the
223
+ * writer's host, boot and PID: a second writer racing the same stream blocks on
224
+ * the lock, so `seq` is a single total order across processes. This is the lock
225
+ * phoenix spike #6673 proved: its only job is an honest order key, not the win.
226
+ *
227
+ * A leftover lock behaves as it does on a fenced {@link fileStore}: one whose
228
+ * holder is proven gone is stolen, so a crash on this machine does not wedge the
229
+ * stream. Any other lock, including one from another host, is waited on for up
230
+ * to 10 seconds, and then the append rejects with a
231
+ * {@link FileLockTimeoutError} naming the file. See
232
+ * {@link FileStoreOptions.fenced} for the cases and the clear step.
185
233
  *
186
234
  * `changes` fires for appends made through THIS instance; a cross-process
187
235
  * append lands on disk but is not broadcast here (that is a polling concern the
@@ -315,4 +363,4 @@ interface NodeSubscribeWithWs<M, Ctx extends NodeSubscribeCtx> extends NodeSubsc
315
363
  declare function nodeSubscribe<M>(): NodeSubscribeBase<M>;
316
364
  declare function nodeSubscribe<M, Ctx extends NodeSubscribeCtx>(opts: NodeSubscribeOpts<M>): NodeSubscribeWithWs<M, Ctx>;
317
365
 
318
- export { type FileStoreOptions, type NodeSignalDeps, type NodeSignalSub, type NodeSub, type NodeSubscribeBase, type NodeSubscribeCtx, type NodeSubscribeOpts, type NodeSubscribeWithWs, type NodeTimerDeps, type NodeTimerSub, type NodeWsDeps, type NodeWsHandlers, type NodeWsRegistry, type NodeWsSub, fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
366
+ export { type FileLockBlock, FileLockTimeoutError, type FileStoreOptions, type NodeSignalDeps, type NodeSignalSub, type NodeSub, type NodeSubscribeBase, type NodeSubscribeCtx, type NodeSubscribeOpts, type NodeSubscribeWithWs, type NodeTimerDeps, type NodeTimerSub, type NodeWsDeps, type NodeWsHandlers, type NodeWsRegistry, type NodeWsSub, fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
@@ -1,9 +1,10 @@
1
- import { dispatchIfPresent } from '../chunk-VHKDUBXC.js';
2
- import { StoreConflictError } from '../chunk-GAVLND5C.js';
3
- import { unlink, mkdir, writeFile, rename, open, readFile } from 'fs/promises';
1
+ import { dispatchIfPresent } from '../chunk-BVFS3ICC.js';
2
+ import { StoreConflictError } from '../chunk-A3QPMX5M.js';
3
+ import { mkdir, writeFile, rename, readFile, unlink, open, stat } from 'fs/promises';
4
4
  import { createRequire } from 'module';
5
5
  import { dirname, join } from 'path';
6
6
  import { AsyncLocalStorage } from 'async_hooks';
7
+ import { uptime, hostname } from 'os';
7
8
 
8
9
  var AsyncLock = class {
9
10
  tail = Promise.resolve();
@@ -60,6 +61,172 @@ function makeJournal(backend) {
60
61
  }
61
62
  };
62
63
  }
64
+ var DEFAULT_LIMITS = { waitMs: 1e4, graceMs: 2e3 };
65
+ var SPIN_MS = 10;
66
+ var BOOT_TOLERANCE_MS = 6e4;
67
+ var BLOCK_EXPLAINED = {
68
+ "another-host": "it was written on another host, which may still be writing, so it is not stolen. If that host is gone, delete the file and try again.",
69
+ "live-holder": "the process it names is alive. If that process is not a writer here, delete the file and try again.",
70
+ unwritten: "it is empty and too young to count as a crash.",
71
+ unreadable: "its contents are not a lock this version can read. If no writer holds it, delete the file and try again.",
72
+ contended: "other writers kept taking it."
73
+ };
74
+ var FileLockTimeoutError = class extends Error {
75
+ constructor(lockPath, waitedMs, blockedBy) {
76
+ super(
77
+ `@demlik/tea: gave up on lock "${lockPath}" after ${Math.round(waitedMs)} ms: ${BLOCK_EXPLAINED[blockedBy]}`
78
+ );
79
+ this.lockPath = lockPath;
80
+ this.waitedMs = waitedMs;
81
+ this.blockedBy = blockedBy;
82
+ }
83
+ lockPath;
84
+ waitedMs;
85
+ blockedBy;
86
+ name = "FileLockTimeoutError";
87
+ _tag = "file_lock_timeout";
88
+ };
89
+ function thisMachine() {
90
+ return {
91
+ host: hostname(),
92
+ bootedAtMs: Math.round(Date.now() - uptime() * 1e3)
93
+ };
94
+ }
95
+ function isValidPid(pid) {
96
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0;
97
+ }
98
+ function parseStamp(body) {
99
+ let raw;
100
+ try {
101
+ raw = JSON.parse(body);
102
+ } catch {
103
+ return { kind: "unreadable" };
104
+ }
105
+ if (typeof raw !== "object" || raw === null) return { kind: "unreadable" };
106
+ const { host, bootedAtMs, pid } = raw;
107
+ if (typeof host !== "string" || typeof bootedAtMs !== "number" || !Number.isFinite(bootedAtMs) || !isValidPid(pid)) {
108
+ return { kind: "unreadable" };
109
+ }
110
+ return { kind: "stamped", host, bootedAtMs, pid };
111
+ }
112
+ function parseHolder(text) {
113
+ const body = text.trim();
114
+ if (body === "") return { kind: "unwritten" };
115
+ if (/^\d+$/.test(body)) {
116
+ const pid = Number(body);
117
+ return isValidPid(pid) ? { kind: "pid-only", pid } : { kind: "unreadable" };
118
+ }
119
+ return parseStamp(body);
120
+ }
121
+ function isProcessAlive(pid) {
122
+ try {
123
+ process.kill(pid, 0);
124
+ return true;
125
+ } catch (err) {
126
+ return err.code === "EPERM";
127
+ }
128
+ }
129
+ function judgeHolder(holder, ageMs, here, graceMs) {
130
+ switch (holder.kind) {
131
+ case "unwritten":
132
+ return ageMs > graceMs ? "stale" : "unwritten";
133
+ case "unreadable":
134
+ return "unreadable";
135
+ case "pid-only":
136
+ return isProcessAlive(holder.pid) ? "live-holder" : "stale";
137
+ case "stamped":
138
+ if (holder.host !== here.host) return "another-host";
139
+ if (Math.abs(holder.bootedAtMs - here.bootedAtMs) > BOOT_TOLERANCE_MS) {
140
+ return "stale";
141
+ }
142
+ return isProcessAlive(holder.pid) ? "live-holder" : "stale";
143
+ }
144
+ }
145
+ async function ageOf(path) {
146
+ try {
147
+ return Date.now() - (await stat(path)).mtimeMs;
148
+ } catch (err) {
149
+ if (err.code === "ENOENT") return "vanished";
150
+ throw err;
151
+ }
152
+ }
153
+ async function inspectLock(lockPath, limits) {
154
+ let text;
155
+ try {
156
+ text = await readFile(lockPath, "utf8");
157
+ } catch (err) {
158
+ if (err.code === "ENOENT") return "vanished";
159
+ throw err;
160
+ }
161
+ const ageMs = await ageOf(lockPath);
162
+ if (ageMs === "vanished") return "vanished";
163
+ return judgeHolder(parseHolder(text), ageMs, thisMachine(), limits.graceMs);
164
+ }
165
+ async function createExclusive(path, body) {
166
+ try {
167
+ const handle = await open(path, "wx");
168
+ try {
169
+ if (body !== "") await handle.writeFile(body);
170
+ } finally {
171
+ await handle.close();
172
+ }
173
+ return true;
174
+ } catch (err) {
175
+ if (err.code === "EEXIST") return false;
176
+ throw err;
177
+ }
178
+ }
179
+ var removeIfPresent = (path) => unlink(path).catch((err) => {
180
+ if (err.code !== "ENOENT") throw err;
181
+ });
182
+ async function stealIfStillStale(lockPath, limits) {
183
+ const guardPath = `${lockPath}.steal`;
184
+ if (!await createExclusive(guardPath, "")) {
185
+ const age = await ageOf(guardPath);
186
+ if (age !== "vanished" && age > limits.graceMs) {
187
+ await removeIfPresent(guardPath);
188
+ }
189
+ return false;
190
+ }
191
+ try {
192
+ const verdict = await inspectLock(lockPath, limits);
193
+ if (verdict === "stale") await removeIfPresent(lockPath);
194
+ return verdict === "stale" || verdict === "vanished";
195
+ } finally {
196
+ await removeIfPresent(guardPath);
197
+ }
198
+ }
199
+ async function acquireFileLock(dir, lockPath, limits) {
200
+ await mkdir(dir, { recursive: true });
201
+ const startedAt = performance.now();
202
+ let blockedBy = "contended";
203
+ for (; ; ) {
204
+ const here = thisMachine();
205
+ const stamp = JSON.stringify({
206
+ host: here.host,
207
+ bootedAtMs: here.bootedAtMs,
208
+ pid: process.pid
209
+ });
210
+ if (await createExclusive(lockPath, stamp)) return;
211
+ const verdict = await inspectLock(lockPath, limits);
212
+ if (verdict !== "stale" && verdict !== "vanished") blockedBy = verdict;
213
+ const freed = verdict === "vanished" || verdict === "stale" && await stealIfStillStale(lockPath, limits);
214
+ const waitedMs = performance.now() - startedAt;
215
+ if (waitedMs >= limits.waitMs) {
216
+ throw new FileLockTimeoutError(lockPath, waitedMs, blockedBy);
217
+ }
218
+ if (!freed) await new Promise((resolve) => setTimeout(resolve, SPIN_MS));
219
+ }
220
+ }
221
+ async function withFileLock(dir, lockPath, fx, limits = DEFAULT_LIMITS) {
222
+ await acquireFileLock(dir, lockPath, limits);
223
+ try {
224
+ return await fx();
225
+ } finally {
226
+ await unlink(lockPath).catch(() => {
227
+ });
228
+ }
229
+ }
63
230
 
64
231
  // src/node/index.ts
65
232
  function fileStore(path, parse, options = {}) {
@@ -123,9 +290,7 @@ function fenceFileStore(base, path) {
123
290
  return { raw: await base.load(), version };
124
291
  },
125
292
  async saveFenced(state, expectedVersion) {
126
- await mkdir(dirname(path), { recursive: true });
127
- await acquireFileLock(dirname(path), lockPath);
128
- try {
293
+ return withFileLock(dirname(path), lockPath, async () => {
129
294
  const actual = await readVersion();
130
295
  if (actual !== expectedVersion) {
131
296
  throw new StoreConflictError(expectedVersion, actual);
@@ -136,23 +301,13 @@ function fenceFileStore(base, path) {
136
301
  await writeFile(tmp, String(next), "utf8");
137
302
  await rename(tmp, fencePath);
138
303
  return next;
139
- } finally {
140
- await unlink(lockPath).catch(() => {
141
- });
142
- }
304
+ });
143
305
  },
144
306
  async delete() {
145
- await acquireFileLock(dirname(path), lockPath);
146
- try {
147
- await base.delete();
148
- } finally {
149
- await unlink(lockPath).catch(() => {
150
- });
151
- }
307
+ await withFileLock(dirname(path), lockPath, () => base.delete());
152
308
  }
153
309
  };
154
310
  }
155
- var LOCK_SPIN_MS = 10;
156
311
  function fileJournal(dir, parse) {
157
312
  const streamPath = (stream) => join(dir, `${encodeURIComponent(stream)}.jsonl`);
158
313
  const readEntries = async (stream) => {
@@ -188,58 +343,10 @@ function fileJournal(dir, parse) {
188
343
  return entry;
189
344
  },
190
345
  async lock(stream, fx) {
191
- const lockPath = `${streamPath(stream)}.lock`;
192
- await acquireFileLock(dir, lockPath);
193
- try {
194
- return await fx();
195
- } finally {
196
- await unlink(lockPath).catch(() => {
197
- });
198
- }
346
+ return withFileLock(dir, `${streamPath(stream)}.lock`, fx);
199
347
  }
200
348
  });
201
349
  }
202
- async function acquireFileLock(dir, lockPath) {
203
- await mkdir(dir, { recursive: true });
204
- for (; ; ) {
205
- try {
206
- const handle = await open(lockPath, "wx");
207
- try {
208
- await handle.writeFile(String(process.pid));
209
- } finally {
210
- await handle.close();
211
- }
212
- return;
213
- } catch (err) {
214
- if (err.code !== "EEXIST") throw err;
215
- if (await stealIfStale(lockPath)) continue;
216
- await new Promise((resolve) => setTimeout(resolve, LOCK_SPIN_MS));
217
- }
218
- }
219
- }
220
- async function stealIfStale(lockPath) {
221
- let holder;
222
- try {
223
- holder = await readFile(lockPath, "utf8");
224
- } catch (err) {
225
- if (err.code === "ENOENT") return true;
226
- throw err;
227
- }
228
- const pid = Number(holder.trim());
229
- if (!Number.isInteger(pid) || pid <= 0) return false;
230
- if (isProcessAlive(pid)) return false;
231
- await unlink(lockPath).catch(() => {
232
- });
233
- return true;
234
- }
235
- function isProcessAlive(pid) {
236
- try {
237
- process.kill(pid, 0);
238
- return true;
239
- } catch (err) {
240
- return err.code === "EPERM";
241
- }
242
- }
243
350
  var cachedWs;
244
351
  function loadWebSocket() {
245
352
  if (cachedWs) return cachedWs;
@@ -328,6 +435,6 @@ function nodeWsRunner(handlers) {
328
435
  };
329
436
  }
330
437
 
331
- export { fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
438
+ export { FileLockTimeoutError, fileJournal, fileStore, nodeSubscribe, sendToWebSocket };
332
439
  //# sourceMappingURL=index.js.map
333
440
  //# sourceMappingURL=index.js.map