Skip to content

Runtime Events

resource-fallback exposes DOM CustomEvents and optional JS function hooks for monitoring, alerting, and degraded UI. In auto-injected setups, prefer DOM events because function values in build config are dropped during serialization.

Event reference

EventWhen fireddetail fields
rf:retrySame URL is retried{ url, attempt }
rf:fallbackSwitched to next candidate URL{ from, to, reason? }
rf:successA recovered page-side session completes successfully{ url, attempts }
rf:errorA page-side session fails, or an SW error is bridged{ url, reason? }

Event sources

Page-side adapters (Observer, Vite, Webpack, SystemJS) hand failures to the RecoveryCoordinator, and the HookBus emits the DOM events. In Hybrid SW mode, the SW posts events back to the page, and the page runtime re-emits the same rf:* names.

  • For page Coordinator events, ErrorEvent.reason is an opaque unknown failure value from the transport/coordinator.
  • For SW-bridged events, reason may include resolver giveup reasons such as 'rules-exhausted' or 'no-match'.
  • Page rf:success is published only after a recovery session succeeds; it is not emitted for an initial first-try success.
  • SW rf:success is emitted only for a usable network response, including an initial successful fetch and a successful fallback-URL fetch. If the network chain is exhausted and a cached response is returned, the current implementation emits rf:error first and does not add an rf:success event.

DOM listener examples

TypeScript

The package does not augment WindowEventMap, so an rf:* listener parameter is inferred as Event. Cast it to CustomEvent before reading detail, as in the examples below.

Basic logging

ts
window.addEventListener('rf:retry', (e) => {
  console.log('[RF] retry', (e as CustomEvent).detail);
});

window.addEventListener('rf:fallback', (e) => {
  const detail = (e as CustomEvent).detail;
  console.log('[RF] fallback', detail.from, '→', detail.to);
});

window.addEventListener('rf:success', (e) => {
  const detail = (e as CustomEvent).detail;
  console.log('[RF] success', detail.url, 'after', detail.attempts, 'attempts');
});

window.addEventListener('rf:error', (e) => {
  console.error('[RF] error', (e as CustomEvent).detail);
});

Degraded UI for entry failures

rf:error is not entry-only; it can also fire for later page resources or for SW-bridged failures. So do not replace the whole page for every rf:error. If you only want entry failure UI, filter by the known entry resource URL and remove the listener after the app boots:

html
<p id="rf-entry-fallback" hidden>Resources failed to load. Please refresh.</p>
<script>
  (function () {
    var expectedEntry = 'https://cdn.example.com/assets/main.js';

    function onRfError(event) {
      var detail = event.detail || {};
      if (detail.url !== expectedEntry) return;

      var fallback = document.getElementById('rf-entry-fallback');
      if (fallback) fallback.hidden = false;
    }

    window.addEventListener('rf:error', onRfError);

    // Remove this from your app entry after successful boot:
    // window.removeEventListener('rf:error', onRfError);
  })();
</script>

Keep this entry fallback generic. Page-side rf:error.detail.reason is not a stable reason-string contract; if you need proof that fallback actually ran, watch rf:retry / rf:fallback separately.

Detect whether fallback actually ran

When testing non-matching URLs, only count retry or fallback as "intercepted":

ts
const events: Array<{ type: string; detail: unknown }> = [];

['rf:retry', 'rf:fallback', 'rf:success', 'rf:error'].forEach((type) => {
  window.addEventListener(type, (e) => {
    events.push({ type, detail: (e as CustomEvent).detail });
  });
});

function didFallbackRun(since: number) {
  return events.slice(since).some((e) => e.type === 'rf:retry' || e.type === 'rf:fallback');
}

JS function hooks

If you need function hooks, call window.__RF__.install() manually in page code so you can pass live function objects directly:

ts
window.__RF__.install({
  rules: [...],
  hooks: {
    onRetry:    (e) => monitor.send('resource.retry', e),
    onFallback: (e) => monitor.send('resource.fallback', e),
    onSuccess:  (e) => monitor.send('resource.success', e),
    onError:    (e) => monitor.send('resource.error', e),
  },
});

Hook serialization limits

buildInjectedTags() and plugin-generated window.__RF__.install(...) calls always serialize the config before it reaches the page, so function hooks from build config are dropped. externalRuntime externalizes only the runtime IIFE; the automatic install(...) call remains inline, and it does not preserve those functions. For auto-injected setups, use DOM rf:* events instead.

Monitoring integration

Recommended pattern — hook DOM events:

ts
window.addEventListener('rf:retry', (e) => {
  monitor.send('resource.retry', (e as CustomEvent).detail);
});
window.addEventListener('rf:fallback', (e) => {
  monitor.send('resource.fallback', (e as CustomEvent).detail);
});
window.addEventListener('rf:error', (e) => {
  monitor.send('resource.error', (e as CustomEvent).detail);
});

Dashboard suggestions

MetricSource
Retry raterf:retry count by host
Fallback raterf:fallback fromto
Terminal errorsrf:error count
Exhaustion rateSW-bridged rf:error where reason === 'rules-exhausted'
Circuit tripshost skipped in fallback chain (via logging + circuit state)

Hybrid SW events

SW events are bridged to the same rf:* events on the page that triggered the fetch (clientId). Rare requests without clientId fall back to window broadcast.

Reason strings such as rules-exhausted / no-match should stay explicitly scoped to those SW-bridged resolver events, not to page-side rf:error as a general API contract.

HookBus and adapter relationship

Debug mode

Set debug: 'auto' (default) and enable at runtime:

js
localStorage.__RF_DEBUG__ = '1';
location.reload();