All files / src/utils limiter.ts

92.1% Statements 35/38
80% Branches 8/10
100% Functions 9/9
91.89% Lines 34/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94                  4x   4x       60x 60x 60x         375x 375x 375x           375x 375x 58x   317x 259x   317x 317x     58x 58x               58x 58x   58x       375x 375x       58x 58x 58x 58x         116x 116x 116x 116x 116x 116x 116x                         60x        
type Callback<T> = () => PromiseLike<T>
 
interface Event<T> {
    getPromise?: Callback<T>
    resolve: (result: T | PromiseLike<T>) => void
    reject: (...args: unknown[]) => void
    number: number
}
 
export class LimiterError extends Error {}
 
export function createLimiter<T = void>(
    interval: number,
    noLimitCount: number
) {
    let currentNumber = 0
    let lastCompletedNumber = 0
    let events: number[] = []
    let timeout: number | undefined
    let lastEvent: Event<T> | undefined
 
    function limited(getPromise?: Callback<T>): PromiseLike<T> {
        return new Promise<T>((resolve, reject) => {
            currentNumber += 1
            const event: Event<T> = {
                getPromise,
                resolve,
                reject,
                number: currentNumber,
            }
            removeOldEvents()
            if (events.length < noLimitCount) {
                execute(event)
            } else {
                if (lastEvent !== undefined) {
                    lastEvent.reject(new LimiterError("rate limit exceeded"))
                }
                lastEvent = event
                if (timeout === undefined) {
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    timeout = setTimeout(() => {
                        timeoutAction()
                    }, interval)
                }
            }
        })
    }
 
    function stop() {
        if (timeout !== undefined) {
            clearTimeout(timeout)
        }
        timeout = undefined
    }
 
    function removeOldEvents() {
        const t = new Date().getTime() - interval * noLimitCount
        events = events.filter(v => v >= t)
    }
 
    function timeoutAction() {
        if (lastEvent !== undefined) {
            execute(lastEvent)
            lastEvent = undefined
            stop()
        }
    }
 
    async function execute(event: Event<T>) {
        events.push(new Date().getTime())
        if (event.getPromise !== undefined) {
            try {
                const result = await event.getPromise()
                if (event.number > lastCompletedNumber) {
                    lastCompletedNumber = event.number
                    event.resolve(result)
                } else E{
                    event.reject(new LimiterError("Got newer event"))
                }
            } catch (e) {
                event.reject(e)
            }
        } else E{
             // @ts-expect-error no result given
            event.resolve()
        }
    }
 
    return {
        limited,
    }
}