dressed
Version:
A sleek, serverless-ready Discord bot framework.
81 lines • 3.69 kB
JavaScript
import { setTimeout } from "node:timers";
export const buckets = new Map();
export const bucketIds = new Map();
let globalReset = -1;
function ensureBucket(id) {
var _a;
const bucketId = (_a = bucketIds.get(id)) !== null && _a !== void 0 ? _a : id;
if (!buckets.has(bucketId)) {
buckets.set(bucketId, {
limit: 1,
remaining: 1,
refresh: -1,
promise: Promise.resolve(),
});
}
const bucket = buckets.get(bucketId);
// Runtimes like CF workers persist global state between requests except promises -> null
if (!bucket.promise)
bucket.promise = Promise.resolve();
return bucket;
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function checkLimit(req, bucketTTL) {
return new Promise((resolveChecker) => {
const bucketIdKey = `${req.method}:${req.url}`;
const bucket = ensureBucket(bucketIdKey);
bucket.promise = bucket.promise.then(async () => {
const deltaG = globalReset - Date.now();
if (deltaG > 0) {
await delay(deltaG);
}
const bucket = ensureBucket(bucketIdKey);
const deltaB = bucket.refresh - Date.now();
if (bucket.remaining-- === 0) {
await delay(Math.max(0, deltaB));
bucket.remaining = bucket.limit - 1;
}
let resolveRequest;
resolveChecker((res) => {
var _a, _b, _c, _d;
const bucketId = res.headers.get("x-ratelimit-bucket");
const limit = parseInt((_a = res.headers.get("x-ratelimit-limit")) !== null && _a !== void 0 ? _a : "", 10);
const remaining = parseInt((_b = res.headers.get("x-ratelimit-remaining")) !== null && _b !== void 0 ? _b : "", 10);
const resetAfter = parseFloat((_c = res.headers.get("x-ratelimit-reset-after")) !== null && _c !== void 0 ? _c : "");
const retryAfter = parseFloat((_d = res.headers.get("retry-after")) !== null && _d !== void 0 ? _d : "");
const scope = res.headers.get("x-ratelimit-scope");
const refreshAfter = (Number.isNaN(retryAfter) ? resetAfter : retryAfter) * 1000;
const tmpBucket = buckets.get(bucketIdKey);
buckets.delete(bucketIdKey);
if (scope === "global" && !Number.isNaN(retryAfter)) {
globalReset = Date.now() + retryAfter * 1000;
return resolveRequest();
}
if ([limit, remaining, resetAfter].some(Number.isNaN) || !bucketId)
return resolveRequest();
bucketIds.set(bucketIdKey, bucketId);
const bucket = ensureBucket(bucketIdKey);
bucket.limit = limit;
bucket.remaining = remaining;
bucket.refresh = Date.now() + refreshAfter;
bucket.promise = tmpBucket ? bucket.promise.then(() => tmpBucket.promise) : bucket.promise;
clearTimeout(bucket.cleaner);
if (bucketTTL !== -1) {
bucket.cleaner = setTimeout(() => {
buckets.delete(bucketId);
bucketIds.forEach((v, k) => {
v === bucketId && bucketIds.delete(k);
});
}, bucketTTL * 1000).unref();
}
resolveRequest();
});
return new Promise((r) => {
resolveRequest = r;
});
});
});
}
//# sourceMappingURL=ratelimit.js.map