Bound the originals cache with adaptive LRU eviction (closes #47)
check / check (push) Successful in 35s

Bounds cacheDirectory/originals to an adaptive limit: min(configured max,
bytesUsed + bytesFree - reserve), bytesFree from statfs, so it tracks disk
pressure (status().originalsLimitBytes exposes it). Each original write evicts
least-recently-used originals until usage fits; last-use is the file mtime,
bumped on every read that returns a path, so order survives restarts.

Never evicted: pinned originals and every original whose write overlaps the
evicting one — its own and any concurrent sibling's, since content writes run in
parallel. So an over-budget fetch keeps the path it returns, and no write can
delete a sibling's file before that fetch returns it; such files become eligible
on a later, non-overlapping write. Defaults: 100 GiB limit, 50 GiB reserve.

Model: opus-4-8
This commit is contained in:
2026-09-22 17:57:20 +00:00
parent 5db59a6e2b
commit e7f77b4b27
3 changed files with 619 additions and 12 deletions
+18
View File
@@ -132,6 +132,14 @@ export interface LibraryOptions {
// Overrides the client's own `contentSource()`; mainly for tests that drive
// the cache with a stand-in source.
contentSource?: ContentSource;
// Bound on `cacheDirectory/originals` (default 100 GiB) and the free space
// to protect on its volume (default 50 GiB). The effective limit adapts
// down as the disk fills; `status().originalsLimitBytes` reports it.
cacheOriginalsMaxBytes?: number;
freeBelowBytes?: number;
// Whether an original is pinned and so never evicted (favorites + latest
// week; the precache unit #48 supplies the set).
isOriginalPinned?: (fileID: number) => boolean;
}
export interface LibraryStatus {
@@ -153,6 +161,10 @@ export interface LibraryStatus {
// when ML fetching is disabled.
mlStored?: number;
mlIndexed?: number;
// Bytes stored in the originals cache and the effective size limit as of the
// last write or open; undefined when no content cache is open.
originalsUsedBytes?: number;
originalsLimitBytes?: number;
closed: boolean;
}
@@ -289,6 +301,9 @@ export class Library {
cacheDirectory,
downloadDirectory: opts.downloadDirectory,
getFile: (fileID) => store.getFileByID(fileID),
cacheOriginalsMaxBytes: opts.cacheOriginalsMaxBytes,
freeBelowBytes: opts.freeBelowBytes,
isPinned: opts.isOriginalPinned,
});
await cache.open();
}
@@ -364,6 +379,7 @@ export class Library {
files += this.store.listFiles(c.id).length;
}
const ml = this.mldata?.stats();
const originals = this.cache?.originalsStatus();
return {
userID: this.store.userID,
collections: collections.length,
@@ -374,6 +390,8 @@ export class Library {
lastMLError: this.lastMLError,
mlStored: ml?.stored,
mlIndexed: ml?.indexed,
originalsUsedBytes: originals?.usedBytes,
originalsLimitBytes: originals?.limitBytes,
closed: this.closed,
};
}