fix: 🐛 Gerber demo: survive a stale cross-origin-isolation state
The demo showed "Live demo unavailable in this browser" on fully capable browsers (e.g. Chrome) whenever a page load wasn't cross-origin isolated — a soft SPA navigation, or a visit cached from before the COOP/COEP headers went live. The old reload guard used a sticky sessionStorage flag that, once set during a non-isolated load, never cleared, so it skipped the recovery reload and showed a misleading "unsupported browser" message. - Reload guard now uses a timestamp, not a sticky flag: a stale flag from an earlier visit no longer permanently strands the demo (reload loop still prevented within a 10s debounce). - Non-isolated pages now open the standalone viewer (/gerber-demo/, isolated on its own and always works) in a new tab instead of dead-ending. - The "needs a recent browser" note only shows when provably incapable (isolated but missing SharedArrayBuffer/WebGL2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b438b48b2
commit
429ac206f9
1 changed files with 41 additions and 28 deletions
|
|
@ -56,6 +56,7 @@ const caption =
|
||||||
<script>
|
<script>
|
||||||
const BOOT_SRC = "/gerber-demo/index.html";
|
const BOOT_SRC = "/gerber-demo/index.html";
|
||||||
const RELOAD_KEY = "gerber-demo-coi-reload";
|
const RELOAD_KEY = "gerber-demo-coi-reload";
|
||||||
|
const RELOAD_DEBOUNCE_MS = 10000;
|
||||||
|
|
||||||
function hasWebGL2(): boolean {
|
function hasWebGL2(): boolean {
|
||||||
try {
|
try {
|
||||||
|
|
@ -65,7 +66,8 @@ const caption =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function launch(root: HTMLElement): void {
|
// Best path: embed the viewer inline (only when this page is cross-origin isolated).
|
||||||
|
function launchInline(root: HTMLElement): void {
|
||||||
if (root.dataset.state === "loading" || root.dataset.state === "ready") return;
|
if (root.dataset.state === "loading" || root.dataset.state === "ready") return;
|
||||||
root.dataset.state = "loading";
|
root.dataset.state = "loading";
|
||||||
|
|
||||||
|
|
@ -95,54 +97,65 @@ const caption =
|
||||||
frame?.appendChild(iframe);
|
frame?.appendChild(iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFallback(root: HTMLElement): void {
|
// Genuinely incapable browser: cross-origin isolated, yet no SharedArrayBuffer
|
||||||
|
// or WebGL2. The only true dead-end — show the poster + the "recent browser"
|
||||||
|
// note. (We can ONLY assert this when isolated; on a non-isolated page
|
||||||
|
// SharedArrayBuffer is always undefined regardless of the browser's support.)
|
||||||
|
function showUnsupported(root: HTMLElement): void {
|
||||||
root.dataset.state = "unsupported";
|
root.dataset.state = "unsupported";
|
||||||
const launchEl = root.querySelector<HTMLElement>(".gerber-demo__launch");
|
const launchEl = root.querySelector<HTMLElement>(".gerber-demo__launch");
|
||||||
const fallback = root.querySelector<HTMLElement>(".gerber-demo__fallback");
|
|
||||||
// Keep the poster (it's the <a> background); hide the play affordance, show note.
|
|
||||||
launchEl?.querySelector(".gerber-demo__play")?.setAttribute("hidden", "");
|
launchEl?.querySelector(".gerber-demo__play")?.setAttribute("hidden", "");
|
||||||
launchEl?.querySelector(".gerber-demo__cta")?.setAttribute("hidden", "");
|
launchEl?.querySelector(".gerber-demo__cta")?.setAttribute("hidden", "");
|
||||||
launchEl?.querySelector(".gerber-demo__sub")?.setAttribute("hidden", "");
|
launchEl?.querySelector(".gerber-demo__sub")?.setAttribute("hidden", "");
|
||||||
if (launchEl) {
|
if (launchEl) {
|
||||||
launchEl.removeAttribute("href"); // not clickable
|
launchEl.removeAttribute("href");
|
||||||
launchEl.style.cursor = "default";
|
launchEl.style.cursor = "default";
|
||||||
}
|
}
|
||||||
|
const fallback = root.querySelector<HTMLElement>(".gerber-demo__fallback");
|
||||||
if (fallback) fallback.hidden = false;
|
if (fallback) fallback.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initDemo(): void {
|
function initDemo(): void {
|
||||||
const root = document.querySelector<HTMLElement>(".gerber-demo");
|
const root = document.querySelector<HTMLElement>(".gerber-demo");
|
||||||
if (!root) return; // page without the demo
|
if (!root) return; // page without the demo
|
||||||
|
const launchEl = root.querySelector<HTMLAnchorElement>(".gerber-demo__launch");
|
||||||
|
|
||||||
const isolated = window.crossOriginIsolated === true;
|
if (window.crossOriginIsolated === true) {
|
||||||
|
|
||||||
// Soft SPA navigation lands here NOT cross-origin isolated; force one real
|
|
||||||
// load so the route's COOP/COEP headers apply. Guard against reload loops.
|
|
||||||
if (!isolated) {
|
|
||||||
if (!sessionStorage.getItem(RELOAD_KEY)) {
|
|
||||||
sessionStorage.setItem(RELOAD_KEY, "1");
|
|
||||||
location.reload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Already retried and still not isolated → genuinely unsupported (or no
|
|
||||||
// headers, e.g. a preview without vercel.json). Fall back.
|
|
||||||
} else {
|
|
||||||
sessionStorage.removeItem(RELOAD_KEY);
|
sessionStorage.removeItem(RELOAD_KEY);
|
||||||
}
|
if (typeof SharedArrayBuffer !== "undefined" && hasWebGL2()) {
|
||||||
|
root.dataset.state = "idle";
|
||||||
const supported =
|
launchEl?.addEventListener("click", (e) => {
|
||||||
isolated && typeof SharedArrayBuffer !== "undefined" && hasWebGL2();
|
e.preventDefault(); // intercept the href; embed inline instead
|
||||||
|
launchInline(root);
|
||||||
if (!supported) {
|
});
|
||||||
showFallback(root);
|
} else {
|
||||||
|
showUnsupported(root);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchEl = root.querySelector<HTMLElement>(".gerber-demo__launch");
|
// NOT cross-origin isolated — usually because a soft SPA navigation delivered
|
||||||
launchEl?.addEventListener("click", (e) => {
|
// this page (the route's COOP/COEP only take effect on a real load). Reload
|
||||||
e.preventDefault(); // intercept the href; embed inline instead
|
// ONCE to get the isolated version so we can embed inline. Guard with a
|
||||||
launch(root);
|
// TIMESTAMP, not a sticky flag: a stale flag from an earlier visit must not
|
||||||
});
|
// permanently strand the demo, but two reloads within the debounce window
|
||||||
|
// would be a loop — so we only suppress a reload we *just* did.
|
||||||
|
const last = Number(sessionStorage.getItem(RELOAD_KEY) || "0");
|
||||||
|
if (Date.now() - last > RELOAD_DEBOUNCE_MS) {
|
||||||
|
sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reloaded moments ago and STILL not isolated (stale cache, missing headers,
|
||||||
|
// a preview without vercel.json…). Don't dead-end and don't blame the
|
||||||
|
// browser: the standalone viewer page is isolated on its own, so let the
|
||||||
|
// button open it in a new tab — capable browsers get the full viewer there.
|
||||||
|
if (launchEl) {
|
||||||
|
launchEl.target = "_blank";
|
||||||
|
launchEl.rel = "noopener";
|
||||||
|
root.dataset.state = "external";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run on first load and on every SPA navigation (ClientRouter).
|
// Run on first load and on every SPA navigation (ClientRouter).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue