feat: add StatsCache for shared per-model usage polling

This commit is contained in:
c4ch3c4d3
2026-08-14 12:52:40 -06:00
parent 37f7268509
commit a03278645d
2 changed files with 168 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import { fetchStats, type UsageStats } from "./stats";
import { type LlamaSwapConfig } from "./util";
const POLL_MS = 5000;
const ACTIVITY_THROTTLE_MS = 2000;
export type FetchFn = (cfg: LlamaSwapConfig, modelId: string) => Promise<UsageStats | null>;
export class StatsCache {
private keys = new Set<string>();
private values = new Map<string, UsageStats | undefined>();
private listeners = new Set<() => void>();
private timer?: ReturnType<typeof setInterval>;
private throttleTimer?: ReturnType<typeof setTimeout>;
private lastRefresh = 0;
constructor(
private cfg: LlamaSwapConfig,
private fetchFn: FetchFn = fetchStats,
private pollMs = POLL_MS,
private throttleMs = ACTIVITY_THROTTLE_MS,
) {}
setConfig(cfg: LlamaSwapConfig): void {
this.cfg = cfg;
this.values.clear();
}
register(key: string): void {
if (this.keys.has(key)) return;
this.keys.add(key);
if (!this.timer) {
void this.refresh();
this.timer = setInterval(() => void this.refresh(), this.pollMs);
}
}
unregister(key: string): void {
this.keys.delete(key);
this.values.delete(key);
if (this.keys.size === 0 && this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
}
get(key: string): UsageStats | undefined {
return this.values.get(key);
}
scheduleRefresh(): void {
const now = Date.now();
const wait = Math.max(0, this.throttleMs - (now - this.lastRefresh));
if (this.throttleTimer) clearTimeout(this.throttleTimer);
this.throttleTimer = setTimeout(() => {
this.throttleTimer = undefined;
void this.refresh();
}, wait);
}
async refresh(): Promise<void> {
this.lastRefresh = Date.now();
for (const key of this.keys) {
try {
const stats = await this.fetchFn(this.cfg, key);
if (stats) this.values.set(key, stats);
} catch {
}
}
this.emit();
}
onChange(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
private emit(): void {
for (const listener of this.listeners) listener();
}
}