文件
tennis-training-hub/client/src/main.tsx
2026-03-15 02:57:44 +08:00

128 行
4.0 KiB
TypeScript

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 superjson from "superjson";
import App from "./App";
import { getLoginUrl } from "./const";
import "./index.css";
const queryClient = new QueryClient();
const ASSET_REFRESH_KEY = "asset-recovery-reloaded";
const redirectToLoginIfUnauthorized = (error: unknown) => {
if (!(error instanceof TRPCClientError)) return;
if (typeof window === "undefined") return;
const isUnauthorized = error.message === UNAUTHED_ERR_MSG;
if (!isUnauthorized) return;
window.location.href = getLoginUrl();
};
function reloadForStaleAsset(reason: string) {
if (typeof window === "undefined") return;
const alreadyReloaded = window.sessionStorage.getItem(ASSET_REFRESH_KEY) === "1";
if (alreadyReloaded) {
console.error("[Asset Recovery] stale asset still failing after reload", reason);
return;
}
window.sessionStorage.setItem(ASSET_REFRESH_KEY, "1");
console.warn("[Asset Recovery] reloading page due to stale asset failure:", reason);
window.location.reload();
}
function clearAssetRecoveryFlag() {
if (typeof window === "undefined") return;
window.sessionStorage.removeItem(ASSET_REFRESH_KEY);
}
if (typeof window !== "undefined") {
window.addEventListener("load", () => {
clearAssetRecoveryFlag();
}, { once: true });
window.addEventListener("vite:preloadError", (event) => {
const customEvent = event as Event & { payload?: unknown; preventDefault: () => void };
customEvent.preventDefault();
reloadForStaleAsset(String(customEvent.payload ?? "vite preload error"));
});
window.addEventListener("error", (event) => {
const target = event.target;
if (!(target instanceof HTMLLinkElement || target instanceof HTMLScriptElement)) {
return;
}
const assetUrl = target instanceof HTMLLinkElement ? target.href : target.src;
if (assetUrl.includes("/assets/")) {
reloadForStaleAsset(assetUrl);
}
}, true);
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason instanceof Error ? event.reason.message : String(event.reason ?? "");
if (
reason.includes("Failed to fetch dynamically imported module") ||
reason.includes("Importing a module script failed") ||
reason.includes("Unable to preload CSS")
) {
reloadForStaleAsset(reason);
}
});
}
queryClient.getQueryCache().subscribe(event => {
if (event.type === "updated" && event.action.type === "error") {
const error = event.query.state.error;
redirectToLoginIfUnauthorized(error);
console.error("[API Query Error]", error);
}
});
queryClient.getMutationCache().subscribe(event => {
if (event.type === "updated" && event.action.type === "error") {
const error = event.mutation.state.error;
redirectToLoginIfUnauthorized(error);
console.error("[API Mutation Error]", error);
}
});
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: "/api/trpc",
transformer: superjson,
fetch(input, init) {
return globalThis.fetch(input, {
...(init ?? {}),
credentials: "include",
});
},
}),
],
});
const analyticsEndpoint = import.meta.env.VITE_ANALYTICS_ENDPOINT;
const analyticsWebsiteId = import.meta.env.VITE_ANALYTICS_WEBSITE_ID;
if (analyticsEndpoint && analyticsWebsiteId && typeof document !== "undefined") {
const script = document.createElement("script");
script.defer = true;
script.src = `${analyticsEndpoint.replace(/\/$/, "")}/umami`;
script.dataset.websiteId = analyticsWebsiteId;
document.head.appendChild(script);
}
createRoot(document.getElementById("root")!).render(
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</trpc.Provider>
);