lunibee 0.1.7 → 0.2.0

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 (78) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +225 -223
  3. package/dist/builders/commands.d.ts +6 -4
  4. package/dist/builders/components.d.ts +23 -0
  5. package/dist/builders/index.d.ts +2 -2
  6. package/dist/builders/index.js +120 -24
  7. package/dist/builders/index.js.map +9 -8
  8. package/dist/collection/index.d.ts +68 -1
  9. package/dist/collection/index.js +335 -1
  10. package/dist/collection/index.js.map +4 -4
  11. package/dist/core/collector.d.ts +13 -0
  12. package/dist/core/events.d.ts +112 -2
  13. package/dist/core/index.d.ts +30 -93
  14. package/dist/core/index.js +3386 -957
  15. package/dist/core/index.js.map +48 -24
  16. package/dist/core/permissions.d.ts +48 -0
  17. package/dist/formatters/index.js.map +1 -1
  18. package/dist/handlers/index.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +3806 -1193
  21. package/dist/index.js.map +64 -43
  22. package/dist/managers/advanced.d.ts +116 -0
  23. package/dist/managers/base.d.ts +13 -2
  24. package/dist/managers/emoji.d.ts +1 -1
  25. package/dist/managers/guild-resources.d.ts +109 -0
  26. package/dist/managers/guild.d.ts +33 -2
  27. package/dist/managers/index.d.ts +44 -6
  28. package/dist/managers/index.js +1186 -150
  29. package/dist/managers/index.js.map +27 -21
  30. package/dist/managers/message.d.ts +10 -1
  31. package/dist/rest/decoder.d.ts +23 -0
  32. package/dist/rest/errors.d.ts +53 -0
  33. package/dist/rest/index.d.ts +21 -17
  34. package/dist/rest/index.js +526 -264
  35. package/dist/rest/index.js.map +14 -8
  36. package/dist/rest/limiter.d.ts +42 -0
  37. package/dist/rest/redis.d.ts +18 -1
  38. package/dist/rest/route.d.ts +31 -0
  39. package/dist/rest/routes.d.ts +22 -0
  40. package/dist/rest/scheduler.d.ts +42 -0
  41. package/dist/rest/store.d.ts +29 -0
  42. package/dist/rest/transport.d.ts +40 -0
  43. package/dist/sharding/bus.d.ts +31 -0
  44. package/dist/sharding/cluster.d.ts +10 -0
  45. package/dist/sharding/index.d.ts +39 -2
  46. package/dist/sharding/index.js +1105 -404
  47. package/dist/sharding/index.js.map +18 -8
  48. package/dist/structures/base.d.ts +10 -0
  49. package/dist/structures/channels.d.ts +56 -0
  50. package/dist/structures/index.d.ts +1 -0
  51. package/dist/structures/index.js +305 -311
  52. package/dist/structures/index.js.map +18 -14
  53. package/dist/structures/interactions.d.ts +38 -52
  54. package/dist/structures/options.d.ts +55 -0
  55. package/dist/structures/resources.d.ts +3 -3
  56. package/dist/types/gateway-events.d.ts +132 -0
  57. package/dist/types/gateway.d.ts +197 -0
  58. package/dist/types/index.d.ts +53 -260
  59. package/dist/types/index.js +44 -2
  60. package/dist/types/index.js.map +5 -4
  61. package/dist/utils/index.js.map +1 -1
  62. package/dist/voice/index.d.ts +32 -1
  63. package/dist/voice/index.js +73 -7
  64. package/dist/voice/index.js.map +3 -3
  65. package/dist/ws/close-codes.d.ts +23 -0
  66. package/dist/ws/decoder.d.ts +58 -0
  67. package/dist/ws/heartbeat.d.ts +87 -0
  68. package/dist/ws/index.d.ts +18 -65
  69. package/dist/ws/index.js +911 -385
  70. package/dist/ws/index.js.map +15 -5
  71. package/dist/ws/opcodes.d.ts +14 -0
  72. package/dist/ws/protocol.d.ts +110 -0
  73. package/dist/ws/reconnect.d.ts +108 -0
  74. package/dist/ws/send-budget.d.ts +17 -0
  75. package/dist/ws/session.d.ts +100 -0
  76. package/dist/ws/state.d.ts +27 -0
  77. package/dist/ws/transport.d.ts +80 -0
  78. 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.2.0",
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,327 @@ class MemoryRateLimitStore {
50
61
  }
