diff --git a/Build/lib/cache-filesystem.ts b/Build/lib/cache-filesystem.ts index d90143dc..f6658b71 100644 --- a/Build/lib/cache-filesystem.ts +++ b/Build/lib/cache-filesystem.ts @@ -16,12 +16,6 @@ import { Custom304NotModifiedError, CustomAbortError, CustomNoETagFallbackError, import type { IncomingHttpHeaders } from 'undici/types/header'; import { Headers } from 'undici'; -const enum CacheStatus { - Hit = 'hit', - Stale = 'stale', - Miss = 'miss' -} - export interface CacheOptions { /** Path to sqlite file dir */ cachePath?: string, @@ -34,6 +28,7 @@ export interface CacheOptions { interface CacheApplyRawOption { ttl?: number | null, + cacheName?: string, temporaryBypass?: boolean, incrementTtlWhenHit?: boolean } @@ -43,7 +38,7 @@ interface CacheApplyNonRawOption extends CacheApplyRawOption { deserializer: (cached: S) => T } -type CacheApplyOption = T extends S ? CacheApplyRawOption : CacheApplyNonRawOption; +export type CacheApplyOption = T extends S ? CacheApplyRawOption : CacheApplyNonRawOption; const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; @@ -164,23 +159,27 @@ export class Cache { }); } - get(key: string, defaultValue?: S): S | undefined { - const rv = this.db.prepare( - `SELECT value FROM ${this.tableName} WHERE key = ? LIMIT 1` + get(key: string): S | null { + const rv = this.db.prepare( + `SELECT ttl, value FROM ${this.tableName} WHERE key = ? LIMIT 1` ).get(key); - if (!rv) return defaultValue; + if (!rv) return null; + + if (rv.ttl < Date.now()) { + this.del(key); + return null; + } + + if (rv.value == null) { + this.del(key); + return null; + } + return rv.value; } - has(key: string): CacheStatus { - const now = Date.now(); - const rv = this.db.prepare(`SELECT ttl FROM ${this.tableName} WHERE key = ?`).get(key); - - return rv ? (rv.ttl > now ? CacheStatus.Hit : CacheStatus.Stale) : CacheStatus.Miss; - } - - private updateTtl(key: string, ttl: number): void { + updateTtl(key: string, ttl: number): void { this.db.prepare(`UPDATE ${this.tableName} SET ttl = ? WHERE key = ?;`).run(Date.now() + ttl, key); } @@ -193,7 +192,7 @@ export class Cache { fn: () => Promise, opt: CacheApplyOption ): Promise { - const { ttl, temporaryBypass, incrementTtlWhenHit } = opt; + const { ttl, temporaryBypass, incrementTtlWhenHit, cacheName } = opt; if (temporaryBypass) { return fn(); @@ -205,7 +204,7 @@ export class Cache { const cached = this.get(key); if (cached == null) { - console.log(picocolors.yellow('[cache] miss'), picocolors.gray(key), picocolors.gray(`ttl: ${TTL.humanReadable(ttl)}`)); + console.log(picocolors.yellow('[cache] miss'), picocolors.gray(cacheName || key), picocolors.gray(`ttl: ${TTL.humanReadable(ttl)}`)); const serializer = 'serializer' in opt ? opt.serializer : identity as any; @@ -217,7 +216,7 @@ export class Cache { }); } - console.log(picocolors.green('[cache] hit'), picocolors.gray(key)); + console.log(picocolors.green('[cache] hit'), picocolors.gray(cacheName || key)); if (incrementTtlWhenHit) { this.updateTtl(key, ttl); diff --git a/Build/lib/fs-memo.ts b/Build/lib/fs-memo.ts new file mode 100644 index 00000000..4d497263 --- /dev/null +++ b/Build/lib/fs-memo.ts @@ -0,0 +1,51 @@ +import path from 'node:path'; +import { Cache } from './cache-filesystem'; +import type { CacheApplyOption } from './cache-filesystem'; +import { isCI } from 'ci-info'; + +const fsMemoCache = new Cache({ cachePath: path.resolve(__dirname, '../../.cache') }); + +const TTL = isCI + // We run CI daily, so 1.5 days TTL is enough to persist the cache across runs + ? 1.5 * 86400 * 1000 + // We run locally less frequently, so we need to persist the cache for longer, 7 days + : 7 * 86400 * 1000; + + type JSONValue = + | string + | number + | boolean + | null + | JSONObject + | JSONArray; + +interface JSONObject { + [key: string]: JSONValue +} + +interface JSONArray extends Array {} + +export function cache( + cb: (...args: Args) => Promise, + opt: Omit, 'ttl'> +): (...args: Args) => Promise { + // TODO if cb.toString() is long we should hash it + const fixedKey = cb.toString(); + + return async function cachedCb(...args: Args) { + // Construct the complete cache key for this function invocation + // TODO stringify is limited. For now we uses typescript to guard the args. + const cacheKey = `${fixedKey}|${JSON.stringify(args)}`; + const cacheName = cb.name || cacheKey; + + return fsMemoCache.apply( + cacheKey, + cb, + { + cacheName, + ...opt, + ttl: TTL + } as CacheApplyOption + ); + }; +}