lunibee 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +223 -223
- package/dist/builders/commands.d.ts +39 -0
- package/dist/builders/components.d.ts +37 -12
- package/dist/builders/embed.d.ts +3 -1
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +192 -29
- package/dist/builders/index.js.map +9 -7
- package/dist/collection/index.d.ts +30 -0
- package/dist/collection/index.js +79 -5
- package/dist/collection/index.js.map +4 -4
- package/dist/core/events.d.ts +88 -2
- package/dist/core/index.d.ts +46 -85
- package/dist/core/index.js +2395 -789
- package/dist/core/index.js.map +44 -22
- package/dist/core/permissions.d.ts +112 -53
- package/dist/formatters/index.d.ts +43 -3
- package/dist/formatters/index.js +64 -1
- package/dist/formatters/index.js.map +3 -3
- package/dist/handlers/index.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3783 -1009
- package/dist/index.js.map +63 -38
- package/dist/managers/emoji.d.ts +38 -0
- package/dist/managers/guild-resources.d.ts +109 -0
- package/dist/managers/guild.d.ts +117 -1
- package/dist/managers/index.d.ts +37 -1
- package/dist/managers/index.js +1125 -81
- package/dist/managers/index.js.map +25 -20
- package/dist/rest/decoder.d.ts +23 -0
- package/dist/rest/errors.d.ts +20 -0
- package/dist/rest/index.d.ts +119 -25
- package/dist/rest/index.js +676 -204
- package/dist/rest/index.js.map +14 -6
- package/dist/rest/limiter.d.ts +42 -0
- package/dist/rest/redis.d.ts +54 -0
- package/dist/rest/route.d.ts +31 -0
- package/dist/rest/routes.d.ts +24 -0
- package/dist/rest/scheduler.d.ts +42 -0
- package/dist/rest/store.d.ts +67 -0
- package/dist/rest/transport.d.ts +40 -0
- package/dist/rest/webhook.d.ts +2 -2
- package/dist/sharding/bus.d.ts +31 -0
- package/dist/sharding/cluster.d.ts +71 -0
- package/dist/sharding/index.d.ts +18 -2
- package/dist/sharding/index.js +1202 -317
- package/dist/sharding/index.js.map +18 -7
- package/dist/structures/audit-log.d.ts +26 -0
- package/dist/structures/base.d.ts +40 -0
- package/dist/structures/channels.d.ts +56 -0
- package/dist/structures/index.d.ts +30 -1
- package/dist/structures/index.js +963 -87
- package/dist/structures/index.js.map +17 -10
- package/dist/structures/interactions.d.ts +12 -52
- package/dist/structures/options.d.ts +55 -0
- package/dist/structures/resources.d.ts +194 -6
- package/dist/types/gateway-events.d.ts +132 -0
- package/dist/types/gateway.d.ts +181 -0
- package/dist/types/index.d.ts +78 -263
- package/dist/types/index.js +44 -2
- package/dist/types/index.js.map +5 -4
- package/dist/utils/index.js.map +2 -2
- package/dist/voice/index.d.ts +116 -0
- package/dist/voice/index.js +261 -10
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/close-codes.d.ts +23 -0
- package/dist/ws/decoder.d.ts +58 -0
- package/dist/ws/heartbeat.d.ts +87 -0
- package/dist/ws/index.d.ts +24 -32
- package/dist/ws/index.js +920 -318
- package/dist/ws/index.js.map +15 -5
- package/dist/ws/opcodes.d.ts +14 -0
- package/dist/ws/protocol.d.ts +110 -0
- package/dist/ws/reconnect.d.ts +108 -0
- package/dist/ws/send-budget.d.ts +17 -0
- package/dist/ws/session.d.ts +100 -0
- package/dist/ws/state.d.ts +27 -0
- package/dist/ws/transport.d.ts +80 -0
- package/package.json +23 -22
- package/dist/structures/permissions.d.ts +0 -73
package/dist/ws/index.js
CHANGED
|
@@ -1,5 +1,702 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/
|
|
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,34 +772,19 @@ function resolveGatewayIntents(intents) {
|
|
|
73
772
|
}
|
|
74
773
|
return 0;
|
|
75
774
|
}
|
|
76
|
-
|
|
77
|
-
// packages/ws/src/index.ts
|
|
78
|
-
var GatewayOpcodes = {
|
|
79
|
-
Dispatch: 0,
|
|
80
|
-
Heartbeat: 1,
|
|
81
|
-
Identify: 2,
|
|
82
|
-
PresenceUpdate: 3,
|
|
83
|
-
VoiceStateUpdate: 4,
|
|
84
|
-
Resume: 6,
|
|
85
|
-
Reconnect: 7,
|
|
86
|
-
RequestGuildMembers: 8,
|
|
87
|
-
InvalidSession: 9,
|
|
88
|
-
Hello: 10,
|
|
89
|
-
HeartbeatAck: 11
|
|
90
|
-
};
|
|
775
|
+
// packages/ws/src/state.ts
|
|
91
776
|
var GatewayState;
|
|
92
|
-
((
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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";
|
|
102
787
|
})(GatewayState ||= {});
|
|
103
|
-
|
|
104
788
|
class GatewayError extends Error {
|
|
105
789
|
code;
|
|
106
790
|
constructor(message, code, options) {
|
|
@@ -109,31 +793,43 @@ class GatewayError extends Error {
|
|
|
109
793
|
this.code = code;
|
|
110
794
|
}
|
|
111
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";
|
|
112
820
|
|
|
113
821
|
class Gateway {
|
|
114
822
|
state = "CONNECT" /* Connect */;
|
|
115
823
|
#options;
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#heartbeatTimer;
|
|
121
|
-
#initialHeartbeat;
|
|
122
|
-
#heartbeatAckTimer;
|
|
123
|
-
#heartbeatACK = true;
|
|
124
|
-
#heartbeatInterval = 0;
|
|
125
|
-
#heartbeatSentAt = 0;
|
|
126
|
-
#lastMessageAt = 0;
|
|
127
|
-
#zombieTimer;
|
|
128
|
-
#zombieReported = false;
|
|
824
|
+
#transport;
|
|
825
|
+
#session = new GatewaySession;
|
|
826
|
+
#token = 0;
|
|
827
|
+
#heartbeat;
|
|
129
828
|
#closed = false;
|
|
130
|
-
#
|
|
131
|
-
#reconnectTimer;
|
|
132
|
-
#connectPromise;
|
|
133
|
-
#resolveConnect;
|
|
134
|
-
#rejectConnect;
|
|
829
|
+
#reconnect;
|
|
135
830
|
#listeners = new Map;
|
|
136
|
-
#
|
|
831
|
+
#sendBudget = new SendBudget;
|
|
832
|
+
ping = -1;
|
|
137
833
|
constructor(options) {
|
|
138
834
|
if (!options.token?.trim())
|
|
139
835
|
throw new TypeError("A Gateway token is required.");
|
|
@@ -154,39 +850,78 @@ class Gateway {
|
|
|
154
850
|
browser: "Discord Android",
|
|
155
851
|
device: "Discord Android"
|
|
156
852
|
},
|
|
157
|
-
presence: {
|
|
158
|
-
|
|
853
|
+
presence: {
|
|
854
|
+
status: "online",
|
|
855
|
+
activities: [],
|
|
856
|
+
afk: false,
|
|
857
|
+
since: null
|
|
858
|
+
},
|
|
859
|
+
...options,
|
|
860
|
+
intents: resolvedIntents
|
|
159
861
|
};
|
|
160
862
|
if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
|
|
161
863
|
throw new RangeError("Gateway shardId must be within the configured shard count.");
|
|
162
864
|
if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
|
|
163
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
|
+
});
|
|
164
893
|
}
|
|
165
|
-
|
|
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) {
|
|
166
907
|
if (this.state === "CLOSED" /* Closed */)
|
|
167
908
|
throw new GatewayError("Gateway has been permanently closed.");
|
|
168
|
-
|
|
169
|
-
|
|
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();
|
|
170
915
|
this.#closed = false;
|
|
171
916
|
this.#setState("CONNECT" /* Connect */);
|
|
172
|
-
this.#
|
|
173
|
-
|
|
174
|
-
this.#rejectConnect = reject;
|
|
175
|
-
this.#open(url);
|
|
176
|
-
});
|
|
177
|
-
return this.#connectPromise;
|
|
917
|
+
const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
|
|
918
|
+
return this.#reconnect.attempt(() => this.#open(connectURL));
|
|
178
919
|
}
|
|
179
920
|
close() {
|
|
180
921
|
this.#closed = true;
|
|
181
922
|
this.#clearTimers();
|
|
182
923
|
this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
|
|
183
|
-
|
|
184
|
-
this.#ws = undefined;
|
|
185
|
-
try {
|
|
186
|
-
ws?.close(1000, "Client closed connection");
|
|
187
|
-
} catch (error) {
|
|
188
|
-
this.#emitError(error);
|
|
189
|
-
}
|
|
924
|
+
this.#transport.destroy(1000, "Client closed connection");
|
|
190
925
|
this.#setState("CLOSED" /* Closed */);
|
|
191
926
|
}
|
|
192
927
|
emit(event, data) {
|
|
@@ -208,23 +943,20 @@ class Gateway {
|
|
|
208
943
|
send(payload) {
|
|
209
944
|
if (!payload || !Number.isInteger(payload.op))
|
|
210
945
|
throw new TypeError("Gateway payload must contain an integer opcode.");
|
|
211
|
-
|
|
946
|
+
return this.#dispatch(payload, false);
|
|
947
|
+
}
|
|
948
|
+
#dispatch(payload, privileged) {
|
|
949
|
+
if (!this.#transport.connected)
|
|
212
950
|
return false;
|
|
213
951
|
const now = Date.now();
|
|
214
|
-
|
|
215
|
-
this.#sendTimestamps.shift();
|
|
216
|
-
if (this.#sendTimestamps.length >= 115) {
|
|
952
|
+
if (!this.#sendBudget.allows(privileged, now)) {
|
|
217
953
|
this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
|
|
218
954
|
return false;
|
|
219
955
|
}
|
|
220
|
-
|
|
221
|
-
this.#ws.send(JSON.stringify(payload));
|
|
222
|
-
this.#sendTimestamps.push(now);
|
|
223
|
-
return true;
|
|
224
|
-
} catch (error) {
|
|
225
|
-
this.#emitError(error);
|
|
956
|
+
if (!this.#transport.send(JSON.stringify(payload)))
|
|
226
957
|
return false;
|
|
227
|
-
|
|
958
|
+
this.#sendBudget.record(now);
|
|
959
|
+
return true;
|
|
228
960
|
}
|
|
229
961
|
setPresence(data) {
|
|
230
962
|
this.#options.presence = { ...this.#options.presence, ...data };
|
|
@@ -260,289 +992,142 @@ class Gateway {
|
|
|
260
992
|
#open(url) {
|
|
261
993
|
if (this.#closed)
|
|
262
994
|
return;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (error) {
|
|
267
|
-
const failure = this.#normalizeError(error);
|
|
268
|
-
this.#emitError(failure);
|
|
269
|
-
if (this.#options.reconnect) {
|
|
270
|
-
this.#setState("RECONNECT" /* Reconnect */);
|
|
271
|
-
this.#scheduleReconnect();
|
|
272
|
-
} else
|
|
273
|
-
this.#settleConnect(failure);
|
|
995
|
+
this.#token = this.#session.beginConnection();
|
|
996
|
+
const result = this.#transport.connect(url);
|
|
997
|
+
if (result.ok)
|
|
274
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);
|
|
275
1006
|
}
|
|
276
|
-
this.#ws = ws;
|
|
277
|
-
ws.addEventListener("open", () => {
|
|
278
|
-
this.#lastMessageAt = Date.now();
|
|
279
|
-
this.#zombieReported = false;
|
|
280
|
-
this.#startZombieDetection();
|
|
281
|
-
this.#emit("open", undefined);
|
|
282
|
-
this.#settleConnect();
|
|
283
|
-
});
|
|
284
|
-
ws.addEventListener("message", (event) => {
|
|
285
|
-
this.#lastMessageAt = Date.now();
|
|
286
|
-
this.#zombieReported = false;
|
|
287
|
-
this.#message(ws, String(event.data));
|
|
288
|
-
});
|
|
289
|
-
ws.addEventListener("close", (event) => this.#close(ws, event.code));
|
|
290
|
-
ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
|
|
291
|
-
}
|
|
292
|
-
#startZombieDetection() {
|
|
293
|
-
if (this.#zombieTimer)
|
|
294
|
-
clearInterval(this.#zombieTimer);
|
|
295
|
-
const interval = Math.max(1, Math.min(250, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
|
|
296
|
-
this.#zombieTimer = setInterval(() => {
|
|
297
|
-
if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
|
|
298
|
-
return;
|
|
299
|
-
const silentFor = Date.now() - this.#lastMessageAt;
|
|
300
|
-
const deadline = Math.max(this.#options.zombieTimeout, this.#heartbeatInterval + this.#options.heartbeatAckTimeout);
|
|
301
|
-
if (silentFor < deadline || this.#zombieReported)
|
|
302
|
-
return;
|
|
303
|
-
this.#zombieReported = true;
|
|
304
|
-
const error = new GatewayError(`Gateway connection appears stale after ${silentFor}ms without traffic.`);
|
|
305
|
-
this.#emit("zombie", { silentFor, timeout: deadline });
|
|
306
|
-
this.#emitError(error);
|
|
307
|
-
try {
|
|
308
|
-
this.#ws.close(1001, "Zombie Gateway connection");
|
|
309
|
-
} catch (closeError) {
|
|
310
|
-
this.#emitError(closeError);
|
|
311
|
-
}
|
|
312
|
-
}, interval);
|
|
313
1007
|
}
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}));
|
|
322
|
-
ws.close(1002, "Invalid JSON");
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (!payload || typeof payload.op !== "number") {
|
|
326
|
-
this.#emitError(new GatewayError("Gateway returned an invalid payload"));
|
|
327
|
-
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))
|
|
328
1015
|
return;
|
|
329
|
-
}
|
|
330
|
-
if (
|
|
331
|
-
this.#sequence
|
|
332
|
-
|
|
333
|
-
|
|
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":
|
|
334
1024
|
this.#setState("DISPATCH" /* Dispatch */);
|
|
335
|
-
this.#handleDispatch(
|
|
336
|
-
|
|
337
|
-
case
|
|
338
|
-
this.#handleHello(
|
|
339
|
-
|
|
340
|
-
case
|
|
1025
|
+
this.#handleDispatch(action.event, action.data, token);
|
|
1026
|
+
return;
|
|
1027
|
+
case "hello":
|
|
1028
|
+
this.#handleHello(action.heartbeatInterval);
|
|
1029
|
+
return;
|
|
1030
|
+
case "heartbeat":
|
|
341
1031
|
this.#setState("HEARTBEAT" /* Heartbeat */);
|
|
342
|
-
this.#sendHeartbeat();
|
|
343
|
-
|
|
344
|
-
case
|
|
345
|
-
this.#
|
|
346
|
-
this.#
|
|
347
|
-
this.#emit("heartbeatAck",
|
|
348
|
-
|
|
349
|
-
case
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
case
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
this.#emit("invalidSession",
|
|
356
|
-
|
|
357
|
-
|
|
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;
|
|
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;
|
|
358
1055
|
}
|
|
359
1056
|
}
|
|
360
|
-
#handleDispatch(event, data) {
|
|
1057
|
+
#handleDispatch(event, data, token) {
|
|
361
1058
|
if (event === "READY") {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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"));
|
|
365
1062
|
return;
|
|
366
1063
|
}
|
|
367
|
-
this.#
|
|
368
|
-
this.#resumeURL = ready.resume_gateway_url;
|
|
369
|
-
this.#attempt = 0;
|
|
1064
|
+
this.#reconnect.reset();
|
|
370
1065
|
this.#setState("READY" /* Ready */);
|
|
371
1066
|
this.#emit("ready", data);
|
|
1067
|
+
} else if (event === "RESUMED") {
|
|
1068
|
+
this.#reconnect.reset();
|
|
1069
|
+
this.#setState("READY" /* Ready */);
|
|
1070
|
+
this.#emit("resumed", data);
|
|
372
1071
|
}
|
|
373
1072
|
this.#emit("RAW", { event: event ?? "UNKNOWN", data });
|
|
374
1073
|
if (this.#closed)
|
|
375
1074
|
return;
|
|
376
1075
|
this.#emit(event ?? "dispatch", data);
|
|
377
1076
|
}
|
|
378
|
-
#handleHello(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
|
|
382
|
-
this.#ws?.close(1002, "Invalid heartbeat interval");
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
this.#heartbeatInterval = interval;
|
|
386
|
-
this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
387
|
-
this.#startHeartbeat(interval);
|
|
1077
|
+
#handleHello(interval) {
|
|
1078
|
+
this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
1079
|
+
this.#heartbeat.start(interval);
|
|
388
1080
|
this.#identifyOrResume();
|
|
389
1081
|
}
|
|
390
1082
|
#identifyOrResume() {
|
|
391
|
-
|
|
1083
|
+
const intent = this.#session.handshake();
|
|
1084
|
+
if (intent.type === "resume") {
|
|
392
1085
|
this.#setState("RESUME" /* Resume */);
|
|
393
|
-
this.
|
|
394
|
-
op: GatewayOpcodes.Resume,
|
|
395
|
-
d: {
|
|
396
|
-
token: this.#options.token,
|
|
397
|
-
session_id: this.#sessionId,
|
|
398
|
-
seq: this.#sequence
|
|
399
|
-
},
|
|
400
|
-
s: null,
|
|
401
|
-
t: null
|
|
402
|
-
});
|
|
1086
|
+
this.#dispatch(resumePayload(this.#options.token, intent), true);
|
|
403
1087
|
return;
|
|
404
1088
|
}
|
|
405
1089
|
this.#setState("IDENTIFY" /* Identify */);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.send({
|
|
415
|
-
op: GatewayOpcodes.Identify,
|
|
416
|
-
d: {
|
|
417
|
-
token: this.#options.token,
|
|
418
|
-
intents: this.#options.intents,
|
|
419
|
-
properties: {
|
|
420
|
-
os,
|
|
421
|
-
browser,
|
|
422
|
-
device,
|
|
423
|
-
$os: os,
|
|
424
|
-
$browser: browser,
|
|
425
|
-
$device: device,
|
|
426
|
-
...props
|
|
427
|
-
},
|
|
428
|
-
presence: {
|
|
429
|
-
since: this.#options.presence?.since ?? null,
|
|
430
|
-
activities: this.#options.presence?.activities ?? [],
|
|
431
|
-
status: this.#options.presence?.status ?? "online",
|
|
432
|
-
afk: Boolean(this.#options.presence?.afk)
|
|
433
|
-
},
|
|
434
|
-
shard: [this.#options.shardId, this.#options.shardCount]
|
|
435
|
-
},
|
|
436
|
-
s: null,
|
|
437
|
-
t: null
|
|
438
|
-
});
|
|
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);
|
|
439
1098
|
}
|
|
440
|
-
#
|
|
441
|
-
this.#clearHeartbeatTimers();
|
|
442
|
-
this.#heartbeatACK = true;
|
|
443
|
-
this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
|
|
444
|
-
this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
|
|
445
|
-
}
|
|
446
|
-
#sendHeartbeat() {
|
|
447
|
-
this.#heartbeatACK = false;
|
|
448
|
-
this.#heartbeatSentAt = Date.now();
|
|
449
|
-
if (!this.send({
|
|
450
|
-
op: GatewayOpcodes.Heartbeat,
|
|
451
|
-
d: this.#sequence,
|
|
452
|
-
s: null,
|
|
453
|
-
t: null
|
|
454
|
-
})) {
|
|
455
|
-
this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
this.#clearHeartbeatAckTimer();
|
|
459
|
-
this.#heartbeatAckTimer = setTimeout(() => {
|
|
460
|
-
if (!this.#heartbeatACK) {
|
|
461
|
-
const elapsed = Date.now() - this.#heartbeatSentAt;
|
|
462
|
-
this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
|
|
463
|
-
try {
|
|
464
|
-
this.#ws?.close(1001, "Heartbeat timeout");
|
|
465
|
-
} catch (error) {
|
|
466
|
-
this.#emitError(error);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}, this.#options.heartbeatAckTimeout);
|
|
470
|
-
}
|
|
471
|
-
#close(ws, code) {
|
|
472
|
-
if (this.#ws !== ws)
|
|
473
|
-
return;
|
|
474
|
-
this.#ws = undefined;
|
|
1099
|
+
#close(code, token) {
|
|
475
1100
|
this.#clearTimers();
|
|
476
|
-
const action = this.#
|
|
1101
|
+
const action = this.#reconnect.classifyClose(code, this.#session.canResume);
|
|
477
1102
|
this.#emit("close", { code, action });
|
|
478
|
-
if (this.#closed || !this.#
|
|
1103
|
+
if (this.#closed || !this.#reconnect.enabled || action === "stop") {
|
|
1104
|
+
if (action === "stop")
|
|
1105
|
+
this.#closed = true;
|
|
479
1106
|
this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
|
|
480
|
-
if (this.#
|
|
481
|
-
this.#
|
|
1107
|
+
if (this.#reconnect.connecting)
|
|
1108
|
+
this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
|
|
482
1109
|
return;
|
|
483
1110
|
}
|
|
484
|
-
if (action === "identify")
|
|
485
|
-
this.#
|
|
486
|
-
this.#sessionId = undefined;
|
|
487
|
-
}
|
|
1111
|
+
if (action === "identify")
|
|
1112
|
+
this.#session.invalidate(token, "session-timeout");
|
|
488
1113
|
this.#setState("RECONNECT" /* Reconnect */);
|
|
489
1114
|
this.#scheduleReconnect();
|
|
490
1115
|
}
|
|
491
|
-
#closeAction(code) {
|
|
492
|
-
if ([4004, 4010, 4011, 4012, 4013, 4014].includes(code))
|
|
493
|
-
return "stop";
|
|
494
|
-
if ([4007, 4009].includes(code))
|
|
495
|
-
return "identify";
|
|
496
|
-
return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
|
|
497
|
-
}
|
|
498
1116
|
#scheduleReconnect() {
|
|
499
|
-
if (this.#closed
|
|
1117
|
+
if (this.#closed)
|
|
500
1118
|
return;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
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")
|
|
504
1121
|
return;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const jitter = Math.random() * Math.min(250, Math.max(1, delay * 0.25));
|
|
508
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
509
|
-
this.#reconnectTimer = undefined;
|
|
510
|
-
this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
|
|
511
|
-
}, delay + jitter);
|
|
1122
|
+
this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
|
|
1123
|
+
this.#setState("CONNECT" /* Connect */);
|
|
512
1124
|
}
|
|
513
1125
|
#settleConnect(error) {
|
|
514
|
-
|
|
515
|
-
const reject = this.#rejectConnect;
|
|
516
|
-
this.#resolveConnect = undefined;
|
|
517
|
-
this.#rejectConnect = undefined;
|
|
518
|
-
this.#connectPromise = undefined;
|
|
519
|
-
if (error)
|
|
520
|
-
reject?.(error);
|
|
521
|
-
else
|
|
522
|
-
resolve?.();
|
|
523
|
-
}
|
|
524
|
-
#clearHeartbeatAckTimer() {
|
|
525
|
-
if (this.#heartbeatAckTimer)
|
|
526
|
-
clearTimeout(this.#heartbeatAckTimer);
|
|
527
|
-
this.#heartbeatAckTimer = undefined;
|
|
528
|
-
}
|
|
529
|
-
#clearHeartbeatTimers() {
|
|
530
|
-
if (this.#heartbeatTimer)
|
|
531
|
-
clearInterval(this.#heartbeatTimer);
|
|
532
|
-
if (this.#initialHeartbeat)
|
|
533
|
-
clearTimeout(this.#initialHeartbeat);
|
|
534
|
-
this.#clearHeartbeatAckTimer();
|
|
535
|
-
this.#heartbeatTimer = undefined;
|
|
536
|
-
this.#initialHeartbeat = undefined;
|
|
1126
|
+
this.#reconnect.settle(error);
|
|
537
1127
|
}
|
|
538
1128
|
#clearTimers() {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (this.#zombieTimer)
|
|
542
|
-
clearInterval(this.#zombieTimer);
|
|
543
|
-
this.#reconnectTimer = undefined;
|
|
544
|
-
this.#zombieTimer = undefined;
|
|
545
|
-
this.#clearHeartbeatTimers();
|
|
1129
|
+
this.#reconnect.cancel();
|
|
1130
|
+
this.#heartbeat.stop();
|
|
546
1131
|
}
|
|
547
1132
|
#normalizeError(error) {
|
|
548
1133
|
return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
|
|
@@ -576,10 +1161,27 @@ class Gateway {
|
|
|
576
1161
|
}
|
|
577
1162
|
}
|
|
578
1163
|
export {
|
|
1164
|
+
FATAL_CLOSE_CODES,
|
|
579
1165
|
Gateway,
|
|
1166
|
+
GatewayCloseCodes,
|
|
580
1167
|
GatewayError,
|
|
1168
|
+
GatewayHeartbeat,
|
|
581
1169
|
GatewayOpcodes,
|
|
582
|
-
|
|
1170
|
+
GatewayReconnect,
|
|
1171
|
+
GatewaySession,
|
|
1172
|
+
GatewayState,
|
|
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
|
|
583
1185
|
};
|
|
584
1186
|
|
|
585
|
-
//# debugId=
|
|
1187
|
+
//# debugId=09C4F2BCB61445DA64756E2164756E21
|