lunibee 0.3.0 → 0.3.1

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 (38) hide show
  1. package/dist/builders/embed.d.ts +1 -0
  2. package/dist/builders/index.js +6 -2
  3. package/dist/builders/index.js.map +4 -4
  4. package/dist/core/index.js +66 -15
  5. package/dist/core/index.js.map +8 -8
  6. package/dist/index.js +2345 -199
  7. package/dist/index.js.map +22 -14
  8. package/dist/managers/index.js +2 -2
  9. package/dist/managers/index.js.map +3 -3
  10. package/dist/rest/errors.d.ts +8 -0
  11. package/dist/rest/index.d.ts +2 -0
  12. package/dist/rest/index.js +65 -14
  13. package/dist/rest/index.js.map +5 -5
  14. package/dist/rest/transport.d.ts +9 -0
  15. package/dist/sharding/bus.d.ts +8 -0
  16. package/dist/sharding/cluster.d.ts +224 -14
  17. package/dist/sharding/context.d.ts +41 -0
  18. package/dist/sharding/gateway-bot.d.ts +20 -0
  19. package/dist/sharding/health.d.ts +94 -0
  20. package/dist/sharding/host.d.ts +70 -0
  21. package/dist/sharding/identify.d.ts +62 -0
  22. package/dist/sharding/index.d.ts +56 -12
  23. package/dist/sharding/index.js +2255 -164
  24. package/dist/sharding/index.js.map +18 -10
  25. package/dist/sharding/protocol.d.ts +47 -0
  26. package/dist/sharding/remote-protocol.d.ts +100 -0
  27. package/dist/sharding/remote.d.ts +118 -0
  28. package/dist/sharding/requests.d.ts +54 -0
  29. package/dist/sharding/worker.d.ts +45 -0
  30. package/dist/structures/index.js +2 -2
  31. package/dist/structures/index.js.map +3 -3
  32. package/dist/types/gateway.d.ts +3 -0
  33. package/dist/types/index.js.map +1 -1
  34. package/dist/ws/index.d.ts +1 -1
  35. package/dist/ws/index.js +2 -2
  36. package/dist/ws/index.js.map +5 -5
  37. package/dist/ws/send-budget.d.ts +1 -1
  38. package/package.json +18 -16
package/dist/index.js CHANGED
@@ -882,7 +882,11 @@ class CreateEmbed {
882
882
  return this.toJSON();
883
883
  }
884
884
  toJSON() {
885
- return structuredClone(this.#data);
885
+ const data = this.#data;
886
+ const total = (data.title?.length ?? 0) + (data.description?.length ?? 0) + (data.footer?.text.length ?? 0) + (data.author?.name.length ?? 0) + (data.fields ?? []).reduce((sum, field) => sum + field.name.length + field.value.length, 0);
887
+ if (total > 6000)
888
+ throw new RangeError(`An embed cannot contain more than 6000 characters in total (this one has ${total}).`);
889
+ return structuredClone(data);
886
890
  }
887
891
  }
