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
| Event | When fired | detail fields |
|---|---|---|
rf:retry | Same URL is retried | { url, attempt } |
rf:fallback | Switched to next candidate URL | { from, to, reason? } |
rf:success | A recovered page-side session completes successfully | { url, attempts } |
rf:error | A 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.reasonis an opaqueunknownfailure value from the transport/coordinator. - For SW-bridged events,
reasonmay include resolver giveup reasons such as'rules-exhausted'or'no-match'. - Page
rf:successis published only after a recovery session succeeds; it is not emitted for an initial first-try success. - SW
rf:successis 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 emitsrf:errorfirst and does not add anrf:successevent.
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
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:
<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":
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:
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:
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
| Metric | Source |
|---|---|
| Retry rate | rf:retry count by host |
| Fallback rate | rf:fallback from → to |
| Terminal errors | rf:error count |
| Exhaustion rate | SW-bridged rf:error where reason === 'rules-exhausted' |
| Circuit trips | host 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:
localStorage.__RF_DEBUG__ = '1';
location.reload();