lunibee 0.1.6 → 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 (80) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +223 -223
  3. package/dist/builders/commands.d.ts +39 -0
  4. package/dist/builders/components.d.ts +37 -12
  5. package/dist/builders/embed.d.ts +3 -1
  6. package/dist/builders/index.d.ts +2 -2
  7. package/dist/builders/index.js +192 -29
  8. package/dist/builders/index.js.map +9 -7
  9. package/dist/collection/index.d.ts +30 -0
  10. package/dist/collection/index.js +79 -5
  11. package/dist/collection/index.js.map +4 -4
  12. package/dist/core/events.d.ts +88 -2
  13. package/dist/core/index.d.ts +46 -85
  14. package/dist/core/index.js +2395 -789
  15. package/dist/core/index.js.map +44 -22
  16. package/dist/core/permissions.d.ts +112 -53
  17. package/dist/formatters/index.d.ts +43 -3
  18. package/dist/formatters/index.js +64 -1
  19. package/dist/formatters/index.js.map +3 -3
  20. package/dist/handlers/index.js.map +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +3783 -1009
  23. package/dist/index.js.map +63 -38
  24. package/dist/managers/emoji.d.ts +38 -0
  25. package/dist/managers/guild-resources.d.ts +109 -0
  26. package/dist/managers/guild.d.ts +117 -1
  27. package/dist/managers/index.d.ts +37 -1
  28. package/dist/managers/index.js +1125 -81
  29. package/dist/managers/index.js.map +25 -20
  30. package/dist/rest/decoder.d.ts +23 -0
  31. package/dist/rest/errors.d.ts +20 -0
  32. package/dist/rest/index.d.ts +119 -25
  33. package/dist/rest/index.js +676 -204
  34. package/dist/rest/index.js.map +14 -6
  35. package/dist/rest/limiter.d.ts +42 -0
  36. package/dist/rest/redis.d.ts +54 -0
  37. package/dist/rest/route.d.ts +31 -0
  38. package/dist/rest/routes.d.ts +24 -0
  39. package/dist/rest/scheduler.d.ts +42 -0
  40. package/dist/rest/store.d.ts +67 -0
  41. package/dist/rest/transport.d.ts +40 -0
  42. package/dist/rest/webhook.d.ts +2 -2
  43. package/dist/sharding/bus.d.ts +31 -0
  44. package/dist/sharding/cluster.d.ts +71 -0
  45. package/dist/sharding/index.d.ts +18 -2
  46. package/dist/sharding/index.js +1202 -317
  47. package/dist/sharding/index.js.map +18 -7
  48. package/dist/structures/audit-log.d.ts +26 -0
  49. package/dist/structures/base.d.ts +40 -0
  50. package/dist/structures/channels.d.ts +56 -0
  51. package/dist/structures/index.d.ts +30 -1
  52. package/dist/structures/index.js +963 -87
  53. package/dist/structures/index.js.map +17 -10
  54. package/dist/structures/interactions.d.ts +12 -52
  55. package/dist/structures/options.d.ts +55 -0
  56. package/dist/structures/resources.d.ts +194 -6
  57. package/dist/types/gateway-events.d.ts +132 -0
  58. package/dist/types/gateway.d.ts +181 -0
  59. package/dist/types/index.d.ts +78 -263
  60. package/dist/types/index.js +44 -2
  61. package/dist/types/index.js.map +5 -4
  62. package/dist/utils/index.js.map +2 -2
  63. package/dist/voice/index.d.ts +116 -0
  64. package/dist/voice/index.js +261 -10
  65. package/dist/voice/index.js.map +3 -3
  66. package/dist/ws/close-codes.d.ts +23 -0
  67. package/dist/ws/decoder.d.ts +58 -0
  68. package/dist/ws/heartbeat.d.ts +87 -0
  69. package/dist/ws/index.d.ts +24 -32
  70. package/dist/ws/index.js +920 -318
  71. package/dist/ws/index.js.map +15 -5
  72. package/dist/ws/opcodes.d.ts +14 -0
  73. package/dist/ws/protocol.d.ts +110 -0
  74. package/dist/ws/reconnect.d.ts +108 -0
  75. package/dist/ws/send-budget.d.ts +17 -0
  76. package/dist/ws/session.d.ts +100 -0
  77. package/dist/ws/state.d.ts +27 -0
  78. package/dist/ws/transport.d.ts +80 -0
  79. package/package.json +23 -22
  80. package/dist/structures/permissions.d.ts +0 -73
