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 const text = (await res.text()) || res.statusText;
7 throw new Error(`${res.status}: ${text}`);
8 }
9 }
10
11 export async function apiRequest(
12 method: string,
13 path: string,
14 data?: unknown | undefined,
15 ): Promise<Response> {
16 const url = apiurl(path);
17 const res = await fetch(url, {
18 method,
19 headers: {
20 ...(data ? { "Content-Type": "application/json" } : {}),
21 'Accept': 'application/json',
22 },
23 body: data ? JSON.stringify(data) : undefined,
24 credentials: "include",
25 keepalive: true, // Performance optimization
26 });
27
28 await throwIfResNotOk(res);
29 return res;
30 }
31
32 type UnauthorizedBehavior = "returnNull" | "throw";
33 export const getQueryFn: <T>(options: {
34 on401: UnauthorizedBehavior;
35 }) => QueryFunction<T> =
36 ({ on401: unauthorizedBehavior }) =>
37 async ({ queryKey }) => {
38 // Join queryKey to path and prepend configured base URL
39 const path = `/${queryKey.join("/")}`;
40 const url = apiurl(path);
41
42 const res = await fetch(url, {
43 credentials: "include",
44 });
45
46 if (unauthorizedBehavior === "returnNull" && res.status === 401) {
47 return null;
48 }
49
50 await throwIfResNotOk(res);
51 return await res.json();
52 };
53
54 // Memory-optimized query client with size limits and better cleanup
55 export const queryClient = new QueryClient({
56 defaultOptions: {
57 queries: {
58 queryFn: getQueryFn({ on401: "throw" }),
59 refetchInterval: false,
60 refetchOnWindowFocus: false,
61 staleTime: 3 * 60 * 1000, // 3 minutes for better memory management
62 gcTime: 5 * 60 * 1000, // 5 minutes garbage collection (reduced from 10)
63 retry: (failureCount, error: any) => {
64 // Reduce retries for faster error handling
65 return failureCount < 1 && !error?.message?.includes('401');
66 },
67 },
68 mutations: {
69 retry: false,
70 gcTime: 2 * 60 * 1000, // 2 minutes for mutations
71 },
72 },
73 });
74
75 // Memory cleanup function
76 export function cleanupQueryCache() {
77 queryClient.clear();
78 // Force garbage collection if available
79 if ('gc' in global) {
80 (global as any).gc();
81 }
82 }
83
84 // Enhanced memory management for React Query
85 export function setupQueryCacheManagement() {
86 // Periodic cleanup to prevent memory buildup
87 const cleanupInterval = setInterval(() => {
88 const cacheSize = queryClient.getQueryCache().getAll().length;
89
90 if (cacheSize > 100) {
91 console.log(`Query cache cleanup: ${cacheSize} entries`);
92
93 // Remove queries that are stale and haven't been accessed recently
94 queryClient.removeQueries({
95 stale: true,
96 exact: false,
97 });
98
99 // If still too many, remove oldest inactive queries
100 const remainingSize = queryClient.getQueryCache().getAll().length;
101 if (remainingSize > 75) {
102 const allQueries = queryClient.getQueryCache().getAll();
103 const inactiveQueries = allQueries
104 .filter(query => query.getObserversCount() === 0)
105 .sort((a, b) => (a.state.dataUpdatedAt || 0) - (b.state.dataUpdatedAt || 0))
106 .slice(0, Math.floor(remainingSize * 0.3)); // Remove oldest 30%
107
108 inactiveQueries.forEach(query => {
109 queryClient.removeQueries({ queryKey: query.queryKey, exact: true });
110 });
111 }
112 }
113 }, 3 * 60 * 1000); // Every 3 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 }