Files
Jyotisha/frontend/src/hooks/use-chart-page.ts
T

104 lines
3.4 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchChartView } from "@/lib/chart-view-client";
import type { ChartViewResponse } from "@/lib/chart-view-contract";
import type { ChartViewLayer } from "@/lib/chart-view-engine";
import { CHART_VIEW_COPY } from "@/lib/chart-view-labels";
import {
peekChartPage,
refreshChartPage,
writeChartPage,
type ChartPageSnapshot,
} from "@/lib/secondary-page-data";
function unavailableBody(): ChartViewResponse {
return { status: "chart_unavailable", billed: false, message: CHART_VIEW_COPY.unavailable };
}
function warnClientFailure(label: string, error: unknown, extra?: Record<string, string>): void {
console.warn(label, {
errorName: error instanceof Error ? error.name : "Error",
message: error instanceof Error ? error.message : "unknown",
...extra,
});
}
function viewFromSnapshot(snapshot: ChartPageSnapshot | null): ChartViewResponse | null {
return snapshot?.kind === "view" ? snapshot.view : null;
}
export function useChartPage() {
const [view, setView] = useState<ChartViewResponse | null>(() => viewFromSnapshot(peekChartPage()));
const [pendingLayers, setPendingLayers] = useState<ReadonlySet<ChartViewLayer>>(new Set());
const loadedLayers = useRef(new Set<ChartViewLayer>());
const inflightLayers = useRef(new Set<ChartViewLayer>());
useEffect(() => {
const cached = peekChartPage();
if (cached?.kind === "unauthenticated") {
window.location.assign("/login");
return;
}
let cancelled = false;
void refreshChartPage()
.then((snapshot) => {
if (cancelled) return;
if (snapshot.kind === "unauthenticated") {
window.location.assign("/login");
return;
}
setView(snapshot.view);
})
.catch((error: unknown) => {
if (cancelled) return;
warnClientFailure("chart_page_fetch_failed", error);
if (peekChartPage()?.kind === "view") return;
const fallback = unavailableBody();
writeChartPage({ kind: "view", view: fallback });
setView(fallback);
});
return () => {
cancelled = true;
};
}, []);
const requestLayer = useCallback((layer: ChartViewLayer) => {
if (loadedLayers.current.has(layer) || inflightLayers.current.has(layer)) return;
inflightLayers.current.add(layer);
setPendingLayers((current) => {
if (current.has(layer)) return current;
const next = new Set(current);
next.add(layer);
return next;
});
void fetchChartView({ layers: [layer] })
.then((result) => {
if (result.body?.status === "ok") {
const stored = writeChartPage({ kind: "view", view: result.body });
if (stored.kind !== "view" || stored.view.status !== "ok") {
setView(stored.kind === "view" ? stored.view : result.body);
return;
}
loadedLayers.current.add(layer);
setView(stored.view);
}
})
.catch((error: unknown) => {
warnClientFailure("chart_page_layer_failed", error, { layer });
})
.finally(() => {
inflightLayers.current.delete(layer);
setPendingLayers((current) => {
if (!current.has(layer)) return current;
const next = new Set(current);
next.delete(layer);
return next;
});
});
}, []);
return { view, pendingLayers, requestLayer };
}