Files
one-pipe-system/src/utils/business/apiRateLimit.ts

75 lines
2.1 KiB
TypeScript

const FIVE_MINUTES_MS = 5 * 60 * 1000
const STORAGE_KEY_PREFIX = 'asset-action-rate-limit'
export type AssetRateLimitedAction = 'refresh' | 'start' | 'stop'
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)
}
}