169 lines
6.5 KiB
Plaintext
169 lines
6.5 KiB
Plaintext
/**
|
|
* Builds a composite ref key from a snapshot ID and project ID.
|
|
*/
|
|
function refKey(snapshotId, projectId) {
|
|
return `${snapshotId}:${projectId}`;
|
|
}
|
|
/**
|
|
* Client-side cache for source files keyed by (path, parseOptionsKey, contentHash).
|
|
*
|
|
* Supports multiple versions of the same file at the same path (e.g., from
|
|
* different snapshots with different file contents). Each version is identified
|
|
* by its content hash and parse options key.
|
|
*
|
|
* Entries are ref-counted by (snapshot, project) pairs. When a snapshot is
|
|
* disposed, all refs for that snapshot across all projects are released,
|
|
* and entries with no remaining references are evicted.
|
|
*
|
|
* When a new snapshot is created, unchanged cache entries from the previous
|
|
* snapshot are retained per-project. Only files within changed or removed
|
|
* projects are invalidated.
|
|
*/
|
|
export class SourceFileCache {
|
|
/** Map from path to all cached versions of that file */
|
|
cache = new Map();
|
|
/** Map from snapshotId to (projectId → Set of paths fetched through that project) */
|
|
snapshotProjectPaths = new Map();
|
|
/**
|
|
* Get a cached source file already retained for the given (snapshot, project) pair.
|
|
* This does not require a content hash or parse options key — it returns the entry
|
|
* if one exists with a matching ref. Used to skip the server request entirely when
|
|
* retainForSnapshot has already carried over the ref.
|
|
*
|
|
* A given (snapshot, project) pair always parses a file the same way, so there is
|
|
* at most one matching entry per ref.
|
|
*/
|
|
getRetained(path, snapshotId, projectId) {
|
|
const entries = this.cache.get(path);
|
|
if (!entries)
|
|
return undefined;
|
|
const key = refKey(snapshotId, projectId);
|
|
const entry = entries.find(e => e.refs.has(key));
|
|
return entry?.file;
|
|
}
|
|
/**
|
|
* Store a source file in the cache and retain it for the given (snapshot, project) pair.
|
|
* Returns the cached file — which may be an existing entry if the hash matches.
|
|
*/
|
|
set(path, file, parseOptionsKey, contentHash, snapshotId, projectId) {
|
|
let entries = this.cache.get(path);
|
|
if (!entries) {
|
|
entries = [];
|
|
this.cache.set(path, entries);
|
|
}
|
|
const ref = refKey(snapshotId, projectId);
|
|
// Check if we already have this exact version
|
|
const existing = entries.find(e => e.parseOptionsKey === parseOptionsKey && e.contentHash === contentHash);
|
|
if (existing) {
|
|
existing.refs.add(ref);
|
|
this.trackPath(snapshotId, projectId, path);
|
|
return existing.file;
|
|
}
|
|
entries.push({ file, contentHash, parseOptionsKey, refs: new Set([ref]) });
|
|
this.trackPath(snapshotId, projectId, path);
|
|
return file;
|
|
}
|
|
/**
|
|
* Retain cache entries from a previous snapshot for a new snapshot.
|
|
* For each project in the previous snapshot:
|
|
* - Removed projects: skip (don't retain any refs).
|
|
* - Changed projects: retain refs for files not listed in changedFiles/deletedFiles.
|
|
* - Unchanged projects: retain all refs.
|
|
*/
|
|
retainForSnapshot(newSnapshotId, previousSnapshotId, changes) {
|
|
const prevProjectMap = this.snapshotProjectPaths.get(previousSnapshotId);
|
|
if (!prevProjectMap)
|
|
return;
|
|
const removedProjects = new Set(changes?.removedProjects ?? []);
|
|
const changedProjects = changes?.changedProjects ?? {};
|
|
for (const [projectId, paths] of prevProjectMap) {
|
|
if (removedProjects.has(projectId))
|
|
continue;
|
|
const projectChanges = changedProjects[projectId];
|
|
let invalidPaths;
|
|
if (projectChanges) {
|
|
invalidPaths = new Set();
|
|
for (const p of projectChanges.changedFiles ?? [])
|
|
invalidPaths.add(p);
|
|
for (const p of projectChanges.deletedFiles ?? [])
|
|
invalidPaths.add(p);
|
|
}
|
|
const prevRef = refKey(previousSnapshotId, projectId);
|
|
const newRef = refKey(newSnapshotId, projectId);
|
|
for (const path of paths) {
|
|
if (invalidPaths?.has(path))
|
|
continue;
|
|
const entries = this.cache.get(path);
|
|
if (!entries)
|
|
continue;
|
|
for (const entry of entries) {
|
|
if (entry.refs.has(prevRef)) {
|
|
entry.refs.add(newRef);
|
|
this.trackPath(newSnapshotId, projectId, path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Release all entries retained by the given snapshot across all projects.
|
|
* Only visits paths that the snapshot actually referenced.
|
|
* Entries with no remaining refs are evicted.
|
|
*/
|
|
releaseSnapshot(snapshotId) {
|
|
const projectMap = this.snapshotProjectPaths.get(snapshotId);
|
|
if (!projectMap)
|
|
return;
|
|
for (const [projectId, paths] of projectMap) {
|
|
const key = refKey(snapshotId, projectId);
|
|
for (const path of paths) {
|
|
const entries = this.cache.get(path);
|
|
if (!entries)
|
|
continue;
|
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
entries[i].refs.delete(key);
|
|
if (entries[i].refs.size === 0) {
|
|
entries.splice(i, 1);
|
|
}
|
|
}
|
|
if (entries.length === 0) {
|
|
this.cache.delete(path);
|
|
}
|
|
}
|
|
}
|
|
this.snapshotProjectPaths.delete(snapshotId);
|
|
}
|
|
trackPath(snapshotId, projectId, path) {
|
|
let projectMap = this.snapshotProjectPaths.get(snapshotId);
|
|
if (!projectMap) {
|
|
projectMap = new Map();
|
|
this.snapshotProjectPaths.set(snapshotId, projectMap);
|
|
}
|
|
let paths = projectMap.get(projectId);
|
|
if (!paths) {
|
|
paths = new Set();
|
|
projectMap.set(projectId, paths);
|
|
}
|
|
paths.add(path);
|
|
}
|
|
/**
|
|
* Clear all entries from the cache.
|
|
*/
|
|
clear() {
|
|
this.cache.clear();
|
|
this.snapshotProjectPaths.clear();
|
|
}
|
|
/**
|
|
* Get the number of unique paths in the cache.
|
|
*/
|
|
get size() {
|
|
return this.cache.size;
|
|
}
|
|
/**
|
|
* Check if a path is in the cache.
|
|
*/
|
|
has(path) {
|
|
return this.cache.has(path);
|
|
}
|
|
}
|
|
//# sourceMappingURL=sourceFileCache.js.map |