lunibee 0.1.5 → 0.1.7

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 (60) hide show
  1. package/README.md +13 -24
  2. package/dist/builders/commands.d.ts +37 -0
  3. package/dist/builders/components.d.ts +127 -12
  4. package/dist/builders/embed.d.ts +3 -1
  5. package/dist/builders/index.d.ts +2 -2
  6. package/dist/builders/index.js +319 -16
  7. package/dist/builders/index.js.map +8 -7
  8. package/dist/collection/index.d.ts +36 -0
  9. package/dist/collection/index.js +110 -6
  10. package/dist/collection/index.js.map +4 -4
  11. package/dist/core/events.d.ts +69 -3
  12. package/dist/core/index.d.ts +158 -90
  13. package/dist/core/index.js +1543 -1169
  14. package/dist/core/index.js.map +24 -23
  15. package/dist/core/permissions.d.ts +112 -53
  16. package/dist/formatters/index.d.ts +43 -3
  17. package/dist/formatters/index.js +64 -1
  18. package/dist/formatters/index.js.map +3 -3
  19. package/dist/handlers/index.js.map +2 -2
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.js +3882 -1588
  22. package/dist/index.js.map +43 -35
  23. package/dist/managers/emoji.d.ts +38 -0
  24. package/dist/managers/guild.d.ts +141 -0
  25. package/dist/managers/index.d.ts +86 -75
  26. package/dist/managers/index.js +1084 -67
  27. package/dist/managers/index.js.map +21 -16
  28. package/dist/managers/message.d.ts +7 -12
  29. package/dist/managers/user.d.ts +20 -0
  30. package/dist/rest/index.d.ts +173 -1
  31. package/dist/rest/index.js +343 -47
  32. package/dist/rest/index.js.map +8 -6
  33. package/dist/rest/redis.d.ts +37 -0
  34. package/dist/rest/routes.d.ts +26 -1
  35. package/dist/rest/store.d.ts +38 -0
  36. package/dist/rest/webhook.d.ts +2 -2
  37. package/dist/sharding/cluster.d.ts +61 -0
  38. package/dist/sharding/index.d.ts +8 -0
  39. package/dist/sharding/index.js +289 -18
  40. package/dist/sharding/index.js.map +8 -7
  41. package/dist/structures/audit-log.d.ts +26 -0
  42. package/dist/structures/base.d.ts +131 -48
  43. package/dist/structures/embed.d.ts +65 -0
  44. package/dist/structures/index.d.ts +74 -26
  45. package/dist/structures/index.js +1528 -59
  46. package/dist/structures/index.js.map +14 -8
  47. package/dist/structures/interactions.d.ts +72 -17
  48. package/dist/structures/resources.d.ts +199 -7
  49. package/dist/types/index.d.ts +605 -84
  50. package/dist/types/index.js +71 -1
  51. package/dist/types/index.js.map +3 -3
  52. package/dist/utils/index.js.map +2 -2
  53. package/dist/voice/index.d.ts +88 -0
  54. package/dist/voice/index.js +204 -5
  55. package/dist/voice/index.js.map +3 -3
  56. package/dist/ws/index.d.ts +40 -1
  57. package/dist/ws/index.js +105 -18
  58. package/dist/ws/index.js.map +4 -4
  59. package/package.json +16 -16
  60. package/dist/structures/permissions.d.ts +0 -73
@@ -1,9 +1,64 @@
1
1
  // @bun
2
+ // packages/rest/package.json
3
+ var package_default = {
4
+ name: "@lunibee/rest",
5
+ version: "0.1.7",
6
+ type: "module",
7
+ module: "./src/index.ts",
8
+ types: "./src/index.ts",
9
+ exports: {
10
+ ".": "./src/index.ts"
11
+ }
12
+ };
13
+
14
+ // packages/rest/src/store.ts
15
+ class MemoryRateLimitStore {
16
+ #buckets = new Map;
17
+ #routeBuckets = new Map;
18
+ #globalResetAt = 0;
19
+ static #PRUNE_INTERVAL = 60000;
20
+ #lastPruneAt = 0;
21
+ getBucketHash(route) {
22
+ return this.#routeBuckets.get(route);
23
+ }
24
+ setBucketHash(route, hash) {
25
+ this.#routeBuckets.set(route, hash);
26
+ }
27
+ getBucket(key) {
28
+ return this.#buckets.get(key);
29
+ }
30
+ updateBucket(key, state) {
31
+ this.#buckets.set(key, state);
32
+ const now = Date.now();
33
+ if (now - this.#lastPruneAt >= MemoryRateLimitStore.#PRUNE_INTERVAL) {
34
+ this.#lastPruneAt = now;
35
+ this.prune();
36
+ }
37
+ }
38
+ prune(maxAge = 0) {
39
+ const cutoff = Date.now() - Math.max(0, maxAge);
40
+ for (const [key, state] of this.#buckets) {
41
+ if (state.resetAt <= cutoff)
42
+ this.#buckets.delete(key);
43
+ }
44
+ }
45
+ getGlobalReset() {
46
+ return this.#globalResetAt;
47
+ }
48
+ setGlobalReset(resetAt) {
49
+ this.#globalResetAt = resetAt;
50
+ }
51
+ }
52
+
2
53
  // packages/rest/src/routes.ts
