swarm-ui: pages that only display start from the answer they last saw, then refresh behind it
usePolled takes a cache key: it saves each answer in localStorage and starts from the saved one, so opening the agents, alerts, builds, review runs or hives view shows the last data at once and the refresh spinner marks the update. A saved answer is dropped after a day, when it was saved by another build of the UI, at log out (a new onLogout of the shared user menu) and when the gateway ends the login (a new onSessionEnded of the session guard). Pages with an editor (schedules, notification flows, broadcast) do not use it.
This commit is contained in:
parent
47c447409b
commit
fd51cbd19d
1 changed files with 238 additions and 14 deletions
|
|
@ -1,5 +1,7 @@
|
|||
// A resource refetched on a timer: `{ data, error, refreshing, refresh, setData }`.
|
||||
// `data` is `null` until the first answer; a failed fetch sets `error` and
|
||||
// `data` is `null` until the first answer (or, with a `cacheKey`, the saved one
|
||||
// of the last visit, which is shown at once and replaced by the fresh answer;
|
||||
// see `answer-cache.ts`); a failed fetch sets `error` and
|
||||
// keeps the last good `data`; the next good answer clears `error`. The
|
||||
// request discipline (skip a tick while one is in flight, drop answers that
|
||||
// are out of date or arrive after unmount) lives in `createPoller`.
|
||||
|
|
@ -16,6 +18,7 @@ import {
|
|||
useRefreshInterval,
|
||||
type RefreshIntervalMs,
|
||||
} from "../refresh-interval/RefreshInterval.js";
|
||||
import { readAnswer, writeAnswer } from "./answer-cache.js";
|
||||
import { createBusyHold } from "./busy-hold.js";
|
||||
import { createPoller, type Poller } from "./poller.js";
|
||||
|
||||
|
|
@ -43,6 +46,10 @@ function toProblem(e: unknown): ProblemDetails {
|
|||
export function usePolled<T>(
|
||||
intervalMs: RefreshIntervalMs,
|
||||
fetcher: (signal: AbortSignal) => Promise<T>,
|
||||
opts: {
|
||||
/** Saves each answer under this key and starts from the saved one. */
|
||||
cacheKey?: string;
|
||||
} = {},
|
||||
): {
|
||||
data: T | null;
|
||||
error: ProblemDetails | null;
|
||||
|
|
@ -50,7 +57,12 @@ export function usePolled<T>(
|
|||
refresh: () => void;
|
||||
setData: (update: (prev: T | null) => T | null) => void;
|
||||
} {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const { cacheKey } = opts;
|
||||
const cacheKeyRef = useRef(cacheKey);
|
||||
cacheKeyRef.current = cacheKey;
|
||||
const [data, setData] = useState<T | null>(() =>
|
||||
cacheKey === undefined ? null : readAnswer<T>(cacheKey),
|
||||
);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
|
|
@ -66,6 +78,9 @@ export function usePolled<T>(
|
|||
pollerRef.current = createPoller<T>({
|
||||
fetch: (signal) => fetcherRef.current(signal),
|
||||
onData: (next) => {
|
||||
if (cacheKeyRef.current !== undefined) {
|
||||
writeAnswer(cacheKeyRef.current, next);
|
||||
}
|
||||
setData(next);
|
||||
setError(null);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue