lunibee 0.3.0 → 0.3.2

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 (87) hide show
  1. package/dist/builders/attachment.d.ts +1 -0
  2. package/dist/builders/clone.d.ts +10 -0
  3. package/dist/builders/commands.d.ts +1 -1
  4. package/dist/builders/components.d.ts +24 -5
  5. package/dist/builders/embed.d.ts +1 -0
  6. package/dist/builders/index.js +68 -17
  7. package/dist/builders/index.js.map +10 -9
  8. package/dist/collection/index.d.ts +1 -0
  9. package/dist/collection/index.js +297 -19
  10. package/dist/collection/index.js.map +5 -4
  11. package/dist/collection/redis.d.ts +98 -0
  12. package/dist/core/collector.d.ts +8 -0
  13. package/dist/core/events.d.ts +1 -0
  14. package/dist/core/index.d.ts +3 -0
  15. package/dist/core/index.js +6252 -4704
  16. package/dist/core/index.js.map +47 -40
  17. package/dist/core/permissions.d.ts +3 -402
  18. package/dist/core/redis-cache.d.ts +85 -0
  19. package/dist/formatters/index.d.ts +5 -1
  20. package/dist/formatters/index.js +7 -3
  21. package/dist/formatters/index.js.map +3 -3
  22. package/dist/handlers/index.js +11 -2
  23. package/dist/handlers/index.js.map +3 -3
  24. package/dist/index.js +10481 -6148
  25. package/dist/index.js.map +70 -56
  26. package/dist/managers/application.d.ts +10 -6
  27. package/dist/managers/base.d.ts +4 -1
  28. package/dist/managers/guild.d.ts +3 -1
  29. package/dist/managers/index.d.ts +6 -1
  30. package/dist/managers/index.js +365 -103
  31. package/dist/managers/index.js.map +19 -22
  32. package/dist/managers/member.d.ts +3 -1
  33. package/dist/managers/message.d.ts +13 -4
  34. package/dist/managers/thread.d.ts +3 -3
  35. package/dist/rest/decoder.d.ts +2 -0
  36. package/dist/rest/errors.d.ts +13 -2
  37. package/dist/rest/headers.d.ts +31 -0
  38. package/dist/rest/identity.d.ts +31 -0
  39. package/dist/rest/index.d.ts +99 -6
  40. package/dist/rest/index.js +1412 -231
  41. package/dist/rest/index.js.map +16 -13
  42. package/dist/rest/limiter.d.ts +75 -10
  43. package/dist/rest/redis.d.ts +103 -22
  44. package/dist/rest/scheduler.d.ts +13 -0
  45. package/dist/rest/store.d.ts +135 -12
  46. package/dist/rest/telemetry.d.ts +151 -0
  47. package/dist/rest/transport.d.ts +9 -0
  48. package/dist/rest/webhook.d.ts +18 -6
  49. package/dist/sharding/bus.d.ts +12 -2
  50. package/dist/sharding/cluster.d.ts +225 -15
  51. package/dist/sharding/context.d.ts +41 -0
  52. package/dist/sharding/gateway-bot.d.ts +20 -0
  53. package/dist/sharding/health.d.ts +94 -0
  54. package/dist/sharding/host.d.ts +76 -0
  55. package/dist/sharding/identify.d.ts +64 -0
  56. package/dist/sharding/index.d.ts +62 -12
  57. package/dist/sharding/index.js +2459 -202
  58. package/dist/sharding/index.js.map +21 -13
  59. package/dist/sharding/protocol.d.ts +47 -0
  60. package/dist/sharding/remote-protocol.d.ts +100 -0
  61. package/dist/sharding/remote.d.ts +134 -0
  62. package/dist/sharding/requests.d.ts +54 -0
  63. package/dist/sharding/supervisor.d.ts +1 -1
  64. package/dist/sharding/transport.d.ts +6 -5
  65. package/dist/sharding/worker.d.ts +55 -0
  66. package/dist/structures/base.d.ts +16 -5
  67. package/dist/structures/index.d.ts +1 -1
  68. package/dist/structures/index.js +57 -91
  69. package/dist/structures/index.js.map +10 -18
  70. package/dist/structures/options.d.ts +3 -2
  71. package/dist/structures/permissions.d.ts +409 -0
  72. package/dist/structures/resources.d.ts +4 -1
  73. package/dist/types/gateway.d.ts +7 -0
  74. package/dist/types/index.d.ts +14 -4
  75. package/dist/types/index.js.map +3 -3
  76. package/dist/utils/args.d.ts +3 -1
  77. package/dist/utils/index.d.ts +6 -1
  78. package/dist/utils/index.js +17 -10
  79. package/dist/utils/index.js.map +4 -4
  80. package/dist/voice/index.d.ts +7 -1
  81. package/dist/voice/index.js +33 -10
  82. package/dist/voice/index.js.map +3 -3
  83. package/dist/ws/index.d.ts +3 -1
  84. package/dist/ws/index.js +33 -5
  85. package/dist/ws/index.js.map +6 -6
  86. package/dist/ws/send-budget.d.ts +1 -1
  87. package/package.json +22 -17
@@ -2,7 +2,7 @@
2
2
  // packages/sharding/package.json
3
3
  var package_default = {
4
4
  name: "@lunibee/sharding",
5
- version: "0.3.0",
5
+ version: "0.3.2",
6
6
  type: "module",
7
7
  module: "./src/index.ts",
8
8
  types: "./src/index.ts",
@@ -38,6 +38,37 @@ async function fetchGatewayBot(token) {
38
38
  };
39
39
  return info;
40
40
  }
41
+ var MAX_GUILDS_PER_SHARD = 2500;
42
+ function assertGuildsPerShard(value) {
43
+ if (!Number.isInteger(value) || value < 1 || value > MAX_GUILDS_PER_SHARD)
44
+ throw new RangeError(`guildsPerShard must be an integer from 1 to ${MAX_GUILDS_PER_SHARD}.`);
45
+ }
46
+ function shardsForGuilds(guilds, guildsPerShard) {
47
+ assertGuildsPerShard(guildsPerShard);
48
+ if (!Number.isInteger(guilds) || guilds < 0)
49
+ throw new RangeError("guildCount must be a non-negative integer.");
50
+ return Math.max(1, Math.ceil(guilds / guildsPerShard));
51
+ }
52
+ async function fetchGuildCount(token) {
53
+ const response = await fetch("https://discord.com/api/v10/applications/@me", {
54
+ headers: {
55
+ Authorization: `Bot ${token}`,
56
+ "User-Agent": `Lunibee/${package_default.version}`
57
+ }
58
+ });
59
+ if (!response.ok)
60
+ throw new Error(`Guild count discovery failed with status ${response.status}`);
61
+ const data = await response.json();
62
+ const count = data.approximate_guild_count;
63
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0)
64
+ throw new Error("Discord did not report a guild count.");
65
+ return count;
66
+ }
67
+ async function resolveShardCount(token, info, options) {
68
+ if (options.guildsPerShard === undefined)
69
+ return info.shards;
70
+ return shardsForGuilds(options.guildCount ?? await fetchGuildCount(token), options.guildsPerShard);
71
+ }
41
72
 
42
73
  // packages/ws/src/session.ts
43
74
  class GatewaySession {
@@ -575,6 +606,7 @@ class WebSocketTransport {
575
606
  decoded = this.#decoder.push(event.data);
576
607
  } catch (error) {
577
608
  this.#handlers.onError(this.#asError(error));
609
+ this.#failDecoding(generation);
578
610
  return;
579
611
  }
580
612
  if (!(decoded instanceof Promise)) {
@@ -592,6 +624,7 @@ class WebSocketTransport {
592
624
  if (!this.#owns(generation))
593
625
  return;
594
626
  this.#handlers.onError(this.#asError(error));
627
+ this.#failDecoding(generation);
595
628
  });
596
629
  });
