lunibee 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +225 -223
  3. package/dist/builders/commands.d.ts +6 -4
  4. package/dist/builders/components.d.ts +23 -0
  5. package/dist/builders/index.d.ts +2 -2
  6. package/dist/builders/index.js +120 -24
  7. package/dist/builders/index.js.map +9 -8
  8. package/dist/collection/index.d.ts +68 -1
  9. package/dist/collection/index.js +335 -1
  10. package/dist/collection/index.js.map +4 -4
  11. package/dist/core/collector.d.ts +13 -0
  12. package/dist/core/events.d.ts +112 -2
  13. package/dist/core/index.d.ts +30 -93
  14. package/dist/core/index.js +3386 -957
  15. package/dist/core/index.js.map +48 -24
  16. package/dist/core/permissions.d.ts +48 -0
  17. package/dist/formatters/index.js.map +1 -1
  18. package/dist/handlers/index.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +3806 -1193
  21. package/dist/index.js.map +64 -43
  22. package/dist/managers/advanced.d.ts +116 -0
  23. package/dist/managers/base.d.ts +13 -2
  24. package/dist/managers/emoji.d.ts +1 -1
  25. package/dist/managers/guild-resources.d.ts +109 -0
  26. package/dist/managers/guild.d.ts +33 -2
  27. package/dist/managers/index.d.ts +44 -6
  28. package/dist/managers/index.js +1186 -150
  29. package/dist/managers/index.js.map +27 -21
  30. package/dist/managers/message.d.ts +10 -1
  31. package/dist/rest/decoder.d.ts +23 -0
  32. package/dist/rest/errors.d.ts +53 -0
  33. package/dist/rest/index.d.ts +21 -17
  34. package/dist/rest/index.js +526 -264
  35. package/dist/rest/index.js.map +14 -8
  36. package/dist/rest/limiter.d.ts +42 -0
  37. package/dist/rest/redis.d.ts +18 -1
  38. package/dist/rest/route.d.ts +31 -0
  39. package/dist/rest/routes.d.ts +22 -0
  40. package/dist/rest/scheduler.d.ts +42 -0
  41. package/dist/rest/store.d.ts +29 -0
  42. package/dist/rest/transport.d.ts +40 -0
  43. package/dist/sharding/bus.d.ts +31 -0
  44. package/dist/sharding/cluster.d.ts +10 -0
  45. package/dist/sharding/index.d.ts +39 -2
  46. package/dist/sharding/index.js +1105 -404
  47. package/dist/sharding/index.js.map +18 -8
  48. package/dist/structures/base.d.ts +10 -0
  49. package/dist/structures/channels.d.ts +56 -0
  50. package/dist/structures/index.d.ts +1 -0
  51. package/dist/structures/index.js +305 -311
  52. package/dist/structures/index.js.map +18 -14
  53. package/dist/structures/interactions.d.ts +38 -52
  54. package/dist/structures/options.d.ts +55 -0
  55. package/dist/structures/resources.d.ts +3 -3
  56. package/dist/types/gateway-events.d.ts +132 -0
  57. package/dist/types/gateway.d.ts +197 -0
  58. package/dist/types/index.d.ts +53 -260
  59. package/dist/types/index.js +44 -2
  60. package/dist/types/index.js.map +5 -4
  61. package/dist/utils/index.js.map +1 -1
  62. package/dist/voice/index.d.ts +32 -1
  63. package/dist/voice/index.js +73 -7
  64. package/dist/voice/index.js.map +3 -3
  65. package/dist/ws/close-codes.d.ts +23 -0
  66. package/dist/ws/decoder.d.ts +58 -0
  67. package/dist/ws/heartbeat.d.ts +87 -0
  68. package/dist/ws/index.d.ts +18 -65
  69. package/dist/ws/index.js +911 -385
  70. package/dist/ws/index.js.map +15 -5
  71. package/dist/ws/opcodes.d.ts +14 -0
  72. package/dist/ws/protocol.d.ts +110 -0
  73. package/dist/ws/reconnect.d.ts +108 -0
  74. package/dist/ws/send-budget.d.ts +17 -0
  75. package/dist/ws/session.d.ts +100 -0
  76. package/dist/ws/state.d.ts +27 -0
  77. package/dist/ws/transport.d.ts +80 -0
  78. package/package.json +21 -20
@@ -1,5 +1,724 @@
1
1
  // @bun
