lunibee 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +223 -223
- package/dist/builders/commands.d.ts +6 -4
- package/dist/builders/components.d.ts +18 -0
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +49 -13
- package/dist/builders/index.js.map +9 -8
- package/dist/collection/index.js +2 -1
- package/dist/collection/index.js.map +4 -4
- package/dist/core/events.d.ts +86 -2
- package/dist/core/index.d.ts +10 -87
- package/dist/core/index.js +1752 -725
- package/dist/core/index.js.map +44 -24
- package/dist/formatters/index.js.map +1 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2103 -800
- package/dist/index.js.map +63 -43
- package/dist/managers/guild-resources.d.ts +109 -0
- package/dist/managers/guild.d.ts +5 -0
- package/dist/managers/index.d.ts +7 -1
- package/dist/managers/index.js +330 -12
- package/dist/managers/index.js.map +25 -21
- package/dist/rest/decoder.d.ts +23 -0
- package/dist/rest/errors.d.ts +20 -0
- package/dist/rest/index.d.ts +21 -17
- package/dist/rest/index.js +479 -263
- package/dist/rest/index.js.map +14 -8
- package/dist/rest/limiter.d.ts +42 -0
- package/dist/rest/redis.d.ts +18 -1
- package/dist/rest/route.d.ts +31 -0
- package/dist/rest/routes.d.ts +2 -0
- package/dist/rest/scheduler.d.ts +42 -0
- package/dist/rest/store.d.ts +29 -0
- package/dist/rest/transport.d.ts +40 -0
- package/dist/sharding/bus.d.ts +31 -0
- package/dist/sharding/cluster.d.ts +10 -0
- package/dist/sharding/index.d.ts +10 -2
- package/dist/sharding/index.js +1011 -394
- package/dist/sharding/index.js.map +18 -8
- package/dist/structures/base.d.ts +8 -0
- package/dist/structures/channels.d.ts +56 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +211 -271
- package/dist/structures/index.js.map +17 -14
- package/dist/structures/interactions.d.ts +12 -52
- package/dist/structures/options.d.ts +55 -0
- package/dist/structures/resources.d.ts +3 -3
- package/dist/types/gateway-events.d.ts +132 -0
- package/dist/types/gateway.d.ts +181 -0
- package/dist/types/index.d.ts +2 -263
- package/dist/types/index.js +44 -2
- package/dist/types/index.js.map +5 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/voice/index.d.ts +29 -1
- package/dist/voice/index.js +59 -7
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/close-codes.d.ts +23 -0
- package/dist/ws/decoder.d.ts +58 -0
- package/dist/ws/heartbeat.d.ts +87 -0
- package/dist/ws/index.d.ts +18 -65
- package/dist/ws/index.js +903 -385
- package/dist/ws/index.js.map +15 -5
- package/dist/ws/opcodes.d.ts +14 -0
- package/dist/ws/protocol.d.ts +110 -0
- package/dist/ws/reconnect.d.ts +108 -0
- package/dist/ws/send-budget.d.ts +17 -0
- package/dist/ws/session.d.ts +100 -0
- package/dist/ws/state.d.ts +27 -0
- package/dist/ws/transport.d.ts +80 -0
- package/package.json +21 -20
package/dist/rest/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// packages/rest/package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@lunibee/rest",
|
|
5
|
-
version: "0.1.
|
|
5
|
+
version: "0.1.8",
|
|
6
6
|
type: "module",
|
|
7
7
|
module: "./src/index.ts",
|
|
8
8
|
types: "./src/index.ts",
|
|
@@ -27,6 +27,17 @@ class MemoryRateLimitStore {
|
|
|
27
27
|
getBucket(key) {
|
|
28
28
|
return this.#buckets.get(key);
|
|
29
29
|
}
|
|
30
|
+
reserve(key) {
|
|
31
|
+
const state = this.#buckets.get(key);
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (!state || state.resetAt <= now)
|
|
34
|
+
return { granted: true };
|
|
35
|
+
if (state.remaining > 0) {
|
|
36
|
+
state.remaining -= 1;
|
|
37
|
+
return { granted: true };
|
|
38
|
+
}
|
|
39
|
+
return { granted: false, retryAfterMs: state.resetAt - now };
|
|
40
|
+
}
|
|
30
41
|
updateBucket(key, state) {
|
|
31
42
|
this.#buckets.set(key, state);
|
|
32
43
|
const now = Date.now();
|
|
@@ -50,6 +61,301 @@ class MemoryRateLimitStore {
|
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
// packages/rest/src/errors.ts
|
|
65
|
+
class RESTError extends Error {
|
|
66
|
+
status;
|
|
67
|
+
code;
|
|
68
|
+
errors;
|
|
69
|
+
method;
|
|
70
|
+
path;
|
|
71
|
+
constructor(message, status, code, errors, options = {}) {
|
|
72
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
73
|
+
this.name = "RESTError";
|
|
74
|
+
this.status = status;
|
|
75
|
+
this.code = code;
|
|
76
|
+
this.errors = errors;
|
|
77
|
+
this.method = options.method;
|
|
78
|
+
this.path = options.path;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function abortError(path, reason) {
|
|
82
|
+
return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason });
|
|
83
|
+
}
|
|
84
|
+
async function sleep(delay, signal, path) {
|
|
85
|
+
if (delay <= 0)
|
|
86
|
+
return;
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(resolve, delay);
|
|
89
|
+
const abort = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(abortError(path, signal?.reason));
|
|
92
|
+
};
|
|
93
|
+
if (signal?.aborted)
|
|
94
|
+
return abort();
|
|
95
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function abortable(promise, signal, path) {
|
|
99
|
+
if (!signal)
|
|
100
|
+
return promise;
|
|
101
|
+
if (signal.aborted)
|
|
102
|
+
throw abortError(path, signal.reason);
|
|
103
|
+
return Promise.race([
|
|
104
|
+
promise,
|
|
105
|
+
new Promise((_, reject) => signal.addEventListener("abort", () => reject(abortError(path, signal.reason)), { once: true }))
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// packages/rest/src/route.ts
|
|
110
|
+
function normalizeRoutePath(path) {
|
|
111
|
+
const queryIndex = path.indexOf("?");
|
|
112
|
+
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
113
|
+
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
114
|
+
}
|
|
115
|
+
function majorParameter(path) {
|
|
116
|
+
const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
|
|
117
|
+
if (!match)
|
|
118
|
+
return "@none";
|
|
119
|
+
if (match[1] === "webhooks" && match[3] !== undefined)
|
|
120
|
+
return `${match[2]}:${match[3]}`;
|
|
121
|
+
return match[2];
|
|
122
|
+
}
|
|
123
|
+
function scopeBucket(hashOrRoute, major) {
|
|
124
|
+
return `${hashOrRoute}|${major}`;
|
|
125
|
+
}
|
|
126
|
+
function createRouteKey(method, path) {
|
|
127
|
+
const normalizedMethod = method.toUpperCase();
|
|
128
|
+
return {
|
|
129
|
+
method: normalizedMethod,
|
|
130
|
+
path,
|
|
131
|
+
route: `${normalizedMethod}:${normalizeRoutePath(path)}`,
|
|
132
|
+
major: majorParameter(path)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function serializeQuery(query) {
|
|
136
|
+
if (query === undefined)
|
|
137
|
+
return "";
|
|
138
|
+
if (typeof query === "string")
|
|
139
|
+
return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
|
|
140
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
|
|
141
|
+
const serialized = params.toString();
|
|
142
|
+
return serialized.length === 0 ? "" : `?${serialized}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// packages/rest/src/scheduler.ts
|
|
146
|
+
class RequestScheduler {
|
|
147
|
+
static MAX_REMAPS = 2;
|
|
148
|
+
#queues = new Map;
|
|
149
|
+
get size() {
|
|
150
|
+
return this.#queues.size;
|
|
151
|
+
}
|
|
152
|
+
async runResolved(resolveKey, signal, path, task) {
|
|
153
|
+
let key = await resolveKey();
|
|
154
|
+
for (let hop = 0;hop < RequestScheduler.MAX_REMAPS; hop++) {
|
|
155
|
+
const outcome = await this.run(key, signal, path, async (release) => {
|
|
156
|
+
const current = await resolveKey();
|
|
157
|
+
if (current !== key)
|
|
158
|
+
return { done: false, key: current };
|
|
159
|
+
return { done: true, value: await task(key, release) };
|
|
160
|
+
});
|
|
161
|
+
if (outcome.done)
|
|
162
|
+
return outcome.value;
|
|
163
|
+
key = outcome.key;
|
|
164
|
+
}
|
|
165
|
+
return this.run(key, signal, path, (release) => task(key, release));
|
|
166
|
+
}
|
|
167
|
+
async run(key, signal, path, task) {
|
|
168
|
+
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
169
|
+
let open;
|
|
170
|
+
const gate = new Promise((resolve) => {
|
|
171
|
+
open = resolve;
|
|
172
|
+
});
|
|
173
|
+
this.#queues.set(key, gate);
|
|
174
|
+
let released = false;
|
|
175
|
+
const release = () => {
|
|
176
|
+
if (released)
|
|
177
|
+
return;
|
|
178
|
+
released = true;
|
|
179
|
+
open();
|
|
180
|
+
if (this.#queues.get(key) === gate)
|
|
181
|
+
this.#queues.delete(key);
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
await abortable(previous, signal, path);
|
|
185
|
+
return await task(release);
|
|
186
|
+
} finally {
|
|
187
|
+
release();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// packages/rest/src/limiter.ts
|
|
193
|
+
class RateLimiter {
|
|
194
|
+
#store;
|
|
195
|
+
#concurrent;
|
|
196
|
+
constructor(store, options = {}) {
|
|
197
|
+
this.#store = store;
|
|
198
|
+
this.#concurrent = options.concurrent ?? false;
|
|
199
|
+
}
|
|
200
|
+
get store() {
|
|
201
|
+
return this.#store;
|
|
202
|
+
}
|
|
203
|
+
async resolveBucketKey(route, major) {
|
|
204
|
+
return scopeBucket(await this.#store.getBucketHash(route) ?? route, major);
|
|
205
|
+
}
|
|
206
|
+
get reserves() {
|
|
207
|
+
return typeof this.#store.reserve === "function";
|
|
208
|
+
}
|
|
209
|
+
async acquire(bucketKey, signal, path) {
|
|
210
|
+
await this.#waitGlobal(signal, path);
|
|
211
|
+
if (!this.#store.reserve) {
|
|
212
|
+
const bucket = await this.#store.getBucket(bucketKey);
|
|
213
|
+
if (bucket) {
|
|
214
|
+
const delay = bucket.resetAt - Date.now();
|
|
215
|
+
if (bucket.remaining <= 0 && delay > 0)
|
|
216
|
+
await sleep(delay, signal, path);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
for (let attempt = 0;attempt < 8; attempt++) {
|
|
221
|
+
const reservation = await this.#store.reserve(bucketKey);
|
|
222
|
+
if (reservation.granted)
|
|
223
|
+
return;
|
|
224
|
+
await sleep(Math.max(1, reservation.retryAfterMs), signal, path);
|
|
225
|
+
await this.#waitGlobal(signal, path);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async#waitGlobal(signal, path) {
|
|
229
|
+
const globalDelay = await this.#store.getGlobalReset() - Date.now();
|
|
230
|
+
if (globalDelay > 0)
|
|
231
|
+
await sleep(globalDelay, signal, path);
|
|
232
|
+
}
|
|
233
|
+
async noteGlobalReset(resetAt) {
|
|
234
|
+
const current = await this.#store.getGlobalReset();
|
|
235
|
+
await this.#store.setGlobalReset(Math.max(current, resetAt));
|
|
236
|
+
}
|
|
237
|
+
async applyResponse(response, bucketKey, route, major) {
|
|
238
|
+
let currentKey = bucketKey;
|
|
239
|
+
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
240
|
+
if (serverBucket) {
|
|
241
|
+
await this.#store.setBucketHash(route, serverBucket);
|
|
242
|
+
currentKey = scopeBucket(serverBucket, major);
|
|
243
|
+
}
|
|
244
|
+
const stored = await this.#store.getBucket(currentKey);
|
|
245
|
+
const bucket = stored ? { ...stored } : { remaining: 1, resetAt: 0 };
|
|
246
|
+
const windowActive = this.#concurrent && !!stored && stored.resetAt > Date.now();
|
|
247
|
+
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
248
|
+
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
249
|
+
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
250
|
+
if (Number.isFinite(remaining))
|
|
251
|
+
bucket.remaining = Math.max(0, remaining);
|
|
252
|
+
if (Number.isFinite(resetAfter))
|
|
253
|
+
bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
|
|
254
|
+
else if (Number.isFinite(reset))
|
|
255
|
+
bucket.resetAt = reset * 1000;
|
|
256
|
+
if (windowActive && stored) {
|
|
257
|
+
bucket.remaining = Math.min(bucket.remaining, stored.remaining);
|
|
258
|
+
bucket.resetAt = Math.max(bucket.resetAt, stored.resetAt);
|
|
259
|
+
}
|
|
260
|
+
if (response.status === 429) {
|
|
261
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
262
|
+
if (Number.isFinite(retryAfter))
|
|
263
|
+
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
264
|
+
}
|
|
265
|
+
await this.#store.updateBucket(currentKey, bucket);
|
|
266
|
+
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
267
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
268
|
+
if (Number.isFinite(retryAfter))
|
|
269
|
+
await this.noteGlobalReset(Date.now() + retryAfter * 1000);
|
|
270
|
+
}
|
|
271
|
+
return currentKey;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// packages/rest/src/transport.ts
|
|
276
|
+
class TransportError extends Error {
|
|
277
|
+
timedOut;
|
|
278
|
+
constructor(message, timedOut, cause) {
|
|
279
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
280
|
+
this.name = "TransportError";
|
|
281
|
+
this.timedOut = timedOut;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class HttpTransport {
|
|
286
|
+
#baseURL;
|
|
287
|
+
#timeout;
|
|
288
|
+
#fetch;
|
|
289
|
+
constructor(options = {}) {
|
|
290
|
+
this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
|
|
291
|
+
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
292
|
+
this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
293
|
+
}
|
|
294
|
+
get baseURL() {
|
|
295
|
+
return this.#baseURL;
|
|
296
|
+
}
|
|
297
|
+
get timeout() {
|
|
298
|
+
return this.#timeout;
|
|
299
|
+
}
|
|
300
|
+
async send(request) {
|
|
301
|
+
if (request.signal?.aborted)
|
|
302
|
+
throw abortError(request.path, request.signal.reason);
|
|
303
|
+
const controller = new AbortController;
|
|
304
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
305
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
|
|
307
|
+
try {
|
|
308
|
+
return await this.#fetch(`${this.#baseURL}${request.path}`, {
|
|
309
|
+
method: request.method,
|
|
310
|
+
headers: request.headers,
|
|
311
|
+
body: request.body,
|
|
312
|
+
signal: controller.signal
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof RESTError)
|
|
316
|
+
throw error;
|
|
317
|
+
const timedOut = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
318
|
+
throw new TransportError(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, error);
|
|
319
|
+
} finally {
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// packages/rest/src/decoder.ts
|
|
327
|
+
class ResponseDecoder {
|
|
328
|
+
async read(response) {
|
|
329
|
+
if (response.status === 204)
|
|
330
|
+
return;
|
|
331
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
332
|
+
if (contentType.includes("application/json"))
|
|
333
|
+
return response.json().catch(() => {
|
|
334
|
+
return;
|
|
335
|
+
});
|
|
336
|
+
return response.text().catch(() => {
|
|
337
|
+
return;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
errorData(payload) {
|
|
341
|
+
if (!payload || typeof payload !== "object")
|
|
342
|
+
return {};
|
|
343
|
+
const data = payload;
|
|
344
|
+
return {
|
|
345
|
+
message: typeof data.message === "string" ? data.message : undefined,
|
|
346
|
+
code: typeof data.code === "number" ? data.code : undefined,
|
|
347
|
+
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
348
|
+
global: data.global === true
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
retryAfter(response, data) {
|
|
352
|
+
const raw = response.headers.get("Retry-After");
|
|
353
|
+
const header = raw === null ? Number.NaN : Number(raw);
|
|
354
|
+
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
355
|
+
return Math.max(0, retryAfter);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
53
359
|
// packages/rest/src/routes.ts
|
|
54
360
|
var Routes = {
|
|
55
361
|
user: () => "/users/@me",
|
|
@@ -91,6 +397,8 @@ var Routes = {
|
|
|
91
397
|
threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
|
|
92
398
|
guildScheduledEvents: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events`,
|
|
93
399
|
guildScheduledEvent: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}`,
|
|
400
|
+
guildScheduledEventUsers: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}/users`,
|
|
401
|
+
channelPermission: (channelId, overwriteId) => `/channels/${snowflake(channelId, "Channel ID")}/permissions/${snowflake(overwriteId, "Overwrite ID")}`,
|
|
94
402
|
guildAutoModerationRules: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules`,
|
|
95
403
|
guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
|
|
96
404
|
voiceRegions: () => "/voice/regions",
|
|
@@ -114,15 +422,36 @@ function snowflake(value, field) {
|
|
|
114
422
|
return value;
|
|
115
423
|
}
|
|
116
424
|
// packages/rest/src/redis.ts
|
|
425
|
+
var RESERVE_SCRIPT = `
|
|
426
|
+
local raw = redis.call('GET', KEYS[1])
|
|
427
|
+
if not raw then return {1, 0} end
|
|
428
|
+
local ok, state = pcall(cjson.decode, raw)
|
|
429
|
+
if not ok or type(state) ~= 'table' or type(state.resetAt) ~= 'number' then
|
|
430
|
+
return {1, 0}
|
|
431
|
+
end
|
|
432
|
+
local now = tonumber(ARGV[1])
|
|
433
|
+
local wait = state.resetAt - now
|
|
434
|
+
if wait <= 0 then return {1, 0} end
|
|
435
|
+
if type(state.remaining) == 'number' and state.remaining > 0 then
|
|
436
|
+
state.remaining = state.remaining - 1
|
|
437
|
+
redis.call('SET', KEYS[1], cjson.encode(state), 'PX', math.ceil(wait))
|
|
438
|
+
return {1, 0}
|
|
439
|
+
end
|
|
440
|
+
return {0, math.ceil(wait)}
|
|
441
|
+
`;
|
|
442
|
+
|
|
117
443
|
class RedisRateLimitStore {
|
|
118
444
|
#client;
|
|
119
445
|
#prefix;
|
|
120
446
|
#onErrorCallback;
|
|
447
|
+
#local = new MemoryRateLimitStore;
|
|
121
448
|
#lastError;
|
|
122
449
|
constructor(options) {
|
|
123
450
|
this.#client = options.client;
|
|
124
451
|
this.#prefix = options.prefix ?? "lunibee:rest:";
|
|
125
452
|
this.#onErrorCallback = options.onError;
|
|
453
|
+
if (typeof options.client.eval === "function")
|
|
454
|
+
this.reserve = (key) => this.#reserve(key);
|
|
126
455
|
}
|
|
127
456
|
get lastError() {
|
|
128
457
|
return this.#lastError;
|
|
@@ -147,10 +476,11 @@ class RedisRateLimitStore {
|
|
|
147
476
|
return hash ?? undefined;
|
|
148
477
|
} catch (error) {
|
|
149
478
|
this.#onError("getBucketHash", error);
|
|
150
|
-
return;
|
|
479
|
+
return this.#local.getBucketHash(route);
|
|
151
480
|
}
|
|
152
481
|
}
|
|
153
482
|
async setBucketHash(route, hash) {
|
|
483
|
+
this.#local.setBucketHash(route, hash);
|
|
154
484
|
try {
|
|
155
485
|
await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
|
|
156
486
|
this.#markHealthy();
|
|
@@ -167,10 +497,11 @@ class RedisRateLimitStore {
|
|
|
167
497
|
return JSON.parse(data);
|
|
168
498
|
} catch (error) {
|
|
169
499
|
this.#onError("getBucket", error);
|
|
170
|
-
return;
|
|
500
|
+
return this.#local.getBucket(key);
|
|
171
501
|
}
|
|
172
502
|
}
|
|
173
503
|
async updateBucket(key, state) {
|
|
504
|
+
this.#local.updateBucket(key, state);
|
|
174
505
|
const bucketKey = `${this.#prefix}bucket:${key}`;
|
|
175
506
|
const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
|
|
176
507
|
try {
|
|
@@ -185,6 +516,23 @@ class RedisRateLimitStore {
|
|
|
185
516
|
this.#onError("updateBucket", error);
|
|
186
517
|
}
|
|
187
518
|
}
|
|
519
|
+
get supportsReservation() {
|
|
520
|
+
return typeof this.#client.eval === "function";
|
|
521
|
+
}
|
|
522
|
+
reserve;
|
|
523
|
+
async#reserve(key) {
|
|
524
|
+
try {
|
|
525
|
+
const result = await this.#client.eval(RESERVE_SCRIPT, 1, `${this.#prefix}bucket:${key}`, Date.now());
|
|
526
|
+
this.#markHealthy();
|
|
527
|
+
if (!Array.isArray(result))
|
|
528
|
+
return { granted: true };
|
|
529
|
+
const [granted, retryAfterMs] = result;
|
|
530
|
+
return Number(granted) === 1 ? { granted: true } : { granted: false, retryAfterMs: Number(retryAfterMs) || 0 };
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.#onError("reserve", error);
|
|
533
|
+
return this.#local.reserve(key);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
188
536
|
async getGlobalReset() {
|
|
189
537
|
try {
|
|
190
538
|
const resetAt = await this.#client.get(`${this.#prefix}global`);
|
|
@@ -192,10 +540,11 @@ class RedisRateLimitStore {
|
|
|
192
540
|
return resetAt ? Number(resetAt) : 0;
|
|
193
541
|
} catch (error) {
|
|
194
542
|
this.#onError("getGlobalReset", error);
|
|
195
|
-
return
|
|
543
|
+
return this.#local.getGlobalReset();
|
|
196
544
|
}
|
|
197
545
|
}
|
|
198
546
|
async setGlobalReset(resetAt) {
|
|
547
|
+
this.#local.setGlobalReset(resetAt);
|
|
199
548
|
const globalKey = `${this.#prefix}global`;
|
|
200
549
|
const ttl = Math.ceil((resetAt - Date.now()) / 1000);
|
|
201
550
|
try {
|
|
@@ -251,22 +600,6 @@ class WebhookClient {
|
|
|
251
600
|
}
|
|
252
601
|
|
|
253
602
|
// packages/rest/src/index.ts
|
|
254
|
-
class RESTError extends Error {
|
|
255
|
-
status;
|
|
256
|
-
code;
|
|
257
|
-
errors;
|
|
258
|
-
method;
|
|
259
|
-
path;
|
|
260
|
-
constructor(message, status, code, errors, options = {}) {
|
|
261
|
-
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
262
|
-
this.name = "RESTError";
|
|
263
|
-
this.status = status;
|
|
264
|
-
this.code = code;
|
|
265
|
-
this.errors = errors;
|
|
266
|
-
this.method = options.method;
|
|
267
|
-
this.path = options.path;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
603
|
var LIBRARY_VERSION = package_default.version;
|
|
271
604
|
var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
|
|
272
605
|
var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
|
|
@@ -293,26 +626,6 @@ function isRequestData(value) {
|
|
|
293
626
|
return false;
|
|
294
627
|
return keys.every((key) => REQUEST_DATA_KEYS.has(key));
|
|
295
628
|
}
|
|
296
|
-
function serializeQuery(query) {
|
|
297
|
-
if (query === undefined)
|
|
298
|
-
return "";
|
|
299
|
-
if (typeof query === "string")
|
|
300
|
-
return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
|
|
301
|
-
const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
|
|
302
|
-
const serialized = params.toString();
|
|
303
|
-
return serialized.length === 0 ? "" : `?${serialized}`;
|
|
304
|
-
}
|
|
305
|
-
function majorParameter(path) {
|
|
306
|
-
const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
|
|
307
|
-
if (!match)
|
|
308
|
-
return "@none";
|
|
309
|
-
if (match[1] === "webhooks" && match[3] !== undefined)
|
|
310
|
-
return `${match[2]}:${match[3]}`;
|
|
311
|
-
return match[2];
|
|
312
|
-
}
|
|
313
|
-
function scopeBucket(hashOrRoute, major) {
|
|
314
|
-
return `${hashOrRoute}|${major}`;
|
|
315
|
-
}
|
|
316
629
|
function createRetryPolicy(maxRetries = 2) {
|
|
317
630
|
if (!Number.isFinite(maxRetries) || maxRetries < 0)
|
|
318
631
|
throw new TypeError("maxRetries must be a non-negative finite number.");
|
|
@@ -320,7 +633,10 @@ function createRetryPolicy(maxRetries = 2) {
|
|
|
320
633
|
maxRetries: Math.floor(maxRetries),
|
|
321
634
|
shouldRetry(method, status) {
|
|
322
635
|
const normalized = method.toUpperCase();
|
|
323
|
-
|
|
636
|
+
const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
|
|
637
|
+
if (status === 0)
|
|
638
|
+
return idempotent;
|
|
639
|
+
return status === 429 || status >= 500 && status <= 599 && idempotent;
|
|
324
640
|
},
|
|
325
641
|
getDelay(attempt, retryAfter) {
|
|
326
642
|
if (retryAfter !== undefined)
|
|
@@ -331,22 +647,32 @@ function createRetryPolicy(maxRetries = 2) {
|
|
|
331
647
|
}
|
|
332
648
|
|
|
333
649
|
class REST {
|
|
334
|
-
#baseURL;
|
|
335
|
-
#timeout;
|
|
336
650
|
#retryPolicy;
|
|
337
651
|
#token;
|
|
338
|
-
#
|
|
339
|
-
#
|
|
652
|
+
#limiter;
|
|
653
|
+
#scheduler = new RequestScheduler;
|
|
654
|
+
#transport;
|
|
655
|
+
#decoder = new ResponseDecoder;
|
|
340
656
|
#hooks = {};
|
|
657
|
+
#concurrentBuckets;
|
|
341
658
|
constructor(options = {}) {
|
|
342
659
|
this.#token = options.token;
|
|
343
|
-
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
344
660
|
this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
|
|
345
|
-
this.#
|
|
346
|
-
|
|
661
|
+
this.#transport = options.transport ?? new HttpTransport({
|
|
662
|
+
baseURL: options.baseURL,
|
|
663
|
+
timeout: options.timeout
|
|
664
|
+
});
|
|
665
|
+
const store = options.store ?? new MemoryRateLimitStore;
|
|
666
|
+
this.#concurrentBuckets = (options.concurrentBuckets ?? false) && typeof store.reserve === "function";
|
|
667
|
+
this.#limiter = new RateLimiter(store, {
|
|
668
|
+
concurrent: this.#concurrentBuckets
|
|
669
|
+
});
|
|
347
670
|
if (options.hooks)
|
|
348
671
|
this.#hooks = options.hooks;
|
|
349
672
|
}
|
|
673
|
+
get store() {
|
|
674
|
+
return this.#limiter.store;
|
|
675
|
+
}
|
|
350
676
|
setHooks(hooks) {
|
|
351
677
|
this.#hooks = hooks;
|
|
352
678
|
return this;
|
|
@@ -357,121 +683,109 @@ class REST {
|
|
|
357
683
|
this.#token = token;
|
|
358
684
|
}
|
|
359
685
|
async request(method, path, body, options = {}) {
|
|
360
|
-
|
|
361
|
-
if (!normalizedMethod)
|
|
686
|
+
if (!method.toUpperCase())
|
|
362
687
|
throw new TypeError("REST method is required.");
|
|
363
688
|
if (!path.startsWith("/"))
|
|
364
689
|
throw new TypeError("REST paths must start with '/'.");
|
|
690
|
+
const route = createRouteKey(method, path);
|
|
365
691
|
const files = options.files;
|
|
366
692
|
if (files && files.length > 0 && !(body instanceof FormData))
|
|
367
693
|
body = this.#fileForm(body, files);
|
|
368
694
|
const query = serializeQuery(options.query);
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
let
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
695
|
+
const requestPath = query ? `${path}${query}` : path;
|
|
696
|
+
return this.#scheduler.runResolved(() => this.#limiter.resolveBucketKey(route.route, route.major), options.signal, path, (bucketKey, release) => this.#attempts(route, requestPath, body, options, bucketKey, release));
|
|
697
|
+
}
|
|
698
|
+
async#attempts(route, requestPath, body, options, initialBucketKey, release) {
|
|
699
|
+
const { method, path } = route;
|
|
700
|
+
let bucketKey = initialBucketKey;
|
|
701
|
+
for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
|
|
702
|
+
await this.#limiter.acquire(bucketKey, options.signal, path);
|
|
703
|
+
if (this.#concurrentBuckets) {
|
|
704
|
+
const bucket = await this.#limiter.store.getBucket(bucketKey);
|
|
705
|
+
if (bucket && bucket.resetAt > Date.now())
|
|
706
|
+
release();
|
|
707
|
+
}
|
|
708
|
+
this.#emit(this.#hooks.onRequest, { method, path, attempt });
|
|
709
|
+
const requestStart = Date.now();
|
|
710
|
+
let response;
|
|
711
|
+
try {
|
|
712
|
+
response = await this.#transport.send({
|
|
713
|
+
method,
|
|
714
|
+
path: requestPath,
|
|
715
|
+
headers: this.#headers(body, options),
|
|
716
|
+
body: this.#encodeBody(body),
|
|
717
|
+
signal: options.signal
|
|
391
718
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (options.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
headers["Content-Type"] = "application/json";
|
|
402
|
-
const fetchBody = isFormData ? body : body === undefined ? undefined : JSON.stringify(body);
|
|
403
|
-
try {
|
|
404
|
-
this.#emit(this.#hooks.onRequest, {
|
|
405
|
-
method: normalizedMethod,
|
|
406
|
-
path,
|
|
407
|
-
attempt
|
|
408
|
-
});
|
|
409
|
-
const requestStart = Date.now();
|
|
410
|
-
const response = await fetch(`${this.#baseURL}${finalPath}`, {
|
|
411
|
-
method: normalizedMethod,
|
|
412
|
-
headers,
|
|
413
|
-
body: fetchBody,
|
|
414
|
-
signal: controller.signal
|
|
415
|
-
});
|
|
416
|
-
this.#emit(this.#hooks.onResponse, {
|
|
417
|
-
method: normalizedMethod,
|
|
418
|
-
path,
|
|
419
|
-
status: response.status,
|
|
420
|
-
durationMs: Date.now() - requestStart
|
|
421
|
-
});
|
|
422
|
-
bucketKey = await this.#update(response, bucketKey, route, major);
|
|
423
|
-
const payload = await this.#readPayload(response);
|
|
424
|
-
if (response.ok)
|
|
425
|
-
return response.status === 204 ? undefined : payload;
|
|
426
|
-
const data = this.#errorData(payload);
|
|
427
|
-
const retryAfter = response.status === 429 ? this.#retryAfter(response, data) : undefined;
|
|
428
|
-
if (data.global && retryAfter !== undefined) {
|
|
429
|
-
const currentGlobal = await this.#store.getGlobalReset();
|
|
430
|
-
await this.#store.setGlobalReset(Math.max(currentGlobal, Date.now() + retryAfter * 1000));
|
|
431
|
-
}
|
|
432
|
-
if (response.status === 429)
|
|
433
|
-
this.#emit(this.#hooks.onRateLimit, {
|
|
434
|
-
method: normalizedMethod,
|
|
435
|
-
path,
|
|
436
|
-
retryAfterMs: (retryAfter ?? 1) * 1000,
|
|
437
|
-
global: data.global ?? false,
|
|
438
|
-
bucket: await this.#store.getBucketHash(route)
|
|
439
|
-
});
|
|
440
|
-
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, response.status)) {
|
|
441
|
-
const delay = this.#retryPolicy.getDelay(attempt, retryAfter);
|
|
442
|
-
this.#emit(this.#hooks.onRetry, {
|
|
443
|
-
method: normalizedMethod,
|
|
444
|
-
path,
|
|
445
|
-
attempt,
|
|
446
|
-
status: response.status,
|
|
447
|
-
delayMs: delay
|
|
448
|
-
});
|
|
449
|
-
await this.#sleep(delay, options.signal, path);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method: normalizedMethod, path });
|
|
453
|
-
} catch (error) {
|
|
454
|
-
if (error instanceof RESTError)
|
|
455
|
-
throw error;
|
|
456
|
-
if (options.signal?.aborted)
|
|
457
|
-
throw this.#abortError(path, options.signal.reason);
|
|
458
|
-
const isTimeout = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
459
|
-
if (!isTimeout && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, 0)) {
|
|
460
|
-
await this.#sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
throw new RESTError(isTimeout ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", 0, undefined, undefined, { method: normalizedMethod, path, cause: error });
|
|
464
|
-
} finally {
|
|
465
|
-
clearTimeout(timer);
|
|
466
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (error instanceof RESTError)
|
|
721
|
+
throw error;
|
|
722
|
+
if (options.signal?.aborted)
|
|
723
|
+
throw abortError(path, options.signal.reason);
|
|
724
|
+
const failure = error instanceof TransportError ? error : new TransportError("Discord REST request failed", false, error);
|
|
725
|
+
if (!failure.timedOut && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, 0)) {
|
|
726
|
+
await sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
|
|
727
|
+
continue;
|
|
467
728
|
}
|
|
729
|
+
throw new RESTError(failure.message, 0, undefined, undefined, {
|
|
730
|
+
method,
|
|
731
|
+
path,
|
|
732
|
+
cause: failure.cause ?? failure
|
|
733
|
+
});
|
|
468
734
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
735
|
+
this.#emit(this.#hooks.onResponse, {
|
|
736
|
+
method,
|
|
737
|
+
path,
|
|
738
|
+
status: response.status,
|
|
739
|
+
durationMs: Date.now() - requestStart
|
|
740
|
+
});
|
|
741
|
+
bucketKey = await this.#limiter.applyResponse(response, bucketKey, route.route, route.major);
|
|
742
|
+
const payload = await this.#decoder.read(response);
|
|
743
|
+
if (response.ok)
|
|
744
|
+
return response.status === 204 ? undefined : payload;
|
|
745
|
+
const data = this.#decoder.errorData(payload);
|
|
746
|
+
const retryAfter = response.status === 429 ? this.#decoder.retryAfter(response, data) : undefined;
|
|
747
|
+
if (data.global && retryAfter !== undefined)
|
|
748
|
+
await this.#limiter.noteGlobalReset(Date.now() + retryAfter * 1000);
|
|
749
|
+
if (response.status === 429)
|
|
750
|
+
this.#emit(this.#hooks.onRateLimit, {
|
|
751
|
+
method,
|
|
752
|
+
path,
|
|
753
|
+
retryAfterMs: (retryAfter ?? 1) * 1000,
|
|
754
|
+
global: data.global ?? false,
|
|
755
|
+
bucket: await this.#limiter.store.getBucketHash(route.route)
|
|
756
|
+
});
|
|
757
|
+
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, response.status)) {
|
|
758
|
+
const delay = this.#retryPolicy.getDelay(attempt, retryAfter);
|
|
759
|
+
this.#emit(this.#hooks.onRetry, {
|
|
760
|
+
method,
|
|
761
|
+
path,
|
|
762
|
+
attempt,
|
|
763
|
+
status: response.status,
|
|
764
|
+
delayMs: delay
|
|
765
|
+
});
|
|
766
|
+
await sleep(delay, options.signal, path);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method, path });
|
|
474
770
|
}
|
|
771
|
+
throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method, path });
|
|
772
|
+
}
|
|
773
|
+
#headers(body, options) {
|
|
774
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
775
|
+
const headers = { ...options.headers };
|
|
776
|
+
if (options.auth !== false && this.#token)
|
|
777
|
+
headers["Authorization"] = `Bot ${this.#token}`;
|
|
778
|
+
headers["User-Agent"] = USER_AGENT;
|
|
779
|
+
if (options.reason !== undefined)
|
|
780
|
+
headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
|
|
781
|
+
if (!isFormData)
|
|
782
|
+
headers["Content-Type"] = "application/json";
|
|
783
|
+
return headers;
|
|
784
|
+
}
|
|
785
|
+
#encodeBody(body) {
|
|
786
|
+
if (typeof FormData !== "undefined" && body instanceof FormData)
|
|
787
|
+
return body;
|
|
788
|
+
return body === undefined ? undefined : JSON.stringify(body);
|
|
475
789
|
}
|
|
476
790
|
#dispatch(method, path, bodyOrOptions, options) {
|
|
477
791
|
if (options === undefined && isRequestData(bodyOrOptions))
|
|
@@ -516,125 +830,27 @@ class REST {
|
|
|
516
830
|
return;
|
|
517
831
|
Promise.resolve().then(() => hook(ctx)).catch(() => {});
|
|
518
832
|
}
|
|
519
|
-
async#wait(bucketKey, signal, path) {
|
|
520
|
-
const bucket = await this.#store.getBucket(bucketKey);
|
|
521
|
-
if (!bucket)
|
|
522
|
-
return;
|
|
523
|
-
const delay = bucket.resetAt - Date.now();
|
|
524
|
-
if (bucket.remaining <= 0 && delay > 0)
|
|
525
|
-
await this.#sleep(delay, signal, path);
|
|
526
|
-
}
|
|
527
|
-
async#waitGlobal(signal, path) {
|
|
528
|
-
const globalResetAt = await this.#store.getGlobalReset();
|
|
529
|
-
const delay = globalResetAt - Date.now();
|
|
530
|
-
if (delay > 0)
|
|
531
|
-
await this.#sleep(delay, signal, path);
|
|
532
|
-
}
|
|
533
|
-
async#sleep(delay, signal, path) {
|
|
534
|
-
if (delay <= 0)
|
|
535
|
-
return;
|
|
536
|
-
await new Promise((resolve, reject) => {
|
|
537
|
-
const timer = setTimeout(resolve, delay);
|
|
538
|
-
const abort = () => {
|
|
539
|
-
clearTimeout(timer);
|
|
540
|
-
reject(this.#abortError(path, signal?.reason));
|
|
541
|
-
};
|
|
542
|
-
if (signal?.aborted)
|
|
543
|
-
return abort();
|
|
544
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
async#abortable(promise, signal, path) {
|
|
548
|
-
if (!signal)
|
|
549
|
-
return promise;
|
|
550
|
-
if (signal.aborted)
|
|
551
|
-
throw this.#abortError(path, signal.reason);
|
|
552
|
-
return Promise.race([
|
|
553
|
-
promise,
|
|
554
|
-
new Promise((_, reject) => signal.addEventListener("abort", () => reject(this.#abortError(path, signal.reason)), { once: true }))
|
|
555
|
-
]);
|
|
556
|
-
}
|
|
557
|
-
#abortError(path, reason) {
|
|
558
|
-
return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason });
|
|
559
|
-
}
|
|
560
|
-
async#readPayload(response) {
|
|
561
|
-
if (response.status === 204)
|
|
562
|
-
return;
|
|
563
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
564
|
-
if (contentType.includes("application/json"))
|
|
565
|
-
return response.json().catch(() => {
|
|
566
|
-
return;
|
|
567
|
-
});
|
|
568
|
-
return response.text().catch(() => {
|
|
569
|
-
return;
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
#errorData(payload) {
|
|
573
|
-
if (!payload || typeof payload !== "object")
|
|
574
|
-
return {};
|
|
575
|
-
const data = payload;
|
|
576
|
-
return {
|
|
577
|
-
message: typeof data.message === "string" ? data.message : undefined,
|
|
578
|
-
code: typeof data.code === "number" ? data.code : undefined,
|
|
579
|
-
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
580
|
-
global: data.global === true
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
#retryAfter(response, data) {
|
|
584
|
-
const header = Number(response.headers.get("Retry-After"));
|
|
585
|
-
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
586
|
-
return Math.max(0, retryAfter);
|
|
587
|
-
}
|
|
588
|
-
#normalizeRoute(path) {
|
|
589
|
-
const queryIndex = path.indexOf("?");
|
|
590
|
-
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
591
|
-
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
592
|
-
}
|
|
593
|
-
async#update(response, bucketKey, route, major) {
|
|
594
|
-
let currentKey = bucketKey;
|
|
595
|
-
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
596
|
-
if (serverBucket) {
|
|
597
|
-
await this.#store.setBucketHash(route, serverBucket);
|
|
598
|
-
currentKey = scopeBucket(serverBucket, major);
|
|
599
|
-
}
|
|
600
|
-
let bucket = await this.#store.getBucket(currentKey) ?? {
|
|
601
|
-
remaining: 1,
|
|
602
|
-
resetAt: 0
|
|
603
|
-
};
|
|
604
|
-
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
605
|
-
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
606
|
-
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
607
|
-
if (Number.isFinite(remaining))
|
|
608
|
-
bucket.remaining = Math.max(0, remaining);
|
|
609
|
-
if (Number.isFinite(resetAfter))
|
|
610
|
-
bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
|
|
611
|
-
else if (Number.isFinite(reset))
|
|
612
|
-
bucket.resetAt = reset * 1000;
|
|
613
|
-
if (response.status === 429) {
|
|
614
|
-
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
615
|
-
if (Number.isFinite(retryAfter))
|
|
616
|
-
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
617
|
-
}
|
|
618
|
-
await this.#store.updateBucket(currentKey, bucket);
|
|
619
|
-
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
620
|
-
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
621
|
-
if (Number.isFinite(retryAfter)) {
|
|
622
|
-
const currentGlobal = await this.#store.getGlobalReset();
|
|
623
|
-
await this.#store.setGlobalReset(Math.max(currentGlobal, Date.now() + retryAfter * 1000));
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return currentKey;
|
|
627
|
-
}
|
|
628
833
|
}
|
|
629
834
|
export {
|
|
630
835
|
RESTError as DiscordAPIError,
|
|
836
|
+
HttpTransport,
|
|
631
837
|
MemoryRateLimitStore,
|
|
632
838
|
REST,
|
|
633
839
|
RESTError,
|
|
840
|
+
RateLimiter,
|
|
634
841
|
RedisRateLimitStore,
|
|
842
|
+
RequestScheduler,
|
|
843
|
+
ResponseDecoder,
|
|
635
844
|
Routes,
|
|
845
|
+
TransportError,
|
|
636
846
|
WebhookClient,
|
|
637
|
-
|
|
847
|
+
abortError,
|
|
848
|
+
createRetryPolicy,
|
|
849
|
+
createRouteKey,
|
|
850
|
+
majorParameter,
|
|
851
|
+
normalizeRoutePath,
|
|
852
|
+
scopeBucket,
|
|
853
|
+
serializeQuery
|
|
638
854
|
};
|
|
639
855
|
|
|
640
|
-
//# debugId=
|
|
856
|
+
//# debugId=CCF20CDF4862826C64756E2164756E21
|