Migrated git root from W:/programming/Projects/ to W:/programming/Projects/Tubearr/. Previous history preserved in Tubearr-full-backup.bundle at parent directory. Completed milestones: M001 through M005 Active: M006/S02 (Add Channel UX)
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import type { Platform } from '../types/index';
|
|
|
|
// ── Types ──
|
|
|
|
export interface RateLimiterConfig {
|
|
minIntervalMs: number;
|
|
}
|
|
|
|
export interface PlatformRateLimitState {
|
|
lastCallTime: number | null;
|
|
errorCount: number;
|
|
effectiveIntervalMs: number;
|
|
}
|
|
|
|
// ── Constants ──
|
|
|
|
/** Maximum backoff interval — 60 seconds. */
|
|
const MAX_BACKOFF_MS = 60_000;
|
|
|
|
// ── Rate Limiter ──
|
|
|
|
/**
|
|
* Per-platform rate limiter with exponential backoff.
|
|
*
|
|
* `acquire(platform)` waits until the minimum interval has elapsed since the
|
|
* last call for that platform. `reportError` doubles the effective interval
|
|
* (up to MAX_BACKOFF_MS). `reportSuccess` resets to the configured minimum.
|
|
*/
|
|
export class RateLimiter {
|
|
private readonly config: Record<string, RateLimiterConfig>;
|
|
private readonly state = new Map<string, PlatformRateLimitState>();
|
|
|
|
constructor(config: Partial<Record<Platform, RateLimiterConfig>>) {
|
|
this.config = config as Record<string, RateLimiterConfig>;
|
|
|
|
// Initialize state for each configured platform
|
|
for (const [platform, cfg] of Object.entries(this.config)) {
|
|
this.state.set(platform, {
|
|
lastCallTime: null,
|
|
errorCount: 0,
|
|
effectiveIntervalMs: cfg.minIntervalMs,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait until enough time has elapsed since the last call for this platform.
|
|
* Updates lastCallTime after the wait completes.
|
|
*/
|
|
async acquire(platform: Platform): Promise<void> {
|
|
const state = this.getOrCreateState(platform);
|
|
const now = Date.now();
|
|
|
|
if (state.lastCallTime !== null) {
|
|
const elapsed = now - state.lastCallTime;
|
|
const remaining = state.effectiveIntervalMs - elapsed;
|
|
|
|
if (remaining > 0) {
|
|
console.log(
|
|
`[rate-limiter] ${platform}: waiting ${remaining}ms (effective interval: ${state.effectiveIntervalMs}ms)`
|
|
);
|
|
await delay(remaining);
|
|
}
|
|
}
|
|
|
|
state.lastCallTime = Date.now();
|
|
}
|
|
|
|
/**
|
|
* Report a rate-limit or transient error for a platform.
|
|
* Doubles the effective interval (exponential backoff) up to MAX_BACKOFF_MS.
|
|
*/
|
|
reportError(platform: Platform): void {
|
|
const state = this.getOrCreateState(platform);
|
|
state.errorCount++;
|
|
const cfg = this.config[platform];
|
|
const baseInterval = cfg?.minIntervalMs ?? 1000;
|
|
state.effectiveIntervalMs = Math.min(
|
|
baseInterval * Math.pow(2, state.errorCount),
|
|
MAX_BACKOFF_MS
|
|
);
|
|
|
|
console.log(
|
|
`[rate-limiter] ${platform}: backoff — errorCount=${state.errorCount}, effectiveInterval=${state.effectiveIntervalMs}ms`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Report a successful call for a platform.
|
|
* Resets error count and effective interval to the configured minimum.
|
|
*/
|
|
reportSuccess(platform: Platform): void {
|
|
const state = this.getOrCreateState(platform);
|
|
const cfg = this.config[platform];
|
|
const baseInterval = cfg?.minIntervalMs ?? 1000;
|
|
state.errorCount = 0;
|
|
state.effectiveIntervalMs = baseInterval;
|
|
}
|
|
|
|
/**
|
|
* Get the current state of all platforms for diagnostic inspection.
|
|
*/
|
|
getState(): Record<string, PlatformRateLimitState> {
|
|
const result: Record<string, PlatformRateLimitState> = {};
|
|
for (const [platform, state] of this.state) {
|
|
result[platform] = { ...state };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ── Internal ──
|
|
|
|
private getOrCreateState(platform: Platform): PlatformRateLimitState {
|
|
let state = this.state.get(platform);
|
|
if (!state) {
|
|
const cfg = this.config[platform];
|
|
const minInterval = cfg?.minIntervalMs ?? 1000;
|
|
state = {
|
|
lastCallTime: null,
|
|
errorCount: 0,
|
|
effectiveIntervalMs: minInterval,
|
|
};
|
|
this.state.set(platform, state);
|
|
}
|
|
return state;
|
|
}
|
|
}
|
|
|
|
// ── Helpers ──
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|