3
54
  var Routes = {
4
55
  user: () => "/users/@me",
5
56
  userById: (userId) => `/users/${snowflake(userId, "User ID")}`,
6
57
  guild: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}`,
58
+ guildPreview: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/preview`,
59
+ guildActiveThreads: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/threads/active`,
60
+ guildWebhooks: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/webhooks`,
61
+ guildInvites: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/invites`,
7
62
  guildChannels: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/channels`,
8
63
  guildMember: (guildId, userId) => `/guilds/${snowflake(guildId, "Guild ID")}/members/${snowflake(userId, "User ID")}`,
9
64
  guildRoles: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/roles`,
@@ -31,7 +86,6 @@ var Routes = {
31
86
  guildApplicationCommand: (applicationId, guildId, commandId) => `/applications/${snowflake(applicationId, "Application ID")}/guilds/${snowflake(guildId, "Guild ID")}/commands/${snowflake(commandId, "Command ID")}`,
32
87
  interactionCallback: (interactionId, token) => `/interactions/${snowflake(interactionId, "Interaction ID")}/${encodeURIComponent(token)}/callback`,
33
88
  interactionOriginalResponse: (applicationId, token) => `/webhooks/${snowflake(applicationId, "Application ID")}/${encodeURIComponent(token)}/messages/@original`,
34
- guildActiveThreads: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/threads/active`,
35
89
  channelPublicArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads/archived/public`,
36
90
  channelPrivateArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads/archived/private`,
37
91
  threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
@@ -41,13 +95,122 @@ var Routes = {
41
95
  guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
42
96
  voiceRegions: () => "/voice/regions",
43
97
  gateway: () => "/gateway",
44
- gatewayBot: () => "/gateway/bot"
98
+ gatewayBot: () => "/gateway/bot",
99
+ guildAuditLog: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/audit-logs`,
100
+ guildMembers: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members`,
101
+ guildWelcomeScreen: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/welcome-screen`,
102
+ guildOnboarding: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/onboarding`,
103
+ guildPrune: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/prune`,
104
+ stageInstances: () => "/stage-instances",
105
+ stageInstanceByChannel: (channelId) => `/stage-instances/${snowflake(channelId, "Channel ID")}`,
106
+ invite: (code) => `/invites/${encodeURIComponent(code)}`,
107
+ guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
108
+ 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`
45
110
  };
46
111
  function snowflake(value, field) {
47
112
  if (!/^\d{1,20}$/.test(value))
48
113
  throw new TypeError(`${field} must be a valid Discord snowflake.`);
49
114
  return value;
50
115
  }
