perfect-postcode/screenshot/src/cache.ts
2026-03-15 21:15:26 +00:00

94 lines
2.4 KiB
TypeScript

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<string, string> = {};
// 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);
}
}