From ab3c8982e2c33e5279bf1b167f523696078629e6 Mon Sep 17 00:00:00 2001 From: u80864958 Date: Fri, 23 Jan 2026 14:32:51 +0100 Subject: [PATCH] feat: lru cache --- package.json | 1 + pnpm-lock.yaml | 9 +++++ src/lib/server/cache.ts | 50 +++++++++++++++++++++++--- src/routes/assets/[...path]/+server.ts | 8 +++-- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 865c971..432f744 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@inlang/paraglide-js": "^1.11.0", "@inlang/paraglide-sveltekit": "^0.16.1", "@ts-stack/markdown": "^1.5.0", + "lru-cache": "^11.2.4", "mime-types": "^3.0.2", "svelte-preprocess": "^6.0.2", "webdav": "^5.8.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a30d4f9..2742813 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@ts-stack/markdown': specifier: ^1.5.0 version: 1.5.0 + lru-cache: + specifier: ^11.2.4 + version: 11.2.4 mime-types: specifier: ^3.0.2 version: 3.0.2 @@ -1269,6 +1272,10 @@ packages: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -2988,6 +2995,8 @@ snapshots: lru-cache@10.2.2: {} + lru-cache@11.2.4: {} + magic-string@0.30.10: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 diff --git a/src/lib/server/cache.ts b/src/lib/server/cache.ts index ec4c41d..11c4b69 100644 --- a/src/lib/server/cache.ts +++ b/src/lib/server/cache.ts @@ -1,5 +1,44 @@ -const cache = new Map(); +import { LRUCache } from "lru-cache"; + +// const cache = new Map(); const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL +const MAX_CACHE_SIZE = 1000 * 1000 * 750; // 750 MB + +const cache = new LRUCache({ + max: 100, + // Maximum allowed size (in bytes) + maxSize: MAX_CACHE_SIZE, + + // IMPORTANT: We must teach the cache how to calculate the size of an item. + // We check for your specific image structure, generic buffers, or fallback to JSON length. + sizeCalculation: (value) => { + // 1. Check if it's our specific image object { buffer: ArrayBuffer, ... } + if (value && typeof value === "object") { + const obj = value as Record; + if (obj.buffer instanceof ArrayBuffer) { + return obj.buffer.byteLength; + } + } + + // 2. Handle raw ArrayBuffers or Node.js Buffers + if (value instanceof ArrayBuffer) { + return value.byteLength; + } + + if (Buffer.isBuffer(value)) { + return value.length; + } + + // 3. Fallback for strings or JSON metadata + try { + const str = JSON.stringify(value); + return str ? str.length : 1; + } catch { + return 1; + } + }, + ttl: DEFAULT_TTL, +}); /** * Caches the result of a promise-returning function. @@ -12,16 +51,17 @@ export async function withCache( fetcher: () => Promise, ttl = DEFAULT_TTL, ): Promise { - const now = Date.now(); const entry = cache.get(key); - if (entry && now - entry.timestamp < ttl) { - return entry.data as T; + if (entry) { + return entry as T; } try { const data = await fetcher(); - cache.set(key, { timestamp: now, data }); + if (data) { + cache.set(key, data, { ttl: ttl }); + } return data; } catch (error) { console.error(`Error fetching data for key ${key}:`, error); diff --git a/src/routes/assets/[...path]/+server.ts b/src/routes/assets/[...path]/+server.ts index 446c199..c58be55 100644 --- a/src/routes/assets/[...path]/+server.ts +++ b/src/routes/assets/[...path]/+server.ts @@ -6,6 +6,9 @@ import mime from "mime-types"; import { env } from "$env/dynamic/private"; import { withCache } from "$lib/server/cache"; +const DEFAULT_TTL = 1000 * 60 * 15; // 15min +const EXTENDED_TTL = 1000 * 60 * 60 * 24 * 7; // 1 week + // Define what we are storing in the cache type CachedAsset = { buffer: ArrayBuffer; @@ -24,6 +27,8 @@ export const GET: RequestHandler = async ({ params, url }) => { // 1. Create a unique cache key for this specific asset variation const cacheKey = `asset:${path}?w=${width ?? ""}&h=${height ?? ""}`; + const ttl = path.toString().includes("cover") ? DEFAULT_TTL : EXTENDED_TTL; + try { // 2. Use withCache to wrap the fetching logic const cachedData = await withCache( @@ -113,8 +118,7 @@ export const GET: RequestHandler = async ({ params, url }) => { throw lastError || new Error("Asset not found"); // --- END OF FETCH LOGIC --- }, - // Optional: Set a specific TTL for images (e.g., 1 hour instead of default 15m) - 1000 * 60 * 60, + ttl, ); // 3. Construct a fresh Response using the cached data