@@ -2,7 +2,7 @@
2
2
  // packages/rest/package.json
3
3
  var package_default = {
4
4
  name: "@lunibee/rest",
5
- version: "0.1.6",
5
+ version: "0.1.8",
6
6
  type: "module",
7
7
  module: "./src/index.ts",
8
8
  types: "./src/index.ts",
@@ -11,6 +11,351 @@ var package_default = {
11
11
  }
12
12
  };
13
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
+ 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
+ }
41
+ updateBucket(key, state) {
42
+ this.#buckets.set(key, state);
43
+ const now = Date.now();
44
+ if (now - this.#lastPruneAt >= MemoryRateLimitStore.#PRUNE_INTERVAL) {
45
+ this.#lastPruneAt = now;
46
+ this.prune();
47
+ }
48
+ }
49
+ prune(maxAge = 0) {
50
+ const cutoff = Date.now() - Math.max(0, maxAge);
51
+ for (const [key, state] of this.#buckets) {
52
+ if (state.resetAt <= cutoff)
53
+ this.#buckets.delete(key);
54
+ }
55
+ }
56
+ getGlobalReset() {
57
+ return this.#globalResetAt;
58
+ }
59
+ setGlobalReset(resetAt) {
60
+ this.#globalResetAt = resetAt;
61
+ }
62
+ }
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
+
14
359
  // packages/rest/src/routes.ts
