mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Both disk caches fed by OVERPASS_UPSTREAMS key only on the query — the Overpass proxy on the sanitized request body, the installations proxy on the bbox — so an entry written against one instance is a hit for the same query against another. After an operator repoints OVERPASS_UPSTREAMS, the previous upstream's answers keep being served for 7 days, 30 for boundary-class queries, and indefinitely through the serve-stale path. An install that predates #742 has a cache written from the public instances that were the default then, and configuring your own instance does not displace it. Cached answers now carry a tag identifying the configured upstream set, and a disk read refuses an entry that does not match it, on the stale path too. With nothing configured any entry is still accepted: there is no instance to contradict, and serving last-good data at any age is what the not-configured path exists for. The tag is a hash of the resolved endpoint list, never the URLs; it is written to disk and compared on read, and is not sent to clients or surfaced in errors. One line is logged once per process when a foreign entry is ignored, naming the variable and what will happen, as local-receivers.js already does for a rejected feed. Cost worth naming: on upgrade every existing entry is untagged, so the first outage after upgrading serves nothing where it previously served stale data, until each query is fetched once from the configured instance.
144 lines
4.6 KiB
JavaScript
144 lines
4.6 KiB
JavaScript
import { isOverpassBoundaryQuery } from './query.js';
|
|
import {
|
|
OVERPASS_BOUNDARY_DISK_TTL_MS,
|
|
OVERPASS_DISK_TTL_MS,
|
|
OVERPASS_DISK_DIR,
|
|
OVERPASS_CACHE_MS,
|
|
OVERPASS_CACHE_MAX_ENTRIES,
|
|
cachedByConfiguredUpstream,
|
|
} from './constants.js';
|
|
import path from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { promises as fsp } from 'node:fs';
|
|
import { overpassPayloadIsData } from './transport.js';
|
|
|
|
/** @type {Map<string,{status:number,body:string,contentType:string,endpoint:string,cachedAt:number}>} */
|
|
const _overpassCache = new Map();
|
|
|
|
/** Disk TTL for a query: boundary geometry keeps for a month, the rest 7 days. */
|
|
function overpassDiskTtlMs(cacheKey) {
|
|
return isOverpassBoundaryQuery(cacheKey)
|
|
? OVERPASS_BOUNDARY_DISK_TTL_MS
|
|
: OVERPASS_DISK_TTL_MS;
|
|
}
|
|
|
|
/** Normalized Overpass query -> stable disk-cache file path. */
|
|
function overpassDiskPath(cacheKey) {
|
|
return path.join(
|
|
OVERPASS_DISK_DIR,
|
|
`${createHash('sha1').update(cacheKey).digest('hex')}.json`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Read a disk-cached Overpass payload. maxAgeMs Infinity = any age (the
|
|
* serve-stale path when every mirror is down).
|
|
* @returns {Promise<?Object>} Payload with cachedAt, or null.
|
|
*/
|
|
async function readOverpassDisk(cacheKey, maxAgeMs) {
|
|
try {
|
|
const raw = await fsp.readFile(overpassDiskPath(cacheKey), 'utf8');
|
|
const payload = JSON.parse(raw);
|
|
if (
|
|
!payload ||
|
|
typeof payload.body !== 'string' ||
|
|
!Number.isFinite(payload.cachedAt)
|
|
)
|
|
return null;
|
|
// Older versions persisted 4xx refusals with normal data TTLs. Ignore
|
|
// them on both fresh and stale reads so an upgrade can recover immediately.
|
|
if (!overpassPayloadIsData(payload)) return null;
|
|
// The key is the query alone, so a file written by a different upstream —
|
|
// including the public instances that were the default before #742 — is a
|
|
// hit for the same query. Refuse it: the operator's configured instance
|
|
// must be what answers, on the stale path too.
|
|
if (!cachedByConfiguredUpstream(payload)) return null;
|
|
if (Date.now() - payload.cachedAt > maxAgeMs) return null;
|
|
return payload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Fire-and-forget disk write for a successful Overpass payload. */
|
|
function writeOverpassDisk(cacheKey, payload) {
|
|
fsp
|
|
.mkdir(OVERPASS_DISK_DIR, { recursive: true })
|
|
.then(() =>
|
|
fsp.writeFile(overpassDiskPath(cacheKey), JSON.stringify(payload)),
|
|
)
|
|
.catch((err) =>
|
|
console.warn(
|
|
'[Overpass Proxy] disk cache write failed:',
|
|
err?.message || err,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve every cache/coalescing layer before admitting a request to the local
|
|
* upstream rate limiter. The injected limiter callback is invoked exactly once
|
|
* for a complete cache miss and never for memory, in-flight, or disk hits.
|
|
* Exported so the admission ordering can be tested without a Vite server.
|
|
*
|
|
* @param {object} options
|
|
* @param {string} options.cacheKey
|
|
* @param {Map<string, object>} options.memoryCache
|
|
* @param {Map<string, Promise<object>>} options.inFlight
|
|
* @param {()=>Promise<object|null>} options.readDisk
|
|
* @param {()=>boolean} options.allowUpstream
|
|
* @param {number} [options.now]
|
|
* @param {number} [options.cacheMs]
|
|
* @returns {Promise<{source:'HIT'|'INFLIGHT'|'DISK'|'UPSTREAM'|'RATE_LIMITED', payload:object|null}>}
|
|
*/
|
|
async function resolveOverpassPreflight({
|
|
cacheKey,
|
|
memoryCache,
|
|
inFlight,
|
|
readDisk,
|
|
allowUpstream,
|
|
now = Date.now(),
|
|
cacheMs = OVERPASS_CACHE_MS,
|
|
}) {
|
|
const cached = memoryCache.get(cacheKey);
|
|
if (overpassPayloadIsData(cached) && now - cached.cachedAt <= cacheMs)
|
|
return { source: 'HIT', payload: cached };
|
|
|
|
const pending = inFlight.get(cacheKey);
|
|
if (pending) return { source: 'INFLIGHT', payload: await pending };
|
|
|
|
const disk = await readDisk();
|
|
if (overpassPayloadIsData(disk)) return { source: 'DISK', payload: disk };
|
|
|
|
return allowUpstream()
|
|
? { source: 'UPSTREAM', payload: null }
|
|
: { source: 'RATE_LIMITED', payload: null };
|
|
}
|
|
|
|
/** Return only last-good Overpass data, regardless of its age. */
|
|
async function readStaleOverpass(cacheKey) {
|
|
const cached = _overpassCache.get(cacheKey);
|
|
return overpassPayloadIsData(cached)
|
|
? cached
|
|
: readOverpassDisk(cacheKey, Infinity);
|
|
}
|
|
|
|
/** Evict oldest Overpass cache entries until size is within the cap. */
|
|
function trimOverpassCache() {
|
|
while (_overpassCache.size > OVERPASS_CACHE_MAX_ENTRIES) {
|
|
const oldestKey = _overpassCache.keys().next().value;
|
|
if (!oldestKey) break;
|
|
_overpassCache.delete(oldestKey);
|
|
}
|
|
}
|
|
|
|
export {
|
|
readOverpassDisk,
|
|
resolveOverpassPreflight,
|
|
_overpassCache,
|
|
overpassDiskTtlMs,
|
|
readStaleOverpass,
|
|
trimOverpassCache,
|
|
writeOverpassDisk,
|
|
};
|