104 lines
3.4 KiB
TypeScript
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 };
|
|
}
|