75 lines
2.1 KiB
TypeScript
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)
|
|
}
|
|
}
|