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