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.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +223 -223
  3. package/dist/builders/commands.d.ts +6 -4
  4. package/dist/builders/components.d.ts +18 -0
  5. package/dist/builders/index.d.ts +2 -2
  6. package/dist/builders/index.js +49 -13
  7. package/dist/builders/index.js.map +9 -8
  8. package/dist/collection/index.js +2 -1
  9. package/dist/collection/index.js.map +4 -4
  10. package/dist/core/events.d.ts +86 -2
  11. package/dist/core/index.d.ts +10 -87
  12. package/dist/core/index.js +1752 -725
  13. package/dist/core/index.js.map +44 -24
  14. package/dist/formatters/index.js.map +1 -1
  15. package/dist/handlers/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +2103 -800
  18. package/dist/index.js.map +63 -43
  19. package/dist/managers/guild-resources.d.ts +109 -0
  20. package/dist/managers/guild.d.ts +5 -0
  21. package/dist/managers/index.d.ts +7 -1
  22. package/dist/managers/index.js +330 -12
  23. package/dist/managers/index.js.map +25 -21
  24. package/dist/rest/decoder.d.ts +23 -0
  25. package/dist/rest/errors.d.ts +20 -0
  26. package/dist/rest/index.d.ts +21 -17
  27. package/dist/rest/index.js +479 -263
  28. package/dist/rest/index.js.map +14 -8
  29. package/dist/rest/limiter.d.ts +42 -0
  30. package/dist/rest/redis.d.ts +18 -1
  31. package/dist/rest/route.d.ts +31 -0
  32. package/dist/rest/routes.d.ts +2 -0
  33. package/dist/rest/scheduler.d.ts +42 -0
  34. package/dist/rest/store.d.ts +29 -0
  35. package/dist/rest/transport.d.ts +40 -0
  36. package/dist/sharding/bus.d.ts +31 -0
  37. package/dist/sharding/cluster.d.ts +10 -0
  38. package/dist/sharding/index.d.ts +10 -2
  39. package/dist/sharding/index.js +1011 -394
  40. package/dist/sharding/index.js.map +18 -8
  41. package/dist/structures/base.d.ts +8 -0
  42. package/dist/structures/channels.d.ts +56 -0
  43. package/dist/structures/index.d.ts +1 -0
  44. package/dist/structures/index.js +211 -271
  45. package/dist/structures/index.js.map +17 -14
  46. package/dist/structures/interactions.d.ts +12 -52
  47. package/dist/structures/options.d.ts +55 -0
  48. package/dist/structures/resources.d.ts +3 -3
  49. package/dist/types/gateway-events.d.ts +132 -0
  50. package/dist/types/gateway.d.ts +181 -0
  51. package/dist/types/index.d.ts +2 -263
  52. package/dist/types/index.js +44 -2
  53. package/dist/types/index.js.map +5 -4
  54. package/dist/utils/index.js.map +1 -1
  55. package/dist/voice/index.d.ts +29 -1
  56. package/dist/voice/index.js +59 -7
  57. package/dist/voice/index.js.map +3 -3
  58. package/dist/ws/close-codes.d.ts +23 -0
  59. package/dist/ws/decoder.d.ts +58 -0
  60. package/dist/ws/heartbeat.d.ts +87 -0
  61. package/dist/ws/index.d.ts +18 -65
  62. package/dist/ws/index.js +903 -385
  63. package/dist/ws/index.js.map +15 -5
  64. package/dist/ws/opcodes.d.ts +14 -0
  65. package/dist/ws/protocol.d.ts +110 -0
  66. package/dist/ws/reconnect.d.ts +108 -0
  67. package/dist/ws/send-budget.d.ts +17 -0
  68. package/dist/ws/session.d.ts +100 -0
  69. package/dist/ws/state.d.ts +27 -0
  70. package/dist/ws/transport.d.ts +80 -0
  71. package/package.json +21 -20
@@ -2,7 +2,7 @@
2
2
  // packages/rest/package.json
3
3
  var package_default = {
4
4
  name: "@lunibee/rest",
5
- version: "0.1.7",
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 0;
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
- return status === 429 || status >= 500 && status <= 599 && ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
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
- #store;
339
- #localQueues = new Map;
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.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
346
- this.#store = options.store ?? new MemoryRateLimitStore;
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
- const normalizedMethod = method.toUpperCase();
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 finalPath = query ? `${path}${query}` : path;
370
- const route = `${normalizedMethod}:${this.#normalizeRoute(path)}`;
371
- const major = majorParameter(path);
372
- let bucketKey = scopeBucket(await this.#store.getBucketHash(route) ?? route, major);
373
- const queueKey = bucketKey;
374
- const previous = this.#localQueues.get(queueKey) ?? Promise.resolve();
375
- let release;
376
- const gate = new Promise((resolve) => {
377
- release = resolve;
378
- });
379
- this.#localQueues.set(queueKey, gate);
380
- await this.#abortable(previous, options.signal, path);
381
- try {
382
- for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
383
- await this.#wait(bucketKey, options.signal, path);
384
- await this.#waitGlobal(options.signal, path);
385
- const controller = new AbortController;
386
- const onAbort = () => controller.abort(options.signal?.reason);
387
- if (options.signal?.aborted)
388
- throw this.#abortError(path, options.signal.reason);
389
- options.signal?.addEventListener("abort", onAbort, {
390
- once: true
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
- const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
393
- const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
394
- const headers = { ...options.headers };
395
- if (options.auth !== false && this.#token)
396
- headers["Authorization"] = `Bot ${this.#token}`;
397
- headers["User-Agent"] = USER_AGENT;
398
- if (options.reason !== undefined)
399
- headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
400
- if (!isFormData)
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
- throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method: normalizedMethod, path });
470
- } finally {
471
- release();
472
- if (this.#localQueues.get(queueKey) === gate)
473
- this.#localQueues.delete(queueKey);
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
- createRetryPolicy
847
+ abortError,
848
+ createRetryPolicy,
849
+ createRouteKey,
850
+ majorParameter,
851
+ normalizeRoutePath,
852
+ scopeBucket,
853
+ serializeQuery
638
854
  };
639
855
 
640
- //# debugId=7273A79583E1900164756E2164756E21
856
+ //# debugId=CCF20CDF4862826C64756E2164756E21