1/** 2 * API Manager 3 * Handles all API calls with unified error handling and request configuration 4 */ 5class APIManager { 6 constructor(csrfToken) { 7 this.csrfToken = csrfToken; 8 this.defaultHeaders = { 9 'Accept': 'application/json', 10 'X-Requested-With': 'XMLHttpRequest', 11 'X-CSRF-TOKEN': csrfToken 12 }; 13 // Initialize cache for GET requests 14 this.cache = new Map(); 15 this.cacheExpiry = new Map(); 16 } 17 18 // Generic API call method 19 async call(url, options = {}) { 20 try { 21 const config = { 22 headers: { ...this.defaultHeaders, ...options.headers }, 23 ...options 24 }; 25 26 const response = await fetch(url, config); 27 28 if (!response.ok) { 29 const errorData = await this.parseErrorResponse(response); 30 throw new Error(errorData.message || `HTTP error! status: ${response.status}`); 31 } 32 33 const contentType = response.headers.get('content-type'); 34 if (contentType && contentType.includes('application/json')) { 35 return await response.json(); 36 } else { 37 // Handle HTML responses (likely error pages) 38 const text = await response.text(); 39 console.error('Server returned HTML instead of JSON:', text.substring(0, 200)); 40 throw new Error(`Server error: Expected JSON but received HTML. Status: ${response.status}`); 41 } 42 } catch (error) { 43 console.error('API call failed:', error); 44 throw error; 45 } 46 } 47 48 // GET request 49 async get(url, params = {}) { 50 const urlWithParams = this.buildUrlWithParams(url, params); 51 return this.call(urlWithParams, { 52 method: 'GET' 53 }); 54 } 55 56 // POST request 57 async post(url, data = {}) { 58 return this.call(url, { 59 method: 'POST', 60 headers: { 61 'Content-Type': 'application/json', 62 'X-Requested-With': 'XMLHttpRequest' 63 }, 64 body: JSON.stringify(data) 65 }); 66 } 67 68 // PUT request 69 async put(url, data = {}) { 70 return this.call(url, { 71 method: 'PUT', 72 headers: { 73 'Content-Type': 'application/json' 74 }, 75 body: JSON.stringify(data) 76 }); 77 } 78 79 // DELETE request 80 async delete(url) { 81 return this.call(url, { 82 method: 'DELETE' 83 }); 84 } 85 86 // PATCH request 87 async patch(url, data = {}) { 88 return this.call(url, { 89 method: 'PATCH', 90 headers: { 91 'Content-Type': 'application/json' 92 }, 93 body: JSON.stringify(data) 94 }); 95 } 96 97 // Build URL with query parameters 98 buildUrlWithParams(url, params) { 99 if (Object.keys(params).length === 0) { 100 return url; 101 } 102 103 const urlObj = new URL(url, window.location.origin); 104 Object.entries(params).forEach(([key, value]) => { 105 if (value !== null && value !== undefined) { 106 urlObj.searchParams.append(key, value); 107 } 108 }); 109 110 return urlObj.toString(); 111 } 112 113 // Parse error response 114 async parseErrorResponse(response) { 115 try { 116 const contentType = response.headers.get('content-type'); 117 118 if (contentType && contentType.includes('application/json')) { 119 return await response.json(); 120 } else { 121 const text = await response.text(); 122 return { message: text || `HTTP ${response.status}: ${response.statusText}` }; 123 } 124 } catch (parseError) { 125 return { message: `HTTP ${response.status}: ${response.statusText}` }; 126 } 127 } 128 129 // Upload file 130 async uploadFile(url, file, additionalData = {}) { 131 const formData = new FormData(); 132 formData.append('file', file); 133 134 Object.entries(additionalData).forEach(([key, value]) => { 135 formData.append(key, value); 136 }); 137 138 return this.call(url, { 139 method: 'POST', 140 headers: { 141 // Don't set Content-Type for FormData, let browser set it with boundary 142 'X-CSRF-TOKEN': this.csrfToken, 143 'X-Requested-With': 'XMLHttpRequest' 144 }, 145 body: formData 146 }); 147 } 148 149 // Download file 150 async downloadFile(url, filename = null) { 151 try { 152 const response = await fetch(url, { 153 method: 'GET', 154 headers: this.defaultHeaders 155 }); 156 157 if (!response.ok) { 158 throw new Error(`Download failed: ${response.status}`); 159 } 160 161 const blob = await response.blob(); 162 const downloadUrl = window.URL.createObjectURL(blob); 163 164 const link = document.createElement('a'); 165 link.href = downloadUrl; 166 link.download = filename || this.extractFilenameFromResponse(response) || 'download'; 167 168 document.body.appendChild(link); 169 link.click(); 170 document.body.removeChild(link); 171 172 window.URL.revokeObjectURL(downloadUrl); 173 174 return true; 175 } catch (error) { 176 console.error('Download failed:', error); 177 throw error; 178 } 179 } 180 181 // Extract filename from response headers 182 extractFilenameFromResponse(response) { 183 const contentDisposition = response.headers.get('content-disposition'); 184 if (contentDisposition) { 185 const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); 186 if (filenameMatch && filenameMatch[1]) { 187 return filenameMatch[1].replace(/['"]/g, ''); 188 } 189 } 190 return null; 191 } 192 193 // Batch requests 194 async batch(requests) { 195 try { 196 const promises = requests.map(request => { 197 const { url, method = 'GET', data = {}, ...options } = request; 198 199 switch (method.toUpperCase()) { 200 case 'GET': 201 return this.get(url, data); 202 case 'POST': 203 return this.post(url, data); 204 case 'PUT': 205 return this.put(url, data);
206 case 'DELETE': 207 return this.delete(url); 208 case 'PATCH': 209 return this.patch(url, data); 210 default: 211 return this.call(url, { method, ...options, body: JSON.stringify(data) }); 212 } 213 }); 214 215 const results = await Promise.allSettled(promises); 216 217 return results.map((result, index) => ({ 218 success: result.status === 'fulfilled', 219 data: result.status === 'fulfilled' ? result.value : null, 220 error: result.status === 'rejected' ? result.reason : null, 221 originalRequest: requests[index] 222 })); 223 } catch (error) { 224 console.error('Batch request failed:', error); 225 throw error; 226 } 227 } 228 229 // Retry mechanism for failed requests 230 async callWithRetry(url, options = {}, maxRetries = 3, delay = 1000) { 231 let lastError; 232 233 for (let attempt = 1; attempt <= maxRetries; attempt++) { 234 try { 235 return await this.call(url, options); 236 } catch (error) { 237 lastError = error; 238 239 if (attempt === maxRetries) { 240 break; 241 } 242 243 // Check if error is retryable (network errors, 5xx status codes) 244 if (!this.isRetryableError(error)) { 245 break; 246 } 247 248 // Wait before retrying with exponential backoff 249 await this.sleep(delay * Math.pow(2, attempt - 1)); 250 } 251 } 252 253 throw lastError; 254 } 255 256 // Check if error is retryable 257 isRetryableError(error) { 258 // Retry on network errors or 5xx server errors 259 return error.message.includes('NetworkError') || 260 error.message.includes('Failed to fetch') || 261 error.message.includes('HTTP 5'); 262 } 263 264 // Sleep utility 265 sleep(ms) { 266 return new Promise(resolve => setTimeout(resolve, ms)); 267 } 268 269 // Cancel request using AbortController 270 createCancellableRequest(url, options = {}) { 271 const controller = new AbortController(); 272 const signal = controller.signal; 273 274 const requestPromise = this.call(url, { ...options, signal }); 275 276 return { 277 promise: requestPromise, 278 cancel: () => controller.abort(), 279 signal 280 }; 281 } 282 283 // Real-time data fetching with automatic retry 284 async pollData(url, options = {}) { 285 const { 286 interval = 5000, 287 maxRetries = 3, 288 onData = () => {}, 289 onError = () => {}, 290 shouldStop = () => false 291 } = options; 292 293 let retryCount = 0; 294 295 const poll = async () => { 296 try { 297 const data = await this.get(url); 298 retryCount = 0; // Reset retry count on success 299 onData(data); 300 301 if (!shouldStop()) { 302 setTimeout(poll, interval); 303 } 304 } catch (error) { 305 retryCount++; 306 307 if (retryCount <= maxRetries) { 308 console.warn(`Polling failed, retrying (${retryCount}/${maxRetries}):`, error); 309 setTimeout(poll, interval * retryCount); // Exponential backoff 310 } else { 311 console.error('Polling failed after max retries:', error); 312 onError(error); 313 } 314 } 315 }; 316 317 // Start polling 318 poll(); 319 320 // Return stop function 321 return () => { 322 options.shouldStop = () => true; 323 }; 324 } 325 326 // Cache management initialization (moved to main constructor) 327 328 // Cached GET request 329 async getCached(url, params = {}, cacheDuration = 300000) { // 5 minutes default 330 const cacheKey = this.buildUrlWithParams(url, params); 331 const now = Date.now(); 332 333 // Check if we have valid cached data 334 if (this.cache.has(cacheKey)) { 335 const expiry = this.cacheExpiry.get(cacheKey); 336 if (expiry && now < expiry) { 337 return this.cache.get(cacheKey); 338 } else { 339 // Remove expired cache 340 this.cache.delete(cacheKey); 341 this.cacheExpiry.delete(cacheKey); 342 } 343 } 344 345 // Fetch fresh data 346 const data = await this.get(url, params); 347 348 // Cache the result 349 this.cache.set(cacheKey, data); 350 this.cacheExpiry.set(cacheKey, now + cacheDuration); 351 352 return data; 353 } 354 355 // Clear cache 356 clearCache(pattern = null) { 357 if (pattern) { 358 // Clear cache entries matching pattern 359 const regex = new RegExp(pattern); 360 for (const key of this.cache.keys()) { 361 if (regex.test(key)) { 362 this.cache.delete(key); 363 this.cacheExpiry.delete(key); 364 } 365 } 366 } else { 367 // Clear all cache 368 this.cache.clear(); 369 this.cacheExpiry.clear(); 370 } 371 } 372 373 // Get cache stats 374 getCacheStats() { 375 return { 376 size: this.cache.size, 377 keys: Array.from(this.cache.keys()), 378 totalMemory: JSON.stringify(Array.from(this.cache.values())
378).length 379 }; 380 } 381}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.