1 import { QueryClient, QueryFunction } from "@tanstack/react-query";
2 import { apiurl } from "./config";
3
4 async function throwIfResNotOk(res: Response) {
5 if (!res.ok) {
6 let message = `${res.status}: ${res.statusText || "Request failed"}`;
7 try {
8 const text = await res.text();
9 try {
10 const parsed = JSON.parse(text);
11 if (parsed && typeof parsed === "object" && parsed.message) {
12 message = parsed.message;
13 } else if (text) {
14 message = text;
15 }
16 } catch {
17 if (text) message = text;
18 }
19 } catch {
20 // ignore
21 }
22 throw new Error(message);
23 }
24 }
25
26 export async function apiRequest(
27 method: string,
28 path: string,
29 data?: unknown | undefined,
30 ): Promise<Response> {
31 const url = apiurl(path);
32 const res = await fetch(url, {
33 method,
34 headers: {
35 ...(data ? { "Content-Type": "application/json" } : {}),
36 'Accept': 'application/json',
37 },
38 body: data ? JSON.stringify(data) : undefined,
39 credentials: "include",
40 keepalive: true, // Performance optimization
41 });
42
43 await throwIfResNotOk(res);
44 return res;
45 }
46
47 type UnauthorizedBehavior = "returnNull" | "throw";
48 export const getQueryFn: <T>(options: {
49 on401: UnauthorizedBehavior;
50 }) => QueryFunction<T> =
51 ({ on401: unauthorizedBehavior }) =>
52 async ({ queryKey }) => {
53 // Join queryKey to path and strip duplicate slashes
54 const segments = queryKey
55 .filter(k => k !== null && k !== undefined && k !== "")
56 .map(k => String(k).replace(/^\/+|\/+$/g, ""));
57 const path = `/${segments.join("/")}`;
58 const url = apiurl(path);
59
60 const res = await fetch(url, {
61 credentials: "include",
62 });
63
64 if (unauthorizedBehavior === "returnNull" && res.status === 401) {
65 return null;
66 }
67
68 await throwIfResNotOk(res);
69 return await res.json();
70 };
71
72 // Performance-optimized query client with long-lived warm caching
73 export const queryClient = new QueryClient({
74 defaultOptions: {
75 queries: {
76 queryFn: getQueryFn({ on401: "throw" }),
77 refetchInterval: false,
78 refetchOnWindowFocus: false,
79 refetchOnReconnect: false,
80 staleTime: 10 * 60 * 1000, // 10 minutes cache duration for instant page switching
81 gcTime: 30 * 60 * 1000, // 30 minutes in memory
82 retry: (failureCount, error: any) => {
83 return failureCount < 1 && !error?.message?.includes('401');
84 },
85 },
86 mutations: {
87 retry: false,
88 gcTime: 5 * 60 * 1000,
89 },
90 },
91 });
92
93 // Memory cleanup function
94 export function cleanupQueryCache() {
95 queryClient.clear();
96 if ('gc' in global) {
97 (global as any).gc();
98 }
99 }
100
101 // Enhanced cache management for React Query
102 export function setupQueryCacheManagement() {
103 const cleanupInterval = setInterval(() => {
104 const cacheSize = queryClient.getQueryCache().getAll().length;
105
106 if (cacheSize > 250) {
107 // Remove only truly stale unobserved queries
108 queryClient.removeQueries({
109 stale: true,
110 type: 'inactive',
111 });
112 }
113 }, 10 * 60 * 1000); // Every 10 minutes
114
115 // Cleanup on page unload
116 const handleUnload = () => {
117 clearInterval(cleanupInterval);
118 cleanupQueryCache();
119 };
120
121 if (typeof window !== 'undefined') {
122 window.addEventListener('beforeunload', handleUnload);
123 }
124
125 return () => {
126 clearInterval(cleanupInterval);
127 if (typeof window !== 'undefined') {
128 window.removeEventListener('beforeunload', handleUnload);
129 }
130 };
131 }
132
133 // Memory usage monitoring for query cache
134 export function getQueryCacheStats() {
135 const cache = queryClient.getQueryCache();
136 const allQueries = cache.getAll();
137
138 const stats = {
139 totalQueries: allQueries.length,
140 activeQueries: allQueries.filter(q => q.getObserversCount() > 0).length,
141 staleQueries: allQueries.filter(q => q.isStale()).length,
142 errorQueries: allQueries.filter(q => q.state.status === 'error').length,
143 estimatedSizeKB: Math.round(allQueries.reduce((size, query) => {
144 try {
145 return size + (JSON.stringify(query.state.data).length / 1024);
146 } catch {
147 return size + 1; // 1KB estimate for non-serializable data
148 }
149 }, 0)),
150 };
151
152 return stats;
153 }