mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34: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.
176 lines
5.7 KiB
JavaScript
176 lines
5.7 KiB
JavaScript
import {
|
|
MILITARY_INSTALLATION_CACHE_MS,
|
|
MILITARY_INSTALLATION_ELEMENT_CAP,
|
|
MILITARY_INSTALLATION_DISK_TTL_MS,
|
|
MILITARY_INSTALLATION_DISK_DIR,
|
|
MILITARY_INSTALLATION_MAX_CACHE,
|
|
} from './constants.js';
|
|
import { cachedByConfiguredUpstream } from '../overpass/constants.js';
|
|
import path from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { promises as fsp } from 'node:fs';
|
|
|
|
const _militaryInstallationCache = new Map();
|
|
|
|
/**
|
|
* Resolve the READ tiers for one installation request, in order: fresh memory,
|
|
* then disk. Returns UPSTREAM when neither can answer.
|
|
*
|
|
* Disk is skipped while a request for this key is already in flight — the
|
|
* caller joins that instead of paying a read. Exported so the tier ORDER is
|
|
* testable against a real temp directory without a Vite server, mirroring
|
|
* resolveOverpassPreflight.
|
|
*
|
|
* @param {object} options
|
|
* @param {string} options.cacheKey
|
|
* @param {Map<string, {payload: object, cachedAt: number}>} options.memoryCache
|
|
* @param {Map<string, Promise>} options.inFlight
|
|
* @param {() => Promise<?{payload: object, cachedAt: number}>} options.readDisk
|
|
* @param {number} [options.now]
|
|
* @param {number} [options.cacheMs]
|
|
* @returns {Promise<{source: 'HIT'|'DISK'|'UPSTREAM', entry: ?object}>}
|
|
*/
|
|
async function resolveMilitaryInstallationTier({
|
|
cacheKey,
|
|
memoryCache,
|
|
inFlight,
|
|
readDisk,
|
|
now = Date.now(),
|
|
cacheMs = MILITARY_INSTALLATION_CACHE_MS,
|
|
}) {
|
|
const cached = memoryCache.get(cacheKey);
|
|
if (cached && now - cached.cachedAt <= cacheMs)
|
|
return { source: 'HIT', entry: cached };
|
|
if (inFlight.has(cacheKey)) return { source: 'UPSTREAM', entry: null };
|
|
const disk = await readDisk();
|
|
return disk
|
|
? { source: 'DISK', entry: disk }
|
|
: { source: 'UPSTREAM', entry: null };
|
|
}
|
|
|
|
/**
|
|
* Bring a stored installation entry up to the current payload shape.
|
|
*
|
|
* Entries written before the saturation guard shipped carry no `saturated`
|
|
* field, and the disk TTL is 30 DAYS — so without this a cached, truncated
|
|
* 700-element snapped response would keep skipping the exact-viewport retry for
|
|
* a month, quietly starving in-view sites. Saturation is DERIVED from the
|
|
* element count rather than invalidating those entries, so warm caches survive
|
|
* the upgrade.
|
|
* @param {?{payload: object, cachedAt: number}} entry
|
|
* @returns {?{payload: object, cachedAt: number}}
|
|
*/
|
|
function migrateMilitaryInstallationEntry(entry) {
|
|
if (!entry?.payload || typeof entry.payload.saturated === 'boolean')
|
|
return entry;
|
|
const elements = Array.isArray(entry.payload.elements)
|
|
? entry.payload.elements
|
|
: [];
|
|
return {
|
|
...entry,
|
|
payload: {
|
|
...entry.payload,
|
|
saturated: elements.length >= MILITARY_INSTALLATION_ELEMENT_CAP,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Whether a stored installation entry is still inside its TTL. */
|
|
function militaryInstallationDiskFresh(
|
|
entry,
|
|
maxAgeMs = MILITARY_INSTALLATION_DISK_TTL_MS,
|
|
now = Date.now(),
|
|
) {
|
|
if (
|
|
!entry ||
|
|
!Number.isFinite(entry.cachedAt) ||
|
|
!Array.isArray(entry.payload?.elements)
|
|
)
|
|
return false;
|
|
return now - entry.cachedAt <= maxAgeMs;
|
|
}
|
|
|
|
/** Cache key -> stable disk-cache file path. */
|
|
function militaryInstallationDiskPath(
|
|
cacheKey,
|
|
dir = MILITARY_INSTALLATION_DISK_DIR,
|
|
) {
|
|
return path.join(
|
|
dir,
|
|
`${createHash('sha1').update(cacheKey).digest('hex')}.json`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Read a disk-cached installation entry. maxAgeMs Infinity = any age (the
|
|
* serve-stale path when Overpass is down).
|
|
* @returns {Promise<?{payload: object, cachedAt: number}>}
|
|
*/
|
|
async function readMilitaryInstallationDisk(
|
|
cacheKey,
|
|
maxAgeMs,
|
|
dir = MILITARY_INSTALLATION_DISK_DIR,
|
|
) {
|
|
try {
|
|
const entry = JSON.parse(
|
|
await fsp.readFile(militaryInstallationDiskPath(cacheKey, dir), 'utf8'),
|
|
);
|
|
// Same reason as the Overpass disk cache: the key is the bbox alone, so an
|
|
// entry written against a different OVERPASS_UPSTREAMS is otherwise a hit.
|
|
if (!cachedByConfiguredUpstream(entry)) return null;
|
|
if (!militaryInstallationDiskFresh(entry, maxAgeMs)) return null;
|
|
return migrateMilitaryInstallationEntry(entry);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist one installation payload ATOMICALLY: serialize to a temp sibling,
|
|
* then rename over the target. A crash or a full disk mid-write leaves the
|
|
* PREVIOUS entry intact — an in-place overwrite would shred the last-good copy
|
|
* and take serve-stale down with it, exactly when it is needed most.
|
|
* @returns {Promise<boolean>} Whether the entry landed.
|
|
*/
|
|
async function writeMilitaryInstallationDisk(
|
|
cacheKey,
|
|
entry,
|
|
dir = MILITARY_INSTALLATION_DISK_DIR,
|
|
) {
|
|
const target = militaryInstallationDiskPath(cacheKey, dir);
|
|
// Same directory, so the rename is atomic on POSIX rather than a cross-device copy.
|
|
const temp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
try {
|
|
await fsp.mkdir(dir, { recursive: true });
|
|
await fsp.writeFile(temp, JSON.stringify(entry));
|
|
await fsp.rename(temp, target);
|
|
return true;
|
|
} catch (err) {
|
|
console.warn(
|
|
'[Installations Proxy] disk cache write failed:',
|
|
err?.message || err,
|
|
);
|
|
await fsp.rm(temp, { force: true }).catch(() => {});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function trimMilitaryInstallationCache() {
|
|
while (_militaryInstallationCache.size > MILITARY_INSTALLATION_MAX_CACHE) {
|
|
const oldest = _militaryInstallationCache.keys().next().value;
|
|
if (oldest === undefined) break;
|
|
_militaryInstallationCache.delete(oldest);
|
|
}
|
|
}
|
|
|
|
export {
|
|
_militaryInstallationCache,
|
|
trimMilitaryInstallationCache,
|
|
writeMilitaryInstallationDisk,
|
|
resolveMilitaryInstallationTier,
|
|
readMilitaryInstallationDisk,
|
|
migrateMilitaryInstallationEntry,
|
|
militaryInstallationDiskFresh,
|
|
militaryInstallationDiskPath,
|
|
};
|