lunibee 0.1.0 → 0.1.2

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 (67) hide show
  1. package/README.md +229 -0
  2. package/dist/builders/attachment.d.ts +20 -0
  3. package/dist/builders/commands.d.ts +118 -0
  4. package/dist/builders/components.d.ts +151 -0
  5. package/dist/builders/embed.d.ts +59 -0
  6. package/dist/builders/index.d.ts +5 -0
  7. package/dist/builders/index.js +828 -0
  8. package/dist/builders/index.js.map +13 -0
  9. package/dist/collection/cache.d.ts +37 -0
  10. package/dist/collection/index.d.ts +47 -0
  11. package/dist/collection/index.js +214 -0
  12. package/dist/collection/index.js.map +11 -0
  13. package/dist/core/client-state.d.ts +10 -0
  14. package/dist/core/collector.d.ts +27 -0
  15. package/dist/core/events.d.ts +5 -0
  16. package/dist/core/index.d.ts +111 -0
  17. package/dist/core/index.js +3188 -0
  18. package/dist/core/index.js.map +29 -0
  19. package/dist/core/permissions.d.ts +293 -0
  20. package/dist/formatters/index.d.ts +10 -0
  21. package/dist/formatters/index.js +43 -0
  22. package/dist/formatters/index.js.map +10 -0
  23. package/dist/handlers/index.d.ts +16 -0
  24. package/dist/handlers/index.js +37 -0
  25. package/dist/handlers/index.js.map +10 -0
  26. package/{src/index.ts → dist/index.d.ts} +2 -29
  27. package/dist/index.js +4182 -0
  28. package/dist/index.js.map +41 -0
  29. package/dist/managers/application.d.ts +28 -0
  30. package/dist/managers/base.d.ts +28 -0
  31. package/dist/managers/index.d.ts +96 -0
  32. package/dist/managers/index.js +947 -0
  33. package/dist/managers/index.js.map +22 -0
  34. package/dist/managers/member.d.ts +35 -0
  35. package/dist/managers/message.d.ts +25 -0
  36. package/dist/managers/role.d.ts +34 -0
  37. package/dist/managers/thread.d.ts +14 -0
  38. package/dist/rest/index.d.ts +49 -0
  39. package/dist/rest/index.js +344 -0
  40. package/dist/rest/index.js.map +12 -0
  41. package/dist/rest/routes.d.ts +44 -0
  42. package/dist/rest/types.d.ts +26 -0
  43. package/dist/rest/webhook.d.ts +40 -0
  44. package/dist/sharding/bus.d.ts +22 -0
  45. package/dist/sharding/index.d.ts +34 -0
  46. package/dist/sharding/index.js +732 -0
  47. package/dist/sharding/index.js.map +13 -0
  48. package/dist/structures/base.d.ts +75 -0
  49. package/dist/structures/index.d.ts +39 -0
  50. package/dist/structures/index.js +571 -0
  51. package/dist/structures/index.js.map +14 -0
  52. package/dist/structures/interactions.d.ts +136 -0
  53. package/dist/structures/permissions.d.ts +73 -0
  54. package/dist/structures/resources.d.ts +64 -0
  55. package/dist/types/index.d.ts +535 -0
  56. package/dist/types/index.js +132 -0
  57. package/dist/types/index.js.map +10 -0
  58. package/dist/utils/index.d.ts +6 -0
  59. package/dist/utils/index.js +18 -0
  60. package/dist/utils/index.js.map +10 -0
  61. package/dist/voice/index.d.ts +97 -0
  62. package/dist/voice/index.js +161 -0
  63. package/dist/voice/index.js.map +10 -0
  64. package/dist/ws/index.d.ts +76 -0
  65. package/dist/ws/index.js +582 -0
  66. package/dist/ws/index.js.map +11 -0
  67. package/package.json +113 -28
