chore: init read-star monorepo
This commit is contained in:
5
packages/axios/src/constant.ts
Normal file
5
packages/axios/src/constant.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/** request id key */
|
||||
export const REQUEST_ID_KEY = 'X-Request-Id'
|
||||
|
||||
/** the backend error code key */
|
||||
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR'
|
||||
180
packages/axios/src/index.ts
Normal file
180
packages/axios/src/index.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'
|
||||
import type {
|
||||
CustomAxiosRequestConfig,
|
||||
FlatRequestInstance,
|
||||
MappedType,
|
||||
RequestInstance,
|
||||
RequestOption,
|
||||
ResponseType,
|
||||
} from './type'
|
||||
import { nanoid } from '@sa/utils'
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import axiosRetry from 'axios-retry'
|
||||
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant'
|
||||
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options'
|
||||
import { transformResponse } from './shared'
|
||||
|
||||
function createCommonRequest<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts = createDefaultOptions<ResponseData, ApiData, State>(options)
|
||||
|
||||
const axiosConf = createAxiosConfig(axiosConfig)
|
||||
const instance = axios.create(axiosConf)
|
||||
|
||||
const abortControllerMap = new Map<string, AbortController>()
|
||||
|
||||
// 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 abort controller
|
||||
if (!config.signal) {
|
||||
const abortController = new AbortController()
|
||||
config.signal = abortController.signal
|
||||
abortControllerMap.set(requestId, abortController)
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
||||
await transformResponse(response)
|
||||
|
||||
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<ResponseData>(
|
||||
'the backend request error',
|
||||
BACKEND_ERROR_CODE,
|
||||
response.config,
|
||||
response.request,
|
||||
response,
|
||||
)
|
||||
|
||||
await opts.onError(backendError)
|
||||
|
||||
return Promise.reject(backendError)
|
||||
},
|
||||
async (error: AxiosError<ResponseData>) => {
|
||||
await opts.onError(error)
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
function cancelAllRequest() {
|
||||
abortControllerMap.forEach((abortController) => {
|
||||
abortController.abort()
|
||||
})
|
||||
abortControllerMap.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
instance,
|
||||
opts,
|
||||
cancelAllRequest,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a request instance
|
||||
*
|
||||
* @param axiosConfig axios config
|
||||
* @param options request options
|
||||
*/
|
||||
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>,
|
||||
) {
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options)
|
||||
|
||||
const request: RequestInstance<ApiData, State> = async function request<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json',
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
const response: AxiosResponse<ResponseData> = await instance(config)
|
||||
|
||||
const responseType = response.config?.responseType || 'json'
|
||||
|
||||
if (responseType === 'json') {
|
||||
return opts.transform(response)
|
||||
}
|
||||
|
||||
return response.data as MappedType<R, T>
|
||||
} as RequestInstance<ApiData, State>
|
||||
|
||||
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<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData, ApiData, State>>,
|
||||
) {
|
||||
const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options)
|
||||
|
||||
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||
T extends ApiData = ApiData,
|
||||
R extends ResponseType = 'json',
|
||||
>(config: CustomAxiosRequestConfig) {
|
||||
try {
|
||||
const response: AxiosResponse<ResponseData> = await instance(config)
|
||||
|
||||
const responseType = response.config?.responseType || 'json'
|
||||
|
||||
if (responseType === 'json') {
|
||||
const data = await opts.transform(response)
|
||||
|
||||
return { data, error: null, response }
|
||||
}
|
||||
|
||||
return { data: response.data as MappedType<R, T>, error: null, response }
|
||||
}
|
||||
catch (error) {
|
||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response }
|
||||
}
|
||||
} as FlatRequestInstance<ResponseData, ApiData, State>
|
||||
|
||||
flatRequest.cancelAllRequest = cancelAllRequest
|
||||
flatRequest.state = {
|
||||
...opts.defaultState,
|
||||
} as State
|
||||
|
||||
return flatRequest
|
||||
}
|
||||
|
||||
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY }
|
||||
export type * from './type'
|
||||
export type { AxiosError, CreateAxiosDefaults }
|
||||
61
packages/axios/src/options.ts
Normal file
61
packages/axios/src/options.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import type { CreateAxiosDefaults } from 'axios'
|
||||
import type { IAxiosRetryConfig } from 'axios-retry'
|
||||
import type { RequestOption } from './type'
|
||||
import { stringify } from 'qs'
|
||||
import { isHttpSuccess } from './shared'
|
||||
|
||||
export function createDefaultOptions<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||
defaultState: {} as State,
|
||||
transform: async response => response.data as unknown as ApiData,
|
||||
transformBackendResponse: async response => response.data as unknown as ApiData,
|
||||
onRequest: async config => config,
|
||||
isBackendSuccess: _response => true,
|
||||
onBackendFail: async () => {},
|
||||
onError: async () => {},
|
||||
}
|
||||
|
||||
if (options?.transform) {
|
||||
opts.transform = options.transform
|
||||
}
|
||||
else {
|
||||
opts.transform = options?.transformBackendResponse || opts.transform
|
||||
}
|
||||
|
||||
Object.assign(opts, options)
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
|
||||
const retryConfig: IAxiosRetryConfig = {
|
||||
retries: 0,
|
||||
}
|
||||
|
||||
Object.assign(retryConfig, config)
|
||||
|
||||
return retryConfig
|
||||
}
|
||||
|
||||
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
|
||||
const TEN_SECONDS = 10 * 1000
|
||||
|
||||
const axiosConfig: CreateAxiosDefaults = {
|
||||
timeout: TEN_SECONDS,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
validateStatus: isHttpSuccess,
|
||||
paramsSerializer: (params) => {
|
||||
return stringify(params)
|
||||
},
|
||||
}
|
||||
|
||||
Object.assign(axiosConfig, config)
|
||||
|
||||
return axiosConfig
|
||||
}
|
||||
83
packages/axios/src/shared.ts
Normal file
83
packages/axios/src/shared.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||
import type { ResponseType } from './type'
|
||||
|
||||
export function getContentType(config: InternalAxiosRequestConfig) {
|
||||
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json'
|
||||
|
||||
return contentType
|
||||
}
|
||||
|
||||
/**
|
||||
* check if http status is success
|
||||
*
|
||||
* @param status
|
||||
*/
|
||||
export function isHttpSuccess(status: number) {
|
||||
const isSuccessCode = status >= 200 && status < 300
|
||||
return isSuccessCode || status === 304
|
||||
}
|
||||
|
||||
/**
|
||||
* is response json
|
||||
*
|
||||
* @param response axios response
|
||||
*/
|
||||
export function isResponseJson(response: AxiosResponse) {
|
||||
const { responseType } = response.config
|
||||
|
||||
return responseType === 'json' || responseType === undefined
|
||||
}
|
||||
|
||||
export async function transformResponse(response: AxiosResponse) {
|
||||
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json'
|
||||
if (responseType === 'json')
|
||||
return
|
||||
|
||||
const isJson = response.headers['content-type']?.includes('application/json')
|
||||
if (!isJson)
|
||||
return
|
||||
|
||||
if (responseType === 'blob') {
|
||||
await transformBlobToJson(response)
|
||||
}
|
||||
|
||||
if (responseType === 'arrayBuffer') {
|
||||
await transformArrayBufferToJson(response)
|
||||
}
|
||||
}
|
||||
|
||||
export async function transformBlobToJson(response: AxiosResponse) {
|
||||
try {
|
||||
let data = response.data
|
||||
|
||||
if (typeof data === 'string') {
|
||||
data = JSON.parse(data)
|
||||
}
|
||||
|
||||
if (Object.prototype.toString.call(data) === '[object Blob]') {
|
||||
const json = await data.text()
|
||||
data = JSON.parse(json)
|
||||
}
|
||||
|
||||
response.data = data
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
export async function transformArrayBufferToJson(response: AxiosResponse) {
|
||||
try {
|
||||
let data = response.data
|
||||
|
||||
if (typeof data === 'string') {
|
||||
data = JSON.parse(data)
|
||||
}
|
||||
|
||||
if (Object.prototype.toString.call(data) === '[object ArrayBuffer]') {
|
||||
const json = new TextDecoder().decode(data)
|
||||
data = JSON.parse(json)
|
||||
}
|
||||
|
||||
response.data = data
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
130
packages/axios/src/type.ts
Normal file
130
packages/axios/src/type.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
export type ContentType =
|
||||
| 'text/html'
|
||||
| 'text/plain'
|
||||
| 'multipart/form-data'
|
||||
| 'application/json'
|
||||
| 'application/x-www-form-urlencoded'
|
||||
| 'application/octet-stream'
|
||||
|
||||
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>
|
||||
|
||||
export interface RequestOption<
|
||||
ResponseData,
|
||||
ApiData = ResponseData,
|
||||
State extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
/**
|
||||
* The default state
|
||||
*/
|
||||
defaultState?: State
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>
|
||||
/**
|
||||
* transform the response data to the api data
|
||||
*
|
||||
* @deprecated use `transform` instead, will be removed in the next major version v3
|
||||
* @param response Axios response
|
||||
*/
|
||||
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>
|
||||
/**
|
||||
* The hook before request
|
||||
*
|
||||
* For example: You can add header token in this hook
|
||||
*
|
||||
* @param config Axios config
|
||||
*/
|
||||
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>
|
||||
/**
|
||||
* The hook to check backend response is success or not
|
||||
*
|
||||
* @param response Axios response
|
||||
*/
|
||||
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean
|
||||
/**
|
||||
* The hook after backend request fail
|
||||
*
|
||||
* For example: You can handle the expired token in this hook
|
||||
*
|
||||
* @param response Axios response
|
||||
* @param instance Axios instance
|
||||
*/
|
||||
onBackendFail: (
|
||||
response: AxiosResponse<ResponseData>,
|
||||
instance: AxiosInstance
|
||||
) => Promise<AxiosResponse | null> | Promise<void>
|
||||
/**
|
||||
* The hook to handle error
|
||||
*
|
||||
* For example: You can show error message in this hook
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
onError: (error: AxiosError<ResponseData>) => void | Promise<void>
|
||||
}
|
||||
|
||||
interface ResponseMap {
|
||||
blob: Blob
|
||||
text: string
|
||||
arrayBuffer: ArrayBuffer
|
||||
stream: ReadableStream<Uint8Array>
|
||||
document: Document
|
||||
}
|
||||
export type ResponseType = keyof ResponseMap | 'json'
|
||||
|
||||
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
|
||||
? ResponseMap[R]
|
||||
: JsonType
|
||||
|
||||
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
|
||||
responseType?: R
|
||||
}
|
||||
|
||||
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||
/**
|
||||
* cancel all request
|
||||
*
|
||||
* if the request provide abort controller sign from config, it will not collect in the abort controller map
|
||||
*/
|
||||
cancelAllRequest: () => void
|
||||
/** you can set custom state in the request instance */
|
||||
state: State
|
||||
}
|
||||
|
||||
/** The request instance */
|
||||
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<MappedType<R, T>>
|
||||
}
|
||||
|
||||
export interface FlatResponseSuccessData<ResponseData, ApiData> {
|
||||
data: ApiData
|
||||
error: null
|
||||
response: AxiosResponse<ResponseData>
|
||||
}
|
||||
|
||||
export interface FlatResponseFailData<ResponseData> {
|
||||
data: null
|
||||
error: AxiosError<ResponseData>
|
||||
response: AxiosResponse<ResponseData>
|
||||
}
|
||||
|
||||
export type FlatResponseData<ResponseData, ApiData> =
|
||||
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||
| FlatResponseFailData<ResponseData>
|
||||
|
||||
export interface FlatRequestInstance<
|
||||
ResponseData,
|
||||
ApiData,
|
||||
State extends Record<string, unknown>,
|
||||
> extends RequestInstanceCommon<State> {
|
||||
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig<R>
|
||||
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>
|
||||
}
|
||||
Reference in New Issue
Block a user