Add utility functions for API client integration

This commit is contained in:
2026-01-28 22:16:51 +00:00
parent 6dfe06a412
commit d5ec0cd9db

View File

@@ -0,0 +1,192 @@
/**
* API Client Utility
*
* Centralized fetch wrapper with:
* - Automatic error handling
* - Retry logic with exponential backoff
* - Request/response interceptors
* - Request deduplication
* - Timeout support
*
* @example
* const client = createApiClient({ baseURL: '/api' });
* const data = await client.get('/users');
*/
const activeRequests = new Map();
/**
* Create an API client with configuration
* @param {Object} config - Client configuration
* @returns {Object} API client with methods
*/
export function createApiClient(config = {}) {
const {
baseURL = '',
timeout = 30000,
maxRetries = 3,
retryDelay = 1000,
headers: defaultHeaders = {},
onRequest = null,
onResponse = null,
onError = null
} = config;
/**
* Make HTTP request
* @param {string} url - Request URL
* @param {Object} options - Fetch options
* @returns {Promise<any>} Response data
*/
async function request(url, options = {}) {
const fullURL = url.startsWith('http') ? url : `${baseURL}${url}`;
const requestKey = `${options.method || 'GET'}:${fullURL}`;
// Check for duplicate request
if (options.deduplicate !== false && activeRequests.has(requestKey)) {
return activeRequests.get(requestKey);
}
const requestPromise = makeRequest(fullURL, options);
// Store active request
if (options.deduplicate !== false) {
activeRequests.set(requestKey, requestPromise);
}
try {
const result = await requestPromise;
return result;
} finally {
activeRequests.delete(requestKey);
}
}
/**
* Make the actual HTTP request with retries
*/
async function makeRequest(url, options) {
const {
retries = maxRetries,
...fetchOptions
} = options;
// Merge headers
const headers = {
'Content-Type': 'application/json',
...defaultHeaders,
...fetchOptions.headers
};
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
let lastError;
let attempt = 0;
while (attempt <= retries) {
try {
// Call request interceptor
let requestOptions = { ...fetchOptions, headers, signal: controller.signal };
if (onRequest) {
requestOptions = await onRequest(url, requestOptions) || requestOptions;
}
const response = await fetch(url, requestOptions);
clearTimeout(timeoutId);
// Call response interceptor
let processedResponse = response;
if (onResponse) {
processedResponse = await onResponse(response.clone()) || response;
}
// Handle HTTP errors
if (!processedResponse.ok) {
const error = new Error(`HTTP ${processedResponse.status}: ${processedResponse.statusText}`);
error.status = processedResponse.status;
error.response = processedResponse;
// Try to parse error body
try {
const contentType = processedResponse.headers.get('content-type');
if (contentType?.includes('application/json')) {
error.data = await processedResponse.json();
} else {
error.data = await processedResponse.text();
}
} catch (e) {
// Ignore parse errors
}
throw error;
}
// Parse response
const contentType = processedResponse.headers.get('content-type');
if (contentType?.includes('application/json')) {
return await processedResponse.json();
}
return await processedResponse.text();
} catch (error) {
clearTimeout(timeoutId);
lastError = error;
// Don't retry on abort
if (error.name === 'AbortError') {
const timeoutError = new Error('Request timeout');
timeoutError.isTimeout = true;
throw timeoutError;
}
// Don't retry on 4xx errors (client errors)
if (error.status && error.status >= 400 && error.status < 500) {
throw error;
}
attempt++;
// If more retries remaining, wait before retrying
if (attempt <= retries) {
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
}
}
}
// All retries exhausted
if (onError) {
onError(lastError);
}
throw lastError;
}
// Convenience methods
return {
request,
get: (url, options = {}) => request(url, { ...options, method: 'GET' }),
post: (url, data, options = {}) => request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
}),
put: (url, data, options = {}) => request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
}),
patch: (url, data, options = {}) => request(url, {
...options,
method: 'PATCH',
body: JSON.stringify(data)
}),
delete: (url, options = {}) => request(url, { ...options, method: 'DELETE' })
};
}
// Export default client instance
export const apiClient = createApiClient({
baseURL: '/api'
});