federation-resilience is a vanilla core with thin, optional adapters. The
core has zero framework dependencies; adapters add ergonomics only — never new
resilience logic.
@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.
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 |
requestIdleCallback → setTimeout |
SSR / non-browser / tests. |
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).
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 rendersonError(error)instead of crashing.lazyRemote(remote, options)— aReact.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>;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.