51
62
  }
52
63
 
64
+ // packages/rest/src/errors.ts
65
+ function kindFromStatus(status) {
66
+ if (status === 429)
67
+ return "rateLimited";
68
+ if (status === 401)
69
+ return "authentication";
70
+ if (status === 403)
71
+ return "permission";
72
+ if (status === 404)
73
+ return "notFound";
74
+ if (status === 400 || status === 422)
75
+ return "validation";
76
+ if (status >= 500)
77
+ return "server";
78
+ if (status === 0)
79
+ return "network";
80
+ return "client";
81
+ }
82
+ function redactPath(path) {
83
+ return path.replace(/^(\/webhooks\/[^/?]+\/)[^/?]+/, "$1:token").replace(/^(\/interactions\/[^/?]+\/)[^/?]+/, "$1:token");
84
+ }
85
+
86
+ class RESTError extends Error {
87
+ kind;
88
+ status;
89
+ code;
90
+ errors;
91
+ method;
92
+ path;
93
+ constructor(message, status, code, errors, options = {}) {
94
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
95
+ this.name = "RESTError";
96
+ this.status = status;
97
+ this.kind = options.kind ?? kindFromStatus(status);
98
+ this.code = code;
99
+ this.errors = errors;
100
+ this.method = options.method;
101
+ this.path = options.path === undefined ? undefined : redactPath(options.path);
102
+ }
103
+ get retryable() {
104
+ return this.kind === "rateLimited" || this.kind === "server" || this.kind === "network" || this.kind === "timeout";
105
+ }
106
+ }
107
+ function abortError(path, reason) {
108
+ return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason, kind: "aborted" });
109
+ }
110
+ async function sleep(delay, signal, path) {
111
+ if (delay <= 0)
112
+ return;
113
+ await new Promise((resolve, reject) => {
114
+ const timer = setTimeout(resolve, delay);
115
+ const abort = () => {
116
+ clearTimeout(timer);
117
+ reject(abortError(path, signal?.reason));
118
+ };
119
+ if (signal?.aborted)
120
+ return abort();
121
+ signal?.addEventListener("abort", abort, { once: true });
122
+ });
123
+ }
124
+ async function abortable(promise, signal, path) {
125
+ if (!signal)
126
+ return promise;
127
+ if (signal.aborted)
128
+ throw abortError(path, signal.reason);
129
+ return Promise.race([
130
+ promise,
131
+ new Promise((_, reject) => signal.addEventListener("abort", () => reject(abortError(path, signal.reason)), { once: true }))
132
+ ]);
133
+ }
134
+
135
+ // packages/rest/src/route.ts
136
+ function normalizeRoutePath(path) {
137
+ const queryIndex = path.indexOf("?");
138
+ const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
139
+ return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
140
+ }
141
+ function majorParameter(path) {
142
+ const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
143
+ if (!match)
144
+ return "@none";
145
+ if (match[1] === "webhooks" && match[3] !== undefined)
146
+ return `${match[2]}:${match[3]}`;
147
+ return match[2];
148
+ }
149
+ function scopeBucket(hashOrRoute, major) {
150
+ return `${hashOrRoute}|${major}`;
151
+ }
152
+ function createRouteKey(method, path) {
153
+ const normalizedMethod = method.toUpperCase();
154
+ return {
155
+ method: normalizedMethod,
156
+ path,
157
+ route: `${normalizedMethod}:${normalizeRoutePath(path)}`,
158
+ major: majorParameter(path)
159
+ };
160
+ }
161
+ function serializeQuery(query) {
162
+ if (query === undefined)
163
+ return "";
164
+ if (typeof query === "string")
165
+ return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
166
+ const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
167
+ const serialized = params.toString();
168
+ return serialized.length === 0 ? "" : `?${serialized}`;
169
+ }
170
+
171
+ // packages/rest/src/scheduler.ts
172
+ class RequestScheduler {
173
+ static MAX_REMAPS = 2;
174
+ #queues = new Map;
175
+ get size() {
176
+ return this.#queues.size;
177
+ }
178
+ async runResolved(resolveKey, signal, path, task) {
179
+ let key = await resolveKey();
180
+ for (let hop = 0;hop < RequestScheduler.MAX_REMAPS; hop++) {
181
+ const outcome = await this.run(key, signal, path, async (release) => {
182
+ const current = await resolveKey();
183
+ if (current !== key)
184
+ return { done: false, key: current };
185
+ return { done: true, value: await task(key, release) };
186
+ });
187
+ if (outcome.done)
188
+ return outcome.value;
189
+ key = outcome.key;
190
+ }
191
+ return this.run(key, signal, path, (release) => task(key, release));
192
+ }
193
+ async run(key, signal, path, task) {
194
+ const previous = this.#queues.get(key) ?? Promise.resolve();
195
+ let open;
196
+ const gate = new Promise((resolve) => {
197
+ open = resolve;
198
+ });
199
+ this.#queues.set(key, gate);
200
+ let released = false;
201
+ const release = () => {
202
+ if (released)
203
+ return;
204
+ released = true;
205
+ open();
206
+ if (this.#queues.get(key) === gate)
207
+ this.#queues.delete(key);
208
+ };
209
+ try {
210
+ await abortable(previous, signal, path);
211
+ return await task(release);
212
+ } finally {
213
+ release();
214
+ }
215
+ }
216
+ }
217
+
218
+ // packages/rest/src/limiter.ts
219
+ class RateLimiter {
220
+ #store;
221
+ #concurrent;
222
+ constructor(store, options = {}) {
223
+ this.#store = store;
224
+ this.#concurrent = options.concurrent ?? false;
225
+ }
226
+ get store() {
227
+ return this.#store;
228
+ }
229
+ async resolveBucketKey(route, major) {
230
+ return scopeBucket(await this.#store.getBucketHash(route) ?? route, major);
231
+ }
232
+ get reserves() {
233
+ return typeof this.#store.reserve === "function";
234
+ }
235
+ async acquire(bucketKey, signal, path) {
236
+ await this.#waitGlobal(signal, path);
237
+ if (!this.#store.reserve) {
238
+ const bucket = await this.#store.getBucket(bucketKey);
239
+ if (bucket) {
240
+ const delay = bucket.resetAt - Date.now();
241
+ if (bucket.remaining <= 0 && delay > 0)
242
+ await sleep(delay, signal, path);
243
+ }
244
+ return;
245
+ }
246
+ for (let attempt = 0;attempt < 8; attempt++) {
247
+ const reservation = await this.#store.reserve(bucketKey);
248
+ if (reservation.granted)
249
+ return;
250
+ await sleep(Math.max(1, reservation.retryAfterMs), signal, path);
251
+ await this.#waitGlobal(signal, path);
252
+ }
253
+ }
254
+ async#waitGlobal(signal, path) {
255
+ const globalDelay = await this.#store.getGlobalReset() - Date.now();
256
+ if (globalDelay > 0)
257
+ await sleep(globalDelay, signal, path);
258
+ }
259
+ async noteGlobalReset(resetAt) {
260
+ const current = await this.#store.getGlobalReset();
261
+ await this.#store.setGlobalReset(Math.max(current, resetAt));
262
+ }
263
+ async applyResponse(response, bucketKey, route, major) {
264
+ let currentKey = bucketKey;
265
+ const serverBucket = response.headers.get("X-RateLimit-Bucket");
266
+ if (serverBucket) {
267
+ await this.#store.setBucketHash(route, serverBucket);
268
+ currentKey = scopeBucket(serverBucket, major);
269
+ }
270
+ const stored = await this.#store.getBucket(currentKey);
271
+ const bucket = stored ? { ...stored } : { remaining: 1, resetAt: 0 };
272
+ const windowActive = this.#concurrent && !!stored && stored.resetAt > Date.now();
273
+ const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
274
+ const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
275
+ const reset = Number(response.headers.get("X-RateLimit-Reset"));
276
+ if (Number.isFinite(remaining))
277
+ bucket.remaining = Math.max(0, remaining);
278
+ if (Number.isFinite(resetAfter))
279
+ bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
280
+ else if (Number.isFinite(reset))
281
+ bucket.resetAt = reset * 1000;
282
+ if (windowActive && stored) {
283
+ bucket.remaining = Math.min(bucket.remaining, stored.remaining);
284
+ bucket.resetAt = Math.max(bucket.resetAt, stored.resetAt);
285
+ }
286
+ if (response.status === 429) {
287
+ const retryAfter = Number(response.headers.get("Retry-After"));
288
+ if (Number.isFinite(retryAfter))
289
+ bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
290
+ }
291
+ await this.#store.updateBucket(currentKey, bucket);
292
+ if (response.headers.get("X-RateLimit-Global") === "true") {
293
+ const retryAfter = Number(response.headers.get("Retry-After"));
294
+ if (Number.isFinite(retryAfter))
295
+ await this.noteGlobalReset(Date.now() + retryAfter * 1000);
296
+ }
297
+ return currentKey;
298
+ }
299
+ }
300
+
301
+ // packages/rest/src/transport.ts
302
+ class TransportError extends Error {
303
+ timedOut;
304
+ constructor(message, timedOut, cause) {
305
+ super(message, cause === undefined ? undefined : { cause });
306
+ this.name = "TransportError";
307
+ this.timedOut = timedOut;
308
+ }
309
+ }
310
+
311
+ class HttpTransport {
312
+ #baseURL;
313
+ #timeout;
314
+ #fetch;
315
+ constructor(options = {}) {
316
+ this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
317
+ this.#timeout = Math.max(1, options.timeout ?? 15000);
318
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
319
+ }
320
+ get baseURL() {
321
+ return this.#baseURL;
322
+ }
323
+ get timeout() {
324
+ return this.#timeout;
325
+ }
326
+ async send(request) {
327
+ if (request.signal?.aborted)
328
+ throw abortError(request.path, request.signal.reason);
329
+ const controller = new AbortController;
330
+ const onAbort = () => controller.abort(request.signal?.reason);
331
+ request.signal?.addEventListener("abort", onAbort, { once: true });
332
+ const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
333
+ try {
334
+ return await this.#fetch(`${this.#baseURL}${request.path}`, {
335
+ method: request.method,
336
+ headers: request.headers,
337
+ body: request.body,
338
+ signal: controller.signal
339
+ });
340
+ } catch (error) {
341
+ if (error instanceof RESTError)
342
+ throw error;
343
+ const timedOut = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
344
+ throw new TransportError(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, error);
345
+ } finally {
346
+ clearTimeout(timer);
347
+ request.signal?.removeEventListener("abort", onAbort);
348
+ }
349
+ }
350
+ }
351
+
352
+ // packages/rest/src/decoder.ts
353
+ class ResponseDecoder {
354
+ async read(response) {
355
+ if (response.status === 204)
356
+ return;
357
+ const contentType = response.headers.get("content-type") ?? "";
358
+ if (contentType.includes("application/json"))
359
+ return response.json().catch(() => {
360
+ return;
361
+ });
362
+ return response.text().catch(() => {
363
+ return;
364
+ });
365
+ }
366
+ errorData(payload) {
367
+ if (!payload || typeof payload !== "object")
368
+ return {};
369
+ const data = payload;
370
+ return {
371
+ message: typeof data.message === "string" ? data.message : undefined,
372
+ code: typeof data.code === "number" ? data.code : undefined,
373
+ retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
374
+ global: data.global === true
375
+ };
376
+ }
377
+ retryAfter(response, data) {
378
+ const raw = response.headers.get("Retry-After");
379
+ const header = raw === null ? Number.NaN : Number(raw);
380
+ const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
381
+ return Math.max(0, retryAfter);
382
+ }
383
+ }
384
+
53
385
  // packages/rest/src/routes.ts
