92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||
|
|
import type { SightingWidgetType } from "../types/types";
|
||
|
|
|
||
|
|
export function useSightingFeed(
|
||
|
|
baseUrl: string,
|
||
|
|
{
|
||
|
|
limit = 7,
|
||
|
|
pollMs = 800,
|
||
|
|
autoSelectLatest = true,
|
||
|
|
}: {
|
||
|
|
limit?: number;
|
||
|
|
pollMs?: number;
|
||
|
|
autoSelectLatest?: boolean;
|
||
|
|
} = {}
|
||
|
|
) {
|
||
|
|
const [items, setItems] = useState<SightingWidgetType[]>(
|
||
|
|
() => Array(limit).fill(null) as unknown as SightingWidgetType[]
|
||
|
|
);
|
||
|
|
const [selectedRef, setSelectedRef] = useState<number | null>(null);
|
||
|
|
const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null);
|
||
|
|
|
||
|
|
const mostRecentRef = useRef<number>(-1);
|
||
|
|
|
||
|
|
// effective selected (fallback to most recent)
|
||
|
|
const selected = useMemo(
|
||
|
|
() =>
|
||
|
|
selectedRef == null
|
||
|
|
? null
|
||
|
|
: items.find((x) => x?.ref === selectedRef) ?? null,
|
||
|
|
[items, selectedRef]
|
||
|
|
);
|
||
|
|
const effectiveSelected = selected ?? mostRecent ?? null;
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let delay = pollMs;
|
||
|
|
let dead = false;
|
||
|
|
const controller = new AbortController();
|
||
|
|
|
||
|
|
async function tick() {
|
||
|
|
try {
|
||
|
|
// Pause when tab hidden to save CPU/network
|
||
|
|
if (document.hidden) {
|
||
|
|
setTimeout(tick, Math.max(delay, 2000));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const url = `http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=${mostRecentRef.current}`;
|
||
|
|
const res = await fetch(url, { signal: controller.signal });
|
||
|
|
if (!res.ok) throw new Error(String(res.status));
|
||
|
|
|
||
|
|
const obj: SightingWidgetType = await res.json();
|
||
|
|
if (obj && typeof obj.ref === "number" && obj.ref > -1) {
|
||
|
|
setItems((prev) => {
|
||
|
|
const next = [obj, ...prev].slice(0, limit);
|
||
|
|
// maintain selection if still present; otherwise select newest if allowed
|
||
|
|
const stillExists =
|
||
|
|
selectedRef != null && next.some((x) => x?.ref === selectedRef);
|
||
|
|
if (autoSelectLatest && !stillExists) {
|
||
|
|
setSelectedRef(obj.ref);
|
||
|
|
}
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
setMostRecent(obj);
|
||
|
|
mostRecentRef.current = obj.ref;
|
||
|
|
delay = pollMs; // reset backoff on success
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// exponential backoff (max 10s)
|
||
|
|
delay = Math.min(delay * 2, 10000);
|
||
|
|
} finally {
|
||
|
|
if (!dead) setTimeout(tick, delay);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const t = setTimeout(tick, pollMs);
|
||
|
|
return () => {
|
||
|
|
dead = true;
|
||
|
|
controller.abort();
|
||
|
|
clearTimeout(t);
|
||
|
|
};
|
||
|
|
}, [baseUrl, limit, pollMs, autoSelectLatest, selectedRef]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
items,
|
||
|
|
selectedRef,
|
||
|
|
setSelectedRef,
|
||
|
|
mostRecent,
|
||
|
|
effectiveSelected,
|
||
|
|
mostRecentRef,
|
||
|
|
};
|
||
|
|
}
|