This commit is contained in:
74
src/utils/business/apiRateLimit.ts
Normal file
74
src/utils/business/apiRateLimit.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
const FIVE_MINUTES_MS = 5 * 60 * 1000
|
||||
const STORAGE_KEY_PREFIX = 'asset-action-rate-limit'
|
||||
|
||||
export type AssetRateLimitedAction = 'refresh' | 'start'
|
||||
|
||||
export class FrontendRateLimitError extends Error {
|
||||
readonly remainingMs: number
|
||||
readonly retryAt: number
|
||||
|
||||
constructor(remainingMs: number, retryAt: number) {
|
||||
super(`操作过于频繁,请${formatRemainingTime(remainingMs)}后再试`)
|
||||
this.name = 'FrontendRateLimitError'
|
||||
this.remainingMs = remainingMs
|
||||
this.retryAt = retryAt
|
||||
}
|
||||
}
|
||||
|
||||
const getStorageKey = (action: AssetRateLimitedAction, identifier: string) =>
|
||||
`${STORAGE_KEY_PREFIX}:${action}:${identifier}`
|
||||
|
||||
const getTimestamp = (key: string): number => {
|
||||
const value = Number(localStorage.getItem(key))
|
||||
return Number.isFinite(value) ? value : 0
|
||||
}
|
||||
|
||||
export const formatRemainingTime = (remainingMs: number): string => {
|
||||
const totalSeconds = Math.max(1, Math.ceil(remainingMs / 1000))
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
if (minutes <= 0) return `${seconds}秒`
|
||||
if (seconds === 0) return `${minutes}分钟`
|
||||
return `${minutes}分${seconds}秒`
|
||||
}
|
||||
|
||||
export const getAssetActionRateLimitState = (
|
||||
action: AssetRateLimitedAction,
|
||||
identifier: string,
|
||||
now: number = Date.now()
|
||||
) => {
|
||||
const key = getStorageKey(action, identifier)
|
||||
const lastCalledAt = getTimestamp(key)
|
||||
const retryAt = lastCalledAt + FIVE_MINUTES_MS
|
||||
const remainingMs = Math.max(0, retryAt - now)
|
||||
|
||||
if (remainingMs <= 0 && lastCalledAt > 0) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
return {
|
||||
limited: remainingMs > 0,
|
||||
remainingMs,
|
||||
retryAt
|
||||
}
|
||||
}
|
||||
|
||||
export const markAssetActionCalled = (
|
||||
action: AssetRateLimitedAction,
|
||||
identifier: string,
|
||||
now: number = Date.now()
|
||||
) => {
|
||||
localStorage.setItem(getStorageKey(action, identifier), String(now))
|
||||
}
|
||||
|
||||
export const assertAssetActionAllowed = (
|
||||
action: AssetRateLimitedAction,
|
||||
identifier: string,
|
||||
now: number = Date.now()
|
||||
) => {
|
||||
const state = getAssetActionRateLimitState(action, identifier, now)
|
||||
if (state.limited) {
|
||||
throw new FrontendRateLimitError(state.remainingMs, state.retryAt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user