116
+ // packages/rest/src/redis.ts
117
+ class RedisRateLimitStore {
118
+ #client;
119
+ #prefix;
120
+ #onErrorCallback;
121
+ #lastError;
122
+ constructor(options) {
123
+ this.#client = options.client;
124
+ this.#prefix = options.prefix ?? "lunibee:rest:";
125
+ this.#onErrorCallback = options.onError;
126
+ }
127
+ get lastError() {
128
+ return this.#lastError;
129
+ }
130
+ isHealthy() {
131
+ return this.#lastError === undefined;
132
+ }
133
+ #onError(operation, error) {
134
+ this.#lastError = error;
135
+ console.warn(`[lunibee/rest] Redis rate-limit store ${operation} failed; ` + `falling back to local limiting for this call.`, error);
136
+ try {
137
+ this.#onErrorCallback?.(operation, error);
138
+ } catch {}
139
+ }
140
+ #markHealthy() {
141
+ this.#lastError = undefined;
142
+ }
143
+ async getBucketHash(route) {
144
+ try {
145
+ const hash = await this.#client.get(`${this.#prefix}route:${route}`);
146
+ this.#markHealthy();
147
+ return hash ?? undefined;
148
+ } catch (error) {
149
+ this.#onError("getBucketHash", error);
150
+ return;
151
+ }
152
+ }
153
+ async setBucketHash(route, hash) {
154
+ try {
155
+ await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
156
+ this.#markHealthy();
157
+ } catch (error) {
158
+ this.#onError("setBucketHash", error);
159
+ }
160
+ }
161
+ async getBucket(key) {
162
+ try {
163
+ const data = await this.#client.get(`${this.#prefix}bucket:${key}`);
164
+ this.#markHealthy();
165
+ if (!data)
166
+ return;
167
+ return JSON.parse(data);
168
+ } catch (error) {
169
+ this.#onError("getBucket", error);
170
+ return;
171
+ }
172
+ }
173
+ async updateBucket(key, state) {
174
+ const bucketKey = `${this.#prefix}bucket:${key}`;
175
+ const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
176
+ try {
177
+ if (ttl <= 0) {
178
+ await this.#client.del(bucketKey);
179
+ this.#markHealthy();
180
+ return;
181
+ }
182
+ await this.#client.set(bucketKey, JSON.stringify(state), "EX", ttl);
183
+ this.#markHealthy();
184
+ } catch (error) {
185
+ this.#onError("updateBucket", error);
186
+ }
187
+ }
188
+ async getGlobalReset() {
189
+ try {
190
+ const resetAt = await this.#client.get(`${this.#prefix}global`);
191
+ this.#markHealthy();
192
+ return resetAt ? Number(resetAt) : 0;
193
+ } catch (error) {
194
+ this.#onError("getGlobalReset", error);
195
+ return 0;
196
+ }
197
+ }
198
+ async setGlobalReset(resetAt) {
199
+ const globalKey = `${this.#prefix}global`;
200
+ const ttl = Math.ceil((resetAt - Date.now()) / 1000);
201
+ try {
202
+ if (ttl <= 0) {
203
+ await this.#client.del(globalKey);
204
+ this.#markHealthy();
205
+ return;
206
+ }
207
+ await this.#client.set(globalKey, String(resetAt), "EX", ttl);
208
+ this.#markHealthy();
209
+ } catch (error) {
210
+ this.#onError("setGlobalReset", error);
211
+ }
212
+ }
213
+ }
51
214
  // packages/rest/src/webhook.ts
