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
package/dist/ws/index.js CHANGED
@@ -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,48 +772,18 @@ 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
  var GatewayState;
108
- ((GatewayState2) => {
109
- GatewayState2["Connect"] = "CONNECT";
110
- GatewayState2["Hello"] = "HELLO";
111
- GatewayState2["Identify"] = "IDENTIFY";
112
- GatewayState2["Resume"] = "RESUME";
113
- GatewayState2["Ready"] = "READY";
114
- GatewayState2["Dispatch"] = "DISPATCH";
115
- GatewayState2["Heartbeat"] = "HEARTBEAT";
116
- GatewayState2["Reconnect"] = "RECONNECT";
117
- GatewayState2["Closed"] = "CLOSED";
777
+ ((GatewayState) => {
778
+ GatewayState["Connect"] = "CONNECT";
779
+ GatewayState["Hello"] = "HELLO";
780
+ GatewayState["Identify"] = "IDENTIFY";
781
+ GatewayState["Resume"] = "RESUME";
782
+ GatewayState["Ready"] = "READY";
783
+ GatewayState["Dispatch"] = "DISPATCH";
784
+ GatewayState["Heartbeat"] = "HEARTBEAT";
785
+ GatewayState["Reconnect"] = "RECONNECT";
786
+ GatewayState["Closed"] = "CLOSED";
118
787
  })(GatewayState ||= {});
119
788
  class GatewayError extends Error {
120
789
  code;
@@ -124,33 +793,43 @@ class GatewayError extends Error {
124
793
  this.code = code;
125
794
  }
126
795
  }
796
+ // packages/ws/src/send-budget.ts
797
+ class SendBudget {
798
+ #timestamps = [];
799
+ #limit;
800
+ #windowMs;
801
+ constructor(limit = 115, windowMs = 60000) {
802
+ this.#limit = limit;
803
+ this.#windowMs = windowMs;
804
+ }
805
+ get used() {
806
+ return this.#timestamps.length;
807
+ }
808
+ allows(privileged, now = Date.now()) {
809
+ while (this.#timestamps.length && now - this.#timestamps[0] >= this.#windowMs)
810
+ this.#timestamps.shift();
811
+ return privileged || this.#timestamps.length < this.#limit;
812
+ }
813
+ record(now = Date.now()) {
814
+ this.#timestamps.push(now);
815
+ }
816
+ }
817
+
818
+ // packages/ws/src/index.ts
819
+ var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
127
820
 
128
821
  class Gateway {
129
822
  state = "CONNECT" /* Connect */;
130
823
  #options;
131
- #ws;
132
- #sequence = null;
133
- #sessionId;
134
- #resumeURL;
135
- #heartbeatTimer;
136
- #initialHeartbeat;
137
- #heartbeatAckTimer;
138
- #heartbeatACK = true;
139
- #heartbeatInterval = 0;
140
- #heartbeatSentAt = 0;
141
- #lastMessageAt = 0;
142
- #zombieTimer;
143
- #zombieReported = false;
824
+ #transport;
825
+ #session = new GatewaySession;
826
+ #token = 0;
827
+ #heartbeat;
144
828
  #closed = false;
145
- #attempt = 0;
146
- #reconnectTimer;
147
- #connectPromise;
148
- #resolveConnect;
149
- #rejectConnect;
829
+ #reconnect;
150
830
  #listeners = new Map;
151
- #sendTimestamps = [];
831
+ #sendBudget = new SendBudget;
152
832
  ping = -1;
153
- #decompressor;
154
833
  constructor(options) {
155
834
  if (!options.token?.trim())
156
835
  throw new TypeError("A Gateway token is required.");
@@ -177,41 +856,72 @@ class Gateway {
177
856
  afk: false,
178
857
  since: null
179
858
  },
180
- ...options
859
+ ...options,
860
+ intents: resolvedIntents
181
861
  };
182
862
  if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
183
863
  throw new RangeError("Gateway shardId must be within the configured shard count.");
184
864
  if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
185
865
  throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
866
+ this.#transport = new WebSocketTransport({
867
+ compress: this.#options.compress,
868
+ createSocket: options.createSocket,
869
+ handlers: {
870
+ onOpen: () => this.#onOpen(),
871
+ onFrame: (raw) => this.#message(raw, this.#token),
872
+ onClose: (code) => this.#close(code, this.#token),
873
+ onError: (error) => this.#emitError(error instanceof GatewayError ? error : new GatewayError(error.message, undefined, {
874
+ cause: error
875
+ }))
876
+ }
877
+ });
878
+ this.#reconnect = new GatewayReconnect({
879
+ enabled: this.#options.reconnect,
880
+ maxAttempts: this.#options.maxReconnectAttempts,
881
+ baseDelay: this.#options.reconnectBaseDelay,
882
+ maxDelay: this.#options.reconnectMaxDelay
883
+ });
884
+ this.#heartbeat = new GatewayHeartbeat({
885
+ ackTimeout: this.#options.heartbeatAckTimeout,
886
+ zombieTimeout: this.#options.zombieTimeout,
887
+ send: (sequence) => this.#dispatch(heartbeatPayload(sequence), true),
888
+ sequence: () => this.#session.sequence,
889
+ isConnected: () => !this.#closed && this.#transport.connected,
890
+ onTimeout: (timeout) => this.#onHeartbeatTimeout(timeout),
891
+ onError: (error) => this.#emitError(error)
892
+ });
186
893
  }