54
386
  var Routes = {
55
387
  user: () => "/users/@me",
@@ -91,6 +423,8 @@ var Routes = {
91
423
  threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
92
424
  guildScheduledEvents: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events`,
93
425
  guildScheduledEvent: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}`,
426
+ guildScheduledEventUsers: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}/users`,
427
+ channelPermission: (channelId, overwriteId) => `/channels/${snowflake(channelId, "Channel ID")}/permissions/${snowflake(overwriteId, "Overwrite ID")}`,
94
428
  guildAutoModerationRules: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules`,
95
429
  guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
96
430
  voiceRegions: () => "/voice/regions",
@@ -106,23 +440,59 @@ var Routes = {
106
440
  invite: (code) => `/invites/${encodeURIComponent(code)}`,
107
441
  guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
108
442
  guildEmoji: (guildId, emojiId) => emojiId ? `/guilds/${snowflake(guildId, "Guild ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/emojis`,
109
- applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis`
443
+ applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis`,
444
+ pollExpire: (channelId, messageId) => `/channels/${snowflake(channelId, "Channel ID")}/polls/${snowflake(messageId, "Message ID")}/expire`,
445
+ pollAnswerVoters: (channelId, messageId, answerId) => `/channels/${snowflake(channelId, "Channel ID")}/polls/${snowflake(messageId, "Message ID")}/answers/${integer(answerId, "Answer ID")}`,
446
+ guildSticker: (guildId, stickerId) => stickerId ? `/guilds/${snowflake(guildId, "Guild ID")}/stickers/${snowflake(stickerId, "Sticker ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/stickers`,
447
+ sendSoundboardSound: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/send-soundboard-sound`,
448
+ soundboardDefaultSounds: () => "/soundboard-default-sounds",
449
+ guildSoundboardSound: (guildId, soundId) => soundId ? `/guilds/${snowflake(guildId, "Guild ID")}/soundboard-sounds/${snowflake(soundId, "Sound ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/soundboard-sounds`,
450
+ applicationSkus: (applicationId) => `/applications/${snowflake(applicationId, "Application ID")}/skus`,
451
+ applicationEntitlement: (applicationId, entitlementId) => entitlementId ? `/applications/${snowflake(applicationId, "Application ID")}/entitlements/${snowflake(entitlementId, "Entitlement ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/entitlements`,
452
+ consumeEntitlement: (applicationId, entitlementId) => `/applications/${snowflake(applicationId, "Application ID")}/entitlements/${snowflake(entitlementId, "Entitlement ID")}/consume`,
453
+ skuSubscription: (skuId, subscriptionId) => subscriptionId ? `/skus/${snowflake(skuId, "SKU ID")}/subscriptions/${snowflake(subscriptionId, "Subscription ID")}` : `/skus/${snowflake(skuId, "SKU ID")}/subscriptions`
110
454
  };
455
+ function integer(value, field) {
456
+ if (!Number.isInteger(value) || value < 0)
457
+ throw new TypeError(`${field} must be a non-negative integer.`);
458
+ return String(value);
459
+ }
111
460
  function snowflake(value, field) {
112
461
  if (!/^\d{1,20}$/.test(value))
113
462
  throw new TypeError(`${field} must be a valid Discord snowflake.`);
114
463
  return value;
115
464
  }
116
465
  // packages/rest/src/redis.ts
466
+ var RESERVE_SCRIPT = `
467
+ local raw = redis.call('GET', KEYS[1])
468
+ if not raw then return {1, 0} end
469
+ local ok, state = pcall(cjson.decode, raw)
470
+ if not ok or type(state) ~= 'table' or type(state.resetAt) ~= 'number' then
471
+ return {1, 0}
472
+ end
473
+ local now = tonumber(ARGV[1])
474
+ local wait = state.resetAt - now
475
+ if wait <= 0 then return {1, 0} end
476
+ if type(state.remaining) == 'number' and state.remaining > 0 then
477
+ state.remaining = state.remaining - 1
478
+ redis.call('SET', KEYS[1], cjson.encode(state), 'PX', math.ceil(wait))
479
+ return {1, 0}
480
+ end
481
+ return {0, math.ceil(wait)}
482
+ `;
483
+
117
484
  class RedisRateLimitStore {
118
485
  #client;
119
486
  #prefix;
120
487
  #onErrorCallback;
488
+ #local = new MemoryRateLimitStore;
121
489
  #lastError;
122
490
  constructor(options) {
123
491
  this.#client = options.client;
124
492
  this.#prefix = options.prefix ?? "lunibee:rest:";
125
493
  this.#onErrorCallback = options.onError;
494
+ if (typeof options.client.eval === "function")
495
+ this.reserve = (key) => this.#reserve(key);
126
496
  }
127
497
  get lastError() {
128
498
  return this.#lastError;
@@ -147,10 +517,11 @@ class RedisRateLimitStore {
147
517
  return hash ?? undefined;
148
518
  } catch (error) {
149
519
  this.#onError("getBucketHash", error);
150
- return;
520
+ return this.#local.getBucketHash(route);
151
521
  }
152
522
  }
153
523
  async setBucketHash(route, hash) {
524
+ this.#local.setBucketHash(route, hash);
154
525
  try {
155
526
  await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
156
527
  this.#markHealthy();
@@ -167,10 +538,11 @@ class RedisRateLimitStore {
167
538
  return JSON.parse(data);
168
539
  } catch (error) {
169
540
  this.#onError("getBucket", error);
170
- return;
541
+ return this.#local.getBucket(key);
171
542
  }
172
543
  }
173
544
  async updateBucket(key, state) {
545
+ this.#local.updateBucket(key, state);
174
546
  const bucketKey = `${this.#prefix}bucket:${key}`;
175
547
  const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
176
548
  try {
@@ -185,6 +557,23 @@ class RedisRateLimitStore {
185
557
  this.#onError("updateBucket", error);
186
558
  }
187
559
  }
560
+ get supportsReservation() {
561
+ return typeof this.#client.eval === "function";
562
+ }
563
+ reserve;
564
+ async#reserve(key) {
565
+ try {
566
+ const result = await this.#client.eval(RESERVE_SCRIPT, 1, `${this.#prefix}bucket:${key}`, Date.now());
567
+ this.#markHealthy();
568
+ if (!Array.isArray(result))
569
+ return { granted: true };
570
+ const [granted, retryAfterMs] = result;
571
+ return Number(granted) === 1 ? { granted: true } : { granted: false, retryAfterMs: Number(retryAfterMs) || 0 };
572
+ } catch (error) {
573
+ this.#onError("reserve", error);
574
+ return this.#local.reserve(key);
575
+ }
576
+ }
188
577
  async getGlobalReset() {
189
578
  try {
190
579
  const resetAt = await this.#client.get(`${this.#prefix}global`);
@@ -192,10 +581,11 @@ class RedisRateLimitStore {
192
581
  return resetAt ? Number(resetAt) : 0;
193
582
  } catch (error) {
194
583
  this.#onError("getGlobalReset", error);
195
- return 0;
584
+ return this.#local.getGlobalReset();
196
585
  }
197
586
  }
198
587
  async setGlobalReset(resetAt) {
588
+ this.#local.setGlobalReset(resetAt);
199
589
  const globalKey = `${this.#prefix}global`;
200
590
  const ttl = Math.ceil((resetAt - Date.now()) / 1000);
201
591
  try {
@@ -251,22 +641,6 @@ class WebhookClient {
251
641
  }
252
642
 
253
643
  // 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
644
  var LIBRARY_VERSION = package_default.version;
271
645
  var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
272
646
  var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
@@ -293,26 +667,6 @@ function isRequestData(value) {
293
667
  return false;
294
668
  return keys.every((key) => REQUEST_DATA_KEYS.has(key));
295
669
  }
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
670
  function createRetryPolicy(maxRetries = 2) {
317
671
  if (!Number.isFinite(maxRetries) || maxRetries < 0)
318
672
  throw new TypeError("maxRetries must be a non-negative finite number.");
@@ -320,7 +674,10 @@ function createRetryPolicy(maxRetries = 2) {
320
674
  maxRetries: Math.floor(maxRetries),
321
675
  shouldRetry(method, status) {
322
676
  const normalized = method.toUpperCase();
323
- return status === 429 || status >= 500 && status <= 599 && ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
677
+ const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
678
+ if (status === 0)
679
+ return idempotent;
680
+ return status === 429 || status >= 500 && status <= 599 && idempotent;
324
681
  },
325
682
  getDelay(attempt, retryAfter) {
326
683
  if (retryAfter !== undefined)
@@ -331,22 +688,32 @@ function createRetryPolicy(maxRetries = 2) {
331
688
  }
332
689
 
333
690
  class REST {
334
- #baseURL;
335
- #timeout;
336
691
  #retryPolicy;
337
692
  #token;
338
- #store;
339
- #localQueues = new Map;
693
+ #limiter;
694
+ #scheduler = new RequestScheduler;
695
+ #transport;
696
+ #decoder = new ResponseDecoder;
340
697
  #hooks = {};
698
+ #concurrentBuckets;
341
699
  constructor(options = {}) {
342
700
  this.#token = options.token;
343
- this.#timeout = Math.max(1, options.timeout ?? 15000);
344
701
  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;
702
+ this.#transport = options.transport ?? new HttpTransport({
703
+ baseURL: options.baseURL,
704
+ timeout: options.timeout
705
+ });
706
+ const store = options.store ?? new MemoryRateLimitStore;
707
+ this.#concurrentBuckets = (options.concurrentBuckets ?? false) && typeof store.reserve === "function";
708
+ this.#limiter = new RateLimiter(store, {
709
+ concurrent: this.#concurrentBuckets
710
+ });
347
711
  if (options.hooks)
348
712
  this.#hooks = options.hooks;
349
713
  }
714
+ get store() {
715
+ return this.#limiter.store;
716
+ }
350
717
  setHooks(hooks) {
351
718
  this.#hooks = hooks;
352
719
  return this;
@@ -357,121 +724,110 @@ class REST {
357
724
  this.#token = token;
358
725
  }
359
726
  async request(method, path, body, options = {}) {
360
- const normalizedMethod = method.toUpperCase();
361
- if (!normalizedMethod)
727
+ if (!method.toUpperCase())
362
728
  throw new TypeError("REST method is required.");
363
729
  if (!path.startsWith("/"))
364
730
  throw new TypeError("REST paths must start with '/'.");
731
+ const route = createRouteKey(method, path);
365
732
  const files = options.files;
366
733
  if (files && files.length > 0 && !(body instanceof FormData))
367
734
  body = this.#fileForm(body, files);
368
735
  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
736
+ const requestPath = query ? `${path}${query}` : path;
737
+ return this.#scheduler.runResolved(() => this.#limiter.resolveBucketKey(route.route, route.major), options.signal, path, (bucketKey, release) => this.#attempts(route, requestPath, body, options, bucketKey, release));
738
+ }
739
+ async#attempts(route, requestPath, body, options, initialBucketKey, release) {
740
+ const { method, path } = route;
741
+ let bucketKey = initialBucketKey;
742
+ for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
743
+ await this.#limiter.acquire(bucketKey, options.signal, path);
744
+ if (this.#concurrentBuckets) {
745
+ const bucket = await this.#limiter.store.getBucket(bucketKey);
746
+ if (bucket && bucket.resetAt > Date.now())
747
+ release();
748
+ }
749
+ this.#emit(this.#hooks.onRequest, { method, path, attempt });
750
+ const requestStart = Date.now();
751
+ let response;
752
+ try {
753
+ response = await this.#transport.send({
754
+ method,
755
+ path: requestPath,
756
+ headers: this.#headers(body, options),
757
+ body: this.#encodeBody(body),
758
+ signal: options.signal
391
759
  });
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);
760
+ } catch (error) {
761
+ if (error instanceof RESTError)
762
+ throw error;
763
+ if (options.signal?.aborted)
764
+ throw abortError(path, options.signal.reason);
765
+ const failure = error instanceof TransportError ? error : new TransportError("Discord REST request failed", false, error);
766
+ if (!failure.timedOut && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, 0)) {
767
+ await sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
768
+ continue;
467
769
  }
770
+ throw new RESTError(failure.message, 0, undefined, undefined, {
771
+ method,
772
+ path,
773
+ cause: failure.cause ?? failure,
774
+ kind: failure.timedOut ? "timeout" : "network"
775
+ });
468
776
  }
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);
777
+ this.#emit(this.#hooks.onResponse, {
778
+ method,
779
+ path,
780
+ status: response.status,
781
+ durationMs: Date.now() - requestStart
782
+ });
783
+ bucketKey = await this.#limiter.applyResponse(response, bucketKey, route.route, route.major);
784
+ const payload = await this.#decoder.read(response);
785
+ if (response.ok)
786
+ return response.status === 204 ? undefined : payload;
787
+ const data = this.#decoder.errorData(payload);
788
+ const retryAfter = response.status === 429 ? this.#decoder.retryAfter(response, data) : undefined;
789
+ if (data.global && retryAfter !== undefined)
790
+ await this.#limiter.noteGlobalReset(Date.now() + retryAfter * 1000);
791
+ if (response.status === 429)
792
+ this.#emit(this.#hooks.onRateLimit, {
793
+ method,
794
+ path,
795
+ retryAfterMs: (retryAfter ?? 1) * 1000,
796
+ global: data.global ?? false,
797
+ bucket: await this.#limiter.store.getBucketHash(route.route)
798
+ });
799
+ if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, response.status)) {
800
+ const delay = this.#retryPolicy.getDelay(attempt, retryAfter);
801
+ this.#emit(this.#hooks.onRetry, {
802
+ method,
803
+ path,
804
+ attempt,
805
+ status: response.status,
806
+ delayMs: delay
807
+ });
808
+ await sleep(delay, options.signal, path);
809
+ continue;
810
+ }
811
+ throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method, path });
474
812
  }
