58 lines
1.4 KiB
TypeScript
58 lines
1.4 KiB
TypeScript
interface DeviceUsage {
|
|||
|
|
count: number;
|
||
|
|
firstUsedAt: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
// In-memory usage map (or keyed by hashed client token)
|
||
|
|
const usageStore = new Map<string, DeviceUsage>();
|
||
|
|
|
||
|
|
export interface RateLimitResult {
|
||
|
|
allowed: boolean;
|
||
|
|
remaining: number;
|
||
|
|
resetMinutes: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function checkRateLimit(
|
||
|
|
clientId: string,
|
||
|
|
maxAllowed: number,
|
||
|
|
cooldownMinutes: number
|
||
|
|
): RateLimitResult {
|
||
|
|
if (!maxAllowed || maxAllowed <= 0) {
|
||
|
|
return { allowed: true, remaining: 999999, resetMinutes: 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
const now = Date.now();
|
||
|
|
const cooldownMs = cooldownMinutes * 60 * 1000;
|
||
|
|
const usage = usageStore.get(clientId);
|
||
|
|
|
||
|
|
if (!usage) {
|
||
|
|
return { allowed: true, remaining: maxAllowed, resetMinutes: 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if cooldown period has elapsed since first use
|
||
|
|
if (now - usage.firstUsedAt > cooldownMs) {
|
||
|
|
usageStore.delete(clientId);
|
||
|
|
return { allowed: true, remaining: maxAllowed, resetMinutes: 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
const remaining = Math.max(0, maxAllowed - usage.count);
|
||
|
|
const remainingCooldownMs = cooldownMs - (now - usage.firstUsedAt);
|
||
|
|
const resetMinutes = Math.ceil(remainingCooldownMs / (60 * 1000));
|
||
|
|
|
||
|
|
return {
|
||
|
|
allowed: usage.count < maxAllowed,
|
||
|
|
remaining,
|
||
|
|
resetMinutes,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function incrementRateLimit(clientId: string): void {
|
||
|
|
const now = Date.now();
|
||
|
|
const usage = usageStore.get(clientId);
|
||
|
|
|
||
|
|
if (!usage) {
|
||
|
|
usageStore.set(clientId, { count: 1, firstUsedAt: now });
|
||
|
|
} else {
|
||
|
|
usage.count += 1;
|
||
|
|
}
|
||
|
|
}
|