Initial SvelteKit frontend port of windwiderstand.de
Full parity with Astro site: content rows, post/tag routes, pagination, event badges + OSM map, comments, Live-Search via /api/search-index, CMS image proxy, RSS, sitemap. Deploy: Dockerfile + docker-compose.prod.yml + Gitea Actions workflow to build + push to git.pm86.de registry and ssh-deploy to Contabo.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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<string, CachedImage>();
|
||||
|
||||
/** 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<string | null> {
|
||||
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<CachedImage | null> {
|
||||
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<void> {
|
||||
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<CachedImage | null> {
|
||||
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<void> {
|
||||
const key = cacheKey(params);
|
||||
const value: CachedImage = { buffer, contentType };
|
||||
if (memoryCache.size >= MAX_ENTRIES) evictOldest();
|
||||
memoryCache.set(key, value);
|
||||
await setInFile(key, value);
|
||||
}
|
||||
Reference in New Issue
Block a user