@@ -0,0 +1,732 @@
1
+ // @bun
2
+ // packages/types/src/index.ts
3
+ var GatewayIntentBits = {
4
+ Guilds: 1 << 0,
5
+ GuildMembers: 1 << 1,
6
+ GuildModeration: 1 << 2,
7
+ GuildExpressions: 1 << 3,
8
+ GuildIntegrations: 1 << 4,
9
+ GuildWebhooks: 1 << 5,
10
+ GuildInvites: 1 << 6,
11
+ GuildVoiceStates: 1 << 7,
12
+ GuildPresences: 1 << 8,
13
+ GuildMessages: 1 << 9,
14
+ GuildMessageReactions: 1 << 10,
15
+ GuildMessageTyping: 1 << 11,
16
+ DirectMessages: 1 << 12,
17
+ DirectMessageReactions: 1 << 13,
18
+ DirectMessageTyping: 1 << 14,
19
+ MessageContent: 1 << 15,
20
+ GuildScheduledEvents: 1 << 16,
21
+ AutoModerationConfiguration: 1 << 20,
22
+ AutoModerationExecution: 1 << 21,
23
+ GuildMessagePolls: 1 << 24,
24
+ DirectMessagePolls: 1 << 25
25
+ };
26
+ var IntentBits = {
27
+ guild: 1 << 0,
28
+ guilds: 1 << 0,
29
+ guildMembers: 1 << 1,
30
+ guildModeration: 1 << 2,
31
+ guildBans: 1 << 2,
32
+ guildExpressions: 1 << 3,
33
+ guildEmojis: 1 << 3,
34
+ guildEmojisAndStickers: 1 << 3,
35
+ guildIntegrations: 1 << 4,
36
+ guildWebhooks: 1 << 5,
37
+ guildInvites: 1 << 6,
38
+ guildVoiceStates: 1 << 7,
39
+ guildPresences: 1 << 8,
40
+ guildMessages: 1 << 9,
41
+ guildMessage: 1 << 9,
42
+ guildMessageReactions: 1 << 10,
43
+ guildMessageTyping: 1 << 11,
44
+ directMessages: 1 << 12,
45
+ directMessageReactions: 1 << 13,
46
+ directMessageTyping: 1 << 14,
47
+ messageContent: 1 << 15,
48
+ guildScheduledEvents: 1 << 16,
49
+ autoModerationConfiguration: 1 << 20,
50
+ autoModerationExecution: 1 << 21,
51
+ guildMessagePolls: 1 << 24,
52
+ directMessagePolls: 1 << 25
53
+ };
54
+ function resolveGatewayIntents(intents) {
55
+ if (typeof intents === "number")
56
+ return intents;
57
+ if (Array.isArray(intents)) {
58
+ return intents.reduce((acc, intent) => {
59
+ return acc | resolveGatewayIntents(intent);
60
+ }, 0);
61
+ }
62
+ if (typeof intents === "string") {
63
+ if (intents in GatewayIntentBits)
64
+ return GatewayIntentBits[intents];
65
+ if (intents in IntentBits)
66
+ return IntentBits[intents];
67
+ const lower = intents.charAt(0).toLowerCase() + intents.slice(1);
68
+ if (lower in IntentBits)
69
+ return IntentBits[lower];
70
+ const num = Number(intents);
71
+ if (!Number.isNaN(num))
72
+ return num;
73
+ }
74
+ return 0;
75
+ }
76
+
77
+ // packages/ws/src/index.ts
78
+ var GatewayOpcodes = {
79
+ Dispatch: 0,
80
+ Heartbeat: 1,
81
+ Identify: 2,
82
+ PresenceUpdate: 3,
83
+ VoiceStateUpdate: 4,
84
+ Resume: 6,
85
+ Reconnect: 7,
86
+ RequestGuildMembers: 8,
87
+ InvalidSession: 9,
88
+ Hello: 10,
89
+ HeartbeatAck: 11
90
+ };
91
+ class GatewayError extends Error {
92
+ code;
93
+ constructor(message, code, options) {
94
+ super(message, options);
95
+ this.name = "GatewayError";
96
+ this.code = code;
97
+ }
98
+ }
99
+
100
+ class Gateway {
101
+ state = "CONNECT" /* Connect */;
102
+ #options;
103
+ #ws;
104
+ #sequence = null;
105
+ #sessionId;
106
+ #resumeURL;
107
+ #heartbeatTimer;
108
+ #initialHeartbeat;
109
+ #heartbeatAckTimer;
110
+ #heartbeatACK = true;
111
+ #heartbeatInterval = 0;
112
+ #heartbeatSentAt = 0;
113
+ #lastMessageAt = 0;
114
+ #zombieTimer;
115
+ #zombieReported = false;
116
+ #closed = false;
117
+ #attempt = 0;
118
+ #reconnectTimer;
119
+ #connectPromise;
120
+ #resolveConnect;
121
+ #rejectConnect;
122
+ #listeners = new Map;
123
+ #sendTimestamps = [];
124
+ constructor(options) {
125
+ if (!options.token?.trim())
126
+ throw new TypeError("A Gateway token is required.");
127
+ const resolvedIntents = resolveGatewayIntents(options.intents);
128
+ if (!Number.isInteger(resolvedIntents) || resolvedIntents < 0)
129
+ throw new TypeError("Gateway intents must be a non-negative integer.");
130
+ this.#options = {
131
+ shardId: 0,
132
+ shardCount: 1,
133
+ reconnect: true,
134
+ maxReconnectAttempts: Infinity,
135
+ reconnectBaseDelay: 1000,
136
+ reconnectMaxDelay: 30000,
137
+ heartbeatAckTimeout: 1e4,
138
+ zombieTimeout: 30000,
139
+ properties: {
140
+ os: "Android",
141
+ browser: "Discord Android",
142
+ device: "Discord Android"
143
+ },
144
+ presence: { status: "online", activities: [], afk: false, since: null },
145
+ ...options
146
+ };
147
+ if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
148
+ throw new RangeError("Gateway shardId must be within the configured shard count.");
149
+ if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
150
+ throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
151
+ }
152
+ connect(url = "wss://gateway.discord.gg/?v=10&encoding=json") {
153
+ if (this.state === "CLOSED" /* Closed */)
154
+ throw new GatewayError("Gateway has been permanently closed.");
155
+ if (this.#connectPromise)
156
+ return this.#connectPromise;
157
+ this.#closed = false;
158
+ this.#setState("CONNECT" /* Connect */);
159
+ this.#connectPromise = new Promise((resolve, reject) => {
160
+ this.#resolveConnect = resolve;
161
+ this.#rejectConnect = reject;
162
+ this.#open(url);
163
+ });
164
+ return this.#connectPromise;
165
+ }
166
+ close() {
167
+ this.#closed = true;
168
+ this.#clearTimers();
169
+ this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
170
+ const ws = this.#ws;
171
+ this.#ws = undefined;
172
+ try {
173
+ ws?.close(1000, "Client closed connection");
174
+ } catch (error) {
175
+ this.#emitError(error);
176
+ }
177
+ this.#setState("CLOSED" /* Closed */);
178
+ }
179
+ emit(event, data) {
180
+ this.#emit(event, data);
181
+ }
182
+ on(event, listener) {
183
+ if (!event || typeof listener !== "function")
184
+ throw new TypeError("Gateway event and listener are required.");
185
+ let listeners = this.#listeners.get(event);
186
+ if (!listeners)
187
+ this.#listeners.set(event, listeners = new Set);
188
+ listeners.add(listener);
189
+ return this;
190
+ }
191
+ off(event, listener) {
192
+ this.#listeners.get(event)?.delete(listener);
193
+ return this;
194
+ }
195
+ send(payload) {
196
+ if (!payload || !Number.isInteger(payload.op))
197
+ throw new TypeError("Gateway payload must contain an integer opcode.");
198
+ if (this.#ws?.readyState !== WebSocket.OPEN)
199
+ return false;
200
+ const now = Date.now();
201
+ while (this.#sendTimestamps.length && now - this.#sendTimestamps[0] >= 60000)
202
+ this.#sendTimestamps.shift();
203
+ if (this.#sendTimestamps.length >= 115) {
204
+ this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
205
+ return false;
206
+ }
207
+ try {
208
+ this.#ws.send(JSON.stringify(payload));
209
+ this.#sendTimestamps.push(now);
210
+ return true;
211
+ } catch (error) {
212
+ this.#emitError(error);
213
+ return false;
214
+ }
215
+ }
216
+ setPresence(data) {
217
+ this.#options.presence = { ...this.#options.presence, ...data };
218
+ const payload = {
219
+ since: this.#options.presence.since ?? null,
220
+ activities: this.#options.presence.activities ?? [],
221
+ status: this.#options.presence.status ?? "online",
222
+ afk: Boolean(this.#options.presence.afk)
223
+ };
224
+ return this.send({
225
+ op: GatewayOpcodes.PresenceUpdate,
226
+ d: payload,
227
+ s: null,
228
+ t: null
229
+ });
230
+ }
231
+ setVoiceState(data) {
232
+ return this.send({
233
+ op: GatewayOpcodes.VoiceStateUpdate,
234
+ d: data,
235
+ s: null,
236
+ t: null
237
+ });
238
+ }
239
+ requestGuildMembers(data) {
240
+ return this.send({
241
+ op: GatewayOpcodes.RequestGuildMembers,
242
+ d: data,
243
+ s: null,
244
+ t: null
245
+ });
246
+ }
247
+ #open(url) {
248
+ if (this.#closed)
249
+ return;
250
+ let ws;
251
+ try {
252
+ ws = new WebSocket(url);
253
+ } catch (error) {
254
+ const failure = this.#normalizeError(error);
255
+ this.#emitError(failure);
256
+ if (this.#options.reconnect) {
257
+ this.#setState("RECONNECT" /* Reconnect */);
258
+ this.#scheduleReconnect();
259
+ } else
260
+ this.#settleConnect(failure);
261
+ return;
262
+ }
263
+ this.#ws = ws;
264
+ ws.addEventListener("open", () => {
265
+ this.#lastMessageAt = Date.now();
266
+ this.#zombieReported = false;
267
+ this.#startZombieDetection();
268
+ this.#emit("open", undefined);
269
+ this.#settleConnect();
270
+ });
271
+ ws.addEventListener("message", (event) => {
272
+ this.#lastMessageAt = Date.now();
273
+ this.#zombieReported = false;
274
+ this.#message(ws, String(event.data));
275
+ });
276
+ ws.addEventListener("close", (event) => this.#close(ws, event.code));
277
+ ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
278
+ }
279
+ #startZombieDetection() {
280
+ if (this.#zombieTimer)
281
+ clearInterval(this.#zombieTimer);
282
+ const interval = Math.max(1, Math.min(250, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
283
+ this.#zombieTimer = setInterval(() => {
284
+ if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
285
+ return;
286
+ const silentFor = Date.now() - this.#lastMessageAt;
287
+ const deadline = Math.max(this.#options.zombieTimeout, this.#heartbeatInterval + this.#options.heartbeatAckTimeout);
288
+ if (silentFor < deadline || this.#zombieReported)
289
+ return;
290
+ this.#zombieReported = true;
291
+ const error = new GatewayError(`Gateway connection appears stale after ${silentFor}ms without traffic.`);
292
+ this.#emit("zombie", { silentFor, timeout: deadline });
293
+ this.#emitError(error);
294
+ try {
295
+ this.#ws.close(1001, "Zombie Gateway connection");
296
+ } catch (closeError) {
297
+ this.#emitError(closeError);
298
+ }
299
+ }, interval);
300
+ }
301
+ #message(ws, raw) {
302
+ let payload;
303
+ try {
304
+ payload = JSON.parse(raw);
305
+ } catch (error) {
306
+ this.#emitError(new GatewayError("Gateway returned invalid JSON", undefined, {
307
+ cause: error
308
+ }));
309
+ ws.close(1002, "Invalid JSON");
310
+ return;
311
+ }
312
+ if (!payload || typeof payload.op !== "number") {
313
+ this.#emitError(new GatewayError("Gateway returned an invalid payload"));
314
+ ws.close(1002, "Invalid payload");
315
+ return;
316
+ }
317
+ if (typeof payload.s === "number")
318
+ this.#sequence = payload.s;
319
+ switch (payload.op) {
320
+ case GatewayOpcodes.Dispatch:
321
+ this.#setState("DISPATCH" /* Dispatch */);
322
+ this.#handleDispatch(payload.t, payload.d);
323
+ break;
324
+ case GatewayOpcodes.Hello:
325
+ this.#handleHello(payload.d);
326
+ break;
327
+ case GatewayOpcodes.Heartbeat:
328
+ this.#setState("HEARTBEAT" /* Heartbeat */);
329
+ this.#sendHeartbeat();
330
+ break;
331
+ case GatewayOpcodes.HeartbeatAck:
332
+ this.#heartbeatACK = true;
333
+ this.#clearHeartbeatAckTimer();
334
+ this.#emit("heartbeatAck", payload.d);
335
+ break;
336
+ case GatewayOpcodes.Reconnect:
337
+ ws.close(1001, "Server requested reconnect");
338
+ break;
339
+ case GatewayOpcodes.InvalidSession:
340
+ this.#sessionId = undefined;
341
+ this.#sequence = null;
342
+ this.#emit("invalidSession", payload.d);
343
+ ws.close(1000, "Invalid session");
344
+ break;
345
+ }
346
+ }
347
+ #handleDispatch(event, data) {
348
+ if (event === "READY") {
349
+ const ready = data;
350
+ if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string") {
351
+ this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
352
+ return;
353
+ }
354
+ this.#sessionId = ready.session_id;
355
+ this.#resumeURL = ready.resume_gateway_url;
356
+ this.#attempt = 0;
357
+ this.#setState("READY" /* Ready */);
358
+ this.#emit("ready", data);
359
+ }
360
+ this.#emit(event ?? "dispatch", data);
361
+ }
362
+ #handleHello(data) {
363
+ const interval = data?.heartbeat_interval;
364
+ if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0) {
365
+ this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
366
+ this.#ws?.close(1002, "Invalid heartbeat interval");
367
+ return;
368
+ }
369
+ this.#heartbeatInterval = interval;
370
+ this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
371
+ this.#startHeartbeat(interval);
372
+ this.#identifyOrResume();
373
+ }
374
+ #identifyOrResume() {
375
+ if (this.#sessionId && this.#sequence !== null && this.#resumeURL) {
376
+ this.#setState("RESUME" /* Resume */);
377
+ this.send({
378
+ op: GatewayOpcodes.Resume,
379
+ d: {
380
+ token: this.#options.token,
381
+ session_id: this.#sessionId,
382
+ seq: this.#sequence
383
+ },
384
+ s: null,
385
+ t: null
386
+ });
387
+ return;
388
+ }
389
+ this.#setState("IDENTIFY" /* Identify */);
390
+ const props = this.#options.properties ?? {
391
+ os: "Android",
392
+ browser: "Discord Android",
393
+ device: "Discord Android"
394
+ };
395
+ const os = props.os ?? "Android";
396
+ const browser = props.browser ?? "Discord Android";
397
+ const device = props.device ?? "Discord Android";
398
+ this.send({
399
+ op: GatewayOpcodes.Identify,
400
+ d: {
401
+ token: this.#options.token,
402
+ intents: this.#options.intents,
403
+ properties: {
404
+ os,
405
+ browser,
406
+ device,
407
+ $os: os,
408
+ $browser: browser,
409
+ $device: device,
410
+ ...props
411
+ },
412
+ presence: {
413
+ since: this.#options.presence?.since ?? null,
414
+ activities: this.#options.presence?.activities ?? [],
415
+ status: this.#options.presence?.status ?? "online",
416
+ afk: Boolean(this.#options.presence?.afk)
417
+ },
418
+ shard: [this.#options.shardId, this.#options.shardCount]
419
+ },
420
+ s: null,
421
+ t: null
422
+ });
423
+ }
424
+ #startHeartbeat(interval) {
425
+ this.#clearHeartbeatTimers();
426
+ this.#heartbeatACK = true;
427
+ this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
428
+ this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
429
+ }
430
+ #sendHeartbeat() {
431
+ this.#heartbeatACK = false;
432
+ this.#heartbeatSentAt = Date.now();
433
+ if (!this.send({
434
+ op: GatewayOpcodes.Heartbeat,
435
+ d: this.#sequence,
436
+ s: null,
437
+ t: null
438
+ })) {
439
+ this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
440
+ return;
441
+ }
442
+ this.#clearHeartbeatAckTimer();
443
+ this.#heartbeatAckTimer = setTimeout(() => {
444
+ if (!this.#heartbeatACK) {
445
+ const elapsed = Date.now() - this.#heartbeatSentAt;
446
+ this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
447
+ try {
448
+ this.#ws?.close(1001, "Heartbeat timeout");
449
+ } catch (error) {
450
+ this.#emitError(error);
451
+ }
452
+ }
453
+ }, this.#options.heartbeatAckTimeout);
454
+ }
455
+ #close(ws, code) {
456
+ if (this.#ws !== ws)
457
+ return;
458
+ this.#ws = undefined;
459
+ this.#clearTimers();
460
+ const action = this.#closeAction(code);
461
+ this.#emit("close", { code, action });
462
+ if (this.#closed || !this.#options.reconnect || action === "stop") {
463
+ this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
464
+ if (this.#connectPromise)
465
+ this.#settleConnect(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
466
+ return;
467
+ }
468
+ if (action === "identify") {
469
+ this.#sequence = null;
470
+ this.#sessionId = undefined;
471
+ }
472
+ this.#setState("RECONNECT" /* Reconnect */);
473
+ this.#scheduleReconnect();
474
+ }
475
+ #closeAction(code) {
476
+ if ([4004, 4010, 4011, 4012, 4013, 4014].includes(code))
477
+ return "stop";
478
+ if ([4007, 4009].includes(code))
479
+ return "identify";
480
+ return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
481
+ }
482
+ #scheduleReconnect() {
483
+ if (this.#closed || !this.#options.reconnect || this.#reconnectTimer)
484
+ return;
485
+ if (this.#attempt >= this.#options.maxReconnectAttempts) {
486
+ this.#settleConnect(new GatewayError("Gateway reconnect attempts exhausted."));
487
+ this.#setState("CONNECT" /* Connect */);
488
+ return;
489
+ }
490
+ const delay = Math.min(this.#options.reconnectMaxDelay, this.#options.reconnectBaseDelay * 2 ** this.#attempt++);
491
+ const jitter = Math.random() * Math.min(250, Math.max(1, delay * 0.25));
492
+ this.#reconnectTimer = setTimeout(() => {
493
+ this.#reconnectTimer = undefined;
494
+ this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
495
+ }, delay + jitter);
496
+ }
497
+ #settleConnect(error) {
498
+ const resolve = this.#resolveConnect;
499
+ const reject = this.#rejectConnect;
500
+ this.#resolveConnect = undefined;
501
+ this.#rejectConnect = undefined;
502
+ this.#connectPromise = undefined;
503
+ if (error)
504
+ reject?.(error);
505
+ else
506
+ resolve?.();
507
+ }
508
+ #clearHeartbeatAckTimer() {
509
+ if (this.#heartbeatAckTimer)
510
+ clearTimeout(this.#heartbeatAckTimer);
511
+ this.#heartbeatAckTimer = undefined;
512
+ }
513
+ #clearHeartbeatTimers() {
514
+ if (this.#heartbeatTimer)
515
+ clearInterval(this.#heartbeatTimer);
516
+ if (this.#initialHeartbeat)
517
+ clearTimeout(this.#initialHeartbeat);
518
+ this.#clearHeartbeatAckTimer();
519
+ this.#heartbeatTimer = undefined;
520
+ this.#initialHeartbeat = undefined;
521
+ }
522
+ #clearTimers() {
523
+ if (this.#reconnectTimer)
524
+ clearTimeout(this.#reconnectTimer);
525
+ if (this.#zombieTimer)
526
+ clearInterval(this.#zombieTimer);
527
+ this.#reconnectTimer = undefined;
528
+ this.#zombieTimer = undefined;
529
+ this.#clearHeartbeatTimers();
530
+ }
531
+ #normalizeError(error) {
532
+ return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
533
+ cause: error
534
+ });
535
+ }
536
+ #emitError(error) {
537
+ this.#emit("error", error instanceof Error ? error : new GatewayError(String(error)));
538
+ }
539
+ #emit(event, data) {
540
+ for (const listener of this.#listeners.get(event) ?? []) {
541
+ try {
542
+ const result = listener(data);
543
+ if (result && typeof result.then === "function")
544
+ Promise.resolve(result).catch((error) => {
545
+ if (event !== "error")
546
+ this.#emitError(error);
547
+ });
548
+ } catch (error) {
549
+ if (event !== "error")
550
+ this.#emitError(error);
551
+ }
552
+ }
553
+ }
554
+ #setState(next) {
555
+ if (this.state === next)
556
+ return;
557
+ const previous = this.state;
558
+ this.state = next;
559
+ this.#emit("stateChange", { previous, next });
560
+ }
561
+ }
562
+
563
+ // packages/sharding/src/bus.ts
564
+ class ShardBus {
565
+ #channel;
566
+ #handlers = new Map;
567
+ #shardId;
568
+ #namespace;
569
+ channelName;
570
+ #counter = 0;
571
+ constructor(shardId, channelName) {
572
+ if (!Number.isInteger(shardId) || shardId < 0)
573
+ throw new RangeError("Shard ID must be a non-negative integer.");
574
+ if (!channelName?.trim())
575
+ throw new TypeError("An application-specific shard channel name is required.");
576
+ this.#shardId = shardId;
577
+ this.#namespace = channelName.trim();
578
+ this.channelName = this.#namespace;
579
+ this.#channel = new BroadcastChannel(this.channelName);
580
+ this.#channel.addEventListener("message", (event) => this.#dispatch(event.data));
581
+ }
582
+ on(type, handler) {
583
+ if (!type.trim() || typeof handler !== "function")
584
+ throw new TypeError("Shard message type and handler are required.");
585
+ let handlers = this.#handlers.get(type);
586
+ if (!handlers)
587
+ this.#handlers.set(type, handlers = new Set);
588
+ handlers.add(handler);
589
+ return this;
590
+ }
591
+ off(type, handler) {
592
+ this.#handlers.get(type)?.delete(handler);
593
+ return this;
594
+ }
595
+ send(target, type, data) {
596
+ if (!Number.isInteger(target) || target < 0)
597
+ throw new RangeError("Shard target must be a non-negative integer.");
598
+ return this.#publish(target, type, data);
599
+ }
600
+ broadcast(type, data) {
601
+ return this.#publish(null, type, data);
602
+ }
603
+ close() {
604
+ this.#channel.close();
605
+ this.#handlers.clear();
606
+ }
607
+ #publish(target, type, data) {
608
+ if (!type.trim())
609
+ throw new TypeError("Shard message type is required.");
610
+ const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
611
+ this.#channel.postMessage({
612
+ source: this.#shardId,
613
+ target,
614
+ type,
615
+ data,
616
+ id
617
+ });
618
+ return id;
619
+ }
620
+ #dispatch(message) {
621
+ if (!message || typeof message !== "object" || typeof message.type !== "string")
622
+ return;
623
+ if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
624
+ return;
625
+ for (const handler of this.#handlers.get(message.type) ?? []) {
626
+ try {
627
+ const result = handler(message);
628
+ if (result && typeof result.then === "function")
629
+ Promise.resolve(result).catch(() => {
630
+ return;
631
+ });
632
+ } catch {}
633
+ }
634
+ }
635
+ }
636
+
637
+ // packages/sharding/src/index.ts
638
+ class ShardManager {
639
+ shards = new Map;
640
+ get shardCount() {
641
+ return this.shards.size;
642
+ }
643
+ #options;
644
+ #resolved = false;
645
+ #destroyed = false;
646
+ constructor(options) {
647
+ if (!options.token?.trim())
648
+ throw new TypeError("A Discord bot token is required.");
649
+ if (!Number.isInteger(options.intents) || options.intents < 0)
650
+ throw new TypeError("Gateway intents must be a non-negative integer.");
651
+ const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
652
+ if (!Number.isInteger(count) || count < 1)
653
+ throw new RangeError("Shard count must be a positive integer.");
654
+ this.#options = { ...options };
655
+ if (options.shardCount !== "auto")
656
+ this.#initialize(count);
657
+ }
658
+ async fetchRecommendedShardCount() {
659
+ const response = await fetch("https://discord.com/api/v10/gateway/bot", {
660
+ headers: {
661
+ Authorization: `Bot ${this.#options.token}`,
662
+ "User-Agent": "Lunibee/0.1.0"
663
+ }
664
+ });
665
+ if (!response.ok)
666
+ throw new Error(`Gateway discovery failed with status ${response.status}`);
667
+ const data = await response.json();
668
+ if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
669
+ throw new Error("Gateway discovery returned an invalid shard count.");
670
+ return data.shards;
671
+ }
672
+ async connect() {
673
+ await this.#ensureInitialized();
674
+ for (const [id, shard] of this.shards) {
675
+ try {
676
+ await shard.connect();
677
+ } catch (error) {
678
+ this.destroy();
679
+ throw new Error(`Failed to connect shard ${id}.`, { cause: error });
680
+ }
681
+ if (id + 1 < this.shardCount && this.#options.spawnDelay && this.#options.spawnDelay > 0)
682
+ await Bun.sleep(this.#options.spawnDelay);
683
+ }
684
+ }
685
+ destroy() {
686
+ for (const shard of this.shards.values())
687
+ shard.close();
688
+ this.shards.clear();
689
+ this.#resolved = false;
690
+ this.#destroyed = true;
691
+ }
692
+ getShardIdForGuild(guildId) {
693
+ const id = BigInt(guildId);
694
+ return Number((id >> 22n) % BigInt(this.shardCount || 1));
695
+ }
696
+ get(id) {
697
+ return this.shards.get(id);
698
+ }
699
+ values() {
700
+ return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
701
+ }
702
+ async#ensureInitialized() {
703
+ if (this.#resolved)
704
+ return;
705
+ if (this.#destroyed)
706
+ this.#destroyed = false;
707
+ const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#options.shardCount ?? 1;
708
+ this.#initialize(count);
709
+ }
710
+ #initialize(count) {
711
+ if (this.#resolved)
712
+ return;
713
+ for (let id = 0;id < count; id++)
714
+ this.shards.set(id, this.#createShard(id, count));
715
+ this.#resolved = true;
716
+ }
717
+ #createShard(id, count) {
718
+ return new Gateway({
719
+ token: this.#options.token,
720
+ intents: this.#options.intents,
721
+ shardId: id,
722
+ shardCount: count,
723
+ reconnect: this.#options.reconnect ?? true
724
+ });
725
+ }
726
+ }
727
+ export {
728
+ ShardBus,
729
+ ShardManager
730
+ };
731
+
732
+ //# debugId=3D6EA7786F9CDC6964756E2164756E21