123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { type SignageApiFailure, SignageRequestError } from "./api.js";
|
|
|
|
/**
|
|
* One loading state, used everywhere, so every panel in the dashboard fails the same way.
|
|
*
|
|
* The states are the ones an operator can actually act on: still loading, loaded, or failed with a
|
|
* reason Signage itself gave. There is no fourth state for "loaded but empty" — a panel decides what
|
|
* empty means for its own data, because "no timelines yet" and "no displays yet" are different
|
|
* sentences.
|
|
*/
|
|
export type Loadable<Value> =
|
|
| { readonly status: "loading" }
|
|
| { readonly status: "ready"; readonly value: Value }
|
|
| { readonly status: "failed"; readonly failure: SignageApiFailure };
|
|
|
|
export interface Resource<Value> {
|
|
readonly state: Loadable<Value>;
|
|
/** Re-reads without flashing the panel back to a spinner: the previous value stays on screen
|
|
* until the new one arrives, which is what makes a five-second refresh bearable to look at. */
|
|
reload(): void;
|
|
}
|
|
|
|
export function useResource<Value>(
|
|
load: () => Promise<Value>,
|
|
dependencies: readonly unknown[],
|
|
): Resource<Value> {
|
|
const [state, setState] = useState<Loadable<Value>>({ status: "loading" });
|
|
const generation = useRef(0);
|
|
const loadRef = useRef(load);
|
|
loadRef.current = load;
|
|
|
|
const run = useCallback(() => {
|
|
generation.current += 1;
|
|
const token = generation.current;
|
|
void loadRef
|
|
.current()
|
|
.then((value) => {
|
|
// A response from a request the panel has already moved on from must never overwrite a
|
|
// newer one.
|
|
if (token === generation.current) {
|
|
setState({ status: "ready", value });
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (token !== generation.current) {
|
|
return;
|
|
}
|
|
setState({
|
|
failure:
|
|
error instanceof SignageRequestError
|
|
? error.failure
|
|
: {
|
|
code: "SIGNAGE_REQUEST_FAILED",
|
|
message: "Signage could not be reached.",
|
|
status: 0,
|
|
},
|
|
status: "failed",
|
|
});
|
|
});
|
|
}, []);
|
|
|
|
// The caller's dependency list is spread deliberately: a resource reloads when *its* inputs
|
|
// change, which is the whole reason this takes one.
|
|
useEffect(run, [run, ...dependencies]);
|
|
|
|
return { reload: run, state };
|
|
}
|
|
|
|
/** A mutation with a pending flag and a sanitized failure, so a button can disable itself and a form
|
|
* can say why a save was refused. */
|
|
export interface Action {
|
|
readonly pending: boolean;
|
|
readonly failure: SignageApiFailure | undefined;
|
|
run(work: () => Promise<unknown>): void;
|
|
clearFailure(): void;
|
|
}
|
|
|
|
export function useAction(onSettled?: () => void): Action {
|
|
const [pending, setPending] = useState(false);
|
|
const [failure, setFailure] = useState<SignageApiFailure | undefined>(undefined);
|
|
const settled = useRef(onSettled);
|
|
settled.current = onSettled;
|
|
|
|
const run = useCallback((work: () => Promise<unknown>) => {
|
|
setPending(true);
|
|
setFailure(undefined);
|
|
void work()
|
|
.then(() => {
|
|
setPending(false);
|
|
settled.current?.();
|
|
})
|
|
.catch((error: unknown) => {
|
|
setPending(false);
|
|
setFailure(
|
|
error instanceof SignageRequestError
|
|
? error.failure
|
|
: {
|
|
code: "SIGNAGE_REQUEST_FAILED",
|
|
message: "Signage could not be reached.",
|
|
status: 0,
|
|
},
|
|
);
|
|
});
|
|
}, []);
|
|
|
|
return { clearFailure: () => setFailure(undefined), failure, pending, run };
|
|
}
|
|
|
|
/** Re-renders on an interval, so a countdown or a "last seen" reads correctly without every panel
|
|
* inventing its own timer. Paused when the interval is zero. */
|
|
export function useTick(intervalMs: number): number {
|
|
const [tick, setTick] = useState(0);
|
|
useEffect(() => {
|
|
if (intervalMs <= 0) {
|
|
return;
|
|
}
|
|
const handle = setInterval(() => setTick((value) => value + 1), intervalMs);
|
|
return () => clearInterval(handle);
|
|
}, [intervalMs]);
|
|
return tick;
|
|
}
|