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.
- package/LICENSE +21 -21
- package/README.md +223 -223
- package/dist/builders/commands.d.ts +6 -4
- package/dist/builders/components.d.ts +18 -0
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +49 -13
- package/dist/builders/index.js.map +9 -8
- package/dist/collection/index.js +2 -1
- package/dist/collection/index.js.map +4 -4
- package/dist/core/events.d.ts +86 -2
- package/dist/core/index.d.ts +10 -87
- package/dist/core/index.js +1752 -725
- package/dist/core/index.js.map +44 -24
- package/dist/formatters/index.js.map +1 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2103 -800
- package/dist/index.js.map +63 -43
- package/dist/managers/guild-resources.d.ts +109 -0
- package/dist/managers/guild.d.ts +5 -0
- package/dist/managers/index.d.ts +7 -1
- package/dist/managers/index.js +330 -12
- package/dist/managers/index.js.map +25 -21
- package/dist/rest/decoder.d.ts +23 -0
- package/dist/rest/errors.d.ts +20 -0
- package/dist/rest/index.d.ts +21 -17
- package/dist/rest/index.js +479 -263
- package/dist/rest/index.js.map +14 -8
- package/dist/rest/limiter.d.ts +42 -0
- package/dist/rest/redis.d.ts +18 -1
- package/dist/rest/route.d.ts +31 -0
- package/dist/rest/routes.d.ts +2 -0
- package/dist/rest/scheduler.d.ts +42 -0
- package/dist/rest/store.d.ts +29 -0
- package/dist/rest/transport.d.ts +40 -0
- package/dist/sharding/bus.d.ts +31 -0
- package/dist/sharding/cluster.d.ts +10 -0
- package/dist/sharding/index.d.ts +10 -2
- package/dist/sharding/index.js +1011 -394
- package/dist/sharding/index.js.map +18 -8
- package/dist/structures/base.d.ts +8 -0
- package/dist/structures/channels.d.ts +56 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +211 -271
- package/dist/structures/index.js.map +17 -14
- package/dist/structures/interactions.d.ts +12 -52
- package/dist/structures/options.d.ts +55 -0
- package/dist/structures/resources.d.ts +3 -3
- package/dist/types/gateway-events.d.ts +132 -0
- package/dist/types/gateway.d.ts +181 -0
- package/dist/types/index.d.ts +2 -263
- package/dist/types/index.js +44 -2
- package/dist/types/index.js.map +5 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/voice/index.d.ts +29 -1
- package/dist/voice/index.js +59 -7
- 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 +18 -65
- package/dist/ws/index.js +903 -385
- 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 +21 -20
package/dist/sharding/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,37 +772,7 @@ function resolveGatewayIntents(intents) {
|
|
|
73
772
|
}
|
|
74
773
|
return 0;
|
|
75
774
|
}
|
|
76
|
-
|
|
77
|
-
// packages/ws/src/index.ts
|
|
78
|
-
var GatewayOpcodes = {
|
|
79
|
-
Dispatch: 0,
|
|
80
|
-
Heartbeat: 1,
|
|
81
|
-
Identify: 2,
|
|
82
|
-
PresenceUpdate: 3,
|
|
83
|
-
VoiceStateUpdate: 4,
|
|
84
|
-
Resume: 6,
|
|
85
|
-
Reconnect: 7,
|
|
86
|
-
RequestGuildMembers: 8,
|
|
87
|
-
InvalidSession: 9,
|
|
88
|
-
Hello: 10,
|
|
89
|
-
HeartbeatAck: 11
|
|
90
|
-
};
|
|
91
|
-
var GatewayCloseCodes = {
|
|
92
|
-
UnknownError: 4000,
|
|
93
|
-
UnknownOpcode: 4001,
|
|
94
|
-
DecodeError: 4002,
|
|
95
|
-
NotAuthenticated: 4003,
|
|
96
|
-
AuthenticationFailed: 4004,
|
|
97
|
-
AlreadyAuthenticated: 4005,
|
|
98
|
-
InvalidSeq: 4007,
|
|
99
|
-
RateLimited: 4008,
|
|
100
|
-
SessionTimedOut: 4009,
|
|
101
|
-
InvalidShard: 4010,
|
|
102
|
-
ShardingRequired: 4011,
|
|
103
|
-
InvalidAPIVersion: 4012,
|
|
104
|
-
InvalidIntents: 4013,
|
|
105
|
-
DisallowedIntents: 4014
|
|
106
|
-
};
|
|
775
|
+
// packages/ws/src/state.ts
|
|
107
776
|
class GatewayError extends Error {
|
|
108
777
|
code;
|
|
109
778
|
constructor(message, code, options) {
|
|
@@ -112,33 +781,43 @@ class GatewayError extends Error {
|
|
|
112
781
|
this.code = code;
|
|
113
782
|
}
|
|
114
783
|
}
|
|
784
|
+
// packages/ws/src/send-budget.ts
|
|
785
|
+
class SendBudget {
|
|
786
|
+
#timestamps = [];
|
|
787
|
+
#limit;
|
|
788
|
+
#windowMs;
|
|
789
|
+
constructor(limit = 115, windowMs = 60000) {
|
|
790
|
+
this.#limit = limit;
|
|
791
|
+
this.#windowMs = windowMs;
|
|
792
|
+
}
|
|
793
|
+
get used() {
|
|
794
|
+
return this.#timestamps.length;
|
|
795
|
+
}
|
|
796
|
+
allows(privileged, now = Date.now()) {
|
|
797
|
+
while (this.#timestamps.length && now - this.#timestamps[0] >= this.#windowMs)
|
|
798
|
+
this.#timestamps.shift();
|
|
799
|
+
return privileged || this.#timestamps.length < this.#limit;
|
|
800
|
+
}
|
|
801
|
+
record(now = Date.now()) {
|
|
802
|
+
this.#timestamps.push(now);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// packages/ws/src/index.ts
|
|
807
|
+
var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
115
808
|
|
|
116
809
|
class Gateway {
|
|
117
810
|
state = "CONNECT" /* Connect */;
|
|
118
811
|
#options;
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#heartbeatTimer;
|
|
124
|
-
#initialHeartbeat;
|
|
125
|
-
#heartbeatAckTimer;
|
|
126
|
-
#heartbeatACK = true;
|
|
127
|
-
#heartbeatInterval = 0;
|
|
128
|
-
#heartbeatSentAt = 0;
|
|
129
|
-
#lastMessageAt = 0;
|
|
130
|
-
#zombieTimer;
|
|
131
|
-
#zombieReported = false;
|
|
812
|
+
#transport;
|
|
813
|
+
#session = new GatewaySession;
|
|
814
|
+
#token = 0;
|
|
815
|
+
#heartbeat;
|
|
132
816
|
#closed = false;
|
|
133
|
-
#
|
|
134
|
-
#reconnectTimer;
|
|
135
|
-
#connectPromise;
|
|
136
|
-
#resolveConnect;
|
|
137
|
-
#rejectConnect;
|
|
817
|
+
#reconnect;
|
|
138
818
|
#listeners = new Map;
|
|
139
|
-
#
|
|
819
|
+
#sendBudget = new SendBudget;
|
|
140
820
|
ping = -1;
|
|
141
|
-
#decompressor;
|
|
142
821
|
constructor(options) {
|
|
143
822
|
if (!options.token?.trim())
|
|
144
823
|
throw new TypeError("A Gateway token is required.");
|
|
@@ -165,41 +844,72 @@ class Gateway {
|
|
|
165
844
|
afk: false,
|
|
166
845
|
since: null
|
|
167
846
|
},
|
|
168
|
-
...options
|
|
847
|
+
...options,
|
|
848
|
+
intents: resolvedIntents
|
|
169
849
|
};
|
|
170
850
|
if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
|
|
171
851
|
throw new RangeError("Gateway shardId must be within the configured shard count.");
|
|
172
852
|
if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
|
|
173
853
|
throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
|
|
854
|
+
this.#transport = new WebSocketTransport({
|
|
855
|
+
compress: this.#options.compress,
|
|
856
|
+
createSocket: options.createSocket,
|
|
857
|
+
handlers: {
|
|
858
|
+
onOpen: () => this.#onOpen(),
|
|
859
|
+
onFrame: (raw) => this.#message(raw, this.#token),
|
|
860
|
+
onClose: (code) => this.#close(code, this.#token),
|
|
861
|
+
onError: (error) => this.#emitError(error instanceof GatewayError ? error : new GatewayError(error.message, undefined, {
|
|
862
|
+
cause: error
|
|
863
|
+
}))
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
this.#reconnect = new GatewayReconnect({
|
|
867
|
+
enabled: this.#options.reconnect,
|
|
868
|
+
maxAttempts: this.#options.maxReconnectAttempts,
|
|
869
|
+
baseDelay: this.#options.reconnectBaseDelay,
|
|
870
|
+
maxDelay: this.#options.reconnectMaxDelay
|
|
871
|
+
});
|
|
872
|
+
this.#heartbeat = new GatewayHeartbeat({
|
|
873
|
+
ackTimeout: this.#options.heartbeatAckTimeout,
|
|
874
|
+
zombieTimeout: this.#options.zombieTimeout,
|
|
875
|
+
send: (sequence) => this.#dispatch(heartbeatPayload(sequence), true),
|
|
876
|
+
sequence: () => this.#session.sequence,
|
|
877
|
+
isConnected: () => !this.#closed && this.#transport.connected,
|
|
878
|
+
onTimeout: (timeout) => this.#onHeartbeatTimeout(timeout),
|
|
879
|
+
onError: (error) => this.#emitError(error)
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
#onHeartbeatTimeout(timeout) {
|
|
883
|
+
if (timeout.type === "zombie") {
|
|
884
|
+
this.#emit("zombie", {
|
|
885
|
+
silentFor: timeout.silentFor,
|
|
886
|
+
timeout: timeout.deadline
|
|
887
|
+
});
|
|
888
|
+
this.#emitError(new GatewayError(`Gateway connection appears stale after ${timeout.silentFor}ms without traffic.`));
|
|
889
|
+
} else {
|
|
890
|
+
this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${timeout.elapsedMs}ms.`));
|
|
891
|
+
}
|
|
892
|
+
this.#transport.close(1001, timeout.type === "zombie" ? "Zombie Gateway connection" : "Heartbeat timeout");
|
|
174
893
|
}
|
|
175
|
-
connect(url =
|
|
894
|
+
connect(url = DEFAULT_GATEWAY_URL) {
|
|
176
895
|
if (this.state === "CLOSED" /* Closed */)
|
|
177
896
|
throw new GatewayError("Gateway has been permanently closed.");
|
|
178
|
-
|
|
179
|
-
|
|
897
|
+
const inFlight = this.#reconnect.attemptPromise;
|
|
898
|
+
if (inFlight)
|
|
899
|
+
return inFlight;
|
|
900
|
+
if (this.#transport.live)
|
|
901
|
+
return Promise.resolve();
|
|
902
|
+
this.#reconnect.cancel();
|
|
180
903
|
this.#closed = false;
|
|
181
904
|
this.#setState("CONNECT" /* Connect */);
|
|
182
905
|
const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
|
|
183
|
-
|
|
184
|
-
this.#initDecompressor();
|
|
185
|
-
this.#connectPromise = new Promise((resolve, reject) => {
|
|
186
|
-
this.#resolveConnect = resolve;
|
|
187
|
-
this.#rejectConnect = reject;
|
|
188
|
-
this.#open(connectURL);
|
|
189
|
-
});
|
|
190
|
-
return this.#connectPromise;
|
|
906
|
+
return this.#reconnect.attempt(() => this.#open(connectURL));
|
|
191
907
|
}
|
|
192
908
|
close() {
|
|
193
909
|
this.#closed = true;
|
|
194
910
|
this.#clearTimers();
|
|
195
911
|
this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
|
|
196
|
-
|
|
197
|
-
this.#ws = undefined;
|
|
198
|
-
try {
|
|
199
|
-
ws?.close(1000, "Client closed connection");
|
|
200
|
-
} catch (error) {
|
|
201
|
-
this.#emitError(error);
|
|
202
|
-
}
|
|
912
|
+
this.#transport.destroy(1000, "Client closed connection");
|
|
203
913
|
this.#setState("CLOSED" /* Closed */);
|
|
204
914
|
}
|
|
205
915
|
emit(event, data) {
|
|
@@ -224,23 +934,17 @@ class Gateway {
|
|
|
224
934
|
return this.#dispatch(payload, false);
|
|
225
935
|
}
|
|
226
936
|
#dispatch(payload, privileged) {
|
|
227
|
-
if (this.#
|
|
937
|
+
if (!this.#transport.connected)
|
|
228
938
|
return false;
|
|
229
939
|
const now = Date.now();
|
|
230
|
-
|
|
231
|
-
this.#sendTimestamps.shift();
|
|
232
|
-
if (!privileged && this.#sendTimestamps.length >= 115) {
|
|
940
|
+
if (!this.#sendBudget.allows(privileged, now)) {
|
|
233
941
|
this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
|
|
234
942
|
return false;
|
|
235
943
|
}
|
|
236
|
-
|
|
237
|
-
this.#ws.send(JSON.stringify(payload));
|
|
238
|
-
this.#sendTimestamps.push(now);
|
|
239
|
-
return true;
|
|
240
|
-
} catch (error) {
|
|
241
|
-
this.#emitError(error);
|
|
944
|
+
if (!this.#transport.send(JSON.stringify(payload)))
|
|
242
945
|
return false;
|
|
243
|
-
|
|
946
|
+
this.#sendBudget.record(now);
|
|
947
|
+
return true;
|
|
244
948
|
}
|
|
245
949
|
setPresence(data) {
|
|
246
950
|
this.#options.presence = { ...this.#options.presence, ...data };
|
|
@@ -276,127 +980,80 @@ class Gateway {
|
|
|
276
980
|
#open(url) {
|
|
277
981
|
if (this.#closed)
|
|
278
982
|
return;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
} catch (error) {
|
|
283
|
-
const failure = this.#normalizeError(error);
|
|
284
|
-
this.#emitError(failure);
|
|
285
|
-
if (this.#options.reconnect) {
|
|
286
|
-
this.#setState("RECONNECT" /* Reconnect */);
|
|
287
|
-
this.#scheduleReconnect();
|
|
288
|
-
} else
|
|
289
|
-
this.#settleConnect(failure);
|
|
983
|
+
this.#token = this.#session.beginConnection();
|
|
984
|
+
const result = this.#transport.connect(url);
|
|
985
|
+
if (result.ok)
|
|
290
986
|
return;
|
|
987
|
+
const failure = this.#normalizeError(result.error);
|
|
988
|
+
this.#emitError(failure);
|
|
989
|
+
if (this.#options.reconnect) {
|
|
990
|
+
this.#setState("RECONNECT" /* Reconnect */);
|
|
991
|
+
this.#scheduleReconnect();
|
|
992
|
+
} else {
|
|
993
|
+
this.#settleConnect(failure);
|
|
291
994
|
}
|
|
292
|
-
this.#ws = ws;
|
|
293
|
-
ws.addEventListener("open", () => {
|
|
294
|
-
this.#lastMessageAt = Date.now();
|
|
295
|
-
this.#zombieReported = false;
|
|
296
|
-
this.#startZombieDetection();
|
|
297
|
-
this.#emit("open", undefined);
|
|
298
|
-
this.#settleConnect();
|
|
299
|
-
});
|
|
300
|
-
ws.addEventListener("message", (event) => {
|
|
301
|
-
this.#lastMessageAt = Date.now();
|
|
302
|
-
this.#zombieReported = false;
|
|
303
|
-
if (this.#options.compress && event.data instanceof ArrayBuffer) {
|
|
304
|
-
this.#decompress(new Uint8Array(event.data)).then((text) => this.#message(ws, text)).catch((err) => this.#emitError(err));
|
|
305
|
-
} else {
|
|
306
|
-
this.#message(ws, String(event.data));
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
ws.addEventListener("close", (event) => this.#close(ws, event.code));
|
|
310
|
-
ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
|
|
311
|
-
}
|
|
312
|
-
#startZombieDetection() {
|
|
313
|
-
if (this.#zombieTimer)
|
|
314
|
-
clearInterval(this.#zombieTimer);
|
|
315
|
-
const interval = Math.max(1, Math.min(1000, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
|
|
316
|
-
this.#zombieTimer = setInterval(() => {
|
|
317
|
-
if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
|
|
318
|
-
return;
|
|
319
|
-
const silentFor = Date.now() - this.#lastMessageAt;
|
|
320
|
-
const deadline = Math.max(this.#options.zombieTimeout, this.#heartbeatInterval + this.#options.heartbeatAckTimeout);
|
|
321
|
-
if (silentFor < deadline || this.#zombieReported)
|
|
322
|
-
return;
|
|
323
|
-
this.#zombieReported = true;
|
|
324
|
-
const error = new GatewayError(`Gateway connection appears stale after ${silentFor}ms without traffic.`);
|
|
325
|
-
this.#emit("zombie", { silentFor, timeout: deadline });
|
|
326
|
-
this.#emitError(error);
|
|
327
|
-
try {
|
|
328
|
-
this.#ws.close(1001, "Zombie Gateway connection");
|
|
329
|
-
} catch (closeError) {
|
|
330
|
-
this.#emitError(closeError);
|
|
331
|
-
}
|
|
332
|
-
}, interval);
|
|
333
995
|
}
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}));
|
|
342
|
-
ws.close(1002, "Invalid JSON");
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (!payload || typeof payload.op !== "number") {
|
|
346
|
-
this.#emitError(new GatewayError("Gateway returned an invalid payload"));
|
|
347
|
-
ws.close(1002, "Invalid payload");
|
|
996
|
+
#onOpen() {
|
|
997
|
+
this.#heartbeat.watch();
|
|
998
|
+
this.#emit("open", undefined);
|
|
999
|
+
this.#settleConnect();
|
|
1000
|
+
}
|
|
1001
|
+
#message(raw, token) {
|
|
1002
|
+
if (!this.#session.owns(token))
|
|
348
1003
|
return;
|
|
349
|
-
}
|
|
350
|
-
if (
|
|
351
|
-
this.#sequence
|
|
352
|
-
|
|
353
|
-
|
|
1004
|
+
const { sequence, action } = classifyFrame(raw);
|
|
1005
|
+
if (sequence !== null)
|
|
1006
|
+
this.#session.recordSequence(sequence, token);
|
|
1007
|
+
this.#apply(action, token);
|
|
1008
|
+
}
|
|
1009
|
+
#apply(action, token) {
|
|
1010
|
+
switch (action.type) {
|
|
1011
|
+
case "dispatch":
|
|
354
1012
|
this.#setState("DISPATCH" /* Dispatch */);
|
|
355
|
-
this.#handleDispatch(
|
|
356
|
-
|
|
357
|
-
case
|
|
358
|
-
this.#handleHello(
|
|
359
|
-
|
|
360
|
-
case
|
|
1013
|
+
this.#handleDispatch(action.event, action.data, token);
|
|
1014
|
+
return;
|
|
1015
|
+
case "hello":
|
|
1016
|
+
this.#handleHello(action.heartbeatInterval);
|
|
1017
|
+
return;
|
|
1018
|
+
case "heartbeat":
|
|
361
1019
|
this.#setState("HEARTBEAT" /* Heartbeat */);
|
|
362
|
-
this.#sendHeartbeat();
|
|
363
|
-
|
|
364
|
-
case
|
|
365
|
-
this.#
|
|
366
|
-
this.ping =
|
|
367
|
-
this.#
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
this.#resumeURL = undefined;
|
|
379
|
-
}
|
|
380
|
-
this.#emit("invalidSession", resumable);
|
|
381
|
-
ws.close(resumable ? GatewayCloseCodes.UnknownError : 1000, resumable ? "Invalid session (resumable)" : "Invalid session");
|
|
382
|
-
break;
|
|
1020
|
+
this.#heartbeat.sendHeartbeat();
|
|
1021
|
+
return;
|
|
1022
|
+
case "heartbeat-ack":
|
|
1023
|
+
this.#heartbeat.acknowledge();
|
|
1024
|
+
this.ping = this.#heartbeat.latency;
|
|
1025
|
+
this.#emit("heartbeatAck", action.data);
|
|
1026
|
+
return;
|
|
1027
|
+
case "reconnect":
|
|
1028
|
+
this.#transport.close(1001, "Server requested reconnect");
|
|
1029
|
+
return;
|
|
1030
|
+
case "invalid-session": {
|
|
1031
|
+
if (!action.resumable)
|
|
1032
|
+
this.#session.invalidate(token, "invalid-session");
|
|
1033
|
+
this.#emit("invalidSession", action.resumable);
|
|
1034
|
+
this.#transport.close(action.resumable ? GatewayCloseCodes.UnknownError : 1000, action.resumable ? "Invalid session (resumable)" : "Invalid session");
|
|
1035
|
+
return;
|
|
383
1036
|
}
|
|
1037
|
+
case "invalid":
|
|
1038
|
+
this.#emitError(new GatewayError(action.message, undefined, action.cause === undefined ? undefined : { cause: action.cause }));
|
|
1039
|
+
this.#transport.close(1002, action.violation === "invalid-json" ? "Invalid JSON" : action.violation === "invalid-hello" ? "Invalid heartbeat interval" : "Invalid payload");
|
|
1040
|
+
return;
|
|
1041
|
+
case "unknown":
|
|
1042
|
+
return;
|
|
384
1043
|
}
|
|
385
1044
|
}
|
|
386
|
-
#handleDispatch(event, data) {
|
|
1045
|
+
#handleDispatch(event, data, token) {
|
|
387
1046
|
if (event === "READY") {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1047
|
+
if (!this.#session.activate(data, token)) {
|
|
1048
|
+
if (this.#session.owns(token))
|
|
1049
|
+
this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
|
|
391
1050
|
return;
|
|
392
1051
|
}
|
|
393
|
-
this.#
|
|
394
|
-
this.#resumeURL = ready.resume_gateway_url;
|
|
395
|
-
this.#attempt = 0;
|
|
1052
|
+
this.#reconnect.reset();
|
|
396
1053
|
this.#setState("READY" /* Ready */);
|
|
397
1054
|
this.#emit("ready", data);
|
|
398
1055
|
} else if (event === "RESUMED") {
|
|
399
|
-
this.#
|
|
1056
|
+
this.#reconnect.reset();
|
|
400
1057
|
this.#setState("READY" /* Ready */);
|
|
401
1058
|
this.#emit("resumed", data);
|
|
402
1059
|
}
|
|
@@ -405,183 +1062,60 @@ class Gateway {
|
|
|
405
1062
|
return;
|
|
406
1063
|
this.#emit(event ?? "dispatch", data);
|
|
407
1064
|
}
|
|
408
|
-
#handleHello(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
|
|
412
|
-
this.#ws?.close(1002, "Invalid heartbeat interval");
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
this.#heartbeatInterval = interval;
|
|
416
|
-
this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
417
|
-
this.#startHeartbeat(interval);
|
|
1065
|
+
#handleHello(interval) {
|
|
1066
|
+
this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
1067
|
+
this.#heartbeat.start(interval);
|
|
418
1068
|
this.#identifyOrResume();
|
|
419
1069
|
}
|
|
420
1070
|
#identifyOrResume() {
|
|
421
|
-
|
|
1071
|
+
const intent = this.#session.handshake();
|
|
1072
|
+
if (intent.type === "resume") {
|
|
422
1073
|
this.#setState("RESUME" /* Resume */);
|
|
423
|
-
this.
|
|
424
|
-
op: GatewayOpcodes.Resume,
|
|
425
|
-
d: {
|
|
426
|
-
token: this.#options.token,
|
|
427
|
-
session_id: this.#sessionId,
|
|
428
|
-
seq: this.#sequence
|
|
429
|
-
},
|
|
430
|
-
s: null,
|
|
431
|
-
t: null
|
|
432
|
-
});
|
|
1074
|
+
this.#dispatch(resumePayload(this.#options.token, intent), true);
|
|
433
1075
|
return;
|
|
434
1076
|
}
|
|
435
1077
|
this.#setState("IDENTIFY" /* Identify */);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
this.send({
|
|
445
|
-
op: GatewayOpcodes.Identify,
|
|
446
|
-
d: {
|
|
447
|
-
token: this.#options.token,
|
|
448
|
-
intents: this.#options.intents,
|
|
449
|
-
properties: {
|
|
450
|
-
os,
|
|
451
|
-
browser,
|
|
452
|
-
device,
|
|
453
|
-
$os: os,
|
|
454
|
-
$browser: browser,
|
|
455
|
-
$device: device,
|
|
456
|
-
...props
|
|
457
|
-
},
|
|
458
|
-
presence: {
|
|
459
|
-
since: this.#options.presence?.since ?? null,
|
|
460
|
-
activities: this.#options.presence?.activities ?? [],
|
|
461
|
-
status: this.#options.presence?.status ?? "online",
|
|
462
|
-
afk: Boolean(this.#options.presence?.afk)
|
|
463
|
-
},
|
|
464
|
-
shard: [this.#options.shardId, this.#options.shardCount]
|
|
465
|
-
},
|
|
466
|
-
s: null,
|
|
467
|
-
t: null
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
#startHeartbeat(interval) {
|
|
471
|
-
this.#clearHeartbeatTimers();
|
|
472
|
-
this.#heartbeatACK = true;
|
|
473
|
-
this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
|
|
474
|
-
this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
|
|
475
|
-
}
|
|
476
|
-
#sendHeartbeat() {
|
|
477
|
-
this.#heartbeatACK = false;
|
|
478
|
-
this.#heartbeatSentAt = Date.now();
|
|
479
|
-
if (!this.#dispatch({
|
|
480
|
-
op: GatewayOpcodes.Heartbeat,
|
|
481
|
-
d: this.#sequence,
|
|
482
|
-
s: null,
|
|
483
|
-
t: null
|
|
484
|
-
}, true)) {
|
|
485
|
-
this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
this.#clearHeartbeatAckTimer();
|
|
489
|
-
this.#heartbeatAckTimer = setTimeout(() => {
|
|
490
|
-
if (!this.#heartbeatACK) {
|
|
491
|
-
const elapsed = Date.now() - this.#heartbeatSentAt;
|
|
492
|
-
this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
|
|
493
|
-
try {
|
|
494
|
-
this.#ws?.close(1001, "Heartbeat timeout");
|
|
495
|
-
} catch (error) {
|
|
496
|
-
this.#emitError(error);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}, this.#options.heartbeatAckTimeout);
|
|
1078
|
+
this.#dispatch(identifyPayload({
|
|
1079
|
+
token: this.#options.token,
|
|
1080
|
+
intents: this.#options.intents,
|
|
1081
|
+
shardId: this.#options.shardId,
|
|
1082
|
+
shardCount: this.#options.shardCount,
|
|
1083
|
+
properties: this.#options.properties,
|
|
1084
|
+
presence: this.#options.presence
|
|
1085
|
+
}), true);
|
|
500
1086
|
}
|
|
501
|
-
#close(
|
|
502
|
-
if (this.#ws !== ws)
|
|
503
|
-
return;
|
|
504
|
-
this.#ws = undefined;
|
|
1087
|
+
#close(code, token) {
|
|
505
1088
|
this.#clearTimers();
|
|
506
|
-
const action = this.#
|
|
1089
|
+
const action = this.#reconnect.classifyClose(code, this.#session.canResume);
|
|
507
1090
|
this.#emit("close", { code, action });
|
|
508
|
-
if (this.#closed || !this.#
|
|
1091
|
+
if (this.#closed || !this.#reconnect.enabled || action === "stop") {
|
|
1092
|
+
if (action === "stop")
|
|
1093
|
+
this.#closed = true;
|
|
509
1094
|
this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
|
|
510
|
-
if (this.#
|
|
511
|
-
this.#
|
|
1095
|
+
if (this.#reconnect.connecting)
|
|
1096
|
+
this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
|
|
512
1097
|
return;
|
|
513
1098
|
}
|
|
514
|
-
if (action === "identify")
|
|
515
|
-
this.#
|
|
516
|
-
this.#sessionId = undefined;
|
|
517
|
-
this.#resumeURL = undefined;
|
|
518
|
-
}
|
|
1099
|
+
if (action === "identify")
|
|
1100
|
+
this.#session.invalidate(token, "session-timeout");
|
|
519
1101
|
this.#setState("RECONNECT" /* Reconnect */);
|
|
520
1102
|
this.#scheduleReconnect();
|
|
521
1103
|
}
|
|
522
|
-
#closeAction(code) {
|
|
523
|
-
const fatal = [
|
|
524
|
-
GatewayCloseCodes.AuthenticationFailed,
|
|
525
|
-
GatewayCloseCodes.InvalidShard,
|
|
526
|
-
GatewayCloseCodes.ShardingRequired,
|
|
527
|
-
GatewayCloseCodes.InvalidAPIVersion,
|
|
528
|
-
GatewayCloseCodes.InvalidIntents,
|
|
529
|
-
GatewayCloseCodes.DisallowedIntents
|
|
530
|
-
];
|
|
531
|
-
if (fatal.includes(code))
|
|
532
|
-
return "stop";
|
|
533
|
-
if (code === GatewayCloseCodes.InvalidSeq || code === GatewayCloseCodes.SessionTimedOut)
|
|
534
|
-
return "identify";
|
|
535
|
-
return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
|
|
536
|
-
}
|
|
537
1104
|
#scheduleReconnect() {
|
|
538
|
-
if (this.#closed
|
|
1105
|
+
if (this.#closed)
|
|
539
1106
|
return;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
this.#setState("CONNECT" /* Connect */);
|
|
1107
|
+
const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
|
|
1108
|
+
if (result.scheduled || result.reason !== "exhausted")
|
|
543
1109
|
return;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const jitter = Math.random() * Math.max(1, delay * 0.25);
|
|
547
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
548
|
-
this.#reconnectTimer = undefined;
|
|
549
|
-
this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
|
|
550
|
-
}, delay + jitter);
|
|
1110
|
+
this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
|
|
1111
|
+
this.#setState("CONNECT" /* Connect */);
|
|
551
1112
|
}
|
|
552
1113
|
#settleConnect(error) {
|
|
553
|
-
|
|
554
|
-
const reject = this.#rejectConnect;
|
|
555
|
-
this.#resolveConnect = undefined;
|
|
556
|
-
this.#rejectConnect = undefined;
|
|
557
|
-
this.#connectPromise = undefined;
|
|
558
|
-
if (error)
|
|
559
|
-
reject?.(error);
|
|
560
|
-
else
|
|
561
|
-
resolve?.();
|
|
562
|
-
}
|
|
563
|
-
#clearHeartbeatAckTimer() {
|
|
564
|
-
if (this.#heartbeatAckTimer)
|
|
565
|
-
clearTimeout(this.#heartbeatAckTimer);
|
|
566
|
-
this.#heartbeatAckTimer = undefined;
|
|
567
|
-
}
|
|
568
|
-
#clearHeartbeatTimers() {
|
|
569
|
-
if (this.#heartbeatTimer)
|
|
570
|
-
clearInterval(this.#heartbeatTimer);
|
|
571
|
-
if (this.#initialHeartbeat)
|
|
572
|
-
clearTimeout(this.#initialHeartbeat);
|
|
573
|
-
this.#clearHeartbeatAckTimer();
|
|
574
|
-
this.#heartbeatTimer = undefined;
|
|
575
|
-
this.#initialHeartbeat = undefined;
|
|
1114
|
+
this.#reconnect.settle(error);
|
|
576
1115
|
}
|
|
577
1116
|
#clearTimers() {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (this.#zombieTimer)
|
|
581
|
-
clearInterval(this.#zombieTimer);
|
|
582
|
-
this.#reconnectTimer = undefined;
|
|
583
|
-
this.#zombieTimer = undefined;
|
|
584
|
-
this.#clearHeartbeatTimers();
|
|
1117
|
+
this.#reconnect.cancel();
|
|
1118
|
+
this.#heartbeat.stop();
|
|
585
1119
|
}
|
|
586
1120
|
#normalizeError(error) {
|
|
587
1121
|
return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
|
|
@@ -613,47 +1147,20 @@ class Gateway {
|
|
|
613
1147
|
this.state = next;
|
|
614
1148
|
this.#emit("stateChange", { previous, next });
|
|
615
1149
|
}
|
|
616
|
-
#initDecompressor() {
|
|
617
|
-
const ds = new DecompressionStream("deflate-raw");
|
|
618
|
-
this.#decompressor = {
|
|
619
|
-
writer: ds.writable.getWriter(),
|
|
620
|
-
reader: ds.readable.getReader()
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
async#decompress(chunk) {
|
|
624
|
-
if (!this.#decompressor)
|
|
625
|
-
this.#initDecompressor();
|
|
626
|
-
const { writer, reader } = this.#decompressor;
|
|
627
|
-
await writer.write(chunk);
|
|
628
|
-
const parts = [];
|
|
629
|
-
while (true) {
|
|
630
|
-
const { done, value } = await Promise.race([
|
|
631
|
-
reader.read(),
|
|
632
|
-
new Promise((resolve) => setTimeout(() => resolve({ done: true, value: undefined }), 50))
|
|
633
|
-
]);
|
|
634
|
-
if (done || !value)
|
|
635
|
-
break;
|
|
636
|
-
parts.push(value);
|
|
637
|
-
}
|
|
638
|
-
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
639
|
-
const merged = new Uint8Array(total);
|
|
640
|
-
let offset = 0;
|
|
641
|
-
for (const p of parts) {
|
|
642
|
-
merged.set(p, offset);
|
|
643
|
-
offset += p.length;
|
|
644
|
-
}
|
|
645
|
-
return new TextDecoder().decode(merged);
|
|
646
|
-
}
|
|
647
1150
|
}
|
|
648
1151
|
|
|
649
1152
|
// packages/sharding/src/bus.ts
|
|
1153
|
+
var REPLY_TYPE = "__lunibee:reply";
|
|
1154
|
+
|
|
650
1155
|
class ShardBus {
|
|
651
1156
|
#channel;
|
|
652
1157
|
#handlers = new Map;
|
|
1158
|
+
#errorHandlers = new Set;
|
|
653
1159
|
#shardId;
|
|
654
1160
|
#namespace;
|
|
655
1161
|
channelName;
|
|
656
1162
|
#counter = 0;
|
|
1163
|
+
#pending = new Map;
|
|
657
1164
|
constructor(shardId, channelName) {
|
|
658
1165
|
if (!Number.isInteger(shardId) || shardId < 0)
|
|
659
1166
|
throw new RangeError("Shard ID must be a non-negative integer.");
|
|
@@ -678,6 +1185,16 @@ class ShardBus {
|
|
|
678
1185
|
this.#handlers.get(type)?.delete(handler);
|
|
679
1186
|
return this;
|
|
680
1187
|
}
|
|
1188
|
+
onError(handler) {
|
|
1189
|
+
if (typeof handler !== "function")
|
|
1190
|
+
throw new TypeError("Shard bus error handler is required.");
|
|
1191
|
+
this.#errorHandlers.add(handler);
|
|
1192
|
+
return this;
|
|
1193
|
+
}
|
|
1194
|
+
offError(handler) {
|
|
1195
|
+
this.#errorHandlers.delete(handler);
|
|
1196
|
+
return this;
|
|
1197
|
+
}
|
|
681
1198
|
send(target, type, data) {
|
|
682
1199
|
if (!Number.isInteger(target) || target < 0)
|
|
683
1200
|
throw new RangeError("Shard target must be a non-negative integer.");
|
|
@@ -686,11 +1203,56 @@ class ShardBus {
|
|
|
686
1203
|
broadcast(type, data) {
|
|
687
1204
|
return this.#publish(null, type, data);
|
|
688
1205
|
}
|
|
1206
|
+
respond(type, handler) {
|
|
1207
|
+
return this.on(type, async (message) => {
|
|
1208
|
+
if (!message.expectsReply)
|
|
1209
|
+
return;
|
|
1210
|
+
const reply = { requestId: message.id };
|
|
1211
|
+
try {
|
|
1212
|
+
reply.result = await handler(message.data, message);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
reply.error = error instanceof Error ? error.message : String(error);
|
|
1215
|
+
}
|
|
1216
|
+
this.#publish(message.source, REPLY_TYPE, reply);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
async request(target, type, data, timeoutMs = 5000) {
|
|
1220
|
+
if (!Number.isInteger(target) || target < 0)
|
|
1221
|
+
throw new RangeError("Shard target must be a non-negative integer.");
|
|
1222
|
+
const [reply] = await this.#collect(target, type, data, timeoutMs, 1);
|
|
1223
|
+
if (!reply)
|
|
1224
|
+
throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
|
|
1225
|
+
if (reply.error !== undefined)
|
|
1226
|
+
throw new Error(reply.error);
|
|
1227
|
+
return reply.result;
|
|
1228
|
+
}
|
|
1229
|
+
broadcastRequest(type, data, options = {}) {
|
|
1230
|
+
return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
|
|
1231
|
+
}
|
|
1232
|
+
#collect(target, type, data, timeoutMs, expected) {
|
|
1233
|
+
const replies = [];
|
|
1234
|
+
return new Promise((resolve) => {
|
|
1235
|
+
let requestId = "";
|
|
1236
|
+
const finish = () => {
|
|
1237
|
+
clearTimeout(timer);
|
|
1238
|
+
this.#pending.delete(requestId);
|
|
1239
|
+
resolve(replies);
|
|
1240
|
+
};
|
|
1241
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1242
|
+
requestId = this.#publish(target, type, data, true);
|
|
1243
|
+
this.#pending.set(requestId, (reply) => {
|
|
1244
|
+
replies.push(reply);
|
|
1245
|
+
if (replies.length >= expected)
|
|
1246
|
+
finish();
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
689
1250
|
close() {
|
|
690
1251
|
this.#channel.close();
|
|
691
1252
|
this.#handlers.clear();
|
|
1253
|
+
this.#errorHandlers.clear();
|
|
692
1254
|
}
|
|
693
|
-
#publish(target, type, data) {
|
|
1255
|
+
#publish(target, type, data, expectsReply = false) {
|
|
694
1256
|
if (!type.trim())
|
|
695
1257
|
throw new TypeError("Shard message type is required.");
|
|
696
1258
|
const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
|
|
@@ -699,7 +1261,8 @@ class ShardBus {
|
|
|
699
1261
|
target,
|
|
700
1262
|
type,
|
|
701
1263
|
data,
|
|
702
|
-
id
|
|
1264
|
+
id,
|
|
1265
|
+
...expectsReply ? { expectsReply } : {}
|
|
703
1266
|
});
|
|
704
1267
|
return id;
|
|
705
1268
|
}
|
|
@@ -708,13 +1271,28 @@ class ShardBus {
|
|
|
708
1271
|
return;
|
|
709
1272
|
if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
|
|
710
1273
|
return;
|
|
1274
|
+
if (message.type === REPLY_TYPE) {
|
|
1275
|
+
const reply = message.data;
|
|
1276
|
+
this.#pending.get(reply?.requestId)?.({
|
|
1277
|
+
shardId: message.source,
|
|
1278
|
+
...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
711
1282
|
for (const handler of this.#handlers.get(message.type) ?? []) {
|
|
712
1283
|
try {
|
|
713
1284
|
const result = handler(message);
|
|
714
1285
|
if (result && typeof result.then === "function")
|
|
715
|
-
Promise.resolve(result).catch(() =>
|
|
716
|
-
|
|
717
|
-
|
|
1286
|
+
Promise.resolve(result).catch((error) => this.#reportError(error, message));
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
this.#reportError(error, message);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
#reportError(error, message) {
|
|
1293
|
+
for (const handler of this.#errorHandlers) {
|
|
1294
|
+
try {
|
|
1295
|
+
handler(error, message);
|
|
718
1296
|
} catch {}
|
|
719
1297
|
}
|
|
720
1298
|
}
|
|
@@ -731,6 +1309,8 @@ class ClusterManager {
|
|
|
731
1309
|
#auto;
|
|
732
1310
|
#autoScaleTimer;
|
|
733
1311
|
#spawned = false;
|
|
1312
|
+
#stopping = false;
|
|
1313
|
+
#restartTimers = new Set;
|
|
734
1314
|
constructor(options) {
|
|
735
1315
|
if (!options.token?.trim())
|
|
736
1316
|
throw new TypeError("A Discord bot token is required.");
|
|
@@ -775,19 +1355,7 @@ class ClusterManager {
|
|
|
775
1355
|
for (let i = 0;i < clusterCount; i++) {
|
|
776
1356
|
if (chunks[i].length === 0)
|
|
777
1357
|
continue;
|
|
778
|
-
|
|
779
|
-
env: {
|
|
780
|
-
...process.env,
|
|
781
|
-
SHARD_LIST: chunks[i].join(","),
|
|
782
|
-
SHARD_COUNT: count.toString(),
|
|
783
|
-
CLUSTER_ID: i.toString()
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
this.clusters.set(i, {
|
|
787
|
-
id: i,
|
|
788
|
-
process: child,
|
|
789
|
-
shards: chunks[i]
|
|
790
|
-
});
|
|
1358
|
+
this.#launch(i, chunks[i], count);
|
|
791
1359
|
await sleep(500);
|
|
792
1360
|
}
|
|
793
1361
|
if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
|
|
@@ -796,6 +1364,37 @@ class ClusterManager {
|
|
|
796
1364
|
}, this.#options.autoScaleInterval);
|
|
797
1365
|
}
|
|
798
1366
|
}
|
|
1367
|
+
#launch(id, shards, shardCount) {
|
|
1368
|
+
const child = fork(this.#options.script, [], {
|
|
1369
|
+
env: {
|
|
1370
|
+
...process.env,
|
|
1371
|
+
SHARD_LIST: shards.join(","),
|
|
1372
|
+
SHARD_COUNT: shardCount.toString(),
|
|
1373
|
+
CLUSTER_ID: id.toString()
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
const info = { id, process: child, shards };
|
|
1377
|
+
this.clusters.set(id, info);
|
|
1378
|
+
child.once("exit", (code, signal) => {
|
|
1379
|
+
if (this.clusters.get(id)?.process !== child)
|
|
1380
|
+
return;
|
|
1381
|
+
try {
|
|
1382
|
+
this.#options.onClusterExit?.(info, code, signal);
|
|
1383
|
+
} catch {}
|
|
1384
|
+
if (this.#stopping || this.#options.restartOnExit === false)
|
|
1385
|
+
return;
|
|
1386
|
+
this.clusters.delete(id);
|
|
1387
|
+
const timer = setTimeout(() => {
|
|
1388
|
+
this.#restartTimers.delete(timer);
|
|
1389
|
+
if (this.#stopping || !this.#spawned)
|
|
1390
|
+
return;
|
|
1391
|
+
this.#launch(id, shards, shardCount);
|
|
1392
|
+
}, this.#options.restartDelay ?? 5000);
|
|
1393
|
+
timer.unref?.();
|
|
1394
|
+
this.#restartTimers.add(timer);
|
|
1395
|
+
});
|
|
1396
|
+
return info;
|
|
1397
|
+
}
|
|
799
1398
|
async checkAutoScale() {
|
|
800
1399
|
if (!this.#auto)
|
|
801
1400
|
return;
|
|
@@ -816,6 +1415,8 @@ class ClusterManager {
|
|
|
816
1415
|
await this.spawn();
|
|
817
1416
|
}
|
|
818
1417
|
async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
|
|
1418
|
+
this.#stopping = true;
|
|
1419
|
+
this.#clearRestartTimers();
|
|
819
1420
|
if (this.#autoScaleTimer) {
|
|
820
1421
|
clearInterval(this.#autoScaleTimer);
|
|
821
1422
|
this.#autoScaleTimer = undefined;
|
|
@@ -823,6 +1424,12 @@ class ClusterManager {
|
|
|
823
1424
|
await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
|
|
824
1425
|
this.clusters.clear();
|
|
825
1426
|
this.#spawned = false;
|
|
1427
|
+
this.#stopping = false;
|
|
1428
|
+
}
|
|
1429
|
+
#clearRestartTimers() {
|
|
1430
|
+
for (const timer of this.#restartTimers)
|
|
1431
|
+
clearTimeout(timer);
|
|
1432
|
+
this.#restartTimers.clear();
|
|
826
1433
|
}
|
|
827
1434
|
async#shutdownCluster(cluster, timeoutMs) {
|
|
828
1435
|
const child = cluster.process;
|
|
@@ -851,6 +1458,8 @@ class ClusterManager {
|
|
|
851
1458
|
});
|
|
852
1459
|
}
|
|
853
1460
|
killAll() {
|
|
1461
|
+
this.#stopping = true;
|
|
1462
|
+
this.#clearRestartTimers();
|
|
854
1463
|
if (this.#autoScaleTimer) {
|
|
855
1464
|
clearInterval(this.#autoScaleTimer);
|
|
856
1465
|
this.#autoScaleTimer = undefined;
|
|
@@ -862,6 +1471,7 @@ class ClusterManager {
|
|
|
862
1471
|
}
|
|
863
1472
|
this.clusters.clear();
|
|
864
1473
|
this.#spawned = false;
|
|
1474
|
+
this.#stopping = false;
|
|
865
1475
|
}
|
|
866
1476
|
}
|
|
867
1477
|
|
|
@@ -869,6 +1479,7 @@ class ClusterManager {
|
|
|
869
1479
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
870
1480
|
|
|
871
1481
|
class ShardManager {
|
|
1482
|
+
static IDENTIFY_INTERVAL = 5000;
|
|
872
1483
|
shards = new Map;
|
|
873
1484
|
get shardCount() {
|
|
874
1485
|
return this.shards.size;
|
|
@@ -888,8 +1499,13 @@ class ShardManager {
|
|
|
888
1499
|
throw new RangeError("Shard count must be a positive integer.");
|
|
889
1500
|
if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
|
|
890
1501
|
throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
|
|
1502
|
+
if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
|
|
1503
|
+
throw new RangeError("spawnDelay must be a non-negative finite number of milliseconds.");
|
|
891
1504
|
this.#auto = options.shardCount === "auto";
|
|
892
|
-
this.#options = {
|
|
1505
|
+
this.#options = {
|
|
1506
|
+
spawnDelay: ShardManager.IDENTIFY_INTERVAL,
|
|
1507
|
+
...options
|
|
1508
|
+
};
|
|
893
1509
|
if (options.shardCount !== "auto")
|
|
894
1510
|
this.#initialize(count);
|
|
895
1511
|
}
|
|
@@ -997,7 +1613,8 @@ class ShardManager {
|
|
|
997
1613
|
export {
|
|
998
1614
|
ClusterManager,
|
|
999
1615
|
ShardBus,
|
|
1000
|
-
ShardManager
|
|
1616
|
+
ShardManager,
|
|
1617
|
+
ShardManager as ShardingManager
|
|
1001
1618
|
};
|
|
1002
1619
|
|
|
1003
|
-
//# debugId=
|
|
1620
|
+
//# debugId=C1062D33C7999C8E64756E2164756E21
|