export const CHART_VIEW_ENGINE_TIMEOUT_MS = 10_000; export const CHART_VIEW_ENGINE_CACHE_TTL_MS = 5 * 60 * 1_000; const ENGINE_CACHE_MAX_ENTRIES = 200; export const CHART_VIEW_LAYERS = ["varga", "chara", "western", "qizheng"] as const; export type ChartViewLayer = (typeof CHART_VIEW_LAYERS)[number]; export type EngineCallResult = | { readonly status: "ok"; readonly payload: Record } | { readonly status: "busy" | "http_error" | "timeout" | "bad_payload"; readonly path: string; readonly elapsedMs: number; readonly httpStatus?: number; readonly errorName?: string; }; type CacheStore = Map; expiresAt: number }>; const state = globalThis as typeof globalThis & { jyotishaChartViewEngineCache?: CacheStore; }; function engineCache(): CacheStore { state.jyotishaChartViewEngineCache ??= new Map(); return state.jyotishaChartViewEngineCache; } export function parseChartViewLayers(raw: string | null | undefined): ChartViewLayer[] { if (!raw) return []; const wanted = new Set(); for (const part of raw.split(",")) { const id = part.trim(); if ((CHART_VIEW_LAYERS as readonly string[]).includes(id)) { wanted.add(id as ChartViewLayer); } } return CHART_VIEW_LAYERS.filter((id) => wanted.has(id)); } export function chartViewEngineCacheKey(input: { userId: string; date: string; time: string; latitude: number; longitude: number; timezoneOffset: number; ayanamsa: string; nodeMode: string; path: string; }): string { return [ input.userId, input.path, input.date, input.time, input.latitude.toFixed(6), input.longitude.toFixed(6), String(input.timezoneOffset), input.ayanamsa, input.nodeMode, ].join("\u001f"); } export function readChartViewEngineCache(key: string, now = Date.now()): Record | null { const entry = engineCache().get(key); if (!entry) return null; if (entry.expiresAt <= now) { engineCache().delete(key); return null; } return entry.payload; } export function writeChartViewEngineCache( key: string, payload: Record, now = Date.now(), ): void { const store = engineCache(); if (store.size >= ENGINE_CACHE_MAX_ENTRIES) { for (const [itemKey, entry] of store) { if (entry.expiresAt <= now) store.delete(itemKey); } if (store.size >= ENGINE_CACHE_MAX_ENTRIES) { const first = store.keys().next().value; if (typeof first === "string") store.delete(first); } } store.set(key, { payload, expiresAt: now + CHART_VIEW_ENGINE_CACHE_TTL_MS }); } export function clearChartViewEngineCache(): void { engineCache().clear(); } export function engineCallFromHttp(input: { path: string; elapsedMs: number; httpStatus: number; payload: unknown; }): EngineCallResult { if (input.httpStatus === 429) { return { status: "busy", path: input.path, elapsedMs: input.elapsedMs, httpStatus: input.httpStatus, }; } if (input.httpStatus < 200 || input.httpStatus >= 300) { return { status: "http_error", path: input.path, elapsedMs: input.elapsedMs, httpStatus: input.httpStatus, }; } if (!input.payload || typeof input.payload !== "object" || Array.isArray(input.payload)) { return { status: "bad_payload", path: input.path, elapsedMs: input.elapsedMs, httpStatus: input.httpStatus, }; } return { status: "ok", payload: input.payload as Record }; } export function engineCallFromThrown(path: string, elapsedMs: number, error: unknown): EngineCallResult { const errorName = error instanceof Error ? error.name : "Error"; const status = errorName === "TimeoutError" || errorName === "AbortError" ? "timeout" : "http_error"; return { status, path, elapsedMs, errorName }; } export function logEngineCallFailure(result: Exclude): void { console.warn("chart_view_engine_failed", { path: result.path, status: result.status, httpStatus: result.httpStatus ?? null, errorName: result.errorName ?? null, elapsedMs: result.elapsedMs, }); } export function enginePayload(result: EngineCallResult): Record | null { return result.status === "ok" ? result.payload : null; }