import { createHash } from 'crypto'; import { existsSync, mkdirSync, statSync, createReadStream, writeFileSync } from 'fs'; import { join } from 'path'; import type { Readable } from 'stream'; const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours export class ScreenshotCache { private dir: string; constructor(dir: string) { this.dir = dir; if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } /** * Build a cache key by quantizing view params and hashing. * - lat/lon quantized to 2 decimal places * - zoom quantized to integer * - filters and POI categories sorted alphabetically */ buildKey(params: URLSearchParams): string { const normalized: Record = {}; // Quantize lat/lon/zoom const lat = params.get('lat'); const lon = params.get('lon'); const zoom = params.get('zoom'); if (lat && lon && zoom) { normalized.lat = parseFloat(lat).toFixed(2); normalized.lon = parseFloat(lon).toFixed(2); normalized.zoom = Math.round(parseFloat(zoom)).toString(); } // Sort filters const filters = params.getAll('filter').sort(); if (filters.length > 0) { normalized.filters = filters.join(','); } // Sort POI categories const pois = params.getAll('poi').sort(); if (pois.length > 0) { normalized.poi = pois.join(','); } if (params.get('tab')) { normalized.tab = params.get('tab')!; } if (params.get('og')) { normalized.og = params.get('og')!; } if (params.get('path')) { normalized.path = params.get('path')!; } const input = JSON.stringify(normalized); const hash = createHash('sha256').update(input).digest('hex').substring(0, 16); return hash; } getPath(key: string): string { return join(this.dir, `${key}.jpg`); } /** * Returns a readable stream if a fresh cached file exists, null otherwise. */ get(key: string): Readable | null { const path = this.getPath(key); if (!existsSync(path)) return null; try { const stat = statSync(path); const age = Date.now() - stat.mtimeMs; if (age > MAX_AGE_MS) return null; return createReadStream(path); } catch { return null; } } /** * Write screenshot bytes to cache. */ set(key: string, data: Buffer): void { const path = this.getPath(key); writeFileSync(path, data); } }