UNPKG

react-native-apikit

Version:

Modern API toolkit for React Native and Expo with automatic token management, smart response parsing, and built-in error handling

267 lines (266 loc) 12.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useApi = useApi; exports.usePaginatedApi = usePaginatedApi; const react_1 = require("react"); const config_1 = require("../config"); function useApi() { const [state, setState] = (0, react_1.useState)({ loading: false, error: null, data: null, }); const reset = (0, react_1.useCallback)(() => { setState({ loading: false, error: null, data: null, }); }, []); const abortControllers = (0, react_1.useRef)({}); const makeRequest = (0, react_1.useCallback)((method, url, data, config) => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e; const apiConfig = (0, config_1.getApiKitConfig)(); const engine = (0, config_1.getEngine)(); if (!apiConfig.baseUrl) { console.warn('ApiKit: baseUrl not configured. Please call configureApiKit first.'); return; } setState(prev => (Object.assign(Object.assign({}, prev), { loading: true, error: null }))); // Get token if storage is configured let token = null; if (apiConfig.tokenStorage) { try { token = yield apiConfig.tokenStorage.getToken(); } catch (error) { console.warn('ApiKit: Failed to get token from storage:', error); } } // Prepare headers const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers), config === null || config === void 0 ? void 0 : config.headers); if (token) { headers.Authorization = `Bearer ${token}`; } // Build full URL const fullUrl = url.startsWith('http') ? url : `${apiConfig.baseUrl}${url}`; // Add query parameters let finalUrl = fullUrl; if (config === null || config === void 0 ? void 0 : config.params) { const params = new URLSearchParams(); Object.entries(config.params).forEach(([key, value]) => { if (value !== undefined && value !== null) { params.append(key, String(value)); } }); const queryString = params.toString(); if (queryString) { finalUrl += (fullUrl.includes('?') ? '&' : '?') + queryString; } } // Setup AbortController let controller; let signal = config === null || config === void 0 ? void 0 : config.signal; if (!signal) { controller = new AbortController(); signal = controller.signal; abortControllers.current[url] = controller; } // Prepare request config const requestConfig = { url: finalUrl, method, data: method !== 'GET' ? data : undefined, headers, timeout: (config === null || config === void 0 ? void 0 : config.timeout) || apiConfig.timeout, signal, }; const maxRetries = typeof apiConfig.retry === 'number' ? apiConfig.retry : 0; let attempt = 0; let lastError = null; while (attempt <= maxRetries) { try { // --- Interceptor: onRequest --- let interceptedRequestConfig = requestConfig; if (apiConfig.onRequest) { interceptedRequestConfig = yield apiConfig.onRequest(requestConfig); } // Caching logic (GET only) const isGet = method === 'GET'; let cacheKey = ''; if (isGet && apiConfig.cache) { cacheKey = url; if (config === null || config === void 0 ? void 0 : config.params) { cacheKey += '?' + Object.entries(config.params).map(([k, v]) => `${k}=${v}`).join('&'); } const cached = yield apiConfig.cache.get(cacheKey); if (cached) { setState({ loading: false, error: null, data: cached.data }); return; } } // Make request const response = yield engine.request(interceptedRequestConfig); // --- Interceptor: onResponse --- let interceptedResponse = response; if (apiConfig.onResponse) { interceptedResponse = yield apiConfig.onResponse(response); } // Handle 401 globally if (interceptedResponse.status === 401 && apiConfig.onUnauthorized) { apiConfig.onUnauthorized(); } // After successful GET, store in cache if (isGet && apiConfig.cache && interceptedResponse.data !== undefined) { const ttl = (config && config.cacheTtlMs) || 60000; // 1 min default yield apiConfig.cache.set(cacheKey, { data: interceptedResponse.data, status: interceptedResponse.status }, ttl); } setState({ loading: false, error: null, data: interceptedResponse.data, }); return; } catch (error) { let apiError; if (error.name === 'AbortError') { apiError = { message: 'Request cancelled', isNetworkError: false, }; } else if ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('Network request failed')) { apiError = { message: 'No internet connection', isNetworkError: true, }; } else if (error.code === 'ECONNABORTED' || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('timeout'))) { apiError = { message: 'Request timeout', isTimeout: true, }; } else { apiError = { message: error.message || 'An error occurred', status: (_c = error.response) === null || _c === void 0 ? void 0 : _c.status, details: (_d = error.response) === null || _d === void 0 ? void 0 : _d.data, isUnauthorized: ((_e = error.response) === null || _e === void 0 ? void 0 : _e.status) === 401, }; } // --- Interceptor: onError --- if (apiConfig.onError) { apiError = yield apiConfig.onError(apiError); } lastError = apiError; // Only retry on network error or timeout if ((apiError.isNetworkError || apiError.isTimeout) && attempt < maxRetries) { const delay = Math.pow(2, attempt) * 200; // Exponential backoff: 200ms, 400ms, 800ms, ... yield new Promise(res => setTimeout(res, delay)); attempt++; continue; } setState({ loading: false, error: apiError, data: null, }); return; } } // If all retries failed setState({ loading: false, error: lastError, data: null, }); // After request completes, clean up controller if (controller) delete abortControllers.current[url]; }), []); // Helper to wrap each method to return cancel function withCancel(fn) { return (...args) => { let url = args[0]; let controller; const promise = fn(...args); if (abortControllers.current[url]) { controller = abortControllers.current[url]; } return { promise, cancel: () => controller === null || controller === void 0 ? void 0 : controller.abort(), }; }; } return Object.assign(Object.assign({}, state), { get: withCancel((url, config) => makeRequest('GET', url, undefined, config)), post: withCancel((url, data, config) => makeRequest('POST', url, data, config)), put: withCancel((url, data, config) => makeRequest('PUT', url, data, config)), patch: withCancel((url, data, config) => makeRequest('PATCH', url, data, config)), del: withCancel((url, config) => makeRequest('DELETE', url, undefined, config)), reset }); } // Paginated API hook function usePaginatedApi(url, { pageSize = 20, initialPage = 1, params = {}, config, } = {}) { const [page, setPage] = (0, react_1.useState)(initialPage); const [data, setData] = (0, react_1.useState)([]); const [loading, setLoading] = (0, react_1.useState)(false); const [error, setError] = (0, react_1.useState)(null); const [hasMore, setHasMore] = (0, react_1.useState)(true); const apiConfig = (0, config_1.getApiKitConfig)(); const engine = (0, config_1.getEngine)(); const fetchPage = (pageNum) => __awaiter(this, void 0, void 0, function* () { setLoading(true); setError(null); try { const fullParams = Object.assign(Object.assign({}, params), { page: pageNum, pageSize }); const fullUrl = url; const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json' }, apiConfig.headers), ((config === null || config === void 0 ? void 0 : config.headers) || {})); const requestConfig = { url: fullUrl, method: 'GET', params: fullParams, headers, timeout: (config === null || config === void 0 ? void 0 : config.timeout) || apiConfig.timeout, signal: config === null || config === void 0 ? void 0 : config.signal, }; const response = yield engine.request(requestConfig); if (response.status === 401 && apiConfig.onUnauthorized) { apiConfig.onUnauthorized(); } setData(prev => (pageNum === 1 ? response.data : [...prev, ...response.data])); setHasMore(response.data.length === pageSize); } catch (err) { setError({ message: err.message || 'An error occurred' }); } finally { setLoading(false); } }); (0, react_1.useEffect)(() => { fetchPage(page); // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, url, JSON.stringify(params)]); const nextPage = () => { if (hasMore && !loading) setPage(p => p + 1); }; const prevPage = () => { if (page > 1 && !loading) setPage(p => p - 1); }; const reset = () => { setPage(initialPage); setData([]); setHasMore(true); setError(null); }; return { page, data, loading, error, hasMore, nextPage, prevPage, reset }; }