2
- // packages/types/src/index.ts
2
+ // packages/sharding/package.json
3
+ var package_default = {
4
+ name: "@lunibee/sharding",
5
+ version: "0.2.0",
6
+ type: "module",
7
+ module: "./src/index.ts",
8
+ types: "./src/index.ts",
9
+ exports: {
10
+ ".": "./src/index.ts"
11
+ },
12
+ dependencies: {
13
+ "@lunibee/ws": "workspace:*"
14
+ }
15
+ };
16
+
17
+ // packages/ws/src/session.ts
18
+ class GatewaySession {
19
+ #state = { status: "none" };
20
+ #sequence = null;
21
+ #generation = 0;
22
+ #lastInvalidation;
23
+ get lastInvalidation() {
24
+ return this.#lastInvalidation;
25
+ }
26
+ get generation() {
27
+ return this.#generation;
28
+ }
29
+ get sessionId() {
30
+ return this.#state.status === "established" ? this.#state.sessionId : undefined;
31
+ }
32
+ get resumeURL() {
33
+ return this.#state.status === "established" ? this.#state.resumeURL : undefined;
34
+ }
35
+ get sequence() {
36
+ return this.#sequence;
37
+ }
38
+ get canResume() {
39
+ return this.#state.status === "established" && this.#sequence !== null;
40
+ }
41
+ beginConnection() {
42
+ return ++this.#generation;
43
+ }
44
+ owns(token) {
45
+ return token === this.#generation;
46
+ }
47
+ recordSequence(sequence, token) {
48
+ if (!this.owns(token))
49
+ return false;
50
+ if (!Number.isInteger(sequence))
51
+ return false;
52
+ if (this.#sequence !== null && sequence < this.#sequence)
53
+ return false;
54
+ this.#sequence = sequence;
55
+ return true;
56
+ }
57
+ activate(payload, token) {
58
+ if (!this.owns(token))
59
+ return false;
60
+ const ready = payload;
61
+ if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string")
62
+ return false;
63
+ this.#state = {
64
+ status: "established",
65
+ sessionId: ready.session_id,
66
+ resumeURL: ready.resume_gateway_url
67
+ };
68
+ return true;
69
+ }
70
+ invalidate(token, reason = "explicit") {
71
+ if (!this.owns(token))
72
+ return false;
73
+ this.#lastInvalidation = reason;
74
+ this.#state = { status: "none" };
75
+ this.#sequence = null;
76
+ return true;
77
+ }
78
+ resumeInfo() {
79
+ if (this.#state.status !== "established")
80
+ return;
81
+ if (this.#sequence === null)
82
+ return;
83
+ const { sessionId, resumeURL } = this.#state;
84
+ return { sessionId, sequence: this.#sequence, resumeURL };
85
+ }
86
+ handshake() {
87
+ const resume = this.resumeInfo();
88
+ return resume ? { type: "resume", ...resume } : { type: "identify" };
89
+ }
90
+ connectURL(fallback) {
91
+ return this.resumeInfo()?.resumeURL ?? fallback;
92
+ }
93
+ }
94
+
95
+ // packages/ws/src/heartbeat.ts
96
+ var MAX_MONITOR_INTERVAL = 1000;
97
+
98
+ class GatewayHeartbeat {
99
+ #options;
100
+ #now;
101
+ #interval = 0;
102
+ #acknowledged = true;
103
+ #sentAt = 0;
104
+ #latency = -1;
105
+ #lastMessageAt = 0;
106
+ #zombieReported = false;
107
+ #timer;
108
+ #initialTimer;
109
+ #ackTimer;
110
+ #monitorTimer;
111
+ constructor(options) {
112
+ if (options.zombieTimeout <= options.ackTimeout)
113
+ throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
114
+ this.#options = options;
115
+ this.#now = options.now ?? (() => Date.now());
116
+ }
117
+ get latency() {
118
+ return this.#latency;
119
+ }
120
+ get interval() {
121
+ return this.#interval;
122
+ }
123
+ get acknowledged() {
124
+ return this.#acknowledged;
125
+ }
126
+ get silentFor() {
127
+ return this.#lastMessageAt === 0 ? 0 : this.#now() - this.#lastMessageAt;
128
+ }
129
+ get stalenessDeadline() {
130
+ return Math.max(this.#options.zombieTimeout, this.#interval + this.#options.ackTimeout);
131
+ }
132
+ isHealthy() {
133
+ if (!this.#acknowledged)
134
+ return false;
135
+ if (this.#lastMessageAt === 0)
136
+ return true;
137
+ return this.silentFor < this.stalenessDeadline;
138
+ }
139
+ watch() {
140
+ this.#lastMessageAt = this.#now();
141
+ this.#zombieReported = false;
142
+ this.#startMonitor();
143
+ }
144
+ receivedMessage() {
145
+ this.#lastMessageAt = this.#now();
146
+ this.#zombieReported = false;
147
+ }
148
+ start(interval) {
149
+ this.#clearBeatTimers();
150
+ this.#interval = interval;
151
+ this.#acknowledged = true;
152
+ this.#initialTimer = setTimeout(() => {
153
+ this.#initialTimer = undefined;
154
+ this.sendHeartbeat();
155
+ this.#timer = setInterval(() => this.sendHeartbeat(), interval);
156
+ }, Math.random() * interval);
157
+ }
158
+ stop() {
159
+ this.#clearBeatTimers();
160
+ if (this.#monitorTimer)
161
+ clearInterval(this.#monitorTimer);
162
+ this.#monitorTimer = undefined;
163
+ }
164
+ sendHeartbeat() {
165
+ this.#acknowledged = false;
166
+ this.#sentAt = this.#now();
167
+ if (!this.#options.send(this.#options.sequence())) {
168
+ this.#options.onError(new Error("Unable to send Gateway heartbeat because the WebSocket is not open."));
169
+ return;
170
+ }
171
+ this.#clearAckTimer();
172
+ this.#ackTimer = setTimeout(() => {
173
+ if (this.#acknowledged)
174
+ return;
175
+ this.#options.onTimeout({
176
+ type: "ack",
177
+ elapsedMs: this.#now() - this.#sentAt
178
+ });
179
+ }, this.#options.ackTimeout);
180
+ }
181
+ acknowledge() {
182
+ if (this.#acknowledged)
183
+ return;
184
+ this.#acknowledged = true;
185
+ this.#latency = this.#now() - this.#sentAt;
186
+ this.#clearAckTimer();
187
+ }
188
+ #startMonitor() {
189
+ if (this.#monitorTimer)
190
+ clearInterval(this.#monitorTimer);
191
+ const interval = Math.max(1, Math.min(MAX_MONITOR_INTERVAL, this.#options.ackTimeout, this.#options.zombieTimeout / 2));
192
+ this.#monitorTimer = setInterval(() => this.#checkStaleness(), interval);
193
+ }
194
+ #checkStaleness() {
195
+ if (!this.#options.isConnected() || this.#lastMessageAt === 0)
196
+ return;
197
+ const silentFor = this.silentFor;
198
+ const deadline = this.stalenessDeadline;
199
+ if (silentFor < deadline || this.#zombieReported)
200
+ return;
201
+ this.#zombieReported = true;
202
+ this.#options.onTimeout({ type: "zombie", silentFor, deadline });
203
+ }
204
+ #clearAckTimer() {
205
+ if (this.#ackTimer)
206
+ clearTimeout(this.#ackTimer);
207
+ this.#ackTimer = undefined;
208
+ }
209
+ #clearBeatTimers() {
210
+ if (this.#timer)
211
+ clearInterval(this.#timer);
212
+ if (this.#initialTimer)
213
+ clearTimeout(this.#initialTimer);
214
+ this.#clearAckTimer();
215
+ this.#timer = undefined;
216
+ this.#initialTimer = undefined;
217
+ }
218
+ }
219
+
220
+ // packages/ws/src/close-codes.ts
221
+ var GatewayCloseCodes = {
222
+ UnknownError: 4000,
223
+ UnknownOpcode: 4001,
224
+ DecodeError: 4002,
225
+ NotAuthenticated: 4003,
226
+ AuthenticationFailed: 4004,
227
+ AlreadyAuthenticated: 4005,
228
+ InvalidSeq: 4007,
229
+ RateLimited: 4008,
230
+ SessionTimedOut: 4009,
231
+ InvalidShard: 4010,
232
+ ShardingRequired: 4011,
233
+ InvalidAPIVersion: 4012,
234
+ InvalidIntents: 4013,
235
+ DisallowedIntents: 4014
236
+ };
237
+
238
+ // packages/ws/src/reconnect.ts
239
+ var FATAL_CLOSE_CODES = [
240
+ GatewayCloseCodes.AuthenticationFailed,
241
+ GatewayCloseCodes.InvalidShard,
242
+ GatewayCloseCodes.ShardingRequired,
243
+ GatewayCloseCodes.InvalidAPIVersion,
244
+ GatewayCloseCodes.InvalidIntents,
245
+ GatewayCloseCodes.DisallowedIntents
246
+ ];
247
+ var IDENTIFY_CLOSE_CODES = [
248
+ GatewayCloseCodes.InvalidSeq,
249
+ GatewayCloseCodes.SessionTimedOut
250
+ ];
251
+ function classifyCloseCode(code, canResume) {
252
+ if (FATAL_CLOSE_CODES.includes(code))
253
+ return "stop";
254
+ if (IDENTIFY_CLOSE_CODES.includes(code))
255
+ return "identify";
256
+ return canResume ? "resume" : "identify";
257
+ }
258
+
259
+ class GatewayReconnect {
260
+ #options;
261
+ #random;
262
+ #attempt = 0;
263
+ #timer;
264
+ #promise;
265
+ #resolve;
266
+ #reject;
267
+ constructor(options) {
268
+ this.#options = options;
269
+ this.#random = options.random ?? Math.random;
270
+ }
271
+ get enabled() {
272
+ return this.#options.enabled;
273
+ }
274
+ get attempts() {
275
+ return this.#attempt;
276
+ }
277
+ get pending() {
278
+ return this.#timer !== undefined;
279
+ }
280
+ get exhausted() {
281
+ return this.#attempt >= this.#options.maxAttempts;
282
+ }
283
+ get connecting() {
284
+ return this.#promise !== undefined;
285
+ }
286
+ get attemptPromise() {
287
+ return this.#promise;
288
+ }
289
+ classifyClose(code, canResume) {
290
+ return classifyCloseCode(code, canResume);
291
+ }
292
+ canReconnect() {
293
+ return this.#options.enabled && !this.exhausted;
294
+ }
295
+ schedule(run) {
296
+ if (!this.#options.enabled)
297
+ return { scheduled: false, reason: "disabled" };
298
+ if (this.#timer)
299
+ return { scheduled: false, reason: "pending" };
300
+ if (this.exhausted)
301
+ return { scheduled: false, reason: "exhausted" };
302
+ const attempt = this.#attempt;
303
+ const delay = Math.min(this.#options.maxDelay, this.#options.baseDelay * 2 ** this.#attempt++);
304
+ const jitter = this.#random() * Math.max(1, delay * 0.25);
305
+ const delayMs = delay + jitter;
306
+ this.#timer = setTimeout(() => {
307
+ this.#timer = undefined;
308
+ run();
309
+ }, delayMs);
310
+ return { scheduled: true, delayMs, attempt };
311
+ }
312
+ cancel() {
313
+ if (this.#timer)
314
+ clearTimeout(this.#timer);
315
+ this.#timer = undefined;
316
+ }
317
+ reset() {
318
+ this.#attempt = 0;
319
+ }
320
+ attempt(start) {
321
+ if (this.#promise)
322
+ return this.#promise;
323
+ this.#promise = new Promise((resolve, reject) => {
324
+ this.#resolve = resolve;
325
+ this.#reject = reject;
326
+ start();
327
+ });
328
+ return this.#promise;
329
+ }
330
+ settle(error) {
331
+ const resolve = this.#resolve;
332
+ const reject = this.#reject;
333
+ this.#resolve = undefined;
334
+ this.#reject = undefined;
335
+ this.#promise = undefined;
336
+ if (error)
337
+ reject?.(error);
338
+ else
339
+ resolve?.();
340
+ }
341
+ }
342
+
343
+ // packages/ws/src/decoder.ts
344
+ import {
345
+ createInflate,
346
+ constants as zlibConstants
347
+ } from "zlib";
348
+ async function toBytes(data) {
349
+ if (data instanceof Uint8Array)
350
+ return data;
351
+ if (data instanceof ArrayBuffer)
352
+ return new Uint8Array(data);
353
+ if (typeof Blob !== "undefined" && data instanceof Blob)
354
+ return new Uint8Array(await data.arrayBuffer());
355
+ throw new TypeError("Gateway returned an unsupported compressed frame type.");
356
+ }
357
+
358
+ class PlainTextDecoder {
359
+ push(data) {
360
+ return [String(data)];
361
+ }
362
+ reset() {}
363
+ }
364
+
365
+ class ZlibStreamDecoder {
366
+ #create;
367
+ #inflate;
368
+ #chunks = [];
369
+ #failure;
370
+ #pendingReject;
371
+ constructor(create = () => createInflate()) {
372
+ this.#create = create;
373
+ }
374
+ push(data) {
375
+ if (typeof data === "string")
376
+ return [data];
377
+ return this.#inflateFrame(data);
378
+ }
379
+ async#inflateFrame(data) {
380
+ const chunk = await toBytes(data);
381
+ const inflate = this.#inflate ??= this.#open();
382
+ if (this.#failure)
383
+ throw this.#failure;
384
+ 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;
385
+ await this.#awaiting((settle) => inflate.write(chunk, (error) => error ? settle(error) : settle()));
386
+ if (!complete)
387
+ return [];
388
+ await this.#awaiting((settle) => inflate.flush(zlibConstants.Z_SYNC_FLUSH, () => settle()));
389
+ const parts = this.#chunks;
390
+ this.#chunks = [];
391
+ if (parts.length === 0)
392
+ return [];
393
+ const total = parts.reduce((size, part) => size + part.length, 0);
394
+ const merged = new Uint8Array(total);
395
+ let offset = 0;
396
+ for (const part of parts) {
397
+ merged.set(part, offset);
398
+ offset += part.length;
399
+ }
400
+ return [new TextDecoder().decode(merged)];
401
+ }
402
+ reset() {
403
+ const inflate = this.#inflate;
404
+ this.#inflate = undefined;
405
+ this.#chunks = [];
406
+ this.#failure = undefined;
407
+ this.#pendingReject = undefined;
408
+ if (!inflate)
409
+ return;
410
+ inflate.removeAllListeners();
411
+ try {
412
+ inflate.close();
413
+ } catch {}
414
+ }
415
+ #awaiting(run) {
416
+ if (this.#failure)
417
+ return Promise.reject(this.#failure);
418
+ return new Promise((resolve, reject) => {
419
+ this.#pendingReject = reject;
420
+ run((error) => {
421
+ this.#pendingReject = undefined;
422
+ if (error)
423
+ reject(error);
424
+ else if (this.#failure)
425
+ reject(this.#failure);
426
+ else
427
+ resolve();
428
+ });
429
+ });
430
+ }
431
+ #open() {
432
+ const inflate = this.#create();
433
+ inflate.on("data", (chunk) => this.#chunks.push(chunk));
434
+ inflate.on("error", (error) => {
435
+ this.#failure = error;
436
+ const reject = this.#pendingReject;
437
+ this.#pendingReject = undefined;
438
+ reject?.(error);
439
+ });
440
+ return inflate;
441
+ }
442
+ }
443
+ function createDecoder(compress) {
444
+ return compress ? new ZlibStreamDecoder : new PlainTextDecoder;
445
+ }
446
+
447
+ // packages/ws/src/transport.ts
448
+ var OPEN = 1;
449
+
450
+ class WebSocketTransport {
451
+ #handlers;
452
+ #decoder;
453
+ #createSocket;
454
+ #compress;
455
+ #socket;
456
+ #generation = 0;
457
+ #queue = Promise.resolve();
458
+ constructor(options) {
459
+ this.#handlers = options.handlers;
460
+ this.#compress = options.compress ?? false;
461
+ this.#decoder = options.decoder ?? createDecoder(options.compress);
462
+ this.#createSocket = options.createSocket ?? ((url) => new WebSocket(url));
463
+ }
464
+ get connected() {
465
+ return this.#socket?.readyState === OPEN;
466
+ }
467
+ get live() {
468
+ return this.#socket !== undefined && this.#socket.readyState <= OPEN;
469
+ }
470
+ get generation() {
471
+ return this.#generation;
472
+ }
473
+ connect(url) {
474
+ this.#discard(1000, "Superseded by a new connection");
475
+ const generation = ++this.#generation;
476
+ this.#decoder.reset();
477
+ this.#queue = Promise.resolve();
478
+ let socket;
479
+ try {
480
+ socket = this.#createSocket(url);
481
+ } catch (error) {
482
+ return { ok: false, error: this.#asError(error) };
483
+ }
484
+ if (this.#compress) {
485
+ try {
486
+ socket.binaryType = "arraybuffer";
487
+ } catch {}
488
+ }
489
+ this.#socket = socket;
490
+ socket.addEventListener("open", () => {
491
+ if (!this.#owns(generation))
492
+ return;
493
+ this.#handlers.onOpen();
494
+ });
495
+ socket.addEventListener("message", (event) => {
496
+ if (!this.#owns(generation))
497
+ return;
498
+ let decoded;
499
+ try {
500
+ decoded = this.#decoder.push(event.data);
501
+ } catch (error) {
502
+ this.#handlers.onError(this.#asError(error));
503
+ return;
504
+ }
505
+ if (!(decoded instanceof Promise)) {
506
+ for (const frame of decoded)
507
+ this.#handlers.onFrame(frame);
508
+ return;
509
+ }
510
+ this.#queue = this.#queue.then(async () => {
511
+ const frames = await decoded;
512
+ if (!this.#owns(generation))
513
+ return;
514
+ for (const frame of frames)
515
+ this.#handlers.onFrame(frame);
516
+ }).catch((error) => {
517
+ if (!this.#owns(generation))
518
+ return;
519
+ this.#handlers.onError(this.#asError(error));
520
+ });
521
+ });
522
+ socket.addEventListener("close", (event) => {
523
+ if (!this.#owns(generation))
524
+ return;
525
+ this.#socket = undefined;
526
+ this.#generation++;
527
+ this.#decoder.reset();
528
+ this.#handlers.onClose(event.code, event.reason ?? "");
529
+ });
530
+ socket.addEventListener("error", () => {
531
+ if (!this.#owns(generation))
532
+ return;
533
+ this.#handlers.onError(new Error("Gateway WebSocket error"));
534
+ });
535
+ return { ok: true };
536
+ }
537
+ send(data) {
538
+ if (!this.connected)
539
+ return false;
540
+ try {
541
+ this.#socket.send(data);
542
+ return true;
543
+ } catch (error) {
544
+ this.#handlers.onError(this.#asError(error));
545
+ return false;
546
+ }
547
+ }
548
+ close(code = 1000, reason = "") {
549
+ const socket = this.#socket;
550
+ if (!socket)
551
+ return;
552
+ try {
553
+ socket.close(code, reason);
554
+ } catch (error) {
555
+ this.#handlers.onError(this.#asError(error));
556
+ }
557
+ }
558
+ destroy(code = 1000, reason = "") {
559
+ this.#discard(code, reason);
560
+ this.#decoder.reset();
561
+ }
562
+ #owns(generation) {
563
+ return generation === this.#generation;
564
+ }
565
+ #discard(code, reason) {
566
+ const socket = this.#socket;
567
+ if (!socket)
568
+ return;
569
+ this.#generation++;
570
+ this.#socket = undefined;
571
+ try {
572
+ socket.close(code, reason);
573
+ } catch (error) {
574
+ this.#handlers.onError(this.#asError(error));
575
+ }
576
+ }
577
+ #asError(error) {
578
+ return error instanceof Error ? error : new Error(String(error));
579
+ }
580
+ }
581
+
582
+ // packages/ws/src/opcodes.ts
583
+ var GatewayOpcodes = {
584
+ Dispatch: 0,
585
+ Heartbeat: 1,
586
+ Identify: 2,
587
+ PresenceUpdate: 3,
588
+ VoiceStateUpdate: 4,
589
+ Resume: 6,
590
+ Reconnect: 7,
591
+ RequestGuildMembers: 8,
592
+ InvalidSession: 9,
593
+ Hello: 10,
594
+ HeartbeatAck: 11
595
+ };
596
+
597
+ // packages/ws/src/protocol.ts
598
+ function classifyFrame(raw) {
599
+ let payload;
600
+ try {
601
+ payload = JSON.parse(raw);
602
+ } catch (error) {
603
+ return {
604
+ sequence: null,
605
+ action: {
606
+ type: "invalid",
607
+ violation: "invalid-json",
608
+ message: "Gateway returned invalid JSON",
609
+ cause: error
610
+ }
611
+ };
612
+ }
613
+ return classifyPayload(payload);
614
+ }
615
+ function classifyPayload(payload) {
616
+ if (payload === null || typeof payload !== "object" || typeof payload.op !== "number")
617
+ return {
618
+ sequence: null,
619
+ action: {
620
+ type: "invalid",
621
+ violation: "invalid-payload",
622
+ message: "Gateway returned an invalid payload"
623
+ }
624
+ };
625
+ const frame = payload;
626
+ const sequence = typeof frame.s === "number" ? frame.s : null;
627
+ switch (frame.op) {
628
+ case GatewayOpcodes.Dispatch:
629
+ return {
630
+ sequence,
631
+ action: {
632
+ type: "dispatch",
633
+ event: typeof frame.t === "string" ? frame.t : null,
634
+ data: frame.d
635
+ }
636
+ };
637
+ case GatewayOpcodes.Hello: {
638
+ const interval = frame.d?.heartbeat_interval;
639
+ if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0)
640
+ return {
641
+ sequence,
642
+ action: {
643
+ type: "invalid",
644
+ violation: "invalid-hello",
645
+ message: "Gateway HELLO payload contains an invalid heartbeat interval"
646
+ }
647
+ };
648
+ return {
649
+ sequence,
650
+ action: { type: "hello", heartbeatInterval: interval }
651
+ };
652
+ }
653
+ case GatewayOpcodes.Heartbeat:
654
+ return { sequence, action: { type: "heartbeat" } };
655
+ case GatewayOpcodes.HeartbeatAck:
656
+ return {
657
+ sequence,
658
+ action: { type: "heartbeat-ack", data: frame.d }
659
+ };
660
+ case GatewayOpcodes.Reconnect:
661
+ return { sequence, action: { type: "reconnect" } };
662
+ case GatewayOpcodes.InvalidSession:
663
+ return {
664
+ sequence,
665
+ action: {
666
+ type: "invalid-session",
667
+ resumable: frame.d === true
668
+ }
669
+ };
670
+ default:
671
+ return { sequence, action: { type: "unknown", opcode: frame.op } };
672
+ }
673
+ }
674
+ function identifyPayload(options) {
675
+ const props = options.properties ?? {};
676
+ const os = props.os ?? "Android";
677
+ const browser = props.browser ?? "Discord Android";
678
+ const device = props.device ?? "Discord Android";
679
+ return {
680
+ op: GatewayOpcodes.Identify,
681
+ d: {
682
+ token: options.token,
683
+ intents: options.intents,
684
+ properties: {
685
+ os,
686
+ browser,
687
+ device,
688
+ $os: os,
689
+ $browser: browser,
690
+ $device: device,
691
+ ...props
692
+ },
693
+ presence: {
694
+ since: options.presence?.since ?? null,
695
+ activities: options.presence?.activities ?? [],
696
+ status: options.presence?.status ?? "online",
697
+ afk: Boolean(options.presence?.afk)
698
+ },
699
+ shard: [options.shardId, options.shardCount]
700
+ },
701
+ s: null,
702
+ t: null
703
+ };
704
+ }
705
+ function resumePayload(token, resume) {
706
+ return {
707
+ op: GatewayOpcodes.Resume,
708
+ d: {
709
+ token,
710
+ session_id: resume.sessionId,
711
+ seq: resume.sequence
712
+ },
713
+ s: null,
714
+ t: null
715
+ };
716
+ }
717
+ function heartbeatPayload(sequence) {
718
+ return { op: GatewayOpcodes.Heartbeat, d: sequence, s: null, t: null };
719
+ }
720
+
721
+ // packages/types/src/gateway.ts
3
722
  var GatewayIntentBits = {
4
723
  Guilds: 1 << 0,
5
724
  GuildMembers: 1 << 1,
@@ -54,6 +773,8 @@ var IntentBits = {
54
773
  function resolveGatewayIntents(intents) {
55
774
  if (typeof intents === "number")
56
775
  return intents;
776
+ if (intents && typeof intents === "object" && !Array.isArray(intents))
777
+ return intents.bitfield;
57
778
  if (Array.isArray(intents)) {
58
779
  return intents.reduce((acc, intent) => {
59
780
  return acc | resolveGatewayIntents(intent);
@@ -73,37 +794,7 @@ function resolveGatewayIntents(intents) {
73
794
  }
74
795
  return 0;
75
796
  }
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
- var GatewayCloseCodes = {
92
- UnknownError: 4000,
93
- UnknownOpcode: 4001,
94
- DecodeError: 4002,
95
- NotAuthenticated: 4003,
96
- AuthenticationFailed: 4004,
97
- AlreadyAuthenticated: 4005,
98
- InvalidSeq: 4007,
99
- RateLimited: 4008,
100
- SessionTimedOut: 4009,
101
- InvalidShard: 4010,
102
- ShardingRequired: 4011,
103
- InvalidAPIVersion: 4012,
104
- InvalidIntents: 4013,
105
- DisallowedIntents: 4014
106
- };
797
+ // packages/ws/src/state.ts
107
798
  class GatewayError extends Error {
108
799
  code;
109
800
  constructor(message, code, options) {
@@ -112,33 +803,44 @@ class GatewayError extends Error {
112
803
  this.code = code;
113
804
  }
114
805
  }
806
+ // packages/ws/src/send-budget.ts
807
+ class SendBudget {
808
+ #timestamps = [];
809
+ #limit;
810
+ #windowMs;
811
+ constructor(limit = 115, windowMs = 60000) {
812
+ this.#limit = limit;
813
+ this.#windowMs = windowMs;
814
+ }
815
+ get used() {
816
+ return this.#timestamps.length;
817
+ }
818
+ allows(privileged, now = Date.now()) {
819
+ while (this.#timestamps.length && now - this.#timestamps[0] >= this.#windowMs)
820
+ this.#timestamps.shift();
821
+ return privileged || this.#timestamps.length < this.#limit;
822
+ }
823
+ record(now = Date.now()) {
824
+ this.#timestamps.push(now);
825
+ }
826
+ }
827
+
828
+ // packages/ws/src/index.ts
829
+ var RESUMABLE_CLOSE_CODE = 4900;
830
+ var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
115
831
 
116
832
  class Gateway {
117
833
  state = "CONNECT" /* Connect */;
118
834
  #options;
119
- #ws;
120
- #sequence = null;
121
- #sessionId;
122
- #resumeURL;
123
- #heartbeatTimer;
124
- #initialHeartbeat;
125
- #heartbeatAckTimer;
126
- #heartbeatACK = true;
127
- #heartbeatInterval = 0;
128
- #heartbeatSentAt = 0;
129
- #lastMessageAt = 0;
130
- #zombieTimer;
131
- #zombieReported = false;
835
+ #transport;
836
+ #session = new GatewaySession;
837
+ #token = 0;
838
+ #heartbeat;
132
839
  #closed = false;
133
- #attempt = 0;
134
- #reconnectTimer;
135
- #connectPromise;
136
- #resolveConnect;
137
- #rejectConnect;
840
+ #reconnect;
138
841
  #listeners = new Map;
139
- #sendTimestamps = [];
842
+ #sendBudget = new SendBudget;
140
843
  ping = -1;
141
- #decompressor;
142
844
  constructor(options) {
143
845
  if (!options.token?.trim())
144
846
  throw new TypeError("A Gateway token is required.");
@@ -165,41 +867,72 @@ class Gateway {
165
867
  afk: false,
166
868
  since: null
167
869
  },
168
- ...options
870
+ ...options,
871
+ intents: resolvedIntents
169
872
  };
170
873
  if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
171
874
  throw new RangeError("Gateway shardId must be within the configured shard count.");
172
875
  if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
173
876
  throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
877
+ this.#transport = new WebSocketTransport({
878
+ compress: this.#options.compress,
879
+ createSocket: options.createSocket,
880
+ handlers: {
881
+ onOpen: () => this.#onOpen(),
882
+ onFrame: (raw) => this.#message(raw, this.#token),
883
+ onClose: (code) => this.#close(code, this.#token),
884
+ onError: (error) => this.#emitError(error instanceof GatewayError ? error : new GatewayError(error.message, undefined, {
885
+ cause: error
886
+ }))
887
+ }
888
+ });
889
+ this.#reconnect = new GatewayReconnect({
890
+ enabled: this.#options.reconnect,
891
+ maxAttempts: this.#options.maxReconnectAttempts,
892
+ baseDelay: this.#options.reconnectBaseDelay,
893
+ maxDelay: this.#options.reconnectMaxDelay
894
+ });
895
+ this.#heartbeat = new GatewayHeartbeat({
896
+ ackTimeout: this.#options.heartbeatAckTimeout,
897
+ zombieTimeout: this.#options.zombieTimeout,
898
+ send: (sequence) => this.#dispatch(heartbeatPayload(sequence), true),
899
+ sequence: () => this.#session.sequence,
900
+ isConnected: () => !this.#closed && this.#transport.connected,
901
+ onTimeout: (timeout) => this.#onHeartbeatTimeout(timeout),
902
+ onError: (error) => this.#emitError(error)
903
+ });
904
+ }
905
+ #onHeartbeatTimeout(timeout) {
906
+ if (timeout.type === "zombie") {
907
+ this.#emit("zombie", {
908
+ silentFor: timeout.silentFor,
909
+ timeout: timeout.deadline
910
+ });
911
+ this.#emitError(new GatewayError(`Gateway connection appears stale after ${timeout.silentFor}ms without traffic.`));
912
+ } else {
913
+ this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${timeout.elapsedMs}ms.`));
914
+ }
915
+ this.#transport.close(RESUMABLE_CLOSE_CODE, timeout.type === "zombie" ? "Zombie Gateway connection" : "Heartbeat timeout");
174
916
  }
175
- connect(url = "wss://gateway.discord.gg/?v=10&encoding=json") {
917
+ connect(url = DEFAULT_GATEWAY_URL) {
176
918
  if (this.state === "CLOSED" /* Closed */)
177
919
  throw new GatewayError("Gateway has been permanently closed.");
178
- if (this.#connectPromise)
179
- return this.#connectPromise;
920
+ const inFlight = this.#reconnect.attemptPromise;
921
+ if (inFlight)
922
+ return inFlight;
923
+ if (this.#transport.live)
924
+ return Promise.resolve();
925
+ this.#reconnect.cancel();
180
926
  this.#closed = false;
181
927
  this.#setState("CONNECT" /* Connect */);
182
928
  const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
183
- if (this.#options.compress)
184
- this.#initDecompressor();
185
- this.#connectPromise = new Promise((resolve, reject) => {
186
- this.#resolveConnect = resolve;
187
- this.#rejectConnect = reject;
188
- this.#open(connectURL);
189
- });
190
- return this.#connectPromise;
929
+ return this.#reconnect.attempt(() => this.#open(connectURL));
191
930
  }
192
931
  close() {
193
932
  this.#closed = true;
194
933
  this.#clearTimers();
195
934
  this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
196
- const ws = this.#ws;
197
- this.#ws = undefined;
198
- try {
199
- ws?.close(1000, "Client closed connection");
200
- } catch (error) {
201
- this.#emitError(error);
202
- }
935
+ this.#transport.destroy(1000, "Client closed connection");
203
936
  this.#setState("CLOSED" /* Closed */);
204
937
  }
205
938
  emit(event, data) {
@@ -224,23 +957,17 @@ class Gateway {
224
957
  return this.#dispatch(payload, false);
225
958
  }
226
959
  #dispatch(payload, privileged) {
227
- if (this.#ws?.readyState !== WebSocket.OPEN)
960
+ if (!this.#transport.connected)
228
961
  return false;
229
962
  const now = Date.now();
230
- while (this.#sendTimestamps.length && now - this.#sendTimestamps[0] >= 60000)
231
- this.#sendTimestamps.shift();
232
- if (!privileged && this.#sendTimestamps.length >= 115) {
963
+ if (!this.#sendBudget.allows(privileged, now)) {
233
964
  this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
234
965
  return false;
235
966
  }
236
- try {
237
- this.#ws.send(JSON.stringify(payload));
238
- this.#sendTimestamps.push(now);
239
- return true;
240
- } catch (error) {
241
- this.#emitError(error);
967
+ if (!this.#transport.send(JSON.stringify(payload)))
242
968
  return false;
243
- }
969
+ this.#sendBudget.record(now);
970
+ return true;
244
971
  }
245
972
  setPresence(data) {
246
973
  this.#options.presence = { ...this.#options.presence, ...data };
@@ -276,127 +1003,80 @@ class Gateway {
276
1003
  #open(url) {
277
1004
  if (this.#closed)
278
1005
  return;
279
- let ws;
280
- try {
281
- ws = new WebSocket(url);
282
- } catch (error) {
283
- const failure = this.#normalizeError(error);
284
- this.#emitError(failure);
285
- if (this.#options.reconnect) {
286
- this.#setState("RECONNECT" /* Reconnect */);
287
- this.#scheduleReconnect();
288
- } else
289
- this.#settleConnect(failure);
1006
+ this.#token = this.#session.beginConnection();
1007
+ const result = this.#transport.connect(url);
1008
+ if (result.ok)
290
1009
  return;
1010
+ const failure = this.#normalizeError(result.error);
1011
+ this.#emitError(failure);
1012
+ if (this.#options.reconnect) {
1013
+ this.#setState("RECONNECT" /* Reconnect */);
1014
+ this.#scheduleReconnect();
1015
+ } else {
1016
+ this.#settleConnect(failure);
291
1017
  }
292
- this.#ws = ws;
293
- ws.addEventListener("open", () => {
294
- this.#lastMessageAt = Date.now();
295
- this.#zombieReported = false;
296
- this.#startZombieDetection();
297
- this.#emit("open", undefined);
298
- this.#settleConnect();
299
- });
300
- ws.addEventListener("message", (event) => {
301
- this.#lastMessageAt = Date.now();
302
- this.#zombieReported = false;
303
- if (this.#options.compress && event.data instanceof ArrayBuffer) {
304
- this.#decompress(new Uint8Array(event.data)).then((text) => this.#message(ws, text)).catch((err) => this.#emitError(err));
305
- } else {
306
- this.#message(ws, String(event.data));
307
- }
308
- });
309
- ws.addEventListener("close", (event) => this.#close(ws, event.code));
310
- ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
311
- }
312
- #startZombieDetection() {
313
- if (this.#zombieTimer)
314
- clearInterval(this.#zombieTimer);
315
- const interval = Math.max(1, Math.min(1000, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
316
- this.#zombieTimer = setInterval(() => {
317
- if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
318
- return;
319
- const silentFor = Date.now() - this.#lastMessageAt;
320
- const deadline = Math.max(this.#options.zombieTimeout, this.#heartbeatInterval + this.#options.heartbeatAckTimeout);
321
- if (silentFor < deadline || this.#zombieReported)
322
- return;
323
- this.#zombieReported = true;
324
- const error = new GatewayError(`Gateway connection appears stale after ${silentFor}ms without traffic.`);
325
- this.#emit("zombie", { silentFor, timeout: deadline });
326
- this.#emitError(error);
327
- try {
328
- this.#ws.close(1001, "Zombie Gateway connection");
329
- } catch (closeError) {
330
- this.#emitError(closeError);
331
- }
332
- }, interval);
333
1018
  }
334
- #message(ws, raw) {
335
- let payload;
336
- try {
337
- payload = JSON.parse(raw);
338
- } catch (error) {
339
- this.#emitError(new GatewayError("Gateway returned invalid JSON", undefined, {
340
- cause: error
341
- }));
342
- ws.close(1002, "Invalid JSON");
343
- return;
344
- }
345
- if (!payload || typeof payload.op !== "number") {
346
- this.#emitError(new GatewayError("Gateway returned an invalid payload"));
347
- ws.close(1002, "Invalid payload");
1019
+ #onOpen() {
1020
+ this.#heartbeat.watch();
1021
+ this.#emit("open", undefined);
1022
+ this.#settleConnect();
1023
+ }
1024
+ #message(raw, token) {
1025
+ if (!this.#session.owns(token))
348
1026
  return;
349
- }
350
- if (typeof payload.s === "number")
351
- this.#sequence = payload.s;
352
- switch (payload.op) {
353
- case GatewayOpcodes.Dispatch:
1027
+ const { sequence, action } = classifyFrame(raw);
1028
+ if (sequence !== null)
1029
+ this.#session.recordSequence(sequence, token);
1030
+ this.#apply(action, token);
1031
+ }
1032
+ #apply(action, token) {
1033
+ switch (action.type) {
1034
+ case "dispatch":
354
1035
  this.#setState("DISPATCH" /* Dispatch */);
355
- this.#handleDispatch(payload.t, payload.d);
356
- break;
357
- case GatewayOpcodes.Hello:
358
- this.#handleHello(payload.d);
359
- break;
360
- case GatewayOpcodes.Heartbeat:
1036
+ this.#handleDispatch(action.event, action.data, token);
1037
+ return;
1038
+ case "hello":
1039
+ this.#handleHello(action.heartbeatInterval);
1040
+ return;
1041
+ case "heartbeat":
361
1042
  this.#setState("HEARTBEAT" /* Heartbeat */);
362
- this.#sendHeartbeat();
363
- break;
364
- case GatewayOpcodes.HeartbeatAck:
365
- this.#heartbeatACK = true;
366
- this.ping = Date.now() - this.#heartbeatSentAt;
367
- this.#clearHeartbeatAckTimer();
368
- this.#emit("heartbeatAck", payload.d);
369
- break;
370
- case GatewayOpcodes.Reconnect:
371
- ws.close(1001, "Server requested reconnect");
372
- break;
373
- case GatewayOpcodes.InvalidSession: {
374
- const resumable = payload.d === true;
375
- if (!resumable) {
376
- this.#sessionId = undefined;
377
- this.#sequence = null;
378
- this.#resumeURL = undefined;
379
- }
380
- this.#emit("invalidSession", resumable);
381
- ws.close(resumable ? GatewayCloseCodes.UnknownError : 1000, resumable ? "Invalid session (resumable)" : "Invalid session");
382
- break;
1043
+ this.#heartbeat.sendHeartbeat();
1044
+ return;
1045
+ case "heartbeat-ack":
1046
+ this.#heartbeat.acknowledge();
1047
+ this.ping = this.#heartbeat.latency;
1048
+ this.#emit("heartbeatAck", action.data);
1049
+ return;
1050
+ case "reconnect":
1051
+ this.#transport.close(RESUMABLE_CLOSE_CODE, "Server requested reconnect");
1052
+ return;
1053
+ case "invalid-session": {
1054
+ if (!action.resumable)
1055
+ this.#session.invalidate(token, "invalid-session");
1056
+ this.#emit("invalidSession", action.resumable);
1057
+ this.#transport.close(action.resumable ? GatewayCloseCodes.UnknownError : 1000, action.resumable ? "Invalid session (resumable)" : "Invalid session");
1058
+ return;
383
1059
  }
1060
+ case "invalid":
1061
+ this.#emitError(new GatewayError(action.message, undefined, action.cause === undefined ? undefined : { cause: action.cause }));
1062
+ this.#transport.close(1002, action.violation === "invalid-json" ? "Invalid JSON" : action.violation === "invalid-hello" ? "Invalid heartbeat interval" : "Invalid payload");
1063
+ return;
1064
+ case "unknown":
1065
+ return;
384
1066
  }
385
1067
  }
386
- #handleDispatch(event, data) {
1068
+ #handleDispatch(event, data, token) {
387
1069
  if (event === "READY") {
388
- const ready = data;
389
- if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string") {
390
- this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
1070
+ if (!this.#session.activate(data, token)) {
1071
+ if (this.#session.owns(token))
1072
+ this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
391
1073
  return;
392
1074
  }
393
- this.#sessionId = ready.session_id;
394
- this.#resumeURL = ready.resume_gateway_url;
395
- this.#attempt = 0;
1075
+ this.#reconnect.reset();
396
1076
  this.#setState("READY" /* Ready */);
397
1077
  this.#emit("ready", data);
398
1078
  } else if (event === "RESUMED") {
399
- this.#attempt = 0;
1079
+ this.#reconnect.reset();
400
1080
  this.#setState("READY" /* Ready */);
401
1081
  this.#emit("resumed", data);
402
1082
  }
@@ -405,183 +1085,60 @@ class Gateway {
405
1085
  return;
406
1086
  this.#emit(event ?? "dispatch", data);
407
1087
  }
408
- #handleHello(data) {
409
- const interval = data?.heartbeat_interval;
410
- if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0) {
411
- this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
412
- this.#ws?.close(1002, "Invalid heartbeat interval");
413
- return;
414
- }
415
- this.#heartbeatInterval = interval;
416
- this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
417
- this.#startHeartbeat(interval);
1088
+ #handleHello(interval) {
1089
+ this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
1090
+ this.#heartbeat.start(interval);
418
1091
  this.#identifyOrResume();
419
1092
  }
420
1093
  #identifyOrResume() {
421
- if (this.#sessionId && this.#sequence !== null && this.#resumeURL) {
1094
+ const intent = this.#session.handshake();
1095
+ if (intent.type === "resume") {
422
1096
  this.#setState("RESUME" /* Resume */);
423
- this.send({
424
- op: GatewayOpcodes.Resume,
425
- d: {
426
- token: this.#options.token,
427
- session_id: this.#sessionId,
428
- seq: this.#sequence
429
- },
430
- s: null,
431
- t: null
432
- });
1097
+ this.#dispatch(resumePayload(this.#options.token, intent), true);
433
1098
  return;
434
1099
  }
435
1100
  this.#setState("IDENTIFY" /* Identify */);
436
- const props = this.#options.properties ?? {
437
- os: "Android",
438
- browser: "Discord Android",
439
- device: "Discord Android"
440
- };
441
- const os = props.os ?? "Android";
442
- const browser = props.browser ?? "Discord Android";
443
- const device = props.device ?? "Discord Android";
444
- this.send({
445
- op: GatewayOpcodes.Identify,
446
- d: {
447
- token: this.#options.token,
448
- intents: this.#options.intents,
449
- properties: {
450
- os,
451
- browser,
452
- device,
453
- $os: os,
454
- $browser: browser,
455
- $device: device,
456
- ...props
457
- },
458
- presence: {
459
- since: this.#options.presence?.since ?? null,
460
- activities: this.#options.presence?.activities ?? [],
461
- status: this.#options.presence?.status ?? "online",
462
- afk: Boolean(this.#options.presence?.afk)
463
- },
464
- shard: [this.#options.shardId, this.#options.shardCount]
465
- },
466
- s: null,
467
- t: null
468
- });
469
- }
470
- #startHeartbeat(interval) {
471
- this.#clearHeartbeatTimers();
472
- this.#heartbeatACK = true;
473
- this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
474
- this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
475
- }
476
- #sendHeartbeat() {
477
- this.#heartbeatACK = false;
478
- this.#heartbeatSentAt = Date.now();
479
- if (!this.#dispatch({
480
- op: GatewayOpcodes.Heartbeat,
481
- d: this.#sequence,
482
- s: null,
483
- t: null
484
- }, true)) {
485
- this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
486
- return;
487
- }
488
- this.#clearHeartbeatAckTimer();
489
- this.#heartbeatAckTimer = setTimeout(() => {
490
- if (!this.#heartbeatACK) {
491
- const elapsed = Date.now() - this.#heartbeatSentAt;
492
- this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
493
- try {
494
- this.#ws?.close(1001, "Heartbeat timeout");
495
- } catch (error) {
496
- this.#emitError(error);
497
- }
498
- }
499
- }, this.#options.heartbeatAckTimeout);
1101
+ this.#dispatch(identifyPayload({
1102
+ token: this.#options.token,
1103
+ intents: this.#options.intents,
1104
+ shardId: this.#options.shardId,
1105
+ shardCount: this.#options.shardCount,
1106
+ properties: this.#options.properties,
1107
+ presence: this.#options.presence
1108
+ }), true);
500
1109
  }
501
- #close(ws, code) {
502
- if (this.#ws !== ws)
503
- return;
504
- this.#ws = undefined;
1110
+ #close(code, token) {
505
1111
  this.#clearTimers();
506
- const action = this.#closeAction(code);
1112
+ const action = this.#reconnect.classifyClose(code, this.#session.canResume);
507
1113
  this.#emit("close", { code, action });
508
- if (this.#closed || !this.#options.reconnect || action === "stop") {
1114
+ if (this.#closed || !this.#reconnect.enabled || action === "stop") {
1115
+ if (action === "stop")
1116
+ this.#closed = true;
509
1117
  this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
510
- if (this.#connectPromise)
511
- this.#settleConnect(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
1118
+ if (this.#reconnect.connecting)
1119
+ this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
512
1120
  return;
513
1121
  }
514
- if (action === "identify") {
515
- this.#sequence = null;
516
- this.#sessionId = undefined;
517
- this.#resumeURL = undefined;
518
- }
1122
+ if (action === "identify")
1123
+ this.#session.invalidate(token, "session-timeout");
519
1124
  this.#setState("RECONNECT" /* Reconnect */);
520
1125
  this.#scheduleReconnect();
521
1126
  }
522
- #closeAction(code) {
523
- const fatal = [
524
- GatewayCloseCodes.AuthenticationFailed,
525
- GatewayCloseCodes.InvalidShard,
526
- GatewayCloseCodes.ShardingRequired,
527
- GatewayCloseCodes.InvalidAPIVersion,
528
- GatewayCloseCodes.InvalidIntents,
529
- GatewayCloseCodes.DisallowedIntents
530
- ];
531
- if (fatal.includes(code))
532
- return "stop";
533
- if (code === GatewayCloseCodes.InvalidSeq || code === GatewayCloseCodes.SessionTimedOut)
534
- return "identify";
535
- return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
536
- }
537
1127
  #scheduleReconnect() {
538
- if (this.#closed || !this.#options.reconnect || this.#reconnectTimer)
1128
+ if (this.#closed)
539
1129
  return;
540
- if (this.#attempt >= this.#options.maxReconnectAttempts) {
541
- this.#settleConnect(new GatewayError("Gateway reconnect attempts exhausted."));
542
- this.#setState("CONNECT" /* Connect */);
1130
+ const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
1131
+ if (result.scheduled || result.reason !== "exhausted")
543
1132
  return;
544
- }
545
- const delay = Math.min(this.#options.reconnectMaxDelay, this.#options.reconnectBaseDelay * 2 ** this.#attempt++);
546
- const jitter = Math.random() * Math.max(1, delay * 0.25);
547
- this.#reconnectTimer = setTimeout(() => {
548
- this.#reconnectTimer = undefined;
549
- this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
550
- }, delay + jitter);
1133
+ this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
1134
+ this.#setState("CONNECT" /* Connect */);
551
1135
  }
552
1136
  #settleConnect(error) {
553
- const resolve = this.#resolveConnect;
554
- const reject = this.#rejectConnect;
555
- this.#resolveConnect = undefined;
556
- this.#rejectConnect = undefined;
557
- this.#connectPromise = undefined;
558
- if (error)
559
- reject?.(error);
560
- else
561
- resolve?.();
562
- }
563
- #clearHeartbeatAckTimer() {
564
- if (this.#heartbeatAckTimer)
565
- clearTimeout(this.#heartbeatAckTimer);
566
- this.#heartbeatAckTimer = undefined;
567
- }
568
- #clearHeartbeatTimers() {
569
- if (this.#heartbeatTimer)
570
- clearInterval(this.#heartbeatTimer);
571
- if (this.#initialHeartbeat)
572
- clearTimeout(this.#initialHeartbeat);
573
- this.#clearHeartbeatAckTimer();
574
- this.#heartbeatTimer = undefined;
575
- this.#initialHeartbeat = undefined;
1137
+ this.#reconnect.settle(error);
576
1138
  }
577
1139
  #clearTimers() {
578
- if (this.#reconnectTimer)
579
- clearTimeout(this.#reconnectTimer);
580
- if (this.#zombieTimer)
581
- clearInterval(this.#zombieTimer);
582
- this.#reconnectTimer = undefined;
583
- this.#zombieTimer = undefined;
584
- this.#clearHeartbeatTimers();
1140
+ this.#reconnect.cancel();
1141
+ this.#heartbeat.stop();
585
1142
  }
586
1143
  #normalizeError(error) {
587
1144
  return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
@@ -613,47 +1170,20 @@ class Gateway {
613
1170
  this.state = next;
614
1171
  this.#emit("stateChange", { previous, next });
615
1172
  }
616
- #initDecompressor() {
617
- const ds = new DecompressionStream("deflate-raw");
618
- this.#decompressor = {
619
- writer: ds.writable.getWriter(),
620
- reader: ds.readable.getReader()
621
- };
622
- }
623
- async#decompress(chunk) {
624
- if (!this.#decompressor)
625
- this.#initDecompressor();
626
- const { writer, reader } = this.#decompressor;
627
- await writer.write(chunk);
628
- const parts = [];
629
- while (true) {
630
- const { done, value } = await Promise.race([
631
- reader.read(),
632
- new Promise((resolve) => setTimeout(() => resolve({ done: true, value: undefined }), 50))
633
- ]);
634
- if (done || !value)
635
- break;
636
- parts.push(value);
637
- }
638
- const total = parts.reduce((n, p) => n + p.length, 0);
639
- const merged = new Uint8Array(total);
640
- let offset = 0;
641
- for (const p of parts) {
642
- merged.set(p, offset);
643
- offset += p.length;
644
- }
645
- return new TextDecoder().decode(merged);
646
- }
647
1173
  }
648
1174
 
649
1175
  // packages/sharding/src/bus.ts
1176
+ var REPLY_TYPE = "__lunibee:reply";
1177
+
650
1178
  class ShardBus {
651
1179
  #channel;
652
1180
  #handlers = new Map;
1181
+ #errorHandlers = new Set;
653
1182
  #shardId;
654
1183
  #namespace;
655
1184
  channelName;
656
1185
  #counter = 0;
1186
+ #pending = new Map;
657
1187
  constructor(shardId, channelName) {
658
1188
  if (!Number.isInteger(shardId) || shardId < 0)
659
1189
  throw new RangeError("Shard ID must be a non-negative integer.");
@@ -678,6 +1208,16 @@ class ShardBus {
678
1208
  this.#handlers.get(type)?.delete(handler);
679
1209
  return this;
680
1210
  }
1211
+ onError(handler) {
1212
+ if (typeof handler !== "function")
1213
+ throw new TypeError("Shard bus error handler is required.");
1214
+ this.#errorHandlers.add(handler);
1215
+ return this;
1216
+ }
1217
+ offError(handler) {
1218
+ this.#errorHandlers.delete(handler);
1219
+ return this;
1220
+ }
681
1221
  send(target, type, data) {
682
1222
  if (!Number.isInteger(target) || target < 0)
683
1223
  throw new RangeError("Shard target must be a non-negative integer.");
@@ -686,11 +1226,56 @@ class ShardBus {
686
1226
  broadcast(type, data) {
687
1227
  return this.#publish(null, type, data);
688
1228
  }
1229
+ respond(type, handler) {
1230
+ return this.on(type, async (message) => {
1231
+ if (!message.expectsReply)
1232
+ return;
1233
+ const reply = { requestId: message.id };
1234
+ try {
1235
+ reply.result = await handler(message.data, message);
1236
+ } catch (error) {
1237
+ reply.error = error instanceof Error ? error.message : String(error);
1238
+ }
1239
+ this.#publish(message.source, REPLY_TYPE, reply);
1240
+ });
1241
+ }
1242
+ async request(target, type, data, timeoutMs = 5000) {
1243
+ if (!Number.isInteger(target) || target < 0)
1244
+ throw new RangeError("Shard target must be a non-negative integer.");
1245
+ const [reply] = await this.#collect(target, type, data, timeoutMs, 1);
1246
+ if (!reply)
1247
+ throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
1248
+ if (reply.error !== undefined)
1249
+ throw new Error(reply.error);
1250
+ return reply.result;
1251
+ }
1252
+ broadcastRequest(type, data, options = {}) {
1253
+ return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
1254
+ }
1255
+ #collect(target, type, data, timeoutMs, expected) {
1256
+ const replies = [];
1257
+ return new Promise((resolve) => {
1258
+ let requestId = "";
1259
+ const finish = () => {
1260
+ clearTimeout(timer);
1261
+ this.#pending.delete(requestId);
1262
+ resolve(replies);
1263
+ };
1264
+ const timer = setTimeout(finish, timeoutMs);
1265
+ requestId = this.#publish(target, type, data, true);
1266
+ this.#pending.set(requestId, (reply) => {
1267
+ replies.push(reply);
1268
+ if (replies.length >= expected)
1269
+ finish();
1270
+ });
1271
+ });
1272
+ }
689
1273
  close() {
690
1274
  this.#channel.close();
691
1275
  this.#handlers.clear();
1276
+ this.#errorHandlers.clear();
692
1277
  }
693
- #publish(target, type, data) {
1278
+ #publish(target, type, data, expectsReply = false) {
694
1279
  if (!type.trim())
695
1280
  throw new TypeError("Shard message type is required.");
696
1281
  const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
@@ -699,7 +1284,8 @@ class ShardBus {
699
1284
  target,
700
1285
  type,
701
1286
  data,
702
- id
1287
+ id,
1288
+ ...expectsReply ? { expectsReply } : {}
703
1289
  });
704
1290
  return id;
705
1291
  }
@@ -708,13 +1294,28 @@ class ShardBus {
708
1294
  return;
709
1295
  if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
710
1296
  return;
1297
+ if (message.type === REPLY_TYPE) {
1298
+ const reply = message.data;
1299
+ this.#pending.get(reply?.requestId)?.({
1300
+ shardId: message.source,
1301
+ ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
1302
+ });
1303
+ return;
1304
+ }
711
1305
  for (const handler of this.#handlers.get(message.type) ?? []) {
712
1306
  try {
713
1307
  const result = handler(message);
714
1308
  if (result && typeof result.then === "function")
715
- Promise.resolve(result).catch(() => {
716
- return;
717
- });
1309
+ Promise.resolve(result).catch((error) => this.#reportError(error, message));
1310
+ } catch (error) {
1311
+ this.#reportError(error, message);
1312
+ }
1313
+ }
1314
+ }
1315
+ #reportError(error, message) {
1316
+ for (const handler of this.#errorHandlers) {
1317
+ try {
1318
+ handler(error, message);
718
1319
  } catch {}
719
1320
  }
720
1321
  }
@@ -731,6 +1332,8 @@ class ClusterManager {
731
1332
  #auto;
732
1333
  #autoScaleTimer;
733
1334
  #spawned = false;
1335
+ #stopping = false;
1336
+ #restartTimers = new Set;
734
1337
  constructor(options) {
735
1338
  if (!options.token?.trim())
736
1339
  throw new TypeError("A Discord bot token is required.");
@@ -746,7 +1349,7 @@ class ClusterManager {
746
1349
  const response = await fetch("https://discord.com/api/v10/gateway/bot", {
747
1350
  headers: {
748
1351
  Authorization: `Bot ${this.#options.token}`,
749
- "User-Agent": "Lunibee/0.1.0"
1352
+ "User-Agent": `Lunibee/${package_default.version}`
750
1353
  }
751
1354
  });
752
1355
  if (!response.ok) {
@@ -775,19 +1378,7 @@ class ClusterManager {
775
1378
  for (let i = 0;i < clusterCount; i++) {
776
1379
  if (chunks[i].length === 0)
777
1380
  continue;
778
- const child = fork(this.#options.script, [], {
779
- env: {
780
- ...process.env,
781
- SHARD_LIST: chunks[i].join(","),
782
- SHARD_COUNT: count.toString(),
783
- CLUSTER_ID: i.toString()
784
- }
785
- });
786
- this.clusters.set(i, {
787
- id: i,
788
- process: child,
789
- shards: chunks[i]
790
- });
1381
+ this.#launch(i, chunks[i], count);
791
1382
  await sleep(500);
792
1383
  }
793
1384
  if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
@@ -796,6 +1387,37 @@ class ClusterManager {
796
1387
  }, this.#options.autoScaleInterval);
797
1388
  }
798
1389
  }
1390
+ #launch(id, shards, shardCount) {
1391
+ const child = fork(this.#options.script, [], {
1392
+ env: {
1393
+ ...process.env,
1394
+ SHARD_LIST: shards.join(","),
1395
+ SHARD_COUNT: shardCount.toString(),
1396
+ CLUSTER_ID: id.toString()
1397
+ }
1398
+ });
1399
+ const info = { id, process: child, shards };
1400
+ this.clusters.set(id, info);
1401
+ child.once("exit", (code, signal) => {
1402
+ if (this.clusters.get(id)?.process !== child)
1403
+ return;
1404
+ try {
1405
+ this.#options.onClusterExit?.(info, code, signal);
1406
+ } catch {}
1407
+ if (this.#stopping || this.#options.restartOnExit === false)
1408
+ return;
1409
+ this.clusters.delete(id);
1410
+ const timer = setTimeout(() => {
1411
+ this.#restartTimers.delete(timer);
1412
+ if (this.#stopping || !this.#spawned)
1413
+ return;
1414
+ this.#launch(id, shards, shardCount);
1415
+ }, this.#options.restartDelay ?? 5000);
1416
+ timer.unref?.();
1417
+ this.#restartTimers.add(timer);
1418
+ });
1419
+ return info;
1420
+ }
799
1421
  async checkAutoScale() {
800
1422
  if (!this.#auto)
801
1423
  return;
@@ -816,6 +1438,8 @@ class ClusterManager {
816
1438
  await this.spawn();
817
1439
  }
818
1440
  async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
1441
+ this.#stopping = true;
1442
+ this.#clearRestartTimers();
819
1443
  if (this.#autoScaleTimer) {
820
1444
  clearInterval(this.#autoScaleTimer);
821
1445
  this.#autoScaleTimer = undefined;
@@ -823,6 +1447,12 @@ class ClusterManager {
823
1447
  await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
824
1448
  this.clusters.clear();
825
1449
  this.#spawned = false;
1450
+ this.#stopping = false;
1451
+ }
1452
+ #clearRestartTimers() {
1453
+ for (const timer of this.#restartTimers)
1454
+ clearTimeout(timer);
1455
+ this.#restartTimers.clear();
826
1456
  }
827
1457
  async#shutdownCluster(cluster, timeoutMs) {
828
1458
  const child = cluster.process;
@@ -851,6 +1481,8 @@ class ClusterManager {
851
1481
  });
852
1482
  }
853
1483
  killAll() {
1484
+ this.#stopping = true;
1485
+ this.#clearRestartTimers();
854
1486
  if (this.#autoScaleTimer) {
855
1487
  clearInterval(this.#autoScaleTimer);
856
1488
  this.#autoScaleTimer = undefined;
@@ -862,13 +1494,43 @@ class ClusterManager {
862
1494
  }
863
1495
  this.clusters.clear();
864
1496
  this.#spawned = false;
1497
+ this.#stopping = false;
865
1498
  }
866
1499
  }
867
1500
 
868
1501
  // packages/sharding/src/index.ts
869
1502
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1503
+ function needsConnection(gateway) {
1504
+ return gateway.state === "CONNECT" /* Connect */ || gateway.state === "RECONNECT" /* Reconnect */ || gateway.state === "CLOSED" /* Closed */;
1505
+ }
1506
+ var HANDSHAKE_SENT = new Set([
1507
+ "IDENTIFY" /* Identify */,
1508
+ "RESUME" /* Resume */,
1509
+ "READY" /* Ready */,
1510
+ "DISPATCH" /* Dispatch */,
1511
+ "HEARTBEAT" /* Heartbeat */,
1512
+ "CLOSED" /* Closed */
1513
+ ]);
1514
+ function handshakeSent(gateway, timeout) {
1515
+ if (HANDSHAKE_SENT.has(gateway.state))
1516
+ return Promise.resolve();
1517
+ return new Promise((resolve) => {
1518
+ const done = () => {
1519
+ clearTimeout(timer);
1520
+ gateway.off("stateChange", onState);
1521
+ resolve();
1522
+ };
1523
+ const onState = (change) => {
1524
+ if (HANDSHAKE_SENT.has(change.next))
1525
+ done();
1526
+ };
1527
+ const timer = setTimeout(done, timeout);
1528
+ gateway.on("stateChange", onState);
1529
+ });
1530
+ }
870
1531
 
871
1532
  class ShardManager {
1533
+ static IDENTIFY_INTERVAL = 5000;
872
1534
  shards = new Map;
873
1535
  get shardCount() {
874
1536
  return this.shards.size;
@@ -878,6 +1540,7 @@ class ShardManager {
878
1540
  #destroyed = false;
879
1541
  #auto;
880
1542
  #autoScaleTimer;
1543
+ #gatewayInfo;
881
1544
  constructor(options) {
882
1545
  if (!options.token?.trim())
883
1546
  throw new TypeError("A Discord bot token is required.");
@@ -888,16 +1551,28 @@ class ShardManager {
888
1551
  throw new RangeError("Shard count must be a positive integer.");
889
1552
  if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
890
1553
  throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
1554
+ if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
1555
+ throw new RangeError("spawnDelay must be a non-negative finite number of milliseconds.");
1556
+ if (options.maxConcurrency !== undefined && (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1))
1557
+ throw new RangeError("maxConcurrency must be a positive integer.");
1558
+ if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
1559
+ throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
891
1560
  this.#auto = options.shardCount === "auto";
892
- this.#options = { ...options };
1561
+ this.#options = {
1562
+ spawnDelay: ShardManager.IDENTIFY_INTERVAL,
1563
+ ...options
1564
+ };
893
1565
  if (options.shardCount !== "auto")
894
1566
  this.#initialize(count);
895
1567
  }
896
1568
  async fetchRecommendedShardCount() {
1569
+ return (await this.fetchGatewayInfo()).shards;
1570
+ }
1571
+ async fetchGatewayInfo() {
897
1572
  const response = await fetch("https://discord.com/api/v10/gateway/bot", {
898
1573
  headers: {
899
1574
  Authorization: `Bot ${this.#options.token}`,
900
- "User-Agent": "Lunibee/0.1.0"
1575
+ "User-Agent": `Lunibee/${package_default.version}`
901
1576
  }
902
1577
  });
903
1578
  if (!response.ok)
@@ -905,7 +1580,17 @@ class ShardManager {
905
1580
  const data = await response.json();
906
1581
  if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
907
1582
  throw new Error("Gateway discovery returned an invalid shard count.");
908
- return data.shards;
1583
+ const limit = data.session_start_limit;
1584
+ const info = { shards: data.shards };
1585
+ if (limit && typeof limit.total === "number" && typeof limit.remaining === "number" && typeof limit.reset_after === "number" && typeof limit.max_concurrency === "number")
1586
+ info.sessionStartLimit = {
1587
+ total: limit.total,
1588
+ remaining: limit.remaining,
1589
+ resetAfter: limit.reset_after,
1590
+ maxConcurrency: Math.max(1, limit.max_concurrency)
1591
+ };
1592
+ this.#gatewayInfo = info;
1593
+ return info;
909
1594
  }
910
1595
  async connect() {
911
1596
  if (this.#autoScaleTimer) {
@@ -913,17 +1598,25 @@ class ShardManager {
913
1598
  this.#autoScaleTimer = undefined;
914
1599
  }
915
1600
  await this.#ensureInitialized();
916
- for (const [id, shard] of this.shards) {
917
- try {
918
- await shard.connect();
919
- } catch (error) {
1601
+ const limit = this.#gatewayInfo?.sessionStartLimit;
1602
+ const ids = [...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id);
1603
+ if (limit && limit.remaining < ids.length)
1604
+ throw new Error(`Session start limit exhausted: ${limit.remaining} of ${limit.total} IDENTIFYs left for ${ids.length} shards; resets in ${Math.ceil(limit.resetAfter / 1000)}s.`);
1605
+ const concurrency = Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
1606
+ for (let start = 0;start < ids.length; start += concurrency) {
1607
+ const round = ids.slice(start, start + concurrency);
1608
+ const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
1609
+ const failed = results.findIndex((result) => result.status === "rejected");
1610
+ if (failed !== -1) {
920
1611
  this.destroy();
921
- throw new Error(`Failed to connect shard ${id}.`, {
922
- cause: error
1612
+ throw new Error(`Failed to connect shard ${round[failed]}.`, {
1613
+ cause: results[failed].reason
923
1614
  });
924
1615
  }
925
- if (id + 1 < this.shardCount && this.#options.spawnDelay && this.#options.spawnDelay > 0)
1616
+ if (start + concurrency < ids.length && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
1617
+ await Promise.all(round.map((id) => handshakeSent(this.shards.get(id), this.#options.handshakeTimeout ?? 15000)));
926
1618
  await sleep2(this.#options.spawnDelay);
1619
+ }
927
1620
  }
928
1621
  if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
929
1622
  this.#autoScaleTimer = setInterval(() => {
@@ -966,6 +1659,13 @@ class ShardManager {
966
1659
  get(id) {
967
1660
  return this.shards.get(id);
968
1661
  }
1662
+ health() {
1663
+ return [...this.shards].map(([id, gateway]) => ({
1664
+ id,
1665
+ state: gateway.state,
1666
+ ping: gateway.ping
1667
+ }));
1668
+ }
969
1669
  values() {
970
1670
  return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
971
1671
  }
@@ -997,7 +1697,8 @@ class ShardManager {
997
1697
  export {
998
1698
  ClusterManager,
999
1699
  ShardBus,
1000
- ShardManager
1700
+ ShardManager,
1701
+ ShardManager as ShardingManager
1001
1702
  };
1002
1703
 
1003
- //# debugId=F2AF359ADCA0121464756E2164756E21
1704
+ //# debugId=80E4CAFE56ABED2364756E2164756E21