diff --git a/apps/client-standalone/src/main.ts b/apps/client-standalone/src/main.ts index 41367aead6..22c7eb2799 100644 --- a/apps/client-standalone/src/main.ts +++ b/apps/client-standalone/src/main.ts @@ -1,31 +1,125 @@ import { attachServiceWorkerBridge, startLocalServerWorker } from "./local-bridge.js"; +async function waitForServiceWorkerControl(): Promise { + if (!("serviceWorker" in navigator)) { + throw new Error("Service Worker not supported in this browser"); + } + + // If already controlling, we're good + if (navigator.serviceWorker.controller) { + console.log("[Bootstrap] Service worker already controlling"); + return; + } + + console.log("[Bootstrap] Waiting for service worker to take control..."); + + // Register service worker + const registration = await navigator.serviceWorker.register("./sw.js", { scope: "/" }); + + // Wait for it to be ready (installed + activated) + await navigator.serviceWorker.ready; + + // Check if we're now controlling + if (navigator.serviceWorker.controller) { + console.log("[Bootstrap] Service worker now controlling"); + return; + } + + // If not controlling yet, we need to reload the page for SW to take control + // This is standard PWA behavior on first install + console.log("[Bootstrap] Service worker installed but not controlling yet - reloading page"); + + // Wait a tiny bit for SW to fully activate + await new Promise(resolve => setTimeout(resolve, 100)); + + // Reload to let SW take control + window.location.reload(); + + // Throw to stop execution (page will reload) + throw new Error("Reloading for service worker activation"); +} + +async function fetchWithRetry(url: string, maxRetries = 3, delayMs = 500): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + console.log(`[Bootstrap] Fetching ${url} (attempt ${attempt + 1}/${maxRetries})`); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // Check if response has content + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + throw new Error(`Invalid content-type: ${contentType || "none"}`); + } + + return response; + } catch (err) { + lastError = err as Error; + console.warn(`[Bootstrap] Fetch attempt ${attempt + 1} failed:`, err); + + if (attempt < maxRetries - 1) { + // Exponential backoff + const delay = delayMs * Math.pow(2, attempt); + console.log(`[Bootstrap] Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + throw new Error(`Failed to fetch ${url} after ${maxRetries} attempts: ${lastError?.message}`); +} + async function bootstrap() { /* fixes https://github.com/webpack/webpack/issues/10035 */ window.global = globalThis; - // 1) Start local worker ASAP (so /bootstrap is fast) - startLocalServerWorker(); + try { + // 1) Start local worker ASAP (so /bootstrap is fast) + startLocalServerWorker(); - // 2) Bridge SW -> local worker - attachServiceWorkerBridge(); + // 2) Bridge SW -> local worker + attachServiceWorkerBridge(); - // 3) Register SW - if ("serviceWorker" in navigator) { - const reg = await navigator.serviceWorker.register("./sw.js", { scope: "/" }); - // Optionally wait for activation - await navigator.serviceWorker.ready; + // 3) Wait for service worker to control the page (may reload on first install) + await waitForServiceWorkerControl(); + + // 4) Now fetch bootstrap - SW is guaranteed to intercept this + await setupGlob(); + + loadStylesheets(); + loadIcons(); + setBodyAttributes(); + await loadScripts(); + } catch (err) { + // If error is from reload, it will stop here (page reloads) + // Otherwise, show error to user + if (err instanceof Error && err.message.includes("Reloading")) { + // Page is reloading, do nothing + return; + } + + console.error("[Bootstrap] Fatal error:", err); + document.body.innerHTML = ` +
+

Failed to Initialize

+

The application failed to start. Please check the browser console for details.

+
${err instanceof Error ? err.message : String(err)}
+ +
+ `; + document.body.style.display = "block"; } - - await setupGlob(); - loadStylesheets(); - loadIcons(); - setBodyAttributes(); - await loadScripts(); } async function setupGlob() { - const response = await fetch("/bootstrap"); + const response = await fetchWithRetry("/bootstrap"); console.log("Service worker state", navigator.serviceWorker.controller); console.log("Resp", response); const json = await response.json(); diff --git a/apps/client-standalone/src/vite-env.d.ts b/apps/client-standalone/src/vite-env.d.ts index 28ae7076ee..3e6ffaec19 100644 --- a/apps/client-standalone/src/vite-env.d.ts +++ b/apps/client-standalone/src/vite-env.d.ts @@ -7,3 +7,25 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv } + +interface Window { + glob: { + assetPath: string; + themeCssUrl?: string; + themeUseNextAsBase?: string; + iconPackCss: string; + device: string; + headingStyle: string; + layoutOrientation: string; + platform: string; + isElectron: boolean; + hasNativeTitleBar: boolean; + hasBackgroundEffects: boolean; + currentLocale: { + id: string; + rtl: boolean; + }; + activeDialog: any; + }; + global: typeof globalThis; +}