import axios, { AxiosError } from 'axios'; import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; import axiosRetry from 'axios-retry'; import { nanoid } from '@sa/utils'; import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options'; import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant'; import type { CustomAxiosRequestConfig, FlatRequestInstance, MappedType, RequestInstance, RequestOption, ResponseType } from './type'; function createCommonRequest( axiosConfig?: CreateAxiosDefaults, options?: Partial> ) { const opts = createDefaultOptions(options); const axiosConf = createAxiosConfig(axiosConfig); const instance = axios.create(axiosConf); const cancelTokenSourceMap = new Map(); // config axios retry const retryOptions = createRetryOptions(axiosConf); axiosRetry(instance, retryOptions); instance.interceptors.request.use(conf => { const config: InternalAxiosRequestConfig = { ...conf }; // set request id const requestId = nanoid(); config.headers.set(REQUEST_ID_KEY, requestId); // config cancel token const cancelTokenSource = axios.CancelToken.source(); config.cancelToken = cancelTokenSource.token; cancelTokenSourceMap.set(requestId, cancelTokenSource); // handle config by hook const handledConfig = opts.onRequest?.(config) || config; return handledConfig; }); instance.interceptors.response.use( async response => { const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json'; if (responseType !== 'json' || opts.isBackendSuccess(response)) { return Promise.resolve(response); } const fail = await opts.onBackendFail(response, instance); if (fail) { return fail; } const backendError = new AxiosError( 'the backend request error', BACKEND_ERROR_CODE, response.config, response.request, response ); await opts.onError(backendError); return Promise.reject(backendError); }, async (error: AxiosError) => { await opts.onError(error); return Promise.reject(error); } ); function cancelRequest(requestId: string) { const cancelTokenSource = cancelTokenSourceMap.get(requestId); if (cancelTokenSource) { cancelTokenSource.cancel(); cancelTokenSourceMap.delete(requestId); } } function cancelAllRequest() { cancelTokenSourceMap.forEach(cancelTokenSource => { cancelTokenSource.cancel(); }); cancelTokenSourceMap.clear(); } return { instance, opts, cancelRequest, cancelAllRequest }; } /** * create a request instance * * @param axiosConfig axios config * @param options request options */ export function createRequest>( axiosConfig?: CreateAxiosDefaults, options?: Partial> ) { const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest(axiosConfig, options); const request: RequestInstance = async function request( config: CustomAxiosRequestConfig ) { const response: AxiosResponse = await instance(config); const responseType = response.config?.responseType || 'json'; if (responseType === 'json') { return opts.transformBackendResponse(response); } return response.data as MappedType; } as RequestInstance; request.cancelRequest = cancelRequest; request.cancelAllRequest = cancelAllRequest; request.state = {} as State; return request; } /** * create a flat request instance * * The response data is a flat object: { data: any, error: AxiosError } * * @param axiosConfig axios config * @param options request options */ export function createFlatRequest>( axiosConfig?: CreateAxiosDefaults, options?: Partial> ) { const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest(axiosConfig, options); const flatRequest: FlatRequestInstance = async function flatRequest< T = any, R extends ResponseType = 'json' >(config: CustomAxiosRequestConfig) { try { const response: AxiosResponse = await instance(config); const responseType = response.config?.responseType || 'json'; if (responseType === 'json') { const data = opts.transformBackendResponse(response); return { data, error: null }; } return { data: response.data as MappedType, error: null }; } catch (error) { return { data: null, error }; } } as FlatRequestInstance; flatRequest.cancelRequest = cancelRequest; flatRequest.cancelAllRequest = cancelAllRequest; flatRequest.state = {} as State; return flatRequest; } export { BACKEND_ERROR_CODE, REQUEST_ID_KEY }; export type * from './type'; export type { CreateAxiosDefaults, AxiosError };