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) => Promise, * 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 }