mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
- One place-name key for server and tiles, so "St. Paul" and "Saint Paul"
agree in Nominatim name checks.
- Tile street names: a lone letter is a direction only beside another name
word ("E Street" is not "East Street"), and a trailing quadrant ("E St NW")
may go unsaid in the ask.
- A building cut at the tile edge that is nearer the point than any whole
one leaves the point instead of a farther neighbour standing in; an edge
without a tile buffer counts as a cut, so a cut area never answers grounds.
- A batch building install re-judges only results that installed; a late
timeout during caption conversion keeps the pending point drawn; a failed
plain recaption keeps the old mark addressable.
- An interrupted Nominatim dispatch is reclaimed by the next lock owner ten
minutes after the start its marker names; node:sqlite loads on first use
without its experimental warning.
- PHOTON_URL accepts only https, which the page's connect-src allows.
- Shared OpenFreeMap tiles decode each layer when a consumer first selects
it; outline selection reuses the shared ring area and simplification, and
ground probes the shared ring centroid; the grounds word list lives once.
175 lines
4.9 KiB
JavaScript
175 lines
4.9 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { normalizePlaceName as foldName } from '../../../src/data/placeNames.js';
|
|
|
|
const DAY_MS = 86_400_000;
|
|
|
|
/** Entry files this cache writes: a 40-hex-digit key hash. */
|
|
const CACHE_FILE_RE = /^[0-9a-f]{40}\.json$/;
|
|
|
|
/** How long an answer is kept, by what it was. */
|
|
export const NOMINATIM_CACHE_TTL_MS = Object.freeze({
|
|
outline: 90 * DAY_MS,
|
|
place: 30 * DAY_MS,
|
|
notFound: DAY_MS,
|
|
});
|
|
|
|
/**
|
|
* Comparable form of a place name for matching results to the ask: the
|
|
* shared place-name key (see `normalizePlaceName` in placeNames.js, so
|
|
* "St. Paul" and "Saint Paul" agree), bounded in length.
|
|
*
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
export function normalizePlaceName(value) {
|
|
return foldName(value).slice(0, 200);
|
|
}
|
|
|
|
/**
|
|
* Memory + disk cache for Nominatim answers.
|
|
*
|
|
* Every entry carries its own expiry. Memory is an LRU bounded by entries;
|
|
* disk is bounded by entries and bytes, with expired files removed during a
|
|
* periodic sweep. A null `dir` keeps everything in memory.
|
|
*/
|
|
export function createNominatimCache({
|
|
dir = null,
|
|
maxMemoryEntries = 256,
|
|
maxDiskEntries = 4000,
|
|
maxDiskBytes = 64 * 1024 * 1024,
|
|
maxEntryBytes = 2 * 1024 * 1024,
|
|
sweepEvery = 50,
|
|
now = Date.now,
|
|
} = {}) {
|
|
const memory = new Map();
|
|
let writesSinceSweep = 0;
|
|
let sweeping = null;
|
|
|
|
const digestFor = (key) =>
|
|
crypto.createHash('sha256').update(key).digest('hex');
|
|
|
|
const fileFor = (key) =>
|
|
path.join(dir, `${digestFor(key).slice(0, 40)}.json`);
|
|
|
|
function remember(key, entry) {
|
|
memory.delete(key);
|
|
memory.set(key, entry);
|
|
while (memory.size > maxMemoryEntries)
|
|
memory.delete(memory.keys().next().value);
|
|
}
|
|
|
|
async function get(key) {
|
|
const hit = memory.get(key);
|
|
if (hit) {
|
|
if (hit.expiresAt > now()) {
|
|
remember(key, hit);
|
|
return hit;
|
|
}
|
|
memory.delete(key);
|
|
}
|
|
if (!dir) return null;
|
|
try {
|
|
const entry = JSON.parse(await fs.readFile(fileFor(key), 'utf8'));
|
|
if (entry?.keyDigest !== digestFor(key) || !(entry.expiresAt > now()))
|
|
return null;
|
|
remember(key, entry);
|
|
return entry;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function set(key, payload, ttlMs) {
|
|
const entry = {
|
|
keyDigest: digestFor(key),
|
|
payload,
|
|
storedAt: now(),
|
|
expiresAt: now() + ttlMs,
|
|
};
|
|
remember(key, entry);
|
|
if (!dir) return;
|
|
const body = JSON.stringify(entry);
|
|
if (Buffer.byteLength(body) > maxEntryBytes) return;
|
|
let temp = null;
|
|
try {
|
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
await fs.chmod(dir, 0o700);
|
|
const file = fileFor(key);
|
|
temp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
await fs.writeFile(temp, body, { mode: 0o600, flag: 'wx' });
|
|
await fs.rename(temp, file);
|
|
temp = null;
|
|
} catch {
|
|
return;
|
|
} finally {
|
|
if (temp) await fs.rm(temp, { force: true }).catch(() => {});
|
|
}
|
|
writesSinceSweep += 1;
|
|
if (writesSinceSweep >= sweepEvery) {
|
|
writesSinceSweep = 0;
|
|
await sweep();
|
|
}
|
|
}
|
|
|
|
/** Remove expired files, then the oldest until both disk bounds hold. */
|
|
async function sweep() {
|
|
if (!dir) return;
|
|
if (sweeping) return sweeping;
|
|
sweeping = (async () => {
|
|
let names;
|
|
try {
|
|
names = await fs.readdir(dir);
|
|
} catch {
|
|
return;
|
|
}
|
|
const files = [];
|
|
for (const name of names) {
|
|
// Only this cache's own entry files; anything else in the directory
|
|
// is left alone.
|
|
if (!CACHE_FILE_RE.test(name)) continue;
|
|
const file = path.join(dir, name);
|
|
let stat;
|
|
let text;
|
|
try {
|
|
[stat, text] = await Promise.all([
|
|
fs.stat(file),
|
|
fs.readFile(file, 'utf8'),
|
|
]);
|
|
} catch {
|
|
// Unreadable right now (gone, busy, too many open files): an error
|
|
// reading is not evidence the entry is bad, so it stays.
|
|
continue;
|
|
}
|
|
let expiresAt;
|
|
try {
|
|
expiresAt = JSON.parse(text)?.expiresAt;
|
|
} catch {
|
|
expiresAt = null; // corrupt: removed below
|
|
}
|
|
if (!(expiresAt > now())) {
|
|
await fs.rm(file, { force: true }).catch(() => {});
|
|
continue;
|
|
}
|
|
files.push({ file, size: stat.size, at: stat.mtimeMs });
|
|
}
|
|
files.sort((a, b) => a.at - b.at);
|
|
let bytes = files.reduce((sum, entry) => sum + entry.size, 0);
|
|
while (
|
|
files.length &&
|
|
(files.length > maxDiskEntries || bytes > maxDiskBytes)
|
|
) {
|
|
const oldest = files.shift();
|
|
bytes -= oldest.size;
|
|
await fs.rm(oldest.file, { force: true }).catch(() => {});
|
|
}
|
|
})().finally(() => {
|
|
sweeping = null;
|
|
});
|
|
return sweeping;
|
|
}
|
|
|
|
return { get, set, sweep };
|
|
}
|