Ctrl K
ComunidadGuías…

shared/utils/lazyWithRetry.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import * as React from "react";

// oxlint-disable no-explicit-any -- ComponentType<any> is the standard React pattern for generic component constraints
type ComponentPromise<T extends React.ComponentType<any>> = Promise<{
  default: T;
}>;

/**
 * Matches errors thrown when a JavaScript or CSS chunk is missing, or when
 * chunks from different builds are mixed together – both usually the result of
 * a deploy happening while the tab was open.
 */
export const staleChunkErrorPattern =
  /dynamically imported module|module script failed|does(n't| not) provide an export named|Unable to preload CSS/i;

/**
 * Whether an error was caused by loading a stale or missing JavaScript chunk.
 *
 * @param error the error to check.
 * @returns true if the error is a chunk loading failure.
 */
export function isStaleChunkError(error: unknown): boolean {
  const message = error instanceof Error ? error.message : String(error);
  return staleChunkErrorPattern.test(message);
}

/**
 * A promise that carries the status fields React's `use` hook reads, so a
 * fulfilled load can be unwrapped synchronously without suspending.
 */
type TrackedPromise<T> = Promise<T> &
  (
    | React.PendingReactPromise<T>
    | React.FulfilledReactPromise<T>
    | React.RejectedReactPromise<T>
  );

export type LazyComponent<T extends React.ComponentType<any>> = React.FC<
  React.ComponentPropsWithRef<T>
> & {
  /**
   * Starts loading the chunk. Once the returned promise settles, rendering
   * the component no longer suspends.
   */
  preload: () => ComponentPromise<T>;
};

/**
 * Lazy load a component with automatic retry on failure.
 *
 * Unlike `React.lazy`, a component whose chunk has already loaded renders
 * synchronously instead of suspending. This matters because React throttles
 * the reveal of a resolved Suspense boundary for up to 300ms after the fallback
 * was shown, which delays content that is otherwise ready to paint.
 *
 * @param component A function that returns a promise of a component.
 * @param retries The number of retries, defaults to 3.
 * @param interval The interval between retries in milliseconds, defaults to 1000.
 * @returns A lazy component with a `preload` method.
 */
export default function lazyWithRetry<T extends React.ComponentType<any>>(
  component: () => ComponentPromise<T>,
  retries?: number,
  interval?: number
): LazyComponent<T> {
  let promise: TrackedPromise<{ default: T }> | undefined;

  const preload = () => {
    if (!promise) {
      const pending = retry(component, retries, interval) as TrackedPromise<{
        default: T;
      }>;
      pending.status = "pending";
      void pending.then(
        (module) => {
          const fulfilled = pending as React.FulfilledReactPromise<{
            default: T;
          }>;
          fulfilled.status = "fulfilled";
          fulfilled.value = module;
        },
        (reason) => {
          const rejected = pending as React.RejectedReactPromise<{
            default: T;
          }>;
          rejected.status = "rejected";
          rejected.reason = reason;
        }
      );
      promise = pending;
    }
    return promise;
  };

  const Lazy = (props: React.ComponentPropsWithRef<T>) => {
    const { default: Component } = React.use(preload());
    return <Component {...props} />;
  };
  Lazy.displayName = "Lazy";

  return Object.assign(Lazy, { preload });
}

function retry<T extends React.ComponentType<any>>(
  fn: () => ComponentPromise<T>,
  retriesLeft = 3,
  interval = 1000
): ComponentPromise<T> {
  return new Promise((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        setTimeout(() => {
          if (retriesLeft === 1) {
            // Retrying a removed chunk is futile, the only recovery is to
            // reload and fetch a fresh index.html with current chunk URLs.
            if (reloadIfStaleChunk(error)) {
              return;
            }
            reject(error);
            return;
          }
          retry(fn, retriesLeft - 1, interval).then(resolve, reject);
        }, interval);
      });
  });
}

const reloadSessionKey = "chunkReload";

/**
 * Reloads the page when a dynamic import fails because the chunk no longer
 * exists, or does not match the chunks already loaded – typically after a
 * deploy left an open tab referencing stale URLs. Guarded so it only reloads
 * once per session to avoid reload loops.
 *
 * @param error The error thrown by the failed dynamic import.
 * @returns Whether a reload was triggered.
 */
function reloadIfStaleChunk(error: unknown): boolean {
  if (!isStaleChunkError(error)) {
    return false;
  }

  try {
    if (sessionStorage.getItem(reloadSessionKey)) {
      return false;
    }
    sessionStorage.setItem(reloadSessionKey, "1");
  } catch {
    // sessionStorage may be unavailable (e.g. Safari private mode). Without it
    // we can't guard against a reload loop, so fail safe and don't reload.
    return false;
  }

  window.location.reload();
  return true;
}