From 54670f073be920814ead9ab1f5e6ce66d0014104 Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Fri, 27 Oct 2023 15:13:44 -0400 Subject: [PATCH] make remaining optional also make sorting fall back to limit, and document getRateLimits function in readme --- readme.md | 19 +++++++++++++++---- source/parser.ts | 39 +++++++++++++++++++++++++++++++++------ source/types.ts | 4 ++-- source/utilities.ts | 17 +++++++++++++++-- test/sort-test.ts | 15 +++++++++++++++ 5 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 test/sort-test.ts diff --git a/readme.md b/readme.md index e654833..3bff4fe 100644 --- a/readme.md +++ b/readme.md @@ -80,14 +80,17 @@ For more examples, take a look at the [`examples/`](examples/) folder. Scans the input for ratelimit headers in a variety of formats and returns the result in a consistent format, or undefined if it fails to find any rate-limit -headers. Returns an object with the following fields, or `undefined` if it does -not find any rate-limit headers. +headers. If multiple ratelimits are found, it chooses the one with the lowest +remaining value. + +Returns an object with the following fields, or `undefined` if it does not find +any rate-limit headers. ```ts type RateLimitInfo = { limit: number - used: number - remaining: number + used: number | undefined + remaining: number | undefined reset: Date | undefined } ``` @@ -112,6 +115,14 @@ type Options = { } ``` +### `getRateLimits(responseOrHeaders, [options]) => object[]` + +For APIs that may return multiple rate limits (e.g. per client & per end-user), +this will parse all of them. + +Accepts the same inputs as `getRateLimit` and returns an array containing zero +or more of the same `RateLimitInfo` objects that `getRateLimit` returns. + ## Issues and Contributing If you encounter a bug or want to see something added/changed, please go ahead diff --git a/source/parser.ts b/source/parser.ts index 767e3a6..cf2c83b 100644 --- a/source/parser.ts +++ b/source/parser.ts @@ -7,7 +7,12 @@ import type { RateLimitInfo, ParserOptions, } from './types' -import { secondsToDate, toInt, getHeader } from './utilities.js' +import { + secondsToDate, + toInt, + getHeader, + toIntOrUndefined, +} from './utilities.js' /** * The following links might be referred to in the below lines of code: @@ -40,6 +45,30 @@ export const getRateLimit = ( return rateLimits.length === 0 ? undefined : rateLimits[0] } +/** + * Function to sort an array of RateLimitInfo[] by remaining, then by limit, whith lower values coming first, and undefined remaining values coming after defined ones + * @param a {RateLimitInfo} + * @param b {RateLimitInfo} + * @returns number + */ +export function remainingSortFn(a: RateLimitInfo, b: RateLimitInfo): number { + const aDefined = a.remaining !== undefined + const bDefined = b.remaining !== undefined + if (a.remaining === b.remaining) { + return a.limit - b.limit + } + + if (aDefined && !bDefined) { + return -1 + } + + if (!aDefined && bDefined) { + return 1 + } + + return a.remaining! - b.remaining! +} + /** * Parses the passed response/headers object and returns rate limit information * extracted from ALL rate limit headers the parser can find. @@ -106,9 +135,7 @@ export const getRateLimits = ( ) as RateLimitInfo[] // Sort so that the limit with the lowest remaining value comes first - rateLimits.sort( - (a: RateLimitInfo, b: RateLimitInfo) => a.remaining - b.remaining, - ) + rateLimits.sort(remainingSortFn) return rateLimits } @@ -217,13 +244,13 @@ const reReset = /reset\s*=\s*(\d+)/i */ export const parseDraft7Header = (header: string): RateLimitInfo => { const limit = toInt(reLimit.exec(header)?.[1]) - const remaining = toInt(reRemaining.exec(header)?.[1]) + const remaining = toIntOrUndefined(reRemaining.exec(header)?.[1]) // Optional per https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-07#name-ratelimit const resetSeconds = toInt(reReset.exec(header)?.[1]) const reset = secondsToDate(resetSeconds) return { limit, - used: limit - remaining, + used: typeof remaining === 'number' ? limit - remaining : undefined, remaining, reset, } diff --git a/source/types.ts b/source/types.ts index aa4a8b8..918714e 100644 --- a/source/types.ts +++ b/source/types.ts @@ -31,12 +31,12 @@ export type RateLimitInfo = { /** * The number of requests already made to that endpoint. */ - used: number + used?: number /** * The number of requests that can be made before reaching the rate limit. */ - remaining: number + remaining?: number /** * The timestamp at which the window resets, and one's hit count is set to zero. diff --git a/source/utilities.ts b/source/utilities.ts index e41e004..f2c0bb0 100644 --- a/source/utilities.ts +++ b/source/utilities.ts @@ -21,14 +21,27 @@ export const secondsToDate = (seconds: number): Date => { * * @param input {string | number | undefined} - The input to convert to a number. * - * @return {number} - The parsed integer. - * @throws {Error} - Thrown if the string does not contain a valid number. + * @return {number} - The parsed integer. May be NaN for unparseable input. */ export const toInt = (input: string | number | undefined): number => { if (typeof input === 'number') return input return Number.parseInt(input ?? '', 10) } +/** + * Converts a string/number to a number or undefined. + * + * @param input {string | number | undefined} - The input to convert to a number. + * + * @return {number | undefined} - The parsed integer. + */ +export const toIntOrUndefined = ( + input: string | number | undefined, +): number | undefined => { + const number_ = toInt(input) + return Number.isNaN(number_) ? undefined : number_ +} + /** * Returns a header (or undefined if it's not present) from the passed * node/fetch-style header object. diff --git a/test/sort-test.ts b/test/sort-test.ts new file mode 100644 index 0000000..7370fb5 --- /dev/null +++ b/test/sort-test.ts @@ -0,0 +1,15 @@ +import { describe, test, expect } from '@jest/globals' +import { remainingSortFn } from '../source/parser.js' + +describe('remainingSortFn', () => { + test('Should short with lowest remaining values first and undefined values last', () => { + const unknown70 = { limit: 70 } + const unknown75 = { limit: 75 } + const five100 = { limit: 100, remaining: 5, used: 95 } + const five101 = { limit: 101, remaining: 5, used: 95 } + const ten99 = { limit: 99, remaining: 10, used: 190 } + const infos = [unknown75, five101, ten99, unknown70, five100] + infos.sort(remainingSortFn) + expect(infos).toMatchObject([five100, five101, ten99, unknown70, unknown75]) + }) +})