index.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import axios, { AxiosError } from 'axios';
  2. import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
  3. import axiosRetry from 'axios-retry';
  4. import { nanoid } from '@sa/utils';
  5. import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
  6. import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
  7. import type {
  8. CustomAxiosRequestConfig,
  9. FlatRequestInstance,
  10. MappedType,
  11. RequestInstance,
  12. RequestOption,
  13. ResponseType
  14. } from './type';
  15. function createCommonRequest<
  16. ResponseData,
  17. ApiData = ResponseData,
  18. State extends Record<string, unknown> = Record<string, unknown>
  19. >(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
  20. const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
  21. const axiosConf = createAxiosConfig(axiosConfig);
  22. const instance = axios.create(axiosConf);
  23. const abortControllerMap = new Map<string, AbortController>();
  24. // config axios retry
  25. const retryOptions = createRetryOptions(axiosConf);
  26. axiosRetry(instance, retryOptions);
  27. instance.interceptors.request.use(conf => {
  28. const config: InternalAxiosRequestConfig = { ...conf };
  29. // set request id
  30. const requestId = nanoid();
  31. config.headers.set(REQUEST_ID_KEY, requestId);
  32. // config abort controller
  33. if (!config.signal) {
  34. const abortController = new AbortController();
  35. config.signal = abortController.signal;
  36. abortControllerMap.set(requestId, abortController);
  37. }
  38. // handle config by hook
  39. const handledConfig = opts.onRequest?.(config) || config;
  40. return handledConfig;
  41. });
  42. instance.interceptors.response.use(
  43. async response => {
  44. const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
  45. if (responseType !== 'json' || opts.isBackendSuccess(response)) {
  46. return Promise.resolve(response);
  47. }
  48. const fail = await opts.onBackendFail(response, instance);
  49. if (fail) {
  50. return fail;
  51. }
  52. const backendError = new AxiosError<ResponseData>(
  53. 'the backend request error',
  54. BACKEND_ERROR_CODE,
  55. response.config,
  56. response.request,
  57. response
  58. );
  59. await opts.onError(backendError);
  60. return Promise.reject(backendError);
  61. },
  62. async (error: AxiosError<ResponseData>) => {
  63. await opts.onError(error);
  64. return Promise.reject(error);
  65. }
  66. );
  67. function cancelAllRequest() {
  68. abortControllerMap.forEach(abortController => {
  69. abortController.abort();
  70. });
  71. abortControllerMap.clear();
  72. }
  73. return {
  74. instance,
  75. opts,
  76. cancelAllRequest
  77. };
  78. }
  79. /**
  80. * create a request instance
  81. *
  82. * @param axiosConfig axios config
  83. * @param options request options
  84. */
  85. export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
  86. axiosConfig?: CreateAxiosDefaults,
  87. options?: Partial<RequestOption<ResponseData, ApiData, State>>
  88. ) {
  89. const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
  90. const request: RequestInstance<ApiData, State> = async function request<
  91. T extends ApiData = ApiData,
  92. R extends ResponseType = 'json'
  93. >(config: CustomAxiosRequestConfig) {
  94. const response: AxiosResponse<ResponseData> = await instance(config);
  95. const responseType = response.config?.responseType || 'json';
  96. if (responseType === 'json') {
  97. return opts.transform(response);
  98. }
  99. return response.data as MappedType<R, T>;
  100. } as RequestInstance<ApiData, State>;
  101. request.cancelAllRequest = cancelAllRequest;
  102. request.state = {} as State;
  103. return request;
  104. }
  105. /**
  106. * create a flat request instance
  107. *
  108. * The response data is a flat object: { data: any, error: AxiosError }
  109. *
  110. * @param axiosConfig axios config
  111. * @param options request options
  112. */
  113. export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
  114. axiosConfig?: CreateAxiosDefaults,
  115. options?: Partial<RequestOption<ResponseData, ApiData, State>>
  116. ) {
  117. const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
  118. const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
  119. T extends ApiData = ApiData,
  120. R extends ResponseType = 'json'
  121. >(config: CustomAxiosRequestConfig) {
  122. try {
  123. const response: AxiosResponse<ResponseData> = await instance(config);
  124. const responseType = response.config?.responseType || 'json';
  125. if (responseType === 'json') {
  126. const data = await opts.transform(response);
  127. return { data, error: null, response };
  128. }
  129. return { data: response.data as MappedType<R, T>, error: null, response };
  130. } catch (error) {
  131. return { data: null, error, response: (error as AxiosError<ResponseData>).response };
  132. }
  133. } as FlatRequestInstance<ResponseData, ApiData, State>;
  134. flatRequest.cancelAllRequest = cancelAllRequest;
  135. flatRequest.state = {
  136. ...opts.defaultState
  137. } as State;
  138. return flatRequest;
  139. }
  140. export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
  141. export type * from './type';
  142. export type { CreateAxiosDefaults, AxiosError };