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
@@ -1,5 +1,702 @@
1
1
  // @bun
2
- // packages/types/src/index.ts
2
+ // packages/ws/src/session.ts
3
+ class GatewaySession {
4
+ #state = { status: "none" };
5
+ #sequence = null;
6
+ #generation = 0;
7
+ #lastInvalidation;
8
+ get lastInvalidation() {
9
+ return this.#lastInvalidation;
10
+ }
11
+ get generation() {
12
+ return this.#generation;
13
+ }
14
+ get sessionId() {
15
+ return this.#state.status === "established" ? this.#state.sessionId : undefined;
16
+ }
17
+ get resumeURL() {
18
+ return this.#state.status === "established" ? this.#state.resumeURL : undefined;
19
+ }
20
+ get sequence() {
21
+ return this.#sequence;
22
+ }
23
+ get canResume() {
24
+ return this.#state.status === "established" && this.#sequence !== null;
25
+ }
26
+ beginConnection() {
27
+ return ++this.#generation;
28
+ }
29
+ owns(token) {
30
+ return token === this.#generation;
31
+ }
32
+ recordSequence(sequence, token) {
33
+ if (!this.owns(token))
34
+ return false;
35
+ if (!Number.isInteger(sequence))
36
+ return false;
37
+ this.#sequence = sequence;
38
+ return true;
39
+ }
40
+ activate(payload, token) {
41
+ if (!this.owns(token))
42
+ return false;
43
+ const ready = payload;
44
+ if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string")
45
+ return false;
46
+ this.#state = {
47
+ status: "established",
48
+ sessionId: ready.session_id,
49
+ resumeURL: ready.resume_gateway_url
50
+ };
51
+ return true;
52
+ }
53
+ invalidate(token, reason = "explicit") {
54
+ if (!this.owns(token))
55
+ return false;
56
+ this.#lastInvalidation = reason;
57
+ this.#state = { status: "none" };
58
+ this.#sequence = null;
59
+ return true;
60
+ }
61
+ resumeInfo() {
62
+ if (this.#state.status !== "established")
63
+ return;
64
+ if (this.#sequence === null)
65
+ return;
66
+ const { sessionId, resumeURL } = this.#state;
67
+ return { sessionId, sequence: this.#sequence, resumeURL };
68
+ }
69
+ handshake() {
70
+ const resume = this.resumeInfo();
71
+ return resume ? { type: "resume", ...resume } : { type: "identify" };
72
+ }
73
+ connectURL(fallback) {
74
+ return this.resumeInfo()?.resumeURL ?? fallback;
75
+ }
76
+ }
77
+
78
+ // packages/ws/src/heartbeat.ts
79
+ var MAX_MONITOR_INTERVAL = 1000;
80
+
81
+ class GatewayHeartbeat {
82
+ #options;
83
+ #now;
84
+ #interval = 0;
85
+ #acknowledged = true;
86
+ #sentAt = 0;
87
+ #latency = -1;
88
+ #lastMessageAt = 0;
89
+ #zombieReported = false;
90
+ #timer;
91
+ #initialTimer;
92
+ #ackTimer;
93
+ #monitorTimer;
94
+ constructor(options) {
95
+ if (options.zombieTimeout <= options.ackTimeout)
96
+ throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
97
+ this.#options = options;
98
+ this.#now = options.now ?? (() => Date.now());
99
+ }
100
+ get latency() {
101
+ return this.#latency;
102
+ }
103
+ get interval() {
104
+ return this.#interval;
105
+ }
106
+ get acknowledged() {
107
+ return this.#acknowledged;
108
+ }
109
+ get silentFor() {
110
+ return this.#lastMessageAt === 0 ? 0 : this.#now() - this.#lastMessageAt;
111
+ }
112
+ get stalenessDeadline() {
113
+ return Math.max(this.#options.zombieTimeout, this.#interval + this.#options.ackTimeout);
114
+ }
115
+ isHealthy() {
116
+ if (!this.#acknowledged)
117
+ return false;
118
+ if (this.#lastMessageAt === 0)
119
+ return true;
120
+ return this.silentFor < this.stalenessDeadline;
121
+ }
122
+ watch() {
123
+ this.#lastMessageAt = this.#now();
124
+ this.#zombieReported = false;
125
+ this.#startMonitor();
126
+ }
127
+ receivedMessage() {
128
+ this.#lastMessageAt = this.#now();
129
+ this.#zombieReported = false;
130
+ }
131
+ start(interval) {
132
+ this.#clearBeatTimers();
133
+ this.#interval = interval;
134
+ this.#acknowledged = true;
135
+ this.#initialTimer = setTimeout(() => this.sendHeartbeat(), Math.random() * interval);
136
+ this.#timer = setInterval(() => this.sendHeartbeat(), interval);
137
+ }
138
+ stop() {
139
+ this.#clearBeatTimers();
140
+ if (this.#monitorTimer)
141
+ clearInterval(this.#monitorTimer);
142
+ this.#monitorTimer = undefined;
143
+ }
144
+ sendHeartbeat() {
145
+ this.#acknowledged = false;
146
+ this.#sentAt = this.#now();
147
+ if (!this.#options.send(this.#options.sequence())) {
148
+ this.#options.onError(new Error("Unable to send Gateway heartbeat because the WebSocket is not open."));
149
+ return;
150
+ }
151
+ this.#clearAckTimer();
152
+ this.#ackTimer = setTimeout(() => {
153
+ if (this.#acknowledged)
154
+ return;
155
+ this.#options.onTimeout({
156
+ type: "ack",
157
+ elapsedMs: this.#now() - this.#sentAt
158
+ });
159
+ }, this.#options.ackTimeout);
160
+ }
161
+ acknowledge() {
162
+ this.#acknowledged = true;
163
+ this.#latency = this.#now() - this.#sentAt;
164
+ this.#clearAckTimer();
165
+ }
166
+ #startMonitor() {
167
+ if (this.#monitorTimer)
168
+ clearInterval(this.#monitorTimer);
169
+ const interval = Math.max(1, Math.min(MAX_MONITOR_INTERVAL, this.#options.ackTimeout, this.#options.zombieTimeout / 2));
170
+ this.#monitorTimer = setInterval(() => this.#checkStaleness(), interval);
171
+ }
172
+ #checkStaleness() {
173
+ if (!this.#options.isConnected() || this.#lastMessageAt === 0)
174
+ return;
175
+ const silentFor = this.silentFor;
176
+ const deadline = this.stalenessDeadline;
177
+ if (silentFor < deadline || this.#zombieReported)
178
+ return;
179
+ this.#zombieReported = true;
180
+ this.#options.onTimeout({ type: "zombie", silentFor, deadline });
181
+ }
182
+ #clearAckTimer() {
183
+ if (this.#ackTimer)
184
+ clearTimeout(this.#ackTimer);
185
+ this.#ackTimer = undefined;
186
+ }
187
+ #clearBeatTimers() {
188
+ if (this.#timer)
189
+ clearInterval(this.#timer);
190
+ if (this.#initialTimer)
191
+ clearTimeout(this.#initialTimer);
192
+ this.#clearAckTimer();
193
+ this.#timer = undefined;
194
+ this.#initialTimer = undefined;
195
+ }
196
+ }
197
+
198
+ // packages/ws/src/close-codes.ts
199
+ var GatewayCloseCodes = {
200
+ UnknownError: 4000,
201
+ UnknownOpcode: 4001,
202
+ DecodeError: 4002,
203
+ NotAuthenticated: 4003,
204
+ AuthenticationFailed: 4004,
205
+ AlreadyAuthenticated: 4005,
206
+ InvalidSeq: 4007,
207
+ RateLimited: 4008,
208
+ SessionTimedOut: 4009,
209
+ InvalidShard: 4010,
210
+ ShardingRequired: 4011,
211
+ InvalidAPIVersion: 4012,
212
+ InvalidIntents: 4013,
213
+ DisallowedIntents: 4014
214
+ };
215
+
216
+ // packages/ws/src/reconnect.ts
217
+ var FATAL_CLOSE_CODES = [
218
+ GatewayCloseCodes.AuthenticationFailed,
219
+ GatewayCloseCodes.InvalidShard,
220
+ GatewayCloseCodes.ShardingRequired,
221
+ GatewayCloseCodes.InvalidAPIVersion,
222
+ GatewayCloseCodes.InvalidIntents,
223
+ GatewayCloseCodes.DisallowedIntents
224
+ ];
225
+ var IDENTIFY_CLOSE_CODES = [
226
+ GatewayCloseCodes.InvalidSeq,
227
+ GatewayCloseCodes.SessionTimedOut
228
+ ];
229
+ function classifyCloseCode(code, canResume) {
230
+ if (FATAL_CLOSE_CODES.includes(code))
231
+ return "stop";
232
+ if (IDENTIFY_CLOSE_CODES.includes(code))
233
+ return "identify";
234
+ return canResume ? "resume" : "identify";
235
+ }
236
+
237
+ class GatewayReconnect {
238
+ #options;
239
+ #random;
240
+ #attempt = 0;
241
+ #timer;
242
+ #promise;
243
+ #resolve;
244
+ #reject;
245
+ constructor(options) {
246
+ this.#options = options;
247
+ this.#random = options.random ?? Math.random;
248
+ }
249
+ get enabled() {
250
+ return this.#options.enabled;
251
+ }
252
+ get attempts() {
253
+ return this.#attempt;
254
+ }
255
+ get pending() {
256
+ return this.#timer !== undefined;
257
+ }
258
+ get exhausted() {
259
+ return this.#attempt >= this.#options.maxAttempts;
260
+ }
261
+ get connecting() {
262
+ return this.#promise !== undefined;
263
+ }
264
+ get attemptPromise() {
265
+ return this.#promise;
266
+ }
267
+ classifyClose(code, canResume) {
268
+ return classifyCloseCode(code, canResume);
269
+ }
270
+ canReconnect() {
271
+ return this.#options.enabled && !this.exhausted;
272
+ }
273
+ schedule(run) {
274
+ if (!this.#options.enabled)
275
+ return { scheduled: false, reason: "disabled" };
276
+ if (this.#timer)
277
+ return { scheduled: false, reason: "pending" };
278
+ if (this.exhausted)
279
+ return { scheduled: false, reason: "exhausted" };
280
+ const attempt = this.#attempt;
281
+ const delay = Math.min(this.#options.maxDelay, this.#options.baseDelay * 2 ** this.#attempt++);
282
+ const jitter = this.#random() * Math.max(1, delay * 0.25);
283
+ const delayMs = delay + jitter;
284
+ this.#timer = setTimeout(() => {
285
+ this.#timer = undefined;
286
+ run();
287
+ }, delayMs);
288
+ return { scheduled: true, delayMs, attempt };
289
+ }
290
+ cancel() {
291
+ if (this.#timer)
292
+ clearTimeout(this.#timer);
293
+ this.#timer = undefined;
294
+ }
295
+ reset() {
296
+ this.#attempt = 0;
297
+ }
298
+ attempt(start) {
299
+ if (this.#promise)
300
+ return this.#promise;
301
+ this.#promise = new Promise((resolve, reject) => {
302
+ this.#resolve = resolve;
303
+ this.#reject = reject;
304
+ start();
305
+ });
306
+ return this.#promise;
307
+ }
308
+ settle(error) {
309
+ const resolve = this.#resolve;
310
+ const reject = this.#reject;
311
+ this.#resolve = undefined;
312
+ this.#reject = undefined;
313
+ this.#promise = undefined;
314
+ if (error)
315
+ reject?.(error);
316
+ else
317
+ resolve?.();
318
+ }
319
+ }
320
+
321
+ // packages/ws/src/decoder.ts
322
+ import {
323
+ createInflate,
324
+ constants as zlibConstants
325
+ } from "zlib";
326
+ async function toBytes(data) {
327
+ if (data instanceof Uint8Array)
328
+ return data;
329
+ if (data instanceof ArrayBuffer)
330
+ return new Uint8Array(data);
331
+ if (typeof Blob !== "undefined" && data instanceof Blob)
332
+ return new Uint8Array(await data.arrayBuffer());
333
+ throw new TypeError("Gateway returned an unsupported compressed frame type.");
334
+ }
335
+
336
+ class PlainTextDecoder {
337
+ push(data) {
338
+ return [String(data)];
339
+ }
340
+ reset() {}
341
+ }
342
+
343
+ class ZlibStreamDecoder {
344
+ #create;
345
+ #inflate;
346
+ #chunks = [];
347
+ #failure;
348
+ #pendingReject;
349
+ constructor(create = () => createInflate()) {
350
+ this.#create = create;
351
+ }
352
+ push(data) {
353
+ if (typeof data === "string")
354
+ return [data];
355
+ return this.#inflateFrame(data);
356
+ }
357
+ async#inflateFrame(data) {
358
+ const chunk = await toBytes(data);
359
+ const inflate = this.#inflate ??= this.#open();
360
+ if (this.#failure)
361
+ throw this.#failure;
362
+ const complete = chunk.length >= 4 && chunk[chunk.length - 4] === 0 && chunk[chunk.length - 3] === 0 && chunk[chunk.length - 2] === 255 && chunk[chunk.length - 1] === 255;
363
+ await this.#awaiting((settle) => inflate.write(chunk, (error) => error ? settle(error) : settle()));
364
+ if (!complete)
365
+ return [];
366
+ await this.#awaiting((settle) => inflate.flush(zlibConstants.Z_SYNC_FLUSH, () => settle()));
367
+ const parts = this.#chunks;
368
+ this.#chunks = [];
369
+ if (parts.length === 0)
370
+ return [];
371
+ const total = parts.reduce((size, part) => size + part.length, 0);
372
+ const merged = new Uint8Array(total);
373
+ let offset = 0;
374
+ for (const part of parts) {
375
+ merged.set(part, offset);
376
+ offset += part.length;
377
+ }
378
+ return [new TextDecoder().decode(merged)];
379
+ }
380
+ reset() {
381
+ const inflate = this.#inflate;
382
+ this.#inflate = undefined;
383
+ this.#chunks = [];
384
+ this.#failure = undefined;
385
+ this.#pendingReject = undefined;
386
+ if (!inflate)
387
+ return;
388
+ inflate.removeAllListeners();
389
+ try {
390
+ inflate.close();
391
+ } catch {}
392
+ }
393
+ #awaiting(run) {
394
+ if (this.#failure)
395
+ return Promise.reject(this.#failure);
396
+ return new Promise((resolve, reject) => {
397
+ this.#pendingReject = reject;
398
+ run((error) => {
399
+ this.#pendingReject = undefined;
400
+ if (error)
401
+ reject(error);
402
+ else if (this.#failure)
403
+ reject(this.#failure);
404
+ else
405
+ resolve();
406
+ });
407
+ });
408
+ }
409
+ #open() {
410
+ const inflate = this.#create();
411
+ inflate.on("data", (chunk) => this.#chunks.push(chunk));
412
+ inflate.on("error", (error) => {
413
+ this.#failure = error;
414
+ const reject = this.#pendingReject;
415
+ this.#pendingReject = undefined;
416
+ reject?.(error);
417
+ });
418
+ return inflate;
419
+ }
420
+ }
421
+ function createDecoder(compress) {
422
+ return compress ? new ZlibStreamDecoder : new PlainTextDecoder;
423
+ }
424
+
425
+ // packages/ws/src/transport.ts
426
+ var OPEN = 1;
427
+
428
+ class WebSocketTransport {
429
+ #handlers;
430
+ #decoder;
431
+ #createSocket;
432
+ #compress;
433
+ #socket;
434
+ #generation = 0;
435
+ #queue = Promise.resolve();
436
+ constructor(options) {
437
+ this.#handlers = options.handlers;
438
+ this.#compress = options.compress ?? false;
439
+ this.#decoder = options.decoder ?? createDecoder(options.compress);
440
+ this.#createSocket = options.createSocket ?? ((url) => new WebSocket(url));
441
+ }
442
+ get connected() {
443
+ return this.#socket?.readyState === OPEN;
444
+ }
445
+ get live() {
446
+ return this.#socket !== undefined && this.#socket.readyState <= OPEN;
447
+ }
448
+ get generation() {
449
+ return this.#generation;
450
+ }
451
+ connect(url) {
452
+ this.#discard(1000, "Superseded by a new connection");
453
+ const generation = ++this.#generation;
454
+ this.#decoder.reset();
455
+ this.#queue = Promise.resolve();
456
+ let socket;
457
+ try {
458
+ socket = this.#createSocket(url);
459
+ } catch (error) {
460
+ return { ok: false, error: this.#asError(error) };
461
+ }
462
+ if (this.#compress) {
463
+ try {
464
+ socket.binaryType = "arraybuffer";
465
+ } catch {}
466
+ }
467
+ this.#socket = socket;
468
+ socket.addEventListener("open", () => {
469
+ if (!this.#owns(generation))
470
+ return;
471
+ this.#handlers.onOpen();
472
+ });
473
+ socket.addEventListener("message", (event) => {
474
+ if (!this.#owns(generation))
475
+ return;
476
+ let decoded;
477
+ try {
478
+ decoded = this.#decoder.push(event.data);
479
+ } catch (error) {
480
+ this.#handlers.onError(this.#asError(error));
481
+ return;
482
+ }
483
+ if (!(decoded instanceof Promise)) {
484
+ for (const frame of decoded)
485
+ this.#handlers.onFrame(frame);
486
+ return;
487
+ }
488
+ this.#queue = this.#queue.then(async () => {
489
+ const frames = await decoded;
490
+ if (!this.#owns(generation))
491
+ return;
492
+ for (const frame of frames)
493
+ this.#handlers.onFrame(frame);
494
+ }).catch((error) => {
495
+ if (!this.#owns(generation))
496
+ return;
497
+ this.#handlers.onError(this.#asError(error));
498
+ });
499
+ });
500
+ socket.addEventListener("close", (event) => {
501
+ if (!this.#owns(generation))
502
+ return;
503
+ this.#socket = undefined;
504
+ this.#generation++;
505
+ this.#decoder.reset();
506
+ this.#handlers.onClose(event.code, event.reason ?? "");
507
+ });
508
+ socket.addEventListener("error", () => {
509
+ if (!this.#owns(generation))
510
+ return;
511
+ this.#handlers.onError(new Error("Gateway WebSocket error"));
512
+ });
513
+ return { ok: true };
514
+ }
515
+ send(data) {
516
+ if (!this.connected)
517
+ return false;
518
+ try {
519
+ this.#socket.send(data);
520
+ return true;
521
+ } catch (error) {
522
+ this.#handlers.onError(this.#asError(error));
523
+ return false;
524
+ }
525
+ }
526
+ close(code = 1000, reason = "") {
527
+ const socket = this.#socket;
528
+ if (!socket)
529
+ return;
530
+ try {
531
+ socket.close(code, reason);
532
+ } catch (error) {
533
+ this.#handlers.onError(this.#asError(error));
534
+ }
535
+ }
536
+ destroy(code = 1000, reason = "") {
537
+ this.#discard(code, reason);
538
+ this.#decoder.reset();
539
+ }
540
+ #owns(generation) {
541
+ return generation === this.#generation;
542
+ }
543
+ #discard(code, reason) {
544
+ const socket = this.#socket;
545
+ if (!socket)
546
+ return;
547
+ this.#generation++;
548
+ this.#socket = undefined;
549
+ try {
550
+ socket.close(code, reason);
551
+ } catch (error) {
552
+ this.#handlers.onError(this.#asError(error));
553
+ }
554
+ }
555
+ #asError(error) {
556
+ return error instanceof Error ? error : new Error(String(error));
557
+ }
558
+ }
559
+
560
+ // packages/ws/src/opcodes.ts
561
+ var GatewayOpcodes = {
562
+ Dispatch: 0,
563
+ Heartbeat: 1,
564
+ Identify: 2,
565
+ PresenceUpdate: 3,
566
+ VoiceStateUpdate: 4,
567
+ Resume: 6,
568
+ Reconnect: 7,
569
+ RequestGuildMembers: 8,
570
+ InvalidSession: 9,
571
+ Hello: 10,
572
+ HeartbeatAck: 11
573
+ };
574
+
575
+ // packages/ws/src/protocol.ts
576
+ function classifyFrame(raw) {
577
+ let payload;
578
+ try {
579
+ payload = JSON.parse(raw);
580
+ } catch (error) {
581
+ return {
582
+ sequence: null,
583
+ action: {
584
+ type: "invalid",
585
+ violation: "invalid-json",
586
+ message: "Gateway returned invalid JSON",
587
+ cause: error
588
+ }
589
+ };
590
+ }
591
+ return classifyPayload(payload);
592
+ }
593
+ function classifyPayload(payload) {
594
+ if (payload === null || typeof payload !== "object" || typeof payload.op !== "number")
595
+ return {
596
+ sequence: null,
597
+ action: {
598
+ type: "invalid",
599
+ violation: "invalid-payload",
600
+ message: "Gateway returned an invalid payload"
601
+ }
602
+ };
603
+ const frame = payload;
604
+ const sequence = typeof frame.s === "number" ? frame.s : null;
605
+ switch (frame.op) {
606
+ case GatewayOpcodes.Dispatch:
607
+ return {
608
+ sequence,
609
+ action: {
610
+ type: "dispatch",
611
+ event: typeof frame.t === "string" ? frame.t : null,
612
+ data: frame.d
613
+ }
614
+ };
615
+ case GatewayOpcodes.Hello: {
616
+ const interval = frame.d?.heartbeat_interval;
617
+ if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0)
618
+ return {
619
+ sequence,
620
+ action: {
621
+ type: "invalid",
622
+ violation: "invalid-hello",
623
+ message: "Gateway HELLO payload contains an invalid heartbeat interval"
624
+ }
625
+ };
626
+ return {
627
+ sequence,
628
+ action: { type: "hello", heartbeatInterval: interval }
629
+ };
630
+ }
631
+ case GatewayOpcodes.Heartbeat:
632
+ return { sequence, action: { type: "heartbeat" } };
633
+ case GatewayOpcodes.HeartbeatAck:
634
+ return {
635
+ sequence,
636
+ action: { type: "heartbeat-ack", data: frame.d }
637
+ };
638
+ case GatewayOpcodes.Reconnect:
639
+ return { sequence, action: { type: "reconnect" } };
640
+ case GatewayOpcodes.InvalidSession:
641
+ return {
642
+ sequence,
643
+ action: {
644
+ type: "invalid-session",
645
+ resumable: frame.d === true
646
+ }
647
+ };
648
+ default:
649
+ return { sequence, action: { type: "unknown", opcode: frame.op } };
650
+ }
651
+ }
652
+ function identifyPayload(options) {
653
+ const props = options.properties ?? {};
654
+ const os = props.os ?? "Android";
655
+ const browser = props.browser ?? "Discord Android";
656
+ const device = props.device ?? "Discord Android";
657
+ return {
658
+ op: GatewayOpcodes.Identify,
659
+ d: {
660
+ token: options.token,
661
+ intents: options.intents,
662
+ properties: {
663
+ os,
664
+ browser,
665
+ device,
666
+ $os: os,
667
+ $browser: browser,
668
+ $device: device,
669
+ ...props
670
+ },
671
+ presence: {
672
+ since: options.presence?.since ?? null,
673
+ activities: options.presence?.activities ?? [],
674
+ status: options.presence?.status ?? "online",
675
+ afk: Boolean(options.presence?.afk)
676
+ },
677
+ shard: [options.shardId, options.shardCount]
678
+ },
679
+ s: null,
680
+ t: null
681
+ };
682
+ }
683
+ function resumePayload(token, resume) {
684
+ return {
685
+ op: GatewayOpcodes.Resume,
686
+ d: {
687
+ token,
688
+ session_id: resume.sessionId,
689
+ seq: resume.sequence
690
+ },
691
+ s: null,
692
+ t: null
693
+ };
694
+ }
695
+ function heartbeatPayload(sequence) {
696
+ return { op: GatewayOpcodes.Heartbeat, d: sequence, s: null, t: null };
697
+ }
698
+
699
+ // packages/types/src/gateway.ts
3
700
  var GatewayIntentBits = {
4
701
  Guilds: 1 << 0,
5
702
  GuildMembers: 1 << 1,
@@ -54,6 +751,8 @@ var IntentBits = {
54
751
  function resolveGatewayIntents(intents) {
55
752
  if (typeof intents === "number")
56
753
  return intents;
754
+ if (intents && typeof intents === "object" && !Array.isArray(intents))
755
+ return intents.bitfield;
57
756
  if (Array.isArray(intents)) {
58
757
  return intents.reduce((acc, intent) => {
59
758
  return acc | resolveGatewayIntents(intent);
@@ -73,21 +772,7 @@ function resolveGatewayIntents(intents) {
73
772
  }
74
773
  return 0;
75
774
  }
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
- };
775
+ // packages/ws/src/state.ts
91
776
  class GatewayError extends Error {
92
777
  code;
93
778
  constructor(message, code, options) {
@@ -96,31 +781,43 @@ class GatewayError extends Error {
96
781
  this.code = code;
97
782
  }
98
783
  }
784
+ // packages/ws/src/send-budget.ts
785
+ class SendBudget {
786
+ #timestamps = [];
787
+ #limit;
788
+ #windowMs;
789
+ constructor(limit = 115, windowMs = 60000) {
790
+ this.#limit = limit;
791
+ this.#windowMs = windowMs;
792
+ }
793
+ get used() {
794
+ return this.#timestamps.length;
795
+ }
796
+ allows(privileged, now = Date.now()) {
797
+ while (this.#timestamps.length && now - this.#timestamps[0] >= this.#windowMs)
798
+ this.#timestamps.shift();
799
+ return privileged || this.#timestamps.length < this.#limit;
800
+ }
801
+ record(now = Date.now()) {
802
+ this.#timestamps.push(now);
803
+ }
804
+ }
805
+
806
+ // packages/ws/src/index.ts
807
+ var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
99
808
 
100
809
  class Gateway {
101
810
  state = "CONNECT" /* Connect */;
102
811
  #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;
812
+ #transport;
813
+ #session = new GatewaySession;
814
+ #token = 0;
815
+ #heartbeat;
116
816
  #closed = false;
117
- #attempt = 0;
118
- #reconnectTimer;
119
- #connectPromise;
120
- #resolveConnect;
121
- #rejectConnect;
817
+ #reconnect;
122
818
  #listeners = new Map;
123
- #sendTimestamps = [];
819
+ #sendBudget = new SendBudget;
820
+ ping = -1;
124
821
  constructor(options) {
125
822
  if (!options.token?.trim())
126
823
  throw new TypeError("A Gateway token is required.");
@@ -141,39 +838,78 @@ class Gateway {
141
838
  browser: "Discord Android",
142
839
  device: "Discord Android"
143
840
  },
144
- presence: { status: "online", activities: [], afk: false, since: null },
145
- ...options
841
+ presence: {
842
+ status: "online",
843
+ activities: [],
844
+ afk: false,
845
+ since: null
846
+ },
847
+ ...options,
848
+ intents: resolvedIntents
146
849
  };
147
850
  if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
148
851
  throw new RangeError("Gateway shardId must be within the configured shard count.");
149
852
  if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
150
853
  throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
854
+ this.#transport = new WebSocketTransport({
855
+ compress: this.#options.compress,
856
+ createSocket: options.createSocket,
857
+ handlers: {
858
+ onOpen: () => this.#onOpen(),
859
+ onFrame: (raw) => this.#message(raw, this.#token),
860
+ onClose: (code) => this.#close(code, this.#token),
861
+ onError: (error) => this.#emitError(error instanceof GatewayError ? error : new GatewayError(error.message, undefined, {
862
+ cause: error
863
+ }))
864
+ }
865
+ });
866
+ this.#reconnect = new GatewayReconnect({
867
+ enabled: this.#options.reconnect,
868
+ maxAttempts: this.#options.maxReconnectAttempts,
869
+ baseDelay: this.#options.reconnectBaseDelay,
870
+ maxDelay: this.#options.reconnectMaxDelay
871
+ });
872
+ this.#heartbeat = new GatewayHeartbeat({
873
+ ackTimeout: this.#options.heartbeatAckTimeout,
874
+ zombieTimeout: this.#options.zombieTimeout,
875
+ send: (sequence) => this.#dispatch(heartbeatPayload(sequence), true),
876
+ sequence: () => this.#session.sequence,
877
+ isConnected: () => !this.#closed && this.#transport.connected,
878
+ onTimeout: (timeout) => this.#onHeartbeatTimeout(timeout),
879
+ onError: (error) => this.#emitError(error)
880
+ });
151
881
  }
152
- connect(url = "wss://gateway.discord.gg/?v=10&encoding=json") {
882
+ #onHeartbeatTimeout(timeout) {
883
+ if (timeout.type === "zombie") {
884
+ this.#emit("zombie", {
885
+ silentFor: timeout.silentFor,
886
+ timeout: timeout.deadline
887
+ });
888
+ this.#emitError(new GatewayError(`Gateway connection appears stale after ${timeout.silentFor}ms without traffic.`));
889
+ } else {
890
+ this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${timeout.elapsedMs}ms.`));
891
+ }
892
+ this.#transport.close(1001, timeout.type === "zombie" ? "Zombie Gateway connection" : "Heartbeat timeout");
893
+ }
894
+ connect(url = DEFAULT_GATEWAY_URL) {
153
895
  if (this.state === "CLOSED" /* Closed */)
154
896
  throw new GatewayError("Gateway has been permanently closed.");
155
- if (this.#connectPromise)
156
- return this.#connectPromise;
897
+ const inFlight = this.#reconnect.attemptPromise;
898
+ if (inFlight)
899
+ return inFlight;
900
+ if (this.#transport.live)
901
+ return Promise.resolve();
902
+ this.#reconnect.cancel();
157
903
  this.#closed = false;
158
904
  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;
905
+ const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
906
+ return this.#reconnect.attempt(() => this.#open(connectURL));
165
907
  }
166
908
  close() {
167
909
  this.#closed = true;
168
910
  this.#clearTimers();
169
911
  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
- }
912
+ this.#transport.destroy(1000, "Client closed connection");
177
913
  this.#setState("CLOSED" /* Closed */);
178
914
  }
179
915
  emit(event, data) {
@@ -195,23 +931,20 @@ class Gateway {
195
931
  send(payload) {
196
932
  if (!payload || !Number.isInteger(payload.op))
197
933
  throw new TypeError("Gateway payload must contain an integer opcode.");
198
- if (this.#ws?.readyState !== WebSocket.OPEN)
934
+ return this.#dispatch(payload, false);
935
+ }
936
+ #dispatch(payload, privileged) {
937
+ if (!this.#transport.connected)
199
938
  return false;
200
939
  const now = Date.now();
201
- while (this.#sendTimestamps.length && now - this.#sendTimestamps[0] >= 60000)
202
- this.#sendTimestamps.shift();
203
- if (this.#sendTimestamps.length >= 115) {
940
+ if (!this.#sendBudget.allows(privileged, now)) {
204
941
  this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
205
942
  return false;
206
943
  }
207
- try {
208
- this.#ws.send(JSON.stringify(payload));
209
- this.#sendTimestamps.push(now);
210
- return true;
211
- } catch (error) {
212
- this.#emitError(error);
944
+ if (!this.#transport.send(JSON.stringify(payload)))
213
945
  return false;
214
- }
946
+ this.#sendBudget.record(now);
947
+ return true;
215
948
  }
216
949
  setPresence(data) {
217
950
  this.#options.presence = { ...this.#options.presence, ...data };
@@ -247,289 +980,142 @@ class Gateway {
247
980
  #open(url) {
248
981
  if (this.#closed)
249
982
  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);
983
+ this.#token = this.#session.beginConnection();
984
+ const result = this.#transport.connect(url);
985
+ if (result.ok)
261
986
  return;
987
+ const failure = this.#normalizeError(result.error);
988
+ this.#emitError(failure);
989
+ if (this.#options.reconnect) {
990
+ this.#setState("RECONNECT" /* Reconnect */);
991
+ this.#scheduleReconnect();
992
+ } else {
993
+ this.#settleConnect(failure);
262
994
  }
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
995
  }
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");
996
+ #onOpen() {
997
+ this.#heartbeat.watch();
998
+ this.#emit("open", undefined);
999
+ this.#settleConnect();
1000
+ }
1001
+ #message(raw, token) {
1002
+ if (!this.#session.owns(token))
315
1003
  return;
316
- }
317
- if (typeof payload.s === "number")
318
- this.#sequence = payload.s;
319
- switch (payload.op) {
320
- case GatewayOpcodes.Dispatch:
1004
+ const { sequence, action } = classifyFrame(raw);
1005
+ if (sequence !== null)
1006
+ this.#session.recordSequence(sequence, token);
1007
+ this.#apply(action, token);
1008
+ }
1009
+ #apply(action, token) {
1010
+ switch (action.type) {
1011
+ case "dispatch":
321
1012
  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:
1013
+ this.#handleDispatch(action.event, action.data, token);
1014
+ return;
1015
+ case "hello":
1016
+ this.#handleHello(action.heartbeatInterval);
1017
+ return;
1018
+ case "heartbeat":
328
1019
  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) {
1020
+ this.#heartbeat.sendHeartbeat();
1021
+ return;
1022
+ case "heartbeat-ack":
1023
+ this.#heartbeat.acknowledge();
1024
+ this.ping = this.#heartbeat.latency;
1025
+ this.#emit("heartbeatAck", action.data);
1026
+ return;
1027
+ case "reconnect":
1028
+ this.#transport.close(1001, "Server requested reconnect");
1029
+ return;
1030
+ case "invalid-session": {
1031
+ if (!action.resumable)
1032
+ this.#session.invalidate(token, "invalid-session");
1033
+ this.#emit("invalidSession", action.resumable);
1034
+ this.#transport.close(action.resumable ? GatewayCloseCodes.UnknownError : 1000, action.resumable ? "Invalid session (resumable)" : "Invalid session");
1035
+ return;
1036
+ }
1037
+ case "invalid":
1038
+ this.#emitError(new GatewayError(action.message, undefined, action.cause === undefined ? undefined : { cause: action.cause }));
1039
+ this.#transport.close(1002, action.violation === "invalid-json" ? "Invalid JSON" : action.violation === "invalid-hello" ? "Invalid heartbeat interval" : "Invalid payload");
1040
+ return;
1041
+ case "unknown":
1042
+ return;
1043
+ }
1044
+ }
1045
+ #handleDispatch(event, data, token) {
348
1046
  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"));
1047
+ if (!this.#session.activate(data, token)) {
1048
+ if (this.#session.owns(token))
1049
+ this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
352
1050
  return;
353
1051
  }
354
- this.#sessionId = ready.session_id;
355
- this.#resumeURL = ready.resume_gateway_url;
356
- this.#attempt = 0;
1052
+ this.#reconnect.reset();
357
1053
  this.#setState("READY" /* Ready */);
358
1054
  this.#emit("ready", data);
1055
+ } else if (event === "RESUMED") {
1056
+ this.#reconnect.reset();
1057
+ this.#setState("READY" /* Ready */);
1058
+ this.#emit("resumed", data);
359
1059
  }
360
1060
  this.#emit("RAW", { event: event ?? "UNKNOWN", data });
361
1061
  if (this.#closed)
362
1062
  return;
363
1063
  this.#emit(event ?? "dispatch", data);
364
1064
  }
365
- #handleHello(data) {
366
- const interval = data?.heartbeat_interval;
367
- if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0) {
368
- this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
369
- this.#ws?.close(1002, "Invalid heartbeat interval");
370
- return;
371
- }
372
- this.#heartbeatInterval = interval;
373
- this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
374
- this.#startHeartbeat(interval);
1065
+ #handleHello(interval) {
1066
+ this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
1067
+ this.#heartbeat.start(interval);
375
1068
  this.#identifyOrResume();
376
1069
  }
377
1070
  #identifyOrResume() {
378
- if (this.#sessionId && this.#sequence !== null && this.#resumeURL) {
1071
+ const intent = this.#session.handshake();
1072
+ if (intent.type === "resume") {
379
1073
  this.#setState("RESUME" /* Resume */);
380
- this.send({
381
- op: GatewayOpcodes.Resume,
382
- d: {
383
- token: this.#options.token,
384
- session_id: this.#sessionId,
385
- seq: this.#sequence
386
- },
387
- s: null,
388
- t: null
389
- });
1074
+ this.#dispatch(resumePayload(this.#options.token, intent), true);
390
1075
  return;
391
1076
  }
392
1077
  this.#setState("IDENTIFY" /* Identify */);
393
- const props = this.#options.properties ?? {
394
- os: "Android",
395
- browser: "Discord Android",
396
- device: "Discord Android"
397
- };
398
- const os = props.os ?? "Android";
399
- const browser = props.browser ?? "Discord Android";
400
- const device = props.device ?? "Discord Android";
401
- this.send({
402
- op: GatewayOpcodes.Identify,
403
- d: {
404
- token: this.#options.token,
405
- intents: this.#options.intents,
406
- properties: {
407
- os,
408
- browser,
409
- device,
410
- $os: os,
411
- $browser: browser,
412
- $device: device,
413
- ...props
414
- },
415
- presence: {
416
- since: this.#options.presence?.since ?? null,
417
- activities: this.#options.presence?.activities ?? [],
418
- status: this.#options.presence?.status ?? "online",
419
- afk: Boolean(this.#options.presence?.afk)
420
- },
421
- shard: [this.#options.shardId, this.#options.shardCount]
422
- },
423
- s: null,
424
- t: null
425
- });
426
- }
427
- #startHeartbeat(interval) {
428
- this.#clearHeartbeatTimers();
429
- this.#heartbeatACK = true;
430
- this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
431
- this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
432
- }
433
- #sendHeartbeat() {
434
- this.#heartbeatACK = false;
435
- this.#heartbeatSentAt = Date.now();
436
- if (!this.send({
437
- op: GatewayOpcodes.Heartbeat,
438
- d: this.#sequence,
439
- s: null,
440
- t: null
441
- })) {
442
- this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
443
- return;
444
- }
445
- this.#clearHeartbeatAckTimer();
446
- this.#heartbeatAckTimer = setTimeout(() => {
447
- if (!this.#heartbeatACK) {
448
- const elapsed = Date.now() - this.#heartbeatSentAt;
449
- this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
450
- try {
451
- this.#ws?.close(1001, "Heartbeat timeout");
452
- } catch (error) {
453
- this.#emitError(error);
454
- }
455
- }
456
- }, this.#options.heartbeatAckTimeout);
1078
+ this.#dispatch(identifyPayload({
1079
+ token: this.#options.token,
1080
+ intents: this.#options.intents,
1081
+ shardId: this.#options.shardId,
1082
+ shardCount: this.#options.shardCount,
1083
+ properties: this.#options.properties,
1084
+ presence: this.#options.presence
1085
+ }), true);
457
1086
  }
458
- #close(ws, code) {
459
- if (this.#ws !== ws)
460
- return;
461
- this.#ws = undefined;
1087
+ #close(code, token) {
462
1088
  this.#clearTimers();
463
- const action = this.#closeAction(code);
1089
+ const action = this.#reconnect.classifyClose(code, this.#session.canResume);
464
1090
  this.#emit("close", { code, action });
465
- if (this.#closed || !this.#options.reconnect || action === "stop") {
1091
+ if (this.#closed || !this.#reconnect.enabled || action === "stop") {
1092
+ if (action === "stop")
1093
+ this.#closed = true;
466
1094
  this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
467
- if (this.#connectPromise)
468
- this.#settleConnect(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
1095
+ if (this.#reconnect.connecting)
1096
+ this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
469
1097
  return;
470
1098
  }
471
- if (action === "identify") {
472
- this.#sequence = null;
473
- this.#sessionId = undefined;
474
- }
1099
+ if (action === "identify")
1100
+ this.#session.invalidate(token, "session-timeout");
475
1101
  this.#setState("RECONNECT" /* Reconnect */);
476
1102
  this.#scheduleReconnect();
477
1103
  }
478
- #closeAction(code) {
479
- if ([4004, 4010, 4011, 4012, 4013, 4014].includes(code))
480
- return "stop";
481
- if ([4007, 4009].includes(code))
482
- return "identify";
483
- return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
484
- }
485
1104
  #scheduleReconnect() {
486
- if (this.#closed || !this.#options.reconnect || this.#reconnectTimer)
1105
+ if (this.#closed)
487
1106
  return;
488
- if (this.#attempt >= this.#options.maxReconnectAttempts) {
489
- this.#settleConnect(new GatewayError("Gateway reconnect attempts exhausted."));
490
- this.#setState("CONNECT" /* Connect */);
1107
+ const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
1108
+ if (result.scheduled || result.reason !== "exhausted")
491
1109
  return;
492
- }
493
- const delay = Math.min(this.#options.reconnectMaxDelay, this.#options.reconnectBaseDelay * 2 ** this.#attempt++);
494
- const jitter = Math.random() * Math.min(250, Math.max(1, delay * 0.25));
495
- this.#reconnectTimer = setTimeout(() => {
496
- this.#reconnectTimer = undefined;
497
- this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
498
- }, delay + jitter);
1110
+ this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
1111
+ this.#setState("CONNECT" /* Connect */);
499
1112
  }
500
1113
  #settleConnect(error) {
501
- const resolve = this.#resolveConnect;
502
- const reject = this.#rejectConnect;
503
- this.#resolveConnect = undefined;
504
- this.#rejectConnect = undefined;
505
- this.#connectPromise = undefined;
506
- if (error)
507
- reject?.(error);
508
- else
509
- resolve?.();
510
- }
511
- #clearHeartbeatAckTimer() {
512
- if (this.#heartbeatAckTimer)
513
- clearTimeout(this.#heartbeatAckTimer);
514
- this.#heartbeatAckTimer = undefined;
515
- }
516
- #clearHeartbeatTimers() {
517
- if (this.#heartbeatTimer)
518
- clearInterval(this.#heartbeatTimer);
519
- if (this.#initialHeartbeat)
520
- clearTimeout(this.#initialHeartbeat);
521
- this.#clearHeartbeatAckTimer();
522
- this.#heartbeatTimer = undefined;
523
- this.#initialHeartbeat = undefined;
1114
+ this.#reconnect.settle(error);
524
1115
  }
525
1116
  #clearTimers() {
526
- if (this.#reconnectTimer)
527
- clearTimeout(this.#reconnectTimer);
528
- if (this.#zombieTimer)
529
- clearInterval(this.#zombieTimer);
530
- this.#reconnectTimer = undefined;
531
- this.#zombieTimer = undefined;
532
- this.#clearHeartbeatTimers();
1117
+ this.#reconnect.cancel();
1118
+ this.#heartbeat.stop();
533
1119
  }
534
1120
  #normalizeError(error) {
535
1121
  return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
@@ -564,13 +1150,17 @@ class Gateway {
564
1150
  }
565
1151
 
566
1152
  // packages/sharding/src/bus.ts
1153
+ var REPLY_TYPE = "__lunibee:reply";
1154
+
567
1155
  class ShardBus {
568
1156
  #channel;
569
1157
  #handlers = new Map;
1158
+ #errorHandlers = new Set;
570
1159
  #shardId;
571
1160
  #namespace;
572
1161
  channelName;
573
1162
  #counter = 0;
1163
+ #pending = new Map;
574
1164
  constructor(shardId, channelName) {
575
1165
  if (!Number.isInteger(shardId) || shardId < 0)
576
1166
  throw new RangeError("Shard ID must be a non-negative integer.");
@@ -595,6 +1185,16 @@ class ShardBus {
595
1185
  this.#handlers.get(type)?.delete(handler);
596
1186
  return this;
597
1187
  }
1188
+ onError(handler) {
1189
+ if (typeof handler !== "function")
1190
+ throw new TypeError("Shard bus error handler is required.");
1191
+ this.#errorHandlers.add(handler);
1192
+ return this;
1193
+ }
1194
+ offError(handler) {
1195
+ this.#errorHandlers.delete(handler);
1196
+ return this;
1197
+ }
598
1198
  send(target, type, data) {
599
1199
  if (!Number.isInteger(target) || target < 0)
600
1200
  throw new RangeError("Shard target must be a non-negative integer.");
@@ -603,11 +1203,56 @@ class ShardBus {
603
1203
  broadcast(type, data) {
604
1204
  return this.#publish(null, type, data);
605
1205
  }
1206
+ respond(type, handler) {
1207
+ return this.on(type, async (message) => {
1208
+ if (!message.expectsReply)
1209
+ return;
1210
+ const reply = { requestId: message.id };
1211
+ try {
1212
+ reply.result = await handler(message.data, message);
1213
+ } catch (error) {
1214
+ reply.error = error instanceof Error ? error.message : String(error);
1215
+ }
1216
+ this.#publish(message.source, REPLY_TYPE, reply);
1217
+ });
1218
+ }
1219
+ async request(target, type, data, timeoutMs = 5000) {
1220
+ if (!Number.isInteger(target) || target < 0)
1221
+ throw new RangeError("Shard target must be a non-negative integer.");
1222
+ const [reply] = await this.#collect(target, type, data, timeoutMs, 1);
1223
+ if (!reply)
1224
+ throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
1225
+ if (reply.error !== undefined)
1226
+ throw new Error(reply.error);
1227
+ return reply.result;
1228
+ }
1229
+ broadcastRequest(type, data, options = {}) {
1230
+ return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
1231
+ }
1232
+ #collect(target, type, data, timeoutMs, expected) {
1233
+ const replies = [];
1234
+ return new Promise((resolve) => {
1235
+ let requestId = "";
1236
+ const finish = () => {
1237
+ clearTimeout(timer);
1238
+ this.#pending.delete(requestId);
1239
+ resolve(replies);
1240
+ };
1241
+ const timer = setTimeout(finish, timeoutMs);
1242
+ requestId = this.#publish(target, type, data, true);
1243
+ this.#pending.set(requestId, (reply) => {
1244
+ replies.push(reply);
1245
+ if (replies.length >= expected)
1246
+ finish();
1247
+ });
1248
+ });
1249
+ }
606
1250
  close() {
607
1251
  this.#channel.close();
608
1252
  this.#handlers.clear();
1253
+ this.#errorHandlers.clear();
609
1254
  }
610
- #publish(target, type, data) {
1255
+ #publish(target, type, data, expectsReply = false) {
611
1256
  if (!type.trim())
612
1257
  throw new TypeError("Shard message type is required.");
613
1258
  const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
@@ -616,7 +1261,8 @@ class ShardBus {
616
1261
  target,
617
1262
  type,
618
1263
  data,
619
- id
1264
+ id,
1265
+ ...expectsReply ? { expectsReply } : {}
620
1266
  });
621
1267
  return id;
622
1268
  }
@@ -625,20 +1271,215 @@ class ShardBus {
625
1271
  return;
626
1272
  if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
627
1273
  return;
1274
+ if (message.type === REPLY_TYPE) {
1275
+ const reply = message.data;
1276
+ this.#pending.get(reply?.requestId)?.({
1277
+ shardId: message.source,
1278
+ ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
1279
+ });
1280
+ return;
1281
+ }
628
1282
  for (const handler of this.#handlers.get(message.type) ?? []) {
629
1283
  try {
630
1284
  const result = handler(message);
631
1285
  if (result && typeof result.then === "function")
632
- Promise.resolve(result).catch(() => {
633
- return;
634
- });
1286
+ Promise.resolve(result).catch((error) => this.#reportError(error, message));
1287
+ } catch (error) {
1288
+ this.#reportError(error, message);
1289
+ }
1290
+ }
1291
+ }
1292
+ #reportError(error, message) {
1293
+ for (const handler of this.#errorHandlers) {
1294
+ try {
1295
+ handler(error, message);
1296
+ } catch {}
1297
+ }
1298
+ }
1299
+ }
1300
+ // packages/sharding/src/cluster.ts
1301
+ import { fork } from "child_process";
1302
+ import { cpus } from "os";
1303
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1304
+
1305
+ class ClusterManager {
1306
+ clusters = new Map;
1307
+ shardCount = 0;
1308
+ #options;
1309
+ #auto;
1310
+ #autoScaleTimer;
1311
+ #spawned = false;
1312
+ #stopping = false;
1313
+ #restartTimers = new Set;
1314
+ constructor(options) {
1315
+ if (!options.token?.trim())
1316
+ throw new TypeError("A Discord bot token is required.");
1317
+ if (!options.script?.trim())
1318
+ throw new TypeError("A script path is required for clustering.");
1319
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
1320
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
1321
+ }
1322
+ this.#auto = options.shardCount === "auto";
1323
+ this.#options = { ...options };
1324
+ }
1325
+ async fetchRecommendedShardCount() {
1326
+ const response = await fetch("https://discord.com/api/v10/gateway/bot", {
1327
+ headers: {
1328
+ Authorization: `Bot ${this.#options.token}`,
1329
+ "User-Agent": "Lunibee/0.1.0"
1330
+ }
1331
+ });
1332
+ if (!response.ok) {
1333
+ throw new Error(`Gateway discovery failed with status ${response.status}`);
1334
+ }
1335
+ const data = await response.json();
1336
+ if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1) {
1337
+ throw new Error("Gateway discovery returned an invalid shard count.");
1338
+ }
1339
+ return data.shards;
1340
+ }
1341
+ async spawn() {
1342
+ if (this.#spawned)
1343
+ return;
1344
+ if (typeof process?.versions?.bun === "string") {
1345
+ throw new Error("ClusterManager requires the Node.js runtime; Bun does not support child_process.fork() for this clustering mode.");
1346
+ }
1347
+ this.#spawned = true;
1348
+ const count = this.#options.shardCount === "auto" || this.#options.shardCount === undefined ? await this.fetchRecommendedShardCount() : this.#options.shardCount;
1349
+ this.shardCount = count;
1350
+ const clusterCount = this.#options.clusterCount ?? cpus().length;
1351
+ const chunks = Array.from({ length: clusterCount }, () => []);
1352
+ for (let i = 0;i < count; i++) {
1353
+ chunks[i % clusterCount].push(i);
1354
+ }
1355
+ for (let i = 0;i < clusterCount; i++) {
1356
+ if (chunks[i].length === 0)
1357
+ continue;
1358
+ this.#launch(i, chunks[i], count);
1359
+ await sleep(500);
1360
+ }
1361
+ if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
1362
+ this.#autoScaleTimer = setInterval(() => {
1363
+ this.checkAutoScale();
1364
+ }, this.#options.autoScaleInterval);
1365
+ }
1366
+ }
1367
+ #launch(id, shards, shardCount) {
1368
+ const child = fork(this.#options.script, [], {
1369
+ env: {
1370
+ ...process.env,
1371
+ SHARD_LIST: shards.join(","),
1372
+ SHARD_COUNT: shardCount.toString(),
1373
+ CLUSTER_ID: id.toString()
1374
+ }
1375
+ });
1376
+ const info = { id, process: child, shards };
1377
+ this.clusters.set(id, info);
1378
+ child.once("exit", (code, signal) => {
1379
+ if (this.clusters.get(id)?.process !== child)
1380
+ return;
1381
+ try {
1382
+ this.#options.onClusterExit?.(info, code, signal);
635
1383
  } catch {}
1384
+ if (this.#stopping || this.#options.restartOnExit === false)
1385
+ return;
1386
+ this.clusters.delete(id);
1387
+ const timer = setTimeout(() => {
1388
+ this.#restartTimers.delete(timer);
1389
+ if (this.#stopping || !this.#spawned)
1390
+ return;
1391
+ this.#launch(id, shards, shardCount);
1392
+ }, this.#options.restartDelay ?? 5000);
1393
+ timer.unref?.();
1394
+ this.#restartTimers.add(timer);
1395
+ });
1396
+ return info;
1397
+ }
1398
+ async checkAutoScale() {
1399
+ if (!this.#auto)
1400
+ return;
1401
+ try {
1402
+ const recommended = await this.fetchRecommendedShardCount();
1403
+ if (recommended !== this.shardCount) {
1404
+ await this.respawn(recommended);
1405
+ }
1406
+ } catch (error) {
1407
+ const normalized = error instanceof Error ? error : new Error(String(error));
1408
+ this.#options.onAutoScaleError?.(normalized);
1409
+ }
1410
+ }
1411
+ async respawn(newShardCount) {
1412
+ this.#options.shardCount = newShardCount;
1413
+ await this.shutdownAll();
1414
+ this.#spawned = false;
1415
+ await this.spawn();
1416
+ }
1417
+ async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
1418
+ this.#stopping = true;
1419
+ this.#clearRestartTimers();
1420
+ if (this.#autoScaleTimer) {
1421
+ clearInterval(this.#autoScaleTimer);
1422
+ this.#autoScaleTimer = undefined;
636
1423
  }
1424
+ await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
1425
+ this.clusters.clear();
1426
+ this.#spawned = false;
1427
+ this.#stopping = false;
1428
+ }
1429
+ #clearRestartTimers() {
1430
+ for (const timer of this.#restartTimers)
1431
+ clearTimeout(timer);
1432
+ this.#restartTimers.clear();
1433
+ }
1434
+ async#shutdownCluster(cluster, timeoutMs) {
1435
+ const child = cluster.process;
1436
+ if (child.exitCode !== null || child.signalCode !== null)
1437
+ return;
1438
+ await new Promise((resolve) => {
1439
+ let timer;
1440
+ const finish = () => {
1441
+ if (timer)
1442
+ clearTimeout(timer);
1443
+ resolve();
1444
+ };
1445
+ child.once("exit", finish);
1446
+ try {
1447
+ child.kill("SIGTERM");
1448
+ } catch {
1449
+ child.off("exit", finish);
1450
+ finish();
1451
+ return;
1452
+ }
1453
+ timer = setTimeout(() => {
1454
+ try {
1455
+ child.kill("SIGKILL");
1456
+ } catch {}
1457
+ }, timeoutMs);
1458
+ });
1459
+ }
1460
+ killAll() {
1461
+ this.#stopping = true;
1462
+ this.#clearRestartTimers();
1463
+ if (this.#autoScaleTimer) {
1464
+ clearInterval(this.#autoScaleTimer);
1465
+ this.#autoScaleTimer = undefined;
1466
+ }
1467
+ for (const cluster of this.clusters.values()) {
1468
+ try {
1469
+ cluster.process.kill();
1470
+ } catch {}
1471
+ }
1472
+ this.clusters.clear();
1473
+ this.#spawned = false;
1474
+ this.#stopping = false;
637
1475
  }
638
1476
  }
639
1477
 
640
1478
  // packages/sharding/src/index.ts
1479
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1480
+
641
1481
  class ShardManager {
1482
+ static IDENTIFY_INTERVAL = 5000;
642
1483
  shards = new Map;
643
1484
  get shardCount() {
644
1485
  return this.shards.size;
@@ -646,6 +1487,8 @@ class ShardManager {
646
1487
  #options;
647
1488
  #resolved = false;
648
1489
  #destroyed = false;
1490
+ #auto;
1491
+ #autoScaleTimer;
649
1492
  constructor(options) {
650
1493
  if (!options.token?.trim())
651
1494
  throw new TypeError("A Discord bot token is required.");
@@ -654,7 +1497,15 @@ class ShardManager {
654
1497
  const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
655
1498
  if (!Number.isInteger(count) || count < 1)
656
1499
  throw new RangeError("Shard count must be a positive integer.");
657
- this.#options = { ...options };
1500
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
1501
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
1502
+ if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
1503
+ throw new RangeError("spawnDelay must be a non-negative finite number of milliseconds.");
1504
+ this.#auto = options.shardCount === "auto";
1505
+ this.#options = {
1506
+ spawnDelay: ShardManager.IDENTIFY_INTERVAL,
1507
+ ...options
1508
+ };
658
1509
  if (options.shardCount !== "auto")
659
1510
  this.#initialize(count);
660
1511
  }
@@ -673,24 +1524,56 @@ class ShardManager {
673
1524
  return data.shards;
674
1525
  }
675
1526
  async connect() {
1527
+ if (this.#autoScaleTimer) {
1528
+ clearInterval(this.#autoScaleTimer);
1529
+ this.#autoScaleTimer = undefined;
1530
+ }
676
1531
  await this.#ensureInitialized();
677
1532
  for (const [id, shard] of this.shards) {
678
1533
  try {
679
1534
  await shard.connect();
680
1535
  } catch (error) {
681
1536
  this.destroy();
682
- throw new Error(`Failed to connect shard ${id}.`, { cause: error });
1537
+ throw new Error(`Failed to connect shard ${id}.`, {
1538
+ cause: error
1539
+ });
683
1540
  }
684
1541
  if (id + 1 < this.shardCount && this.#options.spawnDelay && this.#options.spawnDelay > 0)
685
- await Bun.sleep(this.#options.spawnDelay);
1542
+ await sleep2(this.#options.spawnDelay);
1543
+ }
1544
+ if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
1545
+ this.#autoScaleTimer = setInterval(() => {
1546
+ this.checkAutoScale();
1547
+ }, this.#options.autoScaleInterval);
1548
+ }
1549
+ }
1550
+ async checkAutoScale() {
1551
+ if (!this.#auto)
1552
+ return;
1553
+ try {
1554
+ const recommended = await this.fetchRecommendedShardCount();
1555
+ if (recommended !== this.shardCount) {
1556
+ await this.reshard(recommended);
1557
+ }
1558
+ } catch (error) {
1559
+ this.#options.onAutoScaleError?.(error);
686
1560
  }
687
1561
  }
1562
+ async reshard(count) {
1563
+ this.#options.shardCount = count;
1564
+ this.destroy();
1565
+ await this.connect();
1566
+ }
688
1567
  destroy() {
689
1568
  for (const shard of this.shards.values())
690
1569
  shard.close();
691
1570
  this.shards.clear();
692
1571
  this.#resolved = false;
693
1572
  this.#destroyed = true;
1573
+ if (this.#autoScaleTimer) {
1574
+ clearInterval(this.#autoScaleTimer);
1575
+ this.#autoScaleTimer = undefined;
1576
+ }
694
1577
  }
695
1578
  getShardIdForGuild(guildId) {
696
1579
  const id = BigInt(guildId);
@@ -728,8 +1611,10 @@ class ShardManager {
728
1611
  }
729
1612
  }
730
1613
  export {
1614
+ ClusterManager,
731
1615
  ShardBus,
732
- ShardManager
1616
+ ShardManager,
1617
+ ShardManager as ShardingManager
733
1618
  };
734
1619
 
735
- //# debugId=155A3557E58FD4CF64756E2164756E21
1620
+ //# debugId=C1062D33C7999C8E64756E2164756E21