From f86548e94260998e951fae05f72ff3f5eca991ca Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 4 Jan 2026 09:17:23 +0100 Subject: [PATCH] refactor: standalone re-usable cache util --- app/server/modules/system/system.service.ts | 21 ++--- app/server/utils/cache.ts | 96 +++++++++++++++++++++ 2 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 app/server/utils/cache.ts diff --git a/app/server/modules/system/system.service.ts b/app/server/modules/system/system.service.ts index f7316fbb..9e592aa7 100644 --- a/app/server/modules/system/system.service.ts +++ b/app/server/modules/system/system.service.ts @@ -2,13 +2,9 @@ import { getCapabilities } from "../../core/capabilities"; import { config } from "../../core/config"; import type { UpdateInfoDto } from "./system.dto"; import semver from "semver"; +import { cache } from "../../utils/cache"; -let updateCache: { - data: UpdateInfoDto; - timestamp: number; -} | null = null; - -const CACHE_TTL = 60 * 60 * 1000; // 1 hour +const CACHE_TTL = 60 * 60; const getSystemInfo = async () => { return { @@ -24,9 +20,11 @@ interface GitHubRelease { } const getUpdates = async (): Promise => { - const now = Date.now(); - if (updateCache && now - updateCache.timestamp < CACHE_TTL) { - return updateCache.data; + const CACHE_KEY = `system:updates:${config.appVersion}`; + + const cached = cache.get(CACHE_KEY); + if (cached) { + return cached; } try { @@ -72,10 +70,7 @@ const getUpdates = async (): Promise => { missedReleases, }; - updateCache = { - data, - timestamp: now, - }; + cache.set(CACHE_KEY, data, CACHE_TTL); return data; } catch (error) { diff --git a/app/server/utils/cache.ts b/app/server/utils/cache.ts new file mode 100644 index 00000000..77c7e48e --- /dev/null +++ b/app/server/utils/cache.ts @@ -0,0 +1,96 @@ +import { Database } from "bun:sqlite"; +import path from "node:path"; +import fs from "node:fs"; +import { DATABASE_URL } from "../core/constants"; + +export const ONE_DAY_IN_SECONDS = 60 * 60 * 24; + +export interface CacheOptions { + dbPath?: string; +} + +export const createCache = (options: CacheOptions = {}) => { + const defaultPath = path.join(path.dirname(DATABASE_URL), "cache.db"); + const dbPath = options.dbPath || defaultPath; + + const dir = path.dirname(dbPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const db = new Database(dbPath); + + db.run("CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, expiration INTEGER)"); + + const set = (key: string, value: unknown, expirationSeconds = ONE_DAY_IN_SECONDS) => { + const expiration = Date.now() + expirationSeconds * 1000; + const stmt = db.prepare("INSERT OR REPLACE INTO cache (key, value, expiration) VALUES (?, ?, ?)"); + stmt.run(key, JSON.stringify(value), expiration); + }; + + const get = (key: string): T | undefined => { + const stmt = db.prepare("SELECT value, expiration FROM cache WHERE key = ?"); + const row = stmt.get(key) as { value: string; expiration: number } | undefined; + + if (!row) { + return undefined; + } + + if (row.expiration < Date.now()) { + const delStmt = db.prepare("DELETE FROM cache WHERE key = ?"); + delStmt.run(key); + return undefined; + } + + try { + return JSON.parse(row.value) as T; + } catch { + return undefined; + } + }; + + const del = (key: string) => { + const stmt = db.prepare("DELETE FROM cache WHERE key = ?"); + stmt.run(key); + }; + + const getByPrefix = (prefix: string): { key: string; value: T }[] => { + const stmt = db.prepare("SELECT key, value, expiration FROM cache WHERE key LIKE ?"); + const rows = stmt.all(`${prefix}%`) as { key: string; value: string; expiration: number }[]; + + const now = Date.now(); + const results: { key: string; value: T }[] = []; + + for (const row of rows) { + if (row.expiration < now) { + const delStmt = db.prepare("DELETE FROM cache WHERE key = ?"); + delStmt.run(row.key); + continue; + } + try { + results.push({ + key: row.key, + value: JSON.parse(row.value) as T, + }); + } catch { + // Ignore malformed entries + } + } + + return results; + }; + + const clear = () => { + db.run("DELETE FROM cache"); + }; + + return { + set, + get, + del, + getByPrefix, + clear, + }; +}; + +export const cache = createCache();