15
360
  var Routes = {
16
361
  user: () => "/users/@me",
@@ -52,17 +397,169 @@ var Routes = {
52
397
  threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
53
398
  guildScheduledEvents: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events`,
54
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")}`,
55
402
  guildAutoModerationRules: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules`,
56
403
  guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
57
404
  voiceRegions: () => "/voice/regions",
58
405
  gateway: () => "/gateway",
59
- gatewayBot: () => "/gateway/bot"
406
+ gatewayBot: () => "/gateway/bot",
407
+ guildAuditLog: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/audit-logs`,
408
+ guildMembers: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members`,
409
+ guildWelcomeScreen: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/welcome-screen`,
410
+ guildOnboarding: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/onboarding`,
411
+ guildPrune: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/prune`,
412
+ stageInstances: () => "/stage-instances",
413
+ stageInstanceByChannel: (channelId) => `/stage-instances/${snowflake(channelId, "Channel ID")}`,
414
+ invite: (code) => `/invites/${encodeURIComponent(code)}`,
415
+ guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
416
+ guildEmoji: (guildId, emojiId) => emojiId ? `/guilds/${snowflake(guildId, "Guild ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/emojis`,
417
+ applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis`
60
418
  };
61
419
  function snowflake(value, field) {
62
420
  if (!/^\d{1,20}$/.test(value))
63
421
  throw new TypeError(`${field} must be a valid Discord snowflake.`);
64
422
  return value;
65
423
  }
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
+
443
+ class RedisRateLimitStore {
444
+ #client;
445
+ #prefix;
446
+ #onErrorCallback;
447
+ #local = new MemoryRateLimitStore;
448
+ #lastError;
449
+ constructor(options) {
450
+ this.#client = options.client;
451
+ this.#prefix = options.prefix ?? "lunibee:rest:";
452
+ this.#onErrorCallback = options.onError;
453
+ if (typeof options.client.eval === "function")
454
+ this.reserve = (key) => this.#reserve(key);
455
+ }
456
+ get lastError() {
457
+ return this.#lastError;
458
+ }
459
+ isHealthy() {
460
+ return this.#lastError === undefined;
461
+ }
462
+ #onError(operation, error) {
463
+ this.#lastError = error;
464
+ console.warn(`[lunibee/rest] Redis rate-limit store ${operation} failed; ` + `falling back to local limiting for this call.`, error);
465
+ try {
466
+ this.#onErrorCallback?.(operation, error);
467
+ } catch {}
468
+ }
469
+ #markHealthy() {
470
+ this.#lastError = undefined;
471
+ }
472
+ async getBucketHash(route) {
473
+ try {
474
+ const hash = await this.#client.get(`${this.#prefix}route:${route}`);
475
+ this.#markHealthy();
476
+ return hash ?? undefined;
477
+ } catch (error) {
478
+ this.#onError("getBucketHash", error);
479
+ return this.#local.getBucketHash(route);
480
+ }
481
+ }
482
+ async setBucketHash(route, hash) {
483
+ this.#local.setBucketHash(route, hash);
484
+ try {
485
+ await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
486
+ this.#markHealthy();
487
+ } catch (error) {
488
+ this.#onError("setBucketHash", error);
489
+ }
490
+ }
491
+ async getBucket(key) {
492
+ try {
493
+ const data = await this.#client.get(`${this.#prefix}bucket:${key}`);
494
+ this.#markHealthy();
495
+ if (!data)
496
+ return;
497
+ return JSON.parse(data);
498
+ } catch (error) {
499
+ this.#onError("getBucket", error);
500
+ return this.#local.getBucket(key);
501
+ }
502
+ }
503
+ async updateBucket(key, state) {
504
+ this.#local.updateBucket(key, state);
505
+ const bucketKey = `${this.#prefix}bucket:${key}`;
506
+ const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
507
+ try {
508
+ if (ttl <= 0) {
509
+ await this.#client.del(bucketKey);
510
+ this.#markHealthy();
511
+ return;
512
+ }
513
+ await this.#client.set(bucketKey, JSON.stringify(state), "EX", ttl);
514
+ this.#markHealthy();
515
+ } catch (error) {
516
+ this.#onError("updateBucket", error);
517
+ }
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
+ }
536
+ async getGlobalReset() {
537
+ try {
538
+ const resetAt = await this.#client.get(`${this.#prefix}global`);
539
+ this.#markHealthy();
540
+ return resetAt ? Number(resetAt) : 0;
541
+ } catch (error) {
542
+ this.#onError("getGlobalReset", error);
543
+ return this.#local.getGlobalReset();
544
+ }
545
+ }
546
+ async setGlobalReset(resetAt) {
547
+ this.#local.setGlobalReset(resetAt);
548
+ const globalKey = `${this.#prefix}global`;
549
+ const ttl = Math.ceil((resetAt - Date.now()) / 1000);
550
+ try {
551
+ if (ttl <= 0) {
552
+ await this.#client.del(globalKey);
553
+ this.#markHealthy();
554
+ return;
555
+ }
556
+ await this.#client.set(globalKey, String(resetAt), "EX", ttl);
557
+ this.#markHealthy();
558
+ } catch (error) {
559
+ this.#onError("setGlobalReset", error);
560
+ }
561
+ }
562
+ }
66
563
  // packages/rest/src/webhook.ts
