lunibee 0.1.7 → 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 (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +223 -223
  3. package/dist/builders/commands.d.ts +6 -4
  4. package/dist/builders/components.d.ts +18 -0
  5. package/dist/builders/index.d.ts +2 -2
  6. package/dist/builders/index.js +49 -13
  7. package/dist/builders/index.js.map +9 -8
  8. package/dist/collection/index.js +2 -1
  9. package/dist/collection/index.js.map +4 -4
  10. package/dist/core/events.d.ts +86 -2
  11. package/dist/core/index.d.ts +10 -87
  12. package/dist/core/index.js +1752 -725
  13. package/dist/core/index.js.map +44 -24
  14. package/dist/formatters/index.js.map +1 -1
  15. package/dist/handlers/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +2103 -800
  18. package/dist/index.js.map +63 -43
  19. package/dist/managers/guild-resources.d.ts +109 -0
  20. package/dist/managers/guild.d.ts +5 -0
  21. package/dist/managers/index.d.ts +7 -1
  22. package/dist/managers/index.js +330 -12
  23. package/dist/managers/index.js.map +25 -21
  24. package/dist/rest/decoder.d.ts +23 -0
  25. package/dist/rest/errors.d.ts +20 -0
  26. package/dist/rest/index.d.ts +21 -17
  27. package/dist/rest/index.js +479 -263
  28. package/dist/rest/index.js.map +14 -8
  29. package/dist/rest/limiter.d.ts +42 -0
  30. package/dist/rest/redis.d.ts +18 -1
  31. package/dist/rest/route.d.ts +31 -0
  32. package/dist/rest/routes.d.ts +2 -0
  33. package/dist/rest/scheduler.d.ts +42 -0
  34. package/dist/rest/store.d.ts +29 -0
  35. package/dist/rest/transport.d.ts +40 -0
  36. package/dist/sharding/bus.d.ts +31 -0
  37. package/dist/sharding/cluster.d.ts +10 -0
  38. package/dist/sharding/index.d.ts +10 -2
  39. package/dist/sharding/index.js +1011 -394
  40. package/dist/sharding/index.js.map +18 -8
  41. package/dist/structures/base.d.ts +8 -0
  42. package/dist/structures/channels.d.ts +56 -0
  43. package/dist/structures/index.d.ts +1 -0
  44. package/dist/structures/index.js +211 -271
  45. package/dist/structures/index.js.map +17 -14
  46. package/dist/structures/interactions.d.ts +12 -52
  47. package/dist/structures/options.d.ts +55 -0
  48. package/dist/structures/resources.d.ts +3 -3
  49. package/dist/types/gateway-events.d.ts +132 -0
  50. package/dist/types/gateway.d.ts +181 -0
  51. package/dist/types/index.d.ts +2 -263
  52. package/dist/types/index.js +44 -2
  53. package/dist/types/index.js.map +5 -4
  54. package/dist/utils/index.js.map +1 -1
  55. package/dist/voice/index.d.ts +29 -1
  56. package/dist/voice/index.js +59 -7
  57. package/dist/voice/index.js.map +3 -3
  58. package/dist/ws/close-codes.d.ts +23 -0
  59. package/dist/ws/decoder.d.ts +58 -0
  60. package/dist/ws/heartbeat.d.ts +87 -0
  61. package/dist/ws/index.d.ts +18 -65
  62. package/dist/ws/index.js +903 -385
  63. package/dist/ws/index.js.map +15 -5
  64. package/dist/ws/opcodes.d.ts +14 -0
  65. package/dist/ws/protocol.d.ts +110 -0
  66. package/dist/ws/reconnect.d.ts +108 -0
  67. package/dist/ws/send-budget.d.ts +17 -0
  68. package/dist/ws/session.d.ts +100 -0
  69. package/dist/ws/state.d.ts +27 -0
  70. package/dist/ws/transport.d.ts +80 -0
  71. package/package.json +21 -20
@@ -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,37 +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
- };
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
- };
775
+ // packages/ws/src/state.ts
107
776
  class GatewayError extends Error {
108
777
  code;
109
778
  constructor(message, code, options) {
@@ -112,33 +781,43 @@ class GatewayError extends Error {
112
781
  this.code = code;
113
782
  }
114
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";
115
808
 
116
809
  class Gateway {
117
810
  state = "CONNECT" /* Connect */;
118
811
  #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;
812
+ #transport;
813
+ #session = new GatewaySession;
814
+ #token = 0;
815
+ #heartbeat;
132
816
  #closed = false;
133
- #attempt = 0;
134
- #reconnectTimer;
135
- #connectPromise;
136
- #resolveConnect;
137
- #rejectConnect;
817
+ #reconnect;
138
818
  #listeners = new Map;
139
- #sendTimestamps = [];
819
+ #sendBudget = new SendBudget;
140
820
  ping = -1;
141
- #decompressor;
142
821
  constructor(options) {
143
822
  if (!options.token?.trim())
144
823
  throw new TypeError("A Gateway token is required.");
@@ -165,41 +844,72 @@ class Gateway {
165
844
  afk: false,
166
845
  since: null
167
846
  },
168
- ...options
847
+ ...options,
848
+ intents: resolvedIntents
169
849
  };
170
850
  if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
171
851
  throw new RangeError("Gateway shardId must be within the configured shard count.");
172
852
  if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
173
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
+ });
881
+ }
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");
174
893
  }
