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                  5x   5x       61x 61x 61x         378x 378x 378x           378x 378x 59x   319x 260x   319x 319x     59x 58x               58x 58x   58x       378x 378x       58x 58x 58x 58x         117x 117x 117x 117x 117x 117x 117x                         61x        
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,
  }
}