@davideasden/pi-undo 0.2.14 → 0.2.15
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/package.json +1 -1
- package/src/snapshot-store.ts +113 -1
package/package.json
CHANGED
package/src/snapshot-store.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
|
|
6
|
-
import { fsyncDirectory, writeContentAddressed, writeJsonAtomic } from "./atomic-fs.ts";
|
|
6
|
+
import { fsyncDirectory, writeBytesAtomic, writeContentAddressed, writeJsonAtomic } from "./atomic-fs.ts";
|
|
7
7
|
import {
|
|
8
8
|
assertManifest,
|
|
9
9
|
canonicalJson,
|
|
@@ -47,6 +47,7 @@ const BLOB_CACHE_MAX_BYTES = 128 * 1024 * 1024;
|
|
|
47
47
|
const BLOB_BATCH_MAX_BYTES = 16 * 1024 * 1024;
|
|
48
48
|
const BLOB_BATCH_MAX_ENTRIES = process.platform === "win32" ? 256 : 2_048;
|
|
49
49
|
const RACY_CLEAN_WINDOW_NS = 2_000_000_000n;
|
|
50
|
+
const LEAF_CACHE_FILE = "leaf-cache.json";
|
|
50
51
|
|
|
51
52
|
interface PinRecord {
|
|
52
53
|
readonly schemaVersion: 1;
|
|
@@ -119,6 +120,22 @@ interface CachedBlob {
|
|
|
119
120
|
size: number;
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/** 持久化叶子缓存文件(storeDirectory/leaf-cache.json),schema 不匹配时整体忽略。 */
|
|
124
|
+
interface PersistedLeafCacheFile {
|
|
125
|
+
readonly schemaVersion: 1;
|
|
126
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, PersistedLeafCacheEntry>>>>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface PersistedLeafCacheEntry {
|
|
130
|
+
readonly kind: "file" | "symlink";
|
|
131
|
+
readonly mode: number;
|
|
132
|
+
readonly fingerprint: string;
|
|
133
|
+
readonly cacheable: boolean;
|
|
134
|
+
readonly objectId: string;
|
|
135
|
+
readonly changedAtNs: string;
|
|
136
|
+
readonly verifiedAtNs: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
export interface SnapshotStoreOptions {
|
|
123
140
|
readonly storeRoot?: string;
|
|
124
141
|
readonly git?: GitRunner;
|
|
@@ -184,6 +201,7 @@ export class SnapshotStore {
|
|
|
184
201
|
private readonly treeBlobMembership = new Map<string, string>();
|
|
185
202
|
private readonly blobCache = new Map<string, CachedBlob>();
|
|
186
203
|
private readonly visibleLeafCache = new Map<string, Map<string, CachedVisibleLeaf>>();
|
|
204
|
+
private readonly leafCacheDirectoriesLoaded = new Set<string>();
|
|
187
205
|
private blobCacheBytes = 0;
|
|
188
206
|
|
|
189
207
|
constructor(options: SnapshotStoreOptions = {}) {
|
|
@@ -258,6 +276,8 @@ export class SnapshotStore {
|
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
const storeDirectory = this.storeDirectory(topology);
|
|
279
|
+
// 新进程首次 capture 时从磁盘加载叶子指纹缓存,避免全量重新 hash。
|
|
280
|
+
await this.loadPersistedLeafCache(storeDirectory);
|
|
261
281
|
const transactionsRoot = join(storeDirectory, "transactions");
|
|
262
282
|
await mkdir(transactionsRoot, { recursive: true });
|
|
263
283
|
transactionDirectory = await mkdtemp(join(transactionsRoot, "capture-"));
|
|
@@ -315,6 +335,8 @@ export class SnapshotStore {
|
|
|
315
335
|
await writeContentAddressed(manifestPath, Buffer.from(canonicalJson(manifest), "utf8"));
|
|
316
336
|
this.manifestLocations.set(manifestId, manifestPath);
|
|
317
337
|
for (const update of cacheUpdates) this.rememberVisibleLeaves(update);
|
|
338
|
+
// 指纹缓存落盘:让下一个进程(新会话)的首次 capture 跳过全量内容 hash。
|
|
339
|
+
await this.persistLeafCache(storeDirectory);
|
|
318
340
|
return manifest;
|
|
319
341
|
} catch (error) {
|
|
320
342
|
if (error instanceof SnapshotStoreError) {
|
|
@@ -960,6 +982,69 @@ export class SnapshotStore {
|
|
|
960
982
|
this.visibleLeafCache.set(gitDirectory, cache);
|
|
961
983
|
}
|
|
962
984
|
|
|
985
|
+
/** 从 storeDirectory 读取持久化叶子缓存并合并进内存;进程内已有条目优先。 */
|
|
986
|
+
private async loadPersistedLeafCache(storeDirectory: string): Promise<void> {
|
|
987
|
+
if (this.leafCacheDirectoriesLoaded.has(storeDirectory)) return;
|
|
988
|
+
this.leafCacheDirectoriesLoaded.add(storeDirectory);
|
|
989
|
+
let file: unknown;
|
|
990
|
+
try {
|
|
991
|
+
file = JSON.parse(await readFile(join(storeDirectory, LEAF_CACHE_FILE), "utf8"));
|
|
992
|
+
} catch {
|
|
993
|
+
return; // 缺失或损坏:忽略,本次 capture 走冷路径并重建缓存。
|
|
994
|
+
}
|
|
995
|
+
if (!isPersistedLeafCacheFile(file)) return;
|
|
996
|
+
const prefix = `${storeDirectory}${sep}`;
|
|
997
|
+
for (const [gitDirectory, entries] of Object.entries(file.entries)) {
|
|
998
|
+
if (!gitDirectory.startsWith(prefix) || this.visibleLeafCache.has(gitDirectory)) continue;
|
|
999
|
+
const cache = new Map<string, CachedVisibleLeaf>();
|
|
1000
|
+
for (const [relativePath, entry] of Object.entries(entries)) {
|
|
1001
|
+
cache.set(relativePath, {
|
|
1002
|
+
relativePath,
|
|
1003
|
+
kind: entry.kind,
|
|
1004
|
+
mode: entry.mode,
|
|
1005
|
+
fingerprint: entry.fingerprint,
|
|
1006
|
+
cacheable: entry.cacheable,
|
|
1007
|
+
changedAtNs: BigInt(entry.changedAtNs),
|
|
1008
|
+
objectId: entry.objectId,
|
|
1009
|
+
verifiedAtNs: BigInt(entry.verifiedAtNs),
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
this.visibleLeafCache.set(gitDirectory, cache);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** 把当前 storeDirectory 范围内的叶子缓存原子写入磁盘(best-effort)。 */
|
|
1017
|
+
private async persistLeafCache(storeDirectory: string): Promise<void> {
|
|
1018
|
+
const prefix = `${storeDirectory}${sep}`;
|
|
1019
|
+
const entries: Record<string, Record<string, PersistedLeafCacheEntry>> = {};
|
|
1020
|
+
for (const [gitDirectory, cache] of this.visibleLeafCache) {
|
|
1021
|
+
if (!gitDirectory.startsWith(prefix)) continue;
|
|
1022
|
+
const rootEntries: Record<string, PersistedLeafCacheEntry> = {};
|
|
1023
|
+
for (const [relativePath, leaf] of cache) {
|
|
1024
|
+
rootEntries[relativePath] = {
|
|
1025
|
+
kind: leaf.kind,
|
|
1026
|
+
mode: leaf.mode,
|
|
1027
|
+
fingerprint: leaf.fingerprint,
|
|
1028
|
+
cacheable: leaf.cacheable,
|
|
1029
|
+
objectId: leaf.objectId,
|
|
1030
|
+
changedAtNs: leaf.changedAtNs.toString(),
|
|
1031
|
+
verifiedAtNs: leaf.verifiedAtNs.toString(),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
entries[gitDirectory] = rootEntries;
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
// 用普通 JSON 序列化(非 canonicalJson):缓存只在本机消费,避免大规模排序开销。
|
|
1038
|
+
await writeBytesAtomic(
|
|
1039
|
+
join(storeDirectory, LEAF_CACHE_FILE),
|
|
1040
|
+
Buffer.from(JSON.stringify({ schemaVersion: 1, entries }), "utf8"),
|
|
1041
|
+
0o600,
|
|
1042
|
+
);
|
|
1043
|
+
} catch {
|
|
1044
|
+
// 缓存写入是 best-effort:失败只影响下次性能,不影响正确性。
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
963
1048
|
private async assertVisibleLeavesUnchanged(
|
|
964
1049
|
cwd: string,
|
|
965
1050
|
leaves: readonly VisibleLeaf[],
|
|
@@ -1792,6 +1877,33 @@ function nativeVisibleLeafMetadata(entry: NativeMetadataEntry): VisibleLeafMetad
|
|
|
1792
1877
|
};
|
|
1793
1878
|
}
|
|
1794
1879
|
|
|
1880
|
+
function isPersistedLeafCacheFile(value: unknown): value is PersistedLeafCacheFile {
|
|
1881
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1882
|
+
const file = value as { schemaVersion?: unknown; entries?: unknown };
|
|
1883
|
+
if (file.schemaVersion !== 1 || typeof file.entries !== "object" || file.entries === null) return false;
|
|
1884
|
+
for (const entries of Object.values(file.entries as Record<string, unknown>)) {
|
|
1885
|
+
if (typeof entries !== "object" || entries === null) return false;
|
|
1886
|
+
for (const entry of Object.values(entries as Record<string, unknown>)) {
|
|
1887
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
1888
|
+
const candidate = entry as Partial<PersistedLeafCacheEntry>;
|
|
1889
|
+
if (
|
|
1890
|
+
(candidate.kind !== "file" && candidate.kind !== "symlink") ||
|
|
1891
|
+
typeof candidate.mode !== "number" ||
|
|
1892
|
+
typeof candidate.fingerprint !== "string" ||
|
|
1893
|
+
typeof candidate.cacheable !== "boolean" ||
|
|
1894
|
+
typeof candidate.objectId !== "string" ||
|
|
1895
|
+
typeof candidate.changedAtNs !== "string" ||
|
|
1896
|
+
typeof candidate.verifiedAtNs !== "string" ||
|
|
1897
|
+
!/^[0-9]+$/.test(candidate.changedAtNs) ||
|
|
1898
|
+
!/^[0-9]+$/.test(candidate.verifiedAtNs)
|
|
1899
|
+
) {
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return true;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1795
1907
|
function visibleLeafFingerprint(metadata: VisibleLeafMetadata): string {
|
|
1796
1908
|
return checksum(canonicalJson({
|
|
1797
1909
|
kind: metadata.kind,
|