175
- connect(url = "wss://gateway.discord.gg/?v=10&encoding=json") {
894
+ connect(url = DEFAULT_GATEWAY_URL) {
176
895
  if (this.state === "CLOSED" /* Closed */)
177
896
  throw new GatewayError("Gateway has been permanently closed.");
178
- if (this.#connectPromise)
179
- 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();
180
903
  this.#closed = false;
181
904
  this.#setState("CONNECT" /* Connect */);
182
905
  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;
906
+ return this.#reconnect.attempt(() => this.#open(connectURL));
191
907
  }
192
908
  close() {
193
909
  this.#closed = true;
194
910
  this.#clearTimers();
195
911
  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
- }
912
+ this.#transport.destroy(1000, "Client closed connection");
203
913
  this.#setState("CLOSED" /* Closed */);
204
914
  }
205
915
  emit(event, data) {
@@ -224,23 +934,17 @@ class Gateway {
224
934
  return this.#dispatch(payload, false);
225
935
  }
226
936
  #dispatch(payload, privileged) {
227
- if (this.#ws?.readyState !== WebSocket.OPEN)
937
+ if (!this.#transport.connected)
228
938
  return false;
229
939
  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) {
940
+ if (!this.#sendBudget.allows(privileged, now)) {
233
941
  this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
234
942
  return false;
235
943
  }
236
- try {
237
- this.#ws.send(JSON.stringify(payload));
238
- this.#sendTimestamps.push(now);
239
- return true;
240
- } catch (error) {
241
- this.#emitError(error);
944
+ if (!this.#transport.send(JSON.stringify(payload)))
242
945
  return false;
243
- }
946
+ this.#sendBudget.record(now);
947
+ return true;
244
948
  }
245
949
  setPresence(data) {
246
950
  this.#options.presence = { ...this.#options.presence, ...data };
@@ -276,127 +980,80 @@ class Gateway {
276
980
  #open(url) {
277
981
  if (this.#closed)
278
982
  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);
983
+ this.#token = this.#session.beginConnection();
984
+ const result = this.#transport.connect(url);
985
+ if (result.ok)
290
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);
291
994
  }
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
995
  }
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");
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))
348
1003
  return;
349
- }
350
- if (typeof payload.s === "number")
351
- this.#sequence = payload.s;
352
- switch (payload.op) {
353
- 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":
354
1012
  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:
1013
+ this.#handleDispatch(action.event, action.data, token);
1014
+ return;
1015
+ case "hello":
1016
+ this.#handleHello(action.heartbeatInterval);
1017
+ return;
1018
+ case "heartbeat":
361
1019
  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;
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;
383
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;
384
1043
  }
385
1044
  }
386
- #handleDispatch(event, data) {
1045
+ #handleDispatch(event, data, token) {
387
1046
  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"));
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"));
391
1050
  return;
392
1051
  }
393
- this.#sessionId = ready.session_id;
394
- this.#resumeURL = ready.resume_gateway_url;
395
- this.#attempt = 0;
1052
+ this.#reconnect.reset();
396
1053
  this.#setState("READY" /* Ready */);
397
1054
  this.#emit("ready", data);
398
1055
  } else if (event === "RESUMED") {
399
- this.#attempt = 0;
1056
+ this.#reconnect.reset();
400
1057
  this.#setState("READY" /* Ready */);
401
1058
  this.#emit("resumed", data);
402
1059
  }