187
- connect(url = "wss://gateway.discord.gg/?v=10&encoding=json") {
894
+ #onHeartbeatTimeout(timeout) {
895
+ if (timeout.type === "zombie") {
896
+ this.#emit("zombie", {
897
+ silentFor: timeout.silentFor,
898
+ timeout: timeout.deadline
899
+ });
900
+ this.#emitError(new GatewayError(`Gateway connection appears stale after ${timeout.silentFor}ms without traffic.`));
901
+ } else {
902
+ this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${timeout.elapsedMs}ms.`));
903
+ }
904
+ this.#transport.close(1001, timeout.type === "zombie" ? "Zombie Gateway connection" : "Heartbeat timeout");
905
+ }
906
+ connect(url = DEFAULT_GATEWAY_URL) {
188
907
  if (this.state === "CLOSED" /* Closed */)
189
908
  throw new GatewayError("Gateway has been permanently closed.");
190
- if (this.#connectPromise)
191
- return this.#connectPromise;
909
+ const inFlight = this.#reconnect.attemptPromise;
910
+ if (inFlight)
911
+ return inFlight;
912
+ if (this.#transport.live)
913
+ return Promise.resolve();
914
+ this.#reconnect.cancel();
192
915
  this.#closed = false;
193
916
  this.#setState("CONNECT" /* Connect */);
194
917
  const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
195
- if (this.#options.compress)
196
- this.#initDecompressor();
197
- this.#connectPromise = new Promise((resolve, reject) => {
198
- this.#resolveConnect = resolve;
199
- this.#rejectConnect = reject;
200
- this.#open(connectURL);
201
- });
202
- return this.#connectPromise;
918
+ return this.#reconnect.attempt(() => this.#open(connectURL));
203
919
  }
204
920
  close() {
205
921
  this.#closed = true;
206
922
  this.#clearTimers();
207
923
  this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
208
- const ws = this.#ws;
209
- this.#ws = undefined;
210
- try {
211
- ws?.close(1000, "Client closed connection");
212
- } catch (error) {
213
- this.#emitError(error);
214
- }
924
+ this.#transport.destroy(1000, "Client closed connection");
215
925
  this.#setState("CLOSED" /* Closed */);
216
926
  }
217
927
  emit(event, data) {
@@ -236,23 +946,17 @@ class Gateway {
236
946
  return this.#dispatch(payload, false);
237
947
  }
238
948
  #dispatch(payload, privileged) {
239
- if (this.#ws?.readyState !== WebSocket.OPEN)
949
+ if (!this.#transport.connected)
240
950
  return false;
241
951
  const now = Date.now();
242
- while (this.#sendTimestamps.length && now - this.#sendTimestamps[0] >= 60000)
243
- this.#sendTimestamps.shift();
244
- if (!privileged && this.#sendTimestamps.length >= 115) {
952
+ if (!this.#sendBudget.allows(privileged, now)) {
245
953
  this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
246
954
  return false;
247
955
  }
248
- try {
249
- this.#ws.send(JSON.stringify(payload));
250
- this.#sendTimestamps.push(now);
251
- return true;
252
- } catch (error) {
253
- this.#emitError(error);
956
+ if (!this.#transport.send(JSON.stringify(payload)))
254
957
  return false;
255
- }
958
+ this.#sendBudget.record(now);
959
+ return true;
256
960
  }
257
961
  setPresence(data) {
258
962
  this.#options.presence = { ...this.#options.presence, ...data };
@@ -288,127 +992,80 @@ class Gateway {
288
992
  #open(url) {
289
993
  if (this.#closed)
290
994
  return;
291
- let ws;
292
- try {
293
- ws = new WebSocket(url);
294
- } catch (error) {
295
- const failure = this.#normalizeError(error);
296
- this.#emitError(failure);
297
- if (this.#options.reconnect) {
298
- this.#setState("RECONNECT" /* Reconnect */);
299
- this.#scheduleReconnect();
300
- } else
301
- this.#settleConnect(failure);
995
+ this.#token = this.#session.beginConnection();
996
+ const result = this.#transport.connect(url);
997
+ if (result.ok)
302
998
  return;
999
+ const failure = this.#normalizeError(result.error);
1000
+ this.#emitError(failure);
1001
+ if (this.#options.reconnect) {
1002
+ this.#setState("RECONNECT" /* Reconnect */);
1003
+ this.#scheduleReconnect();
1004
+ } else {
1005
+ this.#settleConnect(failure);
303
1006
  }
304
- this.#ws = ws;
305
- ws.addEventListener("open", () => {
306
- this.#lastMessageAt = Date.now();
307
- this.#zombieReported = false;
308
- this.#startZombieDetection();
309
- this.#emit("open", undefined);
310
- this.#settleConnect();
311
- });
312
- ws.addEventListener("message", (event) => {
313
- this.#lastMessageAt = Date.now();
314
- this.#zombieReported = false;
315
- if (this.#options.compress && event.data instanceof ArrayBuffer) {
316
- this.#decompress(new Uint8Array(event.data)).then((text) => this.#message(ws, text)).catch((err) => this.#emitError(err));
317
- } else {
318
- this.#message(ws, String(event.data));
319
- }
320
- });
321
- ws.addEventListener("close", (event) => this.#close(ws, event.code));
322
- ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
323
- }
324
- #startZombieDetection() {
325
- if (this.#zombieTimer)
326
- clearInterval(this.#zombieTimer);
327
- const interval = Math.max(1, Math.min(1000, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
328
- this.#zombieTimer = setInterval(() => {
329
- if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
330
- return;
331
- const silentFor = Date.now() - this.#lastMessageAt;
332
- const deadline = Math.max(this.#options.zombieTimeout, this.#heartbeatInterval + this.#options.heartbeatAckTimeout);
333
- if (silentFor < deadline || this.#zombieReported)
334
- return;
335
- this.#zombieReported = true;
336
- const error = new GatewayError(`Gateway connection appears stale after ${silentFor}ms without traffic.`);
337
- this.#emit("zombie", { silentFor, timeout: deadline });
338
- this.#emitError(error);
339
- try {
340
- this.#ws.close(1001, "Zombie Gateway connection");
341
- } catch (closeError) {
342
- this.#emitError(closeError);
343
- }
344
- }, interval);
345
1007
  }
346
- #message(ws, raw) {
347
- let payload;
348
- try {
349
- payload = JSON.parse(raw);
350
- } catch (error) {
351
- this.#emitError(new GatewayError("Gateway returned invalid JSON", undefined, {
352
- cause: error
353
- }));
354
- ws.close(1002, "Invalid JSON");
355
- return;
356
- }
357
- if (!payload || typeof payload.op !== "number") {
358
- this.#emitError(new GatewayError("Gateway returned an invalid payload"));
359
- ws.close(1002, "Invalid payload");
1008
+ #onOpen() {
1009
+ this.#heartbeat.watch();
1010
+ this.#emit("open", undefined);
1011
+ this.#settleConnect();
1012
+ }
1013
+ #message(raw, token) {
1014
+ if (!this.#session.owns(token))
360
1015
  return;
361
- }
362
- if (typeof payload.s === "number")
363
- this.#sequence = payload.s;
364
- switch (payload.op) {
365
- case GatewayOpcodes.Dispatch:
1016
+ const { sequence, action } = classifyFrame(raw);
1017
+ if (sequence !== null)
1018
+ this.#session.recordSequence(sequence, token);
1019
+ this.#apply(action, token);
1020
+ }
1021
+ #apply(action, token) {
1022
+ switch (action.type) {
1023
+ case "dispatch":
366
1024
  this.#setState("DISPATCH" /* Dispatch */);
367
- this.#handleDispatch(payload.t, payload.d);
368
- break;
369
- case GatewayOpcodes.Hello:
370
- this.#handleHello(payload.d);
371
- break;
372
- case GatewayOpcodes.Heartbeat:
1025
+ this.#handleDispatch(action.event, action.data, token);
1026
+ return;
1027
+ case "hello":
1028
+ this.#handleHello(action.heartbeatInterval);
1029
+ return;
1030
+ case "heartbeat":
373
1031
  this.#setState("HEARTBEAT" /* Heartbeat */);
374
- this.#sendHeartbeat();
375
- break;
376
- case GatewayOpcodes.HeartbeatAck:
377
- this.#heartbeatACK = true;
378
- this.ping = Date.now() - this.#heartbeatSentAt;
379
- this.#clearHeartbeatAckTimer();
380
- this.#emit("heartbeatAck", payload.d);
381
- break;
382
- case GatewayOpcodes.Reconnect:
383
- ws.close(1001, "Server requested reconnect");
384
- break;
385
- case GatewayOpcodes.InvalidSession: {
386
- const resumable = payload.d === true;
387
- if (!resumable) {
388
- this.#sessionId = undefined;
389
- this.#sequence = null;
390
- this.#resumeURL = undefined;
391
- }
392
- this.#emit("invalidSession", resumable);
393
- ws.close(resumable ? GatewayCloseCodes.UnknownError : 1000, resumable ? "Invalid session (resumable)" : "Invalid session");
394
- break;
1032
+ this.#heartbeat.sendHeartbeat();
1033
+ return;
1034
+ case "heartbeat-ack":
1035
+ this.#heartbeat.acknowledge();
1036
+ this.ping = this.#heartbeat.latency;
1037
+ this.#emit("heartbeatAck", action.data);
1038
+ return;
1039
+ case "reconnect":
1040
+ this.#transport.close(1001, "Server requested reconnect");
1041
+ return;
1042
+ case "invalid-session": {
1043
+ if (!action.resumable)
1044
+ this.#session.invalidate(token, "invalid-session");
1045
+ this.#emit("invalidSession", action.resumable);
1046
+ this.#transport.close(action.resumable ? GatewayCloseCodes.UnknownError : 1000, action.resumable ? "Invalid session (resumable)" : "Invalid session");
1047
+ return;
395
1048
  }
1049
+ case "invalid":
1050
+ this.#emitError(new GatewayError(action.message, undefined, action.cause === undefined ? undefined : { cause: action.cause }));
1051
+ this.#transport.close(1002, action.violation === "invalid-json" ? "Invalid JSON" : action.violation === "invalid-hello" ? "Invalid heartbeat interval" : "Invalid payload");
1052
+ return;
1053
+ case "unknown":
1054
+ return;
396
1055
  }
397
1056
  }
398
- #handleDispatch(event, data) {
1057
+ #handleDispatch(event, data, token) {
399
1058
  if (event === "READY") {
400
- const ready = data;
401
- if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string") {
402
- this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
1059
+ if (!this.#session.activate(data, token)) {
1060
+ if (this.#session.owns(token))
1061
+ this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
403
1062
  return;
404
1063
  }
405
- this.#sessionId = ready.session_id;
406
- this.#resumeURL = ready.resume_gateway_url;
407
- this.#attempt = 0;
1064
+ this.#reconnect.reset();
408
1065
  this.#setState("READY" /* Ready */);
409
1066
  this.#emit("ready", data);
410
1067
  } else if (event === "RESUMED") {
411
- this.#attempt = 0;
1068
+ this.#reconnect.reset();
412
1069
  this.#setState("READY" /* Ready */);
413
1070
  this.#emit("resumed", data);
414
1071
  }
@@ -417,183 +1074,60 @@ class Gateway {
417
1074
  return;
418
1075
  this.#emit(event ?? "dispatch", data);
419
1076
  }
420
- #handleHello(data) {
421
- const interval = data?.heartbeat_interval;
422
- if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0) {
423
- this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
424
- this.#ws?.close(1002, "Invalid heartbeat interval");
425
- return;
426
- }
427
- this.#heartbeatInterval = interval;
428
- this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
429
- this.#startHeartbeat(interval);
1077
+ #handleHello(interval) {
1078
+ this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
1079
+ this.#heartbeat.start(interval);
430
1080
  this.#identifyOrResume();
431
1081
  }
432
1082
  #identifyOrResume() {
433
- if (this.#sessionId && this.#sequence !== null && this.#resumeURL) {
1083
+ const intent = this.#session.handshake();
1084
+ if (intent.type === "resume") {
434
1085
  this.#setState("RESUME" /* Resume */);
435
- this.send({
436
- op: GatewayOpcodes.Resume,
437
- d: {
438
- token: this.#options.token,
439
- session_id: this.#sessionId,
440
- seq: this.#sequence
441
- },
442
- s: null,
443
- t: null
444
- });
1086
+ this.#dispatch(resumePayload(this.#options.token, intent), true);
445
1087
  return;
446
1088
  }
447
1089
  this.#setState("IDENTIFY" /* Identify */);
448
- const props = this.#options.properties ?? {
449
- os: "Android",
450
- browser: "Discord Android",
451
- device: "Discord Android"
452
- };
453
- const os = props.os ?? "Android";
454
- const browser = props.browser ?? "Discord Android";
455
- const device = props.device ?? "Discord Android";
456
- this.send({
457
- op: GatewayOpcodes.Identify,
458
- d: {
459
- token: this.#options.token,
460
- intents: this.#options.intents,
461
- properties: {
462
- os,
463
- browser,
464
- device,
465
- $os: os,
466
- $browser: browser,
467
- $device: device,
468
- ...props
469
- },
470
- presence: {
471
- since: this.#options.presence?.since ?? null,
472
- activities: this.#options.presence?.activities ?? [],
473
- status: this.#options.presence?.status ?? "online",
474
- afk: Boolean(this.#options.presence?.afk)
475
- },
476
- shard: [this.#options.shardId, this.#options.shardCount]
477
- },
478
- s: null,
479
- t: null
480
- });
481
- }
482
- #startHeartbeat(interval) {
483
- this.#clearHeartbeatTimers();
484
- this.#heartbeatACK = true;
485
- this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
486
- this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
487
- }
488
- #sendHeartbeat() {
489
- this.#heartbeatACK = false;
490
- this.#heartbeatSentAt = Date.now();
491
- if (!this.#dispatch({
492
- op: GatewayOpcodes.Heartbeat,
493
- d: this.#sequence,
494
- s: null,
495
- t: null
496
- }, true)) {
497
- this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
498
- return;
499
- }
500
- this.#clearHeartbeatAckTimer();
501
- this.#heartbeatAckTimer = setTimeout(() => {
502
- if (!this.#heartbeatACK) {
503
- const elapsed = Date.now() - this.#heartbeatSentAt;
504
- this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
505
- try {
506
- this.#ws?.close(1001, "Heartbeat timeout");
507
- } catch (error) {
508
- this.#emitError(error);
509
- }
510
- }
511
- }, this.#options.heartbeatAckTimeout);
1090
+ this.#dispatch(identifyPayload({
1091
+ token: this.#options.token,
1092
+ intents: this.#options.intents,
1093
+ shardId: this.#options.shardId,
1094
+ shardCount: this.#options.shardCount,
1095
+ properties: this.#options.properties,
1096
+ presence: this.#options.presence
1097
+ }), true);
512
1098
  }
513
- #close(ws, code) {
514
- if (this.#ws !== ws)
515
- return;
516
- this.#ws = undefined;
1099
+ #close(code, token) {
517
1100
  this.#clearTimers();
518
- const action = this.#closeAction(code);
1101
+ const action = this.#reconnect.classifyClose(code, this.#session.canResume);
519
1102
  this.#emit("close", { code, action });
520
- if (this.#closed || !this.#options.reconnect || action === "stop") {
1103
+ if (this.#closed || !this.#reconnect.enabled || action === "stop") {
1104
+ if (action === "stop")
1105
+ this.#closed = true;
521
1106
  this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
522
- if (this.#connectPromise)
523
- this.#settleConnect(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
1107
+ if (this.#reconnect.connecting)
1108
+ this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
524
1109
  return;
525
1110
  }
526
- if (action === "identify") {
527
- this.#sequence = null;
528
- this.#sessionId = undefined;
529
- this.#resumeURL = undefined;
530
- }
1111
+ if (action === "identify")
1112
+ this.#session.invalidate(token, "session-timeout");
531
1113
  this.#setState("RECONNECT" /* Reconnect */);
532
1114
  this.#scheduleReconnect();
533
1115
  }
534
- #closeAction(code) {
535
- const fatal = [
536
- GatewayCloseCodes.AuthenticationFailed,
537
- GatewayCloseCodes.InvalidShard,
538
- GatewayCloseCodes.ShardingRequired,
539
- GatewayCloseCodes.InvalidAPIVersion,
540
- GatewayCloseCodes.InvalidIntents,
541
- GatewayCloseCodes.DisallowedIntents
542
- ];
543
- if (fatal.includes(code))
544
- return "stop";
545
- if (code === GatewayCloseCodes.InvalidSeq || code === GatewayCloseCodes.SessionTimedOut)
546
- return "identify";
547
- return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
548
- }
549
1116
  #scheduleReconnect() {
550
- if (this.#closed || !this.#options.reconnect || this.#reconnectTimer)
1117
+ if (this.#closed)
551
1118
  return;
552
- if (this.#attempt >= this.#options.maxReconnectAttempts) {
553
- this.#settleConnect(new GatewayError("Gateway reconnect attempts exhausted."));
554
- this.#setState("CONNECT" /* Connect */);
1119
+ const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
1120
+ if (result.scheduled || result.reason !== "exhausted")
555
1121
  return;
556
- }
557
- const delay = Math.min(this.#options.reconnectMaxDelay, this.#options.reconnectBaseDelay * 2 ** this.#attempt++);
558
- const jitter = Math.random() * Math.max(1, delay * 0.25);
559
- this.#reconnectTimer = setTimeout(() => {
560
- this.#reconnectTimer = undefined;
561
- this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
562
- }, delay + jitter);
1122
+ this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
1123
+ this.#setState("CONNECT" /* Connect */);
563
1124
  }
564
1125
  #settleConnect(error) {
565
- const resolve = this.#resolveConnect;
566
- const reject = this.#rejectConnect;
567
- this.#resolveConnect = undefined;
568
- this.#rejectConnect = undefined;
569
- this.#connectPromise = undefined;
570
- if (error)
571
- reject?.(error);
572
- else
573
- resolve?.();
574
- }
575
- #clearHeartbeatAckTimer() {
576
- if (this.#heartbeatAckTimer)
577
- clearTimeout(this.#heartbeatAckTimer);
578
- this.#heartbeatAckTimer = undefined;
579
- }
580
- #clearHeartbeatTimers() {
581
- if (this.#heartbeatTimer)
582
- clearInterval(this.#heartbeatTimer);
583
- if (this.#initialHeartbeat)
584
- clearTimeout(this.#initialHeartbeat);
585
- this.#clearHeartbeatAckTimer();
586
- this.#heartbeatTimer = undefined;
587
- this.#initialHeartbeat = undefined;
1126
+ this.#reconnect.settle(error);
588
1127
  }
589
1128
  #clearTimers() {
590
- if (this.#reconnectTimer)
591
- clearTimeout(this.#reconnectTimer);
592
- if (this.#zombieTimer)
593
- clearInterval(this.#zombieTimer);
594
- this.#reconnectTimer = undefined;
595
- this.#zombieTimer = undefined;
596
- this.#clearHeartbeatTimers();
1129
+ this.#reconnect.cancel();
1130
+ this.#heartbeat.stop();
597
1131
  }
598
1132
  #normalizeError(error) {
599
1133
  return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
@@ -625,45 +1159,29 @@ class Gateway {
625
1159
  this.state = next;
626
1160
  this.#emit("stateChange", { previous, next });
627
1161
  }
628
- #initDecompressor() {
629
- const ds = new DecompressionStream("deflate-raw");
630
- this.#decompressor = {
631
- writer: ds.writable.getWriter(),
632
- reader: ds.readable.getReader()
633
- };
634
- }
635
- async#decompress(chunk) {
636
- if (!this.#decompressor)
637
- this.#initDecompressor();
638
- const { writer, reader } = this.#decompressor;
639
- await writer.write(chunk);
640
- const parts = [];
641
- while (true) {
642
- const { done, value } = await Promise.race([
643
- reader.read(),
644
- new Promise((resolve) => setTimeout(() => resolve({ done: true, value: undefined }), 50))
645
- ]);
646
- if (done || !value)
647
- break;
648
- parts.push(value);
649
- }
650
- const total = parts.reduce((n, p) => n + p.length, 0);
651
- const merged = new Uint8Array(total);
652
- let offset = 0;
653
- for (const p of parts) {
654
- merged.set(p, offset);
655
- offset += p.length;
656
- }
657
- return new TextDecoder().decode(merged);
658
- }
659
1162
  }
660
1163
  export {
1164
+ FATAL_CLOSE_CODES,
661
1165
  Gateway,
662
1166
  GatewayCloseCodes,
663
1167
  GatewayError,
1168
+ GatewayHeartbeat,
664
1169
  GatewayOpcodes,
1170
+ GatewayReconnect,
1171
+ GatewaySession,
665
1172
  GatewayState,
666
- GatewayState as Status
1173
+ IDENTIFY_CLOSE_CODES,
1174
+ PlainTextDecoder,
1175
+ GatewayState as Status,
1176
+ WebSocketTransport,
1177
+ ZlibStreamDecoder,
1178
+ classifyCloseCode,
1179
+ classifyFrame,
1180
+ classifyPayload,
1181
+ createDecoder,
1182
+ heartbeatPayload,
1183
+ identifyPayload,
1184
+ resumePayload
667
1185
  };
668
1186
 
669
- //# debugId=267C8623751A500664756E2164756E21
1187
+ //# debugId=09C4F2BCB61445DA64756E2164756E21