Skip to content
Source
Sign inSign up

delayedStatus.ts

76 lines2.5 KB

1const STATUS_SHOW_DELAY_MS = 400;
2const STATUS_MIN_VISIBLE_MS = 400;
3​
4/**
5 * A status shows only after it has stayed set for a moment, then stays shown
6 * for a minimum time so a quick flicker never flashes on screen.
7 * `deadlinePassed` tells whether `delayedStatusDeadline(state)` has been reached.
8 */
9export type DelayedStatusState<A> =
10 | { readonly phase: "idle"; readonly key: string }
11 | { readonly phase: "pending"; readonly key: string; readonly showAt: number }
12 | {
13 readonly phase: "shown";
14 readonly key: string;
15 readonly value: A;
16 readonly holdUntil: number;
17 };
18​
19export interface DelayedStatusInput<A> {
20 readonly key: string;
21 /** Compared by identity, so use strings or other stable values. */
22 readonly value: A | null;
23 readonly deadlinePassed: boolean;
24 readonly showDelayMs?: number | undefined;
26​
27/**
28 * Returns `state` itself when nothing changes, so it can run on every render.
29 * `now` is read only to stamp a new deadline; it is a function so a memoizing
30 * caller (the React Compiler) cannot cache a stale clock reading.
31 */
32export function stepDelayedStatus<A>(
33 state: DelayedStatusState<A> | null,
34 { key, value, deadlinePassed, showDelayMs = STATUS_SHOW_DELAY_MS }: DelayedStatusInput<A>,
35 now: () => number,
36): DelayedStatusState<A> {
37 const idle = (): DelayedStatusState<A> => ({ phase: "idle", key });
38 const pending = (): DelayedStatusState<A> => ({
39 phase: "pending",
40 key,
41 showAt: now() + showDelayMs,
42 });
43 const shown = (shownValue: A): DelayedStatusState<A> => ({
44 phase: "shown",
45 key,
46 value: shownValue,
47 holdUntil: now() + STATUS_MIN_VISIBLE_MS,
48 });
49​
50 if (state === null || state.key !== key) return value === null ? idle() : pending();
51 switch (state.phase) {
52 case "idle":
53 return value === null ? state : pending();
54 case "pending":
55 if (value === null) return idle();
56 return deadlinePassed ? shown(value) : state;
57 case "shown":
58 if (value === null) return deadlinePassed ? idle() : state;
59 return value === state.value ? state : shown(value);
60 }
62​
63export function delayedStatusDeadline<A>(state: DelayedStatusState<A>): number | null {
64 switch (state.phase) {
65 case "idle":
66 return null;
67 case "pending":
68 return state.showAt;
69 case "shown":
70 return state.holdUntil;
71 }
73​
74export function shownDelayedStatus<A>(state: DelayedStatusState<A>, key: string): A | null {
75 return state.phase === "shown" && state.key === key ? state.value : null;