perf(stability): add request metrics and resilient API retries
This commit is contained in:
@@ -4,6 +4,10 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
let lastToastKey = ''
|
||||
let lastToastAt = 0
|
||||
|
||||
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504])
|
||||
const MAX_RETRY_COUNT = 1
|
||||
const RETRY_BASE_DELAY_MS = 300
|
||||
|
||||
function toastErrorOnce(key, message, minIntervalMs = 1500) {
|
||||
const now = Date.now()
|
||||
if (key === lastToastKey && now - lastToastAt < minIntervalMs) return
|
||||
@@ -18,6 +22,41 @@ function getCookie(name) {
|
||||
return match ? decodeURIComponent(match[1]) : ''
|
||||
}
|
||||
|
||||
function isIdempotentMethod(method) {
|
||||
return ['GET', 'HEAD', 'OPTIONS'].includes(String(method || 'GET').toUpperCase())
|
||||
}
|
||||
|
||||
function shouldRetryRequest(error) {
|
||||
const config = error?.config
|
||||
if (!config || config.__no_retry) return false
|
||||
if (!isIdempotentMethod(config.method)) return false
|
||||
|
||||
const retried = Number(config.__retry_count || 0)
|
||||
if (retried >= MAX_RETRY_COUNT) return false
|
||||
|
||||
const code = String(error?.code || '')
|
||||
if (code === 'ECONNABORTED' || code === 'ERR_NETWORK') return true
|
||||
|
||||
const status = Number(error?.response?.status || 0)
|
||||
return RETRYABLE_STATUS.has(status)
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => {
|
||||
window.setTimeout(resolve, Math.max(0, Number(ms || 0)))
|
||||
})
|
||||
}
|
||||
|
||||
async function retryRequestOnce(error, client) {
|
||||
const config = error?.config || {}
|
||||
const retried = Number(config.__retry_count || 0)
|
||||
config.__retry_count = retried + 1
|
||||
|
||||
const backoffMs = RETRY_BASE_DELAY_MS * (retried + 1)
|
||||
await delay(backoffMs)
|
||||
return client.request(config)
|
||||
}
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: '/yuyx/api',
|
||||
timeout: 30_000,
|
||||
@@ -76,6 +115,10 @@ api.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRetryRequest(error)) {
|
||||
return retryRequestOnce(error, api)
|
||||
}
|
||||
|
||||
if (status === 401) {
|
||||
toastErrorOnce('401', message || '登录已过期,请重新登录', 3000)
|
||||
const pathname = window.location?.pathname || ''
|
||||
|
||||
@@ -34,7 +34,11 @@ async function refreshStats() {
|
||||
|
||||
const loadingBadges = ref(false)
|
||||
const pendingFeedbackCount = ref(0)
|
||||
let badgeTimer
|
||||
|
||||
const BADGE_POLL_ACTIVE_MS = 60_000
|
||||
const BADGE_POLL_HIDDEN_MS = 180_000
|
||||
|
||||
let badgeTimer = null
|
||||
|
||||
async function refreshNavBadges(partial = null) {
|
||||
if (partial && typeof partial === 'object') {
|
||||
@@ -55,6 +59,34 @@ async function refreshNavBadges(partial = null) {
|
||||
}
|
||||
}
|
||||
|
||||
function isPageHidden() {
|
||||
if (typeof document === 'undefined') return false
|
||||
return document.visibilityState === 'hidden'
|
||||
}
|
||||
|
||||
function currentBadgePollDelay() {
|
||||
return isPageHidden() ? BADGE_POLL_HIDDEN_MS : BADGE_POLL_ACTIVE_MS
|
||||
}
|
||||
|
||||
function stopBadgePolling() {
|
||||
if (!badgeTimer) return
|
||||
window.clearTimeout(badgeTimer)
|
||||
badgeTimer = null
|
||||
}
|
||||
|
||||
function scheduleBadgePolling() {
|
||||
stopBadgePolling()
|
||||
badgeTimer = window.setTimeout(async () => {
|
||||
badgeTimer = null
|
||||
await refreshNavBadges().catch(() => {})
|
||||
scheduleBadgePolling()
|
||||
}, currentBadgePollDelay())
|
||||
}
|
||||
|
||||
function onVisibilityChange() {
|
||||
scheduleBadgePolling()
|
||||
}
|
||||
|
||||
provide('refreshStats', refreshStats)
|
||||
provide('adminStats', stats)
|
||||
provide('refreshNavBadges', refreshNavBadges)
|
||||
@@ -75,12 +107,14 @@ onMounted(async () => {
|
||||
|
||||
await refreshStats()
|
||||
await refreshNavBadges()
|
||||
badgeTimer = window.setInterval(refreshNavBadges, 60_000)
|
||||
scheduleBadgePolling()
|
||||
window.addEventListener('visibilitychange', onVisibilityChange)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mediaQuery?.removeEventListener?.('change', syncIsMobile)
|
||||
window.clearInterval(badgeTimer)
|
||||
stopBadgePolling()
|
||||
window.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
|
||||
Reference in New Issue
Block a user