import type { OfficialPosition } from "@/lib/domain/railway"; const CACHE_VERSION = 1; const MAX_CACHE_ENTRIES = 1000; type CacheRecord = Record; const isRecord = (value: unknown): value is CacheRecord => typeof value === "object" && value !== null; const isOfficialPosition = (value: unknown): value is OfficialPosition => { if (!isRecord(value)) return false; return ( typeof value.positionId === "string" && value.positionId.length > 0 && typeof value.pos === "string" && value.pos.length > 0 && typeof value.line === "string" && value.line.length > 0 && typeof value.posNum === "string" && value.posNum.length > 0 && ["unclassified", "station", "between", "prediction", "other"].includes( value.positionType as string, ) && (value.platform === null || typeof value.platform === "string") && (value.track === null || typeof value.track === "string") && (value.note === null || typeof value.note === "string") && ["unreviewed", "partial", "reviewed"].includes( value.enrichmentStatus as string, ) && (value.firstSeenAt === null || typeof value.firstSeenAt === "string") && (value.lastSeenAt === null || typeof value.lastSeenAt === "string") && typeof value.revision === "number" && Number.isInteger(value.revision) && value.revision >= 0 ); }; /** * Parse the local warm cache without trusting its contents. Invalid or old * entries are ignored so a broken cache never blocks the live resolver. */ export const parseOfficialPositionCache = ( raw: unknown, ): Map => { let value = raw; if (typeof value === "string") { try { value = JSON.parse(value) as unknown; } catch { return new Map(); } } if (!isRecord(value) || value.version !== CACHE_VERSION) return new Map(); if (!Array.isArray(value.entries)) return new Map(); const result = new Map(); for (const entry of value.entries.slice(-MAX_CACHE_ENTRIES)) { if (!isRecord(entry)) continue; if (typeof entry.key !== "string" || !entry.key) continue; if (!isOfficialPosition(entry.value)) continue; result.set(entry.key, entry.value); } return result; }; /** Serialize only the bounded, validated metadata cache. */ export const serializeOfficialPositionCache = ( entries: ReadonlyMap, ): string => { const serialized = Array.from(entries.entries()) .filter(([, value]) => isOfficialPosition(value)) .slice(-MAX_CACHE_ENTRIES) .map(([key, value]) => ({ key, value })); return JSON.stringify({ version: CACHE_VERSION, entries: serialized }); };