@@ -405,183 +1062,60 @@ class Gateway {
405
1062
  return;
406
1063
  this.#emit(event ?? "dispatch", data);
407
1064
  }
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);
1065
+ #handleHello(interval) {
1066
+ this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
1067
+ this.#heartbeat.start(interval);
418
1068
  this.#identifyOrResume();
419
1069
  }
420
1070
  #identifyOrResume() {
421
- if (this.#sessionId && this.#sequence !== null && this.#resumeURL) {
1071
+ const intent = this.#session.handshake();
1072
+ if (intent.type === "resume") {
422
1073
  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
- });
1074
+ this.#dispatch(resumePayload(this.#options.token, intent), true);
433
1075
  return;
434
1076
  }
435
1077
  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);
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);
500
1086
  }
501
- #close(ws, code) {
502
- if (this.#ws !== ws)
503
- return;
504
- this.#ws = undefined;
1087
+ #close(code, token) {
505
1088
  this.#clearTimers();
506
- const action = this.#closeAction(code);
1089
+ const action = this.#reconnect.classifyClose(code, this.#session.canResume);
507
1090
  this.#emit("close", { code, action });
508
- 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;
509
1094
  this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
510
- if (this.#connectPromise)
511
- 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));
512
1097
  return;
513
1098
  }
514
- if (action === "identify") {
515
- this.#sequence = null;
516
- this.#sessionId = undefined;
517
- this.#resumeURL = undefined;
518
- }
1099
+ if (action === "identify")
1100
+ this.#session.invalidate(token, "session-timeout");
519
1101
  this.#setState("RECONNECT" /* Reconnect */);
520
1102
  this.#scheduleReconnect();
521
1103
  }
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
1104
  #scheduleReconnect() {
538
- if (this.#closed || !this.#options.reconnect || this.#reconnectTimer)
1105
+ if (this.#closed)
539
1106
  return;
540
- if (this.#attempt >= this.#options.maxReconnectAttempts) {
541
- this.#settleConnect(new GatewayError("Gateway reconnect attempts exhausted."));
542
- 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")
543
1109
  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);
1110
+ this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
1111
+ this.#setState("CONNECT" /* Connect */);
551
1112
  }
552
1113
  #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;
1114
+ this.#reconnect.settle(error);
576
1115
  }
577
1116
  #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();
1117
+ this.#reconnect.cancel();
1118
+ this.#heartbeat.stop();
585
1119
  }
586
1120
  #normalizeError(error) {
587
1121
  return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
@@ -613,47 +1147,20 @@ class Gateway {
613
1147
  this.state = next;
614
1148
  this.#emit("stateChange", { previous, next });
615
1149
  }
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
1150
  }
648
1151
 
649
1152
  // packages/sharding/src/bus.ts
1153
+ var REPLY_TYPE = "__lunibee:reply";
1154
+
650
1155
  class ShardBus {
651
1156
  #channel;
652
1157
  #handlers = new Map;
1158
+ #errorHandlers = new Set;
653
1159
  #shardId;
654
1160
  #namespace;
655
1161
  channelName;
656
1162
  #counter = 0;
1163
+ #pending = new Map;
657
1164
  constructor(shardId, channelName) {
658
1165
  if (!Number.isInteger(shardId) || shardId < 0)
659
1166
  throw new RangeError("Shard ID must be a non-negative integer.");
@@ -678,6 +1185,16 @@ class ShardBus {
678
1185
  this.#handlers.get(type)?.delete(handler);
679
1186
  return this;
680
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
+ }
681
1198
  send(target, type, data) {
682
1199
  if (!Number.isInteger(target) || target < 0)
683
1200
  throw new RangeError("Shard target must be a non-negative integer.");
@@ -686,11 +1203,56 @@ class ShardBus {
686
1203
  broadcast(type, data) {
687
1204
  return this.#publish(null, type, data);
688
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
+ }
689
1250
  close() {
690
1251
  this.#channel.close();
691
1252
  this.#handlers.clear();
1253
+ this.#errorHandlers.clear();
692
1254
  }
693
- #publish(target, type, data) {
1255
+ #publish(target, type, data, expectsReply = false) {
694
1256
  if (!type.trim())
695
1257
  throw new TypeError("Shard message type is required.");
696
1258
  const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
@@ -699,7 +1261,8 @@ class ShardBus {
699
1261
  target,
700
1262
  type,
701
1263
  data,
702
- id
1264
+ id,
1265
+ ...expectsReply ? { expectsReply } : {}
703
1266
  });
704
1267
  return id;
705
1268
  }
@@ -708,13 +1271,28 @@ class ShardBus {
708
1271
  return;
709
1272
  if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
710
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
+ }
711
1282
  for (const handler of this.#handlers.get(message.type) ?? []) {
712
1283
  try {
713
1284
  const result = handler(message);
714
1285
  if (result && typeof result.then === "function")
715
- Promise.resolve(result).catch(() => {
716
- return;
717
- });
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);
718
1296
  } catch {}
719
1297
  }
720
1298
  }
