diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index b35b12b..ae4ea83 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -80,8 +80,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_USERNAME: ${{ github.actor }} run: | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN - git config --global user.name "Autobot" - git config --global user.email "ci@github.com" + git config --global user.email "${GH_USERNAME}@users.noreply.github.com" + git config --global user.name "${GH_USERNAME}" yarn release diff --git a/README.md b/README.md index b6d46c5..c13e298 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ ## Features - **Universal** - Works in all modern browsers, [Node.js](https://nodejs.org/), and [Deno](https://deno.land/) and supports [CLI](https://www.npmjs.com/package/@gitbeaker/cli) usage. -- **Tested** - Greater than 80% test coverage. +- **Zero Dependencies** - Absolutely no dependencies, keeping the package tiny (24kb). +- **Tested** - Greater than 85% test coverage. - **Typed** - Out of the box TypeScript declarations. ## Usage diff --git a/src/Deque.ts b/src/Deque.ts index 9659f57..8096c70 100644 --- a/src/Deque.ts +++ b/src/Deque.ts @@ -1,5 +1,5 @@ // Deque is based on https://github.com/petkaantonov/deque/blob/master/js/deque.js - +// Released under the MIT License: https://github.com/petkaantonov/deque/blob/6ef4b6400ad3ba82853fdcc6531a38eb4f78c18c/LICENSE /*eslint-disable*/ function arrayMove(src: any[], srcIndex: number, dst: any[], dstIndex: number, len: number) { for (let j = 0; j < len; ++j) { @@ -16,15 +16,18 @@ function pow2AtLeast(n: number) { n |= n >> 4; n |= n >> 8; n |= n >> 16; + return n + 1; } -function getCapacity(capacity: number) { - return pow2AtLeast(Math.min(Math.max(16, capacity), 1073741824)); +export const MIN_CAPACITY = 4; +export const MAX_CAPACITY = 1073741824; +export const RESIZE_MULTIPLER = 1; + +export function getCapacity(capacity: number) { + return pow2AtLeast(Math.min(Math.max(MIN_CAPACITY, capacity), MAX_CAPACITY)); } -// Deque is based on https://github.com/petkaantonov/deque/blob/master/js/deque.js -// Released under the MIT License: https://github.com/petkaantonov/deque/blob/6ef4b6400ad3ba82853fdcc6531a38eb4f78c18c/LICENSE export class Deque { private _capacity: number; @@ -34,8 +37,8 @@ export class Deque { private arr: Array; - constructor(capacity: number) { - this._capacity = getCapacity(capacity); + constructor(initialCapacity: number = 4) { + this._capacity = getCapacity(initialCapacity); this._length = 0; this._front = 0; this.arr = []; @@ -93,7 +96,7 @@ export class Deque { private checkCapacity(size: number) { if (this._capacity < size) { - this.resizeTo(getCapacity(this._capacity * 1.5 + 16)); + this.resizeTo(getCapacity(this._capacity * RESIZE_MULTIPLER + MIN_CAPACITY)); } } diff --git a/src/Utils.ts b/src/Utils.ts index bb2ff19..2990357 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -10,6 +10,12 @@ export function createRateLimiter( uniformDistribution?: boolean; } = {}, ): () => Promise { + if (!Number.isInteger(rptu) || rptu < 0) { + throw new TypeError( + 'The rate-limit-per-time-unit (rptu) should be an integer and greater than zero', + ); + } + const sema = new Sema(uniformDistribution ? 1 : rptu); const delay = uniformDistribution ? timeUnit / rptu : timeUnit; diff --git a/test/Deque.ts b/test/Deque.ts index 9e42bfd..4dd0900 100644 --- a/test/Deque.ts +++ b/test/Deque.ts @@ -1,82 +1,126 @@ -import { Sema } from '../src/Sema'; +import { Deque, MAX_CAPACITY, getCapacity } from '../src/Deque'; -import { createRateLimiter } from '../src/Utils'; +describe('MAX_CAPACITY', () => { + it('should restrict max capacity to 1gb', () => { + expect(MAX_CAPACITY).toBe(1073741824); + }); +}); -jest.useFakeTimers(); -jest.spyOn(global, 'setTimeout'); +describe('getCapacity', () => { + it('should accept a initial capacity for the queue, with a default of 4 if passed value is less than 4', () => { + const d = getCapacity(2); -const acquireFn = jest.fn(); -const releaseFn = jest.fn(); + expect(d).toBe(4); + }); -jest.mock('../src/Sema', () => { - return { - Sema: jest.fn(() => ({ - acquire: acquireFn, - release: releaseFn, - })), - }; -}); + it('should increment the capacity through a bit shift for every value greater than the previous bit shift limit', () => { + const d1 = getCapacity(5); -describe('General', () => { - afterEach(() => { - jest.clearAllMocks(); - }); + expect(d1).toBe(8); - it('should create a Sema instance with one maxConcurrency if uniform distribution is true', () => { - createRateLimiter(3, { uniformDistribution: true }); + const d2 = getCapacity(17); - expect(Sema).toHaveBeenCalledWith(1); + expect(d2).toBe(32); }); - it('should create a Sema instance with passed maxConcurrency if uniform distribution is false', () => { - createRateLimiter(3, { uniformDistribution: false }); + it('should restrict capcity to max buffer size (MAX_CAPACITY)', () => { + const d1 = getCapacity(MAX_CAPACITY + 1); - expect(Sema).toHaveBeenCalledWith(3); + expect(d1).toBe(MAX_CAPACITY); }); +}); - it('should create a Sema instance with passed maxConcurrency if a uniform distribution is not passed', () => { - createRateLimiter(3); +describe('Deque.constructor', () => { + it('should accept no arguments and default to a capacity of 4', () => { + const d = new Deque(); - expect(Sema).toHaveBeenCalledWith(3); - }); + /* eslint-disable @typescript-eslint/ban-ts-comment, no-underscore-dangle */ + + // @ts-expect-error + expect(d._capacity).toBe(4); + + // @ts-expect-error + expect(d._length).toBe(0); - it('should create a timeout using default timeUnit as delay if uniformDistribution is not passed', async () => { - const limiter = createRateLimiter(3); + // @ts-expect-error + expect(d.arr.length).toBe(0); - await limiter(); + // @ts-expect-error + expect(d._front).toBe(0); - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000); + /* eslint-enable */ }); - it('should create a timeout using passed timeUnit as delay if uniformDistribution is not passed', async () => { - const limiter = createRateLimiter(3, { timeUnit: 2000 }); + it('should accept a capacity argument', () => { + const d = new Deque(32); + + /* eslint-disable @typescript-eslint/ban-ts-comment, no-underscore-dangle */ + + // @ts-expect-error + expect(d._capacity).toBe(32); - await limiter(); + // @ts-expect-error + expect(d._length).toBe(0); - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000); + // @ts-expect-error + expect(d.arr.length).toBe(0); + + // @ts-expect-error + expect(d._front).toBe(0); + + /* eslint-enable */ }); - it('should create a timeout using passed timeUnit as delay if uniformDistribution is false', async () => { - const limiter = createRateLimiter(3, { uniformDistribution: false, timeUnit: 2000 }); + it('should default to 4 capacity if capacity passed is lesser', () => { + const d = new Deque(2); + + /* eslint-disable @typescript-eslint/ban-ts-comment, no-underscore-dangle */ + + // @ts-expect-error + expect(d._capacity).toBe(4); + + // @ts-expect-error + expect(d._length).toBe(0); + + // @ts-expect-error + expect(d.arr.length).toBe(0); - await limiter(); + // @ts-expect-error + expect(d._front).toBe(0); - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000); + /* eslint-enable */ }); +}); - it('should create a timeout using passed timeUnit divided by requests per time unit if uniformDistribution is true', async () => { - const limiter = createRateLimiter(2, { uniformDistribution: true, timeUnit: 10000 }); +describe('Deque.push', () => { + it('should accept an item to add to the queue and increment the length', () => { + const d = new Deque(); - await limiter(); + d.push(1); - expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 5000); + expect(d.length).toBe(1); }); - it('should call Sema.aquire in limiter function', async () => { - const limiter = createRateLimiter(2); + it('should update capacity if more than capacity items are added', () => { + const d = new Deque(); + + d.push(1); + d.push(1); + d.push(1); + d.push(1); + const finalLength = d.push(1); + + expect(finalLength).toBe(5); + expect(d.length).toBe(5); + + /* eslint-disable @typescript-eslint/ban-ts-comment, no-underscore-dangle */ + + // @ts-expect-error + expect(d._capacity).toBe(8); - await limiter(); + // @ts-expect-error + expect(d.arr).toMatchObject([1, 1, 1, 1, 1]); - expect(acquireFn).toHaveBeenCalledTimes(1); + /* eslint-enable */ }); });