@indigoai-us/hq-cloud 6.15.75 → 6.15.77
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/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +12 -2
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +4 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +95 -4
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +16 -0
- package/dist/index.test.js.map +1 -0
- package/dist/operation-lock.d.ts +31 -28
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +206 -50
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +105 -1
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/sync/area-resolver.d.ts +66 -0
- package/dist/sync/area-resolver.d.ts.map +1 -0
- package/dist/sync/area-resolver.js +306 -0
- package/dist/sync/area-resolver.js.map +1 -0
- package/dist/sync/area-resolver.test.d.ts +2 -0
- package/dist/sync/area-resolver.test.d.ts.map +1 -0
- package/dist/sync/area-resolver.test.js +211 -0
- package/dist/sync/area-resolver.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +5 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +3 -0
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +45 -0
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +2 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts +77 -0
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -0
- package/dist/sync/parallel-sync-coordinator.js +287 -0
- package/dist/sync/parallel-sync-coordinator.js.map +1 -0
- package/dist/sync/parallel-sync-coordinator.test.d.ts +2 -0
- package/dist/sync/parallel-sync-coordinator.test.d.ts.map +1 -0
- package/dist/sync/parallel-sync-coordinator.test.js +281 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -0
- package/dist/sync/path-leases.d.ts +31 -0
- package/dist/sync/path-leases.d.ts.map +1 -0
- package/dist/sync/path-leases.js +74 -0
- package/dist/sync/path-leases.js.map +1 -0
- package/dist/sync/path-leases.test.d.ts +2 -0
- package/dist/sync/path-leases.test.d.ts.map +1 -0
- package/dist/sync/path-leases.test.js +51 -0
- package/dist/sync/path-leases.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +7 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +104 -43
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +62 -1
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync/sync-work.d.ts +64 -0
- package/dist/sync/sync-work.d.ts.map +1 -0
- package/dist/sync/sync-work.js +11 -0
- package/dist/sync/sync-work.js.map +1 -0
- package/dist/watcher.d.ts +65 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +280 -24
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +401 -3
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AreaId, CanonicalPath } from "./sync-work.js";
|
|
2
|
+
export type PathLeaseRequest = {
|
|
3
|
+
targetUid: string;
|
|
4
|
+
areaId: AreaId;
|
|
5
|
+
canonicalPath: CanonicalPath;
|
|
6
|
+
mode: "mutation";
|
|
7
|
+
};
|
|
8
|
+
export type PathLeaseSnapshot = PathLeaseRequest & {
|
|
9
|
+
owner: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function assertCanonicalPath(path: CanonicalPath): void;
|
|
12
|
+
export declare function pathsConflict(left: CanonicalPath, right: CanonicalPath): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Parent-owned mutation claims. Acquisition is intentionally synchronous and
|
|
15
|
+
* all-or-none: callers either own every path in a slice or none of them.
|
|
16
|
+
*/
|
|
17
|
+
export declare class PathLeaseManager {
|
|
18
|
+
private readonly held;
|
|
19
|
+
private nextLeaseId;
|
|
20
|
+
tryAcquire(owner: string, requested: readonly PathLeaseRequest[]): PathLease | undefined;
|
|
21
|
+
inspect(): readonly PathLeaseSnapshot[];
|
|
22
|
+
release(ids: readonly string[]): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class PathLease {
|
|
25
|
+
private readonly manager;
|
|
26
|
+
private readonly ids;
|
|
27
|
+
private released;
|
|
28
|
+
constructor(manager: PathLeaseManager, ids: readonly string[]);
|
|
29
|
+
release(): void;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=path-leases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-leases.d.ts","sourceRoot":"","sources":["../../src/sync/path-leases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE5D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAU7D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAEhF;AAeD;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwC;IAC7D,OAAO,CAAC,WAAW,CAAK;IAExB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,gBAAgB,EAAE,GAAG,SAAS,GAAG,SAAS;IAyBxF,OAAO,IAAI,SAAS,iBAAiB,EAAE;IAIvC,OAAO,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;CAGtC;AAED,qBAAa,SAAS;IAIlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJtB,OAAO,CAAC,QAAQ,CAAS;gBAGN,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,SAAS,MAAM,EAAE;IAGzC,OAAO,IAAI,IAAI;CAKhB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function assertCanonicalPath(path) {
|
|
2
|
+
if (path.length === 0 ||
|
|
3
|
+
path.startsWith("/") ||
|
|
4
|
+
path.endsWith("/") ||
|
|
5
|
+
path.includes("\\") ||
|
|
6
|
+
path.split("/").some((part) => part.length === 0 || part === "." || part === "..")) {
|
|
7
|
+
throw new Error(`path lease requires a canonical relative path: ${path}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function pathsConflict(left, right) {
|
|
11
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
12
|
+
}
|
|
13
|
+
function sameResource(left, right) {
|
|
14
|
+
return left.targetUid === right.targetUid && left.areaId === right.areaId;
|
|
15
|
+
}
|
|
16
|
+
function compareRequests(left, right) {
|
|
17
|
+
return (left.targetUid.localeCompare(right.targetUid) ||
|
|
18
|
+
left.areaId.localeCompare(right.areaId) ||
|
|
19
|
+
left.canonicalPath.localeCompare(right.canonicalPath) ||
|
|
20
|
+
left.mode.localeCompare(right.mode));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parent-owned mutation claims. Acquisition is intentionally synchronous and
|
|
24
|
+
* all-or-none: callers either own every path in a slice or none of them.
|
|
25
|
+
*/
|
|
26
|
+
export class PathLeaseManager {
|
|
27
|
+
held = new Map();
|
|
28
|
+
nextLeaseId = 1;
|
|
29
|
+
tryAcquire(owner, requested) {
|
|
30
|
+
if (owner.length === 0)
|
|
31
|
+
throw new Error("path lease owner is required");
|
|
32
|
+
const normalized = [...requested].sort(compareRequests);
|
|
33
|
+
for (const request of normalized)
|
|
34
|
+
assertCanonicalPath(request.canonicalPath);
|
|
35
|
+
const all = [...this.held.values()];
|
|
36
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
37
|
+
const request = normalized[index];
|
|
38
|
+
if (all.some((lease) => sameResource(lease, request) && pathsConflict(lease.canonicalPath, request.canonicalPath)) ||
|
|
39
|
+
normalized.slice(0, index).some((other) => sameResource(other, request) && pathsConflict(other.canonicalPath, request.canonicalPath))) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const ids = [];
|
|
44
|
+
for (const request of normalized) {
|
|
45
|
+
const id = `${this.nextLeaseId++}`;
|
|
46
|
+
this.held.set(id, { ...request, owner });
|
|
47
|
+
ids.push(id);
|
|
48
|
+
}
|
|
49
|
+
return new PathLease(this, ids);
|
|
50
|
+
}
|
|
51
|
+
inspect() {
|
|
52
|
+
return [...this.held.values()].sort((left, right) => compareRequests(left, right));
|
|
53
|
+
}
|
|
54
|
+
release(ids) {
|
|
55
|
+
for (const id of ids)
|
|
56
|
+
this.held.delete(id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class PathLease {
|
|
60
|
+
manager;
|
|
61
|
+
ids;
|
|
62
|
+
released = false;
|
|
63
|
+
constructor(manager, ids) {
|
|
64
|
+
this.manager = manager;
|
|
65
|
+
this.ids = ids;
|
|
66
|
+
}
|
|
67
|
+
release() {
|
|
68
|
+
if (this.released)
|
|
69
|
+
return;
|
|
70
|
+
this.released = true;
|
|
71
|
+
this.manager.release(this.ids);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=path-leases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-leases.js","sourceRoot":"","sources":["../../src/sync/path-leases.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-leases.test.d.ts","sourceRoot":"","sources":["../../src/sync/path-leases.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PathLeaseManager, pathsConflict } from "./path-leases.js";
|
|
3
|
+
const request = (canonicalPath) => ({
|
|
4
|
+
targetUid: "personal",
|
|
5
|
+
areaId: "root:notes",
|
|
6
|
+
canonicalPath,
|
|
7
|
+
mode: "mutation",
|
|
8
|
+
});
|
|
9
|
+
describe("PathLeaseManager", () => {
|
|
10
|
+
it("acquires all paths atomically in deterministic order", () => {
|
|
11
|
+
const leases = new PathLeaseManager();
|
|
12
|
+
const first = leases.tryAcquire("first", [request("notes/b"), request("notes/a")]);
|
|
13
|
+
expect(first).toBeDefined();
|
|
14
|
+
expect(leases.inspect().map((entry) => entry.canonicalPath)).toEqual(["notes/a", "notes/b"]);
|
|
15
|
+
expect(leases.tryAcquire("second", [request("elsewhere"), request("notes/a/today")])).toBeUndefined();
|
|
16
|
+
expect(leases.inspect().map((entry) => entry.canonicalPath)).toEqual(["notes/a", "notes/b"]);
|
|
17
|
+
first?.release();
|
|
18
|
+
expect(leases.tryAcquire("second", [request("elsewhere"), request("notes/a/today")])).toBeDefined();
|
|
19
|
+
const empty = new PathLeaseManager();
|
|
20
|
+
expect(empty.tryAcquire("invalid-slice", [request("folder"), request("folder/child")])).toBeUndefined();
|
|
21
|
+
expect(empty.inspect()).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
it("uses exact ancestor-prefix semantics", () => {
|
|
24
|
+
expect(pathsConflict("a", "a")).toBe(true);
|
|
25
|
+
expect(pathsConflict("a", "a/b")).toBe(true);
|
|
26
|
+
expect(pathsConflict("a/b", "a")).toBe(true);
|
|
27
|
+
expect(pathsConflict("a", "ab")).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it("matches a randomized prefix model", () => {
|
|
30
|
+
let seed = 0x51c0ffee;
|
|
31
|
+
const next = () => {
|
|
32
|
+
seed = (seed * 1_664_525 + 1_013_904_223) >>> 0;
|
|
33
|
+
return seed;
|
|
34
|
+
};
|
|
35
|
+
const segment = () => String.fromCharCode(97 + (next() % 5));
|
|
36
|
+
const makePath = () => Array.from({ length: 1 + (next() % 4) }, segment).join("/");
|
|
37
|
+
for (let trial = 0; trial < 100; trial += 1) {
|
|
38
|
+
const manager = new PathLeaseManager();
|
|
39
|
+
const held = [];
|
|
40
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
41
|
+
const path = makePath();
|
|
42
|
+
const expected = !held.some((other) => pathsConflict(path, other));
|
|
43
|
+
const lease = manager.tryAcquire(`owner-${trial}-${attempt}`, [request(path)]);
|
|
44
|
+
expect(lease !== undefined).toBe(expected);
|
|
45
|
+
if (lease)
|
|
46
|
+
held.push(path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=path-leases.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-leases.test.js","sourceRoot":"","sources":["../../src/sync/path-leases.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,GAAG,CAAC,aAAqB,EAAE,EAAE,CAAC,CAAC;IAC1C,SAAS,EAAE,UAAU;IACrB,MAAM,EAAE,YAAY;IACpB,aAAa;IACb,IAAI,EAAE,UAAmB;CAC1B,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACnF,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;QAE7F,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QACtG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC;QAC7F,KAAK,EAAE,OAAO,EAAE,CAAC;QACjB,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAEpG,MAAM,KAAK,GAAG,IAAI,gBAAgB,EAAE,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QACxG,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,IAAI,IAAI,GAAG,UAAU,CAAC;QACtB,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,GAAG,CAAC,IAAI,GAAG,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;YAChD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QACF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7D,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEnF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,IAAI,gBAAgB,EAAE,CAAC;YACvC,MAAM,IAAI,GAAa,EAAE,CAAC;YAC1B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,EAAE,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;gBACjD,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACxB,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;gBACnE,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,KAAK,IAAI,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC/E,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC3C,IAAI,KAAK;oBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -18,6 +18,10 @@ export declare function setStateStoreReflinkCopyHookForTest(hook: (() => void) |
|
|
|
18
18
|
export declare function setStateStorePositionalWriteForTest(write: ((fd: number, bytes: Buffer, offset: number, length: number, position: number) => number) | undefined): void;
|
|
19
19
|
/** Test-only seam for rotation-claim PID write failures. */
|
|
20
20
|
export declare function setStateStoreRotationClaimWriteHookForTest(hook: (() => void) | undefined): void;
|
|
21
|
+
/** Test-only seam for proving same-process async lock waiters queue cooperatively. */
|
|
22
|
+
export declare function setStateStoreScopeLockAcquiredHookForTest(hook: (() => void | Promise<void>) | undefined): void;
|
|
23
|
+
/** Test-only seam for exercising async lock deadlines without a minute-long wait. */
|
|
24
|
+
export declare function setStateStoreAsyncLockTimeoutForTest(timeoutMs: number | undefined): void;
|
|
21
25
|
export interface StateStoreRecord<T = unknown> {
|
|
22
26
|
sequence: number;
|
|
23
27
|
type: number;
|
|
@@ -81,6 +85,9 @@ export interface StateStoreMaintenanceOptions {
|
|
|
81
85
|
export declare class StateStoreCorruptionError extends Error {
|
|
82
86
|
constructor(message: string, options?: ErrorOptions);
|
|
83
87
|
}
|
|
88
|
+
export declare class StateStoreLockError extends Error {
|
|
89
|
+
constructor(message: string, options?: ErrorOptions);
|
|
90
|
+
}
|
|
84
91
|
/** A recovered v3 store. `append` is the ordinary hot path. */
|
|
85
92
|
export declare class StateStore<T> {
|
|
86
93
|
private readonly options;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/sync/state-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/sync/state-store.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/state-store.js
CHANGED
|
@@ -37,7 +37,10 @@ const FRAME_FIXED_BODY_BYTES = 8 + 2 + 2 + 4 + 4 + 32;
|
|
|
37
37
|
const MAX_FRAME_BYTES = 256 * 1024 * 1024;
|
|
38
38
|
const DEFAULT_MAX_WAL_BYTES_BEFORE_COMPACTION = 16 * 1024 * 1024;
|
|
39
39
|
const LOCK_RETRY_MS = 10;
|
|
40
|
-
|
|
40
|
+
// A synchronous caller can only be blocked by another process. Keep that
|
|
41
|
+
// exceptional event-loop pause bounded; async callers wait without parking.
|
|
42
|
+
const SYNC_LOCK_TIMEOUT_MS = 5_000;
|
|
43
|
+
const ASYNC_LOCK_TIMEOUT_MS = 60_000;
|
|
41
44
|
const REPLACE_STATE_MAX_ATTEMPTS = 3;
|
|
42
45
|
const OPTIMISTIC_RECOVERY_MAX_RESAMPLES = 3;
|
|
43
46
|
// Test instrumentation for materialisation regressions. Kept at the storage
|
|
@@ -52,6 +55,8 @@ let afterGenerationStageHookForTest;
|
|
|
52
55
|
let reflinkCopyHookForTest;
|
|
53
56
|
let positionalWriteForTest;
|
|
54
57
|
let rotationClaimWriteHookForTest;
|
|
58
|
+
let scopeLockAcquiredHookForTest;
|
|
59
|
+
let asyncLockTimeoutMsForTest;
|
|
55
60
|
export function resetStateStoreSnapshotRecoveryCountForTest() {
|
|
56
61
|
snapshotRecoveryCount = 0;
|
|
57
62
|
}
|
|
@@ -98,13 +103,21 @@ export function setStateStorePositionalWriteForTest(write) {
|
|
|
98
103
|
export function setStateStoreRotationClaimWriteHookForTest(hook) {
|
|
99
104
|
rotationClaimWriteHookForTest = hook;
|
|
100
105
|
}
|
|
106
|
+
/** Test-only seam for proving same-process async lock waiters queue cooperatively. */
|
|
107
|
+
export function setStateStoreScopeLockAcquiredHookForTest(hook) {
|
|
108
|
+
scopeLockAcquiredHookForTest = hook;
|
|
109
|
+
}
|
|
110
|
+
/** Test-only seam for exercising async lock deadlines without a minute-long wait. */
|
|
111
|
+
export function setStateStoreAsyncLockTimeoutForTest(timeoutMs) {
|
|
112
|
+
asyncLockTimeoutMsForTest = timeoutMs;
|
|
113
|
+
}
|
|
101
114
|
export class StateStoreCorruptionError extends Error {
|
|
102
115
|
constructor(message, options) {
|
|
103
116
|
super(`state-store corruption: ${message}`, options);
|
|
104
117
|
this.name = "StateStoreCorruptionError";
|
|
105
118
|
}
|
|
106
119
|
}
|
|
107
|
-
class StateStoreLockError extends Error {
|
|
120
|
+
export class StateStoreLockError extends Error {
|
|
108
121
|
constructor(message, options) {
|
|
109
122
|
super(`state-store lock: ${message}`, options);
|
|
110
123
|
this.name = "StateStoreLockError";
|
|
@@ -1469,7 +1482,7 @@ function readIntoSync(fd, target, position) {
|
|
|
1469
1482
|
}
|
|
1470
1483
|
function withScopeLock(dir, action) {
|
|
1471
1484
|
const lockPath = path.join(dir, "append.lock");
|
|
1472
|
-
const deadline = Date.now() +
|
|
1485
|
+
const deadline = Date.now() + SYNC_LOCK_TIMEOUT_MS;
|
|
1473
1486
|
let fd;
|
|
1474
1487
|
while (fd === undefined) {
|
|
1475
1488
|
try {
|
|
@@ -1577,53 +1590,101 @@ function withRotationClaim(dir, action) {
|
|
|
1577
1590
|
throw actionError;
|
|
1578
1591
|
return result;
|
|
1579
1592
|
}
|
|
1593
|
+
const inProcessAsyncScopeLocks = new Map();
|
|
1580
1594
|
/**
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1595
|
+
* Serialize async acquisitions for one process before they reach append.lock.
|
|
1596
|
+
*
|
|
1597
|
+
* A synchronous state-store operation cannot await this queue. The async lock
|
|
1598
|
+
* implementation below therefore performs every operation after the lock file
|
|
1599
|
+
* is created synchronously: no async owner can leave append.lock behind while
|
|
1600
|
+
* the event loop runs a synchronous contender.
|
|
1584
1601
|
*/
|
|
1585
|
-
async function
|
|
1586
|
-
const
|
|
1587
|
-
const
|
|
1588
|
-
let
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1602
|
+
async function withInProcessAsyncScopeLock(dir, action) {
|
|
1603
|
+
const key = path.resolve(dir);
|
|
1604
|
+
const predecessor = inProcessAsyncScopeLocks.get(key) ?? Promise.resolve();
|
|
1605
|
+
let release;
|
|
1606
|
+
const tail = new Promise((resolve) => {
|
|
1607
|
+
release = resolve;
|
|
1608
|
+
});
|
|
1609
|
+
const queueTail = predecessor.then(() => tail, () => tail);
|
|
1610
|
+
inProcessAsyncScopeLocks.set(key, queueTail);
|
|
1611
|
+
await predecessor.catch(() => undefined);
|
|
1612
|
+
try {
|
|
1613
|
+
return await action();
|
|
1614
|
+
}
|
|
1615
|
+
finally {
|
|
1616
|
+
release();
|
|
1617
|
+
if (inProcessAsyncScopeLocks.get(key) === queueTail) {
|
|
1618
|
+
inProcessAsyncScopeLocks.delete(key);
|
|
1594
1619
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function openExclusiveAsync(lockPath) {
|
|
1623
|
+
return new Promise((resolve, reject) => {
|
|
1624
|
+
fs.open(lockPath, "wx", (error, fd) => {
|
|
1625
|
+
if (error)
|
|
1626
|
+
reject(error);
|
|
1627
|
+
else
|
|
1628
|
+
resolve(fd);
|
|
1629
|
+
});
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
/** Acquire append.lock without yielding after this process owns the file. */
|
|
1633
|
+
async function withScopeLockAsync(dir, action) {
|
|
1634
|
+
// This deadline starts when the caller enters the same-process queue, not
|
|
1635
|
+
// when it reaches the head. Otherwise each queued caller can spend a fresh
|
|
1636
|
+
// timeout behind a live foreign lock, turning one contention window into an
|
|
1637
|
+
// unbounded same-process backlog.
|
|
1638
|
+
const deadline = Date.now() + (asyncLockTimeoutMsForTest ?? ASYNC_LOCK_TIMEOUT_MS);
|
|
1639
|
+
return withInProcessAsyncScopeLock(dir, async () => {
|
|
1640
|
+
const lockPath = path.join(dir, "append.lock");
|
|
1641
|
+
let fd;
|
|
1642
|
+
while (fd === undefined) {
|
|
1601
1643
|
if (Date.now() >= deadline) {
|
|
1602
1644
|
throw new StateStoreLockError(`timed out acquiring ${lockPath}`);
|
|
1603
1645
|
}
|
|
1604
|
-
|
|
1646
|
+
try {
|
|
1647
|
+
fd = await openExclusiveAsync(lockPath);
|
|
1648
|
+
fs.writeFileSync(fd, String(process.pid));
|
|
1649
|
+
fs.fdatasyncSync(fd);
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
if (!isAlreadyExists(error)) {
|
|
1653
|
+
throw new StateStoreLockError(`cannot acquire ${lockPath}`, { cause: error });
|
|
1654
|
+
}
|
|
1655
|
+
if (await removeDeadLockAsync(lockPath))
|
|
1656
|
+
continue;
|
|
1657
|
+
if (Date.now() >= deadline) {
|
|
1658
|
+
throw new StateStoreLockError(`timed out acquiring ${lockPath}`);
|
|
1659
|
+
}
|
|
1660
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
1661
|
+
}
|
|
1605
1662
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1663
|
+
let result;
|
|
1664
|
+
let actionError;
|
|
1665
|
+
try {
|
|
1666
|
+
// All production callers supply a synchronous critical section. Keeping
|
|
1667
|
+
// this turn-free is what prevents a sync waiter from observing our PID.
|
|
1668
|
+
if (scopeLockAcquiredHookForTest)
|
|
1669
|
+
await scopeLockAcquiredHookForTest();
|
|
1670
|
+
result = action();
|
|
1671
|
+
}
|
|
1672
|
+
catch (error) {
|
|
1673
|
+
actionError = error;
|
|
1674
|
+
}
|
|
1675
|
+
finally {
|
|
1676
|
+
fs.closeSync(fd);
|
|
1677
|
+
}
|
|
1678
|
+
try {
|
|
1679
|
+
fs.unlinkSync(lockPath);
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
throw new StateStoreLockError(`cannot release ${lockPath}`, { cause: error });
|
|
1683
|
+
}
|
|
1684
|
+
if (actionError !== undefined)
|
|
1685
|
+
throw actionError;
|
|
1686
|
+
return result;
|
|
1687
|
+
});
|
|
1627
1688
|
}
|
|
1628
1689
|
function removeDeadLock(lockPath) {
|
|
1629
1690
|
let pid;
|