UIs and event pipelines use two rate-shaping tools:
wait seconds, then run once with the latest arguments (search-as-you-type).interval. The first call runs immediately; calls during the cooldown are collapsed into one trailing call with the latest arguments, which runs when the cooldown ends.Time comes from an injected clock() function (so tests don't sleep), and a tick() method stands in for the event loop's timer.
Implement Debouncer(fn, wait, clock) with call(*args) and tick(), and Throttler(fn, interval, clock) with call(*args) and tick():
call records the arguments and time. tick runs fn(*latest_args) if a call is pending and clock() - last_call >= wait.call runs fn immediately if it has never run or clock() - last_run >= interval. Otherwise it stores the arguments as pending. tick runs the pending call once clock() - last_run >= interval, and that counts as a run.