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.
172 lines
5.1 KiB
TypeScript
172 lines
5.1 KiB
TypeScript
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<string, unknown>): Promise<EngineCallResult> {
|
|
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<string, unknown>
|
|
: {};
|
|
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,
|
|
});
|
|
}
|