All files / client/src/app lazyViews.ts

96.96% Statements 32/33
90.9% Branches 10/11
100% Functions 8/8
96.42% Lines 27/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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95                            608x     608x     608x       608x     608x       608x 608x 608x       608x 608x   608x 1447x 1447x                     608x   454x         454x 49x     405x                       608x 454x       454x   454x 47x   454x 38x     454x 1447x      
import { ComponentType } from "react";
import { UserGroup } from "shared/types/models/user";
import { isAdmin, isAdminOrHelper } from "client/utils/checkUserGroup";
import { lazyWithRetry } from "client/utils/lazyWithRetry";
 
// The views kept out of the main bundle: the About tabs carry the bulk of the
// Markdown content, and the admin and helper tools are reachable by a handful
// of users. They render inside the Suspense boundary that wraps the routes.
//
// The prefetch list at the bottom must stay in step with these - a view split
// without a matching entry keeps its loading spinner on every visit.
 
type ViewImport = () => Promise<{ default: ComponentType }>;
 
const importAboutView: ViewImport = async () => ({
  default: (await import("client/views/about/AboutView")).AboutView,
});
const importFaqView: ViewImport = async () => ({
  default: (await import("client/views/about/FaqView")).FaqView,
});
const importInstructionsView: ViewImport = async () => ({
  default: (await import("client/views/about/InstructionsView"))
    .InstructionsView,
});
const importAdminView: ViewImport = async () => ({
  default: (await import("client/views/admin/AdminView")).AdminView,
});
const importHelperView: ViewImport = async () => ({
  default: (await import("client/views/helper/HelperView")).HelperView,
});
 
export const AboutView = lazyWithRetry("AboutView", importAboutView);
export const FaqView = lazyWithRetry("FaqView", importFaqView);
export const InstructionsView = lazyWithRetry(
  "InstructionsView",
  importInstructionsView,
);
export const AdminView = lazyWithRetry("AdminView", importAdminView);
export const HelperView = lazyWithRetry("HelperView", importHelperView);
 
const prefetchView = async (importView: ViewImport): Promise<void> => {
  try {
    await importView();
  } catch {
    // Nothing useful to do here, but note the cost: the browser caches a
    // failed dynamic import for the rest of the session, so this chunk is now
    // poisoned and opening the view will reject instantly and reload the page
    // rather than refetch
  }
};
 
// Some attendees are on metered or barely-working mobile data, where warming
// chunks they may never open is a bad trade
const connectionIsTooPoor = (): boolean => {
  const connection = (
    navigator as Navigator & {
      connection?: { saveData?: boolean; effectiveType?: string };
    }
  ).connection;
 
  if (!connection) {
    return false;
  }
 
  return (
    connection.saveData === true ||
    connection.effectiveType === "slow-2g" ||
    connection.effectiveType === "2g"
  );
};
 
// Warms the chunks above so opening one of these views doesn't wait on a
// download. The caller decides when, because this must not compete with the
// requests that put content on screen. The staff tools are fetched only for
// the roles that can reach them, to keep the transfer off everyone else's
// connection.
export const prefetchLazyViews = (userGroup: UserGroup): void => {
  Iif (connectionIsTooPoor()) {
    return;
  }
 
  const imports = [importAboutView, importFaqView, importInstructionsView];
 
  if (isAdminOrHelper(userGroup)) {
    imports.push(importHelperView);
  }
  if (isAdmin(userGroup)) {
    imports.push(importAdminView);
  }
 
  for (const importView of imports) {
    void prefetchView(importView);
  }
};