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/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,21 +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
|
-
};
|
|
775
|
+
// packages/ws/src/state.ts
|
|
91
776
|
class GatewayError extends Error {
|
|
92
777
|
code;
|
|
93
778
|
constructor(message, code, options) {
|
|
@@ -96,31 +781,43 @@ class GatewayError extends Error {
|
|
|
96
781
|
this.code = code;
|
|
97
782
|
}
|
|
98
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";
|
|
99
808
|
|
|
100
809
|
class Gateway {
|
|
101
810
|
state = "CONNECT" /* Connect */;
|
|
102
811
|
#options;
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#heartbeatTimer;
|
|
108
|
-
#initialHeartbeat;
|
|
109
|
-
#heartbeatAckTimer;
|
|
110
|
-
#heartbeatACK = true;
|
|
111
|
-
#heartbeatInterval = 0;
|
|
112
|
-
#heartbeatSentAt = 0;
|
|
113
|
-
#lastMessageAt = 0;
|
|
114
|
-
#zombieTimer;
|
|
115
|
-
#zombieReported = false;
|
|
812
|
+
#transport;
|
|
813
|
+
#session = new GatewaySession;
|
|
814
|
+
#token = 0;
|
|
815
|
+
#heartbeat;
|
|
116
816
|
#closed = false;
|
|
117
|
-
#
|
|
118
|
-
#reconnectTimer;
|
|
119
|
-
#connectPromise;
|
|
120
|
-
#resolveConnect;
|
|
121
|
-
#rejectConnect;
|
|
817
|
+
#reconnect;
|
|
122
818
|
#listeners = new Map;
|
|
123
|
-
#
|
|
819
|
+
#sendBudget = new SendBudget;
|
|
820
|
+
ping = -1;
|
|
124
821
|
constructor(options) {
|
|
125
822
|
if (!options.token?.trim())
|
|
126
823
|
throw new TypeError("A Gateway token is required.");
|
|
@@ -141,39 +838,78 @@ class Gateway {
|
|
|
141
838
|
browser: "Discord Android",
|
|
142
839
|
device: "Discord Android"
|
|
143
840
|
},
|
|
144
|
-
presence: {
|
|
145
|
-
|
|
841
|
+
presence: {
|
|
842
|
+
status: "online",
|
|
843
|
+
activities: [],
|
|
844
|
+
afk: false,
|
|
845
|
+
since: null
|
|
846
|
+
},
|
|
847
|
+
...options,
|
|
848
|
+
intents: resolvedIntents
|
|
146
849
|
};
|
|
147
850
|
if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
|
|
148
851
|
throw new RangeError("Gateway shardId must be within the configured shard count.");
|
|
149
852
|
if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
|
|
150
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
|
+
});
|
|
151
881
|
}
|
|
152
|
-
|
|
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");
|
|
893
|
+
}
|
|
894
|
+
connect(url = DEFAULT_GATEWAY_URL) {
|
|
153
895
|
if (this.state === "CLOSED" /* Closed */)
|
|
154
896
|
throw new GatewayError("Gateway has been permanently closed.");
|
|
155
|
-
|
|
156
|
-
|
|
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();
|
|
157
903
|
this.#closed = false;
|
|
158
904
|
this.#setState("CONNECT" /* Connect */);
|
|
159
|
-
this.#
|
|
160
|
-
|
|
161
|
-
this.#rejectConnect = reject;
|
|
162
|
-
this.#open(url);
|
|
163
|
-
});
|
|
164
|
-
return this.#connectPromise;
|
|
905
|
+
const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
|
|
906
|
+
return this.#reconnect.attempt(() => this.#open(connectURL));
|
|
165
907
|
}
|
|
166
908
|
close() {
|
|
167
909
|
this.#closed = true;
|
|
168
910
|
this.#clearTimers();
|
|
169
911
|
this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
|
|
170
|
-
|
|
171
|
-
this.#ws = undefined;
|
|
172
|
-
try {
|
|
173
|
-
ws?.close(1000, "Client closed connection");
|
|
174
|
-
} catch (error) {
|
|
175
|
-
this.#emitError(error);
|
|
176
|
-
}
|
|
912
|
+
this.#transport.destroy(1000, "Client closed connection");
|
|
177
913
|
this.#setState("CLOSED" /* Closed */);
|
|
178
914
|
}
|
|
179
915
|
emit(event, data) {
|
|
@@ -195,23 +931,20 @@ class Gateway {
|
|
|
195
931
|
send(payload) {
|
|
196
932
|
if (!payload || !Number.isInteger(payload.op))
|
|
197
933
|
throw new TypeError("Gateway payload must contain an integer opcode.");
|
|
198
|
-
|
|
934
|
+
return this.#dispatch(payload, false);
|
|
935
|
+
}
|
|
936
|
+
#dispatch(payload, privileged) {
|
|
937
|
+
if (!this.#transport.connected)
|
|
199
938
|
return false;
|
|
200
939
|
const now = Date.now();
|
|
201
|
-
|
|
202
|
-
this.#sendTimestamps.shift();
|
|
203
|
-
if (this.#sendTimestamps.length >= 115) {
|
|
940
|
+
if (!this.#sendBudget.allows(privileged, now)) {
|
|
204
941
|
this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
|
|
205
942
|
return false;
|
|
206
943
|
}
|
|
207
|
-
|
|
208
|
-
this.#ws.send(JSON.stringify(payload));
|
|
209
|
-
this.#sendTimestamps.push(now);
|
|
210
|
-
return true;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
this.#emitError(error);
|
|
944
|
+
if (!this.#transport.send(JSON.stringify(payload)))
|
|
213
945
|
return false;
|
|
214
|
-
|
|
946
|
+
this.#sendBudget.record(now);
|
|
947
|
+
return true;
|
|
215
948
|
}
|
|
216
949
|
setPresence(data) {
|
|
217
950
|
this.#options.presence = { ...this.#options.presence, ...data };
|
|
@@ -247,289 +980,142 @@ class Gateway {
|
|
|
247
980
|
#open(url) {
|
|
248
981
|
if (this.#closed)
|
|
249
982
|
return;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
} catch (error) {
|
|
254
|
-
const failure = this.#normalizeError(error);
|
|
255
|
-
this.#emitError(failure);
|
|
256
|
-
if (this.#options.reconnect) {
|
|
257
|
-
this.#setState("RECONNECT" /* Reconnect */);
|
|
258
|
-
this.#scheduleReconnect();
|
|
259
|
-
} else
|
|
260
|
-
this.#settleConnect(failure);
|
|
983
|
+
this.#token = this.#session.beginConnection();
|
|
984
|
+
const result = this.#transport.connect(url);
|
|
985
|
+
if (result.ok)
|
|
261
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);
|
|
262
994
|
}
|
|
263
|
-
this.#ws = ws;
|
|
264
|
-
ws.addEventListener("open", () => {
|
|
265
|
-
this.#lastMessageAt = Date.now();
|
|
266
|
-
this.#zombieReported = false;
|
|
267
|
-
this.#startZombieDetection();
|
|
268
|
-
this.#emit("open", undefined);
|
|
269
|
-
this.#settleConnect();
|
|
270
|
-
});
|
|
271
|
-
ws.addEventListener("message", (event) => {
|
|
272
|
-
this.#lastMessageAt = Date.now();
|
|
273
|
-
this.#zombieReported = false;
|
|
274
|
-
this.#message(ws, String(event.data));
|
|
275
|
-
});
|
|
276
|
-
ws.addEventListener("close", (event) => this.#close(ws, event.code));
|
|
277
|
-
ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
|
|
278
|
-
}
|
|
279
|
-
#startZombieDetection() {
|
|
280
|
-
if (this.#zombieTimer)
|
|
281
|
-
clearInterval(this.#zombieTimer);
|
|
282
|
-
const interval = Math.max(1, Math.min(250, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
|
|
283
|
-
this.#zombieTimer = setInterval(() => {
|
|
284
|
-
if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
|
|
285
|
-
return;
|
|
286
|
-
const silentFor = Date.now() - this.#lastMessageAt;
|
|
287
|
-
const deadline = Math.max(this.#options.zombieTimeout, this.#heartbeatInterval + this.#options.heartbeatAckTimeout);
|
|
288
|
-
if (silentFor < deadline || this.#zombieReported)
|
|
289
|
-
return;
|
|
290
|
-
this.#zombieReported = true;
|
|
291
|
-
const error = new GatewayError(`Gateway connection appears stale after ${silentFor}ms without traffic.`);
|
|
292
|
-
this.#emit("zombie", { silentFor, timeout: deadline });
|
|
293
|
-
this.#emitError(error);
|
|
294
|
-
try {
|
|
295
|
-
this.#ws.close(1001, "Zombie Gateway connection");
|
|
296
|
-
} catch (closeError) {
|
|
297
|
-
this.#emitError(closeError);
|
|
298
|
-
}
|
|
299
|
-
}, interval);
|
|
300
995
|
}
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}));
|
|
309
|
-
ws.close(1002, "Invalid JSON");
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
if (!payload || typeof payload.op !== "number") {
|
|
313
|
-
this.#emitError(new GatewayError("Gateway returned an invalid payload"));
|
|
314
|
-
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))
|
|
315
1003
|
return;
|
|
316
|
-
}
|
|
317
|
-
if (
|
|
318
|
-
this.#sequence
|
|
319
|
-
|
|
320
|
-
|
|
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":
|
|
321
1012
|
this.#setState("DISPATCH" /* Dispatch */);
|
|
322
|
-
this.#handleDispatch(
|
|
323
|
-
|
|
324
|
-
case
|
|
325
|
-
this.#handleHello(
|
|
326
|
-
|
|
327
|
-
case
|
|
1013
|
+
this.#handleDispatch(action.event, action.data, token);
|
|
1014
|
+
return;
|
|
1015
|
+
case "hello":
|
|
1016
|
+
this.#handleHello(action.heartbeatInterval);
|
|
1017
|
+
return;
|
|
1018
|
+
case "heartbeat":
|
|
328
1019
|
this.#setState("HEARTBEAT" /* Heartbeat */);
|
|
329
|
-
this.#sendHeartbeat();
|
|
330
|
-
|
|
331
|
-
case
|
|
332
|
-
this.#
|
|
333
|
-
this.#
|
|
334
|
-
this.#emit("heartbeatAck",
|
|
335
|
-
|
|
336
|
-
case
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
case
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
this.#emit("invalidSession",
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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;
|
|
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;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
#handleDispatch(event, data, token) {
|
|
348
1046
|
if (event === "READY") {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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"));
|
|
352
1050
|
return;
|
|
353
1051
|
}
|
|
354
|
-
this.#
|
|
355
|
-
this.#resumeURL = ready.resume_gateway_url;
|
|
356
|
-
this.#attempt = 0;
|
|
1052
|
+
this.#reconnect.reset();
|
|
357
1053
|
this.#setState("READY" /* Ready */);
|
|
358
1054
|
this.#emit("ready", data);
|
|
1055
|
+
} else if (event === "RESUMED") {
|
|
1056
|
+
this.#reconnect.reset();
|
|
1057
|
+
this.#setState("READY" /* Ready */);
|
|
1058
|
+
this.#emit("resumed", data);
|
|
359
1059
|
}
|
|
360
1060
|
this.#emit("RAW", { event: event ?? "UNKNOWN", data });
|
|
361
1061
|
if (this.#closed)
|
|
362
1062
|
return;
|
|
363
1063
|
this.#emit(event ?? "dispatch", data);
|
|
364
1064
|
}
|
|
365
|
-
#handleHello(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
this.#emitError(new GatewayError("Gateway HELLO payload contains an invalid heartbeat interval"));
|
|
369
|
-
this.#ws?.close(1002, "Invalid heartbeat interval");
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
this.#heartbeatInterval = interval;
|
|
373
|
-
this.#setState(this.#sessionId && this.#sequence !== null && this.#resumeURL ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
374
|
-
this.#startHeartbeat(interval);
|
|
1065
|
+
#handleHello(interval) {
|
|
1066
|
+
this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
1067
|
+
this.#heartbeat.start(interval);
|
|
375
1068
|
this.#identifyOrResume();
|
|
376
1069
|
}
|
|
377
1070
|
#identifyOrResume() {
|
|
378
|
-
|
|
1071
|
+
const intent = this.#session.handshake();
|
|
1072
|
+
if (intent.type === "resume") {
|
|
379
1073
|
this.#setState("RESUME" /* Resume */);
|
|
380
|
-
this.
|
|
381
|
-
op: GatewayOpcodes.Resume,
|
|
382
|
-
d: {
|
|
383
|
-
token: this.#options.token,
|
|
384
|
-
session_id: this.#sessionId,
|
|
385
|
-
seq: this.#sequence
|
|
386
|
-
},
|
|
387
|
-
s: null,
|
|
388
|
-
t: null
|
|
389
|
-
});
|
|
1074
|
+
this.#dispatch(resumePayload(this.#options.token, intent), true);
|
|
390
1075
|
return;
|
|
391
1076
|
}
|
|
392
1077
|
this.#setState("IDENTIFY" /* Identify */);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
this.send({
|
|
402
|
-
op: GatewayOpcodes.Identify,
|
|
403
|
-
d: {
|
|
404
|
-
token: this.#options.token,
|
|
405
|
-
intents: this.#options.intents,
|
|
406
|
-
properties: {
|
|
407
|
-
os,
|
|
408
|
-
browser,
|
|
409
|
-
device,
|
|
410
|
-
$os: os,
|
|
411
|
-
$browser: browser,
|
|
412
|
-
$device: device,
|
|
413
|
-
...props
|
|
414
|
-
},
|
|
415
|
-
presence: {
|
|
416
|
-
since: this.#options.presence?.since ?? null,
|
|
417
|
-
activities: this.#options.presence?.activities ?? [],
|
|
418
|
-
status: this.#options.presence?.status ?? "online",
|
|
419
|
-
afk: Boolean(this.#options.presence?.afk)
|
|
420
|
-
},
|
|
421
|
-
shard: [this.#options.shardId, this.#options.shardCount]
|
|
422
|
-
},
|
|
423
|
-
s: null,
|
|
424
|
-
t: null
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
#startHeartbeat(interval) {
|
|
428
|
-
this.#clearHeartbeatTimers();
|
|
429
|
-
this.#heartbeatACK = true;
|
|
430
|
-
this.#initialHeartbeat = setTimeout(() => this.#sendHeartbeat(), Math.random() * interval);
|
|
431
|
-
this.#heartbeatTimer = setInterval(() => this.#sendHeartbeat(), interval);
|
|
432
|
-
}
|
|
433
|
-
#sendHeartbeat() {
|
|
434
|
-
this.#heartbeatACK = false;
|
|
435
|
-
this.#heartbeatSentAt = Date.now();
|
|
436
|
-
if (!this.send({
|
|
437
|
-
op: GatewayOpcodes.Heartbeat,
|
|
438
|
-
d: this.#sequence,
|
|
439
|
-
s: null,
|
|
440
|
-
t: null
|
|
441
|
-
})) {
|
|
442
|
-
this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
this.#clearHeartbeatAckTimer();
|
|
446
|
-
this.#heartbeatAckTimer = setTimeout(() => {
|
|
447
|
-
if (!this.#heartbeatACK) {
|
|
448
|
-
const elapsed = Date.now() - this.#heartbeatSentAt;
|
|
449
|
-
this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${elapsed}ms.`));
|
|
450
|
-
try {
|
|
451
|
-
this.#ws?.close(1001, "Heartbeat timeout");
|
|
452
|
-
} catch (error) {
|
|
453
|
-
this.#emitError(error);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}, 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);
|
|
457
1086
|
}
|
|
458
|
-
#close(
|
|
459
|
-
if (this.#ws !== ws)
|
|
460
|
-
return;
|
|
461
|
-
this.#ws = undefined;
|
|
1087
|
+
#close(code, token) {
|
|
462
1088
|
this.#clearTimers();
|
|
463
|
-
const action = this.#
|
|
1089
|
+
const action = this.#reconnect.classifyClose(code, this.#session.canResume);
|
|
464
1090
|
this.#emit("close", { code, action });
|
|
465
|
-
if (this.#closed || !this.#
|
|
1091
|
+
if (this.#closed || !this.#reconnect.enabled || action === "stop") {
|
|
1092
|
+
if (action === "stop")
|
|
1093
|
+
this.#closed = true;
|
|
466
1094
|
this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
|
|
467
|
-
if (this.#
|
|
468
|
-
this.#
|
|
1095
|
+
if (this.#reconnect.connecting)
|
|
1096
|
+
this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
|
|
469
1097
|
return;
|
|
470
1098
|
}
|
|
471
|
-
if (action === "identify")
|
|
472
|
-
this.#
|
|
473
|
-
this.#sessionId = undefined;
|
|
474
|
-
}
|
|
1099
|
+
if (action === "identify")
|
|
1100
|
+
this.#session.invalidate(token, "session-timeout");
|
|
475
1101
|
this.#setState("RECONNECT" /* Reconnect */);
|
|
476
1102
|
this.#scheduleReconnect();
|
|
477
1103
|
}
|
|
478
|
-
#closeAction(code) {
|
|
479
|
-
if ([4004, 4010, 4011, 4012, 4013, 4014].includes(code))
|
|
480
|
-
return "stop";
|
|
481
|
-
if ([4007, 4009].includes(code))
|
|
482
|
-
return "identify";
|
|
483
|
-
return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
|
|
484
|
-
}
|
|
485
1104
|
#scheduleReconnect() {
|
|
486
|
-
if (this.#closed
|
|
1105
|
+
if (this.#closed)
|
|
487
1106
|
return;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
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")
|
|
491
1109
|
return;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const jitter = Math.random() * Math.min(250, Math.max(1, delay * 0.25));
|
|
495
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
496
|
-
this.#reconnectTimer = undefined;
|
|
497
|
-
this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
|
|
498
|
-
}, delay + jitter);
|
|
1110
|
+
this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
|
|
1111
|
+
this.#setState("CONNECT" /* Connect */);
|
|
499
1112
|
}
|
|
500
1113
|
#settleConnect(error) {
|
|
501
|
-
|
|
502
|
-
const reject = this.#rejectConnect;
|
|
503
|
-
this.#resolveConnect = undefined;
|
|
504
|
-
this.#rejectConnect = undefined;
|
|
505
|
-
this.#connectPromise = undefined;
|
|
506
|
-
if (error)
|
|
507
|
-
reject?.(error);
|
|
508
|
-
else
|
|
509
|
-
resolve?.();
|
|
510
|
-
}
|
|
511
|
-
#clearHeartbeatAckTimer() {
|
|
512
|
-
if (this.#heartbeatAckTimer)
|
|
513
|
-
clearTimeout(this.#heartbeatAckTimer);
|
|
514
|
-
this.#heartbeatAckTimer = undefined;
|
|
515
|
-
}
|
|
516
|
-
#clearHeartbeatTimers() {
|
|
517
|
-
if (this.#heartbeatTimer)
|
|
518
|
-
clearInterval(this.#heartbeatTimer);
|
|
519
|
-
if (this.#initialHeartbeat)
|
|
520
|
-
clearTimeout(this.#initialHeartbeat);
|
|
521
|
-
this.#clearHeartbeatAckTimer();
|
|
522
|
-
this.#heartbeatTimer = undefined;
|
|
523
|
-
this.#initialHeartbeat = undefined;
|
|
1114
|
+
this.#reconnect.settle(error);
|
|
524
1115
|
}
|
|
525
1116
|
#clearTimers() {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (this.#zombieTimer)
|
|
529
|
-
clearInterval(this.#zombieTimer);
|
|
530
|
-
this.#reconnectTimer = undefined;
|
|
531
|
-
this.#zombieTimer = undefined;
|
|
532
|
-
this.#clearHeartbeatTimers();
|
|
1117
|
+
this.#reconnect.cancel();
|
|
1118
|
+
this.#heartbeat.stop();
|
|
533
1119
|
}
|
|
534
1120
|
#normalizeError(error) {
|
|
535
1121
|
return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
|
|
@@ -564,13 +1150,17 @@ class Gateway {
|
|
|
564
1150
|
}
|
|
565
1151
|
|
|
566
1152
|
// packages/sharding/src/bus.ts
|
|
1153
|
+
var REPLY_TYPE = "__lunibee:reply";
|
|
1154
|
+
|
|
567
1155
|
class ShardBus {
|
|
568
1156
|
#channel;
|
|
569
1157
|
#handlers = new Map;
|
|
1158
|
+
#errorHandlers = new Set;
|
|
570
1159
|
#shardId;
|
|
571
1160
|
#namespace;
|
|
572
1161
|
channelName;
|
|
573
1162
|
#counter = 0;
|
|
1163
|
+
#pending = new Map;
|
|
574
1164
|
constructor(shardId, channelName) {
|
|
575
1165
|
if (!Number.isInteger(shardId) || shardId < 0)
|
|
576
1166
|
throw new RangeError("Shard ID must be a non-negative integer.");
|
|
@@ -595,6 +1185,16 @@ class ShardBus {
|
|
|
595
1185
|
this.#handlers.get(type)?.delete(handler);
|
|
596
1186
|
return this;
|
|
597
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
|
+
}
|
|
598
1198
|
send(target, type, data) {
|
|
599
1199
|
if (!Number.isInteger(target) || target < 0)
|
|
600
1200
|
throw new RangeError("Shard target must be a non-negative integer.");
|
|
@@ -603,11 +1203,56 @@ class ShardBus {
|
|
|
603
1203
|
broadcast(type, data) {
|
|
604
1204
|
return this.#publish(null, type, data);
|
|
605
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
|
+
}
|
|
606
1250
|
close() {
|
|
607
1251
|
this.#channel.close();
|
|
608
1252
|
this.#handlers.clear();
|
|
1253
|
+
this.#errorHandlers.clear();
|
|
609
1254
|
}
|
|
610
|
-
#publish(target, type, data) {
|
|
1255
|
+
#publish(target, type, data, expectsReply = false) {
|
|
611
1256
|
if (!type.trim())
|
|
612
1257
|
throw new TypeError("Shard message type is required.");
|
|
613
1258
|
const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
|
|
@@ -616,7 +1261,8 @@ class ShardBus {
|
|
|
616
1261
|
target,
|
|
617
1262
|
type,
|
|
618
1263
|
data,
|
|
619
|
-
id
|
|
1264
|
+
id,
|
|
1265
|
+
...expectsReply ? { expectsReply } : {}
|
|
620
1266
|
});
|
|
621
1267
|
return id;
|
|
622
1268
|
}
|
|
@@ -625,20 +1271,215 @@ class ShardBus {
|
|
|
625
1271
|
return;
|
|
626
1272
|
if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
|
|
627
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
|
+
}
|
|
628
1282
|
for (const handler of this.#handlers.get(message.type) ?? []) {
|
|
629
1283
|
try {
|
|
630
1284
|
const result = handler(message);
|
|
631
1285
|
if (result && typeof result.then === "function")
|
|
632
|
-
Promise.resolve(result).catch(() =>
|
|
633
|
-
|
|
634
|
-
|
|
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);
|
|
1296
|
+
} catch {}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// packages/sharding/src/cluster.ts
|
|
1301
|
+
import { fork } from "child_process";
|
|
1302
|
+
import { cpus } from "os";
|
|
1303
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1304
|
+
|
|
1305
|
+
class ClusterManager {
|
|
1306
|
+
clusters = new Map;
|
|
1307
|
+
shardCount = 0;
|
|
1308
|
+
#options;
|
|
1309
|
+
#auto;
|
|
1310
|
+
#autoScaleTimer;
|
|
1311
|
+
#spawned = false;
|
|
1312
|
+
#stopping = false;
|
|
1313
|
+
#restartTimers = new Set;
|
|
1314
|
+
constructor(options) {
|
|
1315
|
+
if (!options.token?.trim())
|
|
1316
|
+
throw new TypeError("A Discord bot token is required.");
|
|
1317
|
+
if (!options.script?.trim())
|
|
1318
|
+
throw new TypeError("A script path is required for clustering.");
|
|
1319
|
+
if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
|
|
1320
|
+
throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
|
|
1321
|
+
}
|
|
1322
|
+
this.#auto = options.shardCount === "auto";
|
|
1323
|
+
this.#options = { ...options };
|
|
1324
|
+
}
|
|
1325
|
+
async fetchRecommendedShardCount() {
|
|
1326
|
+
const response = await fetch("https://discord.com/api/v10/gateway/bot", {
|
|
1327
|
+
headers: {
|
|
1328
|
+
Authorization: `Bot ${this.#options.token}`,
|
|
1329
|
+
"User-Agent": "Lunibee/0.1.0"
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
if (!response.ok) {
|
|
1333
|
+
throw new Error(`Gateway discovery failed with status ${response.status}`);
|
|
1334
|
+
}
|
|
1335
|
+
const data = await response.json();
|
|
1336
|
+
if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1) {
|
|
1337
|
+
throw new Error("Gateway discovery returned an invalid shard count.");
|
|
1338
|
+
}
|
|
1339
|
+
return data.shards;
|
|
1340
|
+
}
|
|
1341
|
+
async spawn() {
|
|
1342
|
+
if (this.#spawned)
|
|
1343
|
+
return;
|
|
1344
|
+
if (typeof process?.versions?.bun === "string") {
|
|
1345
|
+
throw new Error("ClusterManager requires the Node.js runtime; Bun does not support child_process.fork() for this clustering mode.");
|
|
1346
|
+
}
|
|
1347
|
+
this.#spawned = true;
|
|
1348
|
+
const count = this.#options.shardCount === "auto" || this.#options.shardCount === undefined ? await this.fetchRecommendedShardCount() : this.#options.shardCount;
|
|
1349
|
+
this.shardCount = count;
|
|
1350
|
+
const clusterCount = this.#options.clusterCount ?? cpus().length;
|
|
1351
|
+
const chunks = Array.from({ length: clusterCount }, () => []);
|
|
1352
|
+
for (let i = 0;i < count; i++) {
|
|
1353
|
+
chunks[i % clusterCount].push(i);
|
|
1354
|
+
}
|
|
1355
|
+
for (let i = 0;i < clusterCount; i++) {
|
|
1356
|
+
if (chunks[i].length === 0)
|
|
1357
|
+
continue;
|
|
1358
|
+
this.#launch(i, chunks[i], count);
|
|
1359
|
+
await sleep(500);
|
|
1360
|
+
}
|
|
1361
|
+
if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
|
|
1362
|
+
this.#autoScaleTimer = setInterval(() => {
|
|
1363
|
+
this.checkAutoScale();
|
|
1364
|
+
}, this.#options.autoScaleInterval);
|
|
1365
|
+
}
|
|
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);
|
|
635
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
|
+
}
|
|
1398
|
+
async checkAutoScale() {
|
|
1399
|
+
if (!this.#auto)
|
|
1400
|
+
return;
|
|
1401
|
+
try {
|
|
1402
|
+
const recommended = await this.fetchRecommendedShardCount();
|
|
1403
|
+
if (recommended !== this.shardCount) {
|
|
1404
|
+
await this.respawn(recommended);
|
|
1405
|
+
}
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1408
|
+
this.#options.onAutoScaleError?.(normalized);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async respawn(newShardCount) {
|
|
1412
|
+
this.#options.shardCount = newShardCount;
|
|
1413
|
+
await this.shutdownAll();
|
|
1414
|
+
this.#spawned = false;
|
|
1415
|
+
await this.spawn();
|
|
1416
|
+
}
|
|
1417
|
+
async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
|
|
1418
|
+
this.#stopping = true;
|
|
1419
|
+
this.#clearRestartTimers();
|
|
1420
|
+
if (this.#autoScaleTimer) {
|
|
1421
|
+
clearInterval(this.#autoScaleTimer);
|
|
1422
|
+
this.#autoScaleTimer = undefined;
|
|
636
1423
|
}
|
|
1424
|
+
await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
|
|
1425
|
+
this.clusters.clear();
|
|
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();
|
|
1433
|
+
}
|
|
1434
|
+
async#shutdownCluster(cluster, timeoutMs) {
|
|
1435
|
+
const child = cluster.process;
|
|
1436
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
1437
|
+
return;
|
|
1438
|
+
await new Promise((resolve) => {
|
|
1439
|
+
let timer;
|
|
1440
|
+
const finish = () => {
|
|
1441
|
+
if (timer)
|
|
1442
|
+
clearTimeout(timer);
|
|
1443
|
+
resolve();
|
|
1444
|
+
};
|
|
1445
|
+
child.once("exit", finish);
|
|
1446
|
+
try {
|
|
1447
|
+
child.kill("SIGTERM");
|
|
1448
|
+
} catch {
|
|
1449
|
+
child.off("exit", finish);
|
|
1450
|
+
finish();
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
timer = setTimeout(() => {
|
|
1454
|
+
try {
|
|
1455
|
+
child.kill("SIGKILL");
|
|
1456
|
+
} catch {}
|
|
1457
|
+
}, timeoutMs);
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
killAll() {
|
|
1461
|
+
this.#stopping = true;
|
|
1462
|
+
this.#clearRestartTimers();
|
|
1463
|
+
if (this.#autoScaleTimer) {
|
|
1464
|
+
clearInterval(this.#autoScaleTimer);
|
|
1465
|
+
this.#autoScaleTimer = undefined;
|
|
1466
|
+
}
|
|
1467
|
+
for (const cluster of this.clusters.values()) {
|
|
1468
|
+
try {
|
|
1469
|
+
cluster.process.kill();
|
|
1470
|
+
} catch {}
|
|
1471
|
+
}
|
|
1472
|
+
this.clusters.clear();
|
|
1473
|
+
this.#spawned = false;
|
|
1474
|
+
this.#stopping = false;
|
|
637
1475
|
}
|
|
638
1476
|
}
|
|
639
1477
|
|
|
640
1478
|
// packages/sharding/src/index.ts
|
|
1479
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1480
|
+
|
|
641
1481
|
class ShardManager {
|
|
1482
|
+
static IDENTIFY_INTERVAL = 5000;
|
|
642
1483
|
shards = new Map;
|
|
643
1484
|
get shardCount() {
|
|
644
1485
|
return this.shards.size;
|
|
@@ -646,6 +1487,8 @@ class ShardManager {
|
|
|
646
1487
|
#options;
|
|
647
1488
|
#resolved = false;
|
|
648
1489
|
#destroyed = false;
|
|
1490
|
+
#auto;
|
|
1491
|
+
#autoScaleTimer;
|
|
649
1492
|
constructor(options) {
|
|
650
1493
|
if (!options.token?.trim())
|
|
651
1494
|
throw new TypeError("A Discord bot token is required.");
|
|
@@ -654,7 +1497,15 @@ class ShardManager {
|
|
|
654
1497
|
const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
|
|
655
1498
|
if (!Number.isInteger(count) || count < 1)
|
|
656
1499
|
throw new RangeError("Shard count must be a positive integer.");
|
|
657
|
-
|
|
1500
|
+
if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
|
|
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.");
|
|
1504
|
+
this.#auto = options.shardCount === "auto";
|
|
1505
|
+
this.#options = {
|
|
1506
|
+
spawnDelay: ShardManager.IDENTIFY_INTERVAL,
|
|
1507
|
+
...options
|
|
1508
|
+
};
|
|
658
1509
|
if (options.shardCount !== "auto")
|
|
659
1510
|
this.#initialize(count);
|
|
660
1511
|
}
|
|
@@ -673,24 +1524,56 @@ class ShardManager {
|
|
|
673
1524
|
return data.shards;
|
|
674
1525
|
}
|
|
675
1526
|
async connect() {
|
|
1527
|
+
if (this.#autoScaleTimer) {
|
|
1528
|
+
clearInterval(this.#autoScaleTimer);
|
|
1529
|
+
this.#autoScaleTimer = undefined;
|
|
1530
|
+
}
|
|
676
1531
|
await this.#ensureInitialized();
|
|
677
1532
|
for (const [id, shard] of this.shards) {
|
|
678
1533
|
try {
|
|
679
1534
|
await shard.connect();
|
|
680
1535
|
} catch (error) {
|
|
681
1536
|
this.destroy();
|
|
682
|
-
throw new Error(`Failed to connect shard ${id}.`, {
|
|
1537
|
+
throw new Error(`Failed to connect shard ${id}.`, {
|
|
1538
|
+
cause: error
|
|
1539
|
+
});
|
|
683
1540
|
}
|
|
684
1541
|
if (id + 1 < this.shardCount && this.#options.spawnDelay && this.#options.spawnDelay > 0)
|
|
685
|
-
await
|
|
1542
|
+
await sleep2(this.#options.spawnDelay);
|
|
1543
|
+
}
|
|
1544
|
+
if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
|
|
1545
|
+
this.#autoScaleTimer = setInterval(() => {
|
|
1546
|
+
this.checkAutoScale();
|
|
1547
|
+
}, this.#options.autoScaleInterval);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async checkAutoScale() {
|
|
1551
|
+
if (!this.#auto)
|
|
1552
|
+
return;
|
|
1553
|
+
try {
|
|
1554
|
+
const recommended = await this.fetchRecommendedShardCount();
|
|
1555
|
+
if (recommended !== this.shardCount) {
|
|
1556
|
+
await this.reshard(recommended);
|
|
1557
|
+
}
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
this.#options.onAutoScaleError?.(error);
|
|
686
1560
|
}
|
|
687
1561
|
}
|
|
1562
|
+
async reshard(count) {
|
|
1563
|
+
this.#options.shardCount = count;
|
|
1564
|
+
this.destroy();
|
|
1565
|
+
await this.connect();
|
|
1566
|
+
}
|
|
688
1567
|
destroy() {
|
|
689
1568
|
for (const shard of this.shards.values())
|
|
690
1569
|
shard.close();
|
|
691
1570
|
this.shards.clear();
|
|
692
1571
|
this.#resolved = false;
|
|
693
1572
|
this.#destroyed = true;
|
|
1573
|
+
if (this.#autoScaleTimer) {
|
|
1574
|
+
clearInterval(this.#autoScaleTimer);
|
|
1575
|
+
this.#autoScaleTimer = undefined;
|
|
1576
|
+
}
|
|
694
1577
|
}
|
|
695
1578
|
getShardIdForGuild(guildId) {
|
|
696
1579
|
const id = BigInt(guildId);
|
|
@@ -728,8 +1611,10 @@ class ShardManager {
|
|
|
728
1611
|
}
|
|
729
1612
|
}
|
|
730
1613
|
export {
|
|
1614
|
+
ClusterManager,
|
|
731
1615
|
ShardBus,
|
|
732
|
-
ShardManager
|
|
1616
|
+
ShardManager,
|
|
1617
|
+
ShardManager as ShardingManager
|
|
733
1618
|
};
|
|
734
1619
|
|
|
735
|
-
//# debugId=
|
|
1620
|
+
//# debugId=C1062D33C7999C8E64756E2164756E21
|