67
564
  class WebhookClient {
68
565
  id;
@@ -87,7 +584,7 @@ class WebhookClient {
87
584
  const payload = typeof options === "string" ? { content: options } : options;
88
585
  const { thread_id, ...body } = payload;
89
586
  const query = thread_id ? `?thread_id=${thread_id}&wait=true` : `?wait=true`;
90
- const formattedEmbeds = body.embeds?.map((e) => typeof e === "object" && e !== null && ("toJSON" in e) ? e.toJSON() : e);
587
+ const formattedEmbeds = body.embeds?.map((e) => ("toJSON" in e) ? e.toJSON() : e);
91
588
  return this.#rest.post(`${Routes.webhook(this.id, this.token)}${query}`, {
92
589
  ...body,
93
590
  embeds: formattedEmbeds
@@ -103,25 +600,32 @@ class WebhookClient {
103
600
  }
104
601
 
105
602
  // packages/rest/src/index.ts
106
- class RESTError extends Error {
107
- status;
108
- code;
109
- errors;
110
- method;
111
- path;
112
- constructor(message, status, code, errors, options = {}) {
113
- super(message, options.cause === undefined ? undefined : { cause: options.cause });
114
- this.name = "RESTError";
115
- this.status = status;
116
- this.code = code;
117
- this.errors = errors;
118
- this.method = options.method;
119
- this.path = options.path;
120
- }
121
- }
122
603
  var LIBRARY_VERSION = package_default.version;
123
604
  var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
124
605
  var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
606
+ var REQUEST_DATA_KEYS = new Set([
607
+ "body",
608
+ "files",
609
+ "query",
610
+ "headers",
611
+ "reason",
612
+ "auth",
613
+ "signal"
614
+ ]);
615
+ function isRequestData(value) {
616
+ if (value === null || typeof value !== "object")
617
+ return false;
618
+ if (Array.isArray(value))
619
+ return false;
620
+ if (typeof FormData !== "undefined" && value instanceof FormData)
621
+ return false;
622
+ if (value instanceof Blob || value instanceof Uint8Array || value instanceof ArrayBuffer)
623
+ return false;
624
+ const keys = Object.keys(value);
625
+ if (keys.length === 0)
626
+ return false;
627
+ return keys.every((key) => REQUEST_DATA_KEYS.has(key));
628
+ }
125
629
  function createRetryPolicy(maxRetries = 2) {
126
630
  if (!Number.isFinite(maxRetries) || maxRetries < 0)
127
631
  throw new TypeError("maxRetries must be a non-negative finite number.");
@@ -129,7 +633,10 @@ function createRetryPolicy(maxRetries = 2) {
129
633
  maxRetries: Math.floor(maxRetries),
130
634
  shouldRetry(method, status) {
131
635
  const normalized = method.toUpperCase();
132
- 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;
133
640
  },
134
641
  getDelay(attempt, retryAfter) {
135
642
  if (retryAfter !== undefined)
@@ -140,18 +647,35 @@ function createRetryPolicy(maxRetries = 2) {
140
647
  }
141
648
 
142
649
  class REST {
143
- #baseURL;
144
- #timeout;
145
650
  #retryPolicy;
146
651
  #token;
147
- #globalResetAt = 0;
148
- #buckets = new Map;
149
- #routeBuckets = new Map;
652
+ #limiter;
653
+ #scheduler = new RequestScheduler;
654
+ #transport;
655
+ #decoder = new ResponseDecoder;
656
+ #hooks = {};
657
+ #concurrentBuckets;
150
658
  constructor(options = {}) {
151
659
  this.#token = options.token;
152
- this.#timeout = Math.max(1, options.timeout ?? 15000);
153
660
  this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
154
- this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
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
+ });
670
+ if (options.hooks)
671
+ this.#hooks = options.hooks;
672
+ }
673
+ get store() {
674
+ return this.#limiter.store;
675
+ }
676
+ setHooks(hooks) {
677
+ this.#hooks = hooks;
678
+ return this;
155
679
  }
156
680
  setToken(token) {
157
681
  if (!token.trim())
@@ -159,99 +683,129 @@ class REST {
159
683
  this.#token = token;
160
684
  }
161
685
  async request(method, path, body, options = {}) {
162
- const normalizedMethod = method.toUpperCase();
163
- if (!normalizedMethod)
686
+ if (!method.toUpperCase())
164
687
  throw new TypeError("REST method is required.");
165
688
  if (!path.startsWith("/"))
166
689
  throw new TypeError("REST paths must start with '/'.");
167
- const route = `${normalizedMethod}:${this.#normalizeRoute(path)}`;
168
- let bucketKey = this.#routeBuckets.get(route) ?? route;
169
- let bucket = this.#buckets.get(bucketKey) ?? {
170
- remaining: 1,
171
- resetAt: 0,
172
- queue: Promise.resolve()
173
- };
174
- this.#buckets.set(bucketKey, bucket);
175
- const previous = bucket.queue;
176
- let release;
177
- bucket.queue = new Promise((resolve) => {
178
- release = resolve;
179
- });
180
- await this.#abortable(previous, options.signal, path);
181
- try {
182
- for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
183
- await this.#wait(bucket, options.signal, path);
184
- await this.#waitGlobal(options.signal, path);
185
- const controller = new AbortController;
186
- const onAbort = () => controller.abort(options.signal?.reason);
690
+ const route = createRouteKey(method, path);
691
+ const files = options.files;
692
+ if (files && files.length > 0 && !(body instanceof FormData))
693
+ body = this.#fileForm(body, files);
694
+ const query = serializeQuery(options.query);
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
718
+ });
719
+ } catch (error) {
720
+ if (error instanceof RESTError)
721
+ throw error;
187
722
  if (options.signal?.aborted)
188
- throw this.#abortError(path, options.signal.reason);
189
- options.signal?.addEventListener("abort", onAbort, { once: true });
190
- const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
191
- const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
192
- const headers = {
193
- ...this.#token ? { Authorization: `Bot ${this.#token}` } : {},
194
- "User-Agent": USER_AGENT
195
- };
196
- if (!isFormData)
197
- headers["Content-Type"] = "application/json";
198
- const fetchBody = isFormData ? body : body === undefined ? undefined : JSON.stringify(body);
199
- try {
200
- const response = await fetch(`${this.#baseURL}${path}`, {
201
- method: normalizedMethod,
202
- headers,
203
- body: fetchBody,
204
- signal: controller.signal
205
- });
206
- bucket = this.#update(response, bucket, route, bucketKey);
207
- bucketKey = this.#routeBuckets.get(route) ?? bucketKey;
208
- const payload = await this.#readPayload(response);
209
- if (response.ok)
210
- return response.status === 204 ? undefined : payload;
211
- const data = this.#errorData(payload);
212
- const retryAfter = response.status === 429 ? this.#retryAfter(response, data) : undefined;
213
- if (data.global && retryAfter !== undefined)
214
- this.#globalResetAt = Date.now() + retryAfter * 1000;
215
- if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, response.status)) {
216
- await this.#sleep(this.#retryPolicy.getDelay(attempt, retryAfter), options.signal, path);
217
- continue;
218
- }
219
- throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method: normalizedMethod, path });
220
- } catch (error) {
221
- if (error instanceof RESTError)
222
- throw error;
223
- if (options.signal?.aborted)
224
- throw this.#abortError(path, options.signal.reason);
225
- const isTimeout = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
226
- if (!isTimeout && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, 0)) {
227
- await this.#sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
228
- continue;
229
- }
230
- 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 });
231
- } finally {
232
- clearTimeout(timer);
233
- options.signal?.removeEventListener("abort", onAbort);
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;
234
728
  }
