PageSourceSearch

https://app.raba7ni.com/js/api-manager.js

js raba7ni.com collected 2026-10-02 01:41:18 UTC 11,829 bytes, 381 lines download raw bytes

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.