182 lines
5.2 KiB
JavaScript
182 lines
5.2 KiB
JavaScript
const { AUTH_EXPIRED_CODES, REQUEST_TIMEOUT, RESULT_CODES } = require('../../config/constants')
|
|
const { getRuntimeConfig } = require('../../config/env')
|
|
const { sessionStore: defaultSessionStore } = require('../../stores/index')
|
|
|
|
class RequestError extends Error {
|
|
constructor(options = {}) {
|
|
super(options.message || 'Request failed')
|
|
this.name = 'RequestError'
|
|
this.code = typeof options.code === 'number' ? options.code : -1
|
|
this.statusCode = options.statusCode || 0
|
|
this.data = options.data || null
|
|
this.originalError = options.originalError
|
|
}
|
|
}
|
|
|
|
function defaultShouldRetry(error) {
|
|
const message = `${error.message || ''}`.toLowerCase()
|
|
return message.includes('timeout') || message.includes('timed out')
|
|
}
|
|
|
|
function createRequestKey(options) {
|
|
return JSON.stringify({
|
|
url: options.url,
|
|
method: options.method || 'GET',
|
|
data: options.data || null
|
|
})
|
|
}
|
|
|
|
function createWxRequestAdapter() {
|
|
return function wxRequestAdapter(options) {
|
|
return new Promise((resolve, reject) => {
|
|
if (typeof wx === 'undefined' || typeof wx.request !== 'function') {
|
|
reject(new Error('wx.request is not available'))
|
|
return
|
|
}
|
|
|
|
wx.request({
|
|
...options,
|
|
success: resolve,
|
|
fail: reject
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
function normalizeError(error, fallback = {}) {
|
|
if (error instanceof RequestError) {
|
|
return error
|
|
}
|
|
|
|
return new RequestError({
|
|
code: typeof fallback.code === 'number' ? fallback.code : -1,
|
|
message: error?.message || fallback.message || 'Network request failed',
|
|
statusCode: fallback.statusCode || 0,
|
|
data: fallback.data || null,
|
|
originalError: error
|
|
})
|
|
}
|
|
|
|
function normalizeResponse(response) {
|
|
const payload = response?.data || {}
|
|
const code = typeof payload.code === 'number' ? payload.code : RESULT_CODES.success
|
|
const data = Object.prototype.hasOwnProperty.call(payload, 'data') ? payload.data : null
|
|
const message = payload.message || ''
|
|
|
|
return {
|
|
code,
|
|
data,
|
|
message,
|
|
statusCode: response?.statusCode || 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* requestAdapter?: (options: Record<string, any>) => Promise<any>,
|
|
* authExpiredCodes?: number[],
|
|
* onUnauthorized?: (error: RequestError) => void,
|
|
* sessionStore?: { getState(): { token: string }, clearSession(): void },
|
|
* getConfig?: () => { baseURL: string, timeout: number },
|
|
* shouldRetry?: (error: RequestError, attempt: number) => boolean
|
|
* }} [options]
|
|
*/
|
|
function createRequester(options = {}) {
|
|
const requestAdapter = options.requestAdapter || createWxRequestAdapter()
|
|
const authExpiredCodes = options.authExpiredCodes || AUTH_EXPIRED_CODES
|
|
const onUnauthorized = options.onUnauthorized
|
|
const sessionStore = options.sessionStore || defaultSessionStore
|
|
const getConfig = options.getConfig || getRuntimeConfig
|
|
const shouldRetry = options.shouldRetry || defaultShouldRetry
|
|
const inflightMap = new Map()
|
|
|
|
async function executeRequest(requestOptions, attempt) {
|
|
const config = getConfig()
|
|
const sessionState = typeof sessionStore.getState === 'function' ? sessionStore.getState() : { token: '' }
|
|
const headers = {
|
|
...(requestOptions.header || {})
|
|
}
|
|
|
|
if (sessionState.token) {
|
|
headers.Authorization = `Bearer ${sessionState.token}`
|
|
}
|
|
|
|
try {
|
|
const response = await requestAdapter({
|
|
url: `${config.baseURL}${requestOptions.url}`,
|
|
method: requestOptions.method || 'GET',
|
|
data: requestOptions.data,
|
|
timeout: requestOptions.timeout || config.timeout || REQUEST_TIMEOUT,
|
|
header: headers
|
|
})
|
|
const normalizedResponse = normalizeResponse(response)
|
|
|
|
if (authExpiredCodes.includes(normalizedResponse.code)) {
|
|
const error = new RequestError(normalizedResponse)
|
|
sessionStore.clearSession?.()
|
|
onUnauthorized?.(error)
|
|
throw error
|
|
}
|
|
|
|
if (normalizedResponse.code !== RESULT_CODES.success) {
|
|
throw new RequestError(normalizedResponse)
|
|
}
|
|
|
|
return {
|
|
code: normalizedResponse.code,
|
|
data: normalizedResponse.data,
|
|
message: normalizedResponse.message
|
|
}
|
|
} catch (error) {
|
|
const normalizedError = normalizeError(error)
|
|
|
|
if (attempt < (requestOptions.retry || 0) && shouldRetry(normalizedError, attempt + 1)) {
|
|
return executeRequest(requestOptions, attempt + 1)
|
|
}
|
|
|
|
throw normalizedError
|
|
}
|
|
}
|
|
|
|
return function request(requestOptions) {
|
|
const dedupeKey = requestOptions.dedupe ? createRequestKey(requestOptions) : ''
|
|
|
|
if (dedupeKey && inflightMap.has(dedupeKey)) {
|
|
return inflightMap.get(dedupeKey)
|
|
}
|
|
|
|
const pendingRequest = executeRequest(requestOptions, 0).finally(() => {
|
|
if (dedupeKey) {
|
|
inflightMap.delete(dedupeKey)
|
|
}
|
|
})
|
|
|
|
if (dedupeKey) {
|
|
inflightMap.set(dedupeKey, pendingRequest)
|
|
}
|
|
|
|
return pendingRequest
|
|
}
|
|
}
|
|
|
|
let unauthorizedHandler = null
|
|
|
|
function setUnauthorizedHandler(handler) {
|
|
unauthorizedHandler = handler
|
|
}
|
|
|
|
const request = createRequester({
|
|
sessionStore: defaultSessionStore,
|
|
onUnauthorized(error) {
|
|
unauthorizedHandler?.(error)
|
|
}
|
|
})
|
|
|
|
module.exports = {
|
|
RequestError,
|
|
createRequestKey,
|
|
createRequester,
|
|
request,
|
|
setUnauthorizedHandler
|
|
}
|