729
+ throw new RESTError(failure.message, 0, undefined, undefined, {
730
+ method,
731
+ path,
732
+ cause: failure.cause ?? failure
733
+ });
235
734
  }
236
- throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method: normalizedMethod, path });
237
- } finally {
238
- release();
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 });
239
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);
789
+ }
790
+ #dispatch(method, path, bodyOrOptions, options) {
791
+ if (options === undefined && isRequestData(bodyOrOptions))
792
+ return this.request(method, path, bodyOrOptions.body, bodyOrOptions);
793
+ return this.request(method, path, bodyOrOptions, options);
240
794
  }
241
795
  get(path, options) {
242
- return this.request("GET", path, undefined, options);
796
+ return this.request("GET", path, options?.body, options);
243
797
  }
244
798
  post(path, body, options) {
245
- return this.request("POST", path, body, options);
799
+ return this.#dispatch("POST", path, body, options);
246
800
  }
247
801
  patch(path, body, options) {
248
- return this.request("PATCH", path, body, options);
802
+ return this.#dispatch("PATCH", path, body, options);
249
803
  }
250
804
  put(path, body, options) {
251
- return this.request("PUT", path, body, options);
805
+ return this.#dispatch("PUT", path, body, options);
252
806
  }
253
807
  delete(path, options) {
254
- return this.request("DELETE", path, undefined, options);
808
+ return this.request("DELETE", path, options?.body, options);
255
809
  }
256
810
  #fileForm(payload, files) {
257
811
  const form = new FormData;
