import { expect } from 'chai' import { afterEach, beforeEach, describe, it } from 'mocha' import sinon from 'sinon' import { LeakyBucket } from '../src/bucket.js' async function promiseState(p: Promise): Promise { const t = {} return await Promise.race([p, t]).then( (v) => (v === t ? 'pending' : 'fulfilled'), () => 'rejected', ) } describe('bucket.ts', () => { let clock: sinon.SinonFakeTimers beforeEach(() => { clock = sinon.useFakeTimers() }) afterEach(() => { sinon.restore() clock.restore() }) describe('LeakyBucket function', () => { it('will return bucket with given options', () => { const options = { max: 6006, refillInterval: 2002, refillAmount: 3003, tokens: 4004, someThingElse: { thing: 'else', }, } const bucket = new LeakyBucket(options) expect(bucket.max).to.equal(options.max) expect(bucket.refillInterval).to.equal(options.refillInterval) expect(bucket.refillAmount).to.equal(options.refillAmount) }) it('will return bucket with refillAmount within max', () => { const options = { max: 111, refillInterval: 2002, refillAmount: 3003, tokens: 4004, } const bucket = new LeakyBucket(options) expect(bucket.refillAmount).to.equal(options.max) }) it('will return bucket with tokensState within max', () => { const options = { max: 111, refillInterval: 2002, refillAmount: 3003, tokens: 4004, } const bucket = new LeakyBucket(options) expect(bucket.refillAmount).to.equal(options.max) }) it('will return bucket with default property', () => { const bucket = new LeakyBucket() expect(bucket.max).equals(1) expect(bucket.refillInterval).equals(5000) expect(bucket.refillAmount).equals(1) expect(bucket.queue).to.deep.equal([]) }) it('will acquire a request', async () => { const bucket = new LeakyBucket({ max: 120, refillInterval: 60000, refillAmount: 120, }) await bucket.acquire(true) expect(bucket.remaining).to.be.equal(119) expect(bucket.used).to.be.equal(1) }) it('will handle multiple requests at once', async () => { const bucket = new LeakyBucket({ max: 120, refillInterval: 60000, refillAmount: 120, }) for (let i = 0; i < 10; i++) { bucket.acquire() } }) it('will handle too many requests', async () => { const bucket = new LeakyBucket({ max: 5, refillInterval: 10000, refillAmount: 5, }) for (let i = 0; i < 10; i++) { bucket.acquire() } }) it('bucket refills are done properly', async () => { const bucket = new LeakyBucket({ max: 2, refillInterval: 500, refillAmount: 2, }) await bucket.acquire() expect(bucket.remaining).equals(1) expect(bucket.used).equals(1) await clock.tickAsync(1000) expect(bucket.remaining).equals(2) expect(bucket.used).equals(0) await bucket.acquire() await clock.tickAsync(1000) }) it('bucket refills when refill amount is < max', async () => { const bucket = new LeakyBucket({ max: 3, refillInterval: 800, refillAmount: 1, }) await bucket.acquire() await bucket.acquire() expect(bucket.remaining).equals(1) expect(bucket.used).equals(2) await clock.tickAsync(1000) expect(bucket.remaining).equals(2) expect(bucket.used).equals(1) await clock.tickAsync(2000) expect(bucket.remaining).equals(3) expect(bucket.used).equals(0) }) it('bucket refills when refill interval is slow', async () => { const bucket = new LeakyBucket({ max: 1, refillInterval: 500, refillAmount: 1, }) const acquired1 = bucket.acquire() const acquired2 = bucket.acquire() // js event loop await (async () => {})() expect(await promiseState(acquired1)).to.equal('fulfilled') expect(await promiseState(acquired2)).to.equal('pending') await clock.tickAsync(499) expect(await promiseState(acquired2)).to.equal('pending') await clock.tickAsync(1) expect(await promiseState(acquired2)).to.equal('fulfilled') expect(bucket.remaining).equals(0) expect(bucket.used).equals(1) }) describe('remaining', () => { it('should be 0 even used too many', () => { const bucket = new LeakyBucket({ max: 1, refillInterval: 500, refillAmount: 1, }) // max is < used bucket.used = 2 expect(bucket.remaining).equals(0) }) }) it("Don't process queue twice", () => { const bucket = new LeakyBucket({ max: 1, refillInterval: 500, refillAmount: 1, }) // fake processing bucket.processing = true // request when already processing bucket.processQueue() }) }) })