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.
Files changed (86) hide show
  1. package/README.md +3 -3
  2. package/dist/builders/attachment.d.ts +8 -4
  3. package/dist/builders/commands.d.ts +122 -149
  4. package/dist/builders/components.d.ts +10 -150
  5. package/dist/builders/embed.d.ts +66 -37
  6. package/dist/builders/index.d.ts +0 -4
  7. package/dist/builders/index.js +112 -221
  8. package/dist/builders/index.js.map +10 -9
  9. package/dist/collection/cache.d.ts +15 -6
  10. package/dist/collection/index.d.ts +92 -17
  11. package/dist/collection/index.js +471 -279
  12. package/dist/collection/index.js.map +4 -4
  13. package/dist/core/api.d.ts +130 -0
  14. package/dist/core/channel-create.d.ts +72 -0
  15. package/dist/core/commands.d.ts +232 -0
  16. package/dist/core/events.d.ts +24 -7
  17. package/dist/core/handles.d.ts +157 -0
  18. package/dist/core/index.d.ts +68 -4
  19. package/dist/core/index.js +2639 -493
  20. package/dist/core/index.js.map +47 -37
  21. package/dist/core/permissions.d.ts +7 -7
  22. package/dist/formatters/index.js +9 -5
  23. package/dist/formatters/index.js.map +3 -3
  24. package/dist/handlers/index.js +5 -10
  25. package/dist/handlers/index.js.map +3 -3
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +9527 -7809
  28. package/dist/index.js.map +63 -53
  29. package/dist/managers/advanced.d.ts +29 -1
  30. package/dist/managers/base.d.ts +22 -14
  31. package/dist/managers/emoji.d.ts +0 -2
  32. package/dist/managers/guild-resources.d.ts +13 -5
  33. package/dist/managers/guild.d.ts +40 -4
  34. package/dist/managers/index.d.ts +21 -13
  35. package/dist/managers/index.js +969 -386
  36. package/dist/managers/index.js.map +30 -25
  37. package/dist/managers/member.d.ts +9 -2
  38. package/dist/managers/message.d.ts +10 -15
  39. package/dist/managers/role.d.ts +18 -10
  40. package/dist/managers/thread.d.ts +68 -0
  41. package/dist/managers/user.d.ts +18 -0
  42. package/dist/rest/errors.d.ts +14 -7
  43. package/dist/rest/index.d.ts +24 -12
  44. package/dist/rest/index.js +70 -20
  45. package/dist/rest/index.js.map +9 -9
  46. package/dist/rest/redis.d.ts +1 -1
  47. package/dist/rest/route.d.ts +8 -4
  48. package/dist/rest/routes.d.ts +122 -48
  49. package/dist/rest/transport.d.ts +10 -5
  50. package/dist/rest/webhook.d.ts +9 -5
  51. package/dist/sharding/bus.d.ts +47 -19
  52. package/dist/sharding/cluster.d.ts +11 -0
  53. package/dist/sharding/gateway-bot.d.ts +18 -0
  54. package/dist/sharding/index.d.ts +64 -25
  55. package/dist/sharding/index.js +439 -124
  56. package/dist/sharding/index.js.map +19 -14
  57. package/dist/sharding/supervisor.d.ts +32 -0
  58. package/dist/sharding/transport.d.ts +45 -0
  59. package/dist/structures/audit-log.d.ts +99 -2
  60. package/dist/structures/base.d.ts +40 -23
  61. package/dist/structures/channels.d.ts +45 -14
  62. package/dist/structures/index.d.ts +8 -12
  63. package/dist/structures/index.js +280 -132
  64. package/dist/structures/index.js.map +21 -15
  65. package/dist/structures/interactions.d.ts +91 -63
  66. package/dist/structures/resources.d.ts +99 -52
  67. package/dist/structures/sticker.d.ts +33 -0
  68. package/dist/types/channel-kind.d.ts +26 -0
  69. package/dist/types/gateway.d.ts +24 -12
  70. package/dist/types/index.d.ts +115 -113
  71. package/dist/types/index.js +53 -48
  72. package/dist/types/index.js.map +6 -5
  73. package/dist/voice/index.d.ts +12 -4
  74. package/dist/voice/index.js +14 -18
  75. package/dist/voice/index.js.map +3 -3
  76. package/dist/ws/decoder.d.ts +6 -2
  77. package/dist/ws/dispatcher.d.ts +19 -0
  78. package/dist/ws/index.d.ts +61 -15
  79. package/dist/ws/index.js +194 -77
  80. package/dist/ws/index.js.map +13 -11
  81. package/dist/ws/protocol.d.ts +10 -2
  82. package/dist/ws/reconnect.d.ts +10 -0
  83. package/dist/ws/send-budget.d.ts +6 -2
  84. package/dist/ws/session.d.ts +14 -4
  85. package/dist/ws/state.d.ts +22 -11
  86. package/package.json +20 -16
@@ -2,7 +2,7 @@
2
2
  // packages/sharding/package.json
3
3
  var package_default = {
4
4
  name: "@lunibee/sharding",
5
- version: "0.2.3",
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
- return this.resumeInfo()?.resumeURL ?? fallback;
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 delay = Math.min(this.#options.maxDelay, this.#options.baseDelay * 2 ** this.#attempt++);
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 { scheduled: true, delayMs, attempt };
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) => this.#chunks.push(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 ?? "Android";
677
- const browser = props.browser ?? "Discord Android";
678
- const device = props.device ?? "Discord Android";
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 (typeof intents === "number")
775
- return intents;
776
- if (intents && typeof intents === "object" && !Array.isArray(intents))
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
- // packages/ws/src/state.ts
798
- class GatewayError extends Error {
799
- code;
800
- constructor(message, code, options) {
801
- super(message, options);
802
- this.name = "GatewayError";
803
- this.code = code;
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 SendBudget {
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
- #listeners = new Map;
842
- #sendBudget = new SendBudget;
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: "Android",
861
- browser: "Discord Android",
862
- device: "Discord Android"
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
- let listeners = this.#listeners.get(event);
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.#listeners.get(event)?.delete(listener);
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 || result.reason !== "exhausted")
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
- for (const listener of this.#listeners.get(event) ?? []) {
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
- #channel;
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.#channel = new BroadcastChannel(this.channelName);
1197
- this.#channel.addEventListener("message", (event) => this.#dispatch(event.data));
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
- return new Promise((resolve) => {
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, (reply) => {
1268
- replies.push(reply);
1269
- if (replies.length >= expected)
1270
- finish();
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.#channel.close();
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.#channel.postMessage({
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
- const response = await fetch("https://discord.com/api/v10/gateway/bot", {
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
- }, this.#options.restartDelay ?? 5000);
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 (!Number.isInteger(options.intents) || options.intents < 0)
1549
- throw new TypeError("Gateway intents must be a non-negative integer.");
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 response = await fetch("https://discord.com/api/v10/gateway/bot", {
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
- for (let start = 0;start < ids.length; start += concurrency) {
1608
- const round = ids.slice(start, start + concurrency);
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 (start + concurrency < ids.length && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
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.shardCount || 1));
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
- return new Gateway({
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
- ShardManager as ShardingManager
2015
+ ShardSupervisor,
2016
+ ShardManager as ShardingManager,
2017
+ fetchGatewayBot
1703
2018
  };
1704
2019
 
1705
- //# debugId=B02B71EDFB220DDE64756E2164756E21
2020
+ //# debugId=8B521E4F34BB811E64756E2164756E21