BUG-715/716/717: typed engine failures with logs and split copy, natal-only open, on-demand tabs, 10s timeout, in-memory cache, no speed-promise eyebrow.
149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
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<string, unknown> }
|
|
| {
|
|
readonly status: "busy" | "http_error" | "timeout" | "bad_payload";
|
|
readonly path: string;
|
|
readonly elapsedMs: number;
|
|
readonly httpStatus?: number;
|
|
readonly errorName?: string;
|
|
};
|
|
|
|
type CacheStore = Map<string, { payload: Record<string, unknown>; 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<ChartViewLayer>();
|
|
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<string, unknown> | 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<string, unknown>,
|
|
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<string, unknown> };
|
|
}
|
|
|
|
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<EngineCallResult, { status: "ok" }>): 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<string, unknown> | null {
|
|
return result.status === "ok" ? result.payload : null;
|
|
}
|