import "server-only"; import { consumeRequestRateLimit } from "./request-rate-limit.ts"; import { ACCOUNT_BIRTH_SELECT, globalBirthProfileFromAccountRow, } from "./server-owned-birth-profile.ts"; import { createServerSupabaseClient } from "./supabase/server.ts"; import { resolveAyanamsa } from "./ayanamsa.ts"; import { assembleChartView, type ChartViewEnginePost } from "./chart-view-load.ts"; import type { ChartViewProfileInput } from "./chart-view-mapper.ts"; import type { ChartViewResponse } from "./chart-view-contract.ts"; import { CHART_VIEW_ENGINE_TIMEOUT_MS, chartViewEngineCacheKey, engineCallFromHttp, engineCallFromThrown, readChartViewEngineCache, writeChartViewEngineCache, type ChartViewLayer, type EngineCallResult, } from "./chart-view-engine.ts"; const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200"; function text(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function finite(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } return undefined; } async function postEngine(path: string, body: Record): Promise { const started = Date.now(); try { const response = await fetch(`${jyotishApiBase}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), cache: "no-store", signal: AbortSignal.timeout(CHART_VIEW_ENGINE_TIMEOUT_MS), }); let payload: unknown; try { payload = await response.json(); } catch (error) { const errorName = error instanceof Error ? error.name : "Error"; if (response.ok) { return { status: "bad_payload", path, elapsedMs: Date.now() - started, httpStatus: response.status, errorName, }; } return engineCallFromHttp({ path, elapsedMs: Date.now() - started, httpStatus: response.status, payload: null, }); } return engineCallFromHttp({ path, elapsedMs: Date.now() - started, httpStatus: response.status, payload, }); } catch (error) { return engineCallFromThrown(path, Date.now() - started, error); } } function cachedPostEngine(userId: string, profile: ChartViewProfileInput): ChartViewEnginePost { const ayanamsa = resolveAyanamsa({ ayanamsa: profile.ayanamsa }); const nodeMode = "mean"; return async (path, body) => { const key = chartViewEngineCacheKey({ userId, date: profile.date, time: profile.time, latitude: profile.latitude, longitude: profile.longitude, timezoneOffset: profile.timezoneOffset, ayanamsa, nodeMode, path, }); const hit = readChartViewEngineCache(key); if (hit) return { status: "ok", payload: hit }; const result = await postEngine(path, body); if (result.status === "ok") writeChartViewEngineCache(key, result.payload); return result; }; } function profileFromRow(row: unknown): ChartViewProfileInput | null { const profile = globalBirthProfileFromAccountRow(row); const record = row && typeof row === "object" && !Array.isArray(row) ? row as Record : {}; if (!profile.date || !profile.time) return null; const latitude = finite(profile.latitude); const longitude = finite(profile.longitude); const timezoneOffset = finite(profile.timezoneOffset); if (latitude === undefined || longitude === undefined || timezoneOffset === undefined) return null; return { name: profile.name, date: profile.date, time: profile.time, placeLabel: text(record.birth_place_label), latitude, longitude, timezoneOffset, timezoneId: profile.timezoneId, ayanamsa: typeof profile.ayanamsa === "string" ? profile.ayanamsa : undefined, birthTimeStatus: profile.birthTimeStatus, }; } export async function loadChartView(input: { now?: Date; layers?: readonly ChartViewLayer[]; } = {}): Promise<{ httpStatus: number; body: ChartViewResponse; }> { const now = input.now ?? new Date(); const supabase = await createServerSupabaseClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return assembleChartView({ userId: null, profile: null, postEngine, asOf: now.toISOString().slice(0, 10), layers: input.layers, }); } const limited = consumeRequestRateLimit({ key: `chartView:${user.id}`, limit: 20, windowMs: 60_000, }); const { data: row } = await supabase .from("profiles") .select(ACCOUNT_BIRTH_SELECT) .eq("id", user.id) .maybeSingle(); const profile = profileFromRow(row); return assembleChartView({ userId: user.id, profile, postEngine: profile ? cachedPostEngine(user.id, profile) : postEngine, asOf: now.toISOString().slice(0, 10), rateLimited: !limited.ok, layers: input.layers, }); }