52
215
  class WebhookClient {
53
216
  id;
@@ -72,7 +235,7 @@ class WebhookClient {
72
235
  const payload = typeof options === "string" ? { content: options } : options;
73
236
  const { thread_id, ...body } = payload;
74
237
  const query = thread_id ? `?thread_id=${thread_id}&wait=true` : `?wait=true`;
75
- const formattedEmbeds = body.embeds?.map((e) => typeof e === "object" && e !== null && ("toJSON" in e) ? e.toJSON() : e);
238
+ const formattedEmbeds = body.embeds?.map((e) => ("toJSON" in e) ? e.toJSON() : e);
76
239
  return this.#rest.post(`${Routes.webhook(this.id, this.token)}${query}`, {
77
240
  ...body,
78
241
  embeds: formattedEmbeds
@@ -104,6 +267,52 @@ class RESTError extends Error {
104
267
  this.path = options.path;
105
268
  }
106
269
  }
270
+ var LIBRARY_VERSION = package_default.version;
271
+ var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
272
+ var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
273
+ var REQUEST_DATA_KEYS = new Set([
274
+ "body",
275
+ "files",
276
+ "query",
277
+ "headers",
278
+ "reason",
279
+ "auth",
280
+ "signal"
281
+ ]);
282
+ function isRequestData(value) {
283
+ if (value === null || typeof value !== "object")
284
+ return false;
285
+ if (Array.isArray(value))
286
+ return false;
287
+ if (typeof FormData !== "undefined" && value instanceof FormData)
288
+ return false;
289
+ if (value instanceof Blob || value instanceof Uint8Array || value instanceof ArrayBuffer)
290
+ return false;
291
+ const keys = Object.keys(value);
292
+ if (keys.length === 0)
293
+ return false;
294
+ return keys.every((key) => REQUEST_DATA_KEYS.has(key));
295
+ }
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
+ }
107
316
  function createRetryPolicy(maxRetries = 2) {
108
317
  if (!Number.isFinite(maxRetries) || maxRetries < 0)
109
318
  throw new TypeError("maxRetries must be a non-negative finite number.");
@@ -126,14 +335,21 @@ class REST {
126
335
  #timeout;
127
336
  #retryPolicy;
128
337
  #token;
129
- #globalResetAt = 0;
130
- #buckets = new Map;
131
- #routeBuckets = new Map;
338
+ #store;
339
+ #localQueues = new Map;
340
+ #hooks = {};
132
341
  constructor(options = {}) {
133
342
  this.#token = options.token;
134
343
  this.#timeout = Math.max(1, options.timeout ?? 15000);
135
344
  this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
136
345
  this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
346
+ this.#store = options.store ?? new MemoryRateLimitStore;
347
+ if (options.hooks)
348
+ this.#hooks = options.hooks;
349
+ }
350
+ setHooks(hooks) {
351
+ this.#hooks = hooks;
352
+ return this;
137
353
  }
138
354
  setToken(token) {
139
355
  if (!token.trim())
@@ -146,52 +362,91 @@ class REST {
146
362
  throw new TypeError("REST method is required.");
147
363
  if (!path.startsWith("/"))
148
364
  throw new TypeError("REST paths must start with '/'.");
365
+ const files = options.files;
366
+ if (files && files.length > 0 && !(body instanceof FormData))
367
+ body = this.#fileForm(body, files);
368
+ const query = serializeQuery(options.query);
369
+ const finalPath = query ? `${path}${query}` : path;
149
370
  const route = `${normalizedMethod}:${this.#normalizeRoute(path)}`;
150
- let bucketKey = this.#routeBuckets.get(route) ?? route;
151
- let bucket = this.#buckets.get(bucketKey) ?? {
152
- remaining: 1,
153
- resetAt: 0,
154
- queue: Promise.resolve()
155
- };
156
- this.#buckets.set(bucketKey, bucket);
157
- const previous = bucket.queue;
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();
158
375
  let release;
159
- bucket.queue = new Promise((resolve) => {
376
+ const gate = new Promise((resolve) => {
160
377
  release = resolve;
161
378
  });
379
+ this.#localQueues.set(queueKey, gate);
162
380
  await this.#abortable(previous, options.signal, path);
163
381
  try {
164
382
  for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
165
- await this.#wait(bucket, options.signal, path);
383
+ await this.#wait(bucketKey, options.signal, path);
166
384
  await this.#waitGlobal(options.signal, path);
167
385
  const controller = new AbortController;
168
386
  const onAbort = () => controller.abort(options.signal?.reason);
169
387
  if (options.signal?.aborted)
170
388
  throw this.#abortError(path, options.signal.reason);
171
- options.signal?.addEventListener("abort", onAbort, { once: true });
389
+ options.signal?.addEventListener("abort", onAbort, {
390
+ once: true
391
+ });
172
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);
173
403
  try {
174
- const response = await fetch(`${this.#baseURL}${path}`, {
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}`, {
175
411
  method: normalizedMethod,
176
- headers: {
177
- ...this.#token ? { Authorization: `Bot ${this.#token}` } : {},
178
- "Content-Type": "application/json",
179
- "User-Agent": "Lunibee/0.1.0"
180
- },
181
- body: body === undefined ? undefined : JSON.stringify(body),
412
+ headers,
413
+ body: fetchBody,
182
414
  signal: controller.signal
183
415
  });
184
- bucket = this.#update(response, bucket, route, bucketKey);
185
- bucketKey = this.#routeBuckets.get(route) ?? bucketKey;
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);
186
423
  const payload = await this.#readPayload(response);
187
424
  if (response.ok)
188
425
  return response.status === 204 ? undefined : payload;
189
426
  const data = this.#errorData(payload);
190
427
  const retryAfter = response.status === 429 ? this.#retryAfter(response, data) : undefined;
191
- if (data.global && retryAfter !== undefined)
192
- this.#globalResetAt = Date.now() + retryAfter * 1000;
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
+ });
193
440
  if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, response.status)) {
194
- await this.#sleep(this.#retryPolicy.getDelay(attempt, retryAfter), options.signal, path);
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);
195
450
  continue;
196
451
  }
197
452
  throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method: normalizedMethod, path });
@@ -214,30 +469,64 @@ class REST {
214
469
  throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method: normalizedMethod, path });
215
470
  } finally {
216
471
  release();
472
+ if (this.#localQueues.get(queueKey) === gate)
473
+ this.#localQueues.delete(queueKey);
217
474
  }
218
475
  }
476
+ #dispatch(method, path, bodyOrOptions, options) {
477
+ if (options === undefined && isRequestData(bodyOrOptions))
478
+ return this.request(method, path, bodyOrOptions.body, bodyOrOptions);
479
+ return this.request(method, path, bodyOrOptions, options);
480
+ }
219
481
  get(path, options) {
220
- return this.request("GET", path, undefined, options);
482
+ return this.request("GET", path, options?.body, options);
221
483
  }
