fix(display): retry a display that could not start, and re-resolve a running one periodically
CI / verify (pull_request) Successful in 1m7s
CI / infrastructure-smoke (pull_request) Successful in 38s
CI / package-release (pull_request) Successful in 50s
CI / migration-smoke (pull_request) Successful in 2m9s
CI / image-build (pull_request) Successful in 57s
CI / deploy-smoke (same-origin) (pull_request) Successful in 53s
CI / deploy-smoke (split) (pull_request) Successful in 2m35s
CI / package-publication (pull_request) Successful in 1m1s
CI / verify (pull_request) Successful in 1m7s
CI / infrastructure-smoke (pull_request) Successful in 38s
CI / package-release (pull_request) Successful in 50s
CI / migration-smoke (pull_request) Successful in 2m9s
CI / image-build (pull_request) Successful in 57s
CI / deploy-smoke (same-origin) (pull_request) Successful in 53s
CI / deploy-smoke (split) (pull_request) Successful in 2m35s
CI / package-publication (pull_request) Successful in 1m1s
A kiosk that booted before its server, as after a power cut, stopped on 'This display could not be reached' until someone pressed Retry: with the network up, no online event ever came. It now retries on its own, from two seconds doubling to a minute. A running display also re-resolves its descriptor every five minutes in the background, so a module upgraded on the server is picked up without a reload, and the same answer disturbs nothing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8f90871be0
commit
9331a8f7df
@@ -42,6 +42,11 @@ export interface DisplayAppProps {
|
||||
/** The shortest interval between two revalidations prompted by refused module requests. Test seam
|
||||
* only. */
|
||||
readonly revalidationIntervalMs?: number;
|
||||
/** How often a running display re-resolves its descriptor on its own. Test seam only. */
|
||||
readonly periodicRevalidationMs?: number;
|
||||
/** The first automatic retry delay after a failed start; doubles to a one-minute ceiling. Test seam
|
||||
* only. */
|
||||
readonly retryBaseMs?: number;
|
||||
}
|
||||
|
||||
/** A Wi-Fi reassociation or a switch reboot lasts seconds and needs no comment on a lobby wall. Only
|
||||
@@ -52,6 +57,16 @@ export const DISPLAY_OFFLINE_INDICATOR_DELAY_MS = 10_000;
|
||||
* display that is merely missing one permission must not turn every refused request into two more. */
|
||||
export const DISPLAY_REVALIDATION_INTERVAL_MS = 15_000;
|
||||
|
||||
/** A running display asks again this often, so a module upgraded on the server is picked up and a
|
||||
* revocation is noticed even by a module that makes no requests of its own. */
|
||||
export const DISPLAY_PERIODIC_REVALIDATION_MS = 5 * 60_000;
|
||||
|
||||
/** A display that could not start tries again on its own: from this delay, doubling to a minute. A
|
||||
* kiosk that boots before its server — after a power cut, say — must come up without anyone pressing
|
||||
* Retry. */
|
||||
export const DISPLAY_RETRY_BASE_MS = 2_000;
|
||||
export const DISPLAY_RETRY_MAX_MS = 60_000;
|
||||
|
||||
/** Whether a fresh resolution describes exactly the display already mounted. Compared structurally:
|
||||
* the descriptor and principal are small, server-produced, and carry nothing volatile, so any
|
||||
* difference — a new version, a new contribution, a different principal — is a real one. */
|
||||
@@ -192,8 +207,12 @@ export function DisplayApp(props: DisplayAppProps): ReactElement {
|
||||
importModule,
|
||||
moduleId,
|
||||
offlineIndicatorDelayMs = DISPLAY_OFFLINE_INDICATOR_DELAY_MS,
|
||||
periodicRevalidationMs = DISPLAY_PERIODIC_REVALIDATION_MS,
|
||||
retryBaseMs = DISPLAY_RETRY_BASE_MS,
|
||||
revalidationIntervalMs = DISPLAY_REVALIDATION_INTERVAL_MS,
|
||||
} = props;
|
||||
/** Consecutive automatic retries since the display last started successfully. */
|
||||
const failures = useRef(0);
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
const revalidation = useRef<{ inFlight: AbortController | undefined; lastAt: number }>({
|
||||
@@ -211,7 +230,7 @@ export function DisplayApp(props: DisplayAppProps): ReactElement {
|
||||
* stays. A display that is not showing anything simply starts over.
|
||||
*/
|
||||
const revalidate = useCallback(
|
||||
(reason: "online" | "authorization"): void => {
|
||||
(reason: "online" | "authorization" | "periodic"): void => {
|
||||
const current = stateRef.current;
|
||||
if (current.status !== "ready") {
|
||||
if (reason === "online") {
|
||||
@@ -264,6 +283,31 @@ export function DisplayApp(props: DisplayAppProps): ReactElement {
|
||||
|
||||
const onAuthorizationResponse = useCallback(() => revalidate("authorization"), [revalidate]);
|
||||
|
||||
// While running: ask again now and then, without disturbing anything when the answer is the same.
|
||||
useEffect(() => {
|
||||
if (state.status !== "ready" || periodicRevalidationMs <= 0) {
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(() => revalidate("periodic"), periodicRevalidationMs);
|
||||
return () => clearInterval(timer);
|
||||
}, [periodicRevalidationMs, revalidate, state.status]);
|
||||
|
||||
// Could not start: try again on its own, backing off. Pairing is not a failure — it has its own
|
||||
// polling — and a ready display has nothing to retry.
|
||||
useEffect(() => {
|
||||
if (state.status === "ready") {
|
||||
failures.current = 0;
|
||||
return;
|
||||
}
|
||||
if (state.status !== "failed" && state.status !== "unavailable") {
|
||||
return;
|
||||
}
|
||||
const delay = Math.min(DISPLAY_RETRY_MAX_MS, retryBaseMs * 2 ** Math.min(failures.current, 10));
|
||||
failures.current += 1;
|
||||
const timer = setTimeout(() => setAttempt((value) => value + 1), delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [retryBaseMs, state.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectivitySource) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user