perfect-postcode/screenshot/src/cache.ts

74 lines
2.1 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 are rounded to 2 decimals and zoom to an integer so nearby views
* share a cache entry; all other params are sorted for order-independence.
*/
buildKey(params: URLSearchParams): string {
const normalized: Record<string, string> = {};
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();
}
const quantized = new Set(['lat', 'lon', 'zoom']);
const keys = [...new Set(params.keys())].filter((k) => !quantized.has(k)).sort();
for (const key of keys) {
normalized[key] = params.getAll(key).sort().join(',');
}
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);
}
}