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);