|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; |
|
|
import { join } from "path"; |
|
|
import { tmpdir } from "os"; |
|
|
|
|
|
interface RateLimitData { |
|
|
|
|
|
requestCount?: number; |
|
|
windowStart?: number; |
|
|
requestTimestamps?: number[]; |
|
|
lastRequestTime: number; |
|
|
isLimited: boolean; |
|
|
retryAfter?: number; |
|
|
processId: string; |
|
|
lastUpdated: number; |
|
|
} |
|
|
|
|
|
export class RateLimitStore { |
|
|
private readonly storeDir: string; |
|
|
private readonly storeFile: string; |
|
|
private readonly processId: string; |
|
|
|
|
|
constructor() { |
|
|
this.storeDir = join(tmpdir(), "duckai"); |
|
|
this.storeFile = join(this.storeDir, "rate-limit.json"); |
|
|
this.processId = `${process.pid}-${Date.now()}`; |
|
|
|
|
|
|
|
|
if (!existsSync(this.storeDir)) { |
|
|
mkdirSync(this.storeDir, { recursive: true }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
read(): RateLimitData | null { |
|
|
try { |
|
|
if (!existsSync(this.storeFile)) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
const data = readFileSync(this.storeFile, "utf8"); |
|
|
|
|
|
|
|
|
if (!data.trim()) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
const parsed: RateLimitData = JSON.parse(data); |
|
|
|
|
|
|
|
|
const now = Date.now(); |
|
|
if (now - parsed.lastUpdated > 5 * 60 * 1000) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
return parsed; |
|
|
} catch (error) { |
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
write(data: Omit<RateLimitData, "processId" | "lastUpdated">): void { |
|
|
try { |
|
|
const storeData: RateLimitData = { |
|
|
...data, |
|
|
processId: this.processId, |
|
|
lastUpdated: Date.now(), |
|
|
}; |
|
|
|
|
|
writeFileSync(this.storeFile, JSON.stringify(storeData, null, 2)); |
|
|
} catch (error) { |
|
|
console.warn("Failed to write rate limit store:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
update(updater: (current: RateLimitData | null) => RateLimitData): void { |
|
|
const current = this.read(); |
|
|
const updated = updater(current); |
|
|
this.write(updated); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clear(): void { |
|
|
try { |
|
|
if (existsSync(this.storeFile)) { |
|
|
const fs = require("fs"); |
|
|
fs.unlinkSync(this.storeFile); |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn("Failed to clear rate limit store:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStorePath(): string { |
|
|
return this.storeFile; |
|
|
} |
|
|
} |
|
|
|