All files / client/src/utils dataPolling.ts

82.14% Statements 23/28
50% Branches 3/6
100% Functions 5/5
82.14% Lines 23/28

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                520x     519x           519x 519x   519x 604x 6x   598x 598x 598x 598x 598x 598x 590x               590x             519x 9x   9x       519x         519x       519x           519x           519x            
import { config } from "shared/config";
import { loadData } from "client/utils/loadData";
import { onPageResume } from "client/utils/pageLifecycle";
 
// Kept out of the component that starts it, both because it is a good deal of
// machinery for a view to hold and because the React Compiler declines any
// function whose body contains a `finally` (or a `try` with no `catch`), which
// the in-flight guard below needs
export const startDataPolling = (
  onLoadFinished: (succeeded: boolean) => void,
): (() => void) => {
  const { dataUpdateInterval } = config.client();
 
  // Refresh triggers can fire together (e.g. an overdue interval tick, the
  // online event, and a page resume when a phone wakes), and concurrent
  // loads could dispatch a slower stale response over a newer one, so only
  // one load runs at a time
  let fetchInFlight = false;
  let fetchQueued = false;
 
  const fetchData = async (): Promise<void> => {
    if (fetchInFlight) {
      return;
    }
    fetchInFlight = true;
    try {
      let succeeded = false;
      do {
        fetchQueued = false;
        succeeded = await loadData();
        onLoadFinished(succeeded);
        // A successful load satisfies triggers that arrived while it ran;
        // a failed one reruns for them (e.g. its requests failed right
        // before connectivity returned). fetchQueued is set while loadData
        // is awaited, which type narrowing can't see
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
      } while (fetchQueued && !succeeded);
    } finally {
      fetchInFlight = false;
    }
  };
 
  // Connectivity and resume refreshes must not be dropped just because a
  // load is in flight - an in-flight request can hang until the request
  // timeout - so they queue a trailing rerun
  const queueFetchData = (): void => {
    fetchQueued = true;
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    fetchData();
  };
 
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
  fetchData();
 
  // Interval ticks don't queue behind an in-flight load: the next tick
  // arrives within the update interval anyway
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
  const updateTimer = setInterval(fetchData, dataUpdateInterval * 1000);
 
  // Refresh immediately when connectivity returns; the successful response
  // also heals a possible stale network error toast
  addEventListener("online", queueFetchData);
 
  // While the page is hidden (screen off, background tab) the browser
  // freezes timers and polling lags behind, so refresh on resume - but only
  // when hidden long enough to actually miss a poll, so that plain tab
  // switching doesn't cause request bursts
  const offPageResume = onPageResume((hiddenDurationMs) => {
    if (hiddenDurationMs >= dataUpdateInterval * 1000) {
      queueFetchData();
    }
  });
 
  return () => {
    clearInterval(updateTimer);
    removeEventListener("online", queueFetchData);
    offPageResume();
  };
};