@untangle-code/storage-local 0.0.0-stage → 0.1.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 (65) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +226 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/abort.d.ts +18 -0
  6. package/dist/abort.d.ts.map +1 -0
  7. package/dist/abort.js +14 -0
  8. package/dist/abort.js.map +1 -0
  9. package/dist/analysis-cache.d.ts +81 -0
  10. package/dist/analysis-cache.d.ts.map +1 -0
  11. package/dist/analysis-cache.js +322 -0
  12. package/dist/analysis-cache.js.map +1 -0
  13. package/dist/checkpoint-store.d.ts +22 -0
  14. package/dist/checkpoint-store.d.ts.map +1 -0
  15. package/dist/checkpoint-store.js +123 -0
  16. package/dist/checkpoint-store.js.map +1 -0
  17. package/dist/embedding-cache.d.ts +38 -0
  18. package/dist/embedding-cache.d.ts.map +1 -0
  19. package/dist/embedding-cache.js +229 -0
  20. package/dist/embedding-cache.js.map +1 -0
  21. package/dist/fs-errors.d.ts +4 -0
  22. package/dist/fs-errors.d.ts.map +1 -0
  23. package/dist/fs-errors.js +14 -0
  24. package/dist/fs-errors.js.map +1 -0
  25. package/dist/index.d.ts +15 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +23 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/layout.d.ts +50 -0
  30. package/dist/layout.d.ts.map +1 -0
  31. package/dist/layout.js +99 -0
  32. package/dist/layout.js.map +1 -0
  33. package/dist/local-file-system.d.ts +37 -0
  34. package/dist/local-file-system.d.ts.map +1 -0
  35. package/dist/local-file-system.js +193 -0
  36. package/dist/local-file-system.js.map +1 -0
  37. package/dist/lock-provider.d.ts +59 -0
  38. package/dist/lock-provider.d.ts.map +1 -0
  39. package/dist/lock-provider.js +318 -0
  40. package/dist/lock-provider.js.map +1 -0
  41. package/dist/real-time.d.ts +16 -0
  42. package/dist/real-time.d.ts.map +1 -0
  43. package/dist/real-time.js +19 -0
  44. package/dist/real-time.js.map +1 -0
  45. package/dist/search-journey-store.d.ts +34 -0
  46. package/dist/search-journey-store.d.ts.map +1 -0
  47. package/dist/search-journey-store.js +530 -0
  48. package/dist/search-journey-store.js.map +1 -0
  49. package/dist/storage.d.ts +42 -0
  50. package/dist/storage.d.ts.map +1 -0
  51. package/dist/storage.js +34 -0
  52. package/dist/storage.js.map +1 -0
  53. package/package.json +60 -3
  54. package/src/abort.ts +26 -0
  55. package/src/analysis-cache.ts +404 -0
  56. package/src/checkpoint-store.ts +155 -0
  57. package/src/embedding-cache.ts +282 -0
  58. package/src/fs-errors.ts +19 -0
  59. package/src/index.ts +30 -0
  60. package/src/layout.ts +149 -0
  61. package/src/local-file-system.ts +232 -0
  62. package/src/lock-provider.ts +381 -0
  63. package/src/real-time.ts +18 -0
  64. package/src/search-journey-store.ts +638 -0
  65. package/src/storage.ts +98 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * `FileSystemPort` adapter backed by `node:fs/promises`, scoped to one or
