diff --git a/module/client.ts b/module/client.ts index 6929c134b..546952789 100644 --- a/module/client.ts +++ b/module/client.ts @@ -9,7 +9,7 @@ import { Typing_Start_Payload, Voice_State_Update_Payload } from "../types/discord.ts" -import { spawnShards } from "./sharding-manager.ts" +import { spawnShards } from "./sharding_manager.ts" import { connectWebSocket, isWebSocketCloseEvent, diff --git a/module/ratelimiter.ts b/module/ratelimiter.ts deleted file mode 100644 index 3fe8221ea..000000000 --- a/module/ratelimiter.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { sleep } from "../utils/utils.ts"; - -export interface Ratelimit { - retryAfter: number; - limit: number; - remaining: number; - reset: number; -} - -export class Ratelimiter { - buckets: Record = {}; - - awaitRatelimit (ratelimit: Ratelimit): Promise { - if (ratelimit.remaining === 0) { - return sleep(ratelimit.retryAfter); - } - - return Promise.resolve(); - } - - addBucket (bucket: string, ratelimit: Ratelimit) { - if (this.buckets[bucket]) { - return; - } - - // Otherwise, add this ratelimit to the registry. - this.buckets[bucket] = ratelimit; - } - - async awaitBucket (bucket: string) { - if (this.buckets[bucket]) { - // POSSIBLE MEMORY LEAK: Some buckets might never get cleaned up. - await this.awaitRatelimit(this.buckets[bucket]); - - // IIRC, we avoid `delete` so v8 doesn't deoptimize this? - this.buckets[bucket] = undefined as any; - } - - return Promise.resolve(); - } -} diff --git a/module/request_manager.ts b/module/request_manager.ts index 78e17eef0..f26617973 100644 --- a/module/request_manager.ts +++ b/module/request_manager.ts @@ -1,18 +1,24 @@ import { RequestMethod } from "../types/fetch.ts" import { authorization } from "./client.ts" +import { sleep } from "../utils/utils.ts" // const queue = new Map() -// const ratelimited_paths = new Map() +const ratelimited_paths = new Map() + +export interface Rate_Limited_Path { + url: string + reset_timestamp: number +} export const Request_Manager = { // Something off about using run_method with get breaks when using fetch - get: (url: string, body?: unknown) => { - // TODO: Check rate limit - + get: async (url: string, body?: unknown) => { + await check_ratelimits(url) const result = await fetch(url, create_request_body(body)) // TODO: Handle rate limiting - console.log('GET headers', result.headers) + console.log("GET headers", result.headers) + process_headers(url, result.headers) return result.json() }, @@ -42,9 +48,8 @@ const create_request_body = (body: unknown, method?: RequestMethod) => { } } -const run_method = (method: RequestMethod, url: string, body?: unknown) => { - // TODO: Check if this url is rate limited - +const run_method = async (method: RequestMethod, url: string, body?: unknown) => { + await check_ratelimits(url) const response = await fetch(url, create_request_body(body, method)) // TODO: Handle ratelimiting @@ -52,3 +57,46 @@ const run_method = (method: RequestMethod, url: string, body?: unknown) => { return response.json() } + +const check_ratelimits = async (url: string) => { + const ratelimited = ratelimited_paths.get(url) + const global = ratelimited_paths.get("global") + + const now = Date.now() + if (ratelimited && now < ratelimited.reset_timestamp) await sleep(now - ratelimited.reset_timestamp) + if (global && now < global.reset_timestamp) await sleep(now - global.reset_timestamp) +} + +const process_headers = (url: string, headers: Headers) => { + // If a rate limit response is encountered this will become true and returned + let ratelimited = false + + // Get all useful headers + const remaining = headers.get("x-ratelimit-remaining") + const reset_timestamp = headers.get("x-ratelimit-reset") + const retry_after = headers.get('retry-after') + const global = headers.get('x-ratelimit-global') + + // If there is no remaining rate limit for this endpoint, we save it in cache + if (remaining && remaining === "0") { + ratelimited = true + + ratelimited_paths.set(url, { + url, + reset_timestamp: Number(reset_timestamp) + }) + } + + // If there is no remaining global limit, we save it in cache + if (global) { + ratelimited = true + + ratelimited_paths.set('global', { + url: 'global', + reset_timestamp: Date.now() + Number(retry_after) + }) + } + + // Returns a boolean to check if we need to request again once the rate limit resets + return ratelimited +} diff --git a/module/routed-request-manager.ts b/module/routed-request-manager.ts deleted file mode 100644 index f0a37cac2..000000000 --- a/module/routed-request-manager.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Request_Manager from "./request_manager.ts" -import Client from "./client.ts" -import { resolveURLs } from "./url.ts" -import { baseEndpoints } from "../constants/discord.ts" -import { Ratelimit, Ratelimiter } from "./ratelimiter.ts" -import { RequestMethod } from "../types/fetch.ts" - -export class RouteAwareRequest_Manager extends Request_Manager { - protected currentRatelimit?: Ratelimit - ratelimiter = new Ratelimiter() - - constructor(public client: Client, public routeName: string) { - super(client) - } - - protected resolveURL(url: string) { - return resolveURLs(baseEndpoints.BASE_URL, this.routeName, url) - } - - async runMethod(method: RequestMethod, url: string, body?: unknown) { - if (this.currentRatelimit) { - await this.ratelimiter.awaitRatelimit(this.currentRatelimit) - } - - const response = await this.baseCreateRequestForMethod(method, url, body) - - // Capture the ratelimit from this request in our cute little store. - - return response.json() - } - - protected createRatelimitFromRequest(_request: Request) {} -} - -export class RoutedRequest_Manager { - protected routeMap = new Map() - - constructor(public client: Client) {} - - forRoute(routeName: string) { - if (this.routeMap.has(routeName)) { - return this.routeMap.get(routeName) - } - - const routeRequestManager = new RouteAwareRequest_Manager(this.client, routeName) - this.routeMap.set(routeName, routeRequestManager) - return routeRequestManager - } -} diff --git a/module/sharding-manager.ts b/module/sharding_manager.ts similarity index 100% rename from module/sharding-manager.ts rename to module/sharding_manager.ts