@@ -731,6 +1309,8 @@ class ClusterManager {
731
1309
  #auto;
732
1310
  #autoScaleTimer;
733
1311
  #spawned = false;
1312
+ #stopping = false;
1313
+ #restartTimers = new Set;
734
1314
  constructor(options) {
735
1315
  if (!options.token?.trim())
736
1316
  throw new TypeError("A Discord bot token is required.");
@@ -775,19 +1355,7 @@ class ClusterManager {
775
1355
  for (let i = 0;i < clusterCount; i++) {
776
1356
  if (chunks[i].length === 0)
777
1357
  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
- });
1358
+ this.#launch(i, chunks[i], count);
791
1359
  await sleep(500);
792
1360
  }
793
1361
  if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
@@ -796,6 +1364,37 @@ class ClusterManager {
796
1364
  }, this.#options.autoScaleInterval);
797
1365
  }
798
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);
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
+ }
799
1398
  async checkAutoScale() {
800
1399
  if (!this.#auto)
801
1400
  return;
@@ -816,6 +1415,8 @@ class ClusterManager {
816
1415
  await this.spawn();
817
1416
  }
818
1417
  async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
1418
+ this.#stopping = true;
1419
+ this.#clearRestartTimers();
819
1420
  if (this.#autoScaleTimer) {
820
1421
  clearInterval(this.#autoScaleTimer);
821
1422
  this.#autoScaleTimer = undefined;
@@ -823,6 +1424,12 @@ class ClusterManager {
823
1424
  await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
824
1425
  this.clusters.clear();
825
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();
826
1433
  }
827
1434
  async#shutdownCluster(cluster, timeoutMs) {
828
1435
  const child = cluster.process;
@@ -851,6 +1458,8 @@ class ClusterManager {
851
1458
  });
852
1459
  }
853
1460
  killAll() {
1461
+ this.#stopping = true;
1462
+ this.#clearRestartTimers();
854
1463
  if (this.#autoScaleTimer) {
855
1464
  clearInterval(this.#autoScaleTimer);
856
1465
  this.#autoScaleTimer = undefined;
@@ -862,6 +1471,7 @@ class ClusterManager {
862
1471
  }
863
1472
  this.clusters.clear();
864
1473
  this.#spawned = false;
1474
+ this.#stopping = false;
865
1475
  }
866
1476
  }
867
1477
 
@@ -869,6 +1479,7 @@ class ClusterManager {
869
1479
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
870
1480
 
871
1481
  class ShardManager {
1482
+ static IDENTIFY_INTERVAL = 5000;
872
1483
  shards = new Map;
873
1484
  get shardCount() {
874
1485
  return this.shards.size;
@@ -888,8 +1499,13 @@ class ShardManager {
888
1499
  throw new RangeError("Shard count must be a positive integer.");
889
1500
  if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
890
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.");
891
1504
  this.#auto = options.shardCount === "auto";
892
- this.#options = { ...options };
1505
+ this.#options = {
1506
+ spawnDelay: ShardManager.IDENTIFY_INTERVAL,
1507
+ ...options
1508
+ };
893
1509
  if (options.shardCount !== "auto")
894
1510
  this.#initialize(count);
895
1511
  }
@@ -997,7 +1613,8 @@ class ShardManager {
997
1613
  export {
998
1614
  ClusterManager,
999
1615
  ShardBus,
1000
- ShardManager
1616
+ ShardManager,
1617
+ ShardManager as ShardingManager
1001
1618
  };
1002
1619
 
1003
- //# debugId=F2AF359ADCA0121464756E2164756E21
1620
+ //# debugId=C1062D33C7999C8E64756E2164756E21