3
+ * more workspace/state roots (architecture.md §2.1, §23.1; docs/BUILD_PLAN.md
4
+ * WP-12 acceptance: "Pfad-Sandbox, Symlink-Prüfung").
5
+ *
6
+ * Every method resolves its path through `assertWithinWorkspace` (domain)
7
+ * BEFORE touching the filesystem — a lexical check that rejects `../` escapes
8
+ * regardless of whether the path exists yet (needed for `writeFile`/`mkdir`
9
+ * of not-yet-existing paths). Where the path (or its parent, for a
10
+ * not-yet-existing leaf) already exists on disk, the adapter additionally
11
+ * resolves the real, symlink-free path and re-asserts against it — the
12
+ * "adapters are responsible for the symlink-aware part of the sandbox"
13
+ * contract from `ports.ts`'s `FileSystemPort` doc comment. See README
14
+ * "Entscheidungen" for the precise guarantee and its documented limits.
15
+ */
16
+ import * as nodeFs from "node:fs/promises";
17
+ import * as nodePath from "node:path";
18
+ import { assertWithinWorkspace, UntangleError } from "@untangle-code/domain";
19
+ import { assertNotAborted } from "./abort.js";
20
+ import { isNotFoundError } from "./fs-errors.js";
21
+ /**
22
+ * Resolves `candidate` as far as the real filesystem allows: walks up to the
23
+ * nearest EXISTING ancestor, `realpath`s that ancestor, then re-appends the
24
+ * not-yet-existing suffix segments lexically. This is what makes the
25
+ * symlink guard correct even for paths that do not exist yet (a `writeFile`
26
+ * target, a directory `mkdir` is about to create) AND for roots that
27
+ * themselves contain an unresolved symlink segment (e.g. macOS'
28
+ * `/var` → `/private/var`) — comparing an already-realpath-resolved
29
+ * candidate against a NOT-resolved root would otherwise falsely report
30
+ * every path under that root as an escape (the same class of bug
31
+ * `@untangle-code/scanner`'s README "Entscheidungen" #7 documents and fixes
32
+ * for its own, narrower realpath use). Returns `candidate` unchanged if NO
33
+ * ancestor exists at all (e.g. the whole tree is hypothetical).
34
+ */
35
+ async function resolveAsFarAsPossible(candidate) {
36
+ const pendingSuffix = [];
37
+ let probe = candidate;
38
+ for (;;) {
39
+ try {
40
+ const real = await nodeFs.realpath(probe);
41
+ return pendingSuffix.length === 0 ? real : nodePath.join(real, ...pendingSuffix.reverse());
42
+ }
43
+ catch (error) {
44
+ if (!isNotFoundError(error)) {
45
+ throw error;
46
+ }
47
+ const parent = nodePath.dirname(probe);
48
+ if (parent === probe) {
49
+ // Reached the filesystem root without finding an existing ancestor.
50
+ return candidate;
51
+ }
52
+ pendingSuffix.push(nodePath.basename(probe));
53
+ probe = parent;
54
+ }
55
+ }
56
+ }
57
+ function kindOf(stats) {
58
+ if (stats.isSymbolicLink()) {
59
+ return "symlink";
60
+ }
61
+ if (stats.isDirectory()) {
62
+ return "directory";
63
+ }
64
+ if (stats.isFile()) {
65
+ return "file";
66
+ }
67
+ return "other";
68
+ }
69
+ function toFileStat(path, stats) {
70
+ return {
71
+ path,
72
+ sizeBytes: stats.size,
73
+ mtimeEpochMs: Math.trunc(stats.mtimeMs),
74
+ kind: kindOf(stats),
75
+ };
76
+ }
77
+ /**
78
+ * Node-backed, path-sandboxed `FileSystemPort`. Construct one per set of
79
+ * roots that should be reachable (typically just the `.untangle` state
80
+ * directory for this package's own callers).
81
+ */
82
+ export class LocalFileSystem {
83
+ roots;
84
+ constructor(roots) {
85
+ if (roots.length === 0) {
86
+ throw new UntangleError("PATH_OUTSIDE_WORKSPACE", "LocalFileSystem needs at least one root", {
87
+ operation: "local_file_system_init",
88
+ });
89
+ }
90
+ this.roots = roots;
91
+ }
92
+ /**
93
+ * Lexical sandbox check (throws `PATH_OUTSIDE_WORKSPACE` on escape), plus a
94
+ * real-path re-check via {@link resolveAsFarAsPossible} on BOTH the
95
+ * candidate and the configured roots — needed on both sides: the candidate
96
+ * may not exist yet (`writeFile`/`mkdir` of a new entry) and a root itself
97
+ * may contain an unresolved symlink segment (e.g. macOS'
98
+ * `/var` → `/private/var`) that would otherwise make every path under it
99
+ * look like an escape. Recomputed per call rather than cached, since which
100
+ * prefix of a root already exists changes over the adapter's lifetime
101
+ * (the root itself usually does not exist yet on first use).
102
+ */
103
+ async guard(candidatePath) {
104
+ const lexical = assertWithinWorkspace(candidatePath, this.roots);
105
+ const resolvedCandidate = await resolveAsFarAsPossible(lexical);
106
+ const resolvedRoots = await Promise.all(this.roots.map(resolveAsFarAsPossible));
107
+ assertWithinWorkspace(resolvedCandidate, resolvedRoots);
108
+ return lexical;
109
+ }
110
+ async stat(path, options) {
111
+ assertNotAborted(options, "stat");
112
+ const resolved = await this.guard(path);
113
+ try {
114
+ const stats = await nodeFs.stat(resolved);
115
+ return toFileStat(path, stats);
116
+ }
117
+ catch (error) {
118
+ if (isNotFoundError(error)) {
119
+ return null;
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ async lstat(path, options) {
125
+ assertNotAborted(options, "lstat");
126
+ const resolved = await this.guard(path);
127
+ try {
128
+ const stats = await nodeFs.lstat(resolved);
129
+ return toFileStat(path, stats);
130
+ }
131
+ catch (error) {
132
+ if (isNotFoundError(error)) {
133
+ return null;
134
+ }
135
+ throw error;
136
+ }
137
+ }
138
+ async readFile(path, options) {
139
+ assertNotAborted(options, "read_file");
140
+ const resolved = await this.guard(path);
141
+ const buffer = await nodeFs.readFile(resolved, { signal: options?.signal });
142
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
143
+ }
144
+ async readTextFile(path, options) {
145
+ assertNotAborted(options, "read_text_file");
146
+ const resolved = await this.guard(path);
147
+ return await nodeFs.readFile(resolved, { encoding: "utf8", signal: options?.signal });
148
+ }
149
+ async readDir(path, options) {
150
+ assertNotAborted(options, "read_dir");
151
+ const resolved = await this.guard(path);
152
+ const entries = await nodeFs.readdir(resolved, { withFileTypes: true });
153
+ return entries.map((entry) => {
154
+ const kind = entry.isSymbolicLink()
155
+ ? "symlink"
156
+ : entry.isDirectory()
157
+ ? "directory"
158
+ : entry.isFile()
159
+ ? "file"
160
+ : "other";
161
+ return { name: entry.name, path: nodePath.join(path, entry.name), kind };
162
+ });
163
+ }
164
+ async realPath(path, options) {
165
+ assertNotAborted(options, "real_path");
166
+ const lexical = assertWithinWorkspace(path, this.roots);
167
+ const real = await nodeFs.realpath(lexical);
168
+ const resolvedRoots = await Promise.all(this.roots.map(resolveAsFarAsPossible));
169
+ return assertWithinWorkspace(real, resolvedRoots);
170
+ }
171
+ async writeFile(path, data, options) {
172
+ assertNotAborted(options, "write_file");
173
+ const resolved = await this.guard(path);
174
+ await nodeFs.writeFile(resolved, data, { signal: options?.signal, mode: options?.mode });
175
+ }
176
+ async mkdir(path, options) {
177
+ assertNotAborted(options, "mkdir");
178
+ const resolved = await this.guard(path);
179
+ await nodeFs.mkdir(resolved, { recursive: options?.recursive ?? false });
180
+ }
181
+ async rename(from, to, options) {
182
+ assertNotAborted(options, "rename");
183
+ const resolvedFrom = await this.guard(from);
184
+ const resolvedTo = await this.guard(to);
185
+ await nodeFs.rename(resolvedFrom, resolvedTo);
186
+ }
187
+ async remove(path, options) {
188
+ assertNotAborted(options, "remove");
189
+ const resolved = await this.guard(path);
190
+ await nodeFs.rm(resolved, { recursive: options?.recursive ?? false, force: true });
191
+ }
192
+ }
193
+ //# sourceMappingURL=local-file-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-file-system.js","sourceRoot":"","sources":["../src/local-file-system.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { Clock, FileSystemPort, LockHandle, LockProvider, LockRecordV1, PortCallOptions, Random } from "@untangle-code/domain";
2
+ /** File-based `LockProvider` rooted at one `locks/` directory. */
3
+ export declare class FileLockProvider implements LockProvider {
4
+ private readonly fs;
5
+ private readonly locksDir;
6
+ private readonly clock;
7
+ private readonly random;
8
+ private readonly host;
9
+ /** Set once `locksDir` is known to exist, to skip a redundant `mkdir` on
10
+ * every `tryAcquire()` call — real disk I/O is not free, and the lease
11
+ * contract tests run with tight (double-digit millisecond) timing
12
+ * margins that a spurious extra round trip can eat into. */
13
+ private locksDirEnsured;
14
+ private readonly corruptGraceMs;
15
+ constructor(fs: FileSystemPort, locksDir: string, clock: Clock, random: Random, host?: string,
16
+ /** Overrides `DEFAULT_CORRUPT_GRACE_MS` — exposed mainly so tests can
17
+ * exercise the grace-period boundary without a real multi-minute wait. */
18
+ corruptGraceMs?: number);
19
+ private lockDirFor;
20
+ private recordPathFor;
21
+ private isExpired;
22
+ private buildRecord;
23
+ /**
24
+ * Reads the current state of a lock directory:
25
+ * - `"absent"` — the directory (or its `record.json`) does not exist yet,
26
+ * INCLUDING the tiny window where a concurrent winner's `mkdir` has
27
+ * landed but its `record.json` write has not — that window is treated as
28
+ * "busy", never as stale, so it can never be mistaken for a takeover
29
+ * opportunity (that would break exclusivity for the very case the
30
+ * directory-mkdir primitive exists to make airtight);
31
+ * - `"corrupt"` — `record.json` exists but does not parse. Because every
32
+ * write goes through temp-file-then-rename (see the file doc comment),
33
+ * this can no longer be an in-flight write caught mid-write — it is a
34
+ * genuine anomaly. The caller still has to check `mtimeEpochMs` against
35
+ * the corruption grace period before treating it as takeover-eligible
36
+ * (see `DEFAULT_CORRUPT_GRACE_MS`) — corrupt is NOT auto-stale;
37
+ * - `"valid"` — a well-formed record, which the caller still has to check
38
+ * for lease expiry itself.
39
+ */
40
+ private readRecordState;
41
+ private isCorruptGraceElapsed;
42
+ /**
43
+ * Writes `record.json` atomically — temp file, then a single `rename` onto
44
+ * the live path — mirroring `checkpoint-store.ts`'s `writeAtomic` in this
45
+ * same package. Used by both the initial `tryAcquire()` write and every
46
+ * `renew()`, so a reader can never observe a half-written record (WP-80).
47
+ */
48
+ private writeRecordAtomic;
49
+ private makeHandle;
50
+ tryAcquire(key: string, options?: PortCallOptions & {
51
+ leaseMs?: number;
52
+ }): Promise<LockHandle | null>;
53
+ acquire(key: string, options?: PortCallOptions & {
54
+ leaseMs?: number;
55
+ waitMs?: number;
56
+ }): Promise<LockHandle>;
57
+ inspect(key: string, options?: PortCallOptions): Promise<LockRecordV1 | null>;
58
+ }
59
+ //# sourceMappingURL=lock-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-provider.d.ts","sourceRoot":"","sources":["../src/lock-provider.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EACV,KAAK,EACL,cAAc,EACd,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,MAAM,EACP,MAAM,uBAAuB,CAAC;AA6E/B,kEAAkE;AAClE,qBAAa,gBAAiB,YAAW,YAAY;IAUjD,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAZzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B;;;gEAG4D;IAC5D,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAGrB,EAAE,EAAE,cAAc,EAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EAC/B,IAAI,CAAC,EAAE,MAAM;IACb;8EAC0E;IAC1E,cAAc,CAAC,EAAE,MAAM;IAMzB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAanB;;;;;;;;;;;;;;;;OAgBG;YACW,eAAe;IA2B7B,OAAO,CAAC,qBAAqB;IAI7B;;;;;OAKG;YACW,iBAAiB;IAW/B,OAAO,CAAC,UAAU;IAuCL,UAAU,CACrB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC/C,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAkEhB,OAAO,CAClB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAChE,OAAO,CAAC,UAAU,CAAC;IA0BT,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAQ3F"}
@@ -0,0 +1,318 @@
1
+ /**
2
+ * File-based `LockProvider` (architecture.md §21.1–§21.2; docs/BUILD_PLAN.md
3
+ * WP-12 acceptance criteria).
4
+ *
5
+ * One lock = one directory named `sha256(key)` under the configured locks
6
+ * root, containing a single `record.json`. `FileSystemPort.mkdir` without
7
+ * `recursive` is the exclusivity primitive: Node's `mkdir` atomically fails
8
+ * with `EEXIST` when the directory already exists, so "did I just create the
9
+ * directory" is a genuine, OS-atomic exclusive-acquire test — no separate
10
+ * locking mechanism is layered on top of it.
11
+ *
12
+ * Stale-lease takeover (an expired holder's lock being stolen) is NOT fully
13
+ * race-free across processes with plain directory operations — see README
14
+ * "Entscheidungen" for the exact, documented race window and why it is an
15
+ * acceptable MVP tradeoff (architecture.md §21.2 explicitly scopes file
16
+ * locks to MVP; "Remote Team Mode" gets a real distributed lease). What IS
17
+ * fully race-free, because it needs no takeover step at all, is contention
18
+ * over a lock nobody holds yet — the common case, and the one this file's
19
+ * own "two providers race for the same lock" test exercises.
20
+ *
21
+ * `record.json` itself is written temp-file-then-rename (mirroring
22
+ * `checkpoint-store.ts`'s "every write is temp-file-then-rename" in this
23
+ * same package) — never by a direct `writeFile` onto the live path. A
24
+ * reader can therefore never observe a half-written record: the file on
25
+ * disk is always either the previous good record or the fully-formed new
26
+ * one, whether the write is happening on the initial `tryAcquire()` or on
27
+ * `renew()`. Before this, a reader racing a writer could see a torn record,
28
+ * fail to parse it, and — because "corrupt" was treated as immediately
29
+ * takeover-eligible — steal a lock that was still validly held (WP-80).
30
+ * See `DEFAULT_CORRUPT_GRACE_MS`'s doc comment and README "Entscheidungen"
31
+ * for how a *genuinely* corrupt record (external tampering, disk fault —
32
+ * atomic rename rules out "caught mid-write") is now handled instead.
33
+ */
34
+ import * as os from "node:os";
35
+ import * as nodePath from "node:path";
36
+ import { sha256Hex, UntangleError } from "@untangle-code/domain";
37
+ import { assertNotAborted } from "./abort.js";
38
+ import { isAlreadyExistsError, isNotFoundError } from "./fs-errors.js";
39
+ import { realNowEpochMs } from "./real-time.js";
40
+ const DEFAULT_LEASE_MS = 30_000;
41
+ const POLL_INTERVAL_MS = 20;
42
+ const RECORD_FILE_NAME = "record.json";
43
+ /** Bounds the stale-takeover retry loop so pathological contention returns
44
+ * "held" rather than spinning forever. */
45
+ const MAX_TAKEOVER_ATTEMPTS = 16;
46
+ /**
47
+ * How long a `record.json` that fails to parse must sit untouched (by
48
+ * mtime) before this provider will treat it as takeover-eligible.
49
+ *
50
+ * Every `record.json` write is now temp-file-then-rename (see the file doc
51
+ * comment), so a reader can no longer see a half-written record — the file
52
+ * on disk is always either the previous good record or the new one, never
53
+ * a torn mix of both. That removes the one legitimate reason the old code
54
+ * had for treating "corrupt" as an automatic, immediate takeover signal.
55
+ * What remains, once "caught mid-write" is off the table, is a genuinely
56
+ * unparsable file: a manual edit, a disk fault, a record written by some
57
+ * future/incompatible format. That is an error condition, not a routine
58
+ * lifecycle state — and unlike an expired lease (which carries its own
59
+ * trustworthy `leaseExpiresAt`), a corrupt record carries no information at
60
+ * all about how long ago it stopped being valid. Immediately stealing it
61
+ * would repeat exactly the WP-80 mistake (treating "I can't explain this" as
62
+ * "safe to take"), just with a different root cause.
63
+ *
64
+ * The chosen policy (documented in README "Entscheidungen"): require a
65
+ * conservative grace period — much longer than any expected lease or write
66
+ * latency — to pass since the file's own last-modified time before a corrupt
67
+ * record becomes takeover-eligible, exactly like an expired lease. Until
68
+ * then `tryAcquire()` treats it like a held lock (returns `null`), never
69
+ * throwing and never stealing. This self-heals without an operator having
70
+ * to intervene, while giving any in-flight (however slow) legitimate write
71
+ * — or a human investigating — a wide window to land or act first.
72
+ */
73
+ const DEFAULT_CORRUPT_GRACE_MS = 5 * 60_000;
74
+ function encode(text) {
75
+ return new TextEncoder().encode(text);
76
+ }
77
+ function toPublicRecord(record) {
78
+ return {
79
+ key: record.key,
80
+ pid: record.pid,
81
+ processInstanceId: record.processInstanceId,
82
+ host: record.host,
83
+ acquiredAt: record.acquiredAt,
84
+ leaseExpiresAt: record.leaseExpiresAt,
85
+ };
86
+ }
87
+ /** File-based `LockProvider` rooted at one `locks/` directory. */
88
+ export class FileLockProvider {
89
+ fs;
90
+ locksDir;
91
+ clock;
92
+ random;
93
+ host;
94
+ /** Set once `locksDir` is known to exist, to skip a redundant `mkdir` on
95
+ * every `tryAcquire()` call — real disk I/O is not free, and the lease
96
+ * contract tests run with tight (double-digit millisecond) timing
97
+ * margins that a spurious extra round trip can eat into. */
98
+ locksDirEnsured = false;
99
+ corruptGraceMs;
100
+ constructor(fs, locksDir, clock, random, host,
101
+ /** Overrides `DEFAULT_CORRUPT_GRACE_MS` — exposed mainly so tests can
102
+ * exercise the grace-period boundary without a real multi-minute wait. */
103
+ corruptGraceMs) {
104
+ this.fs = fs;
105
+ this.locksDir = locksDir;
106
+ this.clock = clock;
107
+ this.random = random;
108
+ this.host = host ?? os.hostname();
109
+ this.corruptGraceMs = corruptGraceMs ?? DEFAULT_CORRUPT_GRACE_MS;
110
+ }
111
+ lockDirFor(key) {
112
+ return nodePath.join(this.locksDir, sha256Hex(key));
113
+ }
114
+ recordPathFor(lockDir) {
115
+ return nodePath.join(lockDir, RECORD_FILE_NAME);
116
+ }
117
+ isExpired(record, nowEpochMs) {
118
+ return Date.parse(record.leaseExpiresAt) <= nowEpochMs;
119
+ }
120
+ buildRecord(key, leaseMs) {
121
+ const nowMs = this.clock.nowEpochMs();
122
+ return {
123
+ key,
124
+ pid: process.pid,
125
+ processInstanceId: this.random.uuid(),
126
+ host: this.host,
127
+ acquiredAt: this.clock.nowIso(),
128
+ leaseExpiresAt: new Date(nowMs + leaseMs).toISOString(),
129
+ processStartedAtEpochMs: Math.round(nowMs - process.uptime() * 1000),
130
+ };
131
+ }
132
+ /**
133
+ * Reads the current state of a lock directory:
134
+ * - `"absent"` — the directory (or its `record.json`) does not exist yet,
135
+ * INCLUDING the tiny window where a concurrent winner's `mkdir` has
136
+ * landed but its `record.json` write has not — that window is treated as
137
+ * "busy", never as stale, so it can never be mistaken for a takeover
138
+ * opportunity (that would break exclusivity for the very case the
139
+ * directory-mkdir primitive exists to make airtight);
140
+ * - `"corrupt"` — `record.json` exists but does not parse. Because every
141
+ * write goes through temp-file-then-rename (see the file doc comment),
142
+ * this can no longer be an in-flight write caught mid-write — it is a
143
+ * genuine anomaly. The caller still has to check `mtimeEpochMs` against
144
+ * the corruption grace period before treating it as takeover-eligible
145
+ * (see `DEFAULT_CORRUPT_GRACE_MS`) — corrupt is NOT auto-stale;
146
+ * - `"valid"` — a well-formed record, which the caller still has to check
147
+ * for lease expiry itself.
148
+ */
149
+ async readRecordState(lockDir, options) {
150
+ const recordPath = this.recordPathFor(lockDir);
151
+ let text;
152
+ try {
153
+ text = await this.fs.readTextFile(recordPath, options);
154
+ }
155
+ catch (error) {
156
+ if (isNotFoundError(error)) {
157
+ return { kind: "absent" };
158
+ }
159
+ throw error;
160
+ }
161
+ try {
162
+ return { kind: "valid", record: JSON.parse(text) };
163
+ }
164
+ catch {
165
+ // `record.json` exists (the readTextFile above succeeded) but does not
166
+ // parse — stat it for the corruption-grace-period check. A `null`
167
+ // stat here means a concurrent takeover removed it between our read
168
+ // and this stat; fall back to "now" so that race resolves toward
169
+ // "not yet eligible" rather than throwing on a missing file.
170
+ const stat = await this.fs.stat(recordPath, options);
171
+ return { kind: "corrupt", mtimeEpochMs: stat?.mtimeEpochMs ?? this.clock.nowEpochMs() };
172
+ }
173
+ }
174
+ isCorruptGraceElapsed(mtimeEpochMs, nowEpochMs) {
175
+ return nowEpochMs - mtimeEpochMs >= this.corruptGraceMs;
176
+ }
177
+ /**
178
+ * Writes `record.json` atomically — temp file, then a single `rename` onto
179
+ * the live path — mirroring `checkpoint-store.ts`'s `writeAtomic` in this
180
+ * same package. Used by both the initial `tryAcquire()` write and every
181
+ * `renew()`, so a reader can never observe a half-written record (WP-80).
182
+ */
183
+ async writeRecordAtomic(lockDir, record, options) {
184
+ const finalPath = this.recordPathFor(lockDir);
185
+ const tempPath = `${finalPath}.${this.random.uuid()}.tmp`;
186
+ await this.fs.writeFile(tempPath, encode(JSON.stringify(record)), options);
187
+ await this.fs.rename(tempPath, finalPath, options);
188
+ }
189
+ makeHandle(lockDir, record, leaseMs) {
190
+ let current = record;
191
+ return {
192
+ get key() {
193
+ return current.key;
194
+ },
195
+ get record() {
196
+ return toPublicRecord(current);
197
+ },
198
+ renew: async (options) => {
199
+ assertNotAborted(options, "lock_renew");
200
+ const state = await this.readRecordState(lockDir, options);
201
+ if (state.kind !== "valid" ||
202
+ state.record.processInstanceId !== current.processInstanceId) {
203
+ throw new UntangleError("LOCK_HELD", `Lock "${current.key}" was lost before renew`, {
204
+ operation: "lock_renew",
205
+ context: { key: current.key },
206
+ });
207
+ }
208
+ const renewed = this.buildRecord(current.key, leaseMs);
209
+ await this.writeRecordAtomic(lockDir, renewed, options);
210
+ current = renewed;
211
+ },
212
+ release: async () => {
213
+ const state = await this.readRecordState(lockDir);
214
+ if (state.kind === "valid" &&
215
+ state.record.processInstanceId === current.processInstanceId) {
216
+ await this.fs.remove(lockDir, { recursive: true });
217
+ }
218
+ // Otherwise: this handle already lost the lock to another holder —
219
+ // a safe no-op, never removing the new holder's directory.
220
+ },
221
+ };
222
+ }
223
+ async tryAcquire(key, options) {
224
+ assertNotAborted(options, "lock_try_acquire");
225
+ const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS;
226
+ const lockDir = this.lockDirFor(key);
227
+ const mkdirOptions = options?.signal ? { signal: options.signal } : undefined;
228
+ // Lets this provider be used before any explicit `ensureStateDirLayout()`
229
+ // call (e.g. in a focused unit test) — but only paid once per instance
230
+ // (see `locksDirEnsured`'s doc comment).
231
+ if (!this.locksDirEnsured) {
232
+ await this.fs.mkdir(this.locksDir, { recursive: true, ...mkdirOptions });
233
+ this.locksDirEnsured = true;
234
+ }
235
+ for (let attempt = 0; attempt < MAX_TAKEOVER_ATTEMPTS; attempt += 1) {
236
+ assertNotAborted(options, "lock_try_acquire");
237
+ try {
238
+ await this.fs.mkdir(lockDir, mkdirOptions);
239
+ }
240
+ catch (error) {
241
+ if (!isAlreadyExistsError(error)) {
242
+ throw error;
243
+ }
244
+ const state = await this.readRecordState(lockDir, options);
245
+ if (state.kind === "absent") {
246
+ // Someone else's mkdir landed but their record.json has not yet —
247
+ // busy, not stale. Report "held" rather than racing to remove a
248
+ // directory that is mid-creation by its legitimate owner.
249
+ return null;
250
+ }
251
+ if (state.kind === "valid" && !this.isExpired(state.record, this.clock.nowEpochMs())) {
252
+ return null; // genuinely held
253
+ }
254
+ if (state.kind === "corrupt" &&
255
+ !this.isCorruptGraceElapsed(state.mtimeEpochMs, this.clock.nowEpochMs())) {
256
+ // Unparsable, but too recently modified to trust as abandoned —
257
+ // could still be a legitimate (if unusually slow) in-flight write,
258
+ // or fresh external damage nobody has had a chance to react to
259
+ // yet. Treat exactly like "genuinely held": never steal it before
260
+ // the corruption grace period elapses (see
261
+ // `DEFAULT_CORRUPT_GRACE_MS`; this is the WP-80 fix).
262
+ return null;
263
+ }
264
+ // Either a really-expired lease, or a record.json that has stayed
265
+ // unparsable for longer than the corruption grace period: attempt
266
+ // takeover by removing the stale directory, then race for mkdir
267
+ // again from the top of the loop.
268
+ try {
269
+ await this.fs.remove(lockDir, { recursive: true, ...mkdirOptions });
270
+ }
271
+ catch (removeError) {
272
+ if (!isNotFoundError(removeError)) {
273
+ throw removeError;
274
+ }
275
+ // Someone else already removed it — loop back and race for mkdir.
276
+ }
277
+ continue;
278
+ }
279
+ // Our mkdir won: we are the exclusive owner of a brand-new directory.
280
+ const record = this.buildRecord(key, leaseMs);
281
+ await this.writeRecordAtomic(lockDir, record, options);
282
+ return this.makeHandle(lockDir, record, leaseMs);
283
+ }
284
+ return null;
285
+ }
286
+ async acquire(key, options) {
287
+ const waitMs = options?.waitMs ?? 0;
288
+ // Bounded by REAL elapsed time, not the injected `Clock` — see
289
+ // `real-time.ts` for why (mirrors `InMemoryLockProvider`'s identical
290
+ // reasoning in `@untangle-code/testkit`).
291
+ const startedAtRealMs = realNowEpochMs();
292
+ for (;;) {
293
+ const handle = await this.tryAcquire(key, options);
294
+ if (handle) {
295
+ return handle;
296
+ }
297
+ const elapsedRealMs = realNowEpochMs() - startedAtRealMs;
298
+ if (elapsedRealMs >= waitMs) {
299
+ throw new UntangleError("LOCK_HELD", `Lock "${key}" is held`, {
300
+ operation: "lock_acquire",
301
+ retryable: true,
302
+ context: { key },
303
+ });
304
+ }
305
+ assertNotAborted(options, "lock_acquire");
306
+ await new Promise((resolve) => setTimeout(resolve, Math.min(POLL_INTERVAL_MS, waitMs - elapsedRealMs)));
307
+ }
308
+ }
309
+ async inspect(key, options) {
310
+ assertNotAborted(options, "lock_inspect");
311
+ const state = await this.readRecordState(this.lockDirFor(key), options);
312
+ if (state.kind !== "valid" || this.isExpired(state.record, this.clock.nowEpochMs())) {
313
+ return null;
314
+ }
315
+ return toPublicRecord(state.record);
316
+ }
317
+ }
318
+ //# sourceMappingURL=lock-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-provider.js","sourceRoot":"","sources":["../src/lock-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The single, isolated real-wall-clock read in this package, used ONLY to
3
+ * bound `FileLockProvider.acquire()`'s retry-polling loop — a real file lock
4
+ * has no injectable `Clock` for "how long have we actually been waiting",
5
+ * since that loop sleeps via real `setTimeout` regardless of what the
6
+ * injected `Clock` reports (see `lock-provider.ts` for the full reasoning;
7
+ * this mirrors `@untangle-code/testkit`'s own `SystemClock`, which exists for
8
+ * exactly the same reason and carries the same isolation comment).
9
+ *
10
+ * Lease/record timestamps themselves (`acquiredAt`, `leaseExpiresAt`, cache
11
+ * and checkpoint `savedAt`/access times) always go through the injected
12
+ * `Clock` port, never this function — this is deliberately the ONE place in
13
+ * the package's non-test code that reads real time directly.
14
+ */
15
+ export declare function realNowEpochMs(): number;
16
+ //# sourceMappingURL=real-time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"real-time.d.ts","sourceRoot":"","sources":["../src/real-time.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The single, isolated real-wall-clock read in this package, used ONLY to
3
+ * bound `FileLockProvider.acquire()`'s retry-polling loop — a real file lock
4
+ * has no injectable `Clock` for "how long have we actually been waiting",
5
+ * since that loop sleeps via real `setTimeout` regardless of what the
6
+ * injected `Clock` reports (see `lock-provider.ts` for the full reasoning;
7
+ * this mirrors `@untangle-code/testkit`'s own `SystemClock`, which exists for
8
+ * exactly the same reason and carries the same isolation comment).
9
+ *
10
+ * Lease/record timestamps themselves (`acquiredAt`, `leaseExpiresAt`, cache
11
+ * and checkpoint `savedAt`/access times) always go through the injected
12
+ * `Clock` port, never this function — this is deliberately the ONE place in
13
+ * the package's non-test code that reads real time directly.
14
+ */
15
+ export function realNowEpochMs() {
16
+ // eslint-disable-next-line no-restricted-syntax -- see file doc comment above.
17
+ return Date.now();
18
+ }
19
+ //# sourceMappingURL=real-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"real-time.js","sourceRoot":"","sources":["../src/real-time.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc;IAC5B,+EAA+E;IAC/E,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC"}