Originals cache size limit with least-recently-used eviction (closes #47)
check / check (push) Successful in 25s

Bounds the originals cache and evicts least-recently-used. cacheOriginalsMaxBytes default 100 GiB; the effective limit adapts down via fs.statfs to keep freeBelowBytes (default 50 GiB) free. Over-limit writes evict unpinned originals oldest-mtime-first (mtime touched on read); pinned files (favorites + latest week) are skipped and an over-budget on-demand fetch proceeds over-limit. Every in-flight write is excluded from eviction, so concurrent fetches never delete each other's just-stored file. Only cacheDirectory/originals is evicted; downloadDirectory and thumbnails never.

Model: opus-4-8
This commit was merged in pull request #71.
This commit is contained in:
2026-09-22 20:13:22 +02:00
parent 5db59a6e2b
commit c05d63a2f0
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,
};
}