export interface RateLimitedPublisher { request: () => void; cancel: () => void; dispose: () => void; } /** * Coalesces bursty state updates while preserving an immediate leading publish. * The trailing publish always contains current state because `publish` reads it * when the timer fires. */ export function createRateLimitedPublisher( publish: () => void, intervalMs: number, now: () => number = () => performance.now(), ): RateLimitedPublisher { let lastPublishedAt = Number.NEGATIVE_INFINITY; let timer: ReturnType | null = null; let disposed = false; const run = () => { timer = null; if (disposed) return; lastPublishedAt = now(); publish(); }; const cancel = () => { if (timer !== null) clearTimeout(timer); timer = null; }; return { request: () => { if (disposed) return; const remaining = intervalMs - (now() - lastPublishedAt); if (remaining <= 0) { if (timer !== null) clearTimeout(timer); run(); return; } if (timer === null) timer = setTimeout(run, remaining); }, cancel, dispose: () => { disposed = true; cancel(); }, }; }