Files
xuanzhi-wx/services/request/index.js
2026-04-22 18:54:52 +08:00

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')
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
}