@untangle-code/storage-local 0.0.0-stage → 0.1.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 +661 -0
- package/NOTICE +90 -0
- package/README.md +226 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/abort.d.ts +18 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +14 -0
- package/dist/abort.js.map +1 -0
- package/dist/analysis-cache.d.ts +81 -0
- package/dist/analysis-cache.d.ts.map +1 -0
- package/dist/analysis-cache.js +322 -0
- package/dist/analysis-cache.js.map +1 -0
- package/dist/checkpoint-store.d.ts +22 -0
- package/dist/checkpoint-store.d.ts.map +1 -0
- package/dist/checkpoint-store.js +123 -0
- package/dist/checkpoint-store.js.map +1 -0
- package/dist/embedding-cache.d.ts +38 -0
- package/dist/embedding-cache.d.ts.map +1 -0
- package/dist/embedding-cache.js +229 -0
- package/dist/embedding-cache.js.map +1 -0
- package/dist/fs-errors.d.ts +4 -0
- package/dist/fs-errors.d.ts.map +1 -0
- package/dist/fs-errors.js +14 -0
- package/dist/fs-errors.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +50 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +99 -0
- package/dist/layout.js.map +1 -0
- package/dist/local-file-system.d.ts +37 -0
- package/dist/local-file-system.d.ts.map +1 -0
- package/dist/local-file-system.js +193 -0
- package/dist/local-file-system.js.map +1 -0
- package/dist/lock-provider.d.ts +59 -0
- package/dist/lock-provider.d.ts.map +1 -0
- package/dist/lock-provider.js +318 -0
- package/dist/lock-provider.js.map +1 -0
- package/dist/real-time.d.ts +16 -0
- package/dist/real-time.d.ts.map +1 -0
- package/dist/real-time.js +19 -0
- package/dist/real-time.js.map +1 -0
- package/dist/search-journey-store.d.ts +34 -0
- package/dist/search-journey-store.d.ts.map +1 -0
- package/dist/search-journey-store.js +530 -0
- package/dist/search-journey-store.js.map +1 -0
- package/dist/storage.d.ts +42 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +34 -0
- package/dist/storage.js.map +1 -0
- package/package.json +60 -3
- package/src/abort.ts +26 -0
- package/src/analysis-cache.ts +404 -0
- package/src/checkpoint-store.ts +155 -0
- package/src/embedding-cache.ts +282 -0
- package/src/fs-errors.ts +19 -0
- package/src/index.ts +30 -0
- package/src/layout.ts +149 -0
- package/src/local-file-system.ts +232 -0
- package/src/lock-provider.ts +381 -0
- package/src/real-time.ts +18 -0
- package/src/search-journey-store.ts +638 -0
- 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"}
|