888
892
  function validate(value, max, field) {
@@ -3994,7 +3998,7 @@ class ResourceManager extends Manager {
3994
3998
  // packages/rest/package.json
3995
3999
  var package_default = {
3996
4000
  name: "@lunibee/rest",
3997
- version: "0.3.0",
4001
+ version: "0.3.1",
3998
4002
  type: "module",
3999
4003
  module: "./src/index.ts",
4000
4004
  types: "./src/index.ts",
@@ -4077,6 +4081,24 @@ function kindFromStatus(status) {
4077
4081
  function redactPath(path) {
4078
4082
  return path.replace(/^(\/webhooks\/[^/?]+\/)[^/?]+/i, "$1:token").replace(/^(\/interactions\/[^/?]+\/)[^/?]+/i, "$1:token");
4079
4083
  }
4084
+ var TOKEN_IN_TEXT = /(\/(?:webhooks|interactions)\/[^/\s?"'`]+\/)[^/\s?"'`]+/gi;
4085
+ function redactText(text) {
4086
+ return text.replace(TOKEN_IN_TEXT, "$1:token");
4087
+ }
4088
+ function sanitizeCause(error) {
4089
+ if (!(error instanceof Error))
4090
+ return;
4091
+ const copy = new Error(redactText(error.message));
4092
+ copy.name = error.name;
4093
+ if (error.stack)
4094
+ copy.stack = redactText(error.stack);
4095
+ for (const key of ["code", "errno", "syscall"]) {
4096
+ const value = error[key];
4097
+ if (typeof value === "string" || typeof value === "number")
4098
+ copy[key] = value;
4099
+ }
4100
+ return copy;
4101
+ }
4080
4102
  var HINTS = {
4081
4103
  50001: "The bot cannot see this resource: check that it is in the guild and has View Channel there.",
4082
4104
  50013: "The bot lacks a permission for this action, or the target role or member is at or above the bot's highest role."
@@ -4112,14 +4134,19 @@ async function sleep2(delay, signal, path) {
4112
4134
  if (delay <= 0)
4113
4135
  return;
4114
4136
  await new Promise((resolve, reject) => {
4115
- const timer = setTimeout(resolve, delay);
4116
- const abort = () => {
4137
+ const finish = (error) => {
4117
4138
  clearTimeout(timer);
4118
- reject(abortError(path, signal?.reason));
4139
+ signal?.removeEventListener("abort", onAbort);
4140
+ if (error)
4141
+ reject(error);
4142
+ else
4143
+ resolve();
4119
4144
  };
4145
+ const onAbort = () => finish(abortError(path, signal?.reason));
4146
+ const timer = setTimeout(finish, delay);
4120
4147
  if (signal?.aborted)
4121
- return abort();
4122
- signal?.addEventListener("abort", abort, { once: true });
4148
+ return onAbort();
4149
+ signal?.addEventListener("abort", onAbort, { once: true });
4123
4150
  });
4124
4151
  }
4125
4152
  async function abortable(promise, signal, path) {
@@ -4127,10 +4154,17 @@ async function abortable(promise, signal, path) {
4127
4154
  return promise;
4128
4155
  if (signal.aborted)
4129
4156
  throw abortError(path, signal.reason);
4130
- return Promise.race([
4131
- promise,
4132
- new Promise((_, reject) => signal.addEventListener("abort", () => reject(abortError(path, signal.reason)), { once: true }))
4133
- ]);
4157
+ return new Promise((resolve, reject) => {
4158
+ const onAbort = () => reject(abortError(path, signal.reason));
4159
+ signal.addEventListener("abort", onAbort, { once: true });
4160
+ promise.then(() => {
4161
+ signal.removeEventListener("abort", onAbort);
4162
+ resolve();
4163
+ }, (error) => {
4164
+ signal.removeEventListener("abort", onAbort);
4165
+ reject(error);
4166
+ });
4167
+ });
4134
4168
  }
4135
4169
 
4136
4170
  // packages/rest/src/route.ts
@@ -4304,6 +4338,25 @@ class RateLimiter {
4304
4338
  }
4305
4339
 
4306
4340
  // packages/rest/src/transport.ts
4341
+ var LOOPBACK = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])$/i;
4342
+ function parseBaseURL(raw, allowInsecure = false) {
4343
+ let url;
4344
+ try {
4345
+ url = new URL(raw);
4346
+ } catch {
4347
+ throw new TypeError("baseURL must be an absolute http(s) URL.");
4348
+ }
4349
+ if (url.protocol !== "https:" && url.protocol !== "http:")
4350
+ throw new TypeError("baseURL must use https.");
4351
+ if (url.username || url.password)
4352
+ throw new TypeError("baseURL must not contain credentials.");
4353
+ if (url.search || url.hash)
4354
+ throw new TypeError("baseURL must not contain a query or fragment.");
4355
+ if (url.protocol === "http:" && !allowInsecure && !LOOPBACK.test(url.hostname))
4356
+ throw new TypeError("baseURL must use https: the bot token would travel in cleartext (allowInsecureBaseURL: true overrides this for a trusted network).");
4357
+ return raw.replace(/\/$/, "");
4358
+ }
4359
+
4307
4360
  class TransportError extends Error {
4308
4361
  timedOut;
4309
4362
  constructor(message, timedOut, cause) {
@@ -4318,7 +4371,7 @@ class HttpTransport {
4318
4371
  #timeout;
4319
4372
  #fetch;
4320
4373
  constructor(options = {}) {
4321
- this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
4374
+ this.#baseURL = parseBaseURL(options.baseURL ?? "https://discord.com/api/v10", options.allowInsecureBaseURL);
4322
4375
  this.#timeout = Math.max(1, options.timeout ?? 15000);
4323
4376
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
4324
4377
  }
@@ -4340,13 +4393,14 @@ class HttpTransport {
4340
4393
  method: request.method,
4341
4394
  headers: request.headers,
4342
4395
  body: request.body,
4343
- signal: controller.signal
4396
+ signal: controller.signal,
4397
+ redirect: "error"
4344
4398
  });
4345
4399
  } catch (error) {
4346
4400
  if (error instanceof RESTError)
4347
4401
  throw error;
4348
4402
  const timedOut = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
4349
- throw new TransportError(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, error);
4403
+ throw new TransportError(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, sanitizeCause(error));
4350
4404
  } finally {
4351
4405
  clearTimeout(timer);
4352
4406
  request.signal?.removeEventListener("abort", onAbort);
@@ -4785,6 +4839,7 @@ class REST {
4785
4839
  this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
4786
4840
  this.#transport = options.transport ?? new HttpTransport({
4787
4841
  baseURL: options.baseURL,
4842
+ allowInsecureBaseURL: options.allowInsecureBaseURL,
4788
4843
  timeout: options.timeout
4789
4844
  });
4790
4845
  const store = options.store ?? new MemoryRateLimitStore;
@@ -9034,7 +9089,7 @@ class Gateway {
9034
9089
  }
9035
9090
  connect(url = DEFAULT_GATEWAY_URL) {
9036
9091
  if (this.state === "CLOSED" /* Closed */)
9037
- throw new GatewayError("Gateway has been permanently closed.");
9092
+ return Promise.reject(new GatewayError("Gateway has been permanently closed."));
9038
9093
  const inFlight = this.#reconnect.attemptPromise;
9039
9094
  if (inFlight)
9040
9095
  return inFlight;
@@ -10242,7 +10297,7 @@ class HandlerRegistry {
10242
10297
  // packages/sharding/package.json
10243
10298
  var package_default2 = {
10244
10299
  name: "@lunibee/sharding",
10245
- version: "0.3.0",
10300
+ version: "0.3.1",
10246
10301
  type: "module",
10247
10302
  module: "./src/index.ts",
10248
10303
  types: "./src/index.ts",
@@ -10278,6 +10333,269 @@ async function fetchGatewayBot(token) {
10278
10333
  };
10279
10334
  return info;
10280
10335
  }
10336
+ var MAX_GUILDS_PER_SHARD = 2500;
10337
+ function assertGuildsPerShard(value) {
10338
+ if (!Number.isInteger(value) || value < 1 || value > MAX_GUILDS_PER_SHARD)
10339
+ throw new RangeError(`guildsPerShard must be an integer from 1 to ${MAX_GUILDS_PER_SHARD}.`);
10340
+ }
10341
+ function shardsForGuilds(guilds, guildsPerShard) {
10342
+ assertGuildsPerShard(guildsPerShard);
10343
+ if (!Number.isInteger(guilds) || guilds < 0)
10344
+ throw new RangeError("guildCount must be a non-negative integer.");
10345
+ return Math.max(1, Math.ceil(guilds / guildsPerShard));
10346
+ }
10347
+ async function fetchGuildCount(token) {
10348
+ const response = await fetch("https://discord.com/api/v10/applications/@me", {
10349
+ headers: {
10350
+ Authorization: `Bot ${token}`,
10351
+ "User-Agent": `Lunibee/${package_default2.version}`
10352
+ }
10353
+ });
10354
+ if (!response.ok)
10355
+ throw new Error(`Guild count discovery failed with status ${response.status}`);
10356
+ const data = await response.json();
10357
+ const count = data.approximate_guild_count;
10358
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0)
10359
+ throw new Error("Discord did not report a guild count.");
10360
+ return count;
10361
+ }
10362
+ async function resolveShardCount(token, info, options) {
10363
+ if (options.guildsPerShard === undefined)
10364
+ return info.shards;
10365
+ return shardsForGuilds(options.guildCount ?? await fetchGuildCount(token), options.guildsPerShard);
10366
+ }
10367
+
10368
+ // packages/sharding/src/context.ts
10369
+ var WORKER_MARKER = "LUNIBEE_CLUSTER_WORKER";
10370
+ function isClusterWorker(env = process.env) {
10371
+ return env[WORKER_MARKER] === "1";
10372
+ }
10373
+ function validateShardIds(shardIds, total) {
10374
+ if (!Number.isInteger(total) || total < 1)
10375
+ throw new RangeError("Shard count must be a positive integer.");
10376
+ const seen = new Set;
10377
+ for (const id of shardIds) {
10378
+ if (!Number.isInteger(id) || id < 0 || id >= total)
10379
+ throw new RangeError(`Shard ID ${String(id)} is outside 0-${total - 1}.`);
10380
+ if (seen.has(id))
10381
+ throw new RangeError(`Shard ID ${id} is listed twice.`);
10382
+ seen.add(id);
10383
+ }
10384
+ }
10385
+ function allocateShards(shardIds, clusterCount) {
10386
+ if (!Number.isInteger(clusterCount) || clusterCount < 1)
10387
+ throw new RangeError("clusterCount must be a positive integer.");
10388
+ const chunks = Array.from({ length: clusterCount }, () => []);
10389
+ shardIds.forEach((id, index) => chunks[index % clusterCount].push(id));
10390
+ return chunks;
10391
+ }
10392
+ function assertCompleteAllocation(chunks, expected) {
10393
+ const owners = new Map;
10394
+ const duplicate = [];
10395
+ chunks.forEach((chunk, cluster) => {
10396
+ for (const id of chunk) {
10397
+ if (owners.has(id))
10398
+ duplicate.push(id);
10399
+ owners.set(id, cluster);
10400
+ }
10401
+ });
10402
+ const want = new Set(expected);
10403
+ const missing = expected.filter((id) => !owners.has(id));
10404
+ const extra = [...owners.keys()].filter((id) => !want.has(id));
10405
+ if (duplicate.length || missing.length || extra.length)
10406
+ throw new Error(`Shard allocation is not exact: duplicate [${duplicate}], missing [${missing}], unexpected [${extra}].`);
10407
+ }
10408
+ function parseInteger(name, value) {
10409
+ if (value === undefined || !/^\d+$/.test(value))
10410
+ throw new RangeError(`${name} must be a non-negative integer.`);
10411
+ return Number(value);
10412
+ }
10413
+ function readClusterContext(env = process.env) {
10414
+ if (!isClusterWorker(env))
10415
+ return null;
10416
+ const shardCount = parseInteger("SHARD_COUNT", env.SHARD_COUNT);
10417
+ const shardIds = (env.SHARD_LIST ?? "").split(",").filter((part) => part !== "").map((part) => parseInteger("SHARD_LIST", part));
10418
+ validateShardIds(shardIds, shardCount);
10419
+ return {
10420
+ clusterId: parseInteger("CLUSTER_ID", env.CLUSTER_ID),
10421
+ clusterCount: parseInteger("CLUSTER_COUNT", env.CLUSTER_COUNT),
10422
+ shardCount,
10423
+ shardIds,
10424
+ coordinatedIdentify: env.LUNIBEE_IDENTIFY === "coordinated"
10425
+ };
10426
+ }
10427
+
10428
+ // packages/sharding/src/protocol.ts
10429
+ var CLUSTER_FRAME = "__lunibeeCluster";
10430
+ var PARENT_ID = -1;
10431
+ var REQUEST_HEALTH = "__lunibee:health";
10432
+ var REQUEST_ASSIGN = "__lunibee:assign";
10433
+ var isRecord = (value) => typeof value === "object" && value !== null;
10434
+ var isShardId = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
10435
+ function parseChildFrame(value) {
10436
+ if (!isRecord(value))
10437
+ return null;
10438
+ switch (value.kind) {
10439
+ case "ready":
10440
+ return Array.isArray(value.shardIds) && value.shardIds.every(isShardId) ? { kind: "ready", shardIds: value.shardIds } : null;
10441
+ case "identify":
10442
+ return typeof value.id === "number" && isShardId(value.shardId) ? { kind: "identify", id: value.id, shardId: value.shardId } : null;
10443
+ case "identify-done":
10444
+ return isShardId(value.shardId) ? { kind: "identify-done", shardId: value.shardId } : null;
10445
+ case "reply":
10446
+ return typeof value.id === "string" && (value.error === undefined || typeof value.error === "string") ? {
10447
+ kind: "reply",
10448
+ id: value.id,
10449
+ ...value.error !== undefined ? { error: value.error } : { result: value.result }
10450
+ } : null;
10451
+ case "health":
10452
+ return isRecord(value.report) && Array.isArray(value.report.shards) && typeof value.report.reportedAt === "number" ? { kind: "health", report: value.report } : null;
10453
+ default:
10454
+ return null;
10455
+ }
10456
+ }
10457
+ function parseParentFrame(value) {
10458
+ if (!isRecord(value))
10459
+ return null;
10460
+ switch (value.kind) {
10461
+ case "identify-grant":
10462
+ return typeof value.id === "number" ? { kind: "identify-grant", id: value.id } : null;
10463
+ case "identify-deny":
10464
+ return typeof value.id === "number" && typeof value.error === "string" ? { kind: "identify-deny", id: value.id, error: value.error } : null;
10465
+ case "request":
10466
+ return typeof value.id === "string" && typeof value.type === "string" ? {
10467
+ kind: "request",
10468
+ id: value.id,
10469
+ type: value.type,
10470
+ data: value.data
10471
+ } : null;
10472
+ default:
10473
+ return null;
10474
+ }
10475
+ }
10476
+
10477
+ // packages/sharding/src/worker.ts
10478
+ class ClusterWorker {
10479
+ static #current;
10480
+ static current() {
10481
+ if (!ClusterWorker.#current) {
10482
+ const context = readClusterContext();
10483
+ if (context && typeof process.send === "function")
10484
+ ClusterWorker.#current = new ClusterWorker(context, process);
10485
+ }
10486
+ return ClusterWorker.#current;
10487
+ }
10488
+ static reset() {
10489
+ ClusterWorker.#current?.close();
10490
+ ClusterWorker.#current = undefined;
10491
+ }
10492
+ context;
10493
+ #endpoint;
10494
+ #handlers = new Map;
10495
+ #waiting = new Map;
10496
+ #onMessage;
10497
+ #onDisconnect;
10498
+ #counter = 0;
10499
+ #closed = false;
10500
+ constructor(context, endpoint = process) {
10501
+ if (typeof endpoint.send !== "function")
10502
+ throw new Error("ClusterWorker needs an IPC channel: run it in a process forked by ClusterManager.");
10503
+ this.context = context;
10504
+ this.#endpoint = endpoint;
10505
+ this.#onMessage = (frame) => this.#handle(frame);
10506
+ this.#onDisconnect = () => this.#failAll("The parent went away.");
10507
+ endpoint.on("message", this.#onMessage);
10508
+ endpoint.on("disconnect", this.#onDisconnect);
10509
+ }
10510
+ get identifyGate() {
10511
+ return this.context.coordinatedIdentify ? this.#gate : undefined;
10512
+ }
10513
+ #gate = {
10514
+ acquire: (shardId) => {
10515
+ if (this.#closed)
10516
+ return Promise.reject(new Error("The cluster worker is closed."));
10517
+ const id = ++this.#counter;
10518
+ return new Promise((resolve, reject) => {
10519
+ this.#waiting.set(id, { resolve, reject });
10520
+ this.#send({ kind: "identify", id, shardId });
10521
+ });
10522
+ },
10523
+ release: (shardId) => this.#send({ kind: "identify-done", shardId })
10524
+ };
10525
+ ready(shardIds = this.context.shardIds) {
10526
+ this.#send({ kind: "ready", shardIds: [...shardIds] });
10527
+ }
10528
+ reportHealth(report) {
10529
+ this.#send({ kind: "health", report });
10530
+ }
10531
+ onRequest(type, handler) {
10532
+ if (!type.trim() || typeof handler !== "function")
10533
+ throw new TypeError("A request type and handler are required.");
10534
+ this.#handlers.set(type, handler);
10535
+ return this;
10536
+ }
10537
+ close() {
10538
+ this.#closed = true;
10539
+ this.#endpoint.off("message", this.#onMessage);
10540
+ this.#endpoint.off("disconnect", this.#onDisconnect);
10541
+ this.#failAll("The cluster worker closed.");
10542
+ }
10543
+ #send(body) {
10544
+ try {
10545
+ this.#endpoint.send({ [CLUSTER_FRAME]: body });
10546
+ } catch (error) {
10547
+ this.#failAll(error instanceof Error ? error.message : String(error));
10548
+ }
10549
+ }
10550
+ #failAll(reason) {
10551
+ for (const waiter of this.#waiting.values())
10552
+ waiter.reject(new Error(reason));
10553
+ this.#waiting.clear();
10554
+ }
10555
+ #handle(frame) {
10556
+ if (!frame || typeof frame !== "object" || !(CLUSTER_FRAME in frame))
10557
+ return;
10558
+ const parsed = parseParentFrame(frame[CLUSTER_FRAME]);
10559
+ if (!parsed)
10560
+ return;
10561
+ if (parsed.kind === "identify-grant" || parsed.kind === "identify-deny") {
10562
+ const waiter = this.#waiting.get(parsed.id);
10563
+ if (!waiter)
10564
+ return;
10565
+ this.#waiting.delete(parsed.id);
10566
+ if (parsed.kind === "identify-grant")
10567
+ waiter.resolve();
10568
+ else
10569
+ waiter.reject(new Error(parsed.error));
10570
+ return;
10571
+ }
10572
+ const handler = this.#handlers.get(parsed.type);
10573
+ const id = parsed.id;
10574
+ if (!handler) {
10575
+ this.#send({
10576
+ kind: "reply",
10577
+ id,
10578
+ error: `No handler for "${parsed.type}".`
10579
+ });
10580
+ return;
10581
+ }
10582
+ (async () => {
10583
+ try {
10584
+ this.#send({
10585
+ kind: "reply",
10586
+ id,
10587
+ result: await handler(parsed.data)
10588
+ });
10589
+ } catch (error) {
10590
+ this.#send({
10591
+ kind: "reply",
10592
+ id,
10593
+ error: error instanceof Error ? error.message : String(error)
10594
+ });
10595
+ }
10596
+ })();
10597
+ }
10598
+ }
10281
10599
  // packages/sharding/src/transport.ts
10282
10600
  class BroadcastChannelTransport {
10283
10601
  #channel;
@@ -10338,6 +10656,13 @@ function defaultTransport(name) {
10338
10656
 
10339
10657
  // packages/sharding/src/bus.ts
10340
10658
  var REPLY_TYPE = "__lunibee:reply";
10659
+ var MAX_NAME_LENGTH = 256;
10660
+ function isShardMessage(message) {
10661
+ if (!message || typeof message !== "object")
10662
+ return false;
10663
+ const m = message;
10664
+ return typeof m.type === "string" && m.type.length > 0 && m.type.length <= MAX_NAME_LENGTH && typeof m.id === "string" && m.id.length <= MAX_NAME_LENGTH && Number.isInteger(m.source) && m.source >= -1 && (m.target === null || Number.isInteger(m.target) && m.target >= -1);
10665
+ }
10341
10666
 
10342
10667
  class ShardBus {
10343
10668
  #transport;
@@ -10349,6 +10674,7 @@ class ShardBus {
10349
10674
  channelName;
10350
10675
  #counter = 0;
10351
10676
  #pending = new Map;
10677
+ #closed = false;
10352
10678
  constructor(shardId, channelName, options = {}) {
10353
10679
  if (!Number.isInteger(shardId) || shardId < 0)
10354
10680
  throw new RangeError("Shard ID must be a non-negative integer.");
@@ -10421,6 +10747,8 @@ class ShardBus {
10421
10747
  }
10422
10748
  #collect(target, type, data, timeoutMs, expected, rejectOnPeerDown = false) {
10423
10749
  const replies = [];
10750
+ if (this.#closed)
10751
+ return Promise.reject(new Error("ShardBus is closed."));
10424
10752
  if (this.#pending.size >= this.#maxPending)
10425
10753
  return Promise.reject(new Error(`Too many shard requests waiting for a reply (${this.#maxPending}).`));
10426
10754
  return new Promise((resolve, reject) => {
@@ -10432,24 +10760,33 @@ class ShardBus {
10432
10760
  };
10433
10761
  const timer = setTimeout(finish, timeoutMs);
10434
10762
  requestId = this.#publish(target, type, data, true);
10763
+ const fail = (error) => {
10764
+ if (!rejectOnPeerDown)
10765
+ return;
10766
+ clearTimeout(timer);
10767
+ this.#pending.delete(requestId);
10768
+ reject(error);
10769
+ };
10435
10770
  this.#pending.set(requestId, {
10436
10771
  target,
10772
+ seen: new Set,
10437
10773
  reply: (reply) => {
10438
10774
  replies.push(reply);
10439
10775
  if (replies.length >= expected)
10440
10776
  finish();
10441
10777
  },
10442
- fail: (error) => {
10443
- if (rejectOnPeerDown) {
10444
- clearTimeout(timer);
10445
- this.#pending.delete(requestId);
10446
- reject(error);
10447
- }
10448
- }
10778
+ fail,
10779
+ cancel: (error) => rejectOnPeerDown ? fail(error) : finish()
10449
10780
  });
10450
10781
  });
10451
10782
  }
10452
10783
  close() {
10784
+ if (this.#closed)
10785
+ return;
10786
+ this.#closed = true;
10787
+ for (const pending of [...this.#pending.values()])
10788
+ pending.cancel(new Error("ShardBus is closed."));
10789
+ this.#pending.clear();
10453
10790
  this.#transport.close();
10454
10791
  this.#handlers.clear();
10455
10792
  this.#errorHandlers.clear();
@@ -10457,6 +10794,8 @@ class ShardBus {
10457
10794
  #publish(target, type, data, expectsReply = false) {
10458
10795
  if (!type.trim())
10459
10796
  throw new TypeError("Shard message type is required.");
10797
+ if (this.#closed)
10798
+ throw new Error("ShardBus is closed.");
10460
10799
  const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
10461
10800
  this.#transport.post({
10462
10801
  source: this.#shardId,
@@ -10469,15 +10808,21 @@ class ShardBus {
10469
10808
  return id;
10470
10809
  }
10471
10810
  #dispatch(message) {
10472
- if (!message || typeof message !== "object" || typeof message.type !== "string")
10811
+ if (this.#closed || !isShardMessage(message))
10473
10812
  return;
10474
10813
  if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
10475
10814
  return;
10476
10815
  if (message.type === REPLY_TYPE) {
10477
10816
  const reply = message.data;
10478
- this.#pending.get(reply?.requestId)?.reply({
10817
+ if (!reply || typeof reply !== "object" || typeof reply.requestId !== "string")
10818
+ return;
10819
+ const pending = this.#pending.get(reply.requestId);
10820
+ if (!pending || pending.seen.has(message.source) || pending.target !== null && pending.target !== message.source)
10821
+ return;
10822
+ pending.seen.add(message.source);
10823
+ pending.reply({
10479
10824
  shardId: message.source,
10480
- ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
10825
+ ...reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result }
10481
10826
  });
10482
10827
  return;
10483
10828
  }
@@ -10558,167 +10903,1527 @@ class ShardSupervisor {
10558
10903
  // packages/sharding/src/cluster.ts
10559
10904
  import { fork } from "child_process";
10560
10905
  import { cpus } from "os";
10561
- var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10562
10906
 
10563
- class ClusterManager {
10564
- clusters = new Map;
10565
- shardCount = 0;
10566
- #options;
10567
- #auto;
10568
- #autoScaleTimer;
10569
- #spawned = false;
10570
- #stopping = false;
10571
- #restartTimers = new Set;
10572
- #supervisor;
10573
- constructor(options) {
10574
- if (!options.token?.trim())
10575
- throw new TypeError("A Discord bot token is required.");
10576
- if (!options.script?.trim())
10577
- throw new TypeError("A script path is required for clustering.");
10578
- if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
10579
- throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
10580
- }
10581
- this.#auto = options.shardCount === "auto";
10582
- this.#options = { ...options };
10583
- this.#supervisor = new ShardSupervisor({
10584
- restartDelay: options.restartDelay,
10585
- ...options.supervisor
10907
+ // packages/sharding/src/identify.ts
10908
+ var IDENTIFY_INTERVAL = 5000;
10909
+
10910
+ class IdentifyCoordinator {
10911
+ maxConcurrency;
10912
+ #interval;
10913
+ #holdTimeout;
10914
+ #now;
10915
+ #schedule;
10916
+ #cancel;
10917
+ #onBudgetWait;
10918
+ #keys = new Map;
10919
+ #remaining;
10920
+ #total;
10921
+ #resetAt = Number.POSITIVE_INFINITY;
10922
+ #timer;
10923
+ #timerAt = Number.POSITIVE_INFINITY;
10924
+ #closed = false;
10925
+ constructor(options = {}) {
10926
+ const concurrency = options.maxConcurrency ?? 1;
10927
+ if (!Number.isInteger(concurrency) || concurrency < 1)
10928
+ throw new RangeError("maxConcurrency must be a positive integer.");
10929
+ const interval = options.interval ?? IDENTIFY_INTERVAL;
10930
+ const hold = options.holdTimeout ?? 60000;
10931
+ if (!(interval >= 0) || !(hold > 0))
10932
+ throw new RangeError("interval cannot be negative and holdTimeout must be positive.");
10933
+ this.maxConcurrency = concurrency;
10934
+ this.#interval = interval;
10935
+ this.#holdTimeout = hold;
10936
+ this.#now = options.now ?? Date.now;
10937
+ this.#schedule = options.schedule ?? ((callback, ms) => {
10938
+ const timer = setTimeout(callback, ms);
10939
+ timer.unref?.();
10940
+ return timer;
10941
+ });
10942
+ this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle));
10943
+ this.#onBudgetWait = options.onBudgetWait;
10944
+ if (options.budget) {
10945
+ this.#total = options.budget.total;
10946
+ this.#remaining = options.budget.remaining;
10947
+ this.#resetAt = this.#now() + options.budget.resetAfter;
10948
+ }
10949
+ }
10950
+ get remaining() {
10951
+ return this.#remaining;
10952
+ }
10953
+ get waiting() {
10954
+ let count = 0;
10955
+ for (const key of this.#keys.values())
10956
+ count += key.queue.length;
10957
+ return count;
10958
+ }
10959
+ get holding() {
10960
+ let count = 0;
10961
+ for (const key of this.#keys.values())
10962
+ if (key.holder)
10963
+ count++;
10964
+ return count;
10965
+ }
10966
+ acquire(owner, shardId) {
10967
+ if (this.#closed)
10968
+ return Promise.reject(new Error("IDENTIFY coordinator is closed."));
10969
+ const index = shardId % this.maxConcurrency;
10970
+ let key = this.#keys.get(index);
10971
+ if (!key)
10972
+ this.#keys.set(index, key = { queue: [], nextAt: 0 });
10973
+ return new Promise((grant, fail) => {
10974
+ key.queue.push({ owner, shardId, grant, fail });
10975
+ this.pump();
10586
10976
  });
10587
10977
  }
10588
- async fetchRecommendedShardCount() {
10589
- return (await fetchGatewayBot(this.#options.token)).shards;
10590
- }
10591
- async spawn() {
10592
- if (this.#spawned)
10978
+ release(owner, shardId) {
10979
+ const key = this.#keys.get(shardId % this.maxConcurrency);
10980
+ const holder = key?.holder;
10981
+ if (!key || !holder || holder.owner !== owner || holder.shardId !== shardId)
10593
10982
  return;
10594
- if (typeof process?.versions?.bun === "string") {
10595
- throw new Error("ClusterManager requires the Node.js runtime; Bun does not support child_process.fork() for this clustering mode.");
10596
- }
10597
- this.#spawned = true;
10598
- const count = this.#options.shardCount === "auto" || this.#options.shardCount === undefined ? await this.fetchRecommendedShardCount() : this.#options.shardCount;
10599
- this.shardCount = count;
10600
- const clusterCount = this.#options.clusterCount ?? cpus().length;
10601
- const chunks = Array.from({ length: clusterCount }, () => []);
10602
- for (let i = 0;i < count; i++) {
10603
- chunks[i % clusterCount]?.push(i);
10604
- }
10605
- for (const [i, chunk] of chunks.entries()) {
10606
- if (chunk.length === 0)
10607
- continue;
10608
- this.#launch(i, chunk, count);
10609
- await sleep3(500);
10983
+ this.#free(key);
10984
+ this.pump();
10985
+ }
10986
+ drop(owner, reason = "The cluster went away.") {
10987
+ for (const key of this.#keys.values()) {
10988
+ const keep = [];
10989
+ for (const waiter of key.queue)
10990
+ if (waiter.owner === owner)
10991
+ waiter.fail(new Error(reason));
10992
+ else
10993
+ keep.push(waiter);
10994
+ key.queue = keep;
10995
+ if (key.holder?.owner === owner)
10996
+ this.#free(key);
10610
10997
  }
10611
- if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
10612
- this.#autoScaleTimer = setInterval(() => {
10613
- this.checkAutoScale();
10614
- }, this.#options.autoScaleInterval);
10998
+ this.pump();
10999
+ }
11000
+ close() {
11001
+ this.#closed = true;
11002
+ if (this.#timer !== undefined)
11003
+ this.#cancel(this.#timer);
11004
+ this.#timer = undefined;
11005
+ this.#timerAt = Number.POSITIVE_INFINITY;
11006
+ for (const key of this.#keys.values()) {
11007
+ if (key.holder)
11008
+ this.#cancel(key.holder.timer);
11009
+ key.holder = undefined;
11010
+ for (const waiter of key.queue)
11011
+ waiter.fail(new Error("IDENTIFY coordinator is closed."));
11012
+ key.queue = [];
10615
11013
  }
10616
11014
  }
10617
- #launch(id, shards, shardCount) {
10618
- const child = fork(this.#options.script, [], {
10619
- env: {
10620
- ...process.env,
10621
- SHARD_LIST: shards.join(","),
10622
- SHARD_COUNT: shardCount.toString(),
10623
- CLUSTER_ID: id.toString(),
10624
- LUNIBEE_SHARD_BUS: "ipc"
11015
+ pump() {
11016
+ if (this.#closed)
11017
+ return;
11018
+ const now = this.#now();
11019
+ if (this.#total !== undefined && now >= this.#resetAt) {
11020
+ this.#remaining = this.#total;
11021
+ this.#resetAt = Number.POSITIVE_INFINITY;
11022
+ }
11023
+ let next = Number.POSITIVE_INFINITY;
11024
+ for (const key of this.#keys.values()) {
11025
+ if (key.holder || key.queue.length === 0)
11026
+ continue;
11027
+ if (key.nextAt > now) {
11028
+ next = Math.min(next, key.nextAt);
11029
+ continue;
10625
11030
  }
10626
- });
10627
- const info = { id, process: child, shards };
10628
- this.clusters.set(id, info);
10629
- child.on("message", (frame) => this.#relay(id, frame));
10630
- child.once("exit", (code, signal) => {
10631
- if (this.clusters.get(id)?.process !== child)
10632
- return;
10633
- try {
10634
- this.#options.onClusterExit?.(info, code, signal);
10635
- } catch {}
10636
- if (!this.#stopping)
10637
- this.#announceDown(id, shards);
10638
- if (this.#stopping || this.#options.restartOnExit === false)
10639
- return;
10640
- this.clusters.delete(id);
10641
- const delay = this.#supervisor.next(id);
10642
- if (delay === null) {
10643
- try {
10644
- this.#options.onGiveUp?.(info);
10645
- } catch {}
10646
- return;
11031
+ if (this.#remaining !== undefined && this.#remaining <= 0) {
11032
+ const waiter = key.queue[0];
11033
+ if (this.#resetAt === Number.POSITIVE_INFINITY) {
11034
+ key.queue.shift();
11035
+ waiter.fail(new Error("The session start limit is exhausted."));
11036
+ continue;
11037
+ }
11038
+ this.#onBudgetWait?.({
11039
+ shardId: waiter.shardId,
11040
+ waitMs: this.#resetAt - now
11041
+ });
11042
+ next = Math.min(next, this.#resetAt);
11043
+ continue;
10647
11044
  }
10648
- const timer = setTimeout(() => {
10649
- this.#restartTimers.delete(timer);
10650
- if (this.#stopping || !this.#spawned)
10651
- return;
10652
- this.#launch(id, shards, shardCount);
10653
- }, delay);
10654
- timer.unref?.();
10655
- this.#restartTimers.add(timer);
10656
- });
10657
- return info;
10658
- }
10659
- #relay(from, frame) {
10660
- if (!frame || typeof frame !== "object" || !(BUS_FRAME in frame))
10661
- return;
10662
- for (const [id, cluster] of this.clusters)
10663
- if (id !== from)
10664
- this.#sendFrame(cluster, frame);
11045
+ const waiter = key.queue.shift();
11046
+ if (this.#remaining !== undefined)
11047
+ this.#remaining--;
11048
+ const timer = this.#schedule(() => {
11049
+ if (key.holder?.timer === timer) {
11050
+ this.#free(key);
11051
+ this.pump();
11052
+ }
11053
+ }, this.#holdTimeout);
11054
+ key.holder = {
11055
+ owner: waiter.owner,
11056
+ shardId: waiter.shardId,
11057
+ timer
11058
+ };
11059
+ waiter.grant();
11060
+ }
11061
+ this.#arm(next);
10665
11062
  }
10666
- #announceDown(exited, shards) {
10667
- for (const [id, cluster] of this.clusters)
10668
- if (id !== exited)
10669
- this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
11063
+ #free(key) {
11064
+ if (key.holder)
11065
+ this.#cancel(key.holder.timer);
11066
+ key.holder = undefined;
11067
+ key.nextAt = this.#now() + this.#interval;
10670
11068
  }
10671
- #sendFrame(cluster, frame) {
10672
- const child = cluster.process;
10673
- if (typeof child.send !== "function" || !child.connected)
11069
+ #arm(at) {
11070
+ if (at === this.#timerAt && this.#timer !== undefined)
10674
11071
  return;
10675
- try {
10676
- child.send(frame);
10677
- } catch {}
10678
- }
10679
- async checkAutoScale() {
10680
- if (!this.#auto)
11072
+ if (this.#timer !== undefined)
11073
+ this.#cancel(this.#timer);
11074
+ this.#timer = undefined;
11075
+ this.#timerAt = Number.POSITIVE_INFINITY;
11076
+ if (at === Number.POSITIVE_INFINITY)
10681
11077
  return;
10682
- try {
10683
- const recommended = await this.fetchRecommendedShardCount();
10684
- if (recommended !== this.shardCount) {
10685
- await this.respawn(recommended);
11078
+ this.#timerAt = at;
11079
+ this.#timer = this.#schedule(() => {
11080
+ this.#timer = undefined;
11081
+ this.#timerAt = Number.POSITIVE_INFINITY;
11082
+ this.pump();
11083
+ }, Math.max(0, at - this.#now()));
11084
+ }
11085
+ }
11086
+
11087
+ // packages/sharding/src/remote.ts
11088
+ import { EventEmitter as EventEmitter3 } from "events";
11089
+ import { randomBytes } from "crypto";
11090
+
11091
+ // packages/sharding/src/remote-protocol.ts
11092
+ import { createHmac, timingSafeEqual } from "crypto";
11093
+ var REMOTE_PROTOCOL_VERSION = 1;
11094
+ var MAX_REMOTE_FRAME_BYTES = 8 * 1024 * 1024;
11095
+ var RemoteClose = {
11096
+ BadRequest: 4400,
11097
+ AuthFailed: 4401,
11098
+ HelloTimeout: 4408,
11099
+ DuplicateHost: 4409,
11100
+ TooManyConnections: 4429,
11101
+ UnknownHost: 4410,
11102
+ VersionMismatch: 4426,
11103
+ HostLost: 4504,
11104
+ Shutdown: 4500
11105
+ };
11106
+ var SPAWN_ENV_KEYS = new Set([
11107
+ "SHARD_LIST",
11108
+ "SHARD_COUNT",
11109
+ "CLUSTER_ID",
11110
+ "CLUSTER_COUNT",
11111
+ "LUNIBEE_SHARD_BUS",
11112
+ "LUNIBEE_CLUSTER_WORKER",
11113
+ "LUNIBEE_IDENTIFY"
11114
+ ]);
11115
+ var KILL_SIGNALS = new Set([
11116
+ "SIGTERM",
11117
+ "SIGKILL",
11118
+ "SIGINT"
11119
+ ]);
11120
+ function isLoopbackHost(hostname) {
11121
+ return /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)$/i.test(hostname);
11122
+ }
11123
+ var HOST_ID = /^[A-Za-z0-9._-]{1,64}$/;
11124
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11125
+ var isInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
11126
+ function utf8Bytes(text) {
11127
+ return Buffer.byteLength(text, "utf8");
11128
+ }
11129
+ function parseJson(text, maxBytes) {
11130
+ if (typeof text !== "string")
11131
+ return null;
11132
+ if (text.length > maxBytes)
11133
+ return null;
11134
+ if (text.length * 3 > maxBytes && utf8Bytes(text) > maxBytes)
11135
+ return null;
11136
+ try {
11137
+ const value = JSON.parse(text);
11138
+ return isRecord2(value) ? value : null;
11139
+ } catch {
11140
+ return null;
11141
+ }
11142
+ }
11143
+ function signChallenge(secret, nonce, hostId) {
11144
+ return createHmac("sha256", secret).update(`${nonce}:${hostId}`).digest("hex");
11145
+ }
11146
+ function verifyChallenge(secret, nonce, hostId, mac) {
11147
+ if (typeof mac !== "string")
11148
+ return false;
11149
+ const expected = Buffer.from(signChallenge(secret, nonce, hostId), "hex");
11150
+ const given = Buffer.from(mac, "hex");
11151
+ return given.length === expected.length && timingSafeEqual(given, expected);
11152
+ }
11153
+ function parseToHost(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
11154
+ const v = parseJson(text, maxBytes);
11155
+ if (!v)
11156
+ return null;
11157
+ switch (v.t) {
11158
+ case "challenge":
11159
+ return typeof v.nonce === "string" && isInt(v.version) ? { t: "challenge", nonce: v.nonce, version: v.version } : null;
11160
+ case "welcome":
11161
+ return isInt(v.leaseMs) && isInt(v.pingMs) ? { t: "welcome", leaseMs: v.leaseMs, pingMs: v.pingMs } : null;
11162
+ case "ping":
11163
+ return isInt(v.n) ? { t: "ping", n: v.n } : null;
11164
+ case "spawn": {
11165
+ if (!isInt(v.cluster) || !isInt(v.epoch) || !isRecord2(v.env))
11166
+ return null;
11167
+ const env = {};
11168
+ for (const [key, value] of Object.entries(v.env)) {
11169
+ if (!SPAWN_ENV_KEYS.has(key) || typeof value !== "string")
11170
+ return null;
11171
+ env[key] = value;
10686
11172
  }
10687
- } catch (error) {
10688
- const normalized = error instanceof Error ? error : new Error(String(error));
10689
- this.#options.onAutoScaleError?.(normalized);
10690
- }
11173
+ return { t: "spawn", cluster: v.cluster, epoch: v.epoch, env };
11174
+ }
11175
+ case "frame":
11176
+ return isInt(v.cluster) && isInt(v.epoch) ? {
11177
+ t: "frame",
11178
+ cluster: v.cluster,
11179
+ epoch: v.epoch,
11180
+ frame: v.frame
11181
+ } : null;
11182
+ case "kill":
11183
+ return isInt(v.cluster) && isInt(v.epoch) && typeof v.signal === "string" && KILL_SIGNALS.has(v.signal) ? {
11184
+ t: "kill",
11185
+ cluster: v.cluster,
11186
+ epoch: v.epoch,
11187
+ signal: v.signal
11188
+ } : null;
11189
+ case "bye":
11190
+ return typeof v.reason === "string" ? { t: "bye", reason: v.reason } : null;
11191
+ default:
11192
+ return null;
10691
11193
  }
10692
- async respawn(newShardCount) {
10693
- this.#options.shardCount = newShardCount;
10694
- await this.shutdownAll();
10695
- this.#spawned = false;
10696
- this.#supervisor.reset();
10697
- await this.spawn();
11194
+ }
11195
+ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
11196
+ const v = parseJson(text, maxBytes);
11197
+ if (!v)
11198
+ return null;
11199
+ switch (v.t) {
11200
+ case "hello":
11201
+ return typeof v.id === "string" && typeof v.mac === "string" && isInt(v.version) ? { t: "hello", id: v.id, mac: v.mac, version: v.version } : null;
11202
+ case "pong":
11203
+ return isInt(v.n) ? { t: "pong", n: v.n } : null;
11204
+ case "spawned":
11205
+ return isInt(v.cluster) && isInt(v.epoch) && (v.pid === undefined || isInt(v.pid)) ? {
11206
+ t: "spawned",
11207
+ cluster: v.cluster,
11208
+ epoch: v.epoch,
11209
+ ...v.pid !== undefined ? { pid: v.pid } : {}
11210
+ } : null;
11211
+ case "frame":
11212
+ return isInt(v.cluster) && isInt(v.epoch) ? {
11213
+ t: "frame",
11214
+ cluster: v.cluster,
11215
+ epoch: v.epoch,
11216
+ frame: v.frame
11217
+ } : null;
11218
+ case "exit":
11219
+ return isInt(v.cluster) && isInt(v.epoch) && (v.code === null || typeof v.code === "number") && (v.signal === null || typeof v.signal === "string") ? {
11220
+ t: "exit",
11221
+ cluster: v.cluster,
11222
+ epoch: v.epoch,
11223
+ code: v.code,
11224
+ signal: v.signal
11225
+ } : null;
11226
+ case "error":
11227
+ return isInt(v.cluster) && isInt(v.epoch) && typeof v.message === "string" ? {
11228
+ t: "error",
11229
+ cluster: v.cluster,
11230
+ epoch: v.epoch,
11231
+ message: v.message
11232
+ } : null;
11233
+ default:
11234
+ return null;
10698
11235
  }
10699
- async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
10700
- this.#stopping = true;
10701
- this.#clearRestartTimers();
10702
- if (this.#autoScaleTimer) {
10703
- clearInterval(this.#autoScaleTimer);
10704
- this.#autoScaleTimer = undefined;
10705
- }
10706
- await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
10707
- this.clusters.clear();
10708
- this.#spawned = false;
10709
- this.#stopping = false;
11236
+ }
11237
+
11238
+ // packages/sharding/src/remote.ts
11239
+ class RemoteChild extends EventEmitter3 {
11240
+ host;
11241
+ cluster;
11242
+ epoch;
11243
+ pid;
11244
+ exitCode = null;
11245
+ signalCode = null;
11246
+ connected = true;
11247
+ #send;
11248
+ #kill;
11249
+ constructor(host, cluster, epoch, send, kill) {
11250
+ super();
11251
+ this.host = host;
11252
+ this.cluster = cluster;
11253
+ this.epoch = epoch;
11254
+ this.#send = send;
11255
+ this.#kill = kill;
10710
11256
  }
10711
- #clearRestartTimers() {
10712
- for (const timer of this.#restartTimers)
10713
- clearTimeout(timer);
10714
- this.#restartTimers.clear();
11257
+ send(frame) {
11258
+ return this.connected && this.exitCode === null && this.signalCode === null ? this.#send(frame) : false;
10715
11259
  }
10716
- async#shutdownCluster(cluster, timeoutMs) {
10717
- const child = cluster.process;
10718
- if (child.exitCode !== null || child.signalCode !== null)
11260
+ kill(signal = "SIGTERM") {
11261
+ if (this.exitCode !== null || this.signalCode !== null)
11262
+ return false;
11263
+ return this.#kill(signal);
11264
+ }
11265
+ spawned(pid) {
11266
+ this.pid = pid;
11267
+ }
11268
+ deliver(frame) {
11269
+ if (this.exitCode === null && this.signalCode === null)
11270
+ this.emit("message", frame);
11271
+ }
11272
+ finish(code, signal) {
11273
+ if (this.exitCode !== null || this.signalCode !== null)
10719
11274
  return;
10720
- await new Promise((resolve) => {
10721
- let timer;
11275
+ this.connected = false;
11276
+ this.exitCode = code ?? (signal === null ? 1 : null);
11277
+ this.signalCode = signal ?? null;
11278
+ this.emit("exit", code, signal);
11279
+ }
11280
+ }
11281
+
11282
+ class RemoteHosts {
11283
+ #options;
11284
+ #links = new Map;
11285
+ #waiters = new Set;
11286
+ #hooks;
11287
+ #server;
11288
+ #watchdog;
11289
+ #epoch = 0;
11290
+ #ping = 0;
11291
+ #unauthenticated = 0;
11292
+ constructor(options, hooks) {
11293
+ if (typeof options.secret !== "string" || options.secret.length < 16)
11294
+ throw new RangeError("remote.secret must be a string of at least 16 characters.");
11295
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)
11296
+ throw new RangeError("remote.port must be an integer from 0 to 65535.");
11297
+ if (options.tls && (typeof options.tls.cert !== "string" || typeof options.tls.key !== "string" || !options.tls.cert || !options.tls.key))
11298
+ throw new TypeError("remote.tls needs a non-empty cert and key.");
11299
+ if (!options.tls && !options.allowInsecure && !isLoopbackHost(options.hostname ?? "127.0.0.1"))
11300
+ throw new RangeError("remote listens on a network address without TLS: set remote.tls, bind 127.0.0.1 behind a TLS proxy, or set remote.allowInsecure for a network you fully trust.");
11301
+ const lease = options.lease ?? 15000;
11302
+ if (!Number.isFinite(lease) || lease < 30)
11303
+ throw new RangeError("remote.lease must be at least 30 milliseconds.");
11304
+ for (const id of options.hosts ?? [])
11305
+ if (!HOST_ID.test(id))
11306
+ throw new RangeError(`"${id}" is not a valid host ID.`);
11307
+ if (options.hosts && (options.hosts.length === 0 || new Set(options.hosts).size !== options.hosts.length))
11308
+ throw new RangeError("remote.hosts must be unique and not empty.");
11309
+ if (options.minHosts !== undefined && (!Number.isInteger(options.minHosts) || options.minHosts < 1))
11310
+ throw new RangeError("remote.minHosts must be a positive integer.");
11311
+ this.#options = { ...options, lease };
11312
+ this.#hooks = hooks;
11313
+ }
11314
+ get port() {
11315
+ return this.#server?.port;
11316
+ }
11317
+ connected() {
11318
+ return [...this.#links.keys()].sort();
11319
+ }
11320
+ start() {
11321
+ if (this.#server)
11322
+ return;
11323
+ const { port, hostname, tls, lease } = this.#options;
11324
+ this.#server = Bun.serve({
11325
+ port,
11326
+ hostname: hostname ?? "127.0.0.1",
11327
+ ...tls ? { tls } : {},
11328
+ fetch: (request, server) => {
11329
+ if (server.upgrade(request, {
11330
+ data: { nonce: randomBytes(16).toString("hex") }
11331
+ }))
11332
+ return;
11333
+ return new Response("Lunibee cluster coordinator", {
11334
+ status: 426
11335
+ });
11336
+ },
11337
+ websocket: {
11338
+ maxPayloadLength: MAX_REMOTE_FRAME_BYTES,
11339
+ idleTimeout: 0,
11340
+ open: (ws) => this.#open(ws),
11341
+ message: (ws, raw) => this.#message(ws, raw),
11342
+ close: (ws, _code, reason) => this.#closed(ws, reason)
11343
+ }
11344
+ });
11345
+ const pingMs = Math.max(10, Math.floor(lease / 3));
11346
+ this.#watchdog = setInterval(() => this.#tick(), pingMs);
11347
+ this.#watchdog.unref?.();
11348
+ }
11349
+ stop() {
11350
+ if (this.#watchdog)
11351
+ clearInterval(this.#watchdog);
11352
+ this.#watchdog = undefined;
11353
+ for (const link of [...this.#links.values()]) {
11354
+ this.#post(link, { t: "bye", reason: "The coordinator stopped." });
11355
+ this.#drop(link, "The coordinator stopped.", RemoteClose.Shutdown);
11356
+ }
11357
+ this.#server?.stop(true);
11358
+ this.#server = undefined;
11359
+ this.abortWaits();
11360
+ }
11361
+ waitFor(hosts, count, timeoutMs) {
11362
+ const done = () => hosts ? hosts.every((id) => this.#links.has(id)) : this.#links.size >= count;
11363
+ if (done())
11364
+ return Promise.resolve();
11365
+ return new Promise((resolve, reject) => {
11366
+ const waiter = {
11367
+ check: () => {
11368
+ if (!done())
11369
+ return;
11370
+ clearTimeout(timer);
11371
+ this.#waiters.delete(waiter);
11372
+ resolve();
11373
+ },
11374
+ abort: (error) => {
11375
+ clearTimeout(timer);
11376
+ this.#waiters.delete(waiter);
11377
+ reject(error);
11378
+ }
11379
+ };
11380
+ const timer = setTimeout(() => {
11381
+ const missing = hosts?.filter((id) => !this.#links.has(id));
11382
+ waiter.abort(new Error(hosts ? `Hosts did not join within ${timeoutMs}ms: [${missing}].` : `Only ${this.#links.size} of ${count} hosts joined within ${timeoutMs}ms.`));
11383
+ }, timeoutMs);
11384
+ this.#waiters.add(waiter);
11385
+ });
11386
+ }
11387
+ abortWaits() {
11388
+ for (const waiter of [...this.#waiters])
11389
+ waiter.abort(new Error("The coordinator stopped."));
11390
+ }
11391
+ status(declared = []) {
11392
+ const ids = new Set([...declared, ...this.#links.keys()]);
11393
+ return [...ids].sort().map((id) => {
11394
+ const link = this.#links.get(id);
11395
+ return {
11396
+ id,
11397
+ connected: link !== undefined,
11398
+ lastSeen: link?.lastSeen ?? 0,
11399
+ clusters: link ? [...link.children.keys()].sort((a, b) => a - b) : []
11400
+ };
11401
+ });
11402
+ }
11403
+ createChild(hostId, cluster, env) {
11404
+ const epoch = ++this.#epoch;
11405
+ const link = this.#links.get(hostId);
11406
+ const child = new RemoteChild(hostId, cluster, epoch, (frame) => link !== undefined && this.#post(link, { t: "frame", cluster, epoch, frame }), (signal) => link !== undefined && this.#post(link, { t: "kill", cluster, epoch, signal }));
11407
+ if (!link) {
11408
+ queueMicrotask(() => child.finish(null, "SIGHUP"));
11409
+ return child;
11410
+ }
11411
+ link.children.get(cluster)?.finish(null, "SIGKILL");
11412
+ link.children.set(cluster, child);
11413
+ if (!this.#post(link, { t: "spawn", cluster, epoch, env }))
11414
+ queueMicrotask(() => child.finish(null, "SIGHUP"));
11415
+ return child;
11416
+ }
11417
+ #post(link, message) {
11418
+ try {
11419
+ return link.ws.send(JSON.stringify(message)) > 0;
11420
+ } catch {
11421
+ return false;
11422
+ }
11423
+ }
11424
+ #open(ws) {
11425
+ if (this.#unauthenticated >= (this.#options.maxUnauthenticated ?? 16)) {
11426
+ ws.close(RemoteClose.TooManyConnections, "Too many connections.");
11427
+ return;
11428
+ }
11429
+ this.#unauthenticated++;
11430
+ ws.data.counted = true;
11431
+ ws.data.helloTimer = setTimeout(() => ws.close(RemoteClose.HelloTimeout, "No hello."), this.#options.helloTimeout ?? 1e4);
11432
+ ws.send(JSON.stringify({
11433
+ t: "challenge",
11434
+ nonce: ws.data.nonce,
11435
+ version: REMOTE_PROTOCOL_VERSION
11436
+ }));
11437
+ }
11438
+ #message(ws, raw) {
11439
+ if (typeof raw !== "string") {
11440
+ ws.close(RemoteClose.BadRequest, "Text frames only.");
11441
+ return;
11442
+ }
11443
+ const message = parseToCoordinator(raw);
11444
+ if (!message) {
11445
+ ws.close(RemoteClose.BadRequest, "Malformed frame.");
11446
+ return;
11447
+ }
11448
+ if (ws.data.id === undefined) {
11449
+ this.#hello(ws, message);
11450
+ return;
11451
+ }
11452
+ const link = this.#links.get(ws.data.id);
11453
+ if (!link || link.ws !== ws)
11454
+ return;
11455
+ link.lastSeen = Date.now();
11456
+ if (message.t === "pong" || message.t === "hello")
11457
+ return;
11458
+ const child = link.children.get(message.cluster);
11459
+ if (!child || child.epoch !== message.epoch)
11460
+ return;
11461
+ switch (message.t) {
11462
+ case "spawned":
11463
+ child.spawned(message.pid);
11464
+ break;
11465
+ case "frame":
11466
+ child.deliver(message.frame);
11467
+ break;
11468
+ case "exit":
11469
+ link.children.delete(message.cluster);
11470
+ child.finish(message.code, message.signal);
11471
+ break;
11472
+ case "error":
11473
+ this.#hooks.error(new Error(`Host ${link.id} could not run cluster ${message.cluster}: ${message.message}`));
11474
+ link.children.delete(message.cluster);
11475
+ child.finish(1, null);
11476
+ break;
11477
+ }
11478
+ }
11479
+ #hello(ws, message) {
11480
+ if (message?.t !== "hello") {
11481
+ ws.close(RemoteClose.BadRequest, "Expected hello.");
11482
+ return;
11483
+ }
11484
+ const { id } = message;
11485
+ if (message.version !== REMOTE_PROTOCOL_VERSION) {
11486
+ ws.close(RemoteClose.VersionMismatch, "Protocol version mismatch.");
11487
+ return;
11488
+ }
11489
+ if (!HOST_ID.test(id) || !verifyChallenge(this.#options.secret, ws.data.nonce, id, message.mac)) {
11490
+ ws.close(RemoteClose.AuthFailed, "Authentication failed.");
11491
+ return;
11492
+ }
11493
+ if (this.#options.hosts && !this.#options.hosts.includes(id)) {
11494
+ ws.close(RemoteClose.UnknownHost, "Host is not part of this deployment.");
11495
+ return;
11496
+ }
11497
+ if (this.#links.has(id)) {
11498
+ ws.close(RemoteClose.DuplicateHost, "A host with this ID is connected.");
11499
+ return;
11500
+ }
11501
+ clearTimeout(ws.data.helloTimer);
11502
+ this.#uncount(ws);
11503
+ ws.data.id = id;
11504
+ const link = {
11505
+ id,
11506
+ ws,
11507
+ lastSeen: Date.now(),
11508
+ children: new Map
11509
+ };
11510
+ this.#links.set(id, link);
11511
+ const lease = this.#options.lease;
11512
+ this.#post(link, {
11513
+ t: "welcome",
11514
+ leaseMs: lease,
11515
+ pingMs: Math.max(10, Math.floor(lease / 3))
11516
+ });
11517
+ this.#hooks.join(id);
11518
+ for (const waiter of [...this.#waiters])
11519
+ waiter.check();
11520
+ }
11521
+ #uncount(ws) {
11522
+ if (ws.data.counted) {
11523
+ ws.data.counted = false;
11524
+ this.#unauthenticated--;
11525
+ }
11526
+ }
11527
+ #closed(ws, reason) {
11528
+ clearTimeout(ws.data.helloTimer);
11529
+ this.#uncount(ws);
11530
+ const link = ws.data.id ? this.#links.get(ws.data.id) : undefined;
11531
+ if (link && link.ws === ws)
11532
+ this.#drop(link, reason || "The connection closed.");
11533
+ }
11534
+ #drop(link, reason, code) {
11535
+ if (this.#links.get(link.id) !== link)
11536
+ return;
11537
+ this.#links.delete(link.id);
11538
+ if (code !== undefined)
11539
+ try {
11540
+ link.ws.close(code, reason.slice(0, 100));
11541
+ } catch {}
11542
+ for (const child of [...link.children.values()])
11543
+ child.finish(null, "SIGHUP");
11544
+ link.children.clear();
11545
+ this.#hooks.leave(link.id, reason);
11546
+ }
11547
+ #tick() {
11548
+ const now = Date.now();
11549
+ for (const link of [...this.#links.values()]) {
11550
+ if (now - link.lastSeen > this.#options.lease * 2) {
11551
+ this.#drop(link, "The host stopped answering.", RemoteClose.HostLost);
11552
+ continue;
11553
+ }
11554
+ this.#post(link, { t: "ping", n: ++this.#ping });
11555
+ }
11556
+ }
11557
+ }
11558
+
11559
+ // packages/sharding/src/requests.ts
11560
+ class RequestTracker {
11561
+ #pending = new Map;
11562
+ #maxPending;
11563
+ #prefix;
11564
+ #counter = 0;
11565
+ constructor(prefix = "parent", maxPending = 1e4) {
11566
+ if (!Number.isInteger(maxPending) || maxPending < 1)
11567
+ throw new RangeError("maxPending must be a positive integer.");
11568
+ this.#prefix = prefix;
11569
+ this.#maxPending = maxPending;
11570
+ }
11571
+ get size() {
11572
+ return this.#pending.size;
11573
+ }
11574
+ begin(options) {
11575
+ if (this.#pending.size >= this.#maxPending)
11576
+ throw new Error(`Too many requests waiting for a reply (${this.#maxPending}).`);
11577
+ const id = `${this.#prefix}:${++this.#counter}`;
11578
+ const done = new Promise((resolve) => {
11579
+ const finish = () => {
11580
+ const entry = this.#pending.get(id);
11581
+ if (!entry)
11582
+ return;
11583
+ this.#pending.delete(id);
11584
+ clearTimeout(entry.timer);
11585
+ entry.cleanup?.();
11586
+ resolve({
11587
+ replies: entry.replies,
11588
+ missing: [...entry.keys.keys()].filter((key) => !entry.answered.has(key))
11589
+ });
11590
+ };
11591
+ const entry = {
11592
+ keys: new Map(options.keys),
11593
+ replies: [],
11594
+ answered: new Set,
11595
+ finish,
11596
+ timer: setTimeout(finish, options.timeoutMs)
11597
+ };
11598
+ if (options.signal) {
11599
+ const signal = options.signal;
11600
+ if (signal.aborted)
11601
+ queueMicrotask(finish);
11602
+ else {
11603
+ signal.addEventListener("abort", finish, { once: true });
11604
+ entry.cleanup = () => signal.removeEventListener("abort", finish);
11605
+ }
11606
+ }
11607
+ this.#pending.set(id, entry);
11608
+ if (entry.keys.size === 0)
11609
+ queueMicrotask(finish);
11610
+ });
11611
+ return { id, done };
11612
+ }
11613
+ answer(id, key, from, value) {
11614
+ const entry = this.#pending.get(id);
11615
+ if (!entry || entry.keys.get(key) !== from)
11616
+ return false;
11617
+ if (entry.answered.has(key))
11618
+ return false;
11619
+ entry.answered.add(key);
11620
+ entry.replies.push({ key, ...value });
11621
+ if (entry.replies.length >= entry.keys.size)
11622
+ entry.finish();
11623
+ return true;
11624
+ }
11625
+ failOwner(owner, error) {
11626
+ for (const entry of [...this.#pending.values()]) {
11627
+ for (const [key, keyOwner] of entry.keys)
11628
+ if (keyOwner === owner && !entry.answered.has(key)) {
11629
+ entry.answered.add(key);
11630
+ entry.replies.push({ key, error });
11631
+ }
11632
+ if (entry.replies.length >= entry.keys.size)
11633
+ entry.finish();
11634
+ }
11635
+ }
11636
+ close() {
11637
+ for (const entry of [...this.#pending.values()])
11638
+ entry.finish();
11639
+ }
11640
+ }
11641
+
11642
+ // packages/sharding/src/cluster.ts
11643
+ var DEFAULT_MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
11644
+ function payloadSize(value) {
11645
+ if (value === undefined || value === null)
11646
+ return 0;
11647
+ if (typeof value === "string")
11648
+ return Buffer.byteLength(value, "utf8");
11649
+ if (typeof value !== "object")
11650
+ return String(value).length;
11651
+ try {
11652
+ const text = JSON.stringify(value);
11653
+ return text === undefined ? 0 : Buffer.byteLength(text, "utf8");
11654
+ } catch {
11655
+ return Number.POSITIVE_INFINITY;
11656
+ }
11657
+ }
11658
+ var isLive = (state) => state === "starting" || state === "ready";
11659
+
11660
+ class ClusterManager {
11661
+ clusters = new Map;
11662
+ shardCount = 0;
11663
+ #options;
11664
+ #auto;
11665
+ #autoScaleTimer;
11666
+ #infos = new Map;
11667
+ #owner = new Map;
11668
+ #startPromise;
11669
+ #stopPromise;
11670
+ #wasStarted = false;
11671
+ #stopping = false;
11672
+ #restartTimers = new Set;
11673
+ #startTimers = new Map;
11674
+ #pauses = new Set;
11675
+ #readyWaiters = new Set;
11676
+ #supervisor;
11677
+ #tracker;
11678
+ #coordinator;
11679
+ #allReady = false;
11680
+ #clusterCount = 0;
11681
+ #identifyWaiting = new Map;
11682
+ #remote;
11683
+ #hostOf = new Map;
11684
+ #lastWait = "";
11685
+ #listeners = new Map;
11686
+ constructor(options) {
11687
+ if (!options.token?.trim())
11688
+ throw new TypeError("A Discord bot token is required.");
11689
+ if (!options.script?.trim())
11690
+ throw new TypeError("A script path is required for clustering.");
11691
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
11692
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
11693
+ }
11694
+ if (options.clusterCount !== undefined && options.shardsPerCluster !== undefined)
11695
+ throw new RangeError("Set either clusterCount or shardsPerCluster, not both.");
11696
+ for (const [name, value] of [
11697
+ ["clusterCount", options.clusterCount],
11698
+ ["shardsPerCluster", options.shardsPerCluster],
11699
+ ["maxConcurrency", options.maxConcurrency]
11700
+ ])
11701
+ if (value !== undefined && (!Number.isInteger(value) || value < 1))
11702
+ throw new RangeError(`${name} must be a positive integer.`);
11703
+ if (options.maxMessageBytes !== undefined && (!Number.isInteger(options.maxMessageBytes) || options.maxMessageBytes < 1))
11704
+ throw new RangeError("maxMessageBytes must be a positive integer.");
11705
+ for (const [name, value] of [
11706
+ ["spawnInterval", options.spawnInterval],
11707
+ ["startTimeout", options.startTimeout],
11708
+ ["requestTimeout", options.requestTimeout]
11709
+ ])
11710
+ if (value !== undefined && !(value >= 0))
11711
+ throw new RangeError(`${name} cannot be negative.`);
11712
+ if (options.guildsPerShard !== undefined) {
11713
+ assertGuildsPerShard(options.guildsPerShard);
11714
+ if (typeof options.shardCount === "number")
11715
+ throw new RangeError('guildsPerShard needs shardCount: "auto".');
11716
+ }
11717
+ if (options.shardIds && typeof options.shardCount === "number")
11718
+ validateShardIds(options.shardIds, options.shardCount);
11719
+ this.#auto = options.shardCount === "auto";
11720
+ this.#options = { ...options };
11721
+ this.#supervisor = new ShardSupervisor({
11722
+ restartDelay: options.restartDelay,
11723
+ ...options.supervisor
11724
+ });
11725
+ this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
11726
+ if (options.remote)
11727
+ this.#remote = new RemoteHosts(options.remote, {
11728
+ join: (id) => this.#emit("hostJoin", id),
11729
+ leave: (id, reason) => this.#emit("hostLeave", id, reason),
11730
+ error: (error) => this.#emit("error", error)
11731
+ });
11732
+ }
11733
+ on(event, listener) {
11734
+ let set = this.#listeners.get(event);
11735
+ if (!set)
11736
+ this.#listeners.set(event, set = new Set);
11737
+ set.add(listener);
11738
+ return this;
11739
+ }
11740
+ off(event, listener) {
11741
+ this.#listeners.get(event)?.delete(listener);
11742
+ return this;
11743
+ }
11744
+ #emit(event, ...args) {
11745
+ for (const listener of this.#listeners.get(event) ?? [])
11746
+ try {
11747
+ listener(...args);
11748
+ } catch {}
11749
+ }
11750
+ async fetchRecommendedShardCount() {
11751
+ return resolveShardCount(this.#options.token, await fetchGatewayBot(this.#options.token), this.#options);
11752
+ }
11753
+ clusterForShard(shardId) {
11754
+ const id = this.#owner.get(shardId);
11755
+ return id === undefined ? undefined : this.#infos.get(id);
11756
+ }
11757
+ assignments() {
11758
+ return new Map(this.#owner);
11759
+ }
11760
+ spawn() {
11761
+ if (isClusterWorker())
11762
+ return Promise.reject(new Error("A cluster worker cannot start a ClusterManager: it would fork the whole fleet again."));
11763
+ if (this.#stopPromise)
11764
+ return this.#stopPromise.then(() => this.spawn());
11765
+ if (!this.#startPromise) {
11766
+ const attempt = this.#start();
11767
+ this.#startPromise = attempt;
11768
+ attempt.catch(() => {
11769
+ if (this.#startPromise === attempt)
11770
+ this.#startPromise = undefined;
11771
+ });
11772
+ }
11773
+ return this.#startPromise;
11774
+ }
11775
+ async#resolveGateway() {
11776
+ const { shardCount, maxConcurrency, fetchLimits } = this.#options;
11777
+ let info;
11778
+ let count;
11779
+ if (shardCount === "auto" || shardCount === undefined) {
11780
+ info = await fetchGatewayBot(this.#options.token);
11781
+ count = await resolveShardCount(this.#options.token, info, this.#options);
11782
+ } else if (fetchLimits)
11783
+ try {
11784
+ info = await fetchGatewayBot(this.#options.token);
11785
+ } catch (error) {
11786
+ this.#emit("error", error instanceof Error ? error : new Error(String(error)));
11787
+ }
11788
+ const limit = info?.sessionStartLimit;
11789
+ return {
11790
+ count: count ?? shardCount,
11791
+ maxConcurrency: Math.min(maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Number.POSITIVE_INFINITY),
11792
+ ...limit ? {
11793
+ budget: {
11794
+ total: limit.total,
11795
+ remaining: limit.remaining,
11796
+ resetAfter: limit.resetAfter
11797
+ }
11798
+ } : {}
11799
+ };
11800
+ }
11801
+ async#start() {
11802
+ const { count, maxConcurrency, budget } = await this.#resolveGateway();
11803
+ if (this.#stopping)
11804
+ return;
11805
+ const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
11806
+ validateShardIds(ids, count);
11807
+ const clusterCount = this.#options.clusterCount ?? (this.#options.shardsPerCluster ? Math.ceil(ids.length / this.#options.shardsPerCluster) : cpus().length);
11808
+ const chunks = allocateShards(ids, clusterCount);
11809
+ assertCompleteAllocation(chunks, ids);
11810
+ const placement = await this.#placeHosts().catch((error) => {
11811
+ if (this.#stopping)
11812
+ return;
11813
+ throw error;
11814
+ });
11815
+ if (!placement || this.#stopping)
11816
+ return;
11817
+ this.shardCount = count;
11818
+ this.#wasStarted = true;
11819
+ this.#owner.clear();
11820
+ this.#infos.clear();
11821
+ const identify = this.#options.identify;
11822
+ this.#coordinator = identify?.coordinate === false ? undefined : new IdentifyCoordinator({
11823
+ maxConcurrency,
11824
+ interval: identify?.interval,
11825
+ holdTimeout: identify?.holdTimeout,
11826
+ budget,
11827
+ onBudgetWait: (wait) => {
11828
+ const key = `${wait.shardId}:${Math.round(wait.waitMs / 1000)}`;
11829
+ if (key === this.#lastWait)
11830
+ return;
11831
+ this.#lastWait = key;
11832
+ this.#emit("identifyWait", wait);
11833
+ }
11834
+ });
11835
+ const active = chunks.map((shards, id) => ({ id, shards })).filter((chunk) => chunk.shards.length > 0);
11836
+ this.#clusterCount = active.length;
11837
+ this.#hostOf.clear();
11838
+ for (const chunk of active)
11839
+ this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
11840
+ for (const [index, chunk] of active.entries()) {
11841
+ if (this.#stopping)
11842
+ return;
11843
+ for (const shard of chunk.shards)
11844
+ this.#owner.set(shard, chunk.id);
11845
+ this.#launch(chunk.id, chunk.shards);
11846
+ const gap = this.#options.spawnInterval ?? 500;
11847
+ if (index < active.length - 1 && gap > 0)
11848
+ await this.#pause(gap);
11849
+ }
11850
+ if (this.#stopping)
11851
+ return;
11852
+ if (this.#options.waitForReady)
11853
+ await this.ready();
11854
+ if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
11855
+ this.#autoScaleTimer = setInterval(() => {
11856
+ this.checkAutoScale();
11857
+ }, this.#options.autoScaleInterval);
11858
+ }
11859
+ }
11860
+ async#placeHosts() {
11861
+ const remote = this.#remote;
11862
+ const options = this.#options.remote;
11863
+ if (!remote || !options)
11864
+ return ["local"];
11865
+ remote.start();
11866
+ try {
11867
+ await remote.waitFor(options.hosts, options.minHosts ?? 1, options.joinTimeout ?? 60000);
11868
+ } catch (error) {
11869
+ remote.stop();
11870
+ throw error;
11871
+ }
11872
+ const ids = options.hosts ?? remote.connected();
11873
+ return options.local ? ["local", ...ids] : ids;
11874
+ }
11875
+ get remotePort() {
11876
+ return this.#remote?.port;
11877
+ }
11878
+ hosts() {
11879
+ return this.#remote?.status(this.#options.remote?.hosts) ?? [];
11880
+ }
11881
+ #pause(ms) {
11882
+ return new Promise((resolve) => {
11883
+ const wake = () => {
11884
+ clearTimeout(timer);
11885
+ this.#pauses.delete(wake);
11886
+ resolve();
11887
+ };
11888
+ const timer = setTimeout(wake, ms);
11889
+ this.#pauses.add(wake);
11890
+ });
11891
+ }
11892
+ #launch(id, shards) {
11893
+ const contract = {
11894
+ SHARD_LIST: shards.join(","),
11895
+ SHARD_COUNT: this.shardCount.toString(),
11896
+ CLUSTER_ID: id.toString(),
11897
+ CLUSTER_COUNT: this.#clusterCount.toString(),
11898
+ LUNIBEE_SHARD_BUS: "ipc",
11899
+ [WORKER_MARKER]: "1",
11900
+ LUNIBEE_IDENTIFY: this.#coordinator ? "coordinated" : "local"
11901
+ };
11902
+ const host = this.#hostOf.get(id) ?? "local";
11903
+ const env = {
11904
+ ...process.env,
11905
+ ...contract
11906
+ };
11907
+ const child = host !== "local" && this.#remote ? this.#remote.createChild(host, id, contract) : this.#options.fork ? this.#options.fork(this.#options.script, env) : fork(this.#options.script, [], {
11908
+ env
11909
+ });
11910
+ const known = this.#infos.get(id);
11911
+ const info = known ?? {
11912
+ id,
11913
+ host,
11914
+ process: child,
11915
+ shards,
11916
+ state: "starting",
11917
+ restarts: 0,
11918
+ startedAt: Date.now()
11919
+ };
11920
+ info.process = child;
11921
+ info.state = "starting";
11922
+ info.startedAt = Date.now();
11923
+ info.readyAt = undefined;
11924
+ this.#infos.set(id, info);
11925
+ this.clusters.set(id, info);
11926
+ this.#allReady = false;
11927
+ child.on("message", (frame) => this.#onFrame(info, child, frame));
11928
+ child.once("exit", (code, signal) => this.#onExit(info, child, code, signal));
11929
+ const timeout = this.#options.startTimeout;
11930
+ if (timeout) {
11931
+ const timer = setTimeout(() => {
11932
+ this.#startTimers.delete(id);
11933
+ if (info.process !== child || info.state !== "starting")
11934
+ return;
11935
+ this.#emit("clusterStartTimeout", info);
11936
+ try {
11937
+ child.kill("SIGKILL");
11938
+ } catch {}
11939
+ }, timeout);
11940
+ timer.unref?.();
11941
+ this.#startTimers.set(id, timer);
11942
+ }
11943
+ this.#emit("clusterSpawn", info);
11944
+ return info;
11945
+ }
11946
+ #onExit(info, child, code, signal) {
11947
+ if (info.process !== child)
11948
+ return;
11949
+ child.removeAllListeners("message");
11950
+ this.#clearStartTimer(info.id);
11951
+ this.#coordinator?.drop(info.id, `Cluster ${info.id} exited.`);
11952
+ this.#identifyWaiting.delete(info.id);
11953
+ this.#tracker.failOwner(info.id, `Cluster ${info.id} exited.`);
11954
+ this.clusters.delete(info.id);
11955
+ this.#allReady = false;
11956
+ try {
11957
+ this.#options.onClusterExit?.(info, code, signal);
11958
+ } catch {}
11959
+ const expected = this.#stopping || info.state === "stopping" || info.state === "stopped";
11960
+ this.#emit("clusterExit", info, code, signal, expected);
11961
+ if (expected) {
11962
+ info.state = "stopped";
11963
+ return;
11964
+ }
11965
+ this.#announceDown(info.id, info.shards);
11966
+ if (this.#options.restartOnExit === false) {
11967
+ info.state = "failed";
11968
+ this.#checkReady();
11969
+ return;
11970
+ }
11971
+ const delay = this.#supervisor.next(info.id);
11972
+ if (delay === null) {
11973
+ info.state = "failed";
11974
+ try {
11975
+ this.#options.onGiveUp?.(info);
11976
+ } catch {}
11977
+ this.#emit("clusterGiveUp", info);
11978
+ this.#checkReady();
11979
+ if (this.#options.reassignOnGiveUp)
11980
+ this.#reassign(info);
11981
+ return;
11982
+ }
11983
+ info.state = "restarting";
11984
+ info.restarts++;
11985
+ this.#emit("clusterRestart", info, { delay, attempt: info.restarts });
11986
+ if (info.restarts >= 3)
11987
+ this.#emit("clusterUnstable", info, {
11988
+ attempt: info.restarts,
11989
+ delay
11990
+ });
11991
+ const timer = setTimeout(() => {
11992
+ this.#restartTimers.delete(timer);
11993
+ if (this.#stopping || info.state !== "restarting")
11994
+ return;
11995
+ this.#launch(info.id, info.shards);
11996
+ }, delay);
11997
+ timer.unref?.();
11998
+ this.#restartTimers.add(timer);
11999
+ }
12000
+ async#reassign(failed) {
12001
+ const shardIds = [...failed.shards];
12002
+ const target = [...this.#infos.values()].filter((cluster) => cluster.state === "ready").sort((a, b) => a.shards.length - b.shards.length)[0];
12003
+ if (!target) {
12004
+ this.#emit("error", new Error(`No ready cluster can take over shards [${shardIds}] of cluster ${failed.id}.`));
12005
+ return;
12006
+ }
12007
+ try {
12008
+ await this.requestCluster(target.id, REQUEST_ASSIGN, { shardIds }, { timeoutMs: this.#options.startTimeout ?? 120000 });
12009
+ } catch (error) {
12010
+ this.#emit("error", new Error(`Cluster ${target.id} could not take over shards [${shardIds}]: ${error instanceof Error ? error.message : String(error)}`));
12011
+ return;
12012
+ }
12013
+ if (this.#stopping || failed.state !== "failed")
12014
+ return;
12015
+ for (const shard of shardIds)
12016
+ this.#owner.set(shard, target.id);
12017
+ target.shards.push(...shardIds);
12018
+ failed.shards = [];
12019
+ this.#emit("shardsReassigned", {
12020
+ shardIds,
12021
+ from: failed.id,
12022
+ to: target.id
12023
+ });
12024
+ }
12025
+ #clearStartTimer(id) {
12026
+ const timer = this.#startTimers.get(id);
12027
+ if (timer)
12028
+ clearTimeout(timer);
12029
+ this.#startTimers.delete(id);
12030
+ }
12031
+ #sendFrame(cluster, frame) {
12032
+ const child = cluster.process;
12033
+ if (!isLive(cluster.state) || typeof child.send !== "function" || !child.connected)
12034
+ return false;
12035
+ try {
12036
+ child.send(frame);
12037
+ return true;
12038
+ } catch {
12039
+ return false;
12040
+ }
12041
+ }
12042
+ #announceDown(exited, shards) {
12043
+ for (const [id, cluster] of this.clusters)
12044
+ if (id !== exited)
12045
+ this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
12046
+ }
12047
+ #onFrame(info, child, frame) {
12048
+ if (info.process !== child)
12049
+ return;
12050
+ if (!frame || typeof frame !== "object")
12051
+ return;
12052
+ if (BUS_FRAME in frame) {
12053
+ this.#onBus(info, frame[BUS_FRAME]);
12054
+ return;
12055
+ }
12056
+ if (CLUSTER_FRAME in frame) {
12057
+ const body = parseChildFrame(frame[CLUSTER_FRAME]);
12058
+ if (body)
12059
+ this.#onControl(info, child, body);
12060
+ else
12061
+ this.#emit("error", new Error(`Cluster ${info.id} sent a malformed control frame.`));
12062
+ }
12063
+ }
12064
+ #onControl(info, child, frame) {
12065
+ switch (frame.kind) {
12066
+ case "ready": {
12067
+ if (!frame.shardIds.every((id) => info.shards.includes(id))) {
12068
+ this.#emit("error", new Error(`Cluster ${info.id} reported shards it does not own: [${frame.shardIds}].`));
12069
+ return;
12070
+ }
12071
+ this.#clearStartTimer(info.id);
12072
+ info.state = "ready";
12073
+ info.readyAt = info.lastReadyAt = Date.now();
12074
+ this.#emit("clusterReady", info);
12075
+ this.#checkReady();
12076
+ return;
12077
+ }
12078
+ case "identify": {
12079
+ const reply = (body) => {
12080
+ if (info.process === child)
12081
+ this.#sendFrame(info, { [CLUSTER_FRAME]: body });
12082
+ };
12083
+ if (!this.#coordinator) {
12084
+ reply({
12085
+ kind: "identify-deny",
12086
+ id: frame.id,
12087
+ error: "IDENTIFY coordination is off."
12088
+ });
12089
+ return;
12090
+ }
12091
+ if (!info.shards.includes(frame.shardId)) {
12092
+ reply({
12093
+ kind: "identify-deny",
12094
+ id: frame.id,
12095
+ error: `Shard ${frame.shardId} does not belong to cluster ${info.id}.`
12096
+ });
12097
+ return;
12098
+ }
12099
+ let waiting = this.#identifyWaiting.get(info.id);
12100
+ if (!waiting)
12101
+ this.#identifyWaiting.set(info.id, waiting = new Set);
12102
+ if (waiting.has(frame.shardId)) {
12103
+ reply({
12104
+ kind: "identify-deny",
12105
+ id: frame.id,
12106
+ error: `Shard ${frame.shardId} is already waiting for a slot.`
12107
+ });
12108
+ return;
12109
+ }
12110
+ waiting.add(frame.shardId);
12111
+ const settled = () => {
12112
+ waiting.delete(frame.shardId);
12113
+ };
12114
+ this.#coordinator.acquire(info.id, frame.shardId).then(() => {
12115
+ settled();
12116
+ reply({ kind: "identify-grant", id: frame.id });
12117
+ }, (error) => {
12118
+ settled();
12119
+ reply({
12120
+ kind: "identify-deny",
12121
+ id: frame.id,
12122
+ error: error instanceof Error ? error.message : String(error)
12123
+ });
12124
+ });
12125
+ return;
12126
+ }
12127
+ case "identify-done":
12128
+ this.#coordinator?.release(info.id, frame.shardId);
12129
+ return;
12130
+ case "reply": {
12131
+ const tooBig = frame.error === undefined && this.#tooBig(frame.result);
12132
+ this.#tracker.answer(frame.id, `c:${info.id}`, info.id, {
12133
+ ...frame.error !== undefined ? { error: frame.error } : tooBig ? {
12134
+ error: `Cluster ${info.id} answered with more than ${this.#maxBytes} bytes.`
12135
+ } : { result: frame.result }
12136
+ });
12137
+ return;
12138
+ }
12139
+ case "health":
12140
+ if (this.#tooBig(frame.report)) {
12141
+ this.#emit("error", new Error(`Cluster ${info.id} sent a health report of more than ${this.#maxBytes} bytes.`));
12142
+ return;
12143
+ }
12144
+ info.report = frame.report;
12145
+ return;
12146
+ }
12147
+ }
12148
+ get #maxBytes() {
12149
+ return this.#options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
12150
+ }
12151
+ #tooBig(value) {
12152
+ return payloadSize(value) > this.#maxBytes;
12153
+ }
12154
+ #assertSize(data, what) {
12155
+ if (this.#tooBig(data))
12156
+ throw new RangeError(`${what} is larger than maxMessageBytes (${this.#maxBytes}).`);
12157
+ }
12158
+ #onBus(from, raw) {
12159
+ const message = raw;
12160
+ if (!message || typeof message !== "object" || typeof message.type !== "string" || typeof message.source !== "number" || message.target !== null && !Number.isInteger(message.target))
12161
+ return;
12162
+ if (this.#tooBig(message.data)) {
12163
+ this.#emit("error", new Error(`Cluster ${from.id} sent a shard-bus message of more than ${this.#maxBytes} bytes; it was not relayed.`));
12164
+ return;
12165
+ }
12166
+ const frame = { [BUS_FRAME]: message };
12167
+ if (message.target === PARENT_ID) {
12168
+ const reply = message.data;
12169
+ if (message.type === REPLY_TYPE && reply && typeof reply.requestId === "string")
12170
+ this.#tracker.answer(reply.requestId, `s:${message.source}`, from.id, reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result });
12171
+ return;
12172
+ }
12173
+ if (message.target === null) {
12174
+ for (const [id, cluster] of this.clusters)
12175
+ if (id !== from.id)
12176
+ this.#sendFrame(cluster, frame);
12177
+ return;
12178
+ }
12179
+ const owner = this.clusterForShard(message.target);
12180
+ if (!owner || owner.id === from.id)
12181
+ return;
12182
+ if (!this.#sendFrame(owner, frame))
12183
+ this.#sendFrame(from, {
12184
+ [PEER_DOWN_FRAME]: [message.target]
12185
+ });
12186
+ }
12187
+ async request(shardId, type, data, options = {}) {
12188
+ this.#assertSize(data, "The request data");
12189
+ const owner = this.clusterForShard(shardId);
12190
+ if (!owner)
12191
+ throw new RangeError(`Shard ${shardId} is not part of this deployment.`);
12192
+ const { id, done } = this.#tracker.begin({
12193
+ keys: new Map([[`s:${shardId}`, owner.id]]),
12194
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
12195
+ signal: options.signal
12196
+ });
12197
+ const sent = this.#sendFrame(owner, {
12198
+ [BUS_FRAME]: {
12199
+ source: PARENT_ID,
12200
+ target: shardId,
12201
+ type,
12202
+ data,
12203
+ id,
12204
+ expectsReply: true
12205
+ }
12206
+ });
12207
+ if (!sent) {
12208
+ this.#tracker.failOwner(owner.id, `Shard ${shardId} is down: cluster ${owner.id} is ${owner.state}.`);
12209
+ }
12210
+ const { replies } = await done;
12211
+ const reply = replies[0];
12212
+ if (!reply)
12213
+ throw new Error(`Shard ${shardId} did not reply to "${type}" in time.`);
12214
+ if (reply.error !== undefined)
12215
+ throw new Error(reply.error);
12216
+ return reply.result;
12217
+ }
12218
+ async broadcast(type, data, options = {}) {
12219
+ this.#assertSize(data, "The request data");
12220
+ const keys = new Map;
12221
+ const down = [];
12222
+ for (const [shard, clusterId] of this.#owner) {
12223
+ const cluster = this.#infos.get(clusterId);
12224
+ if (cluster && isLive(cluster.state))
12225
+ keys.set(`s:${shard}`, clusterId);
12226
+ else
12227
+ down.push(shard);
12228
+ }
12229
+ const { id, done } = this.#tracker.begin({
12230
+ keys,
12231
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
12232
+ signal: options.signal
12233
+ });
12234
+ const frame = {
12235
+ [BUS_FRAME]: {
12236
+ source: PARENT_ID,
12237
+ target: null,
12238
+ type,
12239
+ data,
12240
+ id,
12241
+ expectsReply: true
12242
+ }
12243
+ };
12244
+ for (const cluster of this.clusters.values())
12245
+ if (!this.#sendFrame(cluster, frame))
12246
+ this.#tracker.failOwner(cluster.id, `Cluster ${cluster.id} is ${cluster.state}.`);
12247
+ const { replies, missing } = await done;
12248
+ return {
12249
+ replies: replies.map((reply) => ({
12250
+ shardId: Number(reply.key.slice(2)),
12251
+ ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
12252
+ })),
12253
+ missing: [
12254
+ ...down,
12255
+ ...missing.map((key) => Number(key.slice(2)))
12256
+ ].sort((a, b) => a - b)
12257
+ };
12258
+ }
12259
+ async requestCluster(clusterId, type, data, options = {}) {
12260
+ this.#assertSize(data, "The request data");
12261
+ const cluster = this.#infos.get(clusterId);
12262
+ if (!cluster)
12263
+ throw new RangeError(`Cluster ${clusterId} does not exist.`);
12264
+ const { id, done } = this.#tracker.begin({
12265
+ keys: new Map([[`c:${clusterId}`, clusterId]]),
12266
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
12267
+ signal: options.signal
12268
+ });
12269
+ if (!this.#sendFrame(cluster, {
12270
+ [CLUSTER_FRAME]: { kind: "request", id, type, data }
12271
+ }))
12272
+ this.#tracker.failOwner(clusterId, `Cluster ${clusterId} is ${cluster.state}.`);
12273
+ const { replies } = await done;
12274
+ const reply = replies[0];
12275
+ if (!reply)
12276
+ throw new Error(`Cluster ${clusterId} did not reply to "${type}" in time.`);
12277
+ if (reply.error !== undefined)
12278
+ throw new Error(reply.error);
12279
+ return reply.result;
12280
+ }
12281
+ ready(timeoutMs) {
12282
+ if (this.#isReady())
12283
+ return Promise.resolve();
12284
+ return new Promise((resolve, reject) => {
12285
+ const waiter = {
12286
+ resolve: () => {
12287
+ if (waiter.timer)
12288
+ clearTimeout(waiter.timer);
12289
+ this.#readyWaiters.delete(waiter);
12290
+ resolve();
12291
+ },
12292
+ reject: (error) => {
12293
+ if (waiter.timer)
12294
+ clearTimeout(waiter.timer);
12295
+ this.#readyWaiters.delete(waiter);
12296
+ reject(error);
12297
+ }
12298
+ };
12299
+ if (timeoutMs !== undefined)
12300
+ waiter.timer = setTimeout(() => waiter.reject(new Error(`Clusters were not all ready within ${timeoutMs}ms.`)), timeoutMs);
12301
+ this.#readyWaiters.add(waiter);
12302
+ });
12303
+ }
12304
+ #isReady() {
12305
+ if (this.#infos.size === 0)
12306
+ return false;
12307
+ for (const cluster of this.#infos.values())
12308
+ if (cluster.state !== "ready")
12309
+ return false;
12310
+ return true;
12311
+ }
12312
+ #checkReady() {
12313
+ if (!this.#isReady())
12314
+ return;
12315
+ for (const waiter of [...this.#readyWaiters])
12316
+ waiter.resolve();
12317
+ if (!this.#allReady) {
12318
+ this.#allReady = true;
12319
+ this.#emit("ready");
12320
+ }
12321
+ }
12322
+ health() {
12323
+ const now = Date.now();
12324
+ const clusters = [...this.#infos.values()].sort((a, b) => a.id - b.id).map((cluster) => ({
12325
+ id: cluster.id,
12326
+ state: cluster.state,
12327
+ ...cluster.process.pid !== undefined && isLive(cluster.state) ? { pid: cluster.process.pid } : {},
12328
+ host: cluster.host,
12329
+ shardIds: [...cluster.shards],
12330
+ uptimeMs: isLive(cluster.state) ? now - cluster.startedAt : 0,
12331
+ restarts: cluster.restarts,
12332
+ startedAt: cluster.startedAt,
12333
+ ...cluster.readyAt !== undefined ? { readyAt: cluster.readyAt } : {},
12334
+ ...cluster.lastReadyAt !== undefined ? { lastReadyAt: cluster.lastReadyAt } : {},
12335
+ shards: cluster.report?.shards ?? [],
12336
+ ...cluster.report?.metrics ? { metrics: cluster.report.metrics } : {},
12337
+ ...cluster.report ? { reportedAt: cluster.report.reportedAt } : {}
12338
+ }));
12339
+ const missingShards = [...this.#owner].filter(([, id]) => this.#infos.get(id)?.state !== "ready").map(([shard]) => shard).sort((a, b) => a - b);
12340
+ const states = clusters.map((cluster) => cluster.state);
12341
+ const state = this.#stopping ? "stopping" : clusters.length === 0 ? this.#wasStarted ? "stopped" : "idle" : states.every((s) => s === "stopped") ? "stopped" : states.every((s) => s === "ready") ? "ready" : states.some((s) => s === "restarting" || s === "failed") ? "degraded" : "starting";
12342
+ return {
12343
+ state,
12344
+ totalShards: this.shardCount,
12345
+ clusters,
12346
+ readyClusters: states.filter((s) => s === "ready").length,
12347
+ missingShards
12348
+ };
12349
+ }
12350
+ async refreshHealth(options = {}) {
12351
+ await Promise.all([...this.clusters.values()].map(async (cluster) => {
12352
+ try {
12353
+ cluster.report = await this.requestCluster(cluster.id, REQUEST_HEALTH, { metrics: options.metrics === true }, { timeoutMs: options.timeoutMs ?? 2000 });
12354
+ } catch {}
12355
+ }));
12356
+ return this.health();
12357
+ }
12358
+ async checkAutoScale() {
12359
+ if (!this.#auto)
12360
+ return;
12361
+ try {
12362
+ const recommended = await this.fetchRecommendedShardCount();
12363
+ if (recommended !== this.shardCount) {
12364
+ await this.respawn(recommended);
12365
+ }
12366
+ } catch (error) {
12367
+ const normalized = error instanceof Error ? error : new Error(String(error));
12368
+ this.#options.onAutoScaleError?.(normalized);
12369
+ }
12370
+ }
12371
+ async respawn(newShardCount) {
12372
+ this.#options.shardCount = newShardCount;
12373
+ await this.#stop(this.#options.shutdownTimeout ?? 5000, true);
12374
+ this.#supervisor.reset();
12375
+ await this.spawn();
12376
+ }
12377
+ shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
12378
+ return this.#stop(timeoutMs, false);
12379
+ }
12380
+ #stop(timeoutMs, keepRemote) {
12381
+ this.#stopPromise ??= this.#shutdown(timeoutMs, keepRemote).finally(() => {
12382
+ this.#stopPromise = undefined;
12383
+ });
12384
+ return this.#stopPromise;
12385
+ }
12386
+ async#shutdown(timeoutMs, keepRemote) {
12387
+ this.#stopping = true;
12388
+ this.#halt();
12389
+ await this.#startPromise?.catch(() => {
12390
+ return;
12391
+ });
12392
+ await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
12393
+ for (const cluster of this.#infos.values())
12394
+ cluster.state = "stopped";
12395
+ this.clusters.clear();
12396
+ this.#startPromise = undefined;
12397
+ if (!keepRemote)
12398
+ this.#remote?.stop();
12399
+ this.#stopping = false;
12400
+ }
12401
+ #halt() {
12402
+ for (const timer of this.#restartTimers)
12403
+ clearTimeout(timer);
12404
+ this.#restartTimers.clear();
12405
+ for (const id of [...this.#startTimers.keys()])
12406
+ this.#clearStartTimer(id);
12407
+ for (const wake of [...this.#pauses])
12408
+ wake();
12409
+ if (this.#autoScaleTimer) {
12410
+ clearInterval(this.#autoScaleTimer);
12411
+ this.#autoScaleTimer = undefined;
12412
+ }
12413
+ this.#coordinator?.close();
12414
+ this.#coordinator = undefined;
12415
+ this.#remote?.abortWaits();
12416
+ this.#tracker.close();
12417
+ for (const waiter of [...this.#readyWaiters])
12418
+ waiter.reject(new Error("The cluster manager is stopping."));
12419
+ }
12420
+ async#shutdownCluster(cluster, timeoutMs) {
12421
+ const child = cluster.process;
12422
+ cluster.state = "stopping";
12423
+ if (child.exitCode !== null || child.signalCode !== null)
12424
+ return;
12425
+ await new Promise((resolve) => {
12426
+ let timer;
10722
12427
  const finish = () => {
10723
12428
  if (timer)
10724
12429
  clearTimeout(timer);
@@ -10741,24 +12446,293 @@ class ClusterManager {
10741
12446
  }
10742
12447
  killAll() {
10743
12448
  this.#stopping = true;
10744
- this.#clearRestartTimers();
10745
- if (this.#autoScaleTimer) {
10746
- clearInterval(this.#autoScaleTimer);
10747
- this.#autoScaleTimer = undefined;
10748
- }
10749
- for (const cluster of this.clusters.values()) {
12449
+ this.#halt();
12450
+ for (const cluster of this.#infos.values()) {
12451
+ cluster.state = "stopped";
10750
12452
  try {
10751
12453
  cluster.process.kill();
10752
12454
  } catch {}
10753
12455
  }
10754
12456
  this.clusters.clear();
10755
- this.#spawned = false;
12457
+ this.#startPromise = undefined;
12458
+ this.#remote?.stop();
10756
12459
  this.#stopping = false;
10757
12460
  }
10758
12461
  }
12462
+ // packages/sharding/src/host.ts
12463
+ import { fork as fork2 } from "child_process";
12464
+ class ClusterHost {
12465
+ #options;
12466
+ #running = new Map;
12467
+ #listeners = new Map;
12468
+ #socket;
12469
+ #lease = 15000;
12470
+ #lastRx = 0;
12471
+ #watchdog;
12472
+ #retry;
12473
+ #delay;
12474
+ #closed = true;
12475
+ #connected = false;
12476
+ #firstConnect;
12477
+ constructor(options) {
12478
+ if (!HOST_ID.test(options.id))
12479
+ throw new RangeError("id must be 1-64 letters, digits, '.', '_' or '-'.");
12480
+ if (!options.secret || options.secret.length < 16)
12481
+ throw new RangeError("secret must be at least 16 characters.");
12482
+ if (!options.script?.trim())
12483
+ throw new TypeError("A worker script path is required.");
12484
+ let url;
12485
+ try {
12486
+ url = new URL(options.url);
12487
+ } catch {
12488
+ throw new TypeError("url must be an absolute ws:// or wss:// URL.");
12489
+ }
12490
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
12491
+ throw new TypeError("url must start with ws:// or wss://.");
12492
+ if (url.username || url.password)
12493
+ throw new TypeError("url must not contain credentials.");
12494
+ if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
12495
+ throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
12496
+ this.#options = options;
12497
+ this.#delay = options.reconnectDelay ?? 1000;
12498
+ }
12499
+ get connected() {
12500
+ return this.#connected;
12501
+ }
12502
+ clusters() {
12503
+ return [...this.#running.keys()].sort((a, b) => a - b);
12504
+ }
12505
+ on(event, listener) {
12506
+ let set = this.#listeners.get(event);
12507
+ if (!set)
12508
+ this.#listeners.set(event, set = new Set);
12509
+ set.add(listener);
12510
+ return this;
12511
+ }
12512
+ off(event, listener) {
12513
+ this.#listeners.get(event)?.delete(listener);
12514
+ return this;
12515
+ }
12516
+ #emit(event, ...args) {
12517
+ for (const listener of this.#listeners.get(event) ?? [])
12518
+ try {
12519
+ listener(...args);
12520
+ } catch {}
12521
+ }
12522
+ connect() {
12523
+ if (!this.#closed)
12524
+ return Promise.resolve();
12525
+ this.#closed = false;
12526
+ return new Promise((resolve, reject) => {
12527
+ this.#firstConnect = { resolve, reject };
12528
+ this.#open();
12529
+ });
12530
+ }
12531
+ close() {
12532
+ this.#closed = true;
12533
+ if (this.#retry)
12534
+ clearTimeout(this.#retry);
12535
+ this.#retry = undefined;
12536
+ this.#fence();
12537
+ if (this.#watchdog)
12538
+ clearInterval(this.#watchdog);
12539
+ this.#watchdog = undefined;
12540
+ this.#connected = false;
12541
+ const socket = this.#socket;
12542
+ this.#socket = undefined;
12543
+ this.#firstConnect?.reject(new Error("The host was closed."));
12544
+ this.#firstConnect = undefined;
12545
+ if (!socket || socket.readyState === WebSocket.CLOSED)
12546
+ return Promise.resolve();
12547
+ return new Promise((resolve) => {
12548
+ socket.addEventListener("close", () => resolve(), { once: true });
12549
+ try {
12550
+ socket.close(1000, "Host closed.");
12551
+ } catch {
12552
+ resolve();
12553
+ }
12554
+ });
12555
+ }
12556
+ #open() {
12557
+ const socket = new WebSocket(this.#options.url);
12558
+ this.#socket = socket;
12559
+ socket.addEventListener("message", (event) => this.#message(socket, event.data));
12560
+ socket.addEventListener("error", () => {});
12561
+ socket.addEventListener("close", (event) => {
12562
+ if (this.#socket === socket)
12563
+ this.#lost(event.code, event.reason);
12564
+ });
12565
+ }
12566
+ #send(socket, message) {
12567
+ if (socket.readyState !== WebSocket.OPEN)
12568
+ return false;
12569
+ try {
12570
+ socket.send(JSON.stringify(message));
12571
+ return true;
12572
+ } catch {
12573
+ return false;
12574
+ }
12575
+ }
12576
+ #message(socket, data) {
12577
+ if (this.#socket !== socket)
12578
+ return;
12579
+ const message = parseToHost(data);
12580
+ if (!message) {
12581
+ this.#emit("error", new Error("The coordinator sent a malformed frame."));
12582
+ return;
12583
+ }
12584
+ this.#lastRx = Date.now();
12585
+ switch (message.t) {
12586
+ case "challenge":
12587
+ if (message.version !== REMOTE_PROTOCOL_VERSION) {
12588
+ socket.close(RemoteClose.VersionMismatch, "Protocol version mismatch.");
12589
+ return;
12590
+ }
12591
+ this.#send(socket, {
12592
+ t: "hello",
12593
+ id: this.#options.id,
12594
+ mac: signChallenge(this.#options.secret, message.nonce, this.#options.id),
12595
+ version: REMOTE_PROTOCOL_VERSION
12596
+ });
12597
+ return;
12598
+ case "welcome":
12599
+ this.#lease = message.leaseMs;
12600
+ this.#connected = true;
12601
+ this.#delay = this.#options.reconnectDelay ?? 1000;
12602
+ this.#startWatchdog();
12603
+ this.#firstConnect?.resolve();
12604
+ this.#firstConnect = undefined;
12605
+ this.#emit("connected");
12606
+ return;
12607
+ case "ping":
12608
+ this.#send(socket, { t: "pong", n: message.n });
12609
+ return;
12610
+ case "bye":
12611
+ this.#fence();
12612
+ return;
12613
+ case "spawn":
12614
+ this.#spawn(socket, message.cluster, message.epoch, message.env);
12615
+ return;
12616
+ case "frame": {
12617
+ const run = this.#running.get(message.cluster);
12618
+ if (run?.epoch === message.epoch && run.child.connected)
12619
+ try {
12620
+ run.child.send(message.frame);
12621
+ } catch {}
12622
+ return;
12623
+ }
12624
+ case "kill": {
12625
+ const run = this.#running.get(message.cluster);
12626
+ if (run?.epoch === message.epoch && KILL_SIGNALS.has(message.signal))
12627
+ try {
12628
+ run.child.kill(message.signal);
12629
+ } catch {}
12630
+ return;
12631
+ }
12632
+ }
12633
+ }
12634
+ #spawn(socket, cluster, epoch, env) {
12635
+ const reply = (message) => {
12636
+ if (this.#socket === socket)
12637
+ this.#send(socket, message);
12638
+ };
12639
+ if (this.#running.has(cluster)) {
12640
+ reply({
12641
+ t: "error",
12642
+ cluster,
12643
+ epoch,
12644
+ message: `Cluster ${cluster} is already running here.`
12645
+ });
12646
+ return;
12647
+ }
12648
+ let child;
12649
+ try {
12650
+ const forkWorker = this.#options.fork ?? defaultFork;
12651
+ child = forkWorker(this.#options.script, {
12652
+ ...process.env,
12653
+ ...env
12654
+ });
12655
+ } catch (error) {
12656
+ reply({
12657
+ t: "error",
12658
+ cluster,
12659
+ epoch,
12660
+ message: error instanceof Error ? error.message : String(error)
12661
+ });
12662
+ return;
12663
+ }
12664
+ this.#running.set(cluster, { epoch, child });
12665
+ child.on("message", (frame) => reply({ t: "frame", cluster, epoch, frame }));
12666
+ child.once("exit", (code, signal) => {
12667
+ if (this.#running.get(cluster)?.child === child)
12668
+ this.#running.delete(cluster);
12669
+ this.#emit("clusterExit", cluster, code, signal);
12670
+ reply({ t: "exit", cluster, epoch, code, signal });
12671
+ });
12672
+ child.on("error", (error) => this.#emit("error", error instanceof Error ? error : new Error(String(error))));
12673
+ reply({
12674
+ t: "spawned",
12675
+ cluster,
12676
+ epoch,
12677
+ ...child.pid ? { pid: child.pid } : {}
12678
+ });
12679
+ this.#emit("clusterSpawn", cluster);
12680
+ }
12681
+ #fence() {
12682
+ for (const { child } of [...this.#running.values()])
12683
+ try {
12684
+ child.kill("SIGKILL");
12685
+ } catch {}
12686
+ }
12687
+ #startWatchdog() {
12688
+ if (this.#watchdog)
12689
+ clearInterval(this.#watchdog);
12690
+ this.#lastRx = Date.now();
12691
+ this.#watchdog = setInterval(() => {
12692
+ if (Date.now() - this.#lastRx > this.#lease) {
12693
+ this.#fence();
12694
+ this.#socket?.close(RemoteClose.HostLost, "Coordinator silent.");
12695
+ }
12696
+ }, Math.max(10, Math.floor(this.#lease / 3)));
12697
+ this.#watchdog.unref?.();
12698
+ }
12699
+ #lost(code, reason) {
12700
+ if (this.#watchdog)
12701
+ clearInterval(this.#watchdog);
12702
+ this.#watchdog = undefined;
12703
+ this.#socket = undefined;
12704
+ const wasConnected = this.#connected;
12705
+ this.#connected = false;
12706
+ this.#fence();
12707
+ if (wasConnected)
12708
+ this.#emit("disconnected", { code, reason });
12709
+ if (this.#closed)
12710
+ return;
12711
+ const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
12712
+ const first = this.#firstConnect;
12713
+ if (first) {
12714
+ this.#firstConnect = undefined;
12715
+ this.#closed = true;
12716
+ first.reject(new Error(`The coordinator refused this host (${code}${reason ? `: ${reason}` : ""}).`));
12717
+ return;
12718
+ }
12719
+ if (refused)
12720
+ this.#emit("error", new Error(`The coordinator refused this host (${code}: ${reason}).`));
12721
+ const wait = Math.round(this.#delay * (1 + Math.random() * 0.2));
12722
+ this.#delay = Math.min(this.#options.maxReconnectDelay ?? 30000, this.#delay * 2);
12723
+ this.#retry = setTimeout(() => {
12724
+ this.#retry = undefined;
12725
+ if (!this.#closed)
12726
+ this.#open();
12727
+ }, wait);
12728
+ }
12729
+ }
12730
+ function defaultFork(script, env) {
12731
+ return fork2(script, [], { env });
12732
+ }
10759
12733
 
10760
12734
  // packages/sharding/src/index.ts
10761
- var sleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12735
+ var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10762
12736
  function needsConnection(gateway) {
10763
12737
  return gateway.state === "CONNECT" /* Connect */ || gateway.state === "RECONNECT" /* Reconnect */ || gateway.state === "CLOSED" /* Closed */;
10764
12738
  }
@@ -10792,9 +12766,17 @@ class ShardManager {
10792
12766
  static IDENTIFY_INTERVAL = 5000;
10793
12767
  shards = new Map;
10794
12768
  get shardCount() {
10795
- return this.shards.size;
12769
+ return this.#resolved ? this.#total : 0;
12770
+ }
12771
+ get shardIds() {
12772
+ return [...this.shards.keys()].sort((a, b) => a - b);
10796
12773
  }
10797
12774
  #options;
12775
+ #ids;
12776
+ #gate;
12777
+ #worker;
12778
+ #lifecycle = new Map;
12779
+ #announced = false;
10798
12780
  #resolved = false;
10799
12781
  #destroyed = false;
10800
12782
  #auto;
@@ -10807,9 +12789,30 @@ class ShardManager {
10807
12789
  throw new TypeError("A Discord bot token is required.");
10808
12790
  if (!Array.isArray(options.intents))
10809
12791
  throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds].");
10810
- const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
12792
+ const context = options.cluster === false ? null : options.cluster ?? readClusterContext();
12793
+ if (context) {
12794
+ if (options.shardCount === "auto")
12795
+ throw new RangeError(`shardCount "auto" is the ClusterManager's decision inside a cluster worker.`);
12796
+ if (options.shardCount !== undefined && options.shardCount !== context.shardCount)
12797
+ throw new RangeError(`shardCount ${options.shardCount} differs from the cluster's ${context.shardCount}.`);
12798
+ if (options.autoScaleInterval !== undefined)
12799
+ throw new TypeError("autoScaleInterval belongs to ClusterManager inside a cluster worker.");
12800
+ if (options.shardIds && !options.shardIds.every((id) => context.shardIds.includes(id)))
12801
+ throw new RangeError("shardIds must be a subset of the cluster's own shards.");
12802
+ }
12803
+ const count = context ? context.shardCount : options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
10811
12804
  if (!Number.isInteger(count) || count < 1)
10812
12805
  throw new RangeError("Shard count must be a positive integer.");
12806
+ if (options.guildsPerShard !== undefined) {
12807
+ assertGuildsPerShard(options.guildsPerShard);
12808
+ if (options.shardCount !== "auto")
12809
+ throw new RangeError('guildsPerShard needs shardCount: "auto".');
12810
+ }
12811
+ this.#ids = options.shardIds ?? context?.shardIds;
12812
+ if (this.#ids && options.shardCount !== "auto")
12813
+ validateShardIds(this.#ids, count);
12814
+ this.#worker = options.worker === false ? undefined : options.worker ?? (context && options.cluster === undefined ? ClusterWorker.current() : undefined);
12815
+ this.#gate = options.identify === false ? undefined : options.identify ?? (context?.coordinatedIdentify ? this.#worker?.identifyGate : undefined);
10813
12816
  if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
10814
12817
  throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
10815
12818
  if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
@@ -10824,11 +12827,19 @@ class ShardManager {
10824
12827
  spawnDelay: ShardManager.IDENTIFY_INTERVAL,
10825
12828
  ...options
10826
12829
  };
12830
+ this.#worker?.onRequest(REQUEST_HEALTH, (data) => this.report(data?.metrics === true));
12831
+ this.#worker?.onRequest(REQUEST_ASSIGN, async (data) => {
12832
+ const ids = data?.shardIds;
12833
+ if (!Array.isArray(ids))
12834
+ throw new TypeError("shardIds must be an array.");
12835
+ await this.addShards(ids);
12836
+ return { shardIds: ids };
12837
+ });
10827
12838
  if (options.shardCount !== "auto")
10828
12839
  this.#initialize(count);
10829
12840
  }
10830
12841
  async fetchRecommendedShardCount() {
10831
- return (await this.fetchGatewayInfo()).shards;
12842
+ return resolveShardCount(this.#options.token, await this.fetchGatewayInfo(), this.#options);
10832
12843
  }
10833
12844
  async fetchGatewayInfo() {
10834
12845
  const info = await fetchGatewayBot(this.#options.token);
@@ -10841,8 +12852,63 @@ class ShardManager {
10841
12852
  this.#autoScaleTimer = undefined;
10842
12853
  }
10843
12854
  await this.#ensureInitialized();
12855
+ await this.#start([...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id));
12856
+ if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
12857
+ this.#autoScaleTimer = setInterval(() => {
12858
+ this.checkAutoScale();
12859
+ }, this.#options.autoScaleInterval);
12860
+ }
12861
+ }
12862
+ async addShards(shardIds) {
12863
+ await this.#ensureInitialized();
12864
+ validateShardIds(shardIds, this.#total);
12865
+ for (const id of shardIds)
12866
+ if (this.shards.has(id))
12867
+ throw new RangeError(`Shard ${id} is already owned.`);
12868
+ for (const id of shardIds)
12869
+ this.shards.set(id, this.#createShard(id, this.#total));
12870
+ if (this.#ids)
12871
+ this.#ids = [...this.#ids, ...shardIds];
12872
+ this.#announced = false;
12873
+ try {
12874
+ await this.#start([...shardIds], false);
12875
+ } catch (error) {
12876
+ for (const id of shardIds) {
12877
+ this.shards.get(id)?.close();
12878
+ this.shards.delete(id);
12879
+ this.#lifecycle.delete(id);
12880
+ }
12881
+ if (this.#ids)
12882
+ this.#ids = this.#ids.filter((id) => !shardIds.includes(id));
12883
+ throw error;
12884
+ }
12885
+ }
12886
+ async#start(ids, destroyOnFailure = true) {
12887
+ const gate = this.#gate;
12888
+ if (gate) {
12889
+ const timeout = this.#options.handshakeTimeout ?? 15000;
12890
+ const results = await Promise.allSettled(ids.map(async (id) => {
12891
+ const gateway = this.shards.get(id);
12892
+ this.#setStatus(id, "connecting");
12893
+ await gate.acquire(id);
12894
+ try {
12895
+ await gateway.connect();
12896
+ await handshakeSent(gateway, timeout);
12897
+ } finally {
12898
+ gate.release(id);
12899
+ }
12900
+ }));
12901
+ const failed = results.findIndex((result) => result.status === "rejected");
12902
+ if (failed !== -1) {
12903
+ if (destroyOnFailure)
12904
+ this.destroy();
12905
+ throw new Error(`Failed to connect shard ${ids[failed]}.`, {
12906
+ cause: results[failed].reason
12907
+ });
12908
+ }
12909
+ return;
12910
+ }
10844
12911
  const limit = this.#gatewayInfo?.sessionStartLimit;
10845
- const ids = [...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id);
10846
12912
  if (limit && limit.remaining < ids.length)
10847
12913
  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.`);
10848
12914
  const concurrency = Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
@@ -10858,24 +12924,22 @@ class ShardManager {
10858
12924
  const rounds = Math.max(0, ...[...buckets.values()].map((bucket) => bucket.length));
10859
12925
  for (let index = 0;index < rounds; index++) {
10860
12926
  const round = [...buckets.values()].map((bucket) => bucket[index]).filter((id) => id !== undefined);
12927
+ for (const id of round)
12928
+ this.#setStatus(id, "connecting");
10861
12929
  const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
10862
12930
  const failed = results.findIndex((result) => result.status === "rejected");
10863
12931
  if (failed !== -1) {
10864
- this.destroy();
12932
+ if (destroyOnFailure)
12933
+ this.destroy();
10865
12934
  throw new Error(`Failed to connect shard ${round[failed]}.`, {
10866
12935
  cause: results[failed].reason
10867
12936
  });
10868
12937
  }
10869
12938
  if (index + 1 < rounds && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
10870
12939
  await Promise.all(round.map((id) => handshakeSent(this.shards.get(id), this.#options.handshakeTimeout ?? 15000)));
10871
- await sleep4(this.#options.spawnDelay);
12940
+ await sleep3(this.#options.spawnDelay);
10872
12941
  }
10873
12942
  }
10874
- if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
10875
- this.#autoScaleTimer = setInterval(() => {
10876
- this.checkAutoScale();
10877
- }, this.#options.autoScaleInterval);
10878
- }
10879
12943
  }
10880
12944
  async checkAutoScale() {
10881
12945
  if (!this.#auto)
@@ -10897,7 +12961,11 @@ class ShardManager {
10897
12961
  destroy() {
10898
12962
  for (const shard of this.shards.values())
10899
12963
  shard.close();
12964
+ for (const id of this.shards.keys())
12965
+ this.#setStatus(id, "destroyed");
10900
12966
  this.shards.clear();
12967
+ this.#lifecycle.clear();
12968
+ this.#announced = false;
10901
12969
  this.#resolved = false;
10902
12970
  this.#destroyed = true;
10903
12971
  if (this.#autoScaleTimer) {
@@ -10933,11 +13001,60 @@ class ShardManager {
10933
13001
  return this.shards.get(id);
10934
13002
  }
10935
13003
  health() {
10936
- return [...this.shards].map(([id, gateway]) => ({
10937
- id,
10938
- state: gateway.state,
10939
- ping: gateway.ping
10940
- }));
13004
+ return [...this.shards].map(([id, gateway]) => {
13005
+ const life = this.#lifecycle.get(id);
13006
+ return {
13007
+ id,
13008
+ state: gateway.state,
13009
+ ping: gateway.ping,
13010
+ status: life?.status ?? "idle",
13011
+ ...life?.readyAt !== undefined ? { readyAt: life.readyAt } : {},
13012
+ ...life?.disconnectedAt !== undefined ? { disconnectedAt: life.disconnectedAt } : {}
13013
+ };
13014
+ });
13015
+ }
13016
+ report(metrics = false) {
13017
+ let numbers;
13018
+ if (metrics) {
13019
+ const memory = process.memoryUsage();
13020
+ const cpu = process.cpuUsage();
13021
+ numbers = {
13022
+ rssBytes: memory.rss,
13023
+ heapUsedBytes: memory.heapUsed,
13024
+ cpuUserMicros: cpu.user,
13025
+ cpuSystemMicros: cpu.system
13026
+ };
13027
+ }
13028
+ return {
13029
+ shards: this.health(),
13030
+ ...numbers ? { metrics: numbers } : {},
13031
+ reportedAt: Date.now()
13032
+ };
13033
+ }
13034
+ #setStatus(id, status) {
13035
+ const life = this.#lifecycle.get(id) ?? { status };
13036
+ life.status = status;
13037
+ const now = Date.now();
13038
+ if (status === "ready")
13039
+ life.readyAt = now;
13040
+ if (status === "disconnected")
13041
+ life.disconnectedAt = now;
13042
+ this.#lifecycle.set(id, life);
13043
+ if (status !== "ready")
13044
+ this.#announced = false;
13045
+ else
13046
+ this.#maybeAnnounce();
13047
+ }
13048
+ #maybeAnnounce() {
13049
+ if (this.#announced || this.shards.size === 0)
13050
+ return;
13051
+ for (const id of this.shards.keys())
13052
+ if (this.#lifecycle.get(id)?.status !== "ready")
13053
+ return;
13054
+ this.#announced = true;
13055
+ this.#emit("ready");
13056
+ this.#worker?.ready(this.shardIds);
13057
+ this.#worker?.reportHealth(this.report());
10941
13058
  }
10942
13059
  values() {
10943
13060
  return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
@@ -10954,7 +13071,9 @@ class ShardManager {
10954
13071
  if (this.#resolved)
10955
13072
  return;
10956
13073
  this.#total = count;
10957
- for (let id = 0;id < count; id++)
13074
+ const ids = this.#ids ?? Array.from({ length: count }, (_, i) => i);
13075
+ validateShardIds(ids, count);
13076
+ for (const id of ids)
10958
13077
  this.shards.set(id, this.#createShard(id, count));
10959
13078
  this.#resolved = true;
10960
13079
  }
@@ -10966,11 +13085,23 @@ class ShardManager {
10966
13085
  shardCount: count,
10967
13086
  reconnect: this.#options.reconnect ?? true
10968
13087
  });
10969
- gateway.on("ready", () => this.#emit("shardReady", id));
10970
- gateway.on("resumed", () => this.#emit("shardResume", id));
10971
- gateway.on("close", (close) => this.#emit("shardDisconnect", id, close));
13088
+ gateway.on("ready", () => {
13089
+ this.#setStatus(id, "ready");
13090
+ this.#emit("shardReady", id);
13091
+ });
13092
+ gateway.on("resumed", () => {
13093
+ this.#setStatus(id, "ready");
13094
+ this.#emit("shardResume", id);
13095
+ });
13096
+ gateway.on("close", (close) => {
13097
+ this.#setStatus(id, "disconnected");
13098
+ this.#emit("shardDisconnect", id, close);
13099
+ });
10972
13100
  gateway.on("error", (error) => this.#emit("shardError", id, error));
10973
- gateway.on("unstable", (info) => this.#emit("shardUnstable", id, info));
13101
+ gateway.on("unstable", (info) => {
13102
+ this.#setStatus(id, "unstable");
13103
+ this.#emit("shardUnstable", id, info);
13104
+ });
10974
13105
  return gateway;
10975
13106
  }
10976
13107
  }
@@ -11406,7 +13537,9 @@ export {
11406
13537
  ChannelManager,
11407
13538
  Client,
11408
13539
  ClientEvent,
13540
+ ClusterHost,
11409
13541
  ClusterManager,
13542
+ ClusterWorker,
11410
13543
  Collection,
11411
13544
  Collector,
11412
13545
  CommandInteraction,
@@ -11485,6 +13618,8 @@ export {
11485
13618
  HandlerRegistry,
11486
13619
  HttpTransport,
11487
13620
  IDENTIFY_CLOSE_CODES,
13621
+ IDENTIFY_INTERVAL,
13622
+ IdentifyCoordinator,
11488
13623
  IntentBits,
11489
13624
  Intents,
11490
13625
  IntentsBitField,
@@ -11494,6 +13629,7 @@ export {
11494
13629
  InteractionResponseEnum,
11495
13630
  Invite,
11496
13631
  IpcTransport,
13632
+ MAX_GUILDS_PER_SHARD,
11497
13633
  Manager,
11498
13634
  MediaChannel,
11499
13635
  MemberHandle,
@@ -11516,12 +13652,15 @@ export {
11516
13652
  PermissionsBitField,
11517
13653
  PlainTextDecoder,
11518
13654
  PremiumTier,
13655
+ REMOTE_PROTOCOL_VERSION,
11519
13656
  REST,
11520
13657
  RESTError,
11521
13658
  RESTErrorCode,
11522
13659
  RateLimiter,
11523
13660
  RedisRateLimitStore,
13661
+ RemoteClose,
11524
13662
  RequestScheduler,
13663
+ RequestTracker,
11525
13664
  ResourceManager,
11526
13665
  ResponseDecoder,
11527
13666
  Role,
@@ -11562,7 +13701,9 @@ export {
11562
13701
  WelcomeScreenChannel,
11563
13702
  ZlibStreamDecoder,
11564
13703
  abortError,
13704
+ allocateShards,
11565
13705
  argsFromCommandOptions,
13706
+ assertCompleteAllocation,
11566
13707
  blockQuote,
11567
13708
  bold,
11568
13709
  bulletList,
@@ -11589,11 +13730,13 @@ export {
11589
13730
  entersState,
11590
13731
  escapeMarkdown,
11591
13732
  fetchGatewayBot,
13733
+ fetchGuildCount,
11592
13734
  getVoiceConnection,
11593
13735
  heading,
11594
13736
  heartbeatPayload,
11595
13737
  identifyPayload,
11596
13738
  inlineCode,
13739
+ isClusterWorker,
11597
13740
  isSnowflake,
11598
13741
  italic,
11599
13742
  joinVoiceChannel,
@@ -11610,12 +13753,14 @@ export {
11610
13753
  parseRoleMention,
11611
13754
  parseUserMention,
11612
13755
  randomInt,
13756
+ readClusterContext,
11613
13757
  redactPath,
11614
13758
  resolveGatewayIntents,
11615
13759
  resumePayload,
11616
13760
  roleMention,
11617
13761
  scopeBucket,
11618
13762
  serializeQuery,
13763
+ shardsForGuilds,
11619
13764
  sleep,
11620
13765
  spoiler,
11621
13766
  stickerURL,
@@ -11625,7 +13770,8 @@ export {
11625
13770
  toRequest,
11626
13771
  tokenizeArgs,
11627
13772
  underline,
11628
- userMention
13773
+ userMention,
13774
+ validateShardIds
11629
13775
  };
11630
13776
 
11631
- //# debugId=AF3705D1EBC708FB64756E2164756E21
13777
+ //# debugId=6F25911C0DD073ED64756E2164756E21