lunibee 0.2.3 → 0.3.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/README.md +3 -3
- package/dist/builders/attachment.d.ts +8 -4
- package/dist/builders/commands.d.ts +122 -149
- package/dist/builders/components.d.ts +10 -150
- package/dist/builders/embed.d.ts +66 -37
- package/dist/builders/index.d.ts +0 -4
- package/dist/builders/index.js +112 -221
- package/dist/builders/index.js.map +10 -9
- package/dist/collection/cache.d.ts +15 -6
- package/dist/collection/index.d.ts +92 -17
- package/dist/collection/index.js +471 -279
- package/dist/collection/index.js.map +4 -4
- package/dist/core/api.d.ts +130 -0
- package/dist/core/channel-create.d.ts +72 -0
- package/dist/core/commands.d.ts +232 -0
- package/dist/core/events.d.ts +24 -7
- package/dist/core/handles.d.ts +157 -0
- package/dist/core/index.d.ts +68 -4
- package/dist/core/index.js +2639 -493
- package/dist/core/index.js.map +47 -37
- package/dist/core/permissions.d.ts +7 -7
- package/dist/formatters/index.js +9 -5
- package/dist/formatters/index.js.map +3 -3
- package/dist/handlers/index.js +5 -10
- package/dist/handlers/index.js.map +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9527 -7809
- package/dist/index.js.map +63 -53
- package/dist/managers/advanced.d.ts +29 -1
- package/dist/managers/base.d.ts +22 -14
- package/dist/managers/emoji.d.ts +0 -2
- package/dist/managers/guild-resources.d.ts +13 -5
- package/dist/managers/guild.d.ts +40 -4
- package/dist/managers/index.d.ts +21 -13
- package/dist/managers/index.js +969 -386
- package/dist/managers/index.js.map +30 -25
- package/dist/managers/member.d.ts +9 -2
- package/dist/managers/message.d.ts +10 -15
- package/dist/managers/role.d.ts +18 -10
- package/dist/managers/thread.d.ts +68 -0
- package/dist/managers/user.d.ts +18 -0
- package/dist/rest/errors.d.ts +14 -7
- package/dist/rest/index.d.ts +24 -12
- package/dist/rest/index.js +70 -20
- package/dist/rest/index.js.map +9 -9
- package/dist/rest/redis.d.ts +1 -1
- package/dist/rest/route.d.ts +8 -4
- package/dist/rest/routes.d.ts +122 -48
- package/dist/rest/transport.d.ts +10 -5
- package/dist/rest/webhook.d.ts +9 -5
- package/dist/sharding/bus.d.ts +47 -19
- package/dist/sharding/cluster.d.ts +11 -0
- package/dist/sharding/gateway-bot.d.ts +18 -0
- package/dist/sharding/index.d.ts +64 -25
- package/dist/sharding/index.js +439 -124
- package/dist/sharding/index.js.map +19 -14
- package/dist/sharding/supervisor.d.ts +32 -0
- package/dist/sharding/transport.d.ts +45 -0
- package/dist/structures/audit-log.d.ts +99 -2
- package/dist/structures/base.d.ts +40 -23
- package/dist/structures/channels.d.ts +45 -14
- package/dist/structures/index.d.ts +8 -12
- package/dist/structures/index.js +280 -132
- package/dist/structures/index.js.map +21 -15
- package/dist/structures/interactions.d.ts +91 -63
- package/dist/structures/resources.d.ts +99 -52
- package/dist/structures/sticker.d.ts +33 -0
- package/dist/types/channel-kind.d.ts +26 -0
- package/dist/types/gateway.d.ts +24 -12
- package/dist/types/index.d.ts +115 -113
- package/dist/types/index.js +53 -48
- package/dist/types/index.js.map +6 -5
- package/dist/voice/index.d.ts +12 -4
- package/dist/voice/index.js +14 -18
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/decoder.d.ts +6 -2
- package/dist/ws/dispatcher.d.ts +19 -0
- package/dist/ws/index.d.ts +61 -15
- package/dist/ws/index.js +194 -77
- package/dist/ws/index.js.map +13 -11
- package/dist/ws/protocol.d.ts +10 -2
- package/dist/ws/reconnect.d.ts +10 -0
- package/dist/ws/send-budget.d.ts +6 -2
- package/dist/ws/session.d.ts +14 -4
- package/dist/ws/state.d.ts +22 -11
- package/package.json +20 -16
package/dist/sharding/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// packages/sharding/package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@lunibee/sharding",
|
|
5
|
-
version: "0.
|
|
5
|
+
version: "0.3.0",
|
|
6
6
|
type: "module",
|
|
7
7
|
module: "./src/index.ts",
|
|
8
8
|
types: "./src/index.ts",
|
|
@@ -14,6 +14,31 @@ var package_default = {
|
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// packages/sharding/src/gateway-bot.ts
|
|
18
|
+
async function fetchGatewayBot(token) {
|
|
19
|
+
const response = await fetch("https://discord.com/api/v10/gateway/bot", {
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bot ${token}`,
|
|
22
|
+
"User-Agent": `Lunibee/${package_default.version}`
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
throw new Error(`Gateway discovery failed with status ${response.status}`);
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
|
|
29
|
+
throw new Error("Gateway discovery returned an invalid shard count.");
|
|
30
|
+
const limit = data.session_start_limit;
|
|
31
|
+
const info = { shards: data.shards };
|
|
32
|
+
if (limit && typeof limit.total === "number" && typeof limit.remaining === "number" && typeof limit.reset_after === "number" && typeof limit.max_concurrency === "number")
|
|
33
|
+
info.sessionStartLimit = {
|
|
34
|
+
total: limit.total,
|
|
35
|
+
remaining: limit.remaining,
|
|
36
|
+
resetAfter: limit.reset_after,
|
|
37
|
+
maxConcurrency: Math.max(1, limit.max_concurrency)
|
|
38
|
+
};
|
|
39
|
+
return info;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
// packages/ws/src/session.ts
|
|
18
43
|
class GatewaySession {
|
|
19
44
|
#state = { status: "none" };
|
|
@@ -88,7 +113,26 @@ class GatewaySession {
|
|
|
88
113
|
return resume ? { type: "resume", ...resume } : { type: "identify" };
|
|
89
114
|
}
|
|
90
115
|
connectURL(fallback) {
|
|
91
|
-
|
|
116
|
+
const resume = this.resumeInfo()?.resumeURL;
|
|
117
|
+
return resume !== undefined && isTrustedGatewayURL(resume, fallback) ? resume : fallback;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function isTrustedGatewayURL(url, fallback) {
|
|
121
|
+
let parsed;
|
|
122
|
+
try {
|
|
123
|
+
parsed = new URL(url);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (parsed.protocol !== "wss:" || parsed.username || parsed.password)
|
|
128
|
+
return false;
|
|
129
|
+
const host = parsed.hostname.toLowerCase();
|
|
130
|
+
if (host === "discord.gg" || host.endsWith(".discord.gg"))
|
|
131
|
+
return true;
|
|
132
|
+
try {
|
|
133
|
+
return host === new URL(fallback).hostname.toLowerCase();
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
92
136
|
}
|
|
93
137
|
}
|
|
94
138
|
|
|
@@ -260,6 +304,7 @@ class GatewayReconnect {
|
|
|
260
304
|
#options;
|
|
261
305
|
#random;
|
|
262
306
|
#attempt = 0;
|
|
307
|
+
#recent = [];
|
|
263
308
|
#timer;
|
|
264
309
|
#promise;
|
|
265
310
|
#resolve;
|
|
@@ -300,14 +345,29 @@ class GatewayReconnect {
|
|
|
300
345
|
if (this.exhausted)
|
|
301
346
|
return { scheduled: false, reason: "exhausted" };
|
|
302
347
|
const attempt = this.#attempt;
|
|
303
|
-
const
|
|
348
|
+
const now = (this.#options.now ?? Date.now)();
|
|
349
|
+
const window = this.#options.stormWindow ?? 60000;
|
|
350
|
+
const limit = this.#options.stormLimit ?? 5;
|
|
351
|
+
while (this.#recent.length && now - this.#recent[0] >= window)
|
|
352
|
+
this.#recent.shift();
|
|
353
|
+
const over = this.#recent.length - limit;
|
|
354
|
+
this.#recent.push(now);
|
|
355
|
+
let delay = Math.min(this.#options.maxDelay, this.#options.baseDelay * 2 ** this.#attempt++);
|
|
356
|
+
const unstable = over >= 0;
|
|
357
|
+
if (unstable)
|
|
358
|
+
delay = Math.min(this.#options.maxDelay, Math.max(delay, this.#options.baseDelay * 2 ** (over + 1)));
|
|
304
359
|
const jitter = this.#random() * Math.max(1, delay * 0.25);
|
|
305
360
|
const delayMs = delay + jitter;
|
|
306
361
|
this.#timer = setTimeout(() => {
|
|
307
362
|
this.#timer = undefined;
|
|
308
363
|
run();
|
|
309
364
|
}, delayMs);
|
|
310
|
-
return {
|
|
365
|
+
return {
|
|
366
|
+
scheduled: true,
|
|
367
|
+
delayMs,
|
|
368
|
+
attempt,
|
|
369
|
+
...unstable ? { unstable: { reconnects: this.#recent.length } } : {}
|
|
370
|
+
};
|
|
311
371
|
}
|
|
312
372
|
cancel() {
|
|
313
373
|
if (this.#timer)
|
|
@@ -364,12 +424,15 @@ class PlainTextDecoder {
|
|
|
364
424
|
|
|
365
425
|
class ZlibStreamDecoder {
|
|
366
426
|
#create;
|
|
427
|
+
#maxFrameBytes;
|
|
367
428
|
#inflate;
|
|
368
429
|
#chunks = [];
|
|
430
|
+
#size = 0;
|
|
369
431
|
#failure;
|
|
370
432
|
#pendingReject;
|
|
371
|
-
constructor(create = () => createInflate()) {
|
|
433
|
+
constructor(create = () => createInflate(), maxFrameBytes = 64 * 1024 * 1024) {
|
|
372
434
|
this.#create = create;
|
|
435
|
+
this.#maxFrameBytes = maxFrameBytes;
|
|
373
436
|
}
|
|
374
437
|
push(data) {
|
|
375
438
|
if (typeof data === "string")
|
|
@@ -388,6 +451,7 @@ class ZlibStreamDecoder {
|
|
|
388
451
|
await this.#awaiting((settle) => inflate.flush(zlibConstants.Z_SYNC_FLUSH, () => settle()));
|
|
389
452
|
const parts = this.#chunks;
|
|
390
453
|
this.#chunks = [];
|
|
454
|
+
this.#size = 0;
|
|
391
455
|
if (parts.length === 0)
|
|
392
456
|
return [];
|
|
393
457
|
const total = parts.reduce((size, part) => size + part.length, 0);
|
|
@@ -403,6 +467,7 @@ class ZlibStreamDecoder {
|
|
|
403
467
|
const inflate = this.#inflate;
|
|
404
468
|
this.#inflate = undefined;
|
|
405
469
|
this.#chunks = [];
|
|
470
|
+
this.#size = 0;
|
|
406
471
|
this.#failure = undefined;
|
|
407
472
|
this.#pendingReject = undefined;
|
|
408
473
|
if (!inflate)
|
|
@@ -430,7 +495,17 @@ class ZlibStreamDecoder {
|
|
|
430
495
|
}
|
|
431
496
|
#open() {
|
|
432
497
|
const inflate = this.#create();
|
|
433
|
-
inflate.on("data", (chunk) =>
|
|
498
|
+
inflate.on("data", (chunk) => {
|
|
499
|
+
if (this.#failure)
|
|
500
|
+
return;
|
|
501
|
+
this.#size += chunk.length;
|
|
502
|
+
if (this.#size > this.#maxFrameBytes) {
|
|
503
|
+
this.#chunks = [];
|
|
504
|
+
inflate.emit("error", new Error(`Gateway frame inflated past ${this.#maxFrameBytes} bytes.`));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.#chunks.push(chunk);
|
|
508
|
+
});
|
|
434
509
|
inflate.on("error", (error) => {
|
|
435
510
|
this.#failure = error;
|
|
436
511
|
const reject = this.#pendingReject;
|
|
@@ -673,9 +748,9 @@ function classifyPayload(payload) {
|
|
|
673
748
|
}
|
|
674
749
|
function identifyPayload(options) {
|
|
675
750
|
const props = options.properties ?? {};
|
|
676
|
-
const os = props.os ??
|
|
677
|
-
const browser = props.browser ?? "
|
|
678
|
-
const device = props.device ?? "
|
|
751
|
+
const os = props.os ?? process.platform;
|
|
752
|
+
const browser = props.browser ?? "Lunibee";
|
|
753
|
+
const device = props.device ?? "Lunibee";
|
|
679
754
|
return {
|
|
680
755
|
op: GatewayOpcodes.Identify,
|
|
681
756
|
d: {
|
|
@@ -718,6 +793,21 @@ function heartbeatPayload(sequence) {
|
|
|
718
793
|
return { op: GatewayOpcodes.Heartbeat, d: sequence, s: null, t: null };
|
|
719
794
|
}
|
|
720
795
|
|
|
796
|
+
// packages/types/src/channel-kind.ts
|
|
797
|
+
var ChannelKinds = {
|
|
798
|
+
text: [0],
|
|
799
|
+
dm: [1],
|
|
800
|
+
voice: [2],
|
|
801
|
+
"group-dm": [3],
|
|
802
|
+
category: [4],
|
|
803
|
+
announcement: [5],
|
|
804
|
+
thread: [10, 11, 12],
|
|
805
|
+
stage: [13],
|
|
806
|
+
directory: [14],
|
|
807
|
+
forum: [15],
|
|
808
|
+
media: [16]
|
|
809
|
+
};
|
|
810
|
+
var KIND_OF_TYPE = new Map(Object.keys(ChannelKinds).flatMap((kind) => ChannelKinds[kind].map((type) => [type, kind])));
|
|
721
811
|
// packages/types/src/gateway.ts
|
|
722
812
|
var GatewayIntentBits = {
|
|
723
813
|
Guilds: 1 << 0,
|
|
@@ -771,40 +861,59 @@ var IntentBits = {
|
|
|
771
861
|
directMessagePolls: 1 << 25
|
|
772
862
|
};
|
|
773
863
|
function resolveGatewayIntents(intents) {
|
|
774
|
-
if (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return intents.bitfield;
|
|
778
|
-
if (Array.isArray(intents)) {
|
|
779
|
-
return intents.reduce((acc, intent) => {
|
|
780
|
-
return acc | resolveGatewayIntents(intent);
|
|
781
|
-
}, 0);
|
|
782
|
-
}
|
|
783
|
-
if (typeof intents === "string") {
|
|
784
|
-
if (intents in GatewayIntentBits)
|
|
785
|
-
return GatewayIntentBits[intents];
|
|
786
|
-
if (intents in IntentBits)
|
|
787
|
-
return IntentBits[intents];
|
|
788
|
-
const lower = intents.charAt(0).toLowerCase() + intents.slice(1);
|
|
789
|
-
if (lower in IntentBits)
|
|
790
|
-
return IntentBits[lower];
|
|
791
|
-
const num = Number(intents);
|
|
792
|
-
if (!Number.isNaN(num))
|
|
793
|
-
return num;
|
|
794
|
-
}
|
|
795
|
-
return 0;
|
|
864
|
+
if (!Array.isArray(intents))
|
|
865
|
+
throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages].");
|
|
866
|
+
return intents.reduce((acc, intent) => acc | resolveIntent(intent), 0);
|
|
796
867
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
868
|
+
function resolveIntent(intent) {
|
|
869
|
+
if (typeof intent === "number")
|
|
870
|
+
return intent;
|
|
871
|
+
const bits = {
|
|
872
|
+
...IntentBits,
|
|
873
|
+
...GatewayIntentBits
|
|
874
|
+
};
|
|
875
|
+
const lower = intent.charAt(0).toLowerCase() + intent.slice(1);
|
|
876
|
+
if (Object.hasOwn(bits, intent))
|
|
877
|
+
return bits[intent];
|
|
878
|
+
if (Object.hasOwn(bits, lower))
|
|
879
|
+
return bits[lower];
|
|
880
|
+
const num = Number(intent);
|
|
881
|
+
return Number.isNaN(num) ? 0 : num;
|
|
882
|
+
}
|
|
883
|
+
// packages/ws/src/dispatcher.ts
|
|
884
|
+
class GatewayDispatcher {
|
|
885
|
+
#listeners = new Map;
|
|
886
|
+
#onFailure;
|
|
887
|
+
constructor(onFailure) {
|
|
888
|
+
this.#onFailure = onFailure;
|
|
889
|
+
}
|
|
890
|
+
on(event, listener) {
|
|
891
|
+
let listeners = this.#listeners.get(event);
|
|
892
|
+
if (!listeners)
|
|
893
|
+
this.#listeners.set(event, listeners = new Set);
|
|
894
|
+
listeners.add(listener);
|
|
895
|
+
}
|
|
896
|
+
off(event, listener) {
|
|
897
|
+
this.#listeners.get(event)?.delete(listener);
|
|
898
|
+
}
|
|
899
|
+
emit(event, data) {
|
|
900
|
+
for (const listener of this.#listeners.get(event) ?? []) {
|
|
901
|
+
try {
|
|
902
|
+
const result = listener(data);
|
|
903
|
+
if (result && typeof result.then === "function")
|
|
904
|
+
Promise.resolve(result).catch((error) => {
|
|
905
|
+
if (event !== "error")
|
|
906
|
+
this.#onFailure(error);
|
|
907
|
+
});
|
|
908
|
+
} catch (error) {
|
|
909
|
+
if (event !== "error")
|
|
910
|
+
this.#onFailure(error);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
804
913
|
}
|
|
805
914
|
}
|
|
806
915
|
// packages/ws/src/send-budget.ts
|
|
807
|
-
class
|
|
916
|
+
class GatewaySendLimiter {
|
|
808
917
|
#timestamps = [];
|
|
809
918
|
#limit;
|
|
810
919
|
#windowMs;
|
|
@@ -824,7 +933,15 @@ class SendBudget {
|
|
|
824
933
|
this.#timestamps.push(now);
|
|
825
934
|
}
|
|
826
935
|
}
|
|
827
|
-
|
|
936
|
+
// packages/ws/src/state.ts
|
|
937
|
+
class GatewayError extends Error {
|
|
938
|
+
code;
|
|
939
|
+
constructor(message, code, options) {
|
|
940
|
+
super(message, options);
|
|
941
|
+
this.name = "GatewayError";
|
|
942
|
+
this.code = code;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
828
945
|
// packages/ws/src/index.ts
|
|
829
946
|
var RESUMABLE_CLOSE_CODE = 4900;
|
|
830
947
|
var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
@@ -838,8 +955,9 @@ class Gateway {
|
|
|
838
955
|
#heartbeat;
|
|
839
956
|
#closed = false;
|
|
840
957
|
#reconnect;
|
|
841
|
-
#
|
|
842
|
-
#sendBudget = new
|
|
958
|
+
#events = new GatewayDispatcher((error) => this.#emitError(error));
|
|
959
|
+
#sendBudget = new GatewaySendLimiter;
|
|
960
|
+
#reconnects = 0;
|
|
843
961
|
ping = -1;
|
|
844
962
|
constructor(options) {
|
|
845
963
|
if (!options.token?.trim())
|
|
@@ -857,9 +975,9 @@ class Gateway {
|
|
|
857
975
|
heartbeatAckTimeout: 1e4,
|
|
858
976
|
zombieTimeout: 30000,
|
|
859
977
|
properties: {
|
|
860
|
-
os:
|
|
861
|
-
browser: "
|
|
862
|
-
device: "
|
|
978
|
+
os: process.platform,
|
|
979
|
+
browser: "Lunibee",
|
|
980
|
+
device: "Lunibee"
|
|
863
981
|
},
|
|
864
982
|
presence: {
|
|
865
983
|
status: "online",
|
|
@@ -890,7 +1008,9 @@ class Gateway {
|
|
|
890
1008
|
enabled: this.#options.reconnect,
|
|
891
1009
|
maxAttempts: this.#options.maxReconnectAttempts,
|
|
892
1010
|
baseDelay: this.#options.reconnectBaseDelay,
|
|
893
|
-
maxDelay: this.#options.reconnectMaxDelay
|
|
1011
|
+
maxDelay: this.#options.reconnectMaxDelay,
|
|
1012
|
+
stormLimit: options.reconnectStormLimit,
|
|
1013
|
+
stormWindow: options.reconnectStormWindow
|
|
894
1014
|
});
|
|
895
1015
|
this.#heartbeat = new GatewayHeartbeat({
|
|
896
1016
|
ackTimeout: this.#options.heartbeatAckTimeout,
|
|
@@ -941,16 +1061,21 @@ class Gateway {
|
|
|
941
1061
|
on(event, listener) {
|
|
942
1062
|
if (!event || typeof listener !== "function")
|
|
943
1063
|
throw new TypeError("Gateway event and listener are required.");
|
|
944
|
-
|
|
945
|
-
if (!listeners)
|
|
946
|
-
this.#listeners.set(event, listeners = new Set);
|
|
947
|
-
listeners.add(listener);
|
|
1064
|
+
this.#events.on(event, listener);
|
|
948
1065
|
return this;
|
|
949
1066
|
}
|
|
950
1067
|
off(event, listener) {
|
|
951
|
-
this.#
|
|
1068
|
+
this.#events.off(event, listener);
|
|
952
1069
|
return this;
|
|
953
1070
|
}
|
|
1071
|
+
health() {
|
|
1072
|
+
return {
|
|
1073
|
+
state: this.state,
|
|
1074
|
+
latency: this.#heartbeat.latency,
|
|
1075
|
+
silentFor: this.#heartbeat.silentFor,
|
|
1076
|
+
reconnects: this.#reconnects
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
954
1079
|
send(payload) {
|
|
955
1080
|
if (!payload || !Number.isInteger(payload.op))
|
|
956
1081
|
throw new TypeError("Gateway payload must contain an integer opcode.");
|
|
@@ -992,6 +1117,22 @@ class Gateway {
|
|
|
992
1117
|
t: null
|
|
993
1118
|
});
|
|
994
1119
|
}
|
|
1120
|
+
sendVoiceState(guildId, channelId, options = {}) {
|
|
1121
|
+
if (!this.ownsGuild(guildId))
|
|
1122
|
+
return false;
|
|
1123
|
+
return this.setVoiceState({
|
|
1124
|
+
guild_id: guildId,
|
|
1125
|
+
channel_id: channelId,
|
|
1126
|
+
self_mute: options.selfMute ?? false,
|
|
1127
|
+
self_deaf: options.selfDeaf ?? false
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
ownsGuild(guildId) {
|
|
1131
|
+
const count = this.#options.shardCount;
|
|
1132
|
+
if (count <= 1)
|
|
1133
|
+
return true;
|
|
1134
|
+
return Number((BigInt(guildId) >> 22n) % BigInt(count)) === this.#options.shardId;
|
|
1135
|
+
}
|
|
995
1136
|
requestGuildMembers(data) {
|
|
996
1137
|
return this.send({
|
|
997
1138
|
op: GatewayOpcodes.RequestGuildMembers,
|
|
@@ -1129,7 +1270,16 @@ class Gateway {
|
|
|
1129
1270
|
if (this.#closed)
|
|
1130
1271
|
return;
|
|
1131
1272
|
const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
|
|
1132
|
-
if (result.scheduled
|
|
1273
|
+
if (result.scheduled) {
|
|
1274
|
+
this.#reconnects++;
|
|
1275
|
+
if (result.unstable)
|
|
1276
|
+
this.#emit("unstable", {
|
|
1277
|
+
reconnects: result.unstable.reconnects,
|
|
1278
|
+
delay: result.delayMs
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (result.reason !== "exhausted")
|
|
1133
1283
|
return;
|
|
1134
1284
|
this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
|
|
1135
1285
|
this.#setState("CONNECT" /* Connect */);
|
|
@@ -1150,19 +1300,7 @@ class Gateway {
|
|
|
1150
1300
|
this.#emit("error", error instanceof Error ? error : new GatewayError(String(error)));
|
|
1151
1301
|
}
|
|
1152
1302
|
#emit(event, data) {
|
|
1153
|
-
|
|
1154
|
-
try {
|
|
1155
|
-
const result = listener(data);
|
|
1156
|
-
if (result && typeof result.then === "function")
|
|
1157
|
-
Promise.resolve(result).catch((error) => {
|
|
1158
|
-
if (event !== "error")
|
|
1159
|
-
this.#emitError(error);
|
|
1160
|
-
});
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
if (event !== "error")
|
|
1163
|
-
this.#emitError(error);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1303
|
+
this.#events.emit(event, data);
|
|
1166
1304
|
}
|
|
1167
1305
|
#setState(next) {
|
|
1168
1306
|
if (this.state === next)
|
|
@@ -1172,12 +1310,70 @@ class Gateway {
|
|
|
1172
1310
|
this.#emit("stateChange", { previous, next });
|
|
1173
1311
|
}
|
|
1174
1312
|
}
|
|
1313
|
+
// packages/sharding/src/transport.ts
|
|
1314
|
+
class BroadcastChannelTransport {
|
|
1315
|
+
#channel;
|
|
1316
|
+
constructor(name) {
|
|
1317
|
+
this.#channel = new BroadcastChannel(name);
|
|
1318
|
+
}
|
|
1319
|
+
post(message) {
|
|
1320
|
+
this.#channel.postMessage(message);
|
|
1321
|
+
}
|
|
1322
|
+
onMessage(listener) {
|
|
1323
|
+
this.#channel.addEventListener("message", (event) => listener(event.data));
|
|
1324
|
+
}
|
|
1325
|
+
close() {
|
|
1326
|
+
this.#channel.close();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
var BUS_FRAME = "__lunibeeBus";
|
|
1330
|
+
var PEER_DOWN_FRAME = "__lunibeePeerDown";
|
|
1331
|
+
|
|
1332
|
+
class IpcTransport {
|
|
1333
|
+
#endpoint;
|
|
1334
|
+
#listener;
|
|
1335
|
+
#message;
|
|
1336
|
+
#down;
|
|
1337
|
+
constructor(endpoint = process) {
|
|
1338
|
+
if (typeof endpoint.send !== "function")
|
|
1339
|
+
throw new Error("IpcTransport needs an IPC channel: run the shard in a process forked by ClusterManager.");
|
|
1340
|
+
this.#endpoint = endpoint;
|
|
1341
|
+
this.#listener = (frame) => {
|
|
1342
|
+
const record = frame;
|
|
1343
|
+
if (!record || typeof record !== "object")
|
|
1344
|
+
return;
|
|
1345
|
+
if (BUS_FRAME in record)
|
|
1346
|
+
this.#message?.(record[BUS_FRAME]);
|
|
1347
|
+
else if (PEER_DOWN_FRAME in record)
|
|
1348
|
+
this.#down?.(record[PEER_DOWN_FRAME]);
|
|
1349
|
+
};
|
|
1350
|
+
endpoint.on("message", this.#listener);
|
|
1351
|
+
}
|
|
1352
|
+
post(message) {
|
|
1353
|
+
this.#endpoint.send({ [BUS_FRAME]: message });
|
|
1354
|
+
}
|
|
1355
|
+
onMessage(listener) {
|
|
1356
|
+
this.#message = listener;
|
|
1357
|
+
}
|
|
1358
|
+
onPeerDown(listener) {
|
|
1359
|
+
this.#down = listener;
|
|
1360
|
+
}
|
|
1361
|
+
close() {
|
|
1362
|
+
this.#endpoint.off("message", this.#listener);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function defaultTransport(name) {
|
|
1366
|
+
if (process.env.LUNIBEE_SHARD_BUS === "ipc" && typeof process.send === "function")
|
|
1367
|
+
return new IpcTransport;
|
|
1368
|
+
return new BroadcastChannelTransport(name);
|
|
1369
|
+
}
|
|
1175
1370
|
|
|
1176
1371
|
// packages/sharding/src/bus.ts
|
|
1177
1372
|
var REPLY_TYPE = "__lunibee:reply";
|
|
1178
1373
|
|
|
1179
1374
|
class ShardBus {
|
|
1180
|
-
#
|
|
1375
|
+
#transport;
|
|
1376
|
+
#maxPending;
|
|
1181
1377
|
#handlers = new Map;
|
|
1182
1378
|
#errorHandlers = new Set;
|
|
1183
1379
|
#shardId;
|
|
@@ -1185,7 +1381,7 @@ class ShardBus {
|
|
|
1185
1381
|
channelName;
|
|
1186
1382
|
#counter = 0;
|
|
1187
1383
|
#pending = new Map;
|
|
1188
|
-
constructor(shardId, channelName) {
|
|
1384
|
+
constructor(shardId, channelName, options = {}) {
|
|
1189
1385
|
if (!Number.isInteger(shardId) || shardId < 0)
|
|
1190
1386
|
throw new RangeError("Shard ID must be a non-negative integer.");
|
|
1191
1387
|
if (!channelName?.trim())
|
|
@@ -1193,8 +1389,10 @@ class ShardBus {
|
|
|
1193
1389
|
this.#shardId = shardId;
|
|
1194
1390
|
this.#namespace = channelName.trim();
|
|
1195
1391
|
this.channelName = this.#namespace;
|
|
1196
|
-
this.#
|
|
1197
|
-
this.#
|
|
1392
|
+
this.#maxPending = options.maxPending ?? 1e4;
|
|
1393
|
+
this.#transport = options.transport ?? defaultTransport(this.channelName);
|
|
1394
|
+
this.#transport.onMessage((message) => this.#dispatch(message));
|
|
1395
|
+
this.#transport.onPeerDown?.((shardIds) => this.#peerDown(shardIds));
|
|
1198
1396
|
}
|
|
1199
1397
|
on(type, handler) {
|
|
1200
1398
|
if (!type.trim() || typeof handler !== "function")
|
|
@@ -1243,7 +1441,7 @@ class ShardBus {
|
|
|
1243
1441
|
async request(target, type, data, timeoutMs = 5000) {
|
|
1244
1442
|
if (!Number.isInteger(target) || target < 0)
|
|
1245
1443
|
throw new RangeError("Shard target must be a non-negative integer.");
|
|
1246
|
-
const [reply] = await this.#collect(target, type, data, timeoutMs, 1);
|
|
1444
|
+
const [reply] = await this.#collect(target, type, data, timeoutMs, 1, true);
|
|
1247
1445
|
if (!reply)
|
|
1248
1446
|
throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
|
|
1249
1447
|
if (reply.error !== undefined)
|
|
@@ -1253,9 +1451,11 @@ class ShardBus {
|
|
|
1253
1451
|
broadcastRequest(type, data, options = {}) {
|
|
1254
1452
|
return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
|
|
1255
1453
|
}
|
|
1256
|
-
#collect(target, type, data, timeoutMs, expected) {
|
|
1454
|
+
#collect(target, type, data, timeoutMs, expected, rejectOnPeerDown = false) {
|
|
1257
1455
|
const replies = [];
|
|
1258
|
-
|
|
1456
|
+
if (this.#pending.size >= this.#maxPending)
|
|
1457
|
+
return Promise.reject(new Error(`Too many shard requests waiting for a reply (${this.#maxPending}).`));
|
|
1458
|
+
return new Promise((resolve, reject) => {
|
|
1259
1459
|
let requestId = "";
|
|
1260
1460
|
const finish = () => {
|
|
1261
1461
|
clearTimeout(timer);
|
|
@@ -1264,15 +1464,25 @@ class ShardBus {
|
|
|
1264
1464
|
};
|
|
1265
1465
|
const timer = setTimeout(finish, timeoutMs);
|
|
1266
1466
|
requestId = this.#publish(target, type, data, true);
|
|
1267
|
-
this.#pending.set(requestId,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1467
|
+
this.#pending.set(requestId, {
|
|
1468
|
+
target,
|
|
1469
|
+
reply: (reply) => {
|
|
1470
|
+
replies.push(reply);
|
|
1471
|
+
if (replies.length >= expected)
|
|
1472
|
+
finish();
|
|
1473
|
+
},
|
|
1474
|
+
fail: (error) => {
|
|
1475
|
+
if (rejectOnPeerDown) {
|
|
1476
|
+
clearTimeout(timer);
|
|
1477
|
+
this.#pending.delete(requestId);
|
|
1478
|
+
reject(error);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1271
1481
|
});
|
|
1272
1482
|
});
|
|
1273
1483
|
}
|
|
1274
1484
|
close() {
|
|
1275
|
-
this.#
|
|
1485
|
+
this.#transport.close();
|
|
1276
1486
|
this.#handlers.clear();
|
|
1277
1487
|
this.#errorHandlers.clear();
|
|
1278
1488
|
}
|
|
@@ -1280,7 +1490,7 @@ class ShardBus {
|
|
|
1280
1490
|
if (!type.trim())
|
|
1281
1491
|
throw new TypeError("Shard message type is required.");
|
|
1282
1492
|
const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
|
|
1283
|
-
this.#
|
|
1493
|
+
this.#transport.post({
|
|
1284
1494
|
source: this.#shardId,
|
|
1285
1495
|
target,
|
|
1286
1496
|
type,
|
|
@@ -1297,7 +1507,7 @@ class ShardBus {
|
|
|
1297
1507
|
return;
|
|
1298
1508
|
if (message.type === REPLY_TYPE) {
|
|
1299
1509
|
const reply = message.data;
|
|
1300
|
-
this.#pending.get(reply?.requestId)?.({
|
|
1510
|
+
this.#pending.get(reply?.requestId)?.reply({
|
|
1301
1511
|
shardId: message.source,
|
|
1302
1512
|
...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
|
|
1303
1513
|
});
|
|
@@ -1313,7 +1523,19 @@ class ShardBus {
|
|
|
1313
1523
|
}
|
|
1314
1524
|
}
|
|
1315
1525
|
}
|
|
1526
|
+
#peerDown(shardIds) {
|
|
1527
|
+
const down = new Set(shardIds);
|
|
1528
|
+
for (const [id, pending] of this.#pending)
|
|
1529
|
+
if (pending.target !== null && down.has(pending.target)) {
|
|
1530
|
+
pending.fail(new Error(`Shard ${pending.target} went away before it replied.`));
|
|
1531
|
+
this.#pending.delete(id);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1316
1534
|
#reportError(error, message) {
|
|
1535
|
+
if (this.#errorHandlers.size === 0) {
|
|
1536
|
+
process.emitWarning(error instanceof Error ? error : new Error(String(error)), { code: "LUNIBEE_SHARD_BUS_ERROR" });
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1317
1539
|
for (const handler of this.#errorHandlers) {
|
|
1318
1540
|
try {
|
|
1319
1541
|
handler(error, message);
|
|
@@ -1321,6 +1543,50 @@ class ShardBus {
|
|
|
1321
1543
|
}
|
|
1322
1544
|
}
|
|
1323
1545
|
}
|
|
1546
|
+
// packages/sharding/src/supervisor.ts
|
|
1547
|
+
class ShardSupervisor {
|
|
1548
|
+
#options;
|
|
1549
|
+
#restarts = new Map;
|
|
1550
|
+
#random;
|
|
1551
|
+
constructor(options = {}, random = Math.random) {
|
|
1552
|
+
this.#options = {
|
|
1553
|
+
restartDelay: options.restartDelay ?? 5000,
|
|
1554
|
+
backoff: options.backoff ?? 1,
|
|
1555
|
+
maxDelay: options.maxDelay ?? 300000,
|
|
1556
|
+
jitter: options.jitter ?? 0,
|
|
1557
|
+
maxRestarts: options.maxRestarts ?? Number.POSITIVE_INFINITY,
|
|
1558
|
+
window: options.window ?? 60000
|
|
1559
|
+
};
|
|
1560
|
+
const { restartDelay, backoff, jitter, maxDelay, window, maxRestarts } = this.#options;
|
|
1561
|
+
if (!(restartDelay >= 0) || !(maxDelay >= 0) || !(window > 0))
|
|
1562
|
+
throw new RangeError("restartDelay and maxDelay cannot be negative, and window must be positive.");
|
|
1563
|
+
if (!(backoff >= 1))
|
|
1564
|
+
throw new RangeError("backoff must be at least 1.");
|
|
1565
|
+
if (!(jitter >= 0 && jitter <= 1))
|
|
1566
|
+
throw new RangeError("jitter must be between 0 and 1.");
|
|
1567
|
+
if (!(maxRestarts >= 0))
|
|
1568
|
+
throw new RangeError("maxRestarts cannot be negative.");
|
|
1569
|
+
this.#random = random;
|
|
1570
|
+
}
|
|
1571
|
+
next(id, now = Date.now()) {
|
|
1572
|
+
const { window, maxRestarts, restartDelay, backoff, maxDelay, jitter } = this.#options;
|
|
1573
|
+
const recent = (this.#restarts.get(id) ?? []).filter((at) => now - at < window);
|
|
1574
|
+
if (recent.length >= maxRestarts) {
|
|
1575
|
+
this.#restarts.delete(id);
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
recent.push(now);
|
|
1579
|
+
this.#restarts.set(id, recent);
|
|
1580
|
+
const base = Math.min(maxDelay, restartDelay * backoff ** (recent.length - 1));
|
|
1581
|
+
return Math.round(base * (1 + jitter * this.#random()));
|
|
1582
|
+
}
|
|
1583
|
+
reset(id) {
|
|
1584
|
+
if (id === undefined)
|
|
1585
|
+
this.#restarts.clear();
|
|
1586
|
+
else
|
|
1587
|
+
this.#restarts.delete(id);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1324
1590
|
// packages/sharding/src/cluster.ts
|
|
1325
1591
|
import { fork } from "child_process";
|
|
1326
1592
|
import { cpus } from "os";
|
|
@@ -1335,6 +1601,7 @@ class ClusterManager {
|
|
|
1335
1601
|
#spawned = false;
|
|
1336
1602
|
#stopping = false;
|
|
1337
1603
|
#restartTimers = new Set;
|
|
1604
|
+
#supervisor;
|
|
1338
1605
|
constructor(options) {
|
|
1339
1606
|
if (!options.token?.trim())
|
|
1340
1607
|
throw new TypeError("A Discord bot token is required.");
|
|
@@ -1345,22 +1612,13 @@ class ClusterManager {
|
|
|
1345
1612
|
}
|
|
1346
1613
|
this.#auto = options.shardCount === "auto";
|
|
1347
1614
|
this.#options = { ...options };
|
|
1615
|
+
this.#supervisor = new ShardSupervisor({
|
|
1616
|
+
restartDelay: options.restartDelay,
|
|
1617
|
+
...options.supervisor
|
|
1618
|
+
});
|
|
1348
1619
|
}
|
|
1349
1620
|
async fetchRecommendedShardCount() {
|
|
1350
|
-
|
|
1351
|
-
headers: {
|
|
1352
|
-
Authorization: `Bot ${this.#options.token}`,
|
|
1353
|
-
"User-Agent": `Lunibee/${package_default.version}`
|
|
1354
|
-
}
|
|
1355
|
-
});
|
|
1356
|
-
if (!response.ok) {
|
|
1357
|
-
throw new Error(`Gateway discovery failed with status ${response.status}`);
|
|
1358
|
-
}
|
|
1359
|
-
const data = await response.json();
|
|
1360
|
-
if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1) {
|
|
1361
|
-
throw new Error("Gateway discovery returned an invalid shard count.");
|
|
1362
|
-
}
|
|
1363
|
-
return data.shards;
|
|
1621
|
+
return (await fetchGatewayBot(this.#options.token)).shards;
|
|
1364
1622
|
}
|
|
1365
1623
|
async spawn() {
|
|
1366
1624
|
if (this.#spawned)
|
|
@@ -1394,31 +1652,62 @@ class ClusterManager {
|
|
|
1394
1652
|
...process.env,
|
|
1395
1653
|
SHARD_LIST: shards.join(","),
|
|
1396
1654
|
SHARD_COUNT: shardCount.toString(),
|
|
1397
|
-
CLUSTER_ID: id.toString()
|
|
1655
|
+
CLUSTER_ID: id.toString(),
|
|
1656
|
+
LUNIBEE_SHARD_BUS: "ipc"
|
|
1398
1657
|
}
|
|
1399
1658
|
});
|
|
1400
1659
|
const info = { id, process: child, shards };
|
|
1401
1660
|
this.clusters.set(id, info);
|
|
1661
|
+
child.on("message", (frame) => this.#relay(id, frame));
|
|
1402
1662
|
child.once("exit", (code, signal) => {
|
|
1403
1663
|
if (this.clusters.get(id)?.process !== child)
|
|
1404
1664
|
return;
|
|
1405
1665
|
try {
|
|
1406
1666
|
this.#options.onClusterExit?.(info, code, signal);
|
|
1407
1667
|
} catch {}
|
|
1668
|
+
if (!this.#stopping)
|
|
1669
|
+
this.#announceDown(id, shards);
|
|
1408
1670
|
if (this.#stopping || this.#options.restartOnExit === false)
|
|
1409
1671
|
return;
|
|
1410
1672
|
this.clusters.delete(id);
|
|
1673
|
+
const delay = this.#supervisor.next(id);
|
|
1674
|
+
if (delay === null) {
|
|
1675
|
+
try {
|
|
1676
|
+
this.#options.onGiveUp?.(info);
|
|
1677
|
+
} catch {}
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1411
1680
|
const timer = setTimeout(() => {
|
|
1412
1681
|
this.#restartTimers.delete(timer);
|
|
1413
1682
|
if (this.#stopping || !this.#spawned)
|
|
1414
1683
|
return;
|
|
1415
1684
|
this.#launch(id, shards, shardCount);
|
|
1416
|
-
},
|
|
1685
|
+
}, delay);
|
|
1417
1686
|
timer.unref?.();
|
|
1418
1687
|
this.#restartTimers.add(timer);
|
|
1419
1688
|
});
|
|
1420
1689
|
return info;
|
|
1421
1690
|
}
|
|
1691
|
+
#relay(from, frame) {
|
|
1692
|
+
if (!frame || typeof frame !== "object" || !(BUS_FRAME in frame))
|
|
1693
|
+
return;
|
|
1694
|
+
for (const [id, cluster] of this.clusters)
|
|
1695
|
+
if (id !== from)
|
|
1696
|
+
this.#sendFrame(cluster, frame);
|
|
1697
|
+
}
|
|
1698
|
+
#announceDown(exited, shards) {
|
|
1699
|
+
for (const [id, cluster] of this.clusters)
|
|
1700
|
+
if (id !== exited)
|
|
1701
|
+
this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
|
|
1702
|
+
}
|
|
1703
|
+
#sendFrame(cluster, frame) {
|
|
1704
|
+
const child = cluster.process;
|
|
1705
|
+
if (typeof child.send !== "function" || !child.connected)
|
|
1706
|
+
return;
|
|
1707
|
+
try {
|
|
1708
|
+
child.send(frame);
|
|
1709
|
+
} catch {}
|
|
1710
|
+
}
|
|
1422
1711
|
async checkAutoScale() {
|
|
1423
1712
|
if (!this.#auto)
|
|
1424
1713
|
return;
|
|
@@ -1436,6 +1725,7 @@ class ClusterManager {
|
|
|
1436
1725
|
this.#options.shardCount = newShardCount;
|
|
1437
1726
|
await this.shutdownAll();
|
|
1438
1727
|
this.#spawned = false;
|
|
1728
|
+
this.#supervisor.reset();
|
|
1439
1729
|
await this.spawn();
|
|
1440
1730
|
}
|
|
1441
1731
|
async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
|
|
@@ -1541,12 +1831,14 @@ class ShardManager {
|
|
|
1541
1831
|
#destroyed = false;
|
|
1542
1832
|
#auto;
|
|
1543
1833
|
#autoScaleTimer;
|
|
1834
|
+
#total;
|
|
1835
|
+
#listeners = new Map;
|
|
1544
1836
|
#gatewayInfo;
|
|
1545
1837
|
constructor(options) {
|
|
1546
1838
|
if (!options.token?.trim())
|
|
1547
1839
|
throw new TypeError("A Discord bot token is required.");
|
|
1548
|
-
if (!
|
|
1549
|
-
throw new TypeError("Gateway intents must be
|
|
1840
|
+
if (!Array.isArray(options.intents))
|
|
1841
|
+
throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds].");
|
|
1550
1842
|
const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
|
|
1551
1843
|
if (!Number.isInteger(count) || count < 1)
|
|
1552
1844
|
throw new RangeError("Shard count must be a positive integer.");
|
|
@@ -1558,6 +1850,7 @@ class ShardManager {
|
|
|
1558
1850
|
throw new RangeError("maxConcurrency must be a positive integer.");
|
|
1559
1851
|
if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
|
|
1560
1852
|
throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
|
|
1853
|
+
this.#total = count;
|
|
1561
1854
|
this.#auto = options.shardCount === "auto";
|
|
1562
1855
|
this.#options = {
|
|
1563
1856
|
spawnDelay: ShardManager.IDENTIFY_INTERVAL,
|
|
@@ -1570,26 +1863,7 @@ class ShardManager {
|
|
|
1570
1863
|
return (await this.fetchGatewayInfo()).shards;
|
|
1571
1864
|
}
|
|
1572
1865
|
async fetchGatewayInfo() {
|
|
1573
|
-
const
|
|
1574
|
-
headers: {
|
|
1575
|
-
Authorization: `Bot ${this.#options.token}`,
|
|
1576
|
-
"User-Agent": `Lunibee/${package_default.version}`
|
|
1577
|
-
}
|
|
1578
|
-
});
|
|
1579
|
-
if (!response.ok)
|
|
1580
|
-
throw new Error(`Gateway discovery failed with status ${response.status}`);
|
|
1581
|
-
const data = await response.json();
|
|
1582
|
-
if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
|
|
1583
|
-
throw new Error("Gateway discovery returned an invalid shard count.");
|
|
1584
|
-
const limit = data.session_start_limit;
|
|
1585
|
-
const info = { shards: data.shards };
|
|
1586
|
-
if (limit && typeof limit.total === "number" && typeof limit.remaining === "number" && typeof limit.reset_after === "number" && typeof limit.max_concurrency === "number")
|
|
1587
|
-
info.sessionStartLimit = {
|
|
1588
|
-
total: limit.total,
|
|
1589
|
-
remaining: limit.remaining,
|
|
1590
|
-
resetAfter: limit.reset_after,
|
|
1591
|
-
maxConcurrency: Math.max(1, limit.max_concurrency)
|
|
1592
|
-
};
|
|
1866
|
+
const info = await fetchGatewayBot(this.#options.token);
|
|
1593
1867
|
this.#gatewayInfo = info;
|
|
1594
1868
|
return info;
|
|
1595
1869
|
}
|
|
@@ -1604,8 +1878,18 @@ class ShardManager {
|
|
|
1604
1878
|
if (limit && limit.remaining < ids.length)
|
|
1605
1879
|
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.`);
|
|
1606
1880
|
const concurrency = Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
|
|
1607
|
-
|
|
1608
|
-
|
|
1881
|
+
const buckets = new Map;
|
|
1882
|
+
for (const id of ids) {
|
|
1883
|
+
const key = id % concurrency;
|
|
1884
|
+
const bucket = buckets.get(key);
|
|
1885
|
+
if (bucket)
|
|
1886
|
+
bucket.push(id);
|
|
1887
|
+
else
|
|
1888
|
+
buckets.set(key, [id]);
|
|
1889
|
+
}
|
|
1890
|
+
const rounds = Math.max(0, ...[...buckets.values()].map((bucket) => bucket.length));
|
|
1891
|
+
for (let index = 0;index < rounds; index++) {
|
|
1892
|
+
const round = [...buckets.values()].map((bucket) => bucket[index]).filter((id) => id !== undefined);
|
|
1609
1893
|
const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
|
|
1610
1894
|
const failed = results.findIndex((result) => result.status === "rejected");
|
|
1611
1895
|
if (failed !== -1) {
|
|
@@ -1614,7 +1898,7 @@ class ShardManager {
|
|
|
1614
1898
|
cause: results[failed].reason
|
|
1615
1899
|
});
|
|
1616
1900
|
}
|
|
1617
|
-
if (
|
|
1901
|
+
if (index + 1 < rounds && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
|
|
1618
1902
|
await Promise.all(round.map((id) => handshakeSent(this.shards.get(id), this.#options.handshakeTimeout ?? 15000)));
|
|
1619
1903
|
await sleep2(this.#options.spawnDelay);
|
|
1620
1904
|
}
|
|
@@ -1653,9 +1937,29 @@ class ShardManager {
|
|
|
1653
1937
|
this.#autoScaleTimer = undefined;
|
|
1654
1938
|
}
|
|
1655
1939
|
}
|
|
1940
|
+
on(event, listener) {
|
|
1941
|
+
let set = this.#listeners.get(event);
|
|
1942
|
+
if (!set)
|
|
1943
|
+
this.#listeners.set(event, set = new Set);
|
|
1944
|
+
set.add(listener);
|
|
1945
|
+
return this;
|
|
1946
|
+
}
|
|
1947
|
+
off(event, listener) {
|
|
1948
|
+
this.#listeners.get(event)?.delete(listener);
|
|
1949
|
+
return this;
|
|
1950
|
+
}
|
|
1951
|
+
#emit(event, ...args) {
|
|
1952
|
+
for (const listener of this.#listeners.get(event) ?? [])
|
|
1953
|
+
try {
|
|
1954
|
+
listener(...args);
|
|
1955
|
+
} catch {}
|
|
1956
|
+
}
|
|
1656
1957
|
getShardIdForGuild(guildId) {
|
|
1657
1958
|
const id = BigInt(guildId);
|
|
1658
|
-
return Number((id >> 22n) % BigInt(this
|
|
1959
|
+
return Number((id >> 22n) % BigInt(this.#total));
|
|
1960
|
+
}
|
|
1961
|
+
sendVoiceState(guildId, channelId, options = {}) {
|
|
1962
|
+
return this.shards.get(this.getShardIdForGuild(guildId))?.sendVoiceState(guildId, channelId, options) ?? false;
|
|
1659
1963
|
}
|
|
1660
1964
|
get(id) {
|
|
1661
1965
|
return this.shards.get(id);
|
|
@@ -1681,25 +1985,36 @@ class ShardManager {
|
|
|
1681
1985
|
#initialize(count) {
|
|
1682
1986
|
if (this.#resolved)
|
|
1683
1987
|
return;
|
|
1988
|
+
this.#total = count;
|
|
1684
1989
|
for (let id = 0;id < count; id++)
|
|
1685
1990
|
this.shards.set(id, this.#createShard(id, count));
|
|
1686
1991
|
this.#resolved = true;
|
|
1687
1992
|
}
|
|
1688
1993
|
#createShard(id, count) {
|
|
1689
|
-
|
|
1994
|
+
const gateway = new Gateway({
|
|
1690
1995
|
token: this.#options.token,
|
|
1691
1996
|
intents: this.#options.intents,
|
|
1692
1997
|
shardId: id,
|
|
1693
1998
|
shardCount: count,
|
|
1694
1999
|
reconnect: this.#options.reconnect ?? true
|
|
1695
2000
|
});
|
|
2001
|
+
gateway.on("ready", () => this.#emit("shardReady", id));
|
|
2002
|
+
gateway.on("resumed", () => this.#emit("shardResume", id));
|
|
2003
|
+
gateway.on("close", (close) => this.#emit("shardDisconnect", id, close));
|
|
2004
|
+
gateway.on("error", (error) => this.#emit("shardError", id, error));
|
|
2005
|
+
gateway.on("unstable", (info) => this.#emit("shardUnstable", id, info));
|
|
2006
|
+
return gateway;
|
|
1696
2007
|
}
|
|
1697
2008
|
}
|
|
1698
2009
|
export {
|
|
2010
|
+
BroadcastChannelTransport,
|
|
1699
2011
|
ClusterManager,
|
|
2012
|
+
IpcTransport,
|
|
1700
2013
|
ShardBus,
|
|
1701
2014
|
ShardManager,
|
|
1702
|
-
|
|
2015
|
+
ShardSupervisor,
|
|
2016
|
+
ShardManager as ShardingManager,
|
|
2017
|
+
fetchGatewayBot
|
|
1703
2018
|
};
|
|
1704
2019
|
|
|
1705
|
-
//# debugId=
|
|
2020
|
+
//# debugId=8B521E4F34BB811E64756E2164756E21
|