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          616x               616x 616x       616x 95x 95x     95x 95x 1097x                 616x 709x   93x 93x                     616x 617x 2x             616x 5873x 616x 616x 616x   5873x   5873x 1805x 1805x 9x 9x 9x 9x             36116x  
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
// Date object 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;