| 173 | // ============================================ |
| 174 | |
| 175 | class ApiClient { |
| 176 | constructor(baseUrl = '/api') { |
| 177 | this.baseUrl = baseUrl; |
| 178 | this.inflightRequests = new Map(); |
| 179 | this.activeRequestCount = 0; |
| 180 | this.maxConcurrentRequests = 6; |
| 181 | this.requestQueue = []; |
| 182 | this.networkOnline = typeof navigator === 'undefined' ? true : navigator.onLine !== false; |
| 183 | this._networkToastState = { type: '', at: 0 }; |
| 184 | this.defaultTimeoutMs = 20000; |
| 185 | this.defaultRetryCount = 1; |
| 186 | this.defaultRetryDelayMs = 900; |
| 187 | this.setupNetworkListeners(); |
| 188 | } |
| 189 | |
| 190 | getAdaptiveTimeoutMs() { |
| 191 | const connection = navigator?.connection || navigator?.mozConnection || navigator?.webkitConnection; |
| 192 | const effectiveType = String(connection?.effectiveType || '').toLowerCase(); |
| 193 | if (effectiveType === 'slow-2g' || effectiveType === '2g') return 45000; |
| 194 | if (effectiveType === '3g') return 30000; |
| 195 | return this.defaultTimeoutMs; |
| 196 | } |
| 197 | |
| 198 | cleanupInflightRequest(requestKey, controller) { |
| 199 | if (!requestKey) return; |
| 200 | const current = this.inflightRequests.get(requestKey); |
| 201 | if (current === controller) { |
| 202 | this.inflightRequests.delete(requestKey); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | setupNetworkListeners() { |
| 207 | if (typeof window === 'undefined' || !window.addEventListener) return; |
| 208 | window.addEventListener('online', () => { |
| 209 | this.networkOnline = true; |
| 210 | this.notifyNetworkState('网络已恢复', 'success', 2000); |
| 211 | }); |
| 212 | window.addEventListener('offline', () => { |
| 213 | this.networkOnline = false; |
| 214 | this.notifyNetworkState('网络已断开,后台轮询将自动降频', 'warning', 6000); |
| 215 | }); |
| 216 | } |
| 217 | |
| 218 | notifyNetworkState(message, type, throttleMs = 3000) { |
| 219 | const now = Date.now(); |
| 220 | if ( |
| 221 | this._networkToastState.type === type && |
| 222 | now - Number(this._networkToastState.at || 0) < throttleMs |
| 223 | ) { |
| 224 | return; |
| 225 | } |
| 226 | this._networkToastState = { type, at: now }; |
| 227 | if (type === 'warning') { |
| 228 | toast.warning(message, 2500); |
| 229 | return; |
| 230 | } |
| 231 | if (type === 'success') { |
| 232 | toast.success(message, 1800); |
nothing calls this directly
no outgoing calls
no test coverage detected