Source: utils/debouncer.js


/**
 * A class that debounces function calls, ensuring that a function is not called more often than a specified delay.
 * @alias qui.utils.Debouncer
 */
class Debouncer {

    /**
     * @constructs
     * @param {Function} func the function to debounce
     * @param {Number} [delay] the debouncing delay, in milliseconds (defaults to `0`)
     * @param {?Number} [maxWait] the longest the call may be postponed, in milliseconds, however many times it is
     * made; `null` (the default) postpones it indefinitely
     */
    constructor(func, delay = 0, maxWait = null) {
        this._func = func
        this._delay = delay
        this._maxWait = maxWait
        this._timeoutHandle = null
        this._firstCallTime = null
    }

    /**
     * Call function ensuring debouncing condition. Any previous pending call is cancelled.
     * Arguments are passed to the function when called.
     */
    call(...args) {
        if (this._timeoutHandle !== null) {
            clearTimeout(this._timeoutHandle)
        }

        let delay = this._delay

        if (this._maxWait != null) {
            if (this._firstCallTime == null) {
                this._firstCallTime = Date.now()
            }

            /* Each call pushes the deadline back by the full delay, so a stream of calls arriving faster than the
             * delay postpones the function for as long as it lasts -- which reads as a frozen UI rather than a slow
             * one. Never postpone it past maxWait from the first call of the run. */
            delay = Math.min(delay, Math.max(0, this._maxWait - (Date.now() - this._firstCallTime)))
        }

        this._timeoutHandle = setTimeout(function () {
            this._timeoutHandle = null
            this._firstCallTime = null
            this._func(...args)
        }.bind(this), delay)
    }

    /**
     * Tell if there is a pending call.
     * @return {Boolean}
     */
    isPending() {
        return this._timeoutHandle != null
    }

}

export default Debouncer