init
This commit is contained in:
181
services/request/index.js
Normal file
181
services/request/index.js
Normal file
@@ -0,0 +1,181 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user