From 7d0a7514ade0438a09fa7551f38be50d863dc3f1 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Sat, 14 Feb 2026 11:26:08 +0000 Subject: [PATCH] Added retry support to the HtmlIsland --- src/components/common/HtmlIsland.tsx | 98 ++++++++++++++++++---------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/src/components/common/HtmlIsland.tsx b/src/components/common/HtmlIsland.tsx index ce1d21a..1bb69a4 100644 --- a/src/components/common/HtmlIsland.tsx +++ b/src/components/common/HtmlIsland.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef, useCallback } from "react"; import { toast } from "react-toastify"; import { useTranslation } from "react-i18next"; import { Namespaces } from "../../i18n/i18n"; +import ErrorBlock from "./ErrorBlock"; export interface HtmlIslandProps { url: string; @@ -20,29 +21,62 @@ const HtmlIsland: React.FC = ({ const { t: tIsland } = useTranslation(Namespaces.HtmlIsland); // - // Load the island HTML + // Retry-capable fetch helper // - const loadIsland = useCallback(() => { - setError(null); - - fetch(url, { credentials: "include" }) - .then((r) => { + const fetchWithRetry = useCallback( + async ( + attempt: number, + maxAttempts: number, + delayMs: number, + ): Promise => { + try { + const r = await fetch(url, { credentials: "include" }); if (!r.ok) throw new Error(`Failed to load island: ${r.status}`); - return r.text(); - }) - .then((text) => setHtml(text)) - .catch(() => { - setError(tIsland("island.loadError")); - toast.error(tIsland("island.loadError")); - }); - }, [url, tIsland]); + return await r.text(); + } catch (err) { + if (attempt >= maxAttempts) { + throw err; + } + + const jitter = Math.random() * 150; + const nextDelay = delayMs * Math.pow(2, attempt) + jitter; + + await new Promise((resolve) => setTimeout(resolve, nextDelay)); + + return fetchWithRetry(attempt + 1, maxAttempts, delayMs); + } + }, + [url], + ); // - // Initial load + reload when URL changes + // Initial load + reload when URL changes (with retry + cancellation) // useEffect(() => { - loadIsland(); - }, [loadIsland]); + let cancelled = false; + + const load = async () => { + setError(null); + + try { + const text = await fetchWithRetry(0, 4, 300); + if (!cancelled) { + setHtml(text); + } + } catch { + if (!cancelled) { + setError(tIsland("island.loadError")); + toast.error(tIsland("island.loadError")); + } + } + }; + + load(); + + return () => { + cancelled = true; + }; + }, [url, tIsland, fetchWithRetry]); // // After HTML is injected, run scripts + bind form handling @@ -98,8 +132,7 @@ const HtmlIsland: React.FC = ({ form.getAttribute("data-full-page-redirect") === "true"; if (shouldFullPageRedirect) { - // Do NOT intercept this form – let the browser handle POST + redirect - return; + return; // allow normal POST + redirect } const submitHandler = async (e: Event) => { @@ -131,7 +164,14 @@ const HtmlIsland: React.FC = ({ toast.success( successMessage ? successMessage : tIsland("island.saveSuccess"), ); - loadIsland(); + // reload island + try { + const text = await fetchWithRetry(0, 4, 300); + setHtml(text); + } catch { + setError(tIsland("island.loadError")); + toast.error(tIsland("island.loadError")); + } return; } @@ -150,28 +190,14 @@ const HtmlIsland: React.FC = ({ form.addEventListener("submit", submitHandler); - // Cleanup on re-render return () => { form.removeEventListener("submit", submitHandler); }; - }, [html, islandId, successMessage, tIsland, loadIsland]); + }, [html, islandId, successMessage, tIsland, fetchWithRetry]); return (
- {error && ( -
- {error} -
- )} +