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<typeof setTimeout> | 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();
    },
  };
}
