UNPKG

@izzyjs/route

Version:

Use your AdonisJs routes in your Inertia.js application

241 lines (240 loc) 10.1 kB
// Function to get the XSRF-TOKEN cookie const getCookie = (name) => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop()?.split(';').shift() || null; return null; }; export class HttpClient { config; constructor(config = {}) { this.config = { timeout: 30000, credentials: 'same-origin', retries: 0, retryDelay: 1000, validateStatus: (status) => status >= 200 && status < 300, debug: false, logRequests: false, ...config, }; } getBodyType(body) { if (body instanceof FormData) { // Don't set Content-Type for FormData - browser will set it with boundary return null; } if (body instanceof File) { return body.type || 'application/octet-stream'; } if (body instanceof Blob) { return body.type || 'application/octet-stream'; } if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { return 'application/octet-stream'; } if (typeof body === 'string') { // Check if it's already JSON try { JSON.parse(body); return 'application/json'; } catch { return 'text/plain'; } } if (typeof body === 'object' && body !== null) { return 'application/json'; } // Default fallback return 'application/json'; } async request(url, options = {}) { let attempt = 0; const maxAttempts = (this.config.retries || 0) + 1; while (attempt < maxAttempts) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { // Apply request interceptor let requestConfig = { ...options, url }; if (this.config.requestInterceptor) { requestConfig = await this.config.requestInterceptor(requestConfig); url = requestConfig.url; } // Add CSRF token const token = getCookie('XSRF-TOKEN'); const headers = new Headers(requestConfig.headers); if (token) { headers.set('X-XSRF-TOKEN', token); } // Set default headers if (this.config.headers) { Object.entries(this.config.headers).forEach(([key, value]) => { headers.set(key, value); }); } // Auto-detect Content-Type based on body type if (requestConfig.body && !headers.has('Content-Type')) { const contentType = this.getBodyType(requestConfig.body); if (contentType) { headers.set('Content-Type', contentType); } } // Transform request data let body = requestConfig.body; if (body && this.config.transformRequest) { body = this.config.transformRequest(typeof body === 'string' ? JSON.parse(body) : body); body = typeof body === 'object' ? JSON.stringify(body) : body; } // Log request if enabled if (this.config.logRequests || this.config.debug) { console.log(`[HTTP] ${requestConfig.method || 'GET'} ${url}`, { body: body ? (typeof body === 'string' ? JSON.parse(body) : body) : undefined, }); } const response = await fetch(url, { ...requestConfig, headers, body, credentials: this.config.credentials, signal: controller.signal, }); clearTimeout(timeoutId); // Apply response interceptor let processedResponse = response; if (this.config.responseInterceptor) { processedResponse = await this.config.responseInterceptor(response.clone()); } // Validate status const isValidStatus = this.config.validateStatus ? this.config.validateStatus(processedResponse.status) : processedResponse.status >= 200 && processedResponse.status < 300; // Handle specific status codes if (processedResponse.status === 401) { console.warn('Token expired or unauthorized'); } let data; const contentType = processedResponse.headers.get('content-type'); if (contentType?.includes('application/json')) { data = await processedResponse.json(); } else if (contentType?.includes('text/')) { data = (await processedResponse.text()); } else { data = (await processedResponse.blob()); } // Transform response data if (data && this.config.transformResponse) { data = this.config.transformResponse(data); } // If status is invalid, treat as error for retry logic if (!isValidStatus) { const error = new Error(`HTTP Error ${processedResponse.status}`); error.response = { data, status: processedResponse.status, headers: processedResponse.headers, }; error.status = processedResponse.status; throw error; } return { data, status: processedResponse.status }; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { const timeoutError = new Error('Request timeout'); timeoutError.isTimeout = true; throw timeoutError; } throw error; } } catch (error) { attempt++; // Apply error interceptor if (this.config.errorInterceptor) { try { const interceptorResult = await this.config.errorInterceptor(error); // If interceptor returns data, treat as successful response if (interceptorResult && typeof interceptorResult === 'object' && 'data' in interceptorResult) { return interceptorResult; } } catch { // Error interceptor failed, continue with original error } } // Check if we should retry const shouldRetry = attempt < maxAttempts && (this.config.retryCondition ? this.config.retryCondition(error, attempt) : this.isRetryableError(error)); if (!shouldRetry) { if (this.config.debug) { console.error(`[HTTP] Request failed after ${attempt} attempts:`, error); } throw error; } // Wait before retrying if (this.config.retryDelay && attempt < maxAttempts) { await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay)); } if (this.config.debug) { console.warn(`[HTTP] Retrying request (attempt ${attempt}/${maxAttempts})`); } } } throw new Error('Max retry attempts reached'); } isRetryableError(error) { // Retry on network errors, timeouts, and 5xx status codes if (error.isTimeout) return true; if (!error.response && !error.status) return true; // Network error const status = error.status || error.response?.status; return status >= 500 && status < 600; } async get(url, config = {}) { return this.request(url, { ...config, method: 'GET' }); } async post(url, data, config = {}) { return this.request(url, { ...config, method: 'POST', body: data ? JSON.stringify(data) : undefined, }); } async put(url, data, config = {}) { return this.request(url, { ...config, method: 'PUT', body: data ? JSON.stringify(data) : undefined, }); } async delete(url, config = {}) { return this.request(url, { ...config, method: 'DELETE' }); } async patch(url, data, config = {}) { return this.request(url, { ...config, method: 'PATCH', body: data, }); } // Utility method to create a new instance with merged config withConfig(additionalConfig) { return new HttpClient({ ...this.config, ...additionalConfig }); } // Utility method to get current config getConfig() { return { ...this.config }; } }