@lzehrung/codegraph 2.1.2 → 2.2.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/README.md +2 -2
- package/codegraph-skill/codegraph/SKILL.md +21 -2
- package/dist/agent/explore.d.ts.map +1 -1
- package/dist/agent/explore.js +6 -1
- package/dist/agent/explore.js.map +1 -1
- package/dist/agent/handles.d.ts.map +1 -1
- package/dist/agent/handles.js +2 -8
- package/dist/agent/handles.js.map +1 -1
- package/dist/agent/orient.js +0 -16
- package/dist/agent/orient.js.map +1 -1
- package/dist/agent/query-index/store.d.ts +2 -0
- package/dist/agent/query-index/store.d.ts.map +1 -1
- package/dist/agent/query-index/store.js +4 -0
- package/dist/agent/query-index/store.js.map +1 -1
- package/dist/agent/session.js +3 -3
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cli.js +7306 -5661
- package/dist/bin/queryIndexWorker.js +357 -54
- package/dist/chunking/chunkSplit.js +0 -15
- package/dist/chunking/chunkSplit.js.map +1 -1
- package/dist/cli/commandCatalog.d.ts.map +1 -1
- package/dist/cli/commandCatalog.js +1 -0
- package/dist/cli/commandCatalog.js.map +1 -1
- package/dist/cli/commandTable.d.ts.map +1 -1
- package/dist/cli/commandTable.js +17 -0
- package/dist/cli/commandTable.js.map +1 -1
- package/dist/cli/compileCache.d.ts +5 -0
- package/dist/cli/compileCache.d.ts.map +1 -1
- package/dist/cli/compileCache.js +18 -0
- package/dist/cli/compileCache.js.map +1 -1
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/context.js +0 -1
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/help.d.ts +2 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +23 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/indexPolicy.d.ts.map +1 -1
- package/dist/cli/indexPolicy.js +1 -0
- package/dist/cli/indexPolicy.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +5 -2
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/options.d.ts +1 -1
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/cli/options.js +31 -1
- package/dist/cli/options.js.map +1 -1
- package/dist/cli/server.d.ts +59 -0
- package/dist/cli/server.d.ts.map +1 -0
- package/dist/cli/server.js +906 -0
- package/dist/cli/server.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/documentLinks/check.d.ts.map +1 -1
- package/dist/documentLinks/check.js +4 -3
- package/dist/documentLinks/check.js.map +1 -1
- package/dist/documentLinks.d.ts.map +1 -1
- package/dist/documentLinks.js +0 -1
- package/dist/documentLinks.js.map +1 -1
- package/dist/drift/snapshot.js +1 -1
- package/dist/drift/snapshot.js.map +1 -1
- package/dist/duplicates/scoring.d.ts.map +1 -1
- package/dist/duplicates/scoring.js.map +1 -1
- package/dist/duplicates/unitCache.d.ts.map +1 -1
- package/dist/duplicates/unitCache.js +1 -1
- package/dist/duplicates/unitCache.js.map +1 -1
- package/dist/duplicates/units.d.ts.map +1 -1
- package/dist/duplicates/units.js +2 -3
- package/dist/duplicates/units.js.map +1 -1
- package/dist/duplicates.d.ts.map +1 -1
- package/dist/duplicates.js +0 -6
- package/dist/duplicates.js.map +1 -1
- package/dist/graph-edge-collector.d.ts +0 -2
- package/dist/graph-edge-collector.d.ts.map +1 -1
- package/dist/graph-edge-collector.js +2 -4
- package/dist/graph-edge-collector.js.map +1 -1
- package/dist/graphs/edgeResolution.d.ts.map +1 -1
- package/dist/graphs/specifiers.d.ts +1 -1
- package/dist/graphs/specifiers.d.ts.map +1 -1
- package/dist/graphs/specifiers.js +1 -1
- package/dist/graphs/specifiers.js.map +1 -1
- package/dist/graphs/symbol-graph-detailed.d.ts.map +1 -1
- package/dist/graphs/symbol-graph-detailed.js +0 -1
- package/dist/graphs/symbol-graph-detailed.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/indexer/build-cache/module-cache.d.ts.map +1 -1
- package/dist/indexer/build-cache/module-cache.js +1 -1
- package/dist/indexer/build-cache/module-cache.js.map +1 -1
- package/dist/indexer/build-cache/options.d.ts.map +1 -1
- package/dist/indexer/build-cache/options.js +0 -2
- package/dist/indexer/build-cache/options.js.map +1 -1
- package/dist/indexer/build-cache/project-snapshot.d.ts +9 -1
- package/dist/indexer/build-cache/project-snapshot.d.ts.map +1 -1
- package/dist/indexer/build-cache/project-snapshot.js +1 -5
- package/dist/indexer/build-cache/project-snapshot.js.map +1 -1
- package/dist/indexer/build-index.d.ts.map +1 -1
- package/dist/indexer/build-index.js +59 -44
- package/dist/indexer/build-index.js.map +1 -1
- package/dist/indexer/build-workers.d.ts +29 -2
- package/dist/indexer/build-workers.d.ts.map +1 -1
- package/dist/indexer/build-workers.js +54 -3
- package/dist/indexer/build-workers.js.map +1 -1
- package/dist/indexer/call-hierarchy.d.ts.map +1 -1
- package/dist/indexer/imports.d.ts +0 -3
- package/dist/indexer/imports.d.ts.map +1 -1
- package/dist/indexer/imports.js +1 -1
- package/dist/indexer/imports.js.map +1 -1
- package/dist/indexer/locals-and-exports.d.ts +2 -2
- package/dist/indexer/locals-and-exports.d.ts.map +1 -1
- package/dist/indexer/locals-and-exports.js +2 -3
- package/dist/indexer/locals-and-exports.js.map +1 -1
- package/dist/indexer/navigation-local.d.ts +2 -2
- package/dist/indexer/navigation-local.d.ts.map +1 -1
- package/dist/indexer/navigation-local.js +2 -2
- package/dist/indexer/navigation-local.js.map +1 -1
- package/dist/indexer/navigation-references.d.ts +1 -2
- package/dist/indexer/navigation-references.d.ts.map +1 -1
- package/dist/indexer/navigation-references.js +1 -1
- package/dist/indexer/navigation-references.js.map +1 -1
- package/dist/indexer/navigation.d.ts.map +1 -1
- package/dist/indexer/navigation.js +2 -4
- package/dist/indexer/navigation.js.map +1 -1
- package/dist/indexer/parse-context.d.ts +1 -4
- package/dist/indexer/parse-context.d.ts.map +1 -1
- package/dist/indexer/parse-context.js +11 -6
- package/dist/indexer/parse-context.js.map +1 -1
- package/dist/indexer/scope.d.ts +2 -2
- package/dist/indexer/scope.d.ts.map +1 -1
- package/dist/indexer/scope.js +1 -1
- package/dist/indexer/scope.js.map +1 -1
- package/dist/indexer.d.ts +3 -6
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +5 -8
- package/dist/indexer.js.map +1 -1
- package/dist/installer/registry.js +1 -27
- package/dist/installer/registry.js.map +1 -1
- package/dist/languages/definitions/c.d.ts.map +1 -1
- package/dist/languages/definitions/c.js +0 -1
- package/dist/languages/definitions/c.js.map +1 -1
- package/dist/languages/definitions/cFamily.d.ts +0 -1
- package/dist/languages/definitions/cFamily.d.ts.map +1 -1
- package/dist/languages/definitions/cFamily.js +0 -2
- package/dist/languages/definitions/cFamily.js.map +1 -1
- package/dist/languages/definitions/cpp.d.ts.map +1 -1
- package/dist/languages/definitions/cpp.js +0 -1
- package/dist/languages/definitions/cpp.js.map +1 -1
- package/dist/languages/definitions/csharp.d.ts.map +1 -1
- package/dist/languages/definitions/csharp.js +0 -2
- package/dist/languages/definitions/csharp.js.map +1 -1
- package/dist/languages/definitions/css.d.ts.map +1 -1
- package/dist/languages/definitions/css.js +0 -2
- package/dist/languages/definitions/css.js.map +1 -1
- package/dist/languages/definitions/go.d.ts.map +1 -1
- package/dist/languages/definitions/go.js +0 -2
- package/dist/languages/definitions/go.js.map +1 -1
- package/dist/languages/definitions/html.d.ts.map +1 -1
- package/dist/languages/definitions/html.js +0 -2
- package/dist/languages/definitions/html.js.map +1 -1
- package/dist/languages/definitions/htmlStub.d.ts.map +1 -1
- package/dist/languages/definitions/htmlStub.js +0 -2
- package/dist/languages/definitions/htmlStub.js.map +1 -1
- package/dist/languages/definitions/java.d.ts.map +1 -1
- package/dist/languages/definitions/java.js +0 -2
- package/dist/languages/definitions/java.js.map +1 -1
- package/dist/languages/definitions/javascript.d.ts.map +1 -1
- package/dist/languages/definitions/javascript.js +0 -2
- package/dist/languages/definitions/javascript.js.map +1 -1
- package/dist/languages/definitions/kotlin.d.ts.map +1 -1
- package/dist/languages/definitions/kotlin.js +0 -2
- package/dist/languages/definitions/kotlin.js.map +1 -1
- package/dist/languages/definitions/less.d.ts.map +1 -1
- package/dist/languages/definitions/less.js +0 -2
- package/dist/languages/definitions/less.js.map +1 -1
- package/dist/languages/definitions/php.d.ts.map +1 -1
- package/dist/languages/definitions/php.js +0 -2
- package/dist/languages/definitions/php.js.map +1 -1
- package/dist/languages/definitions/python.d.ts.map +1 -1
- package/dist/languages/definitions/python.js +0 -2
- package/dist/languages/definitions/python.js.map +1 -1
- package/dist/languages/definitions/ruby.d.ts.map +1 -1
- package/dist/languages/definitions/ruby.js +0 -2
- package/dist/languages/definitions/ruby.js.map +1 -1
- package/dist/languages/definitions/rust.d.ts.map +1 -1
- package/dist/languages/definitions/rust.js +0 -2
- package/dist/languages/definitions/rust.js.map +1 -1
- package/dist/languages/definitions/scss.d.ts.map +1 -1
- package/dist/languages/definitions/scss.js +0 -2
- package/dist/languages/definitions/scss.js.map +1 -1
- package/dist/languages/definitions/sql.d.ts.map +1 -1
- package/dist/languages/definitions/sql.js +0 -2
- package/dist/languages/definitions/sql.js.map +1 -1
- package/dist/languages/definitions/svelte.d.ts.map +1 -1
- package/dist/languages/definitions/svelte.js +0 -2
- package/dist/languages/definitions/svelte.js.map +1 -1
- package/dist/languages/definitions/swift.d.ts.map +1 -1
- package/dist/languages/definitions/swift.js +0 -2
- package/dist/languages/definitions/swift.js.map +1 -1
- package/dist/languages/definitions/typescript.d.ts.map +1 -1
- package/dist/languages/definitions/typescript.js +0 -3
- package/dist/languages/definitions/typescript.js.map +1 -1
- package/dist/languages/definitions/vue.d.ts.map +1 -1
- package/dist/languages/definitions/vue.js +0 -2
- package/dist/languages/definitions/vue.js.map +1 -1
- package/dist/languages/definitions/zig.d.ts.map +1 -1
- package/dist/languages/definitions/zig.js +0 -2
- package/dist/languages/definitions/zig.js.map +1 -1
- package/dist/languages/filePrep.d.ts +0 -2
- package/dist/languages/filePrep.d.ts.map +1 -1
- package/dist/languages/filePrep.js +0 -1
- package/dist/languages/filePrep.js.map +1 -1
- package/dist/languages/types.d.ts +19 -5
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages.d.ts +1 -3
- package/dist/languages.d.ts.map +1 -1
- package/dist/languages.js +0 -7
- package/dist/languages.js.map +1 -1
- package/dist/lifecycle/manifest.d.ts.map +1 -1
- package/dist/lifecycle/manifest.js +21 -4
- package/dist/lifecycle/manifest.js.map +1 -1
- package/dist/mcp/handlers.d.ts +275 -0
- package/dist/mcp/handlers.d.ts.map +1 -0
- package/dist/mcp/handlers.js +657 -0
- package/dist/mcp/handlers.js.map +1 -0
- package/dist/mcp/httpTransport.d.ts +14 -0
- package/dist/mcp/httpTransport.d.ts.map +1 -0
- package/dist/mcp/httpTransport.js +185 -0
- package/dist/mcp/httpTransport.js.map +1 -0
- package/dist/mcp/legacySessions.d.ts +38 -0
- package/dist/mcp/legacySessions.d.ts.map +1 -0
- package/dist/mcp/legacySessions.js +264 -0
- package/dist/mcp/legacySessions.js.map +1 -0
- package/dist/mcp/lifecycleHealth.d.ts +12 -0
- package/dist/mcp/lifecycleHealth.d.ts.map +1 -0
- package/dist/mcp/lifecycleHealth.js +37 -0
- package/dist/mcp/lifecycleHealth.js.map +1 -0
- package/dist/mcp/protocol.d.ts +35 -0
- package/dist/mcp/protocol.d.ts.map +1 -0
- package/dist/mcp/protocol.js +577 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -306
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +13 -1609
- package/dist/mcp/server.js.map +1 -1
- package/dist/native/bindingLoader.d.ts +27 -0
- package/dist/native/bindingLoader.d.ts.map +1 -1
- package/dist/native/bindingLoader.js +18 -1
- package/dist/native/bindingLoader.js.map +1 -1
- package/dist/native/contracts.d.ts +59 -12
- package/dist/native/contracts.d.ts.map +1 -1
- package/dist/native/execution.d.ts +10 -1
- package/dist/native/execution.d.ts.map +1 -1
- package/dist/native/execution.js +59 -3
- package/dist/native/execution.js.map +1 -1
- package/dist/native/projectedTree.d.ts +20 -5
- package/dist/native/projectedTree.d.ts.map +1 -1
- package/dist/native/projectedTree.js +100 -48
- package/dist/native/projectedTree.js.map +1 -1
- package/dist/native/runtime.d.ts +47 -1
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +151 -8
- package/dist/native/runtime.js.map +1 -1
- package/dist/native/runtimeCache.d.ts +92 -0
- package/dist/native/runtimeCache.d.ts.map +1 -1
- package/dist/native/runtimeCache.js +443 -13
- package/dist/native/runtimeCache.js.map +1 -1
- package/dist/native/treeShape.d.ts +21 -0
- package/dist/native/treeShape.d.ts.map +1 -0
- package/dist/native/treeShape.js +28 -0
- package/dist/native/treeShape.js.map +1 -0
- package/dist/native/treeSitterNative.d.ts +3 -3
- package/dist/native/treeSitterNative.d.ts.map +1 -1
- package/dist/native/treeSitterNative.js +2 -2
- package/dist/native/treeSitterNative.js.map +1 -1
- package/dist/presets.d.ts +7 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -0
- package/dist/presets.js.map +1 -1
- package/dist/review/deleted.js +2 -2
- package/dist/review/deleted.js.map +1 -1
- package/dist/review.js.map +1 -1
- package/dist/sql/extractFacts.d.ts.map +1 -1
- package/dist/sql/extractFacts.js +1 -1
- package/dist/sql/extractFacts.js.map +1 -1
- package/dist/util/lazySymbols.js +1 -1
- package/dist/util/lazySymbols.js.map +1 -1
- package/dist/util/projectFiles.js +1 -1
- package/dist/util/projectFiles.js.map +1 -1
- package/dist/util/resolution/jvm.js +1 -1
- package/dist/util/resolution/jvm.js.map +1 -1
- package/dist/util/resolution/php.d.ts.map +1 -1
- package/dist/util/resolution/php.js +0 -1
- package/dist/util/resolution/php.js.map +1 -1
- package/dist/util/resolution.d.ts.map +1 -1
- package/dist/util/resolution.js +5 -5
- package/dist/util/resolution.js.map +1 -1
- package/dist/worker/nativeExtractWorker.d.ts +10 -4
- package/dist/worker/nativeExtractWorker.d.ts.map +1 -1
- package/dist/worker/nativeExtractWorker.js +55 -3
- package/dist/worker/nativeExtractWorker.js.map +1 -1
- package/dist/worker/nativeWorkerPool.d.ts +4 -0
- package/dist/worker/nativeWorkerPool.d.ts.map +1 -1
- package/dist/worker/nativeWorkerPool.js +6 -1
- package/dist/worker/nativeWorkerPool.js.map +1 -1
- package/docs/mcp.md +16 -2
- package/package.json +3 -3
- package/dist/languages/definitions/loadLanguage.d.ts +0 -2
- package/dist/languages/definitions/loadLanguage.d.ts.map +0 -1
- package/dist/languages/definitions/loadLanguage.js +0 -2
- package/dist/languages/definitions/loadLanguage.js.map +0 -1
- package/dist/parserBackend.d.ts +0 -57
- package/dist/parserBackend.d.ts.map +0 -1
- package/dist/parserBackend.js +0 -33
- package/dist/parserBackend.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeBindingOrigin } from "./contracts.js";
|
|
1
2
|
export type NativeCacheManifestV1 = {
|
|
2
3
|
schemaVersion: 1;
|
|
3
4
|
packageName: string;
|
|
@@ -8,12 +9,57 @@ export type NativeCacheManifestV1 = {
|
|
|
8
9
|
sourceSha256: string;
|
|
9
10
|
cachedAt: string;
|
|
10
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Written as a path-keyed record beside `manifest.json` after a process has successfully
|
|
14
|
+
* loaded the cached binary. `manifest.json` is content-addressed and published once,
|
|
15
|
+
* immutably; these records are mutable (the TTL refreshes them) and carry facts that only exist
|
|
16
|
+
* after the addon loads. One record per source realpath lets projects sharing the binary retain
|
|
17
|
+
* their own origin without invalidating each other's fast path.
|
|
18
|
+
*/
|
|
19
|
+
export type NativeCacheIdentityV1 = {
|
|
20
|
+
version: 1;
|
|
21
|
+
/** Size and mtime of the installed source binary at the last full verification. */
|
|
22
|
+
sourceSize: number;
|
|
23
|
+
sourceMtimeMs: number;
|
|
24
|
+
/** Size and mtime of the cached copy that was verified and then loaded. */
|
|
25
|
+
cachedSize: number;
|
|
26
|
+
cachedMtimeMs: number;
|
|
27
|
+
sha256: string;
|
|
28
|
+
cacheKey: string;
|
|
29
|
+
sourcePath: string;
|
|
30
|
+
loadedPath: string;
|
|
31
|
+
verifiedAt: string;
|
|
32
|
+
/**
|
|
33
|
+
* The runtime that proved this file loadable. The stat fields say the bytes are unchanged;
|
|
34
|
+
* they say nothing about whether this Node can still load them. A major Node upgrade changes
|
|
35
|
+
* the ABI, so the same bytes stop loading while every other check still matches, and the fast
|
|
36
|
+
* path would report an addon as available that the build then falls back away from.
|
|
37
|
+
*/
|
|
38
|
+
runtime: {
|
|
39
|
+
abi: string;
|
|
40
|
+
platform: string;
|
|
41
|
+
arch: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Recorded so a cache-hit command can answer "which languages does the addon support"
|
|
45
|
+
* without loading it. Present only once a load has succeeded for this exact file.
|
|
46
|
+
*/
|
|
47
|
+
supportedLanguageIds: string[];
|
|
48
|
+
/**
|
|
49
|
+
* The origin the loading process reported, stored whole rather than rebuilt field by field.
|
|
50
|
+
* The runtime fingerprint embeds this object verbatim, so replaying it is what guarantees a
|
|
51
|
+
* fast-path fingerprint is byte-identical to the one a full load produces.
|
|
52
|
+
*/
|
|
53
|
+
origin: NativeBindingOrigin;
|
|
54
|
+
};
|
|
11
55
|
export type PrepareNativeRuntimeCacheRequest = {
|
|
12
56
|
sourcePath: string;
|
|
13
57
|
packageName: string;
|
|
14
58
|
packageVersion: string;
|
|
15
59
|
target: string;
|
|
16
60
|
cacheRoot?: string | undefined;
|
|
61
|
+
/** Injected in tests so the identity TTL branch is reachable without waiting a day. */
|
|
62
|
+
now?: number | undefined;
|
|
17
63
|
};
|
|
18
64
|
export type PrepareNativeRuntimeCacheResult = {
|
|
19
65
|
status: "cached" | "reused";
|
|
@@ -21,6 +67,17 @@ export type PrepareNativeRuntimeCacheResult = {
|
|
|
21
67
|
loadedPath: string;
|
|
22
68
|
cacheKey: string;
|
|
23
69
|
sha256: string;
|
|
70
|
+
/** Verified stat snapshots used to reject a source/cache replacement before identity recording. */
|
|
71
|
+
sourceSize: number;
|
|
72
|
+
sourceMtimeMs: number;
|
|
73
|
+
cachedSize: number;
|
|
74
|
+
cachedMtimeMs: number;
|
|
75
|
+
/**
|
|
76
|
+
* True when this call hashed the files rather than trusting a recorded identity. Only a
|
|
77
|
+
* verified result may refresh the identity record, otherwise each fast-path hit would
|
|
78
|
+
* push the re-verification TTL out and the entry would never be hashed again.
|
|
79
|
+
*/
|
|
80
|
+
verified: boolean;
|
|
24
81
|
} | {
|
|
25
82
|
status: "unavailable";
|
|
26
83
|
sourcePath: string;
|
|
@@ -29,8 +86,43 @@ export type PrepareNativeRuntimeCacheResult = {
|
|
|
29
86
|
type FileHash = {
|
|
30
87
|
sha256: string;
|
|
31
88
|
size: number;
|
|
89
|
+
mtimeMs: number;
|
|
32
90
|
};
|
|
33
91
|
export declare function hashFileStreaming(filePath: string, onChunk?: (bytesRead: number) => void): FileHash;
|
|
92
|
+
export type NativeCacheLookupRequest = {
|
|
93
|
+
sourcePath: string;
|
|
94
|
+
packageName: string;
|
|
95
|
+
packageVersion: string;
|
|
96
|
+
target: string;
|
|
97
|
+
cacheRoot?: string | undefined;
|
|
98
|
+
/** Injected in tests so the TTL branch is reachable without waiting a day. */
|
|
99
|
+
now?: number | undefined;
|
|
100
|
+
};
|
|
101
|
+
export type NativeCacheLookupHit = {
|
|
102
|
+
entryPath: string;
|
|
103
|
+
identity: NativeCacheIdentityV1;
|
|
104
|
+
revalidateAt: number;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Find a cache entry for this (package version, target) whose recorded identity still matches
|
|
108
|
+
* the files on disk, without hashing either of them.
|
|
109
|
+
*
|
|
110
|
+
* The entry directory is named `<packageVersion>-<sha256>`, so locating it the normal way means
|
|
111
|
+
* hashing the source first. Scanning the target directory for the version prefix and comparing
|
|
112
|
+
* the stat fields recorded at the last full verification reaches the same entry with bounded
|
|
113
|
+
* metadata reads. A miss - no entry, no identity file, a stat mismatch, or an expired
|
|
114
|
+
* TTL - returns null, and every caller then does the full hash-and-verify pass.
|
|
115
|
+
*/
|
|
116
|
+
export declare function lookupNativeRuntimeCacheEntry(request: NativeCacheLookupRequest): NativeCacheLookupHit | null;
|
|
117
|
+
/**
|
|
118
|
+
* Record what a successful load proved about this entry, so the next process can skip both
|
|
119
|
+
* SHA-256 passes and the addon load itself. Called only after the binary at `loadedPath` has
|
|
120
|
+
* been fully verified and then loaded, so every field describes a state known to work.
|
|
121
|
+
*
|
|
122
|
+
* Replaces any previous record atomically: the TTL refresh rewrites it, unlike the immutable
|
|
123
|
+
* `manifest.json` beside it.
|
|
124
|
+
*/
|
|
125
|
+
export declare function recordNativeRuntimeCacheIdentity(identity: Omit<NativeCacheIdentityV1, "version" | "runtime">): void;
|
|
34
126
|
export declare function prepareNativeRuntimeCache(request: PrepareNativeRuntimeCacheRequest): PrepareNativeRuntimeCacheResult;
|
|
35
127
|
export {};
|
|
36
128
|
//# sourceMappingURL=runtimeCache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtimeCache.d.ts","sourceRoot":"","sources":["../../src/native/runtimeCache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtimeCache.d.ts","sourceRoot":"","sources":["../../src/native/runtimeCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,32 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const CACHE_SCHEMA_VERSION = 1;
|
|
5
5
|
const HASH_BUFFER_SIZE = 1024 * 1024;
|
|
6
|
+
const IDENTITY_FILE_PREFIX = "identity-";
|
|
7
|
+
const IDENTITY_FILE_SUFFIX = ".json";
|
|
8
|
+
const IDENTITY_VERSION = 1;
|
|
9
|
+
/**
|
|
10
|
+
* How long a stat-only match is trusted before the entry is hashed again.
|
|
11
|
+
*
|
|
12
|
+
* The fast path below skips two 29 MB SHA-256 passes when the source and cached binaries
|
|
13
|
+
* both match the size and mtime recorded at the last full verification. That is weaker than
|
|
14
|
+
* hashing: an attacker who can write the cache directory can also preserve both fields. The
|
|
15
|
+
* TTL bounds how long such a substitution goes unnoticed, so verification still happens -
|
|
16
|
+
* once a day per entry instead of twice per process.
|
|
17
|
+
*/
|
|
18
|
+
const IDENTITY_REVERIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
/**
|
|
20
|
+
* How long an entry for another package version is kept before it is treated as abandoned.
|
|
21
|
+
*
|
|
22
|
+
* The cache root is per-user and shared by every project on the machine, so "not the version I
|
|
23
|
+
* am installing" does not mean "nobody's". Deleting on that basis alone makes two projects
|
|
24
|
+
* pinned to different native versions delete each other's entry on every run, each re-copying
|
|
25
|
+
* 29 MB and never reaching the fast path - worse than the unbounded growth it set out to fix.
|
|
26
|
+
*
|
|
27
|
+
* An entry in use is re-verified at least every IDENTITY_REVERIFY_INTERVAL_MS of use, and that
|
|
28
|
+
* refreshes its timestamp, so an entry older than this window is one no project has run in a
|
|
29
|
+
* month. That is the signal, rather than which version happens to be installing right now.
|
|
30
|
+
*/
|
|
31
|
+
const ABANDONED_ENTRY_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
6
32
|
function asError(error) {
|
|
7
33
|
return error instanceof Error ? error : new Error(String(error));
|
|
8
34
|
}
|
|
@@ -24,7 +50,7 @@ function assertSafePathComponent(componentPath) {
|
|
|
24
50
|
throw new Error(`unsafe native cache path component: ${componentPath}`);
|
|
25
51
|
}
|
|
26
52
|
}
|
|
27
|
-
function prepareSafeDirectory(directoryPath) {
|
|
53
|
+
function prepareSafeDirectory(directoryPath, create = true) {
|
|
28
54
|
const absolute = path.resolve(directoryPath);
|
|
29
55
|
const parsed = path.parse(absolute);
|
|
30
56
|
let current = parsed.root;
|
|
@@ -36,7 +62,7 @@ function prepareSafeDirectory(directoryPath) {
|
|
|
36
62
|
assertSafePathComponent(current);
|
|
37
63
|
}
|
|
38
64
|
catch (error) {
|
|
39
|
-
if (error.code !== "ENOENT") {
|
|
65
|
+
if (error.code !== "ENOENT" || !create) {
|
|
40
66
|
throw error;
|
|
41
67
|
}
|
|
42
68
|
try {
|
|
@@ -51,12 +77,18 @@ function prepareSafeDirectory(directoryPath) {
|
|
|
51
77
|
}
|
|
52
78
|
return fs.realpathSync.native(absolute);
|
|
53
79
|
}
|
|
54
|
-
function
|
|
80
|
+
function findProductionCacheRoot() {
|
|
55
81
|
const localAppData = process.env.LOCALAPPDATA;
|
|
56
|
-
if (!localAppData)
|
|
82
|
+
if (!localAppData)
|
|
83
|
+
return null;
|
|
84
|
+
return path.join(localAppData, "codegraph", "native-cache", `v${CACHE_SCHEMA_VERSION}`);
|
|
85
|
+
}
|
|
86
|
+
function resolveProductionCacheRoot() {
|
|
87
|
+
const cacheRoot = findProductionCacheRoot();
|
|
88
|
+
if (!cacheRoot) {
|
|
57
89
|
throw new Error("LOCALAPPDATA is unavailable; cannot prepare the Windows native runtime cache");
|
|
58
90
|
}
|
|
59
|
-
return
|
|
91
|
+
return cacheRoot;
|
|
60
92
|
}
|
|
61
93
|
export function hashFileStreaming(filePath, onChunk) {
|
|
62
94
|
const before = fs.statSync(filePath);
|
|
@@ -80,10 +112,11 @@ export function hashFileStreaming(filePath, onChunk) {
|
|
|
80
112
|
finally {
|
|
81
113
|
fs.closeSync(descriptor);
|
|
82
114
|
}
|
|
83
|
-
|
|
115
|
+
const after = fs.statSync(filePath);
|
|
116
|
+
if (!after.isFile() || size !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
84
117
|
throw new Error(`native binary changed while hashing: ${filePath}`);
|
|
85
118
|
}
|
|
86
|
-
return { sha256: hash.digest("hex"), size };
|
|
119
|
+
return { sha256: hash.digest("hex"), size, mtimeMs: before.mtimeMs };
|
|
87
120
|
}
|
|
88
121
|
function randomSuffix() {
|
|
89
122
|
return `${process.pid}.${randomBytes(8).toString("hex")}`;
|
|
@@ -124,10 +157,10 @@ function removeRegularTemporaryFile(filePath) {
|
|
|
124
157
|
function verifyFile(filePath, expected) {
|
|
125
158
|
try {
|
|
126
159
|
const actual = hashFileStreaming(filePath);
|
|
127
|
-
return actual.size === expected.size && actual.sha256 === expected.sha256;
|
|
160
|
+
return actual.size === expected.size && actual.sha256 === expected.sha256 ? actual : null;
|
|
128
161
|
}
|
|
129
162
|
catch {
|
|
130
|
-
return
|
|
163
|
+
return null;
|
|
131
164
|
}
|
|
132
165
|
}
|
|
133
166
|
function publishExclusive(temporaryPath, finalPath) {
|
|
@@ -183,11 +216,399 @@ function writeManifest(entryPath, manifest) {
|
|
|
183
216
|
throw error;
|
|
184
217
|
}
|
|
185
218
|
}
|
|
219
|
+
function currentRuntimeStamp() {
|
|
220
|
+
return { abi: process.versions.modules, platform: process.platform, arch: process.arch };
|
|
221
|
+
}
|
|
222
|
+
function identityFilePath(entryPath, sourcePath) {
|
|
223
|
+
const sourceKey = createHash("sha256").update(sourcePath).digest("hex");
|
|
224
|
+
return path.join(entryPath, `${IDENTITY_FILE_PREFIX}${sourceKey}${IDENTITY_FILE_SUFFIX}`);
|
|
225
|
+
}
|
|
226
|
+
function isIdentityFileName(fileName) {
|
|
227
|
+
return /^identity-[a-f0-9]{64}\.json$/.test(fileName);
|
|
228
|
+
}
|
|
229
|
+
function readCacheIdentityFile(filePath) {
|
|
230
|
+
let parsed;
|
|
231
|
+
try {
|
|
232
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Absent (an entry written before this file existed), unreadable, or malformed. All three
|
|
236
|
+
// mean the same thing to the caller: no fast path, fall back to hashing.
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (!parsed || typeof parsed !== "object")
|
|
240
|
+
return null;
|
|
241
|
+
const candidate = parsed;
|
|
242
|
+
if (candidate.version !== IDENTITY_VERSION)
|
|
243
|
+
return null;
|
|
244
|
+
const numbers = [candidate.sourceSize, candidate.sourceMtimeMs, candidate.cachedSize, candidate.cachedMtimeMs];
|
|
245
|
+
if (numbers.some((value) => typeof value !== "number" || !Number.isFinite(value)))
|
|
246
|
+
return null;
|
|
247
|
+
const strings = [
|
|
248
|
+
candidate.sha256,
|
|
249
|
+
candidate.cacheKey,
|
|
250
|
+
candidate.sourcePath,
|
|
251
|
+
candidate.loadedPath,
|
|
252
|
+
candidate.verifiedAt,
|
|
253
|
+
];
|
|
254
|
+
if (strings.some((value) => typeof value !== "string" || !value))
|
|
255
|
+
return null;
|
|
256
|
+
if (!Array.isArray(candidate.supportedLanguageIds))
|
|
257
|
+
return null;
|
|
258
|
+
if (candidate.supportedLanguageIds.some((value) => typeof value !== "string"))
|
|
259
|
+
return null;
|
|
260
|
+
const origin = candidate.origin;
|
|
261
|
+
if (!origin || typeof origin !== "object" || origin.mode !== "cache" || typeof origin.packageName !== "string") {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const runtime = candidate.runtime;
|
|
265
|
+
if (!runtime || typeof runtime !== "object")
|
|
266
|
+
return null;
|
|
267
|
+
if (typeof runtime.abi !== "string" || typeof runtime.platform !== "string" || typeof runtime.arch !== "string") {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return candidate;
|
|
271
|
+
}
|
|
272
|
+
function readCacheIdentity(entryPath, sourcePath) {
|
|
273
|
+
return readCacheIdentityFile(identityFilePath(entryPath, sourcePath));
|
|
274
|
+
}
|
|
275
|
+
function latestCacheIdentityVerifiedAt(entryPath, now) {
|
|
276
|
+
let fileNames;
|
|
277
|
+
try {
|
|
278
|
+
fileNames = fs.readdirSync(entryPath);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return Number.NaN;
|
|
282
|
+
}
|
|
283
|
+
let latest = Number.NaN;
|
|
284
|
+
for (const fileName of fileNames) {
|
|
285
|
+
if (!isIdentityFileName(fileName))
|
|
286
|
+
continue;
|
|
287
|
+
const identity = readCacheIdentityFile(path.join(entryPath, fileName));
|
|
288
|
+
const verifiedAt = identity ? Date.parse(identity.verifiedAt) : Number.NaN;
|
|
289
|
+
if (!Number.isFinite(verifiedAt) || verifiedAt > now)
|
|
290
|
+
continue;
|
|
291
|
+
if (!Number.isFinite(latest) || verifiedAt > latest)
|
|
292
|
+
latest = verifiedAt;
|
|
293
|
+
}
|
|
294
|
+
return latest;
|
|
295
|
+
}
|
|
296
|
+
function statOrNull(filePath) {
|
|
297
|
+
try {
|
|
298
|
+
const stats = fs.statSync(filePath);
|
|
299
|
+
return stats.isFile() ? stats : null;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function lstatRegularFileOrNull(filePath) {
|
|
306
|
+
try {
|
|
307
|
+
const stats = fs.lstatSync(filePath);
|
|
308
|
+
return stats.isFile() && !stats.isSymbolicLink() ? stats : null;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function isSafeExistingDirectory(directoryPath) {
|
|
315
|
+
try {
|
|
316
|
+
prepareSafeDirectory(directoryPath, false);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function pathsResolveEqual(left, right) {
|
|
324
|
+
return path.resolve(left) === path.resolve(right);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Find a cache entry for this (package version, target) whose recorded identity still matches
|
|
328
|
+
* the files on disk, without hashing either of them.
|
|
329
|
+
*
|
|
330
|
+
* The entry directory is named `<packageVersion>-<sha256>`, so locating it the normal way means
|
|
331
|
+
* hashing the source first. Scanning the target directory for the version prefix and comparing
|
|
332
|
+
* the stat fields recorded at the last full verification reaches the same entry with bounded
|
|
333
|
+
* metadata reads. A miss - no entry, no identity file, a stat mismatch, or an expired
|
|
334
|
+
* TTL - returns null, and every caller then does the full hash-and-verify pass.
|
|
335
|
+
*/
|
|
336
|
+
export function lookupNativeRuntimeCacheEntry(request) {
|
|
337
|
+
try {
|
|
338
|
+
assertSafeCacheSegment(request.target, "target");
|
|
339
|
+
assertSafeCacheSegment(request.packageVersion, "package version");
|
|
340
|
+
const cacheRoot = request.cacheRoot ?? findProductionCacheRoot();
|
|
341
|
+
if (!cacheRoot)
|
|
342
|
+
return null;
|
|
343
|
+
const sourceRealPath = fs.realpathSync.native(request.sourcePath);
|
|
344
|
+
const sourceStats = statOrNull(sourceRealPath);
|
|
345
|
+
if (!sourceStats)
|
|
346
|
+
return null;
|
|
347
|
+
const runtime = currentRuntimeStamp();
|
|
348
|
+
const resolvedCacheRoot = path.resolve(cacheRoot);
|
|
349
|
+
const targetPath = path.join(resolvedCacheRoot, request.target);
|
|
350
|
+
if (!isSafeExistingDirectory(resolvedCacheRoot) || !isSafeExistingDirectory(targetPath))
|
|
351
|
+
return null;
|
|
352
|
+
const prefix = `${request.packageVersion}-`;
|
|
353
|
+
let entryNames;
|
|
354
|
+
try {
|
|
355
|
+
entryNames = fs.readdirSync(targetPath);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const now = request.now ?? Date.now();
|
|
361
|
+
for (const entryName of entryNames) {
|
|
362
|
+
if (!entryName.startsWith(prefix))
|
|
363
|
+
continue;
|
|
364
|
+
const entryPath = path.join(targetPath, entryName);
|
|
365
|
+
if (!isSafeExistingDirectory(entryPath))
|
|
366
|
+
continue;
|
|
367
|
+
const identity = readCacheIdentity(entryPath, sourceRealPath);
|
|
368
|
+
if (!identity)
|
|
369
|
+
continue;
|
|
370
|
+
const expectedEntryName = `${request.packageVersion}-${identity.sha256}`;
|
|
371
|
+
const expectedCacheKey = `${request.packageName}@${request.packageVersion}:${request.target}:${identity.sha256}`;
|
|
372
|
+
const loadedPath = path.resolve(identity.loadedPath);
|
|
373
|
+
if (entryName !== expectedEntryName ||
|
|
374
|
+
identity.cacheKey !== expectedCacheKey ||
|
|
375
|
+
identity.loadedPath !== loadedPath ||
|
|
376
|
+
path.dirname(loadedPath) !== entryPath ||
|
|
377
|
+
path.basename(loadedPath) !== path.basename(sourceRealPath)) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// Same version, same bytes, different install. The cache root is shared by every project
|
|
381
|
+
// on the machine, and npm can produce byte-identical files with matching mtimes, so size
|
|
382
|
+
// and mtime alone can match another project's record - whose sourcePath would then be
|
|
383
|
+
// replayed into this build's binding origin and change its runtime fingerprint.
|
|
384
|
+
if (identity.sourcePath !== sourceRealPath)
|
|
385
|
+
continue;
|
|
386
|
+
if (identity.sourceSize !== sourceStats.size || identity.sourceMtimeMs !== sourceStats.mtimeMs)
|
|
387
|
+
continue;
|
|
388
|
+
if (identity.origin.packageName !== request.packageName ||
|
|
389
|
+
identity.origin.packageVersion !== request.packageVersion ||
|
|
390
|
+
identity.origin.target !== request.target ||
|
|
391
|
+
identity.origin.sourcePath === undefined ||
|
|
392
|
+
identity.origin.loadedPath === undefined ||
|
|
393
|
+
!pathsResolveEqual(identity.origin.sourcePath, sourceRealPath) ||
|
|
394
|
+
!pathsResolveEqual(identity.origin.loadedPath, identity.loadedPath) ||
|
|
395
|
+
identity.origin.cacheKey !== identity.cacheKey ||
|
|
396
|
+
identity.origin.sha256 !== identity.sha256) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Recorded by a runtime that could load the file. A different ABI cannot, and claiming
|
|
400
|
+
// otherwise would report native as available for a build that falls back away from it.
|
|
401
|
+
if (identity.runtime.abi !== runtime.abi ||
|
|
402
|
+
identity.runtime.platform !== runtime.platform ||
|
|
403
|
+
identity.runtime.arch !== runtime.arch) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const cachedStats = lstatRegularFileOrNull(identity.loadedPath);
|
|
407
|
+
if (!cachedStats)
|
|
408
|
+
continue;
|
|
409
|
+
if (identity.cachedSize !== cachedStats.size || identity.cachedMtimeMs !== cachedStats.mtimeMs)
|
|
410
|
+
continue;
|
|
411
|
+
const verifiedAt = Date.parse(identity.verifiedAt);
|
|
412
|
+
if (!Number.isFinite(verifiedAt) || verifiedAt > now || now - verifiedAt >= IDENTITY_REVERIFY_INTERVAL_MS) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (!isWithinDirectory(identity.loadedPath, resolvedCacheRoot))
|
|
416
|
+
continue;
|
|
417
|
+
return { entryPath, identity, revalidateAt: verifiedAt + IDENTITY_REVERIFY_INTERVAL_MS };
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Record what a successful load proved about this entry, so the next process can skip both
|
|
427
|
+
* SHA-256 passes and the addon load itself. Called only after the binary at `loadedPath` has
|
|
428
|
+
* been fully verified and then loaded, so every field describes a state known to work.
|
|
429
|
+
*
|
|
430
|
+
* Replaces any previous record atomically: the TTL refresh rewrites it, unlike the immutable
|
|
431
|
+
* `manifest.json` beside it.
|
|
432
|
+
*/
|
|
433
|
+
export function recordNativeRuntimeCacheIdentity(identity) {
|
|
434
|
+
try {
|
|
435
|
+
const sourceStats = statOrNull(identity.sourcePath);
|
|
436
|
+
const cachedStats = lstatRegularFileOrNull(identity.loadedPath);
|
|
437
|
+
if (!sourceStats ||
|
|
438
|
+
!cachedStats ||
|
|
439
|
+
sourceStats.size !== identity.sourceSize ||
|
|
440
|
+
sourceStats.mtimeMs !== identity.sourceMtimeMs ||
|
|
441
|
+
cachedStats.size !== identity.cachedSize ||
|
|
442
|
+
cachedStats.mtimeMs !== identity.cachedMtimeMs) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const entryPath = path.dirname(identity.loadedPath);
|
|
446
|
+
const record = {
|
|
447
|
+
version: IDENTITY_VERSION,
|
|
448
|
+
runtime: currentRuntimeStamp(),
|
|
449
|
+
...identity,
|
|
450
|
+
};
|
|
451
|
+
const finalPath = identityFilePath(entryPath, identity.sourcePath);
|
|
452
|
+
const temporaryPath = `${finalPath}.${randomSuffix()}.tmp`;
|
|
453
|
+
try {
|
|
454
|
+
const descriptor = fs.openSync(temporaryPath, "wx");
|
|
455
|
+
try {
|
|
456
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
457
|
+
fs.fsyncSync(descriptor);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
fs.closeSync(descriptor);
|
|
461
|
+
}
|
|
462
|
+
fs.renameSync(temporaryPath, finalPath);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
removeRegularTemporaryFile(temporaryPath);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Best effort. Failing to record identity costs the next process the slow path, nothing more.
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* When an entry was last known to be in use, and which version it holds.
|
|
474
|
+
*
|
|
475
|
+
* Path-keyed identity records are refreshed whenever a process re-verifies an entry, so their
|
|
476
|
+
* latest non-future timestamp is the freshest signal. `manifest.json`'s `cachedAt` covers an
|
|
477
|
+
* entry that has never been loaded through the fast path. An entry that yields neither is left
|
|
478
|
+
* alone rather than guessed about.
|
|
479
|
+
*/
|
|
480
|
+
function readEntrySummary(entryPath, now) {
|
|
481
|
+
let packageVersion = null;
|
|
482
|
+
let sourceFileName = null;
|
|
483
|
+
let cachedAtMs = null;
|
|
484
|
+
try {
|
|
485
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(entryPath, "manifest.json"), "utf8"));
|
|
486
|
+
if (parsed && typeof parsed === "object") {
|
|
487
|
+
const manifest = parsed;
|
|
488
|
+
if (typeof manifest.packageVersion === "string" && manifest.packageVersion) {
|
|
489
|
+
packageVersion = manifest.packageVersion;
|
|
490
|
+
}
|
|
491
|
+
if (typeof manifest.sourceFileName === "string" &&
|
|
492
|
+
manifest.sourceFileName &&
|
|
493
|
+
path.basename(manifest.sourceFileName) === manifest.sourceFileName) {
|
|
494
|
+
sourceFileName = manifest.sourceFileName;
|
|
495
|
+
}
|
|
496
|
+
const cachedAt = typeof manifest.cachedAt === "string" ? Date.parse(manifest.cachedAt) : Number.NaN;
|
|
497
|
+
if (Number.isFinite(cachedAt))
|
|
498
|
+
cachedAtMs = cachedAt;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// Missing or malformed: indistinguishable from an entry another process is still writing.
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
if (!packageVersion || !sourceFileName)
|
|
506
|
+
return null;
|
|
507
|
+
const verifiedAt = latestCacheIdentityVerifiedAt(entryPath, now);
|
|
508
|
+
const lastUsedMs = Number.isFinite(verifiedAt) ? verifiedAt : cachedAtMs;
|
|
509
|
+
if (lastUsedMs === null)
|
|
510
|
+
return null;
|
|
511
|
+
return { packageVersion, lastUsedMs, sourceFileName };
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Delete the mapped binary before its metadata. Windows rejects an unlink of a loaded DLL,
|
|
515
|
+
* leaving the whole entry unchanged instead of partially deleting it through recursive removal.
|
|
516
|
+
*/
|
|
517
|
+
function removeAbandonedEntry(entryPath, sourceFileName) {
|
|
518
|
+
const cachedBinaryPath = path.join(entryPath, sourceFileName);
|
|
519
|
+
if (!isWithinDirectory(cachedBinaryPath, entryPath) || path.dirname(cachedBinaryPath) !== entryPath) {
|
|
520
|
+
throw new Error("invalid native cache binary path");
|
|
521
|
+
}
|
|
522
|
+
const stats = fs.lstatSync(cachedBinaryPath);
|
|
523
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
524
|
+
throw new Error("unsafe native cache binary");
|
|
525
|
+
fs.unlinkSync(cachedBinaryPath);
|
|
526
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Delete entries that no project has used in a long time.
|
|
530
|
+
*
|
|
531
|
+
* The cache only ever grew: every upgrade left roughly 29 MB behind, and a machine that had
|
|
532
|
+
* tracked a few releases carried well over a hundred megabytes of addons nothing would load
|
|
533
|
+
* again. Keeping the directory small also keeps it warm, which is what the stat-only fast path
|
|
534
|
+
* above depends on.
|
|
535
|
+
*
|
|
536
|
+
* Retention is by age rather than by version, because the cache root is per-user and shared by
|
|
537
|
+
* every project on the machine. Removing "every version that is not the one installing" would
|
|
538
|
+
* make two projects pinned to different native versions delete each other's entry on every run.
|
|
539
|
+
*
|
|
540
|
+
* Best effort throughout, and deliberately conservative:
|
|
541
|
+
* - the entry just produced is never a candidate, nor is any entry for its version;
|
|
542
|
+
* - an entry whose manifest is missing, unreadable, or undated is left alone, because that is
|
|
543
|
+
* what a concurrent population in progress looks like;
|
|
544
|
+
* - Windows holds a lock on a loaded DLL, so unlinking its binary fails with EBUSY or EPERM
|
|
545
|
+
* before recursive metadata removal begins. Those failures preserve the whole entry and are
|
|
546
|
+
* expected rather than exceptional, so they are simply skipped.
|
|
547
|
+
*/
|
|
548
|
+
function pruneAbandonedEntries(targetPath, cacheRoot, keepEntryPath, keepVersion, now) {
|
|
549
|
+
let entryNames;
|
|
550
|
+
try {
|
|
551
|
+
entryNames = fs.readdirSync(targetPath);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
for (const entryName of entryNames) {
|
|
557
|
+
const entryPath = path.join(targetPath, entryName);
|
|
558
|
+
if (entryPath === keepEntryPath)
|
|
559
|
+
continue;
|
|
560
|
+
if (!isWithinDirectory(entryPath, cacheRoot))
|
|
561
|
+
continue;
|
|
562
|
+
try {
|
|
563
|
+
if (!fs.lstatSync(entryPath).isDirectory())
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const summary = readEntrySummary(entryPath, now);
|
|
570
|
+
if (!summary || summary.packageVersion === keepVersion)
|
|
571
|
+
continue;
|
|
572
|
+
if (now - summary.lastUsedMs < ABANDONED_ENTRY_RETENTION_MS)
|
|
573
|
+
continue;
|
|
574
|
+
try {
|
|
575
|
+
removeAbandonedEntry(entryPath, summary.sourceFileName);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// In use, incomplete, or not ours to remove. The next install tries again.
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
186
582
|
export function prepareNativeRuntimeCache(request) {
|
|
187
583
|
const sourcePath = path.resolve(request.sourcePath);
|
|
188
584
|
try {
|
|
189
585
|
assertSafeCacheSegment(request.target, "target");
|
|
190
586
|
assertSafeCacheSegment(request.packageVersion, "package version");
|
|
587
|
+
// Cheap-identity fast path: a previous process verified this exact pair of files and
|
|
588
|
+
// recorded their stat fields, so nothing here needs to be hashed. Population and every
|
|
589
|
+
// mismatch still take the full path below.
|
|
590
|
+
const hit = lookupNativeRuntimeCacheEntry({
|
|
591
|
+
sourcePath,
|
|
592
|
+
packageName: request.packageName,
|
|
593
|
+
packageVersion: request.packageVersion,
|
|
594
|
+
target: request.target,
|
|
595
|
+
...(request.cacheRoot !== undefined ? { cacheRoot: request.cacheRoot } : {}),
|
|
596
|
+
...(request.now !== undefined ? { now: request.now } : {}),
|
|
597
|
+
});
|
|
598
|
+
if (hit) {
|
|
599
|
+
return {
|
|
600
|
+
status: "reused",
|
|
601
|
+
sourcePath: hit.identity.sourcePath,
|
|
602
|
+
loadedPath: hit.identity.loadedPath,
|
|
603
|
+
cacheKey: hit.identity.cacheKey,
|
|
604
|
+
sha256: hit.identity.sha256,
|
|
605
|
+
sourceSize: hit.identity.sourceSize,
|
|
606
|
+
sourceMtimeMs: hit.identity.sourceMtimeMs,
|
|
607
|
+
cachedSize: hit.identity.cachedSize,
|
|
608
|
+
cachedMtimeMs: hit.identity.cachedMtimeMs,
|
|
609
|
+
verified: false,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
191
612
|
const sourceRealPath = fs.realpathSync.native(sourcePath);
|
|
192
613
|
const source = hashFileStreaming(sourceRealPath);
|
|
193
614
|
const cacheRoot = prepareSafeDirectory(request.cacheRoot ?? resolveProductionCacheRoot());
|
|
@@ -203,12 +624,13 @@ export function prepareNativeRuntimeCache(request) {
|
|
|
203
624
|
const sourceFileName = path.basename(sourceRealPath);
|
|
204
625
|
const finalPath = path.join(entryPath, sourceFileName);
|
|
205
626
|
let status = "reused";
|
|
206
|
-
|
|
207
|
-
if (!
|
|
208
|
-
if (
|
|
627
|
+
let cached = fs.existsSync(finalPath) ? verifyFile(finalPath, source) : null;
|
|
628
|
+
if (!cached) {
|
|
629
|
+
if (fs.existsSync(finalPath))
|
|
209
630
|
recoverCorruptFinal(finalPath);
|
|
210
631
|
status = populateBinary(sourceRealPath, finalPath, source) ? "cached" : "reused";
|
|
211
|
-
|
|
632
|
+
cached = verifyFile(finalPath, source);
|
|
633
|
+
if (!cached) {
|
|
212
634
|
throw new Error("native cache winner failed integrity verification");
|
|
213
635
|
}
|
|
214
636
|
}
|
|
@@ -222,12 +644,20 @@ export function prepareNativeRuntimeCache(request) {
|
|
|
222
644
|
sourceSha256: source.sha256,
|
|
223
645
|
cachedAt: new Date().toISOString(),
|
|
224
646
|
});
|
|
647
|
+
// Only on the verified path. A fast-path hit means nothing new arrived, so there is nothing
|
|
648
|
+
// to clean up, and the warm run stays free of extra directory work.
|
|
649
|
+
pruneAbandonedEntries(targetPath, cacheRoot, entryPath, request.packageVersion, request.now ?? Date.now());
|
|
225
650
|
return {
|
|
226
651
|
status,
|
|
227
652
|
sourcePath: sourceRealPath,
|
|
228
653
|
loadedPath: finalPath,
|
|
229
654
|
cacheKey,
|
|
230
655
|
sha256: source.sha256,
|
|
656
|
+
sourceSize: source.size,
|
|
657
|
+
sourceMtimeMs: source.mtimeMs,
|
|
658
|
+
cachedSize: cached.size,
|
|
659
|
+
cachedMtimeMs: cached.mtimeMs,
|
|
660
|
+
verified: true,
|
|
231
661
|
};
|
|
232
662
|
}
|
|
233
663
|
catch (error) {
|