export type CallbackType = ( ... _ : unknown [ ] ) => void ; export function throttle ( callback , waitInMs , timer = NodeTimer ) { const throttler = new Throttler ( timer , waitInMs , callback ) ; return ( ... args ) => throttler . invoke ( ... args ) ; } export type TimeoutType = ReturnType < typeof setTimeout > ; export interface ITimer { setTimeout : ( callback : ( ) => void , ms : number ) => TimeoutType ; clearTimeout : ( timeoutId : TimeoutType ) => void ; dateNow ( ) : number ; } const NodeTimer = { setTimeout : ( callback , ms ) => setTimeout ( callback , ms ) , clearTimeout : ( timeoutId ) => clearTimeout ( timeoutId ) , dateNow : ( ) => Date . now ( ) , } ; interface IThrottler { invoke : CallbackType ; } class Throttler implements IThrottler { private queuedExecutionId : TimeoutType ; private previouslyRun : number ; constructor ( private readonly timer , private readonly waitInMs , private readonly callback ) { if ( ! timer ) { throw new Error ( 'undefined timer' ) ; } if ( ! waitInMs ) { throw new Error ( 'no delay to throttle' ) ; } if ( waitInMs < 0 ) { throw new Error ( 'negative delay' ) ; } if ( ! callback ) { throw new Error ( 'undefined callback' ) ; } } public invoke ( ... args ) { const now = this . timer . dateNow ( ) ; if ( this . queuedExecutionId !== undefined ) { this . timer . clearTimeout ( this . queuedExecutionId ) ; this . queuedExecutionId = undefined ; } if ( ! this . previouslyRun || ( now - this . previouslyRun >= this . waitInMs ) ) { this . callback ( ... args ) ; this . previouslyRun = now ; } else { const nextCall = ( ) => this . invoke ( ... args ) ; const nextCallDelayInMs = this . waitInMs - ( now - this . previouslyRun ) ; this . queuedExecutionId = this . timer . setTimeout ( nextCall , nextCallDelayInMs ) ; } } }