more work on rate limiting

This commit is contained in:
Skillz
2020-03-14 13:13:24 -04:00
parent 633d4700e4
commit 45d49ef441
5 changed files with 57 additions and 99 deletions

View File

@@ -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,

View File

@@ -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<string, Ratelimit> = {};
awaitRatelimit (ratelimit: Ratelimit): Promise<unknown> {
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();
}
}

View File

@@ -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<string, Queued_Request>()
// const ratelimited_paths = new Map<string, Rate_Limited_Path>()
const ratelimited_paths = new Map<string, Rate_Limited_Path>()
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
}

View File

@@ -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<string, Request_Manager>()
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
}
}