import { trpc } from "@/lib/trpc"; import { UNAUTHED_ERR_MSG } from '@shared/const'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { httpBatchLink, TRPCClientError } from "@trpc/client"; import { createRoot } from "react-dom/client"; import React from "react"; import superjson from "superjson"; import App from "./App"; import { GamificationProvider } from "./contexts/GamificationContext"; import { ExportProvider } from "./contexts/ExportContext"; import { ClerkAuthFallback } from "./components/ClerkAuthBridge"; import { ProgressToastProvider } from "./components/ProgressToast"; import "./index.css"; import { preconnect, dnsPrefetch, perfMark, scheduleIdle } from "./lib/microOptimizations"; // P27: Mark app initialization start perfMark('app-init-start'); // Global handler for chunk/module load failures from stale deployments. // When a new version is deployed, old JS chunk filenames no longer exist. // This catches the error globally and reloads the page once to pick up new chunks. window.addEventListener('error', (event) => { const msg = event.message || ''; if ( msg.includes('Failed to fetch dynamically imported module') || msg.includes('Loading chunk') || msg.includes('is not a valid JavaScript MIME type') || msg.includes('Importing a module script failed') ) { const reloadKey = 'chunk-reload-global'; const lastReload = sessionStorage.getItem(reloadKey); const now = Date.now(); if (!lastReload || (now - parseInt(lastReload, 10)) > 30000) { sessionStorage.setItem(reloadKey, String(now)); console.warn('[ChunkReload] Stale chunk detected, reloading...'); window.location.reload(); } } }); // Also catch unhandled promise rejections (dynamic import() returns a promise) window.addEventListener('unhandledrejection', (event) => { const msg = event.reason?.message || String(event.reason) || ''; if ( msg.includes('Failed to fetch dynamically imported module') || msg.includes('Loading chunk') || msg.includes('is not a valid JavaScript MIME type') || msg.includes('Importing a module script failed') ) { const reloadKey = 'chunk-reload-global'; const lastReload = sessionStorage.getItem(reloadKey); const now = Date.now(); if (!lastReload || (now - parseInt(lastReload, 10)) > 30000) { sessionStorage.setItem(reloadKey, String(now)); console.warn('[ChunkReload] Stale chunk detected via promise rejection, reloading...'); window.location.reload(); } } }); // PERFORMANCE OPTIMIZATION: Preconnect to critical origins preconnect('https://financialmodelingprep.com'); preconnect('https://api.manus.im'); dnsPrefetch('https://fonts.googleapis.com'); dnsPrefetch('https://fonts.gstatic.com'); dnsPrefetch('https://api.polygon.io'); // Clerk publishable key from environment const CLERK_PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; // Satellite mode disabled — Clerk satellite requires dashboard configuration // (CNAME records + domain registration in Clerk dashboard) which is not yet complete. // Running in standard mode on all production domains. Users sign in via redirect // to marlowekeynes.org/login and are redirected back after authentication. const currentHost = typeof window !== "undefined" ? window.location.hostname : ""; const isSatelliteDomain = false; // Disabled until Clerk dashboard satellite config is complete // Detect if we're on a dev proxy (Manus dev server, localhost) const DEV_PROXY_PATTERNS = [".manus.computer", "localhost", "127.0.0.1"]; const isDevProxy = DEV_PROXY_PATTERNS.some(p => currentHost.includes(p)); // Clerk disabled — login is hidden for now. Re-enable when ready to activate auth. // const shouldUseClerk = !isDevProxy && !!CLERK_PUBLISHABLE_KEY; const shouldUseClerk = false; // PERFORMANCE OPTIMIZATION: Configure QueryClient with better defaults const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, refetchOnReconnect: false, refetchOnMount: false, // R70: Don't refetch when component remounts (tab switch) retry: 1, staleTime: 1000 * 60 * 15, // R70: 15min global staleTime (was 5min) — data changes slowly gcTime: 1000 * 60 * 60, // R70: 60min garbage collection (was 30min) }, mutations: { retry: 0, }, }, }); // Log errors for debugging but don't auto-redirect queryClient.getQueryCache().subscribe(event => { if (event.type === "updated" && event.action.type === "error") { const error = event.query.state.error; if (error instanceof TRPCClientError && error.message !== UNAUTHED_ERR_MSG) { console.error("[API Query Error]", error); } } }); queryClient.getMutationCache().subscribe(event => { if (event.type === "updated" && event.action.type === "error") { const error = event.mutation.state.error; if (error instanceof TRPCClientError && error.message !== UNAUTHED_ERR_MSG) { console.error("[API Mutation Error]", error); } } }); // PERFORMANCE OPTIMIZATION: Configure tRPC client with batching const trpcClient = trpc.createClient({ links: [ httpBatchLink({ url: "/api/trpc", transformer: superjson, maxURLLength: 2048, fetch(input, init) { return globalThis.fetch(input, { ...(init ?? {}), credentials: "include", }); }, }), ], }); /** * Error boundary that catches Clerk initialization failures and falls back * to rendering the app without Clerk auth. */ class ClerkErrorBoundary extends React.Component< { children: React.ReactNode; fallback: React.ReactNode }, { hasError: boolean } > { constructor(props: { children: React.ReactNode; fallback: React.ReactNode }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error("[ClerkErrorBoundary] Clerk failed, falling back:", error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } // P27: Mark render start perfMark('app-render-start'); const root = createRoot(document.getElementById("root")!); /** * The core app tree WITHOUT any Clerk wrapper. * Used both as the standalone render and as the fallback for Clerk failures. */ function AppTree() { return ( ); } if (shouldUseClerk) { /** * On production domains: dynamically import Clerk SDK and the auth bridge. * This ensures @clerk/clerk-react is NEVER loaded on dev server domains. * * SATELLITE MODE: When on a registered satellite domain, enable isSatellite * so Clerk syncs the session from the primary domain (marlowekeynes.org). * The satellite domain must be registered in Clerk dashboard AND have a * CNAME record: clerk. -> frontend-api.clerk.services * * STANDARD MODE: On unregistered domains, Clerk runs in standard mode. * Users can still sign in via redirect to marlowekeynes.org/login. */ Promise.all([ import("@clerk/clerk-react"), import("./components/ClerkAuthInner"), ]).then(([{ ClerkProvider }, { ClerkAuthInner }]) => { // Build ClerkProvider props based on whether this is a satellite domain const clerkProps: Record = { publishableKey: CLERK_PUBLISHABLE_KEY, signInUrl: "https://marlowekeynes.org/login", afterSignInUrl: window.location.href, afterSignUpUrl: window.location.href, routerPush: (to: string) => window.location.assign(to), routerReplace: (to: string) => window.location.replace(to), }; // Satellite mode disabled — would require: // 1. Register each domain in Clerk dashboard as satellite // 2. Add CNAME: clerk. -> frontend-api.clerk.services // 3. Re-enable isSatelliteDomain detection above // if (isSatelliteDomain) { // clerkProps.isSatellite = true; // clerkProps.domain = "marlowekeynes.org"; // } root.render( }> ); }).catch((err) => { console.error("[Clerk] Failed to load Clerk SDK, falling back:", err); root.render(); }); } else { root.render(); } // P16: Defer non-critical initialization to idle time scheduleIdle(() => { perfMark('app-idle-init'); if (document.fonts?.ready) { document.fonts.ready.then(() => perfMark('fonts-loaded')); } }, 3000);