222
484
  post(path, body, options) {
223
- return this.request("POST", path, body, options);
485
+ return this.#dispatch("POST", path, body, options);
224
486
  }
225
487
  patch(path, body, options) {
226
- return this.request("PATCH", path, body, options);
488
+ return this.#dispatch("PATCH", path, body, options);
227
489
  }
228
490
  put(path, body, options) {
229
- return this.request("PUT", path, body, options);
491
+ return this.#dispatch("PUT", path, body, options);
230
492
  }
231
493
  delete(path, options) {
232
- return this.request("DELETE", path, undefined, options);
494
+ return this.request("DELETE", path, options?.body, options);
495
+ }
496
+ #fileForm(payload, files) {
497
+ const form = new FormData;
498
+ form.append("payload_json", JSON.stringify(payload));
499
+ for (let i = 0;i < files.length; i++) {
500
+ const file = files[i];
501
+ const blob = file.data instanceof Blob ? file.data : new Blob([file.data], {
502
+ type: file.contentType ?? "application/octet-stream"
503
+ });
504
+ form.append(`files[${i}]`, blob, file.name);
505
+ }
506
+ return form;
233
507
  }
234
- async#wait(bucket, signal, path) {
508
+ postWithFiles(path, payload, files, options) {
509
+ return this.request("POST", path, this.#fileForm(payload, files), options);
510
+ }
511
+ patchWithFiles(path, payload, files, options) {
512
+ return this.request("PATCH", path, this.#fileForm(payload, files), options);
513
+ }
514
+ #emit(hook, ctx) {
515
+ if (!hook)
516
+ return;
517
+ Promise.resolve().then(() => hook(ctx)).catch(() => {});
518
+ }
519
+ async#wait(bucketKey, signal, path) {
520
+ const bucket = await this.#store.getBucket(bucketKey);
521
+ if (!bucket)
522
+ return;
235
523
  const delay = bucket.resetAt - Date.now();
236
524
  if (bucket.remaining <= 0 && delay > 0)
237
525
  await this.#sleep(delay, signal, path);
238
526
  }
239
527
  async#waitGlobal(signal, path) {
240
- const delay = this.#globalResetAt - Date.now();
528
+ const globalResetAt = await this.#store.getGlobalReset();
529
+ const delay = globalResetAt - Date.now();
241
530
  if (delay > 0)
242
531
  await this.#sleep(delay, signal, path);
243
532
  }
@@ -301,16 +590,17 @@ class REST {
301
590
  const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
302
591
  return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
303
592
  }
304
- #update(response, bucket, route, bucketKey) {
593
+ async#update(response, bucketKey, route, major) {
594
+ let currentKey = bucketKey;
305
595
  const serverBucket = response.headers.get("X-RateLimit-Bucket");
306
596
  if (serverBucket) {
307
- this.#routeBuckets.set(route, serverBucket);
308
- if (serverBucket !== bucketKey) {
309
- const shared = this.#buckets.get(serverBucket) ?? bucket;
310
- this.#buckets.set(serverBucket, shared);
311
- bucket = shared;
312
- }
597
+ await this.#store.setBucketHash(route, serverBucket);
598
+ currentKey = scopeBucket(serverBucket, major);
313
599
  }
600
+ let bucket = await this.#store.getBucket(currentKey) ?? {
601
+ remaining: 1,
602
+ resetAt: 0
603
+ };
314
604
  const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
315
605
  const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
316
606
  const reset = Number(response.headers.get("X-RateLimit-Reset"));
@@ -325,20 +615,26 @@ class REST {
325
615
  if (Number.isFinite(retryAfter))
326
616
  bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
327
617
  }
618
+ await this.#store.updateBucket(currentKey, bucket);
328
619
  if (response.headers.get("X-RateLimit-Global") === "true") {
329
620
  const retryAfter = Number(response.headers.get("Retry-After"));
330
- if (Number.isFinite(retryAfter))
331
- this.#globalResetAt = Math.max(this.#globalResetAt, Date.now() + retryAfter * 1000);
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
+ }
332
625
  }
333
- return bucket;
626
+ return currentKey;
334
627
  }
335
628
  }
336
629
  export {
630
+ RESTError as DiscordAPIError,
631
+ MemoryRateLimitStore,
337
632
  REST,
338
633
  RESTError,
634
+ RedisRateLimitStore,
339
635
  Routes,
340
636
  WebhookClient,
341
637
  createRetryPolicy
342
638
  };
343
639
 
344
- //# debugId=69F9FDE9A0B8ECD864756E2164756E21
640
+ //# debugId=7273A79583E1900164756E2164756E21