/** * Server-seitiger Cache für /api/transform (Bilder vom CMS). * Reduziert Aufrufe zum CMS; gleiche Parameter = gleiche Antwort (immutable). * * - In-Memory: LRU-artig, begrenzt auf TRANSFORM_CACHE_MAX_ENTRIES (Default 100). * - Optional Datei-Cache: TRANSFORM_CACHE_DIR setzen (z. B. .cache/transform). */ const MAX_ENTRIES = Number(process.env.TRANSFORM_CACHE_MAX_ENTRIES) || 100; const CACHE_DIR = process.env.TRANSFORM_CACHE_DIR?.trim() || ''; interface CachedImage { buffer: ArrayBuffer; contentType: string; } const memoryCache = new Map(); /** Cache-Key aus Query-String (eindeutig pro URL + Parameter). */ function cacheKey(params: string): string { return params; } /** In-Memory: ältesten Eintrag entfernen (Map behält Einfüge-Reihenfolge). */ function evictOldest(): void { const first = memoryCache.keys().next(); if (first.value !== undefined) memoryCache.delete(first.value); } /** Prüft, ob Datei-Cache verfügbar ist (nur Server mit fs). */ async function getCacheDir(): Promise { if (!CACHE_DIR) return null; try { const { existsSync, mkdirSync } = await import('node:fs'); const { join } = await import('node:path'); const dir = join(process.cwd(), CACHE_DIR); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); return dir; } catch { return null; } } /** Sicheren Dateinamen aus Key erzeugen (Hash). */ function fileKey(key: string): string { let h = 0; for (let i = 0; i < key.length; i++) h = (Math.imul(31, h) + key.charCodeAt(i)) | 0; return Math.abs(h).toString(36) + key.length.toString(36); } /** Aus Datei-Cache lesen. */ async function getFromFile(key: string): Promise { const dir = await getCacheDir(); if (!dir) return null; try { const { readFile } = await import('node:fs/promises'); const { join } = await import('node:path'); const base = fileKey(key); const metaPath = join(dir, `${base}.meta`); const dataPath = join(dir, `${base}.bin`); const [ct, buf] = await Promise.all([ readFile(metaPath, 'utf8').catch(() => null), readFile(dataPath).catch(() => null), ]); if (ct && buf) return { buffer: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength), contentType: ct, }; } catch { // ignore } return null; } /** In Datei-Cache schreiben. */ async function setInFile(key: string, value: CachedImage): Promise { const dir = await getCacheDir(); if (!dir) return; try { const { writeFile } = await import('node:fs/promises'); const { join } = await import('node:path'); const base = fileKey(key); await Promise.all([ writeFile(join(dir, `${base}.meta`), value.contentType), writeFile(join(dir, `${base}.bin`), Buffer.from(value.buffer)), ]); } catch { // ignore } } /** * Transform-Ergebnis aus Cache holen (Memory zuerst, dann Datei). */ export async function getCachedTransform(params: string): Promise { const key = cacheKey(params); const mem = memoryCache.get(key); if (mem) return mem; const file = await getFromFile(key); if (file) { if (memoryCache.size >= MAX_ENTRIES) evictOldest(); memoryCache.set(key, file); return file; } return null; } /** * Transform-Ergebnis cachen (Memory + optional Datei). */ export async function setCachedTransform( params: string, buffer: ArrayBuffer, contentType: string, ): Promise { const key = cacheKey(params); const value: CachedImage = { buffer, contentType }; if (memoryCache.size >= MAX_ENTRIES) evictOldest(); memoryCache.set(key, value); await setInFile(key, value); }