@@ -271,114 +825,32 @@ class REST {
271
825
  patchWithFiles(path, payload, files, options) {
272
826
  return this.request("PATCH", path, this.#fileForm(payload, files), options);
273
827
  }
274
- async#wait(bucket, signal, path) {
275
- const delay = bucket.resetAt - Date.now();
276
- if (bucket.remaining <= 0 && delay > 0)
277
- await this.#sleep(delay, signal, path);
278
- }
279
- async#waitGlobal(signal, path) {
280
- const delay = this.#globalResetAt - Date.now();
281
- if (delay > 0)
282
- await this.#sleep(delay, signal, path);
283
- }
284
- async#sleep(delay, signal, path) {
285
- if (delay <= 0)
286
- return;
287
- await new Promise((resolve, reject) => {
288
- const timer = setTimeout(resolve, delay);
289
- const abort = () => {
290
- clearTimeout(timer);
291
- reject(this.#abortError(path, signal?.reason));
292
- };
293
- if (signal?.aborted)
294
- return abort();
295
- signal?.addEventListener("abort", abort, { once: true });
296
- });
297
- }
298
- async#abortable(promise, signal, path) {
299
- if (!signal)
300
- return promise;
301
- if (signal.aborted)
302
- throw this.#abortError(path, signal.reason);
303
- return Promise.race([
304
- promise,
305
- new Promise((_, reject) => signal.addEventListener("abort", () => reject(this.#abortError(path, signal.reason)), { once: true }))
306
- ]);
307
- }
308
- #abortError(path, reason) {
309
- return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason });
310
- }
311
- async#readPayload(response) {
312
- if (response.status === 204)
828
+ #emit(hook, ctx) {
829
+ if (!hook)
313
830
  return;
314
- const contentType = response.headers.get("content-type") ?? "";
315
- if (contentType.includes("application/json"))
316
- return response.json().catch(() => {
317
- return;
318
- });
319
- return response.text().catch(() => {
320
- return;
321
- });
322
- }
323
- #errorData(payload) {
324
- if (!payload || typeof payload !== "object")
325
- return {};
326
- const data = payload;
327
- return {
328
- message: typeof data.message === "string" ? data.message : undefined,
329
- code: typeof data.code === "number" ? data.code : undefined,
330
- retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
331
- global: data.global === true
332
- };
333
- }
334
- #retryAfter(response, data) {
335
- const header = Number(response.headers.get("Retry-After"));
336
- const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
337
- return Math.max(0, retryAfter);
338
- }
339
- #normalizeRoute(path) {
340
- const queryIndex = path.indexOf("?");
341
- const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
342
- return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
343
- }
344
- #update(response, bucket, route, bucketKey) {
345
- const serverBucket = response.headers.get("X-RateLimit-Bucket");
346
- if (serverBucket) {
347
- this.#routeBuckets.set(route, serverBucket);
348
- if (serverBucket !== bucketKey) {
349
- const shared = this.#buckets.get(serverBucket) ?? bucket;
350
- this.#buckets.set(serverBucket, shared);
351
- bucket = shared;
352
- }
353
- }
354
- const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
355
- const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
356
- const reset = Number(response.headers.get("X-RateLimit-Reset"));
357
- if (Number.isFinite(remaining))
358
- bucket.remaining = Math.max(0, remaining);
359
- if (Number.isFinite(resetAfter))
360
- bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
361
- else if (Number.isFinite(reset))
362
- bucket.resetAt = reset * 1000;
363
- if (response.status === 429) {
364
- const retryAfter = Number(response.headers.get("Retry-After"));
365
- if (Number.isFinite(retryAfter))
366
- bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
367
- }
368
- if (response.headers.get("X-RateLimit-Global") === "true") {
369
- const retryAfter = Number(response.headers.get("Retry-After"));
370
- if (Number.isFinite(retryAfter))
371
- this.#globalResetAt = Math.max(this.#globalResetAt, Date.now() + retryAfter * 1000);
372
- }
373
- return bucket;
831
+ Promise.resolve().then(() => hook(ctx)).catch(() => {});
374
832
  }
375
833
  }
376
834
  export {
835
+ RESTError as DiscordAPIError,
836
+ HttpTransport,
837
+ MemoryRateLimitStore,
377
838
  REST,
378
839
  RESTError,
840
+ RateLimiter,
841
+ RedisRateLimitStore,
842
+ RequestScheduler,
843
+ ResponseDecoder,
379
844
  Routes,
845
+ TransportError,
380
846
  WebhookClient,
381
- createRetryPolicy
847
+ abortError,
848
+ createRetryPolicy,
849
+ createRouteKey,
850
+ majorParameter,
851
+ normalizeRoutePath,
852
+ scopeBucket,
853
+ serializeQuery
382
854
  };
383
855
 
384
- //# debugId=429E85E3F97473C964756E2164756E21
856
+ //# debugId=CCF20CDF4862826C64756E2164756E21