Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | 14x 14x 14x 14x 18x 1x 17x 17x 3x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 9x 9x 17x 4x 17x 17x 14x | /**
* @module Shared/ErrorBoundary
* @description Centralized error boundary pattern for browser-side dashboard components.
* Prevents individual component failures from breaking the entire page by wrapping
* render functions with error catching, fallback UI, and optional retry logic.
*
* @intelligence Intelligence platform resilience layer — each dashboard panel is isolated
* so a single data-source failure never cascades to the rest of the page. Retry logic
* maximises successful data acquisition from unstable government APIs.
*
* @business Platform reliability — isolated component failures improve perceived
* reliability and reduce support incidents. Automatic retry reduces manual page refreshes
* and keeps users engaged with available data.
*
* @marketing Enterprise readiness signal — graceful degradation and structured error
* handling demonstrate production quality to government and enterprise prospects.
*/
import { logger } from './logger.js';
import { renderErrorFallback, renderLoadingFallback } from './fallback-ui.js';
/**
* Localised labels for the loading and error states produced by
* {@link renderWithFallback}. Both fields are optional; English defaults
* are used when omitted so the API remains backwards-compatible.
*/
export interface RenderWithFallbackOptions {
/** ARIA label announced by screen readers while the skeleton is visible. */
readonly loadingLabel?: string;
/** Text shown on the retry button in the error card. */
readonly retryLabel?: string;
}
/**
* Wrap a synchronous or asynchronous render function with an error boundary.
*
* - Shows a loading skeleton while an async render is in progress.
* - On success the container is left with whatever the render function produced.
* - On failure the container shows an error card with an optional retry button.
* - Each retry re-runs the full renderFn.
*
* @param container - Target DOM element that will receive the rendered output.
* @param renderFn - Function (sync or async) that populates `container`.
* @param fallbackMessage - Human-readable message shown in the error card.
* @param options - Optional localised labels for the loading/error states.
*/
export async function renderWithFallback(
container: HTMLElement,
renderFn: () => void | Promise<void>,
fallbackMessage = 'Data temporarily unavailable',
options: RenderWithFallbackOptions = {},
): Promise<void> {
// Snapshot original markup so retry attempts can restore pre-existing DOM
// elements (e.g. <canvas> elements) that renderFn depends on.
// Note: restore uses innerHTML, so child element references held by callers
// are not preserved across retries — they will point to recreated nodes.
const originalHTML = container.innerHTML;
let inFlight = false;
let isFirstAttempt = true;
const attempt = async (): Promise<void> => {
if (inFlight) {
// Prevent overlapping attempts that could cause race conditions.
return;
}
inFlight = true;
// On retry, restore the original markup so any required child elements
// (e.g. <canvas> targets) are present for the re-render. The first
// attempt skips this to preserve existing DOM element references held
// by the caller.
if (!isFirstAttempt) {
container.innerHTML = originalHTML;
}
isFirstAttempt = false;
// Append a dedicated loading overlay so the skeleton stays visible while
// the async render is in progress without destroying required children.
const loadingOverlay = document.createElement('div');
loadingOverlay.setAttribute('data-error-boundary-loading', 'true');
loadingOverlay.setAttribute('aria-busy', 'true');
loadingOverlay.className = 'error-boundary-loading-overlay';
// Ensure the container provides a positioning context for the overlay.
// Capture the prior inline value so it can be restored in the finally block.
const priorInlinePosition = container.style.position;
const currentPosition = getComputedStyle(container).position;
Eif (currentPosition === '' || currentPosition === 'static') {
container.style.position = 'relative';
}
// Style the overlay to cover the container without affecting layout.
loadingOverlay.style.position = 'absolute';
loadingOverlay.style.top = '0';
loadingOverlay.style.right = '0';
loadingOverlay.style.bottom = '0';
loadingOverlay.style.left = '0';
loadingOverlay.style.display = 'flex';
loadingOverlay.style.alignItems = 'center';
loadingOverlay.style.justifyContent = 'center';
loadingOverlay.style.zIndex = '1';
loadingOverlay.style.pointerEvents = 'none';
renderLoadingFallback(loadingOverlay, options.loadingLabel);
container.appendChild(loadingOverlay);
try {
await Promise.resolve(renderFn());
} catch (err) {
logger.error('[ErrorBoundary] Render failed:', err);
renderErrorFallback(container, fallbackMessage, attempt, options.retryLabel);
} finally {
// Remove the overlay once the attempt finishes (success or failure).
if (loadingOverlay.parentNode === container) {
container.removeChild(loadingOverlay);
}
// Restore the container's original inline position value to avoid
// permanently altering its stacking context after the overlay is gone.
container.style.position = priorInlinePosition;
inFlight = false;
}
};
await attempt();
}
|