597
630
  socket.addEventListener("close", (event) => {
@@ -620,6 +653,10 @@ class WebSocketTransport {
620
653
  return false;
621
654
  }
622
655
  }
656
+ #failDecoding(generation) {
657
+ if (this.#owns(generation))
658
+ this.close(4000, "decoder failure");
659
+ }
623
660
  close(code = 1000, reason = "") {
624
661
  const socket = this.#socket;
625
662
  if (!socket)
@@ -962,6 +999,13 @@ class Gateway {
962
999
  constructor(options) {
963
1000
  if (!options.token?.trim())
964
1001
  throw new TypeError("A Gateway token is required.");
1002
+ for (const key of [
1003
+ "reconnectBaseDelay",
1004
+ "reconnectMaxDelay",
1005
+ "connectTimeout"
1006
+ ])
1007
+ if (options[key] !== undefined && !(options[key] > 0))
1008
+ throw new RangeError(`Gateway ${key} must be greater than 0.`);
965
1009
  const resolvedIntents = resolveGatewayIntents(options.intents);
966
1010
  if (!Number.isInteger(resolvedIntents) || resolvedIntents < 0)
967
1011
  throw new TypeError("Gateway intents must be a non-negative integer.");
@@ -970,6 +1014,7 @@ class Gateway {
970
1014
  shardCount: 1,
971
1015
  reconnect: true,
972
1016
  maxReconnectAttempts: Infinity,
1017
+ connectTimeout: 30000,
973
1018
  reconnectBaseDelay: 1000,
974
1019
  reconnectMaxDelay: 30000,
975
1020
  heartbeatAckTimeout: 1e4,
@@ -1036,7 +1081,7 @@ class Gateway {
1036
1081
  }
1037
1082
  connect(url = DEFAULT_GATEWAY_URL) {
1038
1083
  if (this.state === "CLOSED" /* Closed */)
1039
- throw new GatewayError("Gateway has been permanently closed.");
1084
+ return Promise.reject(new GatewayError("Gateway has been permanently closed."));
1040
1085
  const inFlight = this.#reconnect.attemptPromise;
1041
1086
  if (inFlight)
1042
1087
  return inFlight;
@@ -1045,7 +1090,7 @@ class Gateway {
1045
1090
  this.#reconnect.cancel();
1046
1091
  this.#closed = false;
1047
1092
  this.#setState("CONNECT" /* Connect */);
1048
- const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
1093
+ const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}${url.includes("?") ? "&" : "?"}compress=zlib-stream` : url;
1049
1094
  return this.#reconnect.attempt(() => this.#open(connectURL));
1050
1095
  }
1051
1096
  close() {
@@ -1131,6 +1176,8 @@ class Gateway {
1131
1176
  const count = this.#options.shardCount;
1132
1177
  if (count <= 1)
1133
1178
  return true;
1179
+ if (!/^\d{1,20}$/.test(guildId))
1180
+ return false;
1134
1181
  return Number((BigInt(guildId) >> 22n) % BigInt(count)) === this.#options.shardId;
1135
1182
  }
1136
1183
  requestGuildMembers(data) {
@@ -1146,8 +1193,16 @@ class Gateway {
1146
1193
  return;
1147
1194
  this.#token = this.#session.beginConnection();
1148
1195
  const result = this.#transport.connect(url);
1149
- if (result.ok)
1196
+ if (result.ok) {
1197
+ const token = this.#token;
1198
+ clearTimeout(this.#connectTimer);
1199
+ this.#connectTimer = setTimeout(() => {
1200
+ if (token === this.#token && !this.#closed)
1201
+ this.#transport.close(4000, "connect timeout");
1202
+ }, this.#options.connectTimeout);
1203
+ this.#connectTimer.unref?.();
1150
1204
  return;
1205
+ }
1151
1206
  const failure = this.#normalizeError(result.error);
1152
1207
  this.#emitError(failure);
1153
1208
  if (this.#options.reconnect) {
@@ -1157,7 +1212,9 @@ class Gateway {
1157
1212
  this.#settleConnect(failure);
1158
1213
  }
1159
1214
  }
1215
+ #connectTimer;
1160
1216
  #onOpen() {
1217
+ clearTimeout(this.#connectTimer);
1161
1218
  this.#heartbeat.watch();
1162
1219
  this.#emit("open", undefined);
1163
1220
  this.#settleConnect();
@@ -1225,7 +1282,8 @@ class Gateway {
1225
1282
  this.#emit("RAW", { event: event ?? "UNKNOWN", data });
1226
1283
  if (this.#closed)
1227
1284
  return;
1228
- this.#emit(event ?? "dispatch", data);
1285
+ if (event == null || /^[A-Z][A-Z0-9_]*$/.test(event))
1286
+ this.#emit(event ?? "dispatch", data);
1229
1287
  }
1230
1288
  #handleHello(interval) {
1231
1289
  this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
@@ -1288,6 +1346,7 @@ class Gateway {
1288
1346
  this.#reconnect.settle(error);
1289
1347
  }
1290
1348
  #clearTimers() {
1349
+ clearTimeout(this.#connectTimer);
1291
1350
  this.#reconnect.cancel();
1292
1351
  this.#heartbeat.stop();
1293
1352
  }
@@ -1310,6 +1369,244 @@ class Gateway {
1310
1369
  this.#emit("stateChange", { previous, next });
1311
1370
  }
1312
1371
  }
1372
+
1373
+ // packages/sharding/src/context.ts
1374
+ var WORKER_MARKER = "LUNIBEE_CLUSTER_WORKER";
1375
+ function isClusterWorker(env = process.env) {
1376
+ return env[WORKER_MARKER] === "1";
1377
+ }
1378
+ function validateShardIds(shardIds, total) {
1379
+ if (!Number.isInteger(total) || total < 1)
1380
+ throw new RangeError("Shard count must be a positive integer.");
1381
+ const seen = new Set;
1382
+ for (const id of shardIds) {
1383
+ if (!Number.isInteger(id) || id < 0 || id >= total)
1384
+ throw new RangeError(`Shard ID ${String(id)} is outside 0-${total - 1}.`);
1385
+ if (seen.has(id))
1386
+ throw new RangeError(`Shard ID ${id} is listed twice.`);
1387
+ seen.add(id);
1388
+ }
1389
+ }
1390
+ function allocateShards(shardIds, clusterCount) {
1391
+ if (!Number.isInteger(clusterCount) || clusterCount < 1)
1392
+ throw new RangeError("clusterCount must be a positive integer.");
1393
+ const chunks = Array.from({ length: clusterCount }, () => []);
1394
+ shardIds.forEach((id, index) => chunks[index % clusterCount].push(id));
1395
+ return chunks;
1396
+ }
1397
+ function assertCompleteAllocation(chunks, expected) {
1398
+ const owners = new Map;
1399
+ const duplicate = [];
1400
+ chunks.forEach((chunk, cluster) => {
1401
+ for (const id of chunk) {
1402
+ if (owners.has(id))
1403
+ duplicate.push(id);
1404
+ owners.set(id, cluster);
1405
+ }
1406
+ });
1407
+ const want = new Set(expected);
1408
+ const missing = expected.filter((id) => !owners.has(id));
1409
+ const extra = [...owners.keys()].filter((id) => !want.has(id));
1410
+ if (duplicate.length || missing.length || extra.length)
1411
+ throw new Error(`Shard allocation is not exact: duplicate [${duplicate}], missing [${missing}], unexpected [${extra}].`);
1412
+ }
1413
+ function parseInteger(name, value) {
1414
+ if (value === undefined || !/^\d+$/.test(value))
1415
+ throw new RangeError(`${name} must be a non-negative integer.`);
1416
+ return Number(value);
1417
+ }
1418
+ function readClusterContext(env = process.env) {
1419
+ if (!isClusterWorker(env))
1420
+ return null;
1421
+ const shardCount = parseInteger("SHARD_COUNT", env.SHARD_COUNT);
1422
+ const shardIds = (env.SHARD_LIST ?? "").split(",").filter((part) => part !== "").map((part) => parseInteger("SHARD_LIST", part));
1423
+ validateShardIds(shardIds, shardCount);
1424
+ return {
1425
+ clusterId: parseInteger("CLUSTER_ID", env.CLUSTER_ID),
1426
+ clusterCount: parseInteger("CLUSTER_COUNT", env.CLUSTER_COUNT),
1427
+ shardCount,
1428
+ shardIds,
1429
+ coordinatedIdentify: env.LUNIBEE_IDENTIFY === "coordinated"
1430
+ };
1431
+ }
1432
+
1433
+ // packages/sharding/src/protocol.ts
1434
+ var CLUSTER_FRAME = "__lunibeeCluster";
1435
+ var PARENT_ID = -1;
1436
+ var REQUEST_HEALTH = "__lunibee:health";
1437
+ var REQUEST_ASSIGN = "__lunibee:assign";
1438
+ var isRecord = (value) => typeof value === "object" && value !== null;
1439
+ var isShardId = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
1440
+ function parseChildFrame(value) {
1441
+ if (!isRecord(value))
1442
+ return null;
1443
+ switch (value.kind) {
1444
+ case "ready":
1445
+ return Array.isArray(value.shardIds) && value.shardIds.every(isShardId) ? { kind: "ready", shardIds: value.shardIds } : null;
1446
+ case "identify":
1447
+ return typeof value.id === "number" && isShardId(value.shardId) ? { kind: "identify", id: value.id, shardId: value.shardId } : null;
1448
+ case "identify-done":
1449
+ return isShardId(value.shardId) ? { kind: "identify-done", shardId: value.shardId } : null;
1450
+ case "reply":
1451
+ return typeof value.id === "string" && (value.error === undefined || typeof value.error === "string") ? {
1452
+ kind: "reply",
1453
+ id: value.id,
1454
+ ...value.error !== undefined ? { error: value.error } : { result: value.result }
1455
+ } : null;
1456
+ case "health":
1457
+ return isRecord(value.report) && Array.isArray(value.report.shards) && typeof value.report.reportedAt === "number" ? { kind: "health", report: value.report } : null;
1458
+ default:
1459
+ return null;
1460
+ }
1461
+ }
1462
+ function parseParentFrame(value) {
1463
+ if (!isRecord(value))
1464
+ return null;
1465
+ switch (value.kind) {
1466
+ case "identify-grant":
1467
+ return typeof value.id === "number" ? { kind: "identify-grant", id: value.id } : null;
1468
+ case "identify-deny":
1469
+ return typeof value.id === "number" && typeof value.error === "string" ? { kind: "identify-deny", id: value.id, error: value.error } : null;
1470
+ case "request":
1471
+ return typeof value.id === "string" && typeof value.type === "string" ? {
1472
+ kind: "request",
1473
+ id: value.id,
1474
+ type: value.type,
1475
+ data: value.data
1476
+ } : null;
1477
+ default:
1478
+ return null;
1479
+ }
1480
+ }
1481
+
1482
+ // packages/sharding/src/worker.ts
1483
+ class ClusterWorker {
1484
+ static #current;
1485
+ static current() {
1486
+ if (!ClusterWorker.#current) {
1487
+ const context = readClusterContext();
1488
+ if (context && typeof process.send === "function")
1489
+ ClusterWorker.#current = new ClusterWorker(context, process);
1490
+ }
1491
+ return ClusterWorker.#current;
1492
+ }
1493
+ static reset() {
1494
+ ClusterWorker.#current?.close();
1495
+ ClusterWorker.#current = undefined;
1496
+ }
1497
+ context;
1498
+ #endpoint;
1499
+ #handlers = new Map;
1500
+ #waiting = new Map;
1501
+ #onMessage;
1502
+ #onDisconnect;
1503
+ #counter = 0;
1504
+ #closed = false;
1505
+ constructor(context, endpoint = process, options = {}) {
1506
+ if (typeof endpoint.send !== "function")
1507
+ throw new Error("ClusterWorker needs an IPC channel: run it in a process forked by ClusterManager.");
1508
+ this.context = context;
1509
+ this.#endpoint = endpoint;
1510
+ this.#onMessage = (frame) => this.#handle(frame);
1511
+ const exitOnLoss = options.exitOnParentLoss ?? endpoint === process;
1512
+ const exit = options.exit ?? ((code) => process.exit(code));
1513
+ this.#onDisconnect = () => {
1514
+ this.#failAll("The parent went away.");
1515
+ if (exitOnLoss && !this.#closed)
1516
+ exit(1);
1517
+ };
1518
+ endpoint.on("message", this.#onMessage);
1519
+ endpoint.on("disconnect", this.#onDisconnect);
1520
+ }
1521
+ get identifyGate() {
1522
+ return this.context.coordinatedIdentify ? this.#gate : undefined;
1523
+ }
1524
+ #gate = {
1525
+ acquire: (shardId) => {
1526
+ if (this.#closed)
1527
+ return Promise.reject(new Error("The cluster worker is closed."));
1528
+ const id = ++this.#counter;
1529
+ return new Promise((resolve, reject) => {
1530
+ this.#waiting.set(id, { resolve, reject });
1531
+ this.#send({ kind: "identify", id, shardId });
1532
+ });
1533
+ },
1534
+ release: (shardId) => this.#send({ kind: "identify-done", shardId })
1535
+ };
1536
+ ready(shardIds = this.context.shardIds) {
1537
+ this.#send({ kind: "ready", shardIds: [...shardIds] });
1538
+ }
1539
+ reportHealth(report) {
1540
+ this.#send({ kind: "health", report });
1541
+ }
1542
+ onRequest(type, handler) {
1543
+ if (!type.trim() || typeof handler !== "function")
1544
+ throw new TypeError("A request type and handler are required.");
1545
+ this.#handlers.set(type, handler);
1546
+ return this;
1547
+ }
1548
+ close() {
1549
+ this.#closed = true;
1550
+ this.#endpoint.off("message", this.#onMessage);
1551
+ this.#endpoint.off("disconnect", this.#onDisconnect);
1552
+ this.#failAll("The cluster worker closed.");
1553
+ }
1554
+ #send(body) {
1555
+ try {
1556
+ this.#endpoint.send({ [CLUSTER_FRAME]: body });
1557
+ } catch (error) {
1558
+ this.#failAll(error instanceof Error ? error.message : String(error));
1559
+ }
1560
+ }
1561
+ #failAll(reason) {
1562
+ for (const waiter of this.#waiting.values())
1563
+ waiter.reject(new Error(reason));
1564
+ this.#waiting.clear();
1565
+ }
1566
+ #handle(frame) {
1567
+ if (!frame || typeof frame !== "object" || !(CLUSTER_FRAME in frame))
1568
+ return;
1569
+ const parsed = parseParentFrame(frame[CLUSTER_FRAME]);
1570
+ if (!parsed)
1571
+ return;
1572
+ if (parsed.kind === "identify-grant" || parsed.kind === "identify-deny") {
1573
+ const waiter = this.#waiting.get(parsed.id);
1574
+ if (!waiter)
1575
+ return;
1576
+ this.#waiting.delete(parsed.id);
1577
+ if (parsed.kind === "identify-grant")
1578
+ waiter.resolve();
1579
+ else
1580
+ waiter.reject(new Error(parsed.error));
1581
+ return;
1582
+ }
1583
+ const handler = this.#handlers.get(parsed.type);
1584
+ const id = parsed.id;
1585
+ if (!handler) {
1586
+ this.#send({
1587
+ kind: "reply",
1588
+ id,
1589
+ error: `No handler for "${parsed.type}".`
1590
+ });
1591
+ return;
1592
+ }
1593
+ (async () => {
1594
+ try {
1595
+ this.#send({
1596
+ kind: "reply",
1597
+ id,
1598
+ result: await handler(parsed.data)
1599
+ });
1600
+ } catch (error) {
1601
+ this.#send({
1602
+ kind: "reply",
1603
+ id,
1604
+ error: error instanceof Error ? error.message : String(error)
1605
+ });
1606
+ }
1607
+ })();
1608
+ }
1609
+ }
1313
1610
  // packages/sharding/src/transport.ts
1314
1611
  class BroadcastChannelTransport {
1315
1612
  #channel;
@@ -1328,6 +1625,7 @@ class BroadcastChannelTransport {
1328
1625
  }
1329
1626
  var BUS_FRAME = "__lunibeeBus";
1330
1627
  var PEER_DOWN_FRAME = "__lunibeePeerDown";
1628
+ var LOCAL = new WeakMap;
1331
1629
 
1332
1630
  class IpcTransport {
1333
1631
  #endpoint;
@@ -1348,9 +1646,18 @@ class IpcTransport {
1348
1646
  this.#down?.(record[PEER_DOWN_FRAME]);
1349
1647
  };
1350
1648
  endpoint.on("message", this.#listener);
1649
+ let local = LOCAL.get(endpoint);
1650
+ if (!local)
1651
+ LOCAL.set(endpoint, local = new Set);
1652
+ local.add(this);
1351
1653
  }
1352
1654
  post(message) {
1353
1655
  this.#endpoint.send({ [BUS_FRAME]: message });
1656
+ for (const peer of LOCAL.get(this.#endpoint) ?? [])
1657
+ if (peer !== this) {
1658
+ const copy = structuredClone(message);
1659
+ queueMicrotask(() => peer.#message?.(copy));
1660
+ }
1354
1661
  }
1355
1662
  onMessage(listener) {
1356
1663
  this.#message = listener;
@@ -1360,6 +1667,7 @@ class IpcTransport {
1360
1667
  }
1361
1668
  close() {
1362
1669
  this.#endpoint.off("message", this.#listener);
1670
+ LOCAL.get(this.#endpoint)?.delete(this);
1363
1671
  }
1364
1672
  }
1365
1673
  function defaultTransport(name) {
@@ -1370,17 +1678,26 @@ function defaultTransport(name) {
1370
1678
 
1371
1679
  // packages/sharding/src/bus.ts
1372
1680
  var REPLY_TYPE = "__lunibee:reply";
1681
+ var MAX_NAME_LENGTH = 256;
1682
+ function isShardMessage(message) {
1683
+ if (!message || typeof message !== "object")
1684
+ return false;
1685
+ const m = message;
1686
+ 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);
1687
+ }
1373
1688
 
1374
1689
  class ShardBus {
1375
1690
  #transport;
1376
1691
  #maxPending;
1377
1692
  #handlers = new Map;
1693
+ #responders = new Map;
1378
1694
  #errorHandlers = new Set;
1379
1695
  #shardId;
1380
1696
  #namespace;
1381
1697
  channelName;
1382
1698
  #counter = 0;
1383
1699
  #pending = new Map;
1700
+ #closed = false;
1384
1701
  constructor(shardId, channelName, options = {}) {
1385
1702
  if (!Number.isInteger(shardId) || shardId < 0)
1386
1703
  throw new RangeError("Shard ID must be a non-negative integer.");
@@ -1404,7 +1721,13 @@ class ShardBus {
1404
1721
  return this;
1405
1722
  }
1406
1723
  off(type, handler) {
1407
- this.#handlers.get(type)?.delete(handler);
1724
+ const handlers = this.#handlers.get(type);
1725
+ handlers?.delete(handler);
1726
+ const responder = this.#responders.get(type)?.get(handler);
1727
+ if (responder) {
1728
+ handlers?.delete(responder);
1729
+ this.#responders.get(type).delete(handler);
1730
+ }
1408
1731
  return this;
1409
1732
  }
1410
1733
  onError(handler) {
@@ -1426,7 +1749,7 @@ class ShardBus {
1426
1749
  return this.#publish(null, type, data);
1427
1750
  }
1428
1751
  respond(type, handler) {
1429
- return this.on(type, async (message) => {
1752
+ const wrapper = async (message) => {
1430
1753
  if (!message.expectsReply)
1431
1754
  return;
1432
1755
  const reply = { requestId: message.id };
@@ -1436,7 +1759,12 @@ class ShardBus {
1436
1759
  reply.error = error instanceof Error ? error.message : String(error);
1437
1760
  }
1438
1761
  this.#publish(message.source, REPLY_TYPE, reply);
1439
- });
1762
+ };
1763
+ let byType = this.#responders.get(type);
1764
+ if (!byType)
1765
+ this.#responders.set(type, byType = new Map);
1766
+ byType.set(handler, wrapper);
1767
+ return this.on(type, wrapper);
1440
1768
  }
1441
1769
  async request(target, type, data, timeoutMs = 5000) {
1442
1770
  if (!Number.isInteger(target) || target < 0)
@@ -1453,6 +1781,8 @@ class ShardBus {
1453
1781
  }
1454
1782
  #collect(target, type, data, timeoutMs, expected, rejectOnPeerDown = false) {
1455
1783
  const replies = [];
1784
+ if (this.#closed)
1785
+ return Promise.reject(new Error("ShardBus is closed."));
1456
1786
  if (this.#pending.size >= this.#maxPending)
1457
1787
  return Promise.reject(new Error(`Too many shard requests waiting for a reply (${this.#maxPending}).`));
1458
1788
  return new Promise((resolve, reject) => {
@@ -1464,24 +1794,33 @@ class ShardBus {
1464
1794
  };
1465
1795
  const timer = setTimeout(finish, timeoutMs);
1466
1796
  requestId = this.#publish(target, type, data, true);
1797
+ const fail = (error) => {
1798
+ if (!rejectOnPeerDown)
1799
+ return;
1800
+ clearTimeout(timer);
1801
+ this.#pending.delete(requestId);
1802
+ reject(error);
1803
+ };
1467
1804
  this.#pending.set(requestId, {
1468
1805
  target,
1806
+ seen: new Set,
1469
1807
  reply: (reply) => {
1470
1808
  replies.push(reply);
1471
1809
  if (replies.length >= expected)
1472
1810
  finish();
1473
1811
  },
1474
- fail: (error) => {
1475
- if (rejectOnPeerDown) {
1476
- clearTimeout(timer);
1477
- this.#pending.delete(requestId);
1478
- reject(error);
1479
- }
1480
- }
1812
+ fail,
1813
+ cancel: (error) => rejectOnPeerDown ? fail(error) : finish()
1481
1814
  });
1482
1815
  });
1483
1816
  }
1484
1817
  close() {
1818
+ if (this.#closed)
1819
+ return;
1820
+ this.#closed = true;
1821
+ for (const pending of [...this.#pending.values()])
1822
+ pending.cancel(new Error("ShardBus is closed."));
1823
+ this.#pending.clear();
1485
1824
  this.#transport.close();
1486
1825
  this.#handlers.clear();
1487
1826
  this.#errorHandlers.clear();
@@ -1489,6 +1828,8 @@ class ShardBus {
1489
1828
  #publish(target, type, data, expectsReply = false) {
1490
1829
  if (!type.trim())
1491
1830
  throw new TypeError("Shard message type is required.");
1831
+ if (this.#closed)
1832
+ throw new Error("ShardBus is closed.");
1492
1833
  const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
1493
1834
  this.#transport.post({
1494
1835
  source: this.#shardId,
@@ -1501,15 +1842,21 @@ class ShardBus {
1501
1842
  return id;
1502
1843
  }
1503
1844
  #dispatch(message) {
1504
- if (!message || typeof message !== "object" || typeof message.type !== "string")
1845
+ if (this.#closed || !isShardMessage(message))
1505
1846
  return;
1506
1847
  if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
1507
1848
  return;
1508
1849
  if (message.type === REPLY_TYPE) {
1509
1850
  const reply = message.data;
1510
- this.#pending.get(reply?.requestId)?.reply({
1851
+ if (!reply || typeof reply !== "object" || typeof reply.requestId !== "string")
1852
+ return;
1853
+ const pending = this.#pending.get(reply.requestId);
1854
+ if (!pending || pending.seen.has(message.source) || pending.target !== null && pending.target !== message.source)
1855
+ return;
1856
+ pending.seen.add(message.source);
1857
+ pending.reply({
1511
1858
  shardId: message.source,
1512
- ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
1859
+ ...reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result }
1513
1860
  });
1514
1861
  return;
1515
1862
  }
@@ -1578,7 +1925,7 @@ class ShardSupervisor {
1578
1925
  recent.push(now);
1579
1926
  this.#restarts.set(id, recent);
1580
1927
  const base = Math.min(maxDelay, restartDelay * backoff ** (recent.length - 1));
1581
- return Math.round(base * (1 + jitter * this.#random()));
1928
+ return Math.min(maxDelay, Math.round(base * (1 + jitter * this.#random())));
1582
1929
  }
1583
1930
  reset(id) {
1584
1931
  if (id === undefined)
@@ -1590,178 +1937,1613 @@ class ShardSupervisor {
1590
1937
  // packages/sharding/src/cluster.ts
1591
1938
  import { fork } from "child_process";
1592
1939
  import { cpus } from "os";
1593
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1594
1940
 
1595
- class ClusterManager {
1596
- clusters = new Map;
1597
- shardCount = 0;
1598
- #options;
1599
- #auto;
1600
- #autoScaleTimer;
1601
- #spawned = false;
1602
- #stopping = false;
1603
- #restartTimers = new Set;
1604
- #supervisor;
1605
- constructor(options) {
1606
- if (!options.token?.trim())
1607
- throw new TypeError("A Discord bot token is required.");
1608
- if (!options.script?.trim())
1609
- throw new TypeError("A script path is required for clustering.");
1610
- if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
1611
- throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
1612
- }
1613
- this.#auto = options.shardCount === "auto";
1614
- this.#options = { ...options };
1615
- this.#supervisor = new ShardSupervisor({
1616
- restartDelay: options.restartDelay,
1617
- ...options.supervisor
1941
+ // packages/sharding/src/identify.ts
1942
+ var IDENTIFY_INTERVAL = 5000;
1943
+
1944
+ class IdentifyCoordinator {
1945
+ maxConcurrency;
1946
+ #interval;
1947
+ #holdTimeout;
1948
+ #now;
1949
+ #schedule;
1950
+ #cancel;
1951
+ #onBudgetWait;
1952
+ #keys = new Map;
1953
+ #remaining;
1954
+ #total;
1955
+ #resetAt = Number.POSITIVE_INFINITY;
1956
+ #period = 24 * 60 * 60 * 1000;
1957
+ #timer;
1958
+ #timerAt = Number.POSITIVE_INFINITY;
1959
+ #closed = false;
1960
+ constructor(options = {}) {
1961
+ const concurrency = options.maxConcurrency ?? 1;
1962
+ if (!Number.isInteger(concurrency) || concurrency < 1)
1963
+ throw new RangeError("maxConcurrency must be a positive integer.");
1964
+ const interval = options.interval ?? IDENTIFY_INTERVAL;
1965
+ const hold = options.holdTimeout ?? 60000;
1966
+ if (!(interval >= 0) || !(hold > 0))
1967
+ throw new RangeError("interval cannot be negative and holdTimeout must be positive.");
1968
+ this.maxConcurrency = concurrency;
1969
+ this.#interval = interval;
1970
+ this.#holdTimeout = hold;
1971
+ this.#now = options.now ?? Date.now;
1972
+ this.#schedule = options.schedule ?? ((callback, ms) => {
1973
+ const timer = setTimeout(callback, ms);
1974
+ timer.unref?.();
1975
+ return timer;
1618
1976
  });
1977
+ this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle));
1978
+ this.#onBudgetWait = options.onBudgetWait;
1979
+ if (options.budget) {
1980
+ this.#total = options.budget.total;
1981
+ this.#remaining = options.budget.remaining;
1982
+ this.#resetAt = this.#now() + options.budget.resetAfter;
1983
+ this.#period = options.budget.period ?? this.#period;
1984
+ if (!(this.#period > 0))
1985
+ throw new RangeError("budget.period must be positive.");
1986
+ }
1619
1987
  }
1620
- async fetchRecommendedShardCount() {
1621
- return (await fetchGatewayBot(this.#options.token)).shards;
1988
+ get remaining() {
1989
+ return this.#remaining;
1990
+ }
1991
+ get waiting() {
1992
+ let count = 0;
1993
+ for (const key of this.#keys.values())
1994
+ count += key.queue.length;
1995
+ return count;
1996
+ }
1997
+ get holding() {
1998
+ let count = 0;
1999
+ for (const key of this.#keys.values())
2000
+ if (key.holder)
2001
+ count++;
2002
+ return count;
2003
+ }
2004
+ acquire(owner, shardId) {
2005
+ if (this.#closed)
2006
+ return Promise.reject(new Error("IDENTIFY coordinator is closed."));
2007
+ const index = shardId % this.maxConcurrency;
2008
+ let key = this.#keys.get(index);
2009
+ if (!key)
2010
+ this.#keys.set(index, key = { queue: [], nextAt: 0 });
2011
+ return new Promise((grant, fail) => {
2012
+ key.queue.push({ owner, shardId, grant, fail });
2013
+ this.pump();
2014
+ });
1622
2015
  }
1623
- async spawn() {
1624
- if (this.#spawned)
2016
+ release(owner, shardId) {
2017
+ const key = this.#keys.get(shardId % this.maxConcurrency);
2018
+ const holder = key?.holder;
2019
+ if (!key || !holder || holder.owner !== owner || holder.shardId !== shardId)
1625
2020
  return;
1626
- if (typeof process?.versions?.bun === "string") {
1627
- throw new Error("ClusterManager requires the Node.js runtime; Bun does not support child_process.fork() for this clustering mode.");
1628
- }
1629
- this.#spawned = true;
1630
- const count = this.#options.shardCount === "auto" || this.#options.shardCount === undefined ? await this.fetchRecommendedShardCount() : this.#options.shardCount;
1631
- this.shardCount = count;
1632
- const clusterCount = this.#options.clusterCount ?? cpus().length;
1633
- const chunks = Array.from({ length: clusterCount }, () => []);
1634
- for (let i = 0;i < count; i++) {
1635
- chunks[i % clusterCount]?.push(i);
1636
- }
1637
- for (const [i, chunk] of chunks.entries()) {
1638
- if (chunk.length === 0)
1639
- continue;
1640
- this.#launch(i, chunk, count);
1641
- await sleep(500);
2021
+ this.#free(key);
2022
+ this.pump();
2023
+ }
2024
+ drop(owner, reason = "The cluster went away.") {
2025
+ for (const key of this.#keys.values()) {
2026
+ const keep = [];
2027
+ for (const waiter of key.queue)
2028
+ if (waiter.owner === owner)
2029
+ waiter.fail(new Error(reason));
2030
+ else
2031
+ keep.push(waiter);
2032
+ key.queue = keep;
2033
+ if (key.holder?.owner === owner)
2034
+ this.#free(key);
1642
2035
  }
1643
- if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
1644
- this.#autoScaleTimer = setInterval(() => {
1645
- this.checkAutoScale();
1646
- }, this.#options.autoScaleInterval);
2036
+ this.pump();
2037
+ }
2038
+ close() {
2039
+ this.#closed = true;
2040
+ if (this.#timer !== undefined)
2041
+ this.#cancel(this.#timer);
2042
+ this.#timer = undefined;
2043
+ this.#timerAt = Number.POSITIVE_INFINITY;
2044
+ for (const key of this.#keys.values()) {
2045
+ if (key.holder)
2046
+ this.#cancel(key.holder.timer);
2047
+ key.holder = undefined;
2048
+ for (const waiter of key.queue)
2049
+ waiter.fail(new Error("IDENTIFY coordinator is closed."));
2050
+ key.queue = [];
1647
2051
  }
1648
2052
  }
1649
- #launch(id, shards, shardCount) {
1650
- const child = fork(this.#options.script, [], {
1651
- env: {
1652
- ...process.env,
1653
- SHARD_LIST: shards.join(","),
1654
- SHARD_COUNT: shardCount.toString(),
1655
- CLUSTER_ID: id.toString(),
1656
- LUNIBEE_SHARD_BUS: "ipc"
2053
+ pump() {
2054
+ if (this.#closed)
2055
+ return;
2056
+ const now = this.#now();
2057
+ if (this.#total !== undefined && now >= this.#resetAt) {
2058
+ this.#remaining = this.#total;
2059
+ while (now >= this.#resetAt)
2060
+ this.#resetAt += this.#period;
2061
+ }
2062
+ let next = Number.POSITIVE_INFINITY;
2063
+ for (const key of this.#keys.values()) {
2064
+ if (key.holder || key.queue.length === 0)
2065
+ continue;
2066
+ if (key.nextAt > now) {
2067
+ next = Math.min(next, key.nextAt);
2068
+ continue;
1657
2069
  }
1658
- });
1659
- const info = { id, process: child, shards };
1660
- this.clusters.set(id, info);
1661
- child.on("message", (frame) => this.#relay(id, frame));
1662
- child.once("exit", (code, signal) => {
1663
- if (this.clusters.get(id)?.process !== child)
1664
- return;
1665
- try {
1666
- this.#options.onClusterExit?.(info, code, signal);
1667
- } catch {}
1668
- if (!this.#stopping)
1669
- this.#announceDown(id, shards);
1670
- if (this.#stopping || this.#options.restartOnExit === false)
1671
- return;
1672
- this.clusters.delete(id);
1673
- const delay = this.#supervisor.next(id);
1674
- if (delay === null) {
1675
- try {
1676
- this.#options.onGiveUp?.(info);
1677
- } catch {}
1678
- return;
2070
+ if (this.#remaining !== undefined && this.#remaining <= 0) {
2071
+ const waiter = key.queue[0];
2072
+ this.#onBudgetWait?.({
2073
+ shardId: waiter.shardId,
2074
+ waitMs: this.#resetAt - now
2075
+ });
2076
+ next = Math.min(next, this.#resetAt);
2077
+ continue;
1679
2078
  }
1680
- const timer = setTimeout(() => {
1681
- this.#restartTimers.delete(timer);
1682
- if (this.#stopping || !this.#spawned)
1683
- return;
1684
- this.#launch(id, shards, shardCount);
1685
- }, delay);
1686
- timer.unref?.();
1687
- this.#restartTimers.add(timer);
1688
- });
1689
- return info;
1690
- }
1691
- #relay(from, frame) {
1692
- if (!frame || typeof frame !== "object" || !(BUS_FRAME in frame))
1693
- return;
1694
- for (const [id, cluster] of this.clusters)
1695
- if (id !== from)
1696
- this.#sendFrame(cluster, frame);
2079
+ const waiter = key.queue.shift();
2080
+ if (this.#remaining !== undefined)
2081
+ this.#remaining--;
2082
+ const timer = this.#schedule(() => {
2083
+ if (key.holder?.timer === timer) {
2084
+ this.#free(key);
2085
+ this.pump();
2086
+ }
2087
+ }, this.#holdTimeout);
2088
+ key.holder = {
2089
+ owner: waiter.owner,
2090
+ shardId: waiter.shardId,
2091
+ timer
2092
+ };
2093
+ waiter.grant();
2094
+ }
2095
+ this.#arm(next);
1697
2096
  }
1698
- #announceDown(exited, shards) {
1699
- for (const [id, cluster] of this.clusters)
1700
- if (id !== exited)
1701
- this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
2097
+ #free(key) {
2098
+ if (key.holder)
2099
+ this.#cancel(key.holder.timer);
2100
+ key.holder = undefined;
2101
+ key.nextAt = this.#now() + this.#interval;
1702
2102
  }
1703
- #sendFrame(cluster, frame) {
1704
- const child = cluster.process;
1705
- if (typeof child.send !== "function" || !child.connected)
2103
+ #arm(at) {
2104
+ if (at === this.#timerAt && this.#timer !== undefined)
1706
2105
  return;
1707
- try {
1708
- child.send(frame);
1709
- } catch {}
1710
- }
1711
- async checkAutoScale() {
1712
- if (!this.#auto)
2106
+ if (this.#timer !== undefined)
2107
+ this.#cancel(this.#timer);
2108
+ this.#timer = undefined;
2109
+ this.#timerAt = Number.POSITIVE_INFINITY;
2110
+ if (at === Number.POSITIVE_INFINITY)
1713
2111
  return;
1714
- try {
1715
- const recommended = await this.fetchRecommendedShardCount();
1716
- if (recommended !== this.shardCount) {
1717
- await this.respawn(recommended);
2112
+ this.#timerAt = at;
2113
+ this.#timer = this.#schedule(() => {
2114
+ this.#timer = undefined;
2115
+ this.#timerAt = Number.POSITIVE_INFINITY;
2116
+ this.pump();
2117
+ }, Math.max(0, at - this.#now()));
2118
+ }
2119
+ }
2120
+
2121
+ // packages/sharding/src/remote.ts
2122
+ import { EventEmitter } from "events";
2123
+ import { randomBytes } from "crypto";
2124
+
2125
+ // packages/sharding/src/remote-protocol.ts
2126
+ import { createHmac, timingSafeEqual } from "crypto";
2127
+ var REMOTE_PROTOCOL_VERSION = 1;
2128
+ var MAX_REMOTE_FRAME_BYTES = 8 * 1024 * 1024;
2129
+ var RemoteClose = {
2130
+ BadRequest: 4400,
2131
+ AuthFailed: 4401,
2132
+ HelloTimeout: 4408,
2133
+ DuplicateHost: 4409,
2134
+ TooManyConnections: 4429,
2135
+ UnknownHost: 4410,
2136
+ VersionMismatch: 4426,
2137
+ HostLost: 4504,
2138
+ Shutdown: 4500
2139
+ };
2140
+ var SPAWN_ENV_KEYS = new Set([
2141
+ "SHARD_LIST",
2142
+ "SHARD_COUNT",
2143
+ "CLUSTER_ID",
2144
+ "CLUSTER_COUNT",
2145
+ "LUNIBEE_SHARD_BUS",
2146
+ "LUNIBEE_CLUSTER_WORKER",
2147
+ "LUNIBEE_IDENTIFY"
2148
+ ]);
2149
+ var KILL_SIGNALS = new Set([
2150
+ "SIGTERM",
2151
+ "SIGKILL",
2152
+ "SIGINT"
2153
+ ]);
2154
+ function isLoopbackHost(hostname) {
2155
+ return /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)$/i.test(hostname);
2156
+ }
2157
+ var HOST_ID = /^[A-Za-z0-9._-]{1,64}$/;
2158
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2159
+ var isInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
2160
+ function utf8Bytes(text) {
2161
+ return Buffer.byteLength(text, "utf8");
2162
+ }
2163
+ function parseJson(text, maxBytes) {
2164
+ if (typeof text !== "string")
2165
+ return null;
2166
+ if (text.length > maxBytes)
2167
+ return null;
2168
+ if (text.length * 3 > maxBytes && utf8Bytes(text) > maxBytes)
2169
+ return null;
2170
+ try {
2171
+ const value = JSON.parse(text);
2172
+ return isRecord2(value) ? value : null;
2173
+ } catch {
2174
+ return null;
2175
+ }
2176
+ }
2177
+ function signChallenge(secret, nonce, hostId) {
2178
+ return createHmac("sha256", secret).update(`${nonce}:${hostId}`).digest("hex");
2179
+ }
2180
+ function verifyChallenge(secret, nonce, hostId, mac) {
2181
+ if (typeof mac !== "string")
2182
+ return false;
2183
+ const expected = Buffer.from(signChallenge(secret, nonce, hostId), "hex");
2184
+ const given = Buffer.from(mac, "hex");
2185
+ return given.length === expected.length && timingSafeEqual(given, expected);
2186
+ }
2187
+ function parseToHost(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
2188
+ const v = parseJson(text, maxBytes);
2189
+ if (!v)
2190
+ return null;
2191
+ switch (v.t) {
2192
+ case "challenge":
2193
+ return typeof v.nonce === "string" && isInt(v.version) ? { t: "challenge", nonce: v.nonce, version: v.version } : null;
2194
+ case "welcome":
2195
+ return isInt(v.leaseMs) && isInt(v.pingMs) ? { t: "welcome", leaseMs: v.leaseMs, pingMs: v.pingMs } : null;
2196
+ case "ping":
2197
+ return isInt(v.n) ? { t: "ping", n: v.n } : null;
2198
+ case "spawn": {
2199
+ if (!isInt(v.cluster) || !isInt(v.epoch) || !isRecord2(v.env))
2200
+ return null;
2201
+ const env = {};
2202
+ for (const [key, value] of Object.entries(v.env)) {
2203
+ if (!SPAWN_ENV_KEYS.has(key) || typeof value !== "string")
2204
+ return null;
2205
+ env[key] = value;
1718
2206
  }
1719
- } catch (error) {
1720
- const normalized = error instanceof Error ? error : new Error(String(error));
1721
- this.#options.onAutoScaleError?.(normalized);
2207
+ return { t: "spawn", cluster: v.cluster, epoch: v.epoch, env };
1722
2208
  }
2209
+ case "frame":
2210
+ return isInt(v.cluster) && isInt(v.epoch) ? {
2211
+ t: "frame",
2212
+ cluster: v.cluster,
2213
+ epoch: v.epoch,
2214
+ frame: v.frame
2215
+ } : null;
2216
+ case "kill":
2217
+ return isInt(v.cluster) && isInt(v.epoch) && typeof v.signal === "string" && KILL_SIGNALS.has(v.signal) ? {
2218
+ t: "kill",
2219
+ cluster: v.cluster,
2220
+ epoch: v.epoch,
2221
+ signal: v.signal
2222
+ } : null;
2223
+ case "bye":
2224
+ return typeof v.reason === "string" ? { t: "bye", reason: v.reason } : null;
2225
+ default:
2226
+ return null;
1723
2227
  }
1724
- async respawn(newShardCount) {
1725
- this.#options.shardCount = newShardCount;
1726
- await this.shutdownAll();
1727
- this.#spawned = false;
1728
- this.#supervisor.reset();
1729
- await this.spawn();
2228
+ }
2229
+ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
2230
+ const v = parseJson(text, maxBytes);
2231
+ if (!v)
2232
+ return null;
2233
+ switch (v.t) {
2234
+ case "hello":
2235
+ return typeof v.id === "string" && typeof v.mac === "string" && isInt(v.version) ? { t: "hello", id: v.id, mac: v.mac, version: v.version } : null;
2236
+ case "pong":
2237
+ return isInt(v.n) ? { t: "pong", n: v.n } : null;
2238
+ case "spawned":
2239
+ return isInt(v.cluster) && isInt(v.epoch) && (v.pid === undefined || isInt(v.pid)) ? {
2240
+ t: "spawned",
2241
+ cluster: v.cluster,
2242
+ epoch: v.epoch,
2243
+ ...v.pid !== undefined ? { pid: v.pid } : {}
2244
+ } : null;
2245
+ case "frame":
2246
+ return isInt(v.cluster) && isInt(v.epoch) ? {
2247
+ t: "frame",
2248
+ cluster: v.cluster,
2249
+ epoch: v.epoch,
2250
+ frame: v.frame
2251
+ } : null;
2252
+ case "exit":
2253
+ return isInt(v.cluster) && isInt(v.epoch) && (v.code === null || typeof v.code === "number") && (v.signal === null || typeof v.signal === "string") ? {
2254
+ t: "exit",
2255
+ cluster: v.cluster,
2256
+ epoch: v.epoch,
2257
+ code: v.code,
2258
+ signal: v.signal
2259
+ } : null;
2260
+ case "error":
2261
+ return isInt(v.cluster) && isInt(v.epoch) && typeof v.message === "string" ? {
2262
+ t: "error",
2263
+ cluster: v.cluster,
2264
+ epoch: v.epoch,
2265
+ message: v.message
2266
+ } : null;
2267
+ default:
2268
+ return null;
1730
2269
  }
1731
- async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
1732
- this.#stopping = true;
1733
- this.#clearRestartTimers();
1734
- if (this.#autoScaleTimer) {
1735
- clearInterval(this.#autoScaleTimer);
1736
- this.#autoScaleTimer = undefined;
1737
- }
1738
- await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
1739
- this.clusters.clear();
1740
- this.#spawned = false;
1741
- this.#stopping = false;
2270
+ }
2271
+
2272
+ // packages/sharding/src/remote.ts
2273
+ class RemoteChild extends EventEmitter {
2274
+ host;
2275
+ cluster;
2276
+ epoch;
2277
+ pid;
2278
+ exitCode = null;
2279
+ signalCode = null;
2280
+ connected = true;
2281
+ #send;
2282
+ #kill;
2283
+ constructor(host, cluster, epoch, send, kill) {
2284
+ super();
2285
+ this.host = host;
2286
+ this.cluster = cluster;
2287
+ this.epoch = epoch;
2288
+ this.#send = send;
2289
+ this.#kill = kill;
2290
+ }
2291
+ send(frame) {
2292
+ return this.connected && this.exitCode === null && this.signalCode === null ? this.#send(frame) : false;
2293
+ }
2294
+ kill(signal = "SIGTERM") {
2295
+ if (this.exitCode !== null || this.signalCode !== null)
2296
+ return false;
2297
+ return this.#kill(signal);
1742
2298
  }
1743
- #clearRestartTimers() {
1744
- for (const timer of this.#restartTimers)
1745
- clearTimeout(timer);
1746
- this.#restartTimers.clear();
2299
+ spawned(pid) {
2300
+ this.pid = pid;
1747
2301
  }
1748
- async#shutdownCluster(cluster, timeoutMs) {
1749
- const child = cluster.process;
1750
- if (child.exitCode !== null || child.signalCode !== null)
2302
+ deliver(frame) {
2303
+ if (this.exitCode === null && this.signalCode === null)
2304
+ this.emit("message", frame);
2305
+ }
2306
+ finish(code, signal) {
2307
+ if (this.exitCode !== null || this.signalCode !== null)
1751
2308
  return;
1752
- await new Promise((resolve) => {
1753
- let timer;
1754
- const finish = () => {
1755
- if (timer)
1756
- clearTimeout(timer);
1757
- resolve();
1758
- };
1759
- child.once("exit", finish);
1760
- try {
1761
- child.kill("SIGTERM");
1762
- } catch {
1763
- child.off("exit", finish);
1764
- finish();
2309
+ this.connected = false;
2310
+ this.exitCode = code ?? (signal === null ? 1 : null);
2311
+ this.signalCode = signal ?? null;
2312
+ this.emit("exit", code, signal);
2313
+ }
2314
+ }
2315
+
2316
+ class RemoteHosts {
2317
+ #options;
2318
+ #links = new Map;
2319
+ #waiters = new Set;
2320
+ #hooks;
2321
+ #server;
2322
+ #watchdog;
2323
+ #epoch = 0;
2324
+ #ping = 0;
2325
+ #pending = new Set;
2326
+ #pendingByAddress = new Map;
2327
+ #rates = new Map;
2328
+ constructor(options, hooks) {
2329
+ if (typeof options.secret !== "string" || options.secret.length < 16)
2330
+ throw new RangeError("remote.secret must be a string of at least 16 characters.");
2331
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)
2332
+ throw new RangeError("remote.port must be an integer from 0 to 65535.");
2333
+ if (options.tls && (typeof options.tls.cert !== "string" || typeof options.tls.key !== "string" || !options.tls.cert || !options.tls.key))
2334
+ throw new TypeError("remote.tls needs a non-empty cert and key.");
2335
+ if (!options.tls && !options.allowInsecure && !isLoopbackHost(options.hostname ?? "127.0.0.1"))
2336
+ 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.");
2337
+ const lease = options.lease ?? 15000;
2338
+ if (!Number.isFinite(lease) || lease < 30)
2339
+ throw new RangeError("remote.lease must be at least 30 milliseconds.");
2340
+ for (const id of options.hosts ?? [])
2341
+ if (!HOST_ID.test(id))
2342
+ throw new RangeError(`"${id}" is not a valid host ID.`);
2343
+ if (options.hosts && (options.hosts.length === 0 || new Set(options.hosts).size !== options.hosts.length))
2344
+ throw new RangeError("remote.hosts must be unique and not empty.");
2345
+ if (options.minHosts !== undefined && (!Number.isInteger(options.minHosts) || options.minHosts < 1))
2346
+ throw new RangeError("remote.minHosts must be a positive integer.");
2347
+ this.#options = { ...options, lease };
2348
+ this.#hooks = hooks;
2349
+ }
2350
+ get port() {
2351
+ return this.#server?.port;
2352
+ }
2353
+ connected() {
2354
+ return [...this.#links.keys()].sort();
2355
+ }
2356
+ start() {
2357
+ if (this.#server)
2358
+ return;
2359
+ const { port, hostname, tls, lease } = this.#options;
2360
+ this.#server = Bun.serve({
2361
+ port,
2362
+ hostname: hostname ?? "127.0.0.1",
2363
+ ...tls ? { tls } : {},
2364
+ fetch: (request, server) => {
2365
+ const peer = server.requestIP(request)?.address;
2366
+ const address = this.#options.clientAddress?.(request, peer) ?? peer ?? "unknown";
2367
+ if (!this.#allowConnection(address))
2368
+ return new Response("Too many connections.", {
2369
+ status: 429
2370
+ });
2371
+ if (server.upgrade(request, {
2372
+ data: {
2373
+ nonce: randomBytes(16).toString("hex"),
2374
+ address
2375
+ }
2376
+ }))
2377
+ return;
2378
+ return new Response("Lunibee cluster coordinator", {
2379
+ status: 426
2380
+ });
2381
+ },
2382
+ websocket: {
2383
+ maxPayloadLength: MAX_REMOTE_FRAME_BYTES,
2384
+ idleTimeout: 0,
2385
+ open: (ws) => this.#open(ws),
2386
+ message: (ws, raw) => this.#message(ws, raw),
2387
+ close: (ws, _code, reason) => this.#closed(ws, reason)
2388
+ }
2389
+ });
2390
+ const pingMs = Math.max(10, Math.floor(lease / 3));
2391
+ this.#watchdog = setInterval(() => this.#tick(), pingMs);
2392
+ this.#watchdog.unref?.();
2393
+ }
2394
+ stop() {
2395
+ if (this.#watchdog)
2396
+ clearInterval(this.#watchdog);
2397
+ this.#watchdog = undefined;
2398
+ for (const link of [...this.#links.values()]) {
2399
+ this.#post(link, { t: "bye", reason: "The coordinator stopped." });
2400
+ this.#drop(link, "The coordinator stopped.", RemoteClose.Shutdown);
2401
+ }
2402
+ this.#server?.stop(true);
2403
+ this.#server = undefined;
2404
+ this.abortWaits();
2405
+ }
2406
+ waitFor(hosts, count, timeoutMs) {
2407
+ const done = () => hosts ? hosts.every((id) => this.#links.has(id)) : this.#links.size >= count;
2408
+ if (done())
2409
+ return Promise.resolve();
2410
+ return new Promise((resolve, reject) => {
2411
+ const waiter = {
2412
+ check: () => {
2413
+ if (!done())
2414
+ return;
2415
+ clearTimeout(timer);
2416
+ this.#waiters.delete(waiter);
2417
+ resolve();
2418
+ },
2419
+ abort: (error) => {
2420
+ clearTimeout(timer);
2421
+ this.#waiters.delete(waiter);
2422
+ reject(error);
2423
+ }
2424
+ };
2425
+ const timer = setTimeout(() => {
2426
+ const missing = hosts?.filter((id) => !this.#links.has(id));
2427
+ waiter.abort(new Error(hosts ? `Hosts did not join within ${timeoutMs}ms: [${missing}].` : `Only ${this.#links.size} of ${count} hosts joined within ${timeoutMs}ms.`));
2428
+ }, timeoutMs);
2429
+ this.#waiters.add(waiter);
2430
+ });
2431
+ }
2432
+ abortWaits() {
2433
+ for (const waiter of [...this.#waiters])
2434
+ waiter.abort(new Error("The coordinator stopped."));
2435
+ }
2436
+ status(declared = []) {
2437
+ const ids = new Set([...declared, ...this.#links.keys()]);
2438
+ return [...ids].sort().map((id) => {
2439
+ const link = this.#links.get(id);
2440
+ return {
2441
+ id,
2442
+ connected: link !== undefined,
2443
+ lastSeen: link?.lastSeen ?? 0,
2444
+ clusters: link ? [...link.children.keys()].sort((a, b) => a - b) : []
2445
+ };
2446
+ });
2447
+ }
2448
+ createChild(hostId, cluster, env) {
2449
+ const epoch = ++this.#epoch;
2450
+ const link = this.#links.get(hostId);
2451
+ 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 }));
2452
+ if (!link) {
2453
+ queueMicrotask(() => child.finish(null, "SIGHUP"));
2454
+ return child;
2455
+ }
2456
+ link.children.get(cluster)?.finish(null, "SIGKILL");
2457
+ link.children.set(cluster, child);
2458
+ if (!this.#post(link, { t: "spawn", cluster, epoch, env }))
2459
+ queueMicrotask(() => child.finish(null, "SIGHUP"));
2460
+ return child;
2461
+ }
2462
+ #post(link, message) {
2463
+ try {
2464
+ return link.ws.send(JSON.stringify(message)) > 0;
2465
+ } catch {
2466
+ return false;
2467
+ }
2468
+ }
2469
+ #allowConnection(address) {
2470
+ const rate = this.#options.connectionsPerSecond ?? 10;
2471
+ const now = Date.now();
2472
+ let bucket = this.#rates.get(address);
2473
+ if (!bucket) {
2474
+ bucket = { tokens: rate, at: now };
2475
+ this.#rates.set(address, bucket);
2476
+ if (this.#rates.size > 1e4)
2477
+ this.#rates.delete(this.#rates.keys().next().value);
2478
+ }
2479
+ bucket.tokens = Math.min(rate, bucket.tokens + (now - bucket.at) / 1000 * rate);
2480
+ bucket.at = now;
2481
+ if (bucket.tokens < 1)
2482
+ return false;
2483
+ bucket.tokens--;
2484
+ return true;
2485
+ }
2486
+ #evict(victim) {
2487
+ this.#uncount(victim);
2488
+ clearTimeout(victim.data.helloTimer);
2489
+ victim.close(RemoteClose.TooManyConnections, "Too many connections.");
2490
+ }
2491
+ #open(ws) {
2492
+ const address = ws.data.address;
2493
+ if ((this.#pendingByAddress.get(address) ?? 0) >= (this.#options.maxUnauthenticatedPerAddress ?? 4)) {
2494
+ for (const other of this.#pending)
2495
+ if (other.data.address === address) {
2496
+ this.#evict(other);
2497
+ break;
2498
+ }
2499
+ }
2500
+ if (this.#pending.size >= (this.#options.maxUnauthenticated ?? 16)) {
2501
+ const oldest = this.#pending.values().next().value;
2502
+ if (oldest)
2503
+ this.#evict(oldest);
2504
+ }
2505
+ this.#pending.add(ws);
2506
+ this.#pendingByAddress.set(address, (this.#pendingByAddress.get(address) ?? 0) + 1);
2507
+ ws.data.counted = true;
2508
+ ws.data.helloTimer = setTimeout(() => ws.close(RemoteClose.HelloTimeout, "No hello."), this.#options.helloTimeout ?? 3000);
2509
+ ws.send(JSON.stringify({
2510
+ t: "challenge",
2511
+ nonce: ws.data.nonce,
2512
+ version: REMOTE_PROTOCOL_VERSION
2513
+ }));
2514
+ }
2515
+ #message(ws, raw) {
2516
+ if (typeof raw !== "string") {
2517
+ ws.close(RemoteClose.BadRequest, "Text frames only.");
2518
+ return;
2519
+ }
2520
+ const message = parseToCoordinator(raw);
2521
+ if (!message) {
2522
+ ws.close(RemoteClose.BadRequest, "Malformed frame.");
2523
+ return;
2524
+ }
2525
+ if (ws.data.id === undefined) {
2526
+ this.#hello(ws, message);
2527
+ return;
2528
+ }
2529
+ const link = this.#links.get(ws.data.id);
2530
+ if (!link || link.ws !== ws)
2531
+ return;
2532
+ link.lastSeen = Date.now();
2533
+ if (message.t === "pong" || message.t === "hello")
2534
+ return;
2535
+ const child = link.children.get(message.cluster);
2536
+ if (!child || child.epoch !== message.epoch)
2537
+ return;
2538
+ switch (message.t) {
2539
+ case "spawned":
2540
+ child.spawned(message.pid);
2541
+ break;
2542
+ case "frame":
2543
+ child.deliver(message.frame);
2544
+ break;
2545
+ case "exit":
2546
+ link.children.delete(message.cluster);
2547
+ child.finish(message.code, message.signal);
2548
+ break;
2549
+ case "error":
2550
+ this.#hooks.error(new Error(`Host ${link.id} could not run cluster ${message.cluster}: ${message.message}`));
2551
+ link.children.delete(message.cluster);
2552
+ child.finish(1, null);
2553
+ break;
2554
+ }
2555
+ }
2556
+ #hello(ws, message) {
2557
+ if (message?.t !== "hello") {
2558
+ ws.close(RemoteClose.BadRequest, "Expected hello.");
2559
+ return;
2560
+ }
2561
+ const { id } = message;
2562
+ if (message.version !== REMOTE_PROTOCOL_VERSION) {
2563
+ ws.close(RemoteClose.VersionMismatch, "Protocol version mismatch.");
2564
+ return;
2565
+ }
2566
+ if (!HOST_ID.test(id) || !verifyChallenge(this.#options.secret, ws.data.nonce, id, message.mac)) {
2567
+ ws.close(RemoteClose.AuthFailed, "Authentication failed.");
2568
+ return;
2569
+ }
2570
+ if (this.#options.hosts && !this.#options.hosts.includes(id)) {
2571
+ ws.close(RemoteClose.UnknownHost, "Host is not part of this deployment.");
2572
+ return;
2573
+ }
2574
+ if (this.#links.has(id)) {
2575
+ ws.close(RemoteClose.DuplicateHost, "A host with this ID is connected.");
2576
+ return;
2577
+ }
2578
+ clearTimeout(ws.data.helloTimer);
2579
+ this.#uncount(ws);
2580
+ ws.data.id = id;
2581
+ const link = {
2582
+ id,
2583
+ ws,
2584
+ lastSeen: Date.now(),
2585
+ children: new Map
2586
+ };
2587
+ this.#links.set(id, link);
2588
+ const lease = this.#options.lease;
2589
+ this.#post(link, {
2590
+ t: "welcome",
2591
+ leaseMs: lease,
2592
+ pingMs: Math.max(10, Math.floor(lease / 3))
2593
+ });
2594
+ this.#hooks.join(id);
2595
+ for (const waiter of [...this.#waiters])
2596
+ waiter.check();
2597
+ }
2598
+ #uncount(ws) {
2599
+ if (!ws.data.counted)
2600
+ return;
2601
+ ws.data.counted = false;
2602
+ this.#pending.delete(ws);
2603
+ const left = (this.#pendingByAddress.get(ws.data.address) ?? 1) - 1;
2604
+ if (left > 0)
2605
+ this.#pendingByAddress.set(ws.data.address, left);
2606
+ else
2607
+ this.#pendingByAddress.delete(ws.data.address);
2608
+ }
2609
+ #closed(ws, reason) {
2610
+ clearTimeout(ws.data.helloTimer);
2611
+ this.#uncount(ws);
2612
+ const link = ws.data.id ? this.#links.get(ws.data.id) : undefined;
2613
+ if (link && link.ws === ws)
2614
+ this.#drop(link, reason || "The connection closed.");
2615
+ }
2616
+ #drop(link, reason, code) {
2617
+ if (this.#links.get(link.id) !== link)
2618
+ return;
2619
+ this.#links.delete(link.id);
2620
+ if (code !== undefined)
2621
+ try {
2622
+ link.ws.close(code, reason.slice(0, 100));
2623
+ } catch {}
2624
+ for (const child of [...link.children.values()])
2625
+ child.finish(null, "SIGHUP");
2626
+ link.children.clear();
2627
+ this.#hooks.leave(link.id, reason);
2628
+ }
2629
+ #tick() {
2630
+ const now = Date.now();
2631
+ for (const link of [...this.#links.values()]) {
2632
+ if (now - link.lastSeen > this.#options.lease * 2) {
2633
+ this.#drop(link, "The host stopped answering.", RemoteClose.HostLost);
2634
+ continue;
2635
+ }
2636
+ this.#post(link, { t: "ping", n: ++this.#ping });
2637
+ }
2638
+ }
2639
+ }
2640
+
2641
+ // packages/sharding/src/requests.ts
2642
+ class RequestTracker {
2643
+ #pending = new Map;
2644
+ #maxPending;
2645
+ #prefix;
2646
+ #counter = 0;
2647
+ constructor(prefix = "parent", maxPending = 1e4) {
2648
+ if (!Number.isInteger(maxPending) || maxPending < 1)
2649
+ throw new RangeError("maxPending must be a positive integer.");
2650
+ this.#prefix = prefix;
2651
+ this.#maxPending = maxPending;
2652
+ }
2653
+ get size() {
2654
+ return this.#pending.size;
2655
+ }
2656
+ begin(options) {
2657
+ if (this.#pending.size >= this.#maxPending)
2658
+ throw new Error(`Too many requests waiting for a reply (${this.#maxPending}).`);
2659
+ const id = `${this.#prefix}:${++this.#counter}`;
2660
+ const done = new Promise((resolve) => {
2661
+ const finish = () => {
2662
+ const entry = this.#pending.get(id);
2663
+ if (!entry)
2664
+ return;
2665
+ this.#pending.delete(id);
2666
+ clearTimeout(entry.timer);
2667
+ entry.cleanup?.();
2668
+ resolve({
2669
+ replies: entry.replies,
2670
+ missing: [...entry.keys.keys()].filter((key) => !entry.answered.has(key))
2671
+ });
2672
+ };
2673
+ const entry = {
2674
+ keys: new Map(options.keys),
2675
+ replies: [],
2676
+ answered: new Set,
2677
+ finish,
2678
+ timer: setTimeout(finish, options.timeoutMs)
2679
+ };
2680
+ if (options.signal) {
2681
+ const signal = options.signal;
2682
+ if (signal.aborted)
2683
+ queueMicrotask(finish);
2684
+ else {
2685
+ signal.addEventListener("abort", finish, { once: true });
2686
+ entry.cleanup = () => signal.removeEventListener("abort", finish);
2687
+ }
2688
+ }
2689
+ this.#pending.set(id, entry);
2690
+ if (entry.keys.size === 0)
2691
+ queueMicrotask(finish);
2692
+ });
2693
+ return { id, done };
2694
+ }
2695
+ answer(id, key, from, value) {
2696
+ const entry = this.#pending.get(id);
2697
+ if (!entry || entry.keys.get(key) !== from)
2698
+ return false;
2699
+ if (entry.answered.has(key))
2700
+ return false;
2701
+ entry.answered.add(key);
2702
+ entry.replies.push({ key, ...value });
2703
+ if (entry.replies.length >= entry.keys.size)
2704
+ entry.finish();
2705
+ return true;
2706
+ }
2707
+ failOwner(owner, error) {
2708
+ for (const entry of [...this.#pending.values()]) {
2709
+ for (const [key, keyOwner] of entry.keys)
2710
+ if (keyOwner === owner && !entry.answered.has(key)) {
2711
+ entry.answered.add(key);
2712
+ entry.replies.push({ key, error });
2713
+ }
2714
+ if (entry.replies.length >= entry.keys.size)
2715
+ entry.finish();
2716
+ }
2717
+ }
2718
+ close() {
2719
+ for (const entry of [...this.#pending.values()])
2720
+ entry.finish();
2721
+ }
2722
+ }
2723
+
2724
+ // packages/sharding/src/cluster.ts
2725
+ var DEFAULT_MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
2726
+ function payloadSize(value) {
2727
+ if (value === undefined || value === null)
2728
+ return 0;
2729
+ if (typeof value === "string")
2730
+ return Buffer.byteLength(value, "utf8");
2731
+ if (typeof value !== "object")
2732
+ return String(value).length;
2733
+ try {
2734
+ const text = JSON.stringify(value);
2735
+ return text === undefined ? 0 : Buffer.byteLength(text, "utf8");
2736
+ } catch {
2737
+ return Number.POSITIVE_INFINITY;
2738
+ }
2739
+ }
2740
+ var isLive = (state) => state === "starting" || state === "ready";
2741
+
2742
+ class ClusterManager {
2743
+ clusters = new Map;
2744
+ shardCount = 0;
2745
+ #options;
2746
+ #auto;
2747
+ #autoScaleTimer;
2748
+ #infos = new Map;
2749
+ #owner = new Map;
2750
+ #startPromise;
2751
+ #stopPromise;
2752
+ #wasStarted = false;
2753
+ #stopping = false;
2754
+ #restartTimers = new Set;
2755
+ #startTimers = new Map;
2756
+ #pauses = new Set;
2757
+ #readyWaiters = new Set;
2758
+ #supervisor;
2759
+ #tracker;
2760
+ #coordinator;
2761
+ #allReady = false;
2762
+ #clusterCount = 0;
2763
+ #identifyWaiting = new Map;
2764
+ #remote;
2765
+ #hostOf = new Map;
2766
+ #lastWait = "";
2767
+ #listeners = new Map;
2768
+ constructor(options) {
2769
+ if (!options.token?.trim())
2770
+ throw new TypeError("A Discord bot token is required.");
2771
+ if (!options.script?.trim())
2772
+ throw new TypeError("A script path is required for clustering.");
2773
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
2774
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
2775
+ }
2776
+ if (options.clusterCount !== undefined && options.shardsPerCluster !== undefined)
2777
+ throw new RangeError("Set either clusterCount or shardsPerCluster, not both.");
2778
+ for (const [name, value] of [
2779
+ ["clusterCount", options.clusterCount],
2780
+ ["shardsPerCluster", options.shardsPerCluster],
2781
+ ["maxConcurrency", options.maxConcurrency]
2782
+ ])
2783
+ if (value !== undefined && (!Number.isInteger(value) || value < 1))
2784
+ throw new RangeError(`${name} must be a positive integer.`);
2785
+ if (options.maxMessageBytes !== undefined && (!Number.isInteger(options.maxMessageBytes) || options.maxMessageBytes < 1))
2786
+ throw new RangeError("maxMessageBytes must be a positive integer.");
2787
+ for (const [name, value] of [
2788
+ ["spawnInterval", options.spawnInterval],
2789
+ ["startTimeout", options.startTimeout],
2790
+ ["requestTimeout", options.requestTimeout]
2791
+ ])
2792
+ if (value !== undefined && !(value >= 0))
2793
+ throw new RangeError(`${name} cannot be negative.`);
2794
+ if (options.guildsPerShard !== undefined) {
2795
+ assertGuildsPerShard(options.guildsPerShard);
2796
+ if (typeof options.shardCount === "number")
2797
+ throw new RangeError('guildsPerShard needs shardCount: "auto".');
2798
+ }
2799
+ if (options.shardIds && typeof options.shardCount === "number")
2800
+ validateShardIds(options.shardIds, options.shardCount);
2801
+ this.#auto = options.shardCount === "auto";
2802
+ this.#options = { ...options };
2803
+ this.#supervisor = new ShardSupervisor({
2804
+ restartDelay: options.restartDelay,
2805
+ ...options.supervisor
2806
+ });
2807
+ this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
2808
+ if (options.remote)
2809
+ this.#remote = new RemoteHosts(options.remote, {
2810
+ join: (id) => this.#emit("hostJoin", id),
2811
+ leave: (id, reason) => this.#emit("hostLeave", id, reason),
2812
+ error: (error) => this.#emit("error", error)
2813
+ });
2814
+ }
2815
+ on(event, listener) {
2816
+ let set = this.#listeners.get(event);
2817
+ if (!set)
2818
+ this.#listeners.set(event, set = new Set);
2819
+ set.add(listener);
2820
+ return this;
2821
+ }
2822
+ off(event, listener) {
2823
+ this.#listeners.get(event)?.delete(listener);
2824
+ return this;
2825
+ }
2826
+ #emit(event, ...args) {
2827
+ for (const listener of this.#listeners.get(event) ?? [])
2828
+ try {
2829
+ listener(...args);
2830
+ } catch {}
2831
+ }
2832
+ async fetchRecommendedShardCount() {
2833
+ return resolveShardCount(this.#options.token, await fetchGatewayBot(this.#options.token), this.#options);
2834
+ }
2835
+ clusterForShard(shardId) {
2836
+ const id = this.#owner.get(shardId);
2837
+ return id === undefined ? undefined : this.#infos.get(id);
2838
+ }
2839
+ assignments() {
2840
+ return new Map(this.#owner);
2841
+ }
2842
+ spawn() {
2843
+ if (isClusterWorker())
2844
+ return Promise.reject(new Error("A cluster worker cannot start a ClusterManager: it would fork the whole fleet again."));
2845
+ if (this.#stopPromise)
2846
+ return this.#stopPromise.then(() => this.spawn());
2847
+ if (!this.#startPromise) {
2848
+ const attempt = this.#start();
2849
+ this.#startPromise = attempt;
2850
+ attempt.catch(() => {
2851
+ if (this.#startPromise === attempt)
2852
+ this.#startPromise = undefined;
2853
+ });
2854
+ }
2855
+ return this.#startPromise;
2856
+ }
2857
+ async#resolveGateway() {
2858
+ const { shardCount, maxConcurrency, fetchLimits } = this.#options;
2859
+ let info;
2860
+ let count;
2861
+ if (shardCount === "auto" || shardCount === undefined) {
2862
+ info = await fetchGatewayBot(this.#options.token);
2863
+ count = await resolveShardCount(this.#options.token, info, this.#options);
2864
+ } else if (fetchLimits)
2865
+ try {
2866
+ info = await fetchGatewayBot(this.#options.token);
2867
+ } catch (error) {
2868
+ this.#emit("error", error instanceof Error ? error : new Error(String(error)));
2869
+ }
2870
+ const limit = info?.sessionStartLimit;
2871
+ return {
2872
+ count: count ?? shardCount,
2873
+ maxConcurrency: Math.min(maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Number.POSITIVE_INFINITY),
2874
+ ...limit ? {
2875
+ budget: {
2876
+ total: limit.total,
2877
+ remaining: limit.remaining,
2878
+ resetAfter: limit.resetAfter
2879
+ }
2880
+ } : {}
2881
+ };
2882
+ }
2883
+ async#start() {
2884
+ const { count, maxConcurrency, budget } = await this.#resolveGateway();
2885
+ if (this.#stopping)
2886
+ return;
2887
+ const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
2888
+ validateShardIds(ids, count);
2889
+ const clusterCount = this.#options.clusterCount ?? (this.#options.shardsPerCluster ? Math.ceil(ids.length / this.#options.shardsPerCluster) : cpus().length);
2890
+ const chunks = allocateShards(ids, clusterCount);
2891
+ assertCompleteAllocation(chunks, ids);
2892
+ const placement = await this.#placeHosts().catch((error) => {
2893
+ if (this.#stopping)
2894
+ return;
2895
+ throw error;
2896
+ });
2897
+ if (!placement || this.#stopping)
2898
+ return;
2899
+ this.shardCount = count;
2900
+ this.#wasStarted = true;
2901
+ this.#owner.clear();
2902
+ this.#infos.clear();
2903
+ const identify = this.#options.identify;
2904
+ this.#coordinator = identify?.coordinate === false ? undefined : new IdentifyCoordinator({
2905
+ maxConcurrency,
2906
+ interval: identify?.interval,
2907
+ holdTimeout: identify?.holdTimeout,
2908
+ budget,
2909
+ onBudgetWait: (wait) => {
2910
+ const key = `${wait.shardId}:${Math.round(wait.waitMs / 1000)}`;
2911
+ if (key === this.#lastWait)
2912
+ return;
2913
+ this.#lastWait = key;
2914
+ this.#emit("identifyWait", wait);
2915
+ }
2916
+ });
2917
+ const active = chunks.map((shards, id) => ({ id, shards })).filter((chunk) => chunk.shards.length > 0);
2918
+ this.#clusterCount = active.length;
2919
+ this.#hostOf.clear();
2920
+ for (const chunk of active)
2921
+ this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
2922
+ for (const [index, chunk] of active.entries()) {
2923
+ if (this.#stopping)
2924
+ return;
2925
+ for (const shard of chunk.shards)
2926
+ this.#owner.set(shard, chunk.id);
2927
+ this.#launch(chunk.id, chunk.shards);
2928
+ const gap = this.#options.spawnInterval ?? 500;
2929
+ if (index < active.length - 1 && gap > 0)
2930
+ await this.#pause(gap);
2931
+ }
2932
+ if (this.#stopping)
2933
+ return;
2934
+ if (this.#options.waitForReady)
2935
+ await this.ready();
2936
+ if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
2937
+ this.#autoScaleTimer = setInterval(() => {
2938
+ this.checkAutoScale();
2939
+ }, this.#options.autoScaleInterval);
2940
+ }
2941
+ }
2942
+ async#placeHosts() {
2943
+ const remote = this.#remote;
2944
+ const options = this.#options.remote;
2945
+ if (!remote || !options)
2946
+ return ["local"];
2947
+ remote.start();
2948
+ try {
2949
+ await remote.waitFor(options.hosts, options.minHosts ?? 1, options.joinTimeout ?? 60000);
2950
+ } catch (error) {
2951
+ remote.stop();
2952
+ throw error;
2953
+ }
2954
+ const ids = options.hosts ?? remote.connected();
2955
+ return options.local ? ["local", ...ids] : ids;
2956
+ }
2957
+ get remotePort() {
2958
+ return this.#remote?.port;
2959
+ }
2960
+ hosts() {
2961
+ return this.#remote?.status(this.#options.remote?.hosts) ?? [];
2962
+ }
2963
+ #pause(ms) {
2964
+ return new Promise((resolve) => {
2965
+ const wake = () => {
2966
+ clearTimeout(timer);
2967
+ this.#pauses.delete(wake);
2968
+ resolve();
2969
+ };
2970
+ const timer = setTimeout(wake, ms);
2971
+ this.#pauses.add(wake);
2972
+ });
2973
+ }
2974
+ #launch(id, shards) {
2975
+ const contract = {
2976
+ SHARD_LIST: shards.join(","),
2977
+ SHARD_COUNT: this.shardCount.toString(),
2978
+ CLUSTER_ID: id.toString(),
2979
+ CLUSTER_COUNT: this.#clusterCount.toString(),
2980
+ LUNIBEE_SHARD_BUS: "ipc",
2981
+ [WORKER_MARKER]: "1",
2982
+ LUNIBEE_IDENTIFY: this.#coordinator ? "coordinated" : "local"
2983
+ };
2984
+ const host = this.#hostOf.get(id) ?? "local";
2985
+ const env = {
2986
+ ...process.env,
2987
+ ...contract
2988
+ };
2989
+ 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, [], {
2990
+ env
2991
+ });
2992
+ const known = this.#infos.get(id);
2993
+ const info = known ?? {
2994
+ id,
2995
+ host,
2996
+ process: child,
2997
+ shards,
2998
+ state: "starting",
2999
+ restarts: 0,
3000
+ startedAt: Date.now()
3001
+ };
3002
+ info.process = child;
3003
+ info.state = "starting";
3004
+ info.startedAt = Date.now();
3005
+ info.readyAt = undefined;
3006
+ this.#infos.set(id, info);
3007
+ this.clusters.set(id, info);
3008
+ this.#allReady = false;
3009
+ child.on("error", (error) => this.#emit("error", new Error(`Cluster ${id} process error: ${error.message}`, {
3010
+ cause: error
3011
+ })));
3012
+ child.on("message", (frame) => this.#onFrame(info, child, frame));
3013
+ child.once("exit", (code, signal) => this.#onExit(info, child, code, signal));
3014
+ const timeout = this.#options.startTimeout;
3015
+ if (timeout) {
3016
+ const timer = setTimeout(() => {
3017
+ this.#startTimers.delete(id);
3018
+ if (info.process !== child || info.state !== "starting")
3019
+ return;
3020
+ this.#emit("clusterStartTimeout", info);
3021
+ try {
3022
+ child.kill("SIGKILL");
3023
+ } catch {}
3024
+ }, timeout);
3025
+ timer.unref?.();
3026
+ this.#startTimers.set(id, timer);
3027
+ }
3028
+ this.#emit("clusterSpawn", info);
3029
+ return info;
3030
+ }
3031
+ #onExit(info, child, code, signal) {
3032
+ if (info.process !== child)
3033
+ return;
3034
+ child.removeAllListeners("message");
3035
+ this.#clearStartTimer(info.id);
3036
+ this.#coordinator?.drop(info.id, `Cluster ${info.id} exited.`);
3037
+ this.#identifyWaiting.delete(info.id);
3038
+ this.#tracker.failOwner(info.id, `Cluster ${info.id} exited.`);
3039
+ this.clusters.delete(info.id);
3040
+ this.#allReady = false;
3041
+ try {
3042
+ this.#options.onClusterExit?.(info, code, signal);
3043
+ } catch {}
3044
+ const expected = this.#stopping || info.state === "stopping" || info.state === "stopped";
3045
+ this.#emit("clusterExit", info, code, signal, expected);
3046
+ if (expected) {
3047
+ info.state = "stopped";
3048
+ return;
3049
+ }
3050
+ this.#announceDown(info.id, info.shards);
3051
+ if (this.#options.restartOnExit === false) {
3052
+ info.state = "failed";
3053
+ this.#rejectReady(info, "restartOnExit is false");
3054
+ return;
3055
+ }
3056
+ const delay = this.#supervisor.next(info.id);
3057
+ if (delay === null) {
3058
+ info.state = "failed";
3059
+ try {
3060
+ this.#options.onGiveUp?.(info);
3061
+ } catch {}
3062
+ this.#emit("clusterGiveUp", info);
3063
+ if (this.#options.reassignOnGiveUp)
3064
+ this.#reassign(info);
3065
+ else
3066
+ this.#rejectReady(info, "restarts exhausted");
3067
+ return;
3068
+ }
3069
+ info.state = "restarting";
3070
+ info.restarts++;
3071
+ this.#emit("clusterRestart", info, { delay, attempt: info.restarts });
3072
+ if (info.restarts >= 3)
3073
+ this.#emit("clusterUnstable", info, {
3074
+ attempt: info.restarts,
3075
+ delay
3076
+ });
3077
+ const timer = setTimeout(() => {
3078
+ this.#restartTimers.delete(timer);
3079
+ if (this.#stopping || info.state !== "restarting")
3080
+ return;
3081
+ this.#launch(info.id, info.shards);
3082
+ }, delay);
3083
+ timer.unref?.();
3084
+ this.#restartTimers.add(timer);
3085
+ }
3086
+ async#reassign(failed) {
3087
+ const shardIds = [...failed.shards];
3088
+ const target = [...this.#infos.values()].filter((cluster) => cluster.state === "ready").sort((a, b) => a.shards.length - b.shards.length)[0];
3089
+ if (!target) {
3090
+ this.#emit("error", new Error(`No ready cluster can take over shards [${shardIds}] of cluster ${failed.id}.`));
3091
+ this.#rejectReady(failed, "no ready cluster to take them");
3092
+ return;
3093
+ }
3094
+ try {
3095
+ await this.requestCluster(target.id, REQUEST_ASSIGN, { shardIds }, { timeoutMs: this.#options.startTimeout ?? 120000 });
3096
+ } catch (error) {
3097
+ this.#emit("error", new Error(`Cluster ${target.id} could not take over shards [${shardIds}]: ${error instanceof Error ? error.message : String(error)}`));
3098
+ this.#rejectReady(failed, "reassignment failed");
3099
+ return;
3100
+ }
3101
+ if (this.#stopping || failed.state !== "failed")
3102
+ return;
3103
+ for (const shard of shardIds)
3104
+ this.#owner.set(shard, target.id);
3105
+ target.shards.push(...shardIds);
3106
+ failed.shards = [];
3107
+ this.#emit("shardsReassigned", {
3108
+ shardIds,
3109
+ from: failed.id,
3110
+ to: target.id
3111
+ });
3112
+ this.#checkReady();
3113
+ }
3114
+ #clearStartTimer(id) {
3115
+ const timer = this.#startTimers.get(id);
3116
+ if (timer)
3117
+ clearTimeout(timer);
3118
+ this.#startTimers.delete(id);
3119
+ }
3120
+ #sendFrame(cluster, frame) {
3121
+ const child = cluster.process;
3122
+ if (!isLive(cluster.state) || typeof child.send !== "function" || !child.connected)
3123
+ return false;
3124
+ try {
3125
+ child.send(frame);
3126
+ return true;
3127
+ } catch {
3128
+ return false;
3129
+ }
3130
+ }
3131
+ #announceDown(exited, shards) {
3132
+ for (const [id, cluster] of this.clusters)
3133
+ if (id !== exited)
3134
+ this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
3135
+ }
3136
+ #onFrame(info, child, frame) {
3137
+ if (info.process !== child)
3138
+ return;
3139
+ if (!frame || typeof frame !== "object")
3140
+ return;
3141
+ if (BUS_FRAME in frame) {
3142
+ this.#onBus(info, frame[BUS_FRAME]);
3143
+ return;
3144
+ }
3145
+ if (CLUSTER_FRAME in frame) {
3146
+ const body = parseChildFrame(frame[CLUSTER_FRAME]);
3147
+ if (body)
3148
+ this.#onControl(info, child, body);
3149
+ else
3150
+ this.#emit("error", new Error(`Cluster ${info.id} sent a malformed control frame.`));
3151
+ }
3152
+ }
3153
+ #onControl(info, child, frame) {
3154
+ switch (frame.kind) {
3155
+ case "ready": {
3156
+ if (!frame.shardIds.every((id) => info.shards.includes(id))) {
3157
+ this.#emit("error", new Error(`Cluster ${info.id} reported shards it does not own: [${frame.shardIds}].`));
3158
+ return;
3159
+ }
3160
+ this.#clearStartTimer(info.id);
3161
+ info.state = "ready";
3162
+ info.readyAt = info.lastReadyAt = Date.now();
3163
+ this.#emit("clusterReady", info);
3164
+ this.#checkReady();
3165
+ return;
3166
+ }
3167
+ case "identify": {
3168
+ const reply = (body) => {
3169
+ if (info.process === child)
3170
+ this.#sendFrame(info, { [CLUSTER_FRAME]: body });
3171
+ };
3172
+ if (!this.#coordinator) {
3173
+ reply({
3174
+ kind: "identify-deny",
3175
+ id: frame.id,
3176
+ error: "IDENTIFY coordination is off."
3177
+ });
3178
+ return;
3179
+ }
3180
+ if (!info.shards.includes(frame.shardId)) {
3181
+ reply({
3182
+ kind: "identify-deny",
3183
+ id: frame.id,
3184
+ error: `Shard ${frame.shardId} does not belong to cluster ${info.id}.`
3185
+ });
3186
+ return;
3187
+ }
3188
+ let waiting = this.#identifyWaiting.get(info.id);
3189
+ if (!waiting)
3190
+ this.#identifyWaiting.set(info.id, waiting = new Set);
3191
+ if (waiting.has(frame.shardId)) {
3192
+ reply({
3193
+ kind: "identify-deny",
3194
+ id: frame.id,
3195
+ error: `Shard ${frame.shardId} is already waiting for a slot.`
3196
+ });
3197
+ return;
3198
+ }
3199
+ waiting.add(frame.shardId);
3200
+ const settled = () => {
3201
+ waiting.delete(frame.shardId);
3202
+ };
3203
+ this.#coordinator.acquire(info.id, frame.shardId).then(() => {
3204
+ settled();
3205
+ reply({ kind: "identify-grant", id: frame.id });
3206
+ }, (error) => {
3207
+ settled();
3208
+ reply({
3209
+ kind: "identify-deny",
3210
+ id: frame.id,
3211
+ error: error instanceof Error ? error.message : String(error)
3212
+ });
3213
+ });
3214
+ return;
3215
+ }
3216
+ case "identify-done":
3217
+ this.#coordinator?.release(info.id, frame.shardId);
3218
+ return;
3219
+ case "reply": {
3220
+ const tooBig = frame.error === undefined && this.#tooBig(frame.result);
3221
+ this.#tracker.answer(frame.id, `c:${info.id}`, info.id, {
3222
+ ...frame.error !== undefined ? { error: frame.error } : tooBig ? {
3223
+ error: `Cluster ${info.id} answered with more than ${this.#maxBytes} bytes.`
3224
+ } : { result: frame.result }
3225
+ });
3226
+ return;
3227
+ }
3228
+ case "health":
3229
+ if (this.#tooBig(frame.report)) {
3230
+ this.#emit("error", new Error(`Cluster ${info.id} sent a health report of more than ${this.#maxBytes} bytes.`));
3231
+ return;
3232
+ }
3233
+ info.report = frame.report;
3234
+ return;
3235
+ }
3236
+ }
3237
+ get #maxBytes() {
3238
+ return this.#options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
3239
+ }
3240
+ #tooBig(value) {
3241
+ return payloadSize(value) > this.#maxBytes;
3242
+ }
3243
+ #assertSize(data, what) {
3244
+ if (this.#tooBig(data))
3245
+ throw new RangeError(`${what} is larger than maxMessageBytes (${this.#maxBytes}).`);
3246
+ }
3247
+ #onBus(from, raw) {
3248
+ let message = raw;
3249
+ if (!message || typeof message !== "object" || typeof message.type !== "string" || message.type.length === 0 || message.type.length > 256 || message.id === undefined || typeof message.id !== "string" || message.id.length > 256 || !Number.isInteger(message.source) || message.target !== null && !Number.isInteger(message.target))
3250
+ return;
3251
+ if (!from.shards.includes(message.source))
3252
+ return;
3253
+ const { type, id, source, target, data, expectsReply } = message;
3254
+ message = {
3255
+ type,
3256
+ id,
3257
+ source,
3258
+ target,
3259
+ data,
3260
+ ...expectsReply === undefined ? {} : { expectsReply }
3261
+ };
3262
+ if (this.#tooBig(message.data)) {
3263
+ this.#emit("error", new Error(`Cluster ${from.id} sent a shard-bus message of more than ${this.#maxBytes} bytes; it was not relayed.`));
3264
+ return;
3265
+ }
3266
+ const frame = { [BUS_FRAME]: message };
3267
+ if (message.target === PARENT_ID) {
3268
+ const reply = message.data;
3269
+ if (message.type === REPLY_TYPE && reply && typeof reply.requestId === "string")
3270
+ this.#tracker.answer(reply.requestId, `s:${message.source}`, from.id, reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result });
3271
+ return;
3272
+ }
3273
+ if (message.target === null) {
3274
+ for (const [id, cluster] of this.clusters)
3275
+ if (id !== from.id)
3276
+ this.#sendFrame(cluster, frame);
3277
+ return;
3278
+ }
3279
+ const owner = this.clusterForShard(message.target);
3280
+ if (!owner || owner.id === from.id)
3281
+ return;
3282
+ if (!this.#sendFrame(owner, frame))
3283
+ this.#sendFrame(from, {
3284
+ [PEER_DOWN_FRAME]: [message.target]
3285
+ });
3286
+ }
3287
+ async request(shardId, type, data, options = {}) {
3288
+ this.#assertSize(data, "The request data");
3289
+ const owner = this.clusterForShard(shardId);
3290
+ if (!owner)
3291
+ throw new RangeError(`Shard ${shardId} is not part of this deployment.`);
3292
+ const { id, done } = this.#tracker.begin({
3293
+ keys: new Map([[`s:${shardId}`, owner.id]]),
3294
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
3295
+ signal: options.signal
3296
+ });
3297
+ const sent = this.#sendFrame(owner, {
3298
+ [BUS_FRAME]: {
3299
+ source: PARENT_ID,
3300
+ target: shardId,
3301
+ type,
3302
+ data,
3303
+ id,
3304
+ expectsReply: true
3305
+ }
3306
+ });
3307
+ if (!sent) {
3308
+ this.#tracker.failOwner(owner.id, `Shard ${shardId} is down: cluster ${owner.id} is ${owner.state}.`);
3309
+ }
3310
+ const { replies } = await done;
3311
+ const reply = replies[0];
3312
+ if (!reply)
3313
+ throw new Error(`Shard ${shardId} did not reply to "${type}" in time.`);
3314
+ if (reply.error !== undefined)
3315
+ throw new Error(reply.error);
3316
+ return reply.result;
3317
+ }
3318
+ async broadcast(type, data, options = {}) {
3319
+ this.#assertSize(data, "The request data");
3320
+ const keys = new Map;
3321
+ const down = [];
3322
+ for (const [shard, clusterId] of this.#owner) {
3323
+ const cluster = this.#infos.get(clusterId);
3324
+ if (cluster && isLive(cluster.state))
3325
+ keys.set(`s:${shard}`, clusterId);
3326
+ else
3327
+ down.push(shard);
3328
+ }
3329
+ const { id, done } = this.#tracker.begin({
3330
+ keys,
3331
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
3332
+ signal: options.signal
3333
+ });
3334
+ const frame = {
3335
+ [BUS_FRAME]: {
3336
+ source: PARENT_ID,
3337
+ target: null,
3338
+ type,
3339
+ data,
3340
+ id,
3341
+ expectsReply: true
3342
+ }
3343
+ };
3344
+ for (const cluster of this.clusters.values())
3345
+ if (!this.#sendFrame(cluster, frame))
3346
+ this.#tracker.failOwner(cluster.id, `Cluster ${cluster.id} is ${cluster.state}.`);
3347
+ const { replies, missing } = await done;
3348
+ return {
3349
+ replies: replies.map((reply) => ({
3350
+ shardId: Number(reply.key.slice(2)),
3351
+ ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
3352
+ })),
3353
+ missing: [
3354
+ ...down,
3355
+ ...missing.map((key) => Number(key.slice(2)))
3356
+ ].sort((a, b) => a - b)
3357
+ };
3358
+ }
3359
+ async requestCluster(clusterId, type, data, options = {}) {
3360
+ this.#assertSize(data, "The request data");
3361
+ const cluster = this.#infos.get(clusterId);
3362
+ if (!cluster)
3363
+ throw new RangeError(`Cluster ${clusterId} does not exist.`);
3364
+ const { id, done } = this.#tracker.begin({
3365
+ keys: new Map([[`c:${clusterId}`, clusterId]]),
3366
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
3367
+ signal: options.signal
3368
+ });
3369
+ if (!this.#sendFrame(cluster, {
3370
+ [CLUSTER_FRAME]: { kind: "request", id, type, data }
3371
+ }))
3372
+ this.#tracker.failOwner(clusterId, `Cluster ${clusterId} is ${cluster.state}.`);
3373
+ const { replies } = await done;
3374
+ const reply = replies[0];
3375
+ if (!reply)
3376
+ throw new Error(`Cluster ${clusterId} did not reply to "${type}" in time.`);
3377
+ if (reply.error !== undefined)
3378
+ throw new Error(reply.error);
3379
+ return reply.result;
3380
+ }
3381
+ ready(timeoutMs) {
3382
+ if (this.#isReady())
3383
+ return Promise.resolve();
3384
+ return new Promise((resolve, reject) => {
3385
+ const waiter = {
3386
+ resolve: () => {
3387
+ if (waiter.timer)
3388
+ clearTimeout(waiter.timer);
3389
+ this.#readyWaiters.delete(waiter);
3390
+ resolve();
3391
+ },
3392
+ reject: (error) => {
3393
+ if (waiter.timer)
3394
+ clearTimeout(waiter.timer);
3395
+ this.#readyWaiters.delete(waiter);
3396
+ reject(error);
3397
+ }
3398
+ };
3399
+ if (timeoutMs !== undefined)
3400
+ waiter.timer = setTimeout(() => waiter.reject(new Error(`Clusters were not all ready within ${timeoutMs}ms.`)), timeoutMs);
3401
+ this.#readyWaiters.add(waiter);
3402
+ });
3403
+ }
3404
+ #isReady() {
3405
+ let ready = 0;
3406
+ for (const cluster of this.#infos.values()) {
3407
+ if (cluster.state === "ready")
3408
+ ready++;
3409
+ else if (!(cluster.state === "failed" && cluster.shards.length === 0))
3410
+ return false;
3411
+ }
3412
+ return ready > 0;
3413
+ }
3414
+ #rejectReady(failed, why) {
3415
+ if (failed.shards.length === 0)
3416
+ return;
3417
+ const error = new Error(`Cluster ${failed.id} failed and shards [${failed.shards}] have no owner (${why}).`);
3418
+ for (const waiter of [...this.#readyWaiters])
3419
+ waiter.reject(error);
3420
+ }
3421
+ #checkReady() {
3422
+ if (!this.#isReady())
3423
+ return;
3424
+ for (const waiter of [...this.#readyWaiters])
3425
+ waiter.resolve();
3426
+ if (!this.#allReady) {
3427
+ this.#allReady = true;
3428
+ this.#emit("ready");
3429
+ }
3430
+ }
3431
+ health() {
3432
+ const now = Date.now();
3433
+ const clusters = [...this.#infos.values()].sort((a, b) => a.id - b.id).map((cluster) => ({
3434
+ id: cluster.id,
3435
+ state: cluster.state,
3436
+ ...cluster.process.pid !== undefined && isLive(cluster.state) ? { pid: cluster.process.pid } : {},
3437
+ host: cluster.host,
3438
+ shardIds: [...cluster.shards],
3439
+ uptimeMs: isLive(cluster.state) ? now - cluster.startedAt : 0,
3440
+ restarts: cluster.restarts,
3441
+ startedAt: cluster.startedAt,
3442
+ ...cluster.readyAt !== undefined ? { readyAt: cluster.readyAt } : {},
3443
+ ...cluster.lastReadyAt !== undefined ? { lastReadyAt: cluster.lastReadyAt } : {},
3444
+ shards: cluster.report?.shards ?? [],
3445
+ ...cluster.report?.metrics ? { metrics: cluster.report.metrics } : {},
3446
+ ...cluster.report ? { reportedAt: cluster.report.reportedAt } : {}
3447
+ }));
3448
+ const missingShards = [...this.#owner].filter(([, id]) => this.#infos.get(id)?.state !== "ready").map(([shard]) => shard).sort((a, b) => a - b);
3449
+ const states = clusters.map((cluster) => cluster.state);
3450
+ 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";
3451
+ return {
3452
+ state,
3453
+ totalShards: this.shardCount,
3454
+ clusters,
3455
+ readyClusters: states.filter((s) => s === "ready").length,
3456
+ missingShards
3457
+ };
3458
+ }
3459
+ async refreshHealth(options = {}) {
3460
+ await Promise.all([...this.clusters.values()].map(async (cluster) => {
3461
+ try {
3462
+ cluster.report = await this.requestCluster(cluster.id, REQUEST_HEALTH, { metrics: options.metrics === true }, { timeoutMs: options.timeoutMs ?? 2000 });
3463
+ } catch {}
3464
+ }));
3465
+ return this.health();
3466
+ }
3467
+ async checkAutoScale() {
3468
+ if (!this.#auto)
3469
+ return;
3470
+ try {
3471
+ const recommended = await this.fetchRecommendedShardCount();
3472
+ if (recommended !== this.shardCount) {
3473
+ await this.respawn(recommended);
3474
+ }
3475
+ } catch (error) {
3476
+ const normalized = error instanceof Error ? error : new Error(String(error));
3477
+ this.#options.onAutoScaleError?.(normalized);
3478
+ }
3479
+ }
3480
+ async respawn(newShardCount) {
3481
+ this.#options.shardCount = newShardCount;
3482
+ await this.#stop(this.#options.shutdownTimeout ?? 5000, true);
3483
+ this.#supervisor.reset();
3484
+ await this.spawn();
3485
+ }
3486
+ shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
3487
+ return this.#stop(timeoutMs, false);
3488
+ }
3489
+ #stop(timeoutMs, keepRemote) {
3490
+ this.#stopPromise ??= this.#shutdown(timeoutMs, keepRemote).finally(() => {
3491
+ this.#stopPromise = undefined;
3492
+ });
3493
+ return this.#stopPromise;
3494
+ }
3495
+ async#shutdown(timeoutMs, keepRemote) {
3496
+ this.#stopping = true;
3497
+ this.#halt();
3498
+ await this.#startPromise?.catch(() => {
3499
+ return;
3500
+ });
3501
+ await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
3502
+ for (const cluster of this.#infos.values())
3503
+ cluster.state = "stopped";
3504
+ this.clusters.clear();
3505
+ this.#startPromise = undefined;
3506
+ if (!keepRemote)
3507
+ this.#remote?.stop();
3508
+ this.#stopping = false;
3509
+ }
3510
+ #halt() {
3511
+ for (const timer of this.#restartTimers)
3512
+ clearTimeout(timer);
3513
+ this.#restartTimers.clear();
3514
+ for (const id of [...this.#startTimers.keys()])
3515
+ this.#clearStartTimer(id);
3516
+ for (const wake of [...this.#pauses])
3517
+ wake();
3518
+ if (this.#autoScaleTimer) {
3519
+ clearInterval(this.#autoScaleTimer);
3520
+ this.#autoScaleTimer = undefined;
3521
+ }
3522
+ this.#coordinator?.close();
3523
+ this.#coordinator = undefined;
3524
+ this.#remote?.abortWaits();
3525
+ this.#tracker.close();
3526
+ for (const waiter of [...this.#readyWaiters])
3527
+ waiter.reject(new Error("The cluster manager is stopping."));
3528
+ }
3529
+ async#shutdownCluster(cluster, timeoutMs) {
3530
+ const child = cluster.process;
3531
+ cluster.state = "stopping";
3532
+ if (child.exitCode !== null || child.signalCode !== null)
3533
+ return;
3534
+ await new Promise((resolve) => {
3535
+ let timer;
3536
+ const finish = () => {
3537
+ if (timer)
3538
+ clearTimeout(timer);
3539
+ resolve();
3540
+ };
3541
+ child.once("exit", finish);
3542
+ try {
3543
+ child.kill("SIGTERM");
3544
+ } catch {
3545
+ child.off("exit", finish);
3546
+ finish();
1765
3547
  return;
1766
3548
  }
1767
3549
  timer = setTimeout(() => {
@@ -1773,24 +3555,322 @@ class ClusterManager {
1773
3555
  }
1774
3556
  killAll() {
1775
3557
  this.#stopping = true;
1776
- this.#clearRestartTimers();
1777
- if (this.#autoScaleTimer) {
1778
- clearInterval(this.#autoScaleTimer);
1779
- this.#autoScaleTimer = undefined;
1780
- }
1781
- for (const cluster of this.clusters.values()) {
3558
+ this.#halt();
3559
+ for (const cluster of this.#infos.values()) {
3560
+ cluster.state = "stopped";
1782
3561
  try {
1783
3562
  cluster.process.kill();
1784
3563
  } catch {}
1785
3564
  }
1786
3565
  this.clusters.clear();
1787
- this.#spawned = false;
3566
+ this.#startPromise = undefined;
3567
+ this.#remote?.stop();
1788
3568
  this.#stopping = false;
1789
3569
  }
1790
3570
  }
3571
+ // packages/sharding/src/host.ts
3572
+ import { fork as fork2 } from "child_process";
3573
+ class ClusterHost {
3574
+ #options;
3575
+ #running = new Map;
3576
+ #listeners = new Map;
3577
+ #socket;
3578
+ #lease = 15000;
3579
+ #lastRx = 0;
3580
+ #watchdog;
3581
+ #retry;
3582
+ #delay;
3583
+ #busyAttempts = 0;
3584
+ #closed = true;
3585
+ #connected = false;
3586
+ #firstConnect;
3587
+ constructor(options) {
3588
+ if (!HOST_ID.test(options.id))
3589
+ throw new RangeError("id must be 1-64 letters, digits, '.', '_' or '-'.");
3590
+ if (!options.secret || options.secret.length < 16)
3591
+ throw new RangeError("secret must be at least 16 characters.");
3592
+ if (!options.script?.trim())
3593
+ throw new TypeError("A worker script path is required.");
3594
+ let url;
3595
+ try {
3596
+ url = new URL(options.url);
3597
+ } catch {
3598
+ throw new TypeError("url must be an absolute ws:// or wss:// URL.");
3599
+ }
3600
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
3601
+ throw new TypeError("url must start with ws:// or wss://.");
3602
+ if (url.username || url.password)
3603
+ throw new TypeError("url must not contain credentials.");
3604
+ if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
3605
+ throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
3606
+ this.#options = options;
3607
+ this.#delay = options.reconnectDelay ?? 1000;
3608
+ }
3609
+ get connected() {
3610
+ return this.#connected;
3611
+ }
3612
+ clusters() {
3613
+ return [...this.#running.keys()].sort((a, b) => a - b);
3614
+ }
3615
+ on(event, listener) {
3616
+ let set = this.#listeners.get(event);
3617
+ if (!set)
3618
+ this.#listeners.set(event, set = new Set);
3619
+ set.add(listener);
3620
+ return this;
3621
+ }
3622
+ off(event, listener) {
3623
+ this.#listeners.get(event)?.delete(listener);
3624
+ return this;
3625
+ }
3626
+ #emit(event, ...args) {
3627
+ for (const listener of this.#listeners.get(event) ?? [])
3628
+ try {
3629
+ listener(...args);
3630
+ } catch {}
3631
+ }
3632
+ connect() {
3633
+ if (!this.#closed)
3634
+ return Promise.resolve();
3635
+ this.#closed = false;
3636
+ this.#busyAttempts = 0;
3637
+ return new Promise((resolve, reject) => {
3638
+ this.#firstConnect = { resolve, reject };
3639
+ this.#open();
3640
+ });
3641
+ }
3642
+ close() {
3643
+ this.#closed = true;
3644
+ if (this.#retry)
3645
+ clearTimeout(this.#retry);
3646
+ this.#retry = undefined;
3647
+ this.#fence();
3648
+ if (this.#watchdog)
3649
+ clearInterval(this.#watchdog);
3650
+ this.#watchdog = undefined;
3651
+ this.#connected = false;
3652
+ const socket = this.#socket;
3653
+ this.#socket = undefined;
3654
+ this.#firstConnect?.reject(new Error("The host was closed."));
3655
+ this.#firstConnect = undefined;
3656
+ if (!socket || socket.readyState === WebSocket.CLOSED)
3657
+ return Promise.resolve();
3658
+ return new Promise((resolve) => {
3659
+ socket.addEventListener("close", () => resolve(), { once: true });
3660
+ try {
3661
+ socket.close(1000, "Host closed.");
3662
+ } catch {
3663
+ resolve();
3664
+ }
3665
+ });
3666
+ }
3667
+ #open() {
3668
+ const socket = new WebSocket(this.#options.url);
3669
+ this.#socket = socket;
3670
+ socket.addEventListener("message", (event) => this.#message(socket, event.data));
3671
+ socket.addEventListener("error", () => {});
3672
+ socket.addEventListener("close", (event) => {
3673
+ if (this.#socket === socket)
3674
+ this.#lost(event.code, event.reason);
3675
+ });
3676
+ }
3677
+ #send(socket, message) {
3678
+ if (socket.readyState !== WebSocket.OPEN)
3679
+ return false;
3680
+ try {
3681
+ socket.send(JSON.stringify(message));
3682
+ return true;
3683
+ } catch {
3684
+ return false;
3685
+ }
3686
+ }
3687
+ #refuseSpawn(socket, data) {
3688
+ if (typeof data !== "string" || data.length > 65536)
3689
+ return;
3690
+ let frame;
3691
+ try {
3692
+ frame = JSON.parse(data);
3693
+ } catch {
3694
+ return;
3695
+ }
3696
+ if (frame?.t !== "spawn" || !Number.isInteger(frame.cluster) || !Number.isInteger(frame.epoch))
3697
+ return;
3698
+ this.#send(socket, {
3699
+ t: "error",
3700
+ cluster: frame.cluster,
3701
+ epoch: frame.epoch,
3702
+ message: "This host refused the spawn request (malformed or not allowed)."
3703
+ });
3704
+ }
3705
+ #message(socket, data) {
3706
+ if (this.#socket !== socket)
3707
+ return;
3708
+ const message = parseToHost(data);
3709
+ if (!message) {
3710
+ this.#emit("error", new Error("The coordinator sent a malformed frame."));
3711
+ this.#refuseSpawn(socket, data);
3712
+ return;
3713
+ }
3714
+ this.#lastRx = Date.now();
3715
+ switch (message.t) {
3716
+ case "challenge":
3717
+ if (message.version !== REMOTE_PROTOCOL_VERSION) {
3718
+ socket.close(RemoteClose.VersionMismatch, "Protocol version mismatch.");
3719
+ return;
3720
+ }
3721
+ this.#send(socket, {
3722
+ t: "hello",
3723
+ id: this.#options.id,
3724
+ mac: signChallenge(this.#options.secret, message.nonce, this.#options.id),
3725
+ version: REMOTE_PROTOCOL_VERSION
3726
+ });
3727
+ return;
3728
+ case "welcome":
3729
+ this.#lease = message.leaseMs;
3730
+ this.#connected = true;
3731
+ this.#delay = this.#options.reconnectDelay ?? 1000;
3732
+ this.#startWatchdog();
3733
+ this.#firstConnect?.resolve();
3734
+ this.#firstConnect = undefined;
3735
+ this.#emit("connected");
3736
+ return;
3737
+ case "ping":
3738
+ this.#send(socket, { t: "pong", n: message.n });
3739
+ return;
3740
+ case "bye":
3741
+ this.#fence();
3742
+ return;
3743
+ case "spawn":
3744
+ this.#spawn(socket, message.cluster, message.epoch, message.env);
3745
+ return;
3746
+ case "frame": {
3747
+ const run = this.#running.get(message.cluster);
3748
+ if (run?.epoch === message.epoch && run.child.connected)
3749
+ try {
3750
+ run.child.send(message.frame);
3751
+ } catch {}
3752
+ return;
3753
+ }
3754
+ case "kill": {
3755
+ const run = this.#running.get(message.cluster);
3756
+ if (run?.epoch === message.epoch && KILL_SIGNALS.has(message.signal))
3757
+ try {
3758
+ run.child.kill(message.signal);
3759
+ } catch {}
3760
+ return;
3761
+ }
3762
+ }
3763
+ }
3764
+ #spawn(socket, cluster, epoch, env) {
3765
+ const reply = (message) => {
3766
+ if (this.#socket === socket)
3767
+ this.#send(socket, message);
3768
+ };
3769
+ if (this.#running.has(cluster)) {
3770
+ reply({
3771
+ t: "error",
3772
+ cluster,
3773
+ epoch,
3774
+ message: `Cluster ${cluster} is already running here.`
3775
+ });
3776
+ return;
3777
+ }
3778
+ let child;
3779
+ try {
3780
+ const forkWorker = this.#options.fork ?? defaultFork;
3781
+ child = forkWorker(this.#options.script, {
3782
+ ...process.env,
3783
+ ...env
3784
+ });
3785
+ } catch (error) {
3786
+ reply({
3787
+ t: "error",
3788
+ cluster,
3789
+ epoch,
3790
+ message: error instanceof Error ? error.message : String(error)
3791
+ });
3792
+ return;
3793
+ }
3794
+ this.#running.set(cluster, { epoch, child });
3795
+ child.on("message", (frame) => reply({ t: "frame", cluster, epoch, frame }));
3796
+ child.once("exit", (code, signal) => {
3797
+ if (this.#running.get(cluster)?.child === child)
3798
+ this.#running.delete(cluster);
3799
+ this.#emit("clusterExit", cluster, code, signal);
3800
+ reply({ t: "exit", cluster, epoch, code, signal });
3801
+ });
3802
+ child.on("error", (error) => this.#emit("error", error instanceof Error ? error : new Error(String(error))));
3803
+ reply({
3804
+ t: "spawned",
3805
+ cluster,
3806
+ epoch,
3807
+ ...child.pid ? { pid: child.pid } : {}
3808
+ });
3809
+ this.#emit("clusterSpawn", cluster);
3810
+ }
3811
+ #fence() {
3812
+ for (const { child } of [...this.#running.values()])
3813
+ try {
3814
+ child.kill("SIGKILL");
3815
+ } catch {}
3816
+ }
3817
+ #startWatchdog() {
3818
+ if (this.#watchdog)
3819
+ clearInterval(this.#watchdog);
3820
+ this.#lastRx = Date.now();
3821
+ this.#watchdog = setInterval(() => {
3822
+ if (Date.now() - this.#lastRx > this.#lease) {
3823
+ this.#fence();
3824
+ this.#socket?.close(RemoteClose.HostLost, "Coordinator silent.");
3825
+ }
3826
+ }, Math.max(10, Math.floor(this.#lease / 3)));
3827
+ this.#watchdog.unref?.();
3828
+ }
3829
+ #lost(code, reason) {
3830
+ if (this.#watchdog)
3831
+ clearInterval(this.#watchdog);
3832
+ this.#watchdog = undefined;
3833
+ this.#socket = undefined;
3834
+ const wasConnected = this.#connected;
3835
+ this.#connected = false;
3836
+ this.#fence();
3837
+ if (wasConnected)
3838
+ this.#emit("disconnected", { code, reason });
3839
+ if (this.#closed)
3840
+ return;
3841
+ const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
3842
+ const first = this.#firstConnect;
3843
+ const busy = code === RemoteClose.TooManyConnections || code === RemoteClose.HelloTimeout;
3844
+ if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
3845
+ this.#retryLater();
3846
+ return;
3847
+ }
3848
+ if (first) {
3849
+ this.#firstConnect = undefined;
3850
+ this.#closed = true;
3851
+ first.reject(new Error(`The coordinator refused this host (${code}${reason ? `: ${reason}` : ""}).`));
3852
+ return;
3853
+ }
3854
+ if (refused)
3855
+ this.#emit("error", new Error(`The coordinator refused this host (${code}: ${reason}).`));
3856
+ this.#retryLater();
3857
+ }
3858
+ #retryLater() {
3859
+ const wait = Math.round(this.#delay * (1 + Math.random() * 0.2));
3860
+ this.#delay = Math.min(this.#options.maxReconnectDelay ?? 30000, this.#delay * 2);
3861
+ this.#retry = setTimeout(() => {
3862
+ this.#retry = undefined;
3863
+ if (!this.#closed)
3864
+ this.#open();
3865
+ }, wait);
3866
+ }
3867
+ }
3868
+ function defaultFork(script, env) {
3869
+ return fork2(script, [], { env });
3870
+ }
1791
3871
 
1792
3872
  // packages/sharding/src/index.ts
1793
- var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3873
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1794
3874
  function needsConnection(gateway) {
1795
3875
  return gateway.state === "CONNECT" /* Connect */ || gateway.state === "RECONNECT" /* Reconnect */ || gateway.state === "CLOSED" /* Closed */;
1796
3876
  }
@@ -1824,12 +3904,21 @@ class ShardManager {
1824
3904
  static IDENTIFY_INTERVAL = 5000;
1825
3905
  shards = new Map;
1826
3906
  get shardCount() {
1827
- return this.shards.size;
3907
+ return this.#resolved ? this.#total : 0;
3908
+ }
3909
+ get shardIds() {
3910
+ return [...this.shards.keys()].sort((a, b) => a - b);
1828
3911
  }
1829
3912
  #options;
3913
+ #ids;
3914
+ #gate;
3915
+ #worker;
3916
+ #lifecycle = new Map;
3917
+ #announced = false;
1830
3918
  #resolved = false;
1831
3919
  #destroyed = false;
1832
3920
  #auto;
3921
+ #fixedCount;
1833
3922
  #autoScaleTimer;
1834
3923
  #total;
1835
3924
  #listeners = new Map;
@@ -1839,9 +3928,30 @@ class ShardManager {
1839
3928
  throw new TypeError("A Discord bot token is required.");
1840
3929
  if (!Array.isArray(options.intents))
1841
3930
  throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds].");
1842
- const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
3931
+ const context = options.cluster === false ? null : options.cluster ?? readClusterContext();
3932
+ if (context) {
3933
+ if (options.shardCount === "auto")
3934
+ throw new RangeError(`shardCount "auto" is the ClusterManager's decision inside a cluster worker.`);
3935
+ if (options.shardCount !== undefined && options.shardCount !== context.shardCount)
3936
+ throw new RangeError(`shardCount ${options.shardCount} differs from the cluster's ${context.shardCount}.`);
3937
+ if (options.autoScaleInterval !== undefined)
3938
+ throw new TypeError("autoScaleInterval belongs to ClusterManager inside a cluster worker.");
3939
+ if (options.shardIds && !options.shardIds.every((id) => context.shardIds.includes(id)))
3940
+ throw new RangeError("shardIds must be a subset of the cluster's own shards.");
3941
+ }
3942
+ const count = context ? context.shardCount : options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
1843
3943
  if (!Number.isInteger(count) || count < 1)
1844
3944
  throw new RangeError("Shard count must be a positive integer.");
3945
+ if (options.guildsPerShard !== undefined) {
3946
+ assertGuildsPerShard(options.guildsPerShard);
3947
+ if (options.shardCount !== "auto")
3948
+ throw new RangeError('guildsPerShard needs shardCount: "auto".');
3949
+ }
3950
+ this.#ids = options.shardIds ?? context?.shardIds;
3951
+ if (this.#ids && options.shardCount !== "auto")
3952
+ validateShardIds(this.#ids, count);
3953
+ this.#worker = options.worker === false ? undefined : options.worker ?? (context && options.cluster === undefined ? ClusterWorker.current() : undefined);
3954
+ this.#gate = options.identify === false ? undefined : options.identify ?? (context?.coordinatedIdentify ? this.#worker?.identifyGate : undefined);
1845
3955
  if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
1846
3956
  throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
1847
3957
  if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
@@ -1851,16 +3961,25 @@ class ShardManager {
1851
3961
  if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
1852
3962
  throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
1853
3963
  this.#total = count;
3964
+ this.#fixedCount = count;
1854
3965
  this.#auto = options.shardCount === "auto";
1855
3966
  this.#options = {
1856
3967
  spawnDelay: ShardManager.IDENTIFY_INTERVAL,
1857
3968
  ...options
1858
3969
  };
3970
+ this.#worker?.onRequest(REQUEST_HEALTH, (data) => this.report(data?.metrics === true));
3971
+ this.#worker?.onRequest(REQUEST_ASSIGN, async (data) => {
3972
+ const ids = data?.shardIds;
3973
+ if (!Array.isArray(ids))
3974
+ throw new TypeError("shardIds must be an array.");
3975
+ await this.addShards(ids);
3976
+ return { shardIds: ids };
3977
+ });
1859
3978
  if (options.shardCount !== "auto")
1860
3979
  this.#initialize(count);
1861
3980
  }
1862
3981
  async fetchRecommendedShardCount() {
1863
- return (await this.fetchGatewayInfo()).shards;
3982
+ return resolveShardCount(this.#options.token, await this.fetchGatewayInfo(), this.#options);
1864
3983
  }
1865
3984
  async fetchGatewayInfo() {
1866
3985
  const info = await fetchGatewayBot(this.#options.token);
@@ -1873,8 +3992,63 @@ class ShardManager {
1873
3992
  this.#autoScaleTimer = undefined;
1874
3993
  }
1875
3994
  await this.#ensureInitialized();
3995
+ await this.#start([...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id));
3996
+ if (this.#options.autoScaleInterval && this.#auto && !this.#autoScaleTimer) {
3997
+ this.#autoScaleTimer = setInterval(() => {
3998
+ this.checkAutoScale();
3999
+ }, this.#options.autoScaleInterval);
4000
+ }
4001
+ }
4002
+ async addShards(shardIds) {
4003
+ await this.#ensureInitialized();
4004
+ validateShardIds(shardIds, this.#total);
4005
+ for (const id of shardIds)
4006
+ if (this.shards.has(id))
4007
+ throw new RangeError(`Shard ${id} is already owned.`);
4008
+ for (const id of shardIds)
4009
+ this.shards.set(id, this.#createShard(id, this.#total));
4010
+ if (this.#ids)
4011
+ this.#ids = [...this.#ids, ...shardIds];
4012
+ this.#announced = false;
4013
+ try {
4014
+ await this.#start([...shardIds], false);
4015
+ } catch (error) {
4016
+ for (const id of shardIds) {
4017
+ this.shards.get(id)?.close();
4018
+ this.shards.delete(id);
4019
+ this.#lifecycle.delete(id);
4020
+ }
4021
+ if (this.#ids)
4022
+ this.#ids = this.#ids.filter((id) => !shardIds.includes(id));
4023
+ throw error;
4024
+ }
4025
+ }
4026
+ async#start(ids, destroyOnFailure = true) {
4027
+ const gate = this.#gate;
4028
+ if (gate) {
4029
+ const timeout = this.#options.handshakeTimeout ?? 15000;
4030
+ const results = await Promise.allSettled(ids.map(async (id) => {
4031
+ const gateway = this.shards.get(id);
4032
+ this.#setStatus(id, "connecting");
4033
+ await gate.acquire(id);
4034
+ try {
4035
+ await gateway.connect();
4036
+ await handshakeSent(gateway, timeout);
4037
+ } finally {
4038
+ gate.release(id);
4039
+ }
4040
+ }));
4041
+ const failed = results.findIndex((result) => result.status === "rejected");
4042
+ if (failed !== -1) {
4043
+ if (destroyOnFailure)
4044
+ this.destroy();
4045
+ throw new Error(`Failed to connect shard ${ids[failed]}.`, {
4046
+ cause: results[failed].reason
4047
+ });
4048
+ }
4049
+ return;
4050
+ }
1876
4051
  const limit = this.#gatewayInfo?.sessionStartLimit;
1877
- const ids = [...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id);
1878
4052
  if (limit && limit.remaining < ids.length)
1879
4053
  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.`);
1880
4054
  const concurrency = Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
@@ -1890,24 +4064,24 @@ class ShardManager {
1890
4064
  const rounds = Math.max(0, ...[...buckets.values()].map((bucket) => bucket.length));
1891
4065
  for (let index = 0;index < rounds; index++) {
1892
4066
  const round = [...buckets.values()].map((bucket) => bucket[index]).filter((id) => id !== undefined);
4067
+ for (const id of round)
4068
+ this.#setStatus(id, "connecting");
1893
4069
  const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
1894
4070
  const failed = results.findIndex((result) => result.status === "rejected");
4071
+ if (limit)
4072
+ limit.remaining = Math.max(0, limit.remaining - results.filter((r) => r.status === "fulfilled").length);
1895
4073
  if (failed !== -1) {
1896
- this.destroy();
4074
+ if (destroyOnFailure)
4075
+ this.destroy();
1897
4076
  throw new Error(`Failed to connect shard ${round[failed]}.`, {
1898
4077
  cause: results[failed].reason
1899
4078
  });
1900
4079
  }
1901
4080
  if (index + 1 < rounds && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
1902
4081
  await Promise.all(round.map((id) => handshakeSent(this.shards.get(id), this.#options.handshakeTimeout ?? 15000)));
1903
- await sleep2(this.#options.spawnDelay);
4082
+ await sleep(this.#options.spawnDelay);
1904
4083
  }
1905
4084
  }
1906
- if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
1907
- this.#autoScaleTimer = setInterval(() => {
1908
- this.checkAutoScale();
1909
- }, this.#options.autoScaleInterval);
1910
- }
1911
4085
  }
1912
4086
  async checkAutoScale() {
1913
4087
  if (!this.#auto)
@@ -1929,7 +4103,11 @@ class ShardManager {
1929
4103
  destroy() {
1930
4104
  for (const shard of this.shards.values())
1931
4105
  shard.close();
4106
+ for (const id of this.shards.keys())
4107
+ this.#setStatus(id, "destroyed");
1932
4108
  this.shards.clear();
4109
+ this.#lifecycle.clear();
4110
+ this.#announced = false;
1933
4111
  this.#resolved = false;
1934
4112
  this.#destroyed = true;
1935
4113
  if (this.#autoScaleTimer) {
@@ -1965,11 +4143,60 @@ class ShardManager {
1965
4143
  return this.shards.get(id);
1966
4144
  }
1967
4145
  health() {
1968
- return [...this.shards].map(([id, gateway]) => ({
1969
- id,
1970
- state: gateway.state,
1971
- ping: gateway.ping
1972
- }));
4146
+ return [...this.shards].map(([id, gateway]) => {
4147
+ const life = this.#lifecycle.get(id);
4148
+ return {
4149
+ id,
4150
+ state: gateway.state,
4151
+ ping: gateway.ping,
4152
+ status: life?.status ?? "idle",
4153
+ ...life?.readyAt !== undefined ? { readyAt: life.readyAt } : {},
4154
+ ...life?.disconnectedAt !== undefined ? { disconnectedAt: life.disconnectedAt } : {}
4155
+ };
4156
+ });
4157
+ }
4158
+ report(metrics = false) {
4159
+ let numbers;
4160
+ if (metrics) {
4161
+ const memory = process.memoryUsage();
4162
+ const cpu = process.cpuUsage();
4163
+ numbers = {
4164
+ rssBytes: memory.rss,
4165
+ heapUsedBytes: memory.heapUsed,
4166
+ cpuUserMicros: cpu.user,
4167
+ cpuSystemMicros: cpu.system
4168
+ };
4169
+ }
4170
+ return {
4171
+ shards: this.health(),
4172
+ ...numbers ? { metrics: numbers } : {},
4173
+ reportedAt: Date.now()
4174
+ };
4175
+ }
4176
+ #setStatus(id, status) {
4177
+ const life = this.#lifecycle.get(id) ?? { status };
4178
+ life.status = status;
4179
+ const now = Date.now();
4180
+ if (status === "ready")
4181
+ life.readyAt = now;
4182
+ if (status === "disconnected")
4183
+ life.disconnectedAt = now;
4184
+ this.#lifecycle.set(id, life);
4185
+ if (status !== "ready")
4186
+ this.#announced = false;
4187
+ else
4188
+ this.#maybeAnnounce();
4189
+ }
4190
+ #maybeAnnounce() {
4191
+ if (this.#announced || this.shards.size === 0)
4192
+ return;
4193
+ for (const id of this.shards.keys())
4194
+ if (this.#lifecycle.get(id)?.status !== "ready")
4195
+ return;
4196
+ this.#announced = true;
4197
+ this.#emit("ready");
4198
+ this.#worker?.ready(this.shardIds);
4199
+ this.#worker?.reportHealth(this.report());
1973
4200
  }
1974
4201
  values() {
1975
4202
  return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
@@ -1979,14 +4206,16 @@ class ShardManager {
1979
4206
  return;
1980
4207
  if (this.#destroyed)
1981
4208
  this.#destroyed = false;
1982
- const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#options.shardCount ?? 1;
4209
+ const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#fixedCount;
1983
4210
  this.#initialize(count);
1984
4211
  }
1985
4212
  #initialize(count) {
1986
4213
  if (this.#resolved)
1987
4214
  return;
1988
4215
  this.#total = count;
1989
- for (let id = 0;id < count; id++)
4216
+ const ids = this.#ids ?? Array.from({ length: count }, (_, i) => i);
4217
+ validateShardIds(ids, count);
4218
+ for (const id of ids)
1990
4219
  this.shards.set(id, this.#createShard(id, count));
1991
4220
  this.#resolved = true;
1992
4221
  }
@@ -1998,23 +4227,51 @@ class ShardManager {
1998
4227
  shardCount: count,
1999
4228
  reconnect: this.#options.reconnect ?? true
2000
4229
  });
2001
- gateway.on("ready", () => this.#emit("shardReady", id));
2002
- gateway.on("resumed", () => this.#emit("shardResume", id));
2003
- gateway.on("close", (close) => this.#emit("shardDisconnect", id, close));
4230
+ gateway.on("ready", () => {
4231
+ this.#setStatus(id, "ready");
4232
+ this.#emit("shardReady", id);
4233
+ });
4234
+ gateway.on("resumed", () => {
4235
+ this.#setStatus(id, "ready");
4236
+ this.#emit("shardResume", id);
4237
+ });
4238
+ gateway.on("close", (close) => {
4239
+ this.#setStatus(id, "disconnected");
4240
+ this.#emit("shardDisconnect", id, close);
4241
+ });
2004
4242
  gateway.on("error", (error) => this.#emit("shardError", id, error));
2005
- gateway.on("unstable", (info) => this.#emit("shardUnstable", id, info));
4243
+ gateway.on("unstable", (info) => {
4244
+ this.#setStatus(id, "unstable");
4245
+ this.#emit("shardUnstable", id, info);
4246
+ });
4247
+ this.#emit("shardCreate", id, gateway);
2006
4248
  return gateway;
2007
4249
  }
2008
4250
  }
2009
4251
  export {
2010
4252
  BroadcastChannelTransport,
4253
+ ClusterHost,
2011
4254
  ClusterManager,
4255
+ ClusterWorker,
4256
+ IDENTIFY_INTERVAL,
4257
+ IdentifyCoordinator,
2012
4258
  IpcTransport,
4259
+ MAX_GUILDS_PER_SHARD,
4260
+ REMOTE_PROTOCOL_VERSION,
4261
+ RemoteClose,
4262
+ RequestTracker,
2013
4263
  ShardBus,
2014
4264
  ShardManager,
2015
4265
  ShardSupervisor,
2016
4266
  ShardManager as ShardingManager,
2017
- fetchGatewayBot
4267
+ allocateShards,
4268
+ assertCompleteAllocation,
4269
+ fetchGatewayBot,
4270
+ fetchGuildCount,
4271
+ isClusterWorker,
4272
+ readClusterContext,
4273
+ shardsForGuilds,
4274
+ validateShardIds
2018
4275
  };
2019
4276
 
2020
- //# debugId=8B521E4F34BB811E64756E2164756E21
4277
+ //# debugId=8AD6CE4D87FD51CB64756E2164756E21