Skip to content
File

Blob: src/client/main.tsx

typescript163 lines
1import { StrictMode } from "react";
2import { createRoot } from "react-dom/client";
3import { RouterProvider, createRouter } from "@tanstack/react-router";
4import { QueryClientProvider } from "@tanstack/react-query";
5import { refreshSession } from "./lib/api";
6import { getClientConfigErrorSnapshot, getClientConfigSnapshot } from "./lib/client-config";
7import { getSessionBootstrapStrategy, hasOidcMarker, performBootstrapRefresh } from "./lib/session-bootstrap";
8import { queryClient } from "./lib/query-client";
9import { routeTree } from "./route-tree";
10import { primeClientErrorReporting, reportClientError } from "./lib/report-client-error";
11import { isBenignBrowserError } from "./lib/benign-browser-errors";
12import { removeStorageItem } from "./lib/storage";
13import { STORAGE_KEYS } from "./lib/constants";
14import { useAuthStore } from "./stores/auth-store";
15import {
16 installWorkspaceLocalOwnerAutoHydrator,
17 rehydrateWorkspaceLocalOwner,
18 waitForWorkspaceLocalHydration,
19} from "./stores/bootstrap";
20import { registerServiceWorker } from "./lib/pwa";
21import { installManifestGate } from "./lib/install-gate";
22import "@/styles/app.css";
23 
24const router = createRouter({
25 routeTree,
26 defaultPreload: "intent",
27});
28 
29// Route-aware re-hydrate: users who bootstrapped on a non-local path
30// (e.g., `/login` or `/s/$token`) and then navigate into a workspace or
31// shared-with-me route need the projections hydrated without a full reload.
32router.subscribe("onResolved", () => {
33 rehydrateWorkspaceLocalOwner();
34});
35 
36declare module "@tanstack/react-router" {
37 interface Register {
38 router: typeof router;
39 }
40}
41 
42let listenersRegistered = false;
43 
44function registerGlobalErrorListeners() {
45 if (listenersRegistered) {
46 return;
47 }
48 
49 listenersRegistered = true;
50 
51 window.addEventListener("error", (event) => {
52 if (isBenignBrowserError(event)) {
53 event.preventDefault();
54 return;
55 }
56 reportClientError({
57 source: "window.error",
58 error: event.error ?? new Error(event.message),
59 context: {
60 filename: event.filename || null,
61 lineno: event.lineno || null,
62 colno: event.colno || null,
63 },
64 });
65 });
66 
67 window.addEventListener("unhandledrejection", (event) => {
68 reportClientError({
69 source: "window.unhandledrejection",
70 error: event.reason,
71 });
72 });
73}
74 
75async function bootstrap() {
76 const clientConfig = getClientConfigSnapshot();
77 const clientConfigError = getClientConfigErrorSnapshot();
78 
79 void primeClientErrorReporting(clientConfig);
80 if (clientConfigError) {
81 reportClientError({
82 source: "client-config.bootstrap",
83 error: clientConfigError,
84 });
85 }
86 
87 const store = useAuthStore.getState();
88 const bootstrapStrategy = getSessionBootstrapStrategy(
89 window.location.pathname,
90 !!store.user,
91 document.cookie,
92 window.location.search,
93 );
94 
95 if (bootstrapStrategy === "block") {
96 const outcome = await performBootstrapRefresh(hasOidcMarker(window.location.search), {
97 refreshSession,
98 clearAuth: () => useAuthStore.getState().clearAuth(),
99 navigate: (url) => window.location.replace(url),
100 });
101 if (outcome === "redirected") return;
102 }
103 
104 // Evict the orphan v6 zustand persist blob from previous releases.
105 removeStorageItem(STORAGE_KEYS.WORKSPACE);
106 
107 // Hydrate the local workspace replica before route loaders trust cached
108 // data. Anonymous / share / login / invite paths schedule a no-op hydrate.
109 // The auto-hydrator below handles later auth transitions; the router
110 // subscription above re-schedules on route changes. Both paths flow
111 // through `scheduleHydration`, which dedupes by (userId, needsLocal).
112 rehydrateWorkspaceLocalOwner();
113 await waitForWorkspaceLocalHydration();
114 installWorkspaceLocalOwnerAutoHydrator();
115 
116 createRoot(document.getElementById("root")!, {
117 onUncaughtError(error, errorInfo) {
118 reportClientError({
119 source: "react.root-uncaught",
120 error,
121 context: {
122 componentStack: errorInfo.componentStack ?? null,
123 },
124 });
125 },
126 onCaughtError(error, errorInfo) {
127 reportClientError({
128 source: "react.root-caught",
129 error,
130 context: {
131 componentStack: errorInfo.componentStack ?? null,
132 errorBoundary: errorInfo.errorBoundary?.constructor?.name ?? null,
133 },
134 });
135 },
136 onRecoverableError(error, errorInfo) {
137 reportClientError({
138 source: "react.root-recoverable",
139 error,
140 context: {
141 componentStack: errorInfo.componentStack ?? null,
142 },
143 });
144 },
145 }).render(
146 <StrictMode>
147 <QueryClientProvider client={queryClient}>
148 <RouterProvider router={router} />
149 </QueryClientProvider>
150 </StrictMode>,
151 );
152 
153 if (bootstrapStrategy === "background") {
154 void refreshSession();
155 }
156 
157 installManifestGate();
158 registerServiceWorker();
159}
160 
161registerGlobalErrorListeners();
162bootstrap();