All files / client/src/utils clock.ts

96.87% Statements 31/32
87.5% Branches 7/8
100% Functions 7/7
96.77% Lines 30/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81          527x               527x 527x       527x 92x 92x     92x 92x 1047x                 527x 617x   90x 90x                     527x 528x 2x             527x 5230x 527x 527x 527x   5230x   5230x 1607x 1607x 9x 9x 9x 9x             32126x  
import { onPageResume } from "client/utils/pageLifecycle";
 
// Everything the current time is compared against - sign-up windows above all -
// is minute-granular, and the data it is compared against is refetched about
// this often anyway, so a finer tick would only re-render for no visible change
const CLOCK_TICK_MS = 60 * 1000;
 
// One instant shared by every consumer, replaced only on a tick. Reading the
// clock while rendering would be impure: React has nothing to key a cached
// value on, so a component keeps whichever instant it first rendered with.
// Ticking also keeps the value stable in between, so consumers can list it as
// a dependency like any other value. Kept as epoch milliseconds rather than a
// Dayjs so that an unchanged instant compares equal instead of merely equivalent
let timeNowMs = Date.now();
const listeners = new Set<() => void>();
let clockTimer: ReturnType<typeof setTimeout> | undefined;
let stopListeningForPageResume: (() => void) | undefined;
 
const advanceClock = (): void => {
  const now = Date.now();
  Iif (now === timeNowMs) {
    return;
  }
  timeNowMs = now;
  for (const listener of listeners) {
    listener();
  }
};
 
// Ticks land on wall-clock minute boundaries rather than on whatever phase the
// page happened to load at. A sign-up window opening at 18:15 therefore opens
// at the same moment for everyone, instead of up to a full tick later for
// whoever loaded at an unlucky offset - which for first-come-first-served
// sign-ups is the difference between a spot and no spot
const scheduleTick = (): void => {
  clockTimer = setTimeout(
    () => {
      advanceClock();
      scheduleTick();
    },
    CLOCK_TICK_MS - (Date.now() % CLOCK_TICK_MS),
  );
};
 
// A frozen page (screen off, background tab) runs no timers, so on resume the
// app can render freshly polled data against an instant from before the sleep.
// The same applies after any spell with no subscribers. Only acts on a gap
// worth a tick, so subscribing right after the module loaded doesn't hand
// every consumer a second, pointlessly different instant to re-render for
const catchUpClock = (): void => {
  if (Date.now() - timeNowMs >= CLOCK_TICK_MS) {
    advanceClock();
  }
};
 
// The subscribe half of a store for useSyncExternalStore. The timer only runs
// while something is subscribed, so an app that never reads the clock doesn't
// wake the page for it
export const subscribeToClock = (listener: () => void): (() => void) => {
  if (listeners.size === 0) {
    catchUpClock();
    scheduleTick();
    stopListeningForPageResume = onPageResume(catchUpClock);
  }
  listeners.add(listener);
 
  return () => {
    listeners.delete(listener);
    if (listeners.size === 0) {
      clearTimeout(clockTimer);
      clockTimer = undefined;
      stopListeningForPageResume?.();
      stopListeningForPageResume = undefined;
    }
  };
};
 
// Must keep returning the same value until the clock ticks: React re-reads it
// after every render and re-renders again whenever it changed
export const getTimeNowSnapshot = (): number => timeNowMs;