LCOV - code coverage report
Current view: top level - dashql-app/src/storage - query_result_cache_eviction.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 20 20
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : /// The eviction policy of the query result cache.
       2              : ///
       3              : /// The cache of the app stores query results as Parquet files in the folder `cache/` of the origin private file
       4              : /// system (see `opfs_query_result_cache.ts`). Listing and deleting the files live behind `QueryResultCacheStore`, the
       5              : /// policy below is shared and unit-testable.
       6              : ///
       7              : /// Eviction is a true LRU keyed on `lastAccessMs`. The origin private file system exposes no last-access time, and
       8              : /// re-touching a file on a hit would rewrite a potentially large file and clobber its write time. So each file has an
       9              : /// empty sibling marker (`<file>.last_access`) whose own mtime is bumped on every hit; the store reports that marker's
      10              : /// mtime as `lastAccessMs`. The marker is advisory, a file without a marker falls back to its write time.
      11              : ///
      12              : /// The cache is best-effort, not transactional: under concurrent writes the size/count totals are approximate, a
      13              : /// delete of an already-gone file is tolerated, and a file that is read is kept (see `deleteCacheFile`).
      14              : 
      15              : /// Size/recency metadata for a single cached file.
      16              : export interface CacheFileStat {
      17              :     /// The file name, unique within the cache folder.
      18              :     name: string;
      19              :     /// The file size in bytes.
      20              :     size: number;
      21              :     /// The last-modified (write) time in milliseconds since the epoch. Doubles as the "cached at"
      22              :     /// timestamp surfaced in the UI (a cache hit never re-touches the payload).
      23              :     mtimeMs: number;
      24              :     /// The last-*access* time in milliseconds since the epoch: the mtime of the entry's
      25              :     /// `<file>.last_access` marker, bumped on every cache hit. Eviction drops the
      26              :     /// least-recently-accessed entries first. When no marker exists the backend falls back to
      27              :     /// `mtimeMs` here, so an un-accessed entry evicts by its write time.
      28              :     lastAccessMs: number;
      29              : }
      30              : 
      31              : /// The primitives of the cache folder that the eviction policy operates over.
      32              : export interface QueryResultCacheStore {
      33              :     /// List every file of the cache folder with its size and times. Returns an empty array when the folder does not
      34              :     /// exist yet.
      35              :     listCacheFiles(): Promise<CacheFileStat[]>;
      36              :     /// Delete a single cache file by name. Must tolerate a missing file (treat NotFound as success). Resolves false if
      37              :     /// the file is kept because it is read.
      38              :     deleteCacheFile(name: string): Promise<boolean | void>;
      39              : }
      40              : 
      41              : /// Default cap on the total size of the query result cache of the app.
      42            1 : export const DEFAULT_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
      43              : /// Default cap on the number of cached query results of the app.
      44            1 : export const DEFAULT_CACHE_MAX_FILES = 1000;
      45              : 
      46              : /// Evict least-recently-accessed cache files until the cache is within both the size and count thresholds, e.g.
      47              : /// after a write.
      48              : ///
      49              : /// Age is last-access time (`lastAccessMs`, the marker's mtime — see the module header): the
      50              : /// least-recently-*used* entries are dropped first, so this is a true LRU. Files are deleted
      51              : /// oldest-access-first until `totalBytes <= maxBytes` and `count <= maxFiles`. A file that is read stays, the next
      52              : /// one goes instead, so a single file larger than `maxBytes` empties the rest of the cache while it is read.
      53            1 : export async function evictToFit(
      54            1 :     store: QueryResultCacheStore,
      55            1 :     maxBytes: number = DEFAULT_CACHE_MAX_BYTES,
      56            1 :     maxFiles: number = DEFAULT_CACHE_MAX_FILES,
      57            1 : ): Promise<void> {
      58            1 :     const files = await store.listCacheFiles();
      59            1 :     let totalBytes = files.reduce((total, f) => total + f.size, 0);
      60            1 :     let count = files.length;
      61            1 :     const fits = () => totalBytes <= maxBytes && count <= maxFiles;
      62            1 :     if (fits()) return;
      63              : 
      64              :     // Least-recently-accessed first (ascending lastAccessMs) so we drop the coldest entries.
      65            1 :     const byAge = [...files].sort((a, b) => a.lastAccessMs - b.lastAccessMs);
      66            1 :     for (const f of byAge) {
      67            1 :         if (fits()) break;
      68              :         // A file that is read stays, the next one goes instead
      69            1 :         if (await store.deleteCacheFile(f.name) === false) continue;
      70            1 :         totalBytes -= f.size;
      71            1 :         count -= 1;
      72            1 :     }
      73            1 : }
        

Generated by: LCOV version 2.0-1