Skip to content
59 linesCodeBlameRaw
1/**
2 * A button at work, for a button that asks the desktop app to do
3 * something whose effect shows later, in a status the app sends: Retry,
4 * Start, Stop, Check for runner updates, Restart now. From the click it is
5 * busy (a spinner, pressed no more, `aria-busy`) until the status it waits
6 * on changes, the app says it couldn't, or `BUSY_LIMIT` passes with no
7 * change, so a press is never seen to do nothing and never leaves a
8 * button dead. The app's own pages do the same (apps/desktop launcher/busy.js).
9 */
10import { useCallback, useEffect, useState } from "react";
11
12/** How long a button waits for the status that follows before it is given back. */
13export const BUSY_LIMIT = 20_000;
14
15/** A press: the status as it was when the button was pressed, and when. */
16export type Press = { status: string; at: number };
17
18/** Whether a press still waits: the status is as it was, and not for too long. */
19export function stillBusy(press: Press | null, status: string, now: number): boolean {
20 return press !== null && press.status === status && now - press.at < BUSY_LIMIT;
21}
22
23/**
24 * A button's busy state, waiting on `status` (any string that changes when
25 * what the button asked for shows). `run` does the work: with `waits`, the
26 * button stays busy until the status changes; without, until the work is
27 * done. A failure gives the button back and goes to `onError`.
28 */
29export function useBusy(status: string, onError?: (error: unknown) => void) {
30 const [press, setPress] = useState<Press | null>(null);
31 const [now, setNow] = useState(0);
32 // Given back after the limit, with nothing heard.
33 useEffect(() => {
34 if (!press) return;
35 const timer = window.setTimeout(() => setNow(Date.now()), Math.max(0, press.at + BUSY_LIMIT - Date.now()));
36 return () => window.clearTimeout(timer);
37 }, [press]);
38 const busy = stillBusy(press, status, Math.max(now, press?.at ?? 0));
39 const run = useCallback(
40 (work: () => Promise<unknown> | undefined, waits = true) => {
41 const pressed = { status, at: Date.now() };
42 setPress(pressed);
43 const done = () => setPress((current) => (current === pressed ? null : current));
44 Promise.resolve()
45 .then(work)
46 .then(
47 () => {
48 if (!waits) done();
49 },
50 (error: unknown) => {
51 done();
52 onError?.(error);
53 },
54 );
55 },
56 [status, onError],
57 );
58 return { busy, run };
59}