Files
discordeno/rest/createInvalidRequestBucket.ts
Jonathan Ho 5a1887462d Fix: typing (#2589)
* deno fmt

* import SortOrderTypes

* add bigString to reverse snowflake
2022-11-14 15:49:25 -06:00

148 lines
5.1 KiB
TypeScript

import { delay } from "../mod.ts";
/**
* A invalid request bucket is used in a similar manner as a leaky bucket but a invalid request bucket can be refilled as needed.
* It's purpose is to make sure the bot does not hit the limit to getting a 1 hr ban.
*
* @param options The options used to configure this bucket.
* @returns RefillingBucket
*/
export function createInvalidRequestBucket(options: InvalidRequestBucketOptions): InvalidRequestBucket {
const bucket: InvalidRequestBucket = {
current: options.current ?? 0,
max: options.max ?? 10000,
interval: options.interval ?? 600000,
timeoutId: options.timeoutId ?? 0,
safety: options.safety ?? 1,
frozenAt: options.frozenAt ?? 0,
errorStatuses: options.errorStatuses ?? [401, 403, 429],
requested: options.requested ?? 0,
processing: false,
waiting: [],
requestsAllowed: function () {
return bucket.max - bucket.current - bucket.requested - bucket.safety;
},
isRequestAllowed: function () {
return bucket.requestsAllowed() > 0;
},
waitUntilRequestAvailable: async function () {
return new Promise(async (resolve) => {
// If whatever amount of requests is left is more than the safety margin, allow the request
if (bucket.isRequestAllowed()) {
bucket.requested++;
resolve();
} else {
bucket.waiting.push(resolve);
await bucket.processWaiting();
}
});
},
processWaiting: async function () {
// If already processing, that loop will handle all waiting requests.
if (bucket.processing) {
return;
}
// Mark as processing so other loops don't start
bucket.processing = true;
while (bucket.waiting.length) {
if (bucket.isRequestAllowed()) {
bucket.requested++;
// Resolve the next item in the queue
bucket.waiting.shift()?.();
} else {
await delay(1000);
}
}
// Mark as false so next pending request can be triggered by new loop.
bucket.processing = false;
},
handleCompletedRequest: function (code, sharedScope) {
// Since request is complete, we can remove one from requested.
bucket.requested--;
// Since it is as a valid request, we don't need to do anything
if (!bucket.errorStatuses.includes(code)) return;
// Shared scope is not considered invalid
if (code === 429 && sharedScope) return;
// INVALID REQUEST WAS MADE
// If it was not frozen before, mark it frozen
if (!bucket.frozenAt) bucket.frozenAt = Date.now();
// Mark a request has been invalid
bucket.current++;
// If a timeout was not started, start a timeout to reset this bucket
if (!bucket.timeoutId) {
bucket.timeoutId = setTimeout(() => {
bucket.frozenAt = 0;
bucket.current = 0;
bucket.timeoutId = 0;
}, bucket.frozenAt + bucket.interval);
}
},
};
return bucket;
}
export interface InvalidRequestBucketOptions {
/** current invalid amount */
current?: number;
/** max invalid requests allowed until ban. Defaults to 10,000 */
max?: number;
/** The time that discord allows to make the max number of invalid requests. Defaults to 10 minutes */
interval?: number;
/** timer to reset to 0 */
timeoutId?: number;
/** how safe to be from max. Defaults to 1 */
safety?: number;
/** when first request in this period was made */
frozenAt?: number;
/** The request statuses that count as an invalid request. */
errorStatuses?: number[];
/** The amount of requests that were requested from this bucket. */
requested?: number;
}
export interface InvalidRequestBucket {
/** current invalid amount */
current: number;
/** max invalid requests allowed until ban. Defaults to 10,000 */
max: number;
/** The time that discord allows to make the max number of invalid requests. Defaults to 10 minutes */
interval: number;
/** timer to reset to 0 */
timeoutId: number;
/** how safe to be from max. Defaults to 1 */
safety: number;
/** when first request in this period was made */
frozenAt: number;
/** The request statuses that count as an invalid request. */
errorStatuses: number[];
/** The amount of requests that were requested from this bucket. */
requested: number;
/** The requests that are currently pending. */
waiting: ((value: void | PromiseLike<void>) => void)[];
/** Whether or not the waiting queue is already processing. */
processing: boolean;
/** Gives the number of requests that are currently allowed. */
requestsAllowed: () => number;
/** Checks if a request is allowed at this time. */
isRequestAllowed: () => boolean;
/** Waits until a request is available */
waitUntilRequestAvailable: () => Promise<void>;
/** Begins processing the waiting queue of requests. */
processWaiting: () => Promise<void>;
/** Handler for whenever a request is validated. This should update the requested values or trigger any other necessary stuff. */
handleCompletedRequest: (code: number, sharedScope: boolean) => void;
}