Files
Jyotisha/frontend/src/lib/chart-view-engine.ts
T
jesse-ux 5a8328e591
Independent Staging Quality Gate / validate (push) Successful in 27m22s
Independent Staging Quality Gate / publish (push) Canceled after 47s
fix(web): paint the chart page shell before engine calls
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.
2026-09-15 22:51:15 +08:00

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;
}