|
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
|
} |