813
+ throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method, path });
814
+ }
815
+ #headers(body, options) {
816
+ const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
817
+ const headers = { ...options.headers };
818
+ if (options.auth !== false && this.#token)
819
+ headers["Authorization"] = `Bot ${this.#token}`;
820
+ headers["User-Agent"] = USER_AGENT;
821
+ if (options.reason !== undefined)
822
+ headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
823
+ if (!isFormData)
824
+ headers["Content-Type"] = "application/json";
825
+ return headers;
826
+ }
827
+ #encodeBody(body) {
828
+ if (typeof FormData !== "undefined" && body instanceof FormData)
829
+ return body;
830
+ return body === undefined ? undefined : JSON.stringify(body);
475
831
  }
476
832
  #dispatch(method, path, bodyOrOptions, options) {
477
833
  if (options === undefined && isRequestData(bodyOrOptions))
@@ -514,127 +870,33 @@ class REST {
514
870
  #emit(hook, ctx) {
515
871
  if (!hook)
516
872
  return;
873
+ const withPath = ctx;
874
+ if (typeof withPath.path === "string")
875
+ ctx = { ...ctx, path: redactPath(withPath.path) };
517
876
  Promise.resolve().then(() => hook(ctx)).catch(() => {});
518
877
  }
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
878
  }
629
879
  export {
630
880
  RESTError as DiscordAPIError,
881
+ HttpTransport,
631
882
  MemoryRateLimitStore,
632
883
  REST,
633
884
  RESTError,
885
+ RateLimiter,
634
886
  RedisRateLimitStore,
887
+ RequestScheduler,
888
+ ResponseDecoder,
635
889
  Routes,
890
+ TransportError,
636
891
  WebhookClient,
637
- createRetryPolicy
892
+ abortError,
893
+ createRetryPolicy,
894
+ createRouteKey,
895
+ majorParameter,
896
+ normalizeRoutePath,
897
+ redactPath,
898
+ scopeBucket,
899
+ serializeQuery
638
900
  };
639
901
 
640
- //# debugId=7273A79583E1900164756E2164756E21
902
+ //# debugId=3E6A1976705386FE64756E2164756E21