Skip to content

Latest commit

 

History

History
97 lines (77 loc) · 3.86 KB

File metadata and controls

97 lines (77 loc) · 3.86 KB

Adapter guide

federation-resilience is a vanilla core with thin, optional adapters. The core has zero framework dependencies; adapters add ergonomics only — never new resilience logic.

Architecture

@module-federation/enhanced/runtime   (peer dep — confirmed v2.5.1)
        │  loadRemote<T>(id) => Promise<T | null>
        ▼
src/adapters/vanilla.ts ── default MF-backed LoadFn + cache-bust runtime plugin
        │
        ▼
src/core/resilient-loader.ts ── attempts · backoff · fallback · telemetry
        ├── core/backoff.ts        exponential schedule + jitter
        ├── core/cache-bust.ts     unique token + applyCacheBust(url)
        ├── core/fallback.ts       deterministic pinned fallback
        ├── core/prefetch.ts       idle warm, non-interfering
        └── telemetry/hooks.ts     5 safe lifecycle hooks
        ▲
src/adapters/react.tsx ── <ResilientRemote>, useResilientRemote(), lazyRemote()

All public types come from the single canonical module src/types.ts.

The injectable seams

Every behaviour is overridable, which is what makes the core testable with zero network and adaptable to any host:

Seam Default Override when
load MF loadRemote(id) + cache-bust plugin You have a custom remote transport, or you're not on MF at all.
sleep setTimeout promise Tests (instant) / a virtual clock.
random Math.random Reproducible jitter (seeded PRNG).
mintCacheBust counter + random Custom token format.
requestIdle requestIdleCallbacksetTimeout SSR / non-browser / tests.

Cache-busting: how the default adapter wires it

MF2's top-level loadRemote(id) takes a remote id, not a URL, so we can't append a query param at the call site. Instead the vanilla adapter registers one MF runtime plugin (idempotently) whose afterResolve hook rewrites the resolved remoteInfo.entry URL with the current attempt's token via applyCacheBust. The token is generated by the core before every retry and is also surfaced on LoadContext.cacheBust, so a custom load can apply it itself:

import { applyCacheBust } from "federation-resilience";

const load = async (id, ctx) => {
  const url = ctx.cacheBust ? applyCacheBust(myEntryUrl(id), ctx.cacheBust) : myEntryUrl(id);
  return import(/* @vite-ignore */ url).then((m) => m.get(expose));
};

If your MF version rejects plugin registration, the adapter degrades gracefully: retries still happen; only the URL rewrite is skipped (and your custom load can still use the token).

React adapter

Three entry points, all built on loadResilientRemote:

  • useResilientRemote(remote, options) — a { status, module, error } state machine that never throws during render. Cancels stale loads on unmount/remote change.
  • <ResilientRemote remote fallback loading onError render/children /> — a declarative boundary; the give-up case renders onError(error) instead of crashing.
  • lazyRemote(remote, options) — a React.lazy-compatible component for <Suspense> users. Resilience (retry/backoff/cache-bust/fallback) happens inside the lazy factory, so a flaky remote no longer rejects the Suspense boundary on the first failure. Pair with any error boundary for the give-up case.
import { Suspense } from "react";
import { lazyRemote } from "federation-resilience/react";

const Cart = lazyRemote<{ default: React.ComponentType }>("checkout/Cart", {
  fallback: "checkout-stable/Cart",
});

<Suspense fallback={<Spinner />}>
  <Cart />
</Suspense>;

Non-React hosts

Vue, Angular, Svelte, Solid, Qwik, and bare ESM all use the same two functions — loadResilientRemote and prefetchFallback. See the README integration guide for copy-paste snippets. Because the core is framework-free, there is nothing framework-specific to install.