lunibee 0.1.7 → 0.2.0
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 +225 -223
- package/dist/builders/commands.d.ts +6 -4
- package/dist/builders/components.d.ts +23 -0
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +120 -24
- package/dist/builders/index.js.map +9 -8
- package/dist/collection/index.d.ts +68 -1
- package/dist/collection/index.js +335 -1
- package/dist/collection/index.js.map +4 -4
- package/dist/core/collector.d.ts +13 -0
- package/dist/core/events.d.ts +112 -2
- package/dist/core/index.d.ts +30 -93
- package/dist/core/index.js +3386 -957
- package/dist/core/index.js.map +48 -24
- package/dist/core/permissions.d.ts +48 -0
- 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 +3806 -1193
- package/dist/index.js.map +64 -43
- package/dist/managers/advanced.d.ts +116 -0
- package/dist/managers/base.d.ts +13 -2
- package/dist/managers/emoji.d.ts +1 -1
- package/dist/managers/guild-resources.d.ts +109 -0
- package/dist/managers/guild.d.ts +33 -2
- package/dist/managers/index.d.ts +44 -6
- package/dist/managers/index.js +1186 -150
- package/dist/managers/index.js.map +27 -21
- package/dist/managers/message.d.ts +10 -1
- package/dist/rest/decoder.d.ts +23 -0
- package/dist/rest/errors.d.ts +53 -0
- package/dist/rest/index.d.ts +21 -17
- package/dist/rest/index.js +526 -264
- 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 +22 -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 +39 -2
- package/dist/sharding/index.js +1105 -404
- package/dist/sharding/index.js.map +18 -8
- package/dist/structures/base.d.ts +10 -0
- package/dist/structures/channels.d.ts +56 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +305 -311
- package/dist/structures/index.js.map +18 -14
- package/dist/structures/interactions.d.ts +38 -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 +197 -0
- package/dist/types/index.d.ts +53 -260
- 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 +32 -1
- package/dist/voice/index.js +73 -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 +911 -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,724 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/
|
|
2
|
+
// packages/sharding/package.json
|
|
3
|
+
var package_default = {
|
|
4
|
+
name: "@lunibee/sharding",
|
|
5
|
+
version: "0.2.0",
|
|
6
|
+
type: "module",
|
|
7
|
+
module: "./src/index.ts",
|
|
8
|
+
types: "./src/index.ts",
|
|
9
|
+
exports: {
|
|
10
|
+
".": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
dependencies: {
|
|
13
|
+
"@lunibee/ws": "workspace:*"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// packages/ws/src/session.ts
|
|
18
|
+
class GatewaySession {
|
|
19
|
+
#state = { status: "none" };
|
|
20
|
+
#sequence = null;
|
|
21
|
+
#generation = 0;
|
|
22
|
+
#lastInvalidation;
|
|
23
|
+
get lastInvalidation() {
|
|
24
|
+
return this.#lastInvalidation;
|
|
25
|
+
}
|
|
26
|
+
get generation() {
|
|
27
|
+
return this.#generation;
|
|
28
|
+
}
|
|
29
|
+
get sessionId() {
|
|
30
|
+
return this.#state.status === "established" ? this.#state.sessionId : undefined;
|
|
31
|
+
}
|
|
32
|
+
get resumeURL() {
|
|
33
|
+
return this.#state.status === "established" ? this.#state.resumeURL : undefined;
|
|
34
|
+
}
|
|
35
|
+
get sequence() {
|
|
36
|
+
return this.#sequence;
|
|
37
|
+
}
|
|
38
|
+
get canResume() {
|
|
39
|
+
return this.#state.status === "established" && this.#sequence !== null;
|
|
40
|
+
}
|
|
41
|
+
beginConnection() {
|
|
42
|
+
return ++this.#generation;
|
|
43
|
+
}
|
|
44
|
+
owns(token) {
|
|
45
|
+
return token === this.#generation;
|
|
46
|
+
}
|
|
47
|
+
recordSequence(sequence, token) {
|
|
48
|
+
if (!this.owns(token))
|
|
49
|
+
return false;
|
|
50
|
+
if (!Number.isInteger(sequence))
|
|
51
|
+
return false;
|
|
52
|
+
if (this.#sequence !== null && sequence < this.#sequence)
|
|
53
|
+
return false;
|
|
54
|
+
this.#sequence = sequence;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
activate(payload, token) {
|
|
58
|
+
if (!this.owns(token))
|
|
59
|
+
return false;
|
|
60
|
+
const ready = payload;
|
|
61
|
+
if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string")
|
|
62
|
+
return false;
|
|
63
|
+
this.#state = {
|
|
64
|
+
status: "established",
|
|
65
|
+
sessionId: ready.session_id,
|
|
66
|
+
resumeURL: ready.resume_gateway_url
|
|
67
|
+
};
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
invalidate(token, reason = "explicit") {
|
|
71
|
+
if (!this.owns(token))
|
|
72
|
+
return false;
|
|
73
|
+
this.#lastInvalidation = reason;
|
|
74
|
+
this.#state = { status: "none" };
|
|
75
|
+
this.#sequence = null;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
resumeInfo() {
|
|
79
|
+
if (this.#state.status !== "established")
|
|
80
|
+
return;
|
|
81
|
+
if (this.#sequence === null)
|
|
82
|
+
return;
|
|
83
|
+
const { sessionId, resumeURL } = this.#state;
|
|
84
|
+
return { sessionId, sequence: this.#sequence, resumeURL };
|
|
85
|
+
}
|
|
86
|
+
handshake() {
|
|
87
|
+
const resume = this.resumeInfo();
|
|
88
|
+
return resume ? { type: "resume", ...resume } : { type: "identify" };
|
|
89
|
+
}
|
|
90
|
+
connectURL(fallback) {
|
|
91
|
+
return this.resumeInfo()?.resumeURL ?? fallback;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// packages/ws/src/heartbeat.ts
|
|
96
|
+
var MAX_MONITOR_INTERVAL = 1000;
|
|
97
|
+
|
|
98
|
+
class GatewayHeartbeat {
|
|
99
|
+
#options;
|
|
100
|
+
#now;
|
|
101
|
+
#interval = 0;
|
|
102
|
+
#acknowledged = true;
|
|
103
|
+
#sentAt = 0;
|
|
104
|
+
#latency = -1;
|
|
105
|
+
#lastMessageAt = 0;
|
|
106
|
+
#zombieReported = false;
|
|
107
|
+
#timer;
|
|
108
|
+
#initialTimer;
|
|
109
|
+
#ackTimer;
|
|
110
|
+
#monitorTimer;
|
|
111
|
+
constructor(options) {
|
|
112
|
+
if (options.zombieTimeout <= options.ackTimeout)
|
|
113
|
+
throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
|
|
114
|
+
this.#options = options;
|
|
115
|
+
this.#now = options.now ?? (() => Date.now());
|
|
116
|
+
}
|
|
117
|
+
get latency() {
|
|
118
|
+
return this.#latency;
|
|
119
|
+
}
|
|
120
|
+
get interval() {
|
|
121
|
+
return this.#interval;
|
|
122
|
+
}
|
|
123
|
+
get acknowledged() {
|
|
124
|
+
return this.#acknowledged;
|
|
125
|
+
}
|
|
126
|
+
get silentFor() {
|
|
127
|
+
return this.#lastMessageAt === 0 ? 0 : this.#now() - this.#lastMessageAt;
|
|
128
|
+
}
|
|
129
|
+
get stalenessDeadline() {
|
|
130
|
+
return Math.max(this.#options.zombieTimeout, this.#interval + this.#options.ackTimeout);
|
|
131
|
+
}
|
|
132
|
+
isHealthy() {
|
|
133
|
+
if (!this.#acknowledged)
|
|
134
|
+
return false;
|
|
135
|
+
if (this.#lastMessageAt === 0)
|
|
136
|
+
return true;
|
|
137
|
+
return this.silentFor < this.stalenessDeadline;
|
|
138
|
+
}
|
|
139
|
+
watch() {
|
|
140
|
+
this.#lastMessageAt = this.#now();
|
|
141
|
+
this.#zombieReported = false;
|
|
142
|
+
this.#startMonitor();
|
|
143
|
+
}
|
|
144
|
+
receivedMessage() {
|
|
145
|
+
this.#lastMessageAt = this.#now();
|
|
146
|
+
this.#zombieReported = false;
|
|
147
|
+
}
|
|
148
|
+
start(interval) {
|
|
149
|
+
this.#clearBeatTimers();
|
|
150
|
+
this.#interval = interval;
|
|
151
|
+
this.#acknowledged = true;
|
|
152
|
+
this.#initialTimer = setTimeout(() => {
|
|
153
|
+
this.#initialTimer = undefined;
|
|
154
|
+
this.sendHeartbeat();
|
|
155
|
+
this.#timer = setInterval(() => this.sendHeartbeat(), interval);
|
|
156
|
+
}, Math.random() * interval);
|
|
157
|
+
}
|
|
158
|
+
stop() {
|
|
159
|
+
this.#clearBeatTimers();
|
|
160
|
+
if (this.#monitorTimer)
|
|
161
|
+
clearInterval(this.#monitorTimer);
|
|
162
|
+
this.#monitorTimer = undefined;
|
|
163
|
+
}
|
|
164
|
+
sendHeartbeat() {
|
|
165
|
+
this.#acknowledged = false;
|
|
166
|
+
this.#sentAt = this.#now();
|
|
167
|
+
if (!this.#options.send(this.#options.sequence())) {
|
|
168
|
+
this.#options.onError(new Error("Unable to send Gateway heartbeat because the WebSocket is not open."));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.#clearAckTimer();
|
|
172
|
+
this.#ackTimer = setTimeout(() => {
|
|
173
|
+
if (this.#acknowledged)
|
|
174
|
+
return;
|
|
175
|
+
this.#options.onTimeout({
|
|
176
|
+
type: "ack",
|
|
177
|
+
elapsedMs: this.#now() - this.#sentAt
|
|
178
|
+
});
|
|
179
|
+
}, this.#options.ackTimeout);
|
|
180
|
+
}
|
|
181
|
+
acknowledge() {
|
|
182
|
+
if (this.#acknowledged)
|
|
183
|
+
return;
|
|
184
|
+
this.#acknowledged = true;
|
|
185
|
+
this.#latency = this.#now() - this.#sentAt;
|
|
186
|
+
this.#clearAckTimer();
|
|
187
|
+
}
|
|
188
|
+
#startMonitor() {
|
|
189
|
+
if (this.#monitorTimer)
|
|
190
|
+
clearInterval(this.#monitorTimer);
|
|
191
|
+
const interval = Math.max(1, Math.min(MAX_MONITOR_INTERVAL, this.#options.ackTimeout, this.#options.zombieTimeout / 2));
|
|
192
|
+
this.#monitorTimer = setInterval(() => this.#checkStaleness(), interval);
|
|
193
|
+
}
|
|
194
|
+
#checkStaleness() {
|
|
195
|
+
if (!this.#options.isConnected() || this.#lastMessageAt === 0)
|
|
196
|
+
return;
|
|
197
|
+
const silentFor = this.silentFor;
|
|
198
|
+
const deadline = this.stalenessDeadline;
|
|
199
|
+
if (silentFor < deadline || this.#zombieReported)
|
|
200
|
+
return;
|
|
201
|
+
this.#zombieReported = true;
|
|
202
|
+
this.#options.onTimeout({ type: "zombie", silentFor, deadline });
|
|
203
|
+
}
|
|
204
|
+
#clearAckTimer() {
|
|
205
|
+
if (this.#ackTimer)
|
|
206
|
+
clearTimeout(this.#ackTimer);
|
|
207
|
+
this.#ackTimer = undefined;
|
|
208
|
+
}
|
|
209
|
+
#clearBeatTimers() {
|
|
210
|
+
if (this.#timer)
|
|
211
|
+
clearInterval(this.#timer);
|
|
212
|
+
if (this.#initialTimer)
|
|
213
|
+
clearTimeout(this.#initialTimer);
|
|
214
|
+
this.#clearAckTimer();
|
|
215
|
+
this.#timer = undefined;
|
|
216
|
+
this.#initialTimer = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// packages/ws/src/close-codes.ts
|
|
221
|
+
var GatewayCloseCodes = {
|
|
222
|
+
UnknownError: 4000,
|
|
223
|
+
UnknownOpcode: 4001,
|
|
224
|
+
DecodeError: 4002,
|
|
225
|
+
NotAuthenticated: 4003,
|
|
226
|
+
AuthenticationFailed: 4004,
|
|
227
|
+
AlreadyAuthenticated: 4005,
|
|
228
|
+
InvalidSeq: 4007,
|
|
229
|
+
RateLimited: 4008,
|
|
230
|
+
SessionTimedOut: 4009,
|
|
231
|
+
InvalidShard: 4010,
|
|
232
|
+
ShardingRequired: 4011,
|
|
233
|
+
InvalidAPIVersion: 4012,
|
|
234
|
+
InvalidIntents: 4013,
|
|
235
|
+
DisallowedIntents: 4014
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// packages/ws/src/reconnect.ts
|
|
239
|
+
var FATAL_CLOSE_CODES = [
|
|
240
|
+
GatewayCloseCodes.AuthenticationFailed,
|
|
241
|
+
GatewayCloseCodes.InvalidShard,
|
|
242
|
+
GatewayCloseCodes.ShardingRequired,
|
|
243
|
+
GatewayCloseCodes.InvalidAPIVersion,
|
|
244
|
+
GatewayCloseCodes.InvalidIntents,
|
|
245
|
+
GatewayCloseCodes.DisallowedIntents
|
|
246
|
+
];
|
|
247
|
+
var IDENTIFY_CLOSE_CODES = [
|
|
248
|
+
GatewayCloseCodes.InvalidSeq,
|
|
249
|
+
GatewayCloseCodes.SessionTimedOut
|
|
250
|
+
];
|
|
251
|
+
function classifyCloseCode(code, canResume) {
|
|
252
|
+
if (FATAL_CLOSE_CODES.includes(code))
|
|
253
|
+
return "stop";
|
|
254
|
+
if (IDENTIFY_CLOSE_CODES.includes(code))
|
|
255
|
+
return "identify";
|
|
256
|
+
return canResume ? "resume" : "identify";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class GatewayReconnect {
|
|
260
|
+
#options;
|
|
261
|
+
#random;
|
|
262
|
+
#attempt = 0;
|
|
263
|
+
#timer;
|
|
264
|
+
#promise;
|
|
265
|
+
#resolve;
|
|
266
|
+
#reject;
|
|
267
|
+
constructor(options) {
|
|
268
|
+
this.#options = options;
|
|
269
|
+
this.#random = options.random ?? Math.random;
|
|
270
|
+
}
|
|
271
|
+
get enabled() {
|
|
272
|
+
return this.#options.enabled;
|
|
273
|
+
}
|
|
274
|
+
get attempts() {
|
|
275
|
+
return this.#attempt;
|
|
276
|
+
}
|
|
277
|
+
get pending() {
|
|
278
|
+
return this.#timer !== undefined;
|
|
279
|
+
}
|
|
280
|
+
get exhausted() {
|
|
281
|
+
return this.#attempt >= this.#options.maxAttempts;
|
|
282
|
+
}
|
|
283
|
+
get connecting() {
|
|
284
|
+
return this.#promise !== undefined;
|
|
285
|
+
}
|
|
286
|
+
get attemptPromise() {
|
|
287
|
+
return this.#promise;
|
|
288
|
+
}
|
|
289
|
+
classifyClose(code, canResume) {
|
|
290
|
+
return classifyCloseCode(code, canResume);
|
|
291
|
+
}
|
|
292
|
+
canReconnect() {
|
|
293
|
+
return this.#options.enabled && !this.exhausted;
|
|
294
|
+
}
|
|
295
|
+
schedule(run) {
|
|
296
|
+
if (!this.#options.enabled)
|
|
297
|
+
return { scheduled: false, reason: "disabled" };
|
|
298
|
+
if (this.#timer)
|
|
299
|
+
return { scheduled: false, reason: "pending" };
|
|
300
|
+
if (this.exhausted)
|
|
301
|
+
return { scheduled: false, reason: "exhausted" };
|
|
302
|
+
const attempt = this.#attempt;
|
|
303
|
+
const delay = Math.min(this.#options.maxDelay, this.#options.baseDelay * 2 ** this.#attempt++);
|
|
304
|
+
const jitter = this.#random() * Math.max(1, delay * 0.25);
|
|
305
|
+
const delayMs = delay + jitter;
|
|
306
|
+
this.#timer = setTimeout(() => {
|
|
307
|
+
this.#timer = undefined;
|
|
308
|
+
run();
|
|
309
|
+
}, delayMs);
|
|
310
|
+
return { scheduled: true, delayMs, attempt };
|
|
311
|
+
}
|
|
312
|
+
cancel() {
|
|
313
|
+
if (this.#timer)
|
|
314
|
+
clearTimeout(this.#timer);
|
|
315
|
+
this.#timer = undefined;
|
|
316
|
+
}
|
|
317
|
+
reset() {
|
|
318
|
+
this.#attempt = 0;
|
|
319
|
+
}
|
|
320
|
+
attempt(start) {
|
|
321
|
+
if (this.#promise)
|
|
322
|
+
return this.#promise;
|
|
323
|
+
this.#promise = new Promise((resolve, reject) => {
|
|
324
|
+
this.#resolve = resolve;
|
|
325
|
+
this.#reject = reject;
|
|
326
|
+
start();
|
|
327
|
+
});
|
|
328
|
+
return this.#promise;
|
|
329
|
+
}
|
|
330
|
+
settle(error) {
|
|
331
|
+
const resolve = this.#resolve;
|
|
332
|
+
const reject = this.#reject;
|
|
333
|
+
this.#resolve = undefined;
|
|
334
|
+
this.#reject = undefined;
|
|
335
|
+
this.#promise = undefined;
|
|
336
|
+
if (error)
|
|
337
|
+
reject?.(error);
|
|
338
|
+
else
|
|
339
|
+
resolve?.();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// packages/ws/src/decoder.ts
|
|
344
|
+
import {
|
|
345
|
+
createInflate,
|
|
346
|
+
constants as zlibConstants
|
|
347
|
+
} from "zlib";
|
|
348
|
+
async function toBytes(data) {
|
|
349
|
+
if (data instanceof Uint8Array)
|
|
350
|
+
return data;
|
|
351
|
+
if (data instanceof ArrayBuffer)
|
|
352
|
+
return new Uint8Array(data);
|
|
353
|
+
if (typeof Blob !== "undefined" && data instanceof Blob)
|
|
354
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
355
|
+
throw new TypeError("Gateway returned an unsupported compressed frame type.");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
class PlainTextDecoder {
|
|
359
|
+
push(data) {
|
|
360
|
+
return [String(data)];
|
|
361
|
+
}
|
|
362
|
+
reset() {}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
class ZlibStreamDecoder {
|
|
366
|
+
#create;
|
|
367
|
+
#inflate;
|
|
368
|
+
#chunks = [];
|
|
369
|
+
#failure;
|
|
370
|
+
#pendingReject;
|
|
371
|
+
constructor(create = () => createInflate()) {
|
|
372
|
+
this.#create = create;
|
|
373
|
+
}
|
|
374
|
+
push(data) {
|
|
375
|
+
if (typeof data === "string")
|
|
376
|
+
return [data];
|
|
377
|
+
return this.#inflateFrame(data);
|
|
378
|
+
}
|
|
379
|
+
async#inflateFrame(data) {
|
|
380
|
+
const chunk = await toBytes(data);
|
|
381
|
+
const inflate = this.#inflate ??= this.#open();
|
|
382
|
+
if (this.#failure)
|
|
383
|
+
throw this.#failure;
|
|
384
|
+
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;
|
|
385
|
+
await this.#awaiting((settle) => inflate.write(chunk, (error) => error ? settle(error) : settle()));
|
|
386
|
+
if (!complete)
|
|
387
|
+
return [];
|
|
388
|
+
await this.#awaiting((settle) => inflate.flush(zlibConstants.Z_SYNC_FLUSH, () => settle()));
|
|
389
|
+
const parts = this.#chunks;
|
|
390
|
+
this.#chunks = [];
|
|
391
|
+
if (parts.length === 0)
|
|
392
|
+
return [];
|
|
393
|
+
const total = parts.reduce((size, part) => size + part.length, 0);
|
|
394
|
+
const merged = new Uint8Array(total);
|
|
395
|
+
let offset = 0;
|
|
396
|
+
for (const part of parts) {
|
|
397
|
+
merged.set(part, offset);
|
|
398
|
+
offset += part.length;
|
|
399
|
+
}
|
|
400
|
+
return [new TextDecoder().decode(merged)];
|
|
401
|
+
}
|
|
402
|
+
reset() {
|
|
403
|
+
const inflate = this.#inflate;
|
|
404
|
+
this.#inflate = undefined;
|
|
405
|
+
this.#chunks = [];
|
|
406
|
+
this.#failure = undefined;
|
|
407
|
+
this.#pendingReject = undefined;
|
|
408
|
+
if (!inflate)
|
|
409
|
+
return;
|
|
410
|
+
inflate.removeAllListeners();
|
|
411
|
+
try {
|
|
412
|
+
inflate.close();
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
#awaiting(run) {
|
|
416
|
+
if (this.#failure)
|
|
417
|
+
return Promise.reject(this.#failure);
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
this.#pendingReject = reject;
|
|
420
|
+
run((error) => {
|
|
421
|
+
this.#pendingReject = undefined;
|
|
422
|
+
if (error)
|
|
423
|
+
reject(error);
|
|
424
|
+
else if (this.#failure)
|
|
425
|
+
reject(this.#failure);
|
|
426
|
+
else
|
|
427
|
+
resolve();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
#open() {
|
|
432
|
+
const inflate = this.#create();
|
|
433
|
+
inflate.on("data", (chunk) => this.#chunks.push(chunk));
|
|
434
|
+
inflate.on("error", (error) => {
|
|
435
|
+
this.#failure = error;
|
|
436
|
+
const reject = this.#pendingReject;
|
|
437
|
+
this.#pendingReject = undefined;
|
|
438
|
+
reject?.(error);
|
|
439
|
+
});
|
|
440
|
+
return inflate;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function createDecoder(compress) {
|
|
444
|
+
return compress ? new ZlibStreamDecoder : new PlainTextDecoder;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// packages/ws/src/transport.ts
|
|
448
|
+
var OPEN = 1;
|
|
449
|
+
|
|
450
|
+
class WebSocketTransport {
|
|
451
|
+
#handlers;
|
|
452
|
+
#decoder;
|
|
453
|
+
#createSocket;
|
|
454
|
+
#compress;
|
|
455
|
+
#socket;
|
|
456
|
+
#generation = 0;
|
|
457
|
+
#queue = Promise.resolve();
|
|
458
|
+
constructor(options) {
|
|
459
|
+
this.#handlers = options.handlers;
|
|
460
|
+
this.#compress = options.compress ?? false;
|
|
461
|
+
this.#decoder = options.decoder ?? createDecoder(options.compress);
|
|
462
|
+
this.#createSocket = options.createSocket ?? ((url) => new WebSocket(url));
|
|
463
|
+
}
|
|
464
|
+
get connected() {
|
|
465
|
+
return this.#socket?.readyState === OPEN;
|
|
466
|
+
}
|
|
467
|
+
get live() {
|
|
468
|
+
return this.#socket !== undefined && this.#socket.readyState <= OPEN;
|
|
469
|
+
}
|
|
470
|
+
get generation() {
|
|
471
|
+
return this.#generation;
|
|
472
|
+
}
|
|
473
|
+
connect(url) {
|
|
474
|
+
this.#discard(1000, "Superseded by a new connection");
|
|
475
|
+
const generation = ++this.#generation;
|
|
476
|
+
this.#decoder.reset();
|
|
477
|
+
this.#queue = Promise.resolve();
|
|
478
|
+
let socket;
|
|
479
|
+
try {
|
|
480
|
+
socket = this.#createSocket(url);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
return { ok: false, error: this.#asError(error) };
|
|
483
|
+
}
|
|
484
|
+
if (this.#compress) {
|
|
485
|
+
try {
|
|
486
|
+
socket.binaryType = "arraybuffer";
|
|
487
|
+
} catch {}
|
|
488
|
+
}
|
|
489
|
+
this.#socket = socket;
|
|
490
|
+
socket.addEventListener("open", () => {
|
|
491
|
+
if (!this.#owns(generation))
|
|
492
|
+
return;
|
|
493
|
+
this.#handlers.onOpen();
|
|
494
|
+
});
|
|
495
|
+
socket.addEventListener("message", (event) => {
|
|
496
|
+
if (!this.#owns(generation))
|
|
497
|
+
return;
|
|
498
|
+
let decoded;
|
|
499
|
+
try {
|
|
500
|
+
decoded = this.#decoder.push(event.data);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this.#handlers.onError(this.#asError(error));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (!(decoded instanceof Promise)) {
|
|
506
|
+
for (const frame of decoded)
|
|
507
|
+
this.#handlers.onFrame(frame);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.#queue = this.#queue.then(async () => {
|
|
511
|
+
const frames = await decoded;
|
|
512
|
+
if (!this.#owns(generation))
|
|
513
|
+
return;
|
|
514
|
+
for (const frame of frames)
|
|
515
|
+
this.#handlers.onFrame(frame);
|
|
516
|
+
}).catch((error) => {
|
|
517
|
+
if (!this.#owns(generation))
|
|
518
|
+
return;
|
|
519
|
+
this.#handlers.onError(this.#asError(error));
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
socket.addEventListener("close", (event) => {
|
|
523
|
+
if (!this.#owns(generation))
|
|
524
|
+
return;
|
|
525
|
+
this.#socket = undefined;
|
|
526
|
+
this.#generation++;
|
|
527
|
+
this.#decoder.reset();
|
|
528
|
+
this.#handlers.onClose(event.code, event.reason ?? "");
|
|
529
|
+
});
|
|
530
|
+
socket.addEventListener("error", () => {
|
|
531
|
+
if (!this.#owns(generation))
|
|
532
|
+
return;
|
|
533
|
+
this.#handlers.onError(new Error("Gateway WebSocket error"));
|
|
534
|
+
});
|
|
535
|
+
return { ok: true };
|
|
536
|
+
}
|
|
537
|
+
send(data) {
|
|
538
|
+
if (!this.connected)
|
|
539
|
+
return false;
|
|
540
|
+
try {
|
|
541
|
+
this.#socket.send(data);
|
|
542
|
+
return true;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.#handlers.onError(this.#asError(error));
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
close(code = 1000, reason = "") {
|
|
549
|
+
const socket = this.#socket;
|
|
550
|
+
if (!socket)
|
|
551
|
+
return;
|
|
552
|
+
try {
|
|
553
|
+
socket.close(code, reason);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.#handlers.onError(this.#asError(error));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
destroy(code = 1000, reason = "") {
|
|
559
|
+
this.#discard(code, reason);
|
|
560
|
+
this.#decoder.reset();
|
|
561
|
+
}
|
|
562
|
+
#owns(generation) {
|
|
563
|
+
return generation === this.#generation;
|
|
564
|
+
}
|
|
565
|
+
#discard(code, reason) {
|
|
566
|
+
const socket = this.#socket;
|
|
567
|
+
if (!socket)
|
|
568
|
+
return;
|
|
569
|
+
this.#generation++;
|
|
570
|
+
this.#socket = undefined;
|
|
571
|
+
try {
|
|
572
|
+
socket.close(code, reason);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
this.#handlers.onError(this.#asError(error));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
#asError(error) {
|
|
578
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// packages/ws/src/opcodes.ts
|
|
583
|
+
var GatewayOpcodes = {
|
|
584
|
+
Dispatch: 0,
|
|
585
|
+
Heartbeat: 1,
|
|
586
|
+
Identify: 2,
|
|
587
|
+
PresenceUpdate: 3,
|
|
588
|
+
VoiceStateUpdate: 4,
|
|
589
|
+
Resume: 6,
|
|
590
|
+
Reconnect: 7,
|
|
591
|
+
RequestGuildMembers: 8,
|
|
592
|
+
InvalidSession: 9,
|
|
593
|
+
Hello: 10,
|
|
594
|
+
HeartbeatAck: 11
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// packages/ws/src/protocol.ts
|
|
598
|
+
function classifyFrame(raw) {
|
|
599
|
+
let payload;
|
|
600
|
+
try {
|
|
601
|
+
payload = JSON.parse(raw);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
return {
|
|
604
|
+
sequence: null,
|
|
605
|
+
action: {
|
|
606
|
+
type: "invalid",
|
|
607
|
+
violation: "invalid-json",
|
|
608
|
+
message: "Gateway returned invalid JSON",
|
|
609
|
+
cause: error
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return classifyPayload(payload);
|
|
614
|
+
}
|
|
615
|
+
function classifyPayload(payload) {
|
|
616
|
+
if (payload === null || typeof payload !== "object" || typeof payload.op !== "number")
|
|
617
|
+
return {
|
|
618
|
+
sequence: null,
|
|
619
|
+
action: {
|
|
620
|
+
type: "invalid",
|
|
621
|
+
violation: "invalid-payload",
|
|
622
|
+
message: "Gateway returned an invalid payload"
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const frame = payload;
|
|
626
|
+
const sequence = typeof frame.s === "number" ? frame.s : null;
|
|
627
|
+
switch (frame.op) {
|
|
628
|
+
case GatewayOpcodes.Dispatch:
|
|
629
|
+
return {
|
|
630
|
+
sequence,
|
|
631
|
+
action: {
|
|
632
|
+
type: "dispatch",
|
|
633
|
+
event: typeof frame.t === "string" ? frame.t : null,
|
|
634
|
+
data: frame.d
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
case GatewayOpcodes.Hello: {
|
|
638
|
+
const interval = frame.d?.heartbeat_interval;
|
|
639
|
+
if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0)
|
|
640
|
+
return {
|
|
641
|
+
sequence,
|
|
642
|
+
action: {
|
|
643
|
+
type: "invalid",
|
|
644
|
+
violation: "invalid-hello",
|
|
645
|
+
message: "Gateway HELLO payload contains an invalid heartbeat interval"
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
return {
|
|
649
|
+
sequence,
|
|
650
|
+
action: { type: "hello", heartbeatInterval: interval }
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
case GatewayOpcodes.Heartbeat:
|
|
654
|
+
return { sequence, action: { type: "heartbeat" } };
|
|
655
|
+
case GatewayOpcodes.HeartbeatAck:
|
|
656
|
+
return {
|
|
657
|
+
sequence,
|
|
658
|
+
action: { type: "heartbeat-ack", data: frame.d }
|
|
659
|
+
};
|
|
660
|
+
case GatewayOpcodes.Reconnect:
|
|
661
|
+
return { sequence, action: { type: "reconnect" } };
|
|
662
|
+
case GatewayOpcodes.InvalidSession:
|
|
663
|
+
return {
|
|
664
|
+
sequence,
|
|
665
|
+
action: {
|
|
666
|
+
type: "invalid-session",
|
|
667
|
+
resumable: frame.d === true
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
default:
|
|
671
|
+
return { sequence, action: { type: "unknown", opcode: frame.op } };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function identifyPayload(options) {
|
|
675
|
+
const props = options.properties ?? {};
|
|
676
|
+
const os = props.os ?? "Android";
|
|
677
|
+
const browser = props.browser ?? "Discord Android";
|
|
678
|
+
const device = props.device ?? "Discord Android";
|
|
679
|
+
return {
|
|
680
|
+
op: GatewayOpcodes.Identify,
|
|
681
|
+
d: {
|
|
682
|
+
token: options.token,
|
|
683
|
+
intents: options.intents,
|
|
684
|
+
properties: {
|
|
685
|
+
os,
|
|
686
|
+
browser,
|
|
687
|
+
device,
|
|
688
|
+
$os: os,
|
|
689
|
+
$browser: browser,
|
|
690
|
+
$device: device,
|
|
691
|
+
...props
|
|
692
|
+
},
|
|
693
|
+
presence: {
|
|
694
|
+
since: options.presence?.since ?? null,
|
|
695
|
+
activities: options.presence?.activities ?? [],
|
|
696
|
+
status: options.presence?.status ?? "online",
|
|
697
|
+
afk: Boolean(options.presence?.afk)
|
|
698
|
+
},
|
|
699
|
+
shard: [options.shardId, options.shardCount]
|
|
700
|
+
},
|
|
701
|
+
s: null,
|
|
702
|
+
t: null
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function resumePayload(token, resume) {
|
|
706
|
+
return {
|
|
707
|
+
op: GatewayOpcodes.Resume,
|
|
708
|
+
d: {
|
|
709
|
+
token,
|
|
710
|
+
session_id: resume.sessionId,
|
|
711
|
+
seq: resume.sequence
|
|
712
|
+
},
|
|
713
|
+
s: null,
|
|
714
|
+
t: null
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function heartbeatPayload(sequence) {
|
|
718
|
+
return { op: GatewayOpcodes.Heartbeat, d: sequence, s: null, t: null };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// packages/types/src/gateway.ts
|
|
3
722
|
var GatewayIntentBits = {
|
|
4
723
|
Guilds: 1 << 0,
|
|
5
724
|
GuildMembers: 1 << 1,
|
|
@@ -54,6 +773,8 @@ var IntentBits = {
|
|
|
54
773
|
function resolveGatewayIntents(intents) {
|
|
55
774
|
if (typeof intents === "number")
|
|
56
775
|
return intents;
|
|
776
|
+
if (intents && typeof intents === "object" && !Array.isArray(intents))
|
|
777
|
+
return intents.bitfield;
|
|
57
778
|
if (Array.isArray(intents)) {
|
|
58
779
|
return intents.reduce((acc, intent) => {
|
|
59
780
|
return acc | resolveGatewayIntents(intent);
|
|
@@ -73,37 +794,7 @@ function resolveGatewayIntents(intents) {
|
|
|
73
794
|
}
|
|
74
795
|
return 0;
|
|
75
796
|
}
|
|
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
|
-
};
|
|
797
|
+
// packages/ws/src/state.ts
|
|
107
798
|
class GatewayError extends Error {
|
|
108
799
|
code;
|
|
109
800
|
constructor(message, code, options) {
|
|
@@ -112,33 +803,44 @@ class GatewayError extends Error {
|
|
|
112
803
|
this.code = code;
|
|
113
804
|
}
|
|
114
805
|
}
|
|
806
|
+
// packages/ws/src/send-budget.ts
|
|
807
|
+
class SendBudget {
|
|
808
|
+
#timestamps = [];
|
|
809
|
+
#limit;
|
|
810
|
+
#windowMs;
|
|
811
|
+
constructor(limit = 115, windowMs = 60000) {
|
|
812
|
+
this.#limit = limit;
|
|
813
|
+
this.#windowMs = windowMs;
|
|
814
|
+
}
|
|
815
|
+
get used() {
|
|
816
|
+
return this.#timestamps.length;
|
|
817
|
+
}
|
|
818
|
+
allows(privileged, now = Date.now()) {
|
|
819
|
+
while (this.#timestamps.length && now - this.#timestamps[0] >= this.#windowMs)
|
|
820
|
+
this.#timestamps.shift();
|
|
821
|
+
return privileged || this.#timestamps.length < this.#limit;
|
|
822
|
+
}
|
|
823
|
+
record(now = Date.now()) {
|
|
824
|
+
this.#timestamps.push(now);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// packages/ws/src/index.ts
|
|
829
|
+
var RESUMABLE_CLOSE_CODE = 4900;
|
|
830
|
+
var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
115
831
|
|
|
116
832
|
class Gateway {
|
|
117
833
|
state = "CONNECT" /* Connect */;
|
|
118
834
|
#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;
|
|
835
|
+
#transport;
|
|
836
|
+
#session = new GatewaySession;
|
|
837
|
+
#token = 0;
|
|
838
|
+
#heartbeat;
|
|
132
839
|
#closed = false;
|
|
133
|
-
#
|
|
134
|
-
#reconnectTimer;
|
|
135
|
-
#connectPromise;
|
|
136
|
-
#resolveConnect;
|
|
137
|
-
#rejectConnect;
|
|
840
|
+
#reconnect;
|
|
138
841
|
#listeners = new Map;
|
|
139
|
-
#
|
|
842
|
+
#sendBudget = new SendBudget;
|
|
140
843
|
ping = -1;
|
|
141
|
-
#decompressor;
|
|
142
844
|
constructor(options) {
|
|
143
845
|
if (!options.token?.trim())
|
|
144
846
|
throw new TypeError("A Gateway token is required.");
|
|
@@ -165,41 +867,72 @@ class Gateway {
|
|
|
165
867
|
afk: false,
|
|
166
868
|
since: null
|
|
167
869
|
},
|
|
168
|
-
...options
|
|
870
|
+
...options,
|
|
871
|
+
intents: resolvedIntents
|
|
169
872
|
};
|
|
170
873
|
if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
|
|
171
874
|
throw new RangeError("Gateway shardId must be within the configured shard count.");
|
|
172
875
|
if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
|
|
173
876
|
throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
|
|
877
|
+
this.#transport = new WebSocketTransport({
|
|
878
|
+
compress: this.#options.compress,
|
|
879
|
+
createSocket: options.createSocket,
|
|
880
|
+
handlers: {
|
|
881
|
+
onOpen: () => this.#onOpen(),
|
|
882
|
+
onFrame: (raw) => this.#message(raw, this.#token),
|
|
883
|
+
onClose: (code) => this.#close(code, this.#token),
|
|
884
|
+
onError: (error) => this.#emitError(error instanceof GatewayError ? error : new GatewayError(error.message, undefined, {
|
|
885
|
+
cause: error
|
|
886
|
+
}))
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
this.#reconnect = new GatewayReconnect({
|
|
890
|
+
enabled: this.#options.reconnect,
|
|
891
|
+
maxAttempts: this.#options.maxReconnectAttempts,
|
|
892
|
+
baseDelay: this.#options.reconnectBaseDelay,
|
|
893
|
+
maxDelay: this.#options.reconnectMaxDelay
|
|
894
|
+
});
|
|
895
|
+
this.#heartbeat = new GatewayHeartbeat({
|
|
896
|
+
ackTimeout: this.#options.heartbeatAckTimeout,
|
|
897
|
+
zombieTimeout: this.#options.zombieTimeout,
|
|
898
|
+
send: (sequence) => this.#dispatch(heartbeatPayload(sequence), true),
|
|
899
|
+
sequence: () => this.#session.sequence,
|
|
900
|
+
isConnected: () => !this.#closed && this.#transport.connected,
|
|
901
|
+
onTimeout: (timeout) => this.#onHeartbeatTimeout(timeout),
|
|
902
|
+
onError: (error) => this.#emitError(error)
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
#onHeartbeatTimeout(timeout) {
|
|
906
|
+
if (timeout.type === "zombie") {
|
|
907
|
+
this.#emit("zombie", {
|
|
908
|
+
silentFor: timeout.silentFor,
|
|
909
|
+
timeout: timeout.deadline
|
|
910
|
+
});
|
|
911
|
+
this.#emitError(new GatewayError(`Gateway connection appears stale after ${timeout.silentFor}ms without traffic.`));
|
|
912
|
+
} else {
|
|
913
|
+
this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${timeout.elapsedMs}ms.`));
|
|
914
|
+
}
|
|
915
|
+
this.#transport.close(RESUMABLE_CLOSE_CODE, timeout.type === "zombie" ? "Zombie Gateway connection" : "Heartbeat timeout");
|
|
174
916
|
}
|
|
175
|
-
connect(url =
|
|
917
|
+
connect(url = DEFAULT_GATEWAY_URL) {
|
|
176
918
|
if (this.state === "CLOSED" /* Closed */)
|
|
177
919
|
throw new GatewayError("Gateway has been permanently closed.");
|
|
178
|
-
|
|
179
|
-
|
|
920
|
+
const inFlight = this.#reconnect.attemptPromise;
|
|
921
|
+
if (inFlight)
|
|
922
|
+
return inFlight;
|
|
923
|
+
if (this.#transport.live)
|
|
924
|
+
return Promise.resolve();
|
|
925
|
+
this.#reconnect.cancel();
|
|
180
926
|
this.#closed = false;
|
|
181
927
|
this.#setState("CONNECT" /* Connect */);
|
|
182
928
|
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;
|
|
929
|
+
return this.#reconnect.attempt(() => this.#open(connectURL));
|
|
191
930
|
}
|
|
192
931
|
close() {
|
|
193
932
|
this.#closed = true;
|
|
194
933
|
this.#clearTimers();
|
|
195
934
|
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
|
-
}
|
|
935
|
+
this.#transport.destroy(1000, "Client closed connection");
|
|
203
936
|
this.#setState("CLOSED" /* Closed */);
|
|
204
937
|
}
|
|
205
938
|
emit(event, data) {
|
|
@@ -224,23 +957,17 @@ class Gateway {
|
|
|
224
957
|
return this.#dispatch(payload, false);
|
|
225
958
|
}
|
|
226
959
|
#dispatch(payload, privileged) {
|
|
227
|
-
if (this.#
|
|
960
|
+
if (!this.#transport.connected)
|
|
228
961
|
return false;
|
|
229
962
|
const now = Date.now();
|
|
230
|
-
|
|
231
|
-
this.#sendTimestamps.shift();
|
|
232
|
-
if (!privileged && this.#sendTimestamps.length >= 115) {
|
|
963
|
+
if (!this.#sendBudget.allows(privileged, now)) {
|
|
233
964
|
this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
|
|
234
965
|
return false;
|
|
235
966
|
}
|
|
236
|
-
|
|
237
|
-
this.#ws.send(JSON.stringify(payload));
|
|
238
|
-
this.#sendTimestamps.push(now);
|
|
239
|
-
return true;
|
|
240
|
-
} catch (error) {
|
|
241
|
-
this.#emitError(error);
|
|
967
|
+
if (!this.#transport.send(JSON.stringify(payload)))
|
|
242
968
|
return false;
|
|
243
|
-
|
|
969
|
+
this.#sendBudget.record(now);
|
|
970
|
+
return true;
|
|
244
971
|
}
|
|
245
972
|
setPresence(data) {
|
|
246
973
|
this.#options.presence = { ...this.#options.presence, ...data };
|
|
@@ -276,127 +1003,80 @@ class Gateway {
|
|
|
276
1003
|
#open(url) {
|
|
277
1004
|
if (this.#closed)
|
|
278
1005
|
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);
|
|
1006
|
+
this.#token = this.#session.beginConnection();
|
|
1007
|
+
const result = this.#transport.connect(url);
|
|
1008
|
+
if (result.ok)
|
|
290
1009
|
return;
|
|
1010
|
+
const failure = this.#normalizeError(result.error);
|
|
1011
|
+
this.#emitError(failure);
|
|
1012
|
+
if (this.#options.reconnect) {
|
|
1013
|
+
this.#setState("RECONNECT" /* Reconnect */);
|
|
1014
|
+
this.#scheduleReconnect();
|
|
1015
|
+
} else {
|
|
1016
|
+
this.#settleConnect(failure);
|
|
291
1017
|
}
|
|
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
1018
|
}
|
|
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");
|
|
1019
|
+
#onOpen() {
|
|
1020
|
+
this.#heartbeat.watch();
|
|
1021
|
+
this.#emit("open", undefined);
|
|
1022
|
+
this.#settleConnect();
|
|
1023
|
+
}
|
|
1024
|
+
#message(raw, token) {
|
|
1025
|
+
if (!this.#session.owns(token))
|
|
348
1026
|
return;
|
|
349
|
-
}
|
|
350
|
-
if (
|
|
351
|
-
this.#sequence
|
|
352
|
-
|
|
353
|
-
|
|
1027
|
+
const { sequence, action } = classifyFrame(raw);
|
|
1028
|
+
if (sequence !== null)
|
|
1029
|
+
this.#session.recordSequence(sequence, token);
|
|
1030
|
+
this.#apply(action, token);
|
|
1031
|
+
}
|
|
1032
|
+
#apply(action, token) {
|
|
1033
|
+
switch (action.type) {
|
|
1034
|
+
case "dispatch":
|
|
354
1035
|
this.#setState("DISPATCH" /* Dispatch */);
|
|
355
|
-
this.#handleDispatch(
|
|
356
|
-
|
|
357
|
-
case
|
|
358
|
-
this.#handleHello(
|
|
359
|
-
|
|
360
|
-
case
|
|
1036
|
+
this.#handleDispatch(action.event, action.data, token);
|
|
1037
|
+
return;
|
|
1038
|
+
case "hello":
|
|
1039
|
+
this.#handleHello(action.heartbeatInterval);
|
|
1040
|
+
return;
|
|
1041
|
+
case "heartbeat":
|
|
361
1042
|
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;
|
|
1043
|
+
this.#heartbeat.sendHeartbeat();
|
|
1044
|
+
return;
|
|
1045
|
+
case "heartbeat-ack":
|
|
1046
|
+
this.#heartbeat.acknowledge();
|
|
1047
|
+
this.ping = this.#heartbeat.latency;
|
|
1048
|
+
this.#emit("heartbeatAck", action.data);
|
|
1049
|
+
return;
|
|
1050
|
+
case "reconnect":
|
|
1051
|
+
this.#transport.close(RESUMABLE_CLOSE_CODE, "Server requested reconnect");
|
|
1052
|
+
return;
|
|
1053
|
+
case "invalid-session": {
|
|
1054
|
+
if (!action.resumable)
|
|
1055
|
+
this.#session.invalidate(token, "invalid-session");
|
|
1056
|
+
this.#emit("invalidSession", action.resumable);
|
|
1057
|
+
this.#transport.close(action.resumable ? GatewayCloseCodes.UnknownError : 1000, action.resumable ? "Invalid session (resumable)" : "Invalid session");
|
|
1058
|
+
return;
|
|
383
1059
|
}
|
|
1060
|
+
case "invalid":
|
|
1061
|
+
this.#emitError(new GatewayError(action.message, undefined, action.cause === undefined ? undefined : { cause: action.cause }));
|
|
1062
|
+
this.#transport.close(1002, action.violation === "invalid-json" ? "Invalid JSON" : action.violation === "invalid-hello" ? "Invalid heartbeat interval" : "Invalid payload");
|
|
1063
|
+
return;
|
|
1064
|
+
case "unknown":
|
|
1065
|
+
return;
|
|
384
1066
|
}
|
|
385
1067
|
}
|
|
386
|
-
#handleDispatch(event, data) {
|
|
1068
|
+
#handleDispatch(event, data, token) {
|
|
387
1069
|
if (event === "READY") {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1070
|
+
if (!this.#session.activate(data, token)) {
|
|
1071
|
+
if (this.#session.owns(token))
|
|
1072
|
+
this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
|
|
391
1073
|
return;
|
|
392
1074
|
}
|
|
393
|
-
this.#
|
|
394
|
-
this.#resumeURL = ready.resume_gateway_url;
|
|
395
|
-
this.#attempt = 0;
|
|
1075
|
+
this.#reconnect.reset();
|
|
396
1076
|
this.#setState("READY" /* Ready */);
|
|
397
1077
|
this.#emit("ready", data);
|
|
398
1078
|
} else if (event === "RESUMED") {
|
|
399
|
-
this.#
|
|
1079
|
+
this.#reconnect.reset();
|
|
400
1080
|
this.#setState("READY" /* Ready */);
|
|
401
1081
|
this.#emit("resumed", data);
|
|
402
1082
|
}
|
|
@@ -405,183 +1085,60 @@ class Gateway {
|
|
|
405
1085
|
return;
|
|
406
1086
|
this.#emit(event ?? "dispatch", data);
|
|
407
1087
|
}
|
|
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);
|
|
1088
|
+
#handleHello(interval) {
|
|
1089
|
+
this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
1090
|
+
this.#heartbeat.start(interval);
|
|
418
1091
|
this.#identifyOrResume();
|
|
419
1092
|
}
|
|
420
1093
|
#identifyOrResume() {
|
|
421
|
-
|
|
1094
|
+
const intent = this.#session.handshake();
|
|
1095
|
+
if (intent.type === "resume") {
|
|
422
1096
|
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
|
-
});
|
|
1097
|
+
this.#dispatch(resumePayload(this.#options.token, intent), true);
|
|
433
1098
|
return;
|
|
434
1099
|
}
|
|
435
1100
|
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);
|
|
1101
|
+
this.#dispatch(identifyPayload({
|
|
1102
|
+
token: this.#options.token,
|
|
1103
|
+
intents: this.#options.intents,
|
|
1104
|
+
shardId: this.#options.shardId,
|
|
1105
|
+
shardCount: this.#options.shardCount,
|
|
1106
|
+
properties: this.#options.properties,
|
|
1107
|
+
presence: this.#options.presence
|
|
1108
|
+
}), true);
|
|
500
1109
|
}
|
|
501
|
-
#close(
|
|
502
|
-
if (this.#ws !== ws)
|
|
503
|
-
return;
|
|
504
|
-
this.#ws = undefined;
|
|
1110
|
+
#close(code, token) {
|
|
505
1111
|
this.#clearTimers();
|
|
506
|
-
const action = this.#
|
|
1112
|
+
const action = this.#reconnect.classifyClose(code, this.#session.canResume);
|
|
507
1113
|
this.#emit("close", { code, action });
|
|
508
|
-
if (this.#closed || !this.#
|
|
1114
|
+
if (this.#closed || !this.#reconnect.enabled || action === "stop") {
|
|
1115
|
+
if (action === "stop")
|
|
1116
|
+
this.#closed = true;
|
|
509
1117
|
this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
|
|
510
|
-
if (this.#
|
|
511
|
-
this.#
|
|
1118
|
+
if (this.#reconnect.connecting)
|
|
1119
|
+
this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
|
|
512
1120
|
return;
|
|
513
1121
|
}
|
|
514
|
-
if (action === "identify")
|
|
515
|
-
this.#
|
|
516
|
-
this.#sessionId = undefined;
|
|
517
|
-
this.#resumeURL = undefined;
|
|
518
|
-
}
|
|
1122
|
+
if (action === "identify")
|
|
1123
|
+
this.#session.invalidate(token, "session-timeout");
|
|
519
1124
|
this.#setState("RECONNECT" /* Reconnect */);
|
|
520
1125
|
this.#scheduleReconnect();
|
|
521
1126
|
}
|
|
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
1127
|
#scheduleReconnect() {
|
|
538
|
-
if (this.#closed
|
|
1128
|
+
if (this.#closed)
|
|
539
1129
|
return;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
this.#setState("CONNECT" /* Connect */);
|
|
1130
|
+
const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
|
|
1131
|
+
if (result.scheduled || result.reason !== "exhausted")
|
|
543
1132
|
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);
|
|
1133
|
+
this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
|
|
1134
|
+
this.#setState("CONNECT" /* Connect */);
|
|
551
1135
|
}
|
|
552
1136
|
#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;
|
|
1137
|
+
this.#reconnect.settle(error);
|
|
576
1138
|
}
|
|
577
1139
|
#clearTimers() {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (this.#zombieTimer)
|
|
581
|
-
clearInterval(this.#zombieTimer);
|
|
582
|
-
this.#reconnectTimer = undefined;
|
|
583
|
-
this.#zombieTimer = undefined;
|
|
584
|
-
this.#clearHeartbeatTimers();
|
|
1140
|
+
this.#reconnect.cancel();
|
|
1141
|
+
this.#heartbeat.stop();
|
|
585
1142
|
}
|
|
586
1143
|
#normalizeError(error) {
|
|
587
1144
|
return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
|
|
@@ -613,47 +1170,20 @@ class Gateway {
|
|
|
613
1170
|
this.state = next;
|
|
614
1171
|
this.#emit("stateChange", { previous, next });
|
|
615
1172
|
}
|
|
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
1173
|
}
|
|
648
1174
|
|
|
649
1175
|
// packages/sharding/src/bus.ts
|
|
1176
|
+
var REPLY_TYPE = "__lunibee:reply";
|
|
1177
|
+
|
|
650
1178
|
class ShardBus {
|
|
651
1179
|
#channel;
|
|
652
1180
|
#handlers = new Map;
|
|
1181
|
+
#errorHandlers = new Set;
|
|
653
1182
|
#shardId;
|
|
654
1183
|
#namespace;
|
|
655
1184
|
channelName;
|
|
656
1185
|
#counter = 0;
|
|
1186
|
+
#pending = new Map;
|
|
657
1187
|
constructor(shardId, channelName) {
|
|
658
1188
|
if (!Number.isInteger(shardId) || shardId < 0)
|
|
659
1189
|
throw new RangeError("Shard ID must be a non-negative integer.");
|
|
@@ -678,6 +1208,16 @@ class ShardBus {
|
|
|
678
1208
|
this.#handlers.get(type)?.delete(handler);
|
|
679
1209
|
return this;
|
|
680
1210
|
}
|
|
1211
|
+
onError(handler) {
|
|
1212
|
+
if (typeof handler !== "function")
|
|
1213
|
+
throw new TypeError("Shard bus error handler is required.");
|
|
1214
|
+
this.#errorHandlers.add(handler);
|
|
1215
|
+
return this;
|
|
1216
|
+
}
|
|
1217
|
+
offError(handler) {
|
|
1218
|
+
this.#errorHandlers.delete(handler);
|
|
1219
|
+
return this;
|
|
1220
|
+
}
|
|
681
1221
|
send(target, type, data) {
|
|
682
1222
|
if (!Number.isInteger(target) || target < 0)
|
|
683
1223
|
throw new RangeError("Shard target must be a non-negative integer.");
|
|
@@ -686,11 +1226,56 @@ class ShardBus {
|
|
|
686
1226
|
broadcast(type, data) {
|
|
687
1227
|
return this.#publish(null, type, data);
|
|
688
1228
|
}
|
|
1229
|
+
respond(type, handler) {
|
|
1230
|
+
return this.on(type, async (message) => {
|
|
1231
|
+
if (!message.expectsReply)
|
|
1232
|
+
return;
|
|
1233
|
+
const reply = { requestId: message.id };
|
|
1234
|
+
try {
|
|
1235
|
+
reply.result = await handler(message.data, message);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
reply.error = error instanceof Error ? error.message : String(error);
|
|
1238
|
+
}
|
|
1239
|
+
this.#publish(message.source, REPLY_TYPE, reply);
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
async request(target, type, data, timeoutMs = 5000) {
|
|
1243
|
+
if (!Number.isInteger(target) || target < 0)
|
|
1244
|
+
throw new RangeError("Shard target must be a non-negative integer.");
|
|
1245
|
+
const [reply] = await this.#collect(target, type, data, timeoutMs, 1);
|
|
1246
|
+
if (!reply)
|
|
1247
|
+
throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
|
|
1248
|
+
if (reply.error !== undefined)
|
|
1249
|
+
throw new Error(reply.error);
|
|
1250
|
+
return reply.result;
|
|
1251
|
+
}
|
|
1252
|
+
broadcastRequest(type, data, options = {}) {
|
|
1253
|
+
return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
|
|
1254
|
+
}
|
|
1255
|
+
#collect(target, type, data, timeoutMs, expected) {
|
|
1256
|
+
const replies = [];
|
|
1257
|
+
return new Promise((resolve) => {
|
|
1258
|
+
let requestId = "";
|
|
1259
|
+
const finish = () => {
|
|
1260
|
+
clearTimeout(timer);
|
|
1261
|
+
this.#pending.delete(requestId);
|
|
1262
|
+
resolve(replies);
|
|
1263
|
+
};
|
|
1264
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1265
|
+
requestId = this.#publish(target, type, data, true);
|
|
1266
|
+
this.#pending.set(requestId, (reply) => {
|
|
1267
|
+
replies.push(reply);
|
|
1268
|
+
if (replies.length >= expected)
|
|
1269
|
+
finish();
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
689
1273
|
close() {
|
|
690
1274
|
this.#channel.close();
|
|
691
1275
|
this.#handlers.clear();
|
|
1276
|
+
this.#errorHandlers.clear();
|
|
692
1277
|
}
|
|
693
|
-
#publish(target, type, data) {
|
|
1278
|
+
#publish(target, type, data, expectsReply = false) {
|
|
694
1279
|
if (!type.trim())
|
|
695
1280
|
throw new TypeError("Shard message type is required.");
|
|
696
1281
|
const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
|
|
@@ -699,7 +1284,8 @@ class ShardBus {
|
|
|
699
1284
|
target,
|
|
700
1285
|
type,
|
|
701
1286
|
data,
|
|
702
|
-
id
|
|
1287
|
+
id,
|
|
1288
|
+
...expectsReply ? { expectsReply } : {}
|
|
703
1289
|
});
|
|
704
1290
|
return id;
|
|
705
1291
|
}
|
|
@@ -708,13 +1294,28 @@ class ShardBus {
|
|
|
708
1294
|
return;
|
|
709
1295
|
if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
|
|
710
1296
|
return;
|
|
1297
|
+
if (message.type === REPLY_TYPE) {
|
|
1298
|
+
const reply = message.data;
|
|
1299
|
+
this.#pending.get(reply?.requestId)?.({
|
|
1300
|
+
shardId: message.source,
|
|
1301
|
+
...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
|
|
1302
|
+
});
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
711
1305
|
for (const handler of this.#handlers.get(message.type) ?? []) {
|
|
712
1306
|
try {
|
|
713
1307
|
const result = handler(message);
|
|
714
1308
|
if (result && typeof result.then === "function")
|
|
715
|
-
Promise.resolve(result).catch(() =>
|
|
716
|
-
|
|
717
|
-
|
|
1309
|
+
Promise.resolve(result).catch((error) => this.#reportError(error, message));
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
this.#reportError(error, message);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
#reportError(error, message) {
|
|
1316
|
+
for (const handler of this.#errorHandlers) {
|
|
1317
|
+
try {
|
|
1318
|
+
handler(error, message);
|
|
718
1319
|
} catch {}
|
|
719
1320
|
}
|
|
720
1321
|
}
|
|
@@ -731,6 +1332,8 @@ class ClusterManager {
|
|
|
731
1332
|
#auto;
|
|
732
1333
|
#autoScaleTimer;
|
|
733
1334
|
#spawned = false;
|
|
1335
|
+
#stopping = false;
|
|
1336
|
+
#restartTimers = new Set;
|
|
734
1337
|
constructor(options) {
|
|
735
1338
|
if (!options.token?.trim())
|
|
736
1339
|
throw new TypeError("A Discord bot token is required.");
|
|
@@ -746,7 +1349,7 @@ class ClusterManager {
|
|
|
746
1349
|
const response = await fetch("https://discord.com/api/v10/gateway/bot", {
|
|
747
1350
|
headers: {
|
|
748
1351
|
Authorization: `Bot ${this.#options.token}`,
|
|
749
|
-
"User-Agent":
|
|
1352
|
+
"User-Agent": `Lunibee/${package_default.version}`
|
|
750
1353
|
}
|
|
751
1354
|
});
|
|
752
1355
|
if (!response.ok) {
|
|
@@ -775,19 +1378,7 @@ class ClusterManager {
|
|
|
775
1378
|
for (let i = 0;i < clusterCount; i++) {
|
|
776
1379
|
if (chunks[i].length === 0)
|
|
777
1380
|
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
|
-
});
|
|
1381
|
+
this.#launch(i, chunks[i], count);
|
|
791
1382
|
await sleep(500);
|
|
792
1383
|
}
|
|
793
1384
|
if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
|
|
@@ -796,6 +1387,37 @@ class ClusterManager {
|
|
|
796
1387
|
}, this.#options.autoScaleInterval);
|
|
797
1388
|
}
|
|
798
1389
|
}
|
|
1390
|
+
#launch(id, shards, shardCount) {
|
|
1391
|
+
const child = fork(this.#options.script, [], {
|
|
1392
|
+
env: {
|
|
1393
|
+
...process.env,
|
|
1394
|
+
SHARD_LIST: shards.join(","),
|
|
1395
|
+
SHARD_COUNT: shardCount.toString(),
|
|
1396
|
+
CLUSTER_ID: id.toString()
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
const info = { id, process: child, shards };
|
|
1400
|
+
this.clusters.set(id, info);
|
|
1401
|
+
child.once("exit", (code, signal) => {
|
|
1402
|
+
if (this.clusters.get(id)?.process !== child)
|
|
1403
|
+
return;
|
|
1404
|
+
try {
|
|
1405
|
+
this.#options.onClusterExit?.(info, code, signal);
|
|
1406
|
+
} catch {}
|
|
1407
|
+
if (this.#stopping || this.#options.restartOnExit === false)
|
|
1408
|
+
return;
|
|
1409
|
+
this.clusters.delete(id);
|
|
1410
|
+
const timer = setTimeout(() => {
|
|
1411
|
+
this.#restartTimers.delete(timer);
|
|
1412
|
+
if (this.#stopping || !this.#spawned)
|
|
1413
|
+
return;
|
|
1414
|
+
this.#launch(id, shards, shardCount);
|
|
1415
|
+
}, this.#options.restartDelay ?? 5000);
|
|
1416
|
+
timer.unref?.();
|
|
1417
|
+
this.#restartTimers.add(timer);
|
|
1418
|
+
});
|
|
1419
|
+
return info;
|
|
1420
|
+
}
|
|
799
1421
|
async checkAutoScale() {
|
|
800
1422
|
if (!this.#auto)
|
|
801
1423
|
return;
|
|
@@ -816,6 +1438,8 @@ class ClusterManager {
|
|
|
816
1438
|
await this.spawn();
|
|
817
1439
|
}
|
|
818
1440
|
async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
|
|
1441
|
+
this.#stopping = true;
|
|
1442
|
+
this.#clearRestartTimers();
|
|
819
1443
|
if (this.#autoScaleTimer) {
|
|
820
1444
|
clearInterval(this.#autoScaleTimer);
|
|
821
1445
|
this.#autoScaleTimer = undefined;
|
|
@@ -823,6 +1447,12 @@ class ClusterManager {
|
|
|
823
1447
|
await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
|
|
824
1448
|
this.clusters.clear();
|
|
825
1449
|
this.#spawned = false;
|
|
1450
|
+
this.#stopping = false;
|
|
1451
|
+
}
|
|
1452
|
+
#clearRestartTimers() {
|
|
1453
|
+
for (const timer of this.#restartTimers)
|
|
1454
|
+
clearTimeout(timer);
|
|
1455
|
+
this.#restartTimers.clear();
|
|
826
1456
|
}
|
|
827
1457
|
async#shutdownCluster(cluster, timeoutMs) {
|
|
828
1458
|
const child = cluster.process;
|
|
@@ -851,6 +1481,8 @@ class ClusterManager {
|
|
|
851
1481
|
});
|
|
852
1482
|
}
|
|
853
1483
|
killAll() {
|
|
1484
|
+
this.#stopping = true;
|
|
1485
|
+
this.#clearRestartTimers();
|
|
854
1486
|
if (this.#autoScaleTimer) {
|
|
855
1487
|
clearInterval(this.#autoScaleTimer);
|
|
856
1488
|
this.#autoScaleTimer = undefined;
|
|
@@ -862,13 +1494,43 @@ class ClusterManager {
|
|
|
862
1494
|
}
|
|
863
1495
|
this.clusters.clear();
|
|
864
1496
|
this.#spawned = false;
|
|
1497
|
+
this.#stopping = false;
|
|
865
1498
|
}
|
|
866
1499
|
}
|
|
867
1500
|
|
|
868
1501
|
// packages/sharding/src/index.ts
|
|
869
1502
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1503
|
+
function needsConnection(gateway) {
|
|
1504
|
+
return gateway.state === "CONNECT" /* Connect */ || gateway.state === "RECONNECT" /* Reconnect */ || gateway.state === "CLOSED" /* Closed */;
|
|
1505
|
+
}
|
|
1506
|
+
var HANDSHAKE_SENT = new Set([
|
|
1507
|
+
"IDENTIFY" /* Identify */,
|
|
1508
|
+
"RESUME" /* Resume */,
|
|
1509
|
+
"READY" /* Ready */,
|
|
1510
|
+
"DISPATCH" /* Dispatch */,
|
|
1511
|
+
"HEARTBEAT" /* Heartbeat */,
|
|
1512
|
+
"CLOSED" /* Closed */
|
|
1513
|
+
]);
|
|
1514
|
+
function handshakeSent(gateway, timeout) {
|
|
1515
|
+
if (HANDSHAKE_SENT.has(gateway.state))
|
|
1516
|
+
return Promise.resolve();
|
|
1517
|
+
return new Promise((resolve) => {
|
|
1518
|
+
const done = () => {
|
|
1519
|
+
clearTimeout(timer);
|
|
1520
|
+
gateway.off("stateChange", onState);
|
|
1521
|
+
resolve();
|
|
1522
|
+
};
|
|
1523
|
+
const onState = (change) => {
|
|
1524
|
+
if (HANDSHAKE_SENT.has(change.next))
|
|
1525
|
+
done();
|
|
1526
|
+
};
|
|
1527
|
+
const timer = setTimeout(done, timeout);
|
|
1528
|
+
gateway.on("stateChange", onState);
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
870
1531
|
|
|
871
1532
|
class ShardManager {
|
|
1533
|
+
static IDENTIFY_INTERVAL = 5000;
|
|
872
1534
|
shards = new Map;
|
|
873
1535
|
get shardCount() {
|
|
874
1536
|
return this.shards.size;
|
|
@@ -878,6 +1540,7 @@ class ShardManager {
|
|
|
878
1540
|
#destroyed = false;
|
|
879
1541
|
#auto;
|
|
880
1542
|
#autoScaleTimer;
|
|
1543
|
+
#gatewayInfo;
|
|
881
1544
|
constructor(options) {
|
|
882
1545
|
if (!options.token?.trim())
|
|
883
1546
|
throw new TypeError("A Discord bot token is required.");
|
|
@@ -888,16 +1551,28 @@ class ShardManager {
|
|
|
888
1551
|
throw new RangeError("Shard count must be a positive integer.");
|
|
889
1552
|
if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
|
|
890
1553
|
throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
|
|
1554
|
+
if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
|
|
1555
|
+
throw new RangeError("spawnDelay must be a non-negative finite number of milliseconds.");
|
|
1556
|
+
if (options.maxConcurrency !== undefined && (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1))
|
|
1557
|
+
throw new RangeError("maxConcurrency must be a positive integer.");
|
|
1558
|
+
if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
|
|
1559
|
+
throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
|
|
891
1560
|
this.#auto = options.shardCount === "auto";
|
|
892
|
-
this.#options = {
|
|
1561
|
+
this.#options = {
|
|
1562
|
+
spawnDelay: ShardManager.IDENTIFY_INTERVAL,
|
|
1563
|
+
...options
|
|
1564
|
+
};
|
|
893
1565
|
if (options.shardCount !== "auto")
|
|
894
1566
|
this.#initialize(count);
|
|
895
1567
|
}
|
|
896
1568
|
async fetchRecommendedShardCount() {
|
|
1569
|
+
return (await this.fetchGatewayInfo()).shards;
|
|
1570
|
+
}
|
|
1571
|
+
async fetchGatewayInfo() {
|
|
897
1572
|
const response = await fetch("https://discord.com/api/v10/gateway/bot", {
|
|
898
1573
|
headers: {
|
|
899
1574
|
Authorization: `Bot ${this.#options.token}`,
|
|
900
|
-
"User-Agent":
|
|
1575
|
+
"User-Agent": `Lunibee/${package_default.version}`
|
|
901
1576
|
}
|
|
902
1577
|
});
|
|
903
1578
|
if (!response.ok)
|
|
@@ -905,7 +1580,17 @@ class ShardManager {
|
|
|
905
1580
|
const data = await response.json();
|
|
906
1581
|
if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
|
|
907
1582
|
throw new Error("Gateway discovery returned an invalid shard count.");
|
|
908
|
-
|
|
1583
|
+
const limit = data.session_start_limit;
|
|
1584
|
+
const info = { shards: data.shards };
|
|
1585
|
+
if (limit && typeof limit.total === "number" && typeof limit.remaining === "number" && typeof limit.reset_after === "number" && typeof limit.max_concurrency === "number")
|
|
1586
|
+
info.sessionStartLimit = {
|
|
1587
|
+
total: limit.total,
|
|
1588
|
+
remaining: limit.remaining,
|
|
1589
|
+
resetAfter: limit.reset_after,
|
|
1590
|
+
maxConcurrency: Math.max(1, limit.max_concurrency)
|
|
1591
|
+
};
|
|
1592
|
+
this.#gatewayInfo = info;
|
|
1593
|
+
return info;
|
|
909
1594
|
}
|
|
910
1595
|
async connect() {
|
|
911
1596
|
if (this.#autoScaleTimer) {
|
|
@@ -913,17 +1598,25 @@ class ShardManager {
|
|
|
913
1598
|
this.#autoScaleTimer = undefined;
|
|
914
1599
|
}
|
|
915
1600
|
await this.#ensureInitialized();
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
1601
|
+
const limit = this.#gatewayInfo?.sessionStartLimit;
|
|
1602
|
+
const ids = [...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id);
|
|
1603
|
+
if (limit && limit.remaining < ids.length)
|
|
1604
|
+
throw new Error(`Session start limit exhausted: ${limit.remaining} of ${limit.total} IDENTIFYs left for ${ids.length} shards; resets in ${Math.ceil(limit.resetAfter / 1000)}s.`);
|
|
1605
|
+
const concurrency = Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
|
|
1606
|
+
for (let start = 0;start < ids.length; start += concurrency) {
|
|
1607
|
+
const round = ids.slice(start, start + concurrency);
|
|
1608
|
+
const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
|
|
1609
|
+
const failed = results.findIndex((result) => result.status === "rejected");
|
|
1610
|
+
if (failed !== -1) {
|
|
920
1611
|
this.destroy();
|
|
921
|
-
throw new Error(`Failed to connect shard ${
|
|
922
|
-
cause:
|
|
1612
|
+
throw new Error(`Failed to connect shard ${round[failed]}.`, {
|
|
1613
|
+
cause: results[failed].reason
|
|
923
1614
|
});
|
|
924
1615
|
}
|
|
925
|
-
if (
|
|
1616
|
+
if (start + concurrency < ids.length && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
|
|
1617
|
+
await Promise.all(round.map((id) => handshakeSent(this.shards.get(id), this.#options.handshakeTimeout ?? 15000)));
|
|
926
1618
|
await sleep2(this.#options.spawnDelay);
|
|
1619
|
+
}
|
|
927
1620
|
}
|
|
928
1621
|
if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
|
|
929
1622
|
this.#autoScaleTimer = setInterval(() => {
|
|
@@ -966,6 +1659,13 @@ class ShardManager {
|
|
|
966
1659
|
get(id) {
|
|
967
1660
|
return this.shards.get(id);
|
|
968
1661
|
}
|
|
1662
|
+
health() {
|
|
1663
|
+
return [...this.shards].map(([id, gateway]) => ({
|
|
1664
|
+
id,
|
|
1665
|
+
state: gateway.state,
|
|
1666
|
+
ping: gateway.ping
|
|
1667
|
+
}));
|
|
1668
|
+
}
|
|
969
1669
|
values() {
|
|
970
1670
|
return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
|
|
971
1671
|
}
|
|
@@ -997,7 +1697,8 @@ class ShardManager {
|
|
|
997
1697
|
export {
|
|
998
1698
|
ClusterManager,
|
|
999
1699
|
ShardBus,
|
|
1000
|
-
ShardManager
|
|
1700
|
+
ShardManager,
|
|
1701
|
+
ShardManager as ShardingManager
|
|
1001
1702
|
};
|
|
1002
1703
|
|
|
1003
|
-
//# debugId=
|
|
1704
|
+
//# debugId=80E4CAFE56ABED2364756E2164756E21
|