lunibee 0.3.1 → 0.3.3

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 (81) 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/index.js +64 -17
  6. package/dist/builders/index.js.map +10 -9
  7. package/dist/collection/index.d.ts +1 -0
  8. package/dist/collection/index.js +297 -19
  9. package/dist/collection/index.js.map +5 -4
  10. package/dist/collection/redis.d.ts +98 -0
  11. package/dist/core/collector.d.ts +8 -0
  12. package/dist/core/events.d.ts +1 -0
  13. package/dist/core/index.d.ts +3 -0
  14. package/dist/core/index.js +6242 -4733
  15. package/dist/core/index.js.map +47 -40
  16. package/dist/core/permissions.d.ts +3 -402
  17. package/dist/core/redis-cache.d.ts +85 -0
  18. package/dist/formatters/index.d.ts +5 -1
  19. package/dist/formatters/index.js +7 -3
  20. package/dist/formatters/index.js.map +3 -3
  21. package/dist/handlers/index.js +11 -2
  22. package/dist/handlers/index.js.map +3 -3
  23. package/dist/index.js +8688 -6136
  24. package/dist/index.js.map +67 -59
  25. package/dist/managers/application.d.ts +10 -6
  26. package/dist/managers/base.d.ts +11 -1
  27. package/dist/managers/guild.d.ts +3 -1
  28. package/dist/managers/index.d.ts +6 -1
  29. package/dist/managers/index.js +383 -109
  30. package/dist/managers/index.js.map +20 -23
  31. package/dist/managers/member.d.ts +3 -1
  32. package/dist/managers/message.d.ts +13 -4
  33. package/dist/managers/thread.d.ts +3 -3
  34. package/dist/rest/decoder.d.ts +2 -0
  35. package/dist/rest/errors.d.ts +5 -2
  36. package/dist/rest/headers.d.ts +31 -0
  37. package/dist/rest/identity.d.ts +31 -0
  38. package/dist/rest/index.d.ts +97 -6
  39. package/dist/rest/index.js +1355 -220
  40. package/dist/rest/index.js.map +16 -13
  41. package/dist/rest/limiter.d.ts +75 -10
  42. package/dist/rest/redis.d.ts +103 -22
  43. package/dist/rest/scheduler.d.ts +13 -0
  44. package/dist/rest/store.d.ts +135 -12
  45. package/dist/rest/telemetry.d.ts +151 -0
  46. package/dist/rest/webhook.d.ts +18 -6
  47. package/dist/sharding/bus.d.ts +4 -2
  48. package/dist/sharding/cluster.d.ts +29 -1
  49. package/dist/sharding/health.d.ts +1 -1
  50. package/dist/sharding/host.d.ts +13 -2
  51. package/dist/sharding/identify.d.ts +2 -0
  52. package/dist/sharding/index.d.ts +8 -0
  53. package/dist/sharding/index.js +579 -65
  54. package/dist/sharding/index.js.map +17 -15
  55. package/dist/sharding/leader.d.ts +34 -0
  56. package/dist/sharding/remote.d.ts +36 -2
  57. package/dist/sharding/supervisor.d.ts +1 -1
  58. package/dist/sharding/thread.d.ts +30 -0
  59. package/dist/sharding/transport.d.ts +6 -5
  60. package/dist/sharding/worker.d.ts +11 -1
  61. package/dist/structures/base.d.ts +16 -5
  62. package/dist/structures/index.d.ts +1 -1
  63. package/dist/structures/index.js +57 -91
  64. package/dist/structures/index.js.map +10 -18
  65. package/dist/structures/options.d.ts +3 -2
  66. package/dist/structures/permissions.d.ts +409 -0
  67. package/dist/structures/resources.d.ts +4 -1
  68. package/dist/types/gateway.d.ts +4 -0
  69. package/dist/types/index.d.ts +14 -4
  70. package/dist/types/index.js.map +3 -3
  71. package/dist/utils/args.d.ts +3 -1
  72. package/dist/utils/index.d.ts +6 -1
  73. package/dist/utils/index.js +17 -10
  74. package/dist/utils/index.js.map +4 -4
  75. package/dist/voice/index.d.ts +7 -1
  76. package/dist/voice/index.js +33 -10
  77. package/dist/voice/index.js.map +3 -3
  78. package/dist/ws/index.d.ts +2 -0
  79. package/dist/ws/index.js +32 -4
  80. package/dist/ws/index.js.map +5 -5
  81. package/package.json +21 -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.1",
5
+ version: "0.3.3",
6
6
  type: "module",
7
7
  module: "./src/index.ts",
8
8
  types: "./src/index.ts",
@@ -606,6 +606,7 @@ class WebSocketTransport {
606
606
  decoded = this.#decoder.push(event.data);
607
607
  } catch (error) {
608
608
  this.#handlers.onError(this.#asError(error));
609
+ this.#failDecoding(generation);
609
610
  return;
610
611
  }
611
612
  if (!(decoded instanceof Promise)) {
@@ -623,6 +624,7 @@ class WebSocketTransport {
623
624
  if (!this.#owns(generation))
624
625
  return;
625
626
  this.#handlers.onError(this.#asError(error));
627
+ this.#failDecoding(generation);
626
628
  });
627
629
  });
628
630
  socket.addEventListener("close", (event) => {
@@ -651,6 +653,10 @@ class WebSocketTransport {
651
653
  return false;
652
654
  }
653
655
  }
656
+ #failDecoding(generation) {
657
+ if (this.#owns(generation))
658
+ this.close(4000, "decoder failure");
659
+ }
654
660
  close(code = 1000, reason = "") {
655
661
  const socket = this.#socket;
656
662
  if (!socket)
@@ -993,6 +999,13 @@ class Gateway {
993
999
  constructor(options) {
994
1000
  if (!options.token?.trim())
995
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.`);
996
1009
  const resolvedIntents = resolveGatewayIntents(options.intents);
997
1010
  if (!Number.isInteger(resolvedIntents) || resolvedIntents < 0)
998
1011
  throw new TypeError("Gateway intents must be a non-negative integer.");
@@ -1001,6 +1014,7 @@ class Gateway {
1001
1014
  shardCount: 1,
1002
1015
  reconnect: true,
1003
1016
  maxReconnectAttempts: Infinity,
1017
+ connectTimeout: 30000,
1004
1018
  reconnectBaseDelay: 1000,
1005
1019
  reconnectMaxDelay: 30000,
1006
1020
  heartbeatAckTimeout: 1e4,
@@ -1076,7 +1090,7 @@ class Gateway {
1076
1090
  this.#reconnect.cancel();
1077
1091
  this.#closed = false;
1078
1092
  this.#setState("CONNECT" /* Connect */);
1079
- 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;
1080
1094
  return this.#reconnect.attempt(() => this.#open(connectURL));
1081
1095
  }
1082
1096
  close() {
@@ -1162,6 +1176,8 @@ class Gateway {
1162
1176
  const count = this.#options.shardCount;
1163
1177
  if (count <= 1)
1164
1178
  return true;
1179
+ if (!/^\d{1,20}$/.test(guildId))
1180
+ return false;
1165
1181
  return Number((BigInt(guildId) >> 22n) % BigInt(count)) === this.#options.shardId;
1166
1182
  }
1167
1183
  requestGuildMembers(data) {
@@ -1177,8 +1193,16 @@ class Gateway {
1177
1193
  return;
1178
1194
  this.#token = this.#session.beginConnection();
1179
1195
  const result = this.#transport.connect(url);
1180
- 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?.();
1181
1204
  return;
1205
+ }
1182
1206
  const failure = this.#normalizeError(result.error);
1183
1207
  this.#emitError(failure);
1184
1208
  if (this.#options.reconnect) {
@@ -1188,7 +1212,9 @@ class Gateway {
1188
1212
  this.#settleConnect(failure);
1189
1213
  }
1190
1214
  }
1215
+ #connectTimer;
1191
1216
  #onOpen() {
1217
+ clearTimeout(this.#connectTimer);
1192
1218
  this.#heartbeat.watch();
1193
1219
  this.#emit("open", undefined);
1194
1220
  this.#settleConnect();
@@ -1256,7 +1282,8 @@ class Gateway {
1256
1282
  this.#emit("RAW", { event: event ?? "UNKNOWN", data });
1257
1283
  if (this.#closed)
1258
1284
  return;
1259
- this.#emit(event ?? "dispatch", data);
1285
+ if (event == null || /^[A-Z][A-Z0-9_]*$/.test(event))
1286
+ this.#emit(event ?? "dispatch", data);
1260
1287
  }
1261
1288
  #handleHello(interval) {
1262
1289
  this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
@@ -1319,6 +1346,7 @@ class Gateway {
1319
1346
  this.#reconnect.settle(error);
1320
1347
  }
1321
1348
  #clearTimers() {
1349
+ clearTimeout(this.#connectTimer);
1322
1350
  this.#reconnect.cancel();
1323
1351
  this.#heartbeat.stop();
1324
1352
  }
@@ -1451,14 +1479,109 @@ function parseParentFrame(value) {
1451
1479
  }
1452
1480
  }
1453
1481
 
1482
+ // packages/sharding/src/thread.ts
1483
+ import { EventEmitter } from "events";
1484
+ import {
1485
+ isMainThread,
1486
+ parentPort,
1487
+ Worker,
1488
+ workerData
1489
+ } from "worker_threads";
1490
+ var THREAD_MARKER = "__lunibeeClusterThread";
1491
+
1492
+ class ThreadChild extends EventEmitter {
1493
+ pid;
1494
+ exitCode = null;
1495
+ signalCode = null;
1496
+ connected = true;
1497
+ #worker;
1498
+ #signal;
1499
+ constructor(worker) {
1500
+ super();
1501
+ this.#worker = worker;
1502
+ this.pid = worker.threadId;
1503
+ worker.on("message", (frame) => this.emit("message", frame));
1504
+ worker.on("error", (error) => this.emit("error", error));
1505
+ worker.once("exit", (code) => {
1506
+ this.connected = false;
1507
+ if (this.#signal) {
1508
+ this.signalCode = this.#signal;
1509
+ this.emit("exit", null, this.#signal);
1510
+ } else {
1511
+ this.exitCode = code;
1512
+ this.emit("exit", code, null);
1513
+ }
1514
+ });
1515
+ }
1516
+ send(frame) {
1517
+ if (!this.connected)
1518
+ return false;
1519
+ try {
1520
+ this.#worker.postMessage(frame);
1521
+ return true;
1522
+ } catch {
1523
+ return false;
1524
+ }
1525
+ }
1526
+ kill(signal = "SIGTERM") {
1527
+ if (!this.connected)
1528
+ return false;
1529
+ this.#signal ??= signal;
1530
+ this.#worker.terminate();
1531
+ return true;
1532
+ }
1533
+ }
1534
+ function threadFork(options = {}) {
1535
+ return (script, env) => {
1536
+ const defined = {};
1537
+ for (const [key, value] of Object.entries(env))
1538
+ if (value !== undefined)
1539
+ defined[key] = value;
1540
+ const worker = new Worker(script, {
1541
+ env: defined,
1542
+ workerData: { [THREAD_MARKER]: true },
1543
+ ...options.resourceLimits ? { resourceLimits: options.resourceLimits } : {}
1544
+ });
1545
+ return new ThreadChild(worker);
1546
+ };
1547
+ }
1548
+ var endpoint;
1549
+ function threadEndpoint() {
1550
+ const port = parentPort;
1551
+ if (isMainThread || !port || !workerData?.[THREAD_MARKER])
1552
+ return;
1553
+ endpoint ??= portEndpoint(port);
1554
+ return endpoint;
1555
+ }
1556
+ function portEndpoint(port) {
1557
+ const wrapped = {
1558
+ send: (message) => {
1559
+ port.postMessage(message);
1560
+ return true;
1561
+ },
1562
+ on: (event, listener) => {
1563
+ if (event === "message")
1564
+ port.on("message", listener);
1565
+ return wrapped;
1566
+ },
1567
+ off: (event, listener) => {
1568
+ if (event === "message")
1569
+ port.off("message", listener);
1570
+ return wrapped;
1571
+ }
1572
+ };
1573
+ return wrapped;
1574
+ }
1575
+
1454
1576
  // packages/sharding/src/worker.ts
1455
1577
  class ClusterWorker {
1456
1578
  static #current;
1457
1579
  static current() {
1458
1580
  if (!ClusterWorker.#current) {
1459
1581
  const context = readClusterContext();
1460
- if (context && typeof process.send === "function")
1461
- ClusterWorker.#current = new ClusterWorker(context, process);
1582
+ const endpoint = typeof process.send === "function" ? process : threadEndpoint();
1583
+ if (context && endpoint)
1584
+ ClusterWorker.#current = new ClusterWorker(context, endpoint);
1462
1585
  }
1463
1586
  return ClusterWorker.#current;
1464
1587
  }
@@ -1474,13 +1597,19 @@ class ClusterWorker {
1474
1597
  #onDisconnect;
1475
1598
  #counter = 0;
1476
1599
  #closed = false;
1477
- constructor(context, endpoint = process) {
1600
+ constructor(context, endpoint = process, options = {}) {
1478
1601
  if (typeof endpoint.send !== "function")
1479
1602
  throw new Error("ClusterWorker needs an IPC channel: run it in a process forked by ClusterManager.");
1480
1603
  this.context = context;
1481
1604
  this.#endpoint = endpoint;
1482
1605
  this.#onMessage = (frame) => this.#handle(frame);
1483
- this.#onDisconnect = () => this.#failAll("The parent went away.");
1606
+ const exitOnLoss = options.exitOnParentLoss ?? endpoint === process;
1607
+ const exit = options.exit ?? ((code) => process.exit(code));
1608
+ this.#onDisconnect = () => {
1609
+ this.#failAll("The parent went away.");
1610
+ if (exitOnLoss && !this.#closed)
1611
+ exit(1);
1612
+ };
1484
1613
  endpoint.on("message", this.#onMessage);
1485
1614
  endpoint.on("disconnect", this.#onDisconnect);
1486
1615
  }
@@ -1591,6 +1720,7 @@ class BroadcastChannelTransport {
1591
1720
  }
1592
1721
  var BUS_FRAME = "__lunibeeBus";
1593
1722
  var PEER_DOWN_FRAME = "__lunibeePeerDown";
1723
+ var LOCAL = new WeakMap;
1594
1724
 
1595
1725
  class IpcTransport {
1596
1726
  #endpoint;
@@ -1611,9 +1741,18 @@ class IpcTransport {
1611
1741
  this.#down?.(record[PEER_DOWN_FRAME]);
1612
1742
  };
1613
1743
  endpoint.on("message", this.#listener);
1744
+ let local = LOCAL.get(endpoint);
1745
+ if (!local)
1746
+ LOCAL.set(endpoint, local = new Set);
1747
+ local.add(this);
1614
1748
  }
1615
1749
  post(message) {
1616
1750
  this.#endpoint.send({ [BUS_FRAME]: message });
1751
+ for (const peer of LOCAL.get(this.#endpoint) ?? [])
1752
+ if (peer !== this) {
1753
+ const copy = structuredClone(message);
1754
+ queueMicrotask(() => peer.#message?.(copy));
1755
+ }
1617
1756
  }
1618
1757
  onMessage(listener) {
1619
1758
  this.#message = listener;
@@ -1623,11 +1762,17 @@ class IpcTransport {
1623
1762
  }
1624
1763
  close() {
1625
1764
  this.#endpoint.off("message", this.#listener);
1765
+ LOCAL.get(this.#endpoint)?.delete(this);
1626
1766
  }
1627
1767
  }
1628
1768
  function defaultTransport(name) {
1629
- if (process.env.LUNIBEE_SHARD_BUS === "ipc" && typeof process.send === "function")
1630
- return new IpcTransport;
1769
+ if (process.env.LUNIBEE_SHARD_BUS === "ipc") {
1770
+ if (typeof process.send === "function")
1771
+ return new IpcTransport;
1772
+ const thread = threadEndpoint();
1773
+ if (thread)
1774
+ return new IpcTransport(thread);
1775
+ }
1631
1776
  return new BroadcastChannelTransport(name);
1632
1777
  }
1633
1778
 
@@ -1645,6 +1790,7 @@ class ShardBus {
1645
1790
  #transport;
1646
1791
  #maxPending;
1647
1792
  #handlers = new Map;
1793
+ #responders = new Map;
1648
1794
  #errorHandlers = new Set;
1649
1795
  #shardId;
1650
1796
  #namespace;
@@ -1675,7 +1821,13 @@ class ShardBus {
1675
1821
  return this;
1676
1822
  }
1677
1823
  off(type, handler) {
1678
- this.#handlers.get(type)?.delete(handler);
1824
+ const handlers = this.#handlers.get(type);
1825
+ handlers?.delete(handler);
1826
+ const responder = this.#responders.get(type)?.get(handler);
1827
+ if (responder) {
1828
+ handlers?.delete(responder);
1829
+ this.#responders.get(type).delete(handler);
1830
+ }
1679
1831
  return this;
1680
1832
  }
1681
1833
  onError(handler) {
@@ -1697,7 +1849,7 @@ class ShardBus {
1697
1849
  return this.#publish(null, type, data);
1698
1850
  }
1699
1851
  respond(type, handler) {
1700
- return this.on(type, async (message) => {
1852
+ const wrapper = async (message) => {
1701
1853
  if (!message.expectsReply)
1702
1854
  return;
1703
1855
  const reply = { requestId: message.id };
@@ -1707,7 +1859,12 @@ class ShardBus {
1707
1859
  reply.error = error instanceof Error ? error.message : String(error);
1708
1860
  }
1709
1861
  this.#publish(message.source, REPLY_TYPE, reply);
1710
- });
1862
+ };
1863
+ let byType = this.#responders.get(type);
1864
+ if (!byType)
1865
+ this.#responders.set(type, byType = new Map);
1866
+ byType.set(handler, wrapper);
1867
+ return this.on(type, wrapper);
1711
1868
  }
1712
1869
  async request(target, type, data, timeoutMs = 5000) {
1713
1870
  if (!Number.isInteger(target) || target < 0)
@@ -1868,7 +2025,7 @@ class ShardSupervisor {
1868
2025
  recent.push(now);
1869
2026
  this.#restarts.set(id, recent);
1870
2027
  const base = Math.min(maxDelay, restartDelay * backoff ** (recent.length - 1));
1871
- return Math.round(base * (1 + jitter * this.#random()));
2028
+ return Math.min(maxDelay, Math.round(base * (1 + jitter * this.#random())));
1872
2029
  }
1873
2030
  reset(id) {
1874
2031
  if (id === undefined)
@@ -1896,6 +2053,7 @@ class IdentifyCoordinator {
1896
2053
  #remaining;
1897
2054
  #total;
1898
2055
  #resetAt = Number.POSITIVE_INFINITY;
2056
+ #period = 24 * 60 * 60 * 1000;
1899
2057
  #timer;
1900
2058
  #timerAt = Number.POSITIVE_INFINITY;
1901
2059
  #closed = false;
@@ -1922,6 +2080,9 @@ class IdentifyCoordinator {
1922
2080
  this.#total = options.budget.total;
1923
2081
  this.#remaining = options.budget.remaining;
1924
2082
  this.#resetAt = this.#now() + options.budget.resetAfter;
2083
+ this.#period = options.budget.period ?? this.#period;
2084
+ if (!(this.#period > 0))
2085
+ throw new RangeError("budget.period must be positive.");
1925
2086
  }
1926
2087
  }
1927
2088
  get remaining() {
@@ -1995,7 +2156,8 @@ class IdentifyCoordinator {
1995
2156
  const now = this.#now();
1996
2157
  if (this.#total !== undefined && now >= this.#resetAt) {
1997
2158
  this.#remaining = this.#total;
1998
- this.#resetAt = Number.POSITIVE_INFINITY;
2159
+ while (now >= this.#resetAt)
2160
+ this.#resetAt += this.#period;
1999
2161
  }
2000
2162
  let next = Number.POSITIVE_INFINITY;
2001
2163
  for (const key of this.#keys.values()) {
@@ -2007,11 +2169,6 @@ class IdentifyCoordinator {
2007
2169
  }
2008
2170
  if (this.#remaining !== undefined && this.#remaining <= 0) {
2009
2171
  const waiter = key.queue[0];
2010
- if (this.#resetAt === Number.POSITIVE_INFINITY) {
2011
- key.queue.shift();
2012
- waiter.fail(new Error("The session start limit is exhausted."));
2013
- continue;
2014
- }
2015
2172
  this.#onBudgetWait?.({
2016
2173
  shardId: waiter.shardId,
2017
2174
  waitMs: this.#resetAt - now
@@ -2062,7 +2219,7 @@ class IdentifyCoordinator {
2062
2219
  }
2063
2220
 
2064
2221
  // packages/sharding/src/remote.ts
2065
- import { EventEmitter } from "events";
2222
+ import { EventEmitter as EventEmitter2 } from "events";
2066
2223
  import { randomBytes } from "crypto";
2067
2224
 
2068
2225
  // packages/sharding/src/remote-protocol.ts
@@ -2213,7 +2370,7 @@ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
2213
2370
  }
2214
2371
 
2215
2372
  // packages/sharding/src/remote.ts
2216
- class RemoteChild extends EventEmitter {
2373
+ class RemoteChild extends EventEmitter2 {
2217
2374
  host;
2218
2375
  cluster;
2219
2376
  epoch;
@@ -2259,13 +2416,16 @@ class RemoteChild extends EventEmitter {
2259
2416
  class RemoteHosts {
2260
2417
  #options;
2261
2418
  #links = new Map;
2419
+ #droppedAt = new Map;
2262
2420
  #waiters = new Set;
2263
2421
  #hooks;
2264
2422
  #server;
2265
2423
  #watchdog;
2266
2424
  #epoch = 0;
2267
2425
  #ping = 0;
2268
- #unauthenticated = 0;
2426
+ #pending = new Set;
2427
+ #pendingByAddress = new Map;
2428
+ #rates = new Map;
2269
2429
  constructor(options, hooks) {
2270
2430
  if (typeof options.secret !== "string" || options.secret.length < 16)
2271
2431
  throw new RangeError("remote.secret must be a string of at least 16 characters.");
@@ -2294,6 +2454,13 @@ class RemoteHosts {
2294
2454
  connected() {
2295
2455
  return [...this.#links.keys()].sort();
2296
2456
  }
2457
+ isConnected(hostId) {
2458
+ return this.#links.has(hostId);
2459
+ }
2460
+ fencedAt(hostId) {
2461
+ const dropped = this.#droppedAt.get(hostId);
2462
+ return dropped === undefined || this.#links.has(hostId) ? 0 : dropped + this.#options.lease * 2;
2463
+ }
2297
2464
  start() {
2298
2465
  if (this.#server)
2299
2466
  return;
@@ -2303,8 +2470,17 @@ class RemoteHosts {
2303
2470
  hostname: hostname ?? "127.0.0.1",
2304
2471
  ...tls ? { tls } : {},
2305
2472
  fetch: (request, server) => {
2473
+ const peer = server.requestIP(request)?.address;
2474
+ const address = this.#options.clientAddress?.(request, peer) ?? peer ?? "unknown";
2475
+ if (!this.#allowConnection(address))
2476
+ return new Response("Too many connections.", {
2477
+ status: 429
2478
+ });
2306
2479
  if (server.upgrade(request, {
2307
- data: { nonce: randomBytes(16).toString("hex") }
2480
+ data: {
2481
+ nonce: randomBytes(16).toString("hex"),
2482
+ address
2483
+ }
2308
2484
  }))
2309
2485
  return;
2310
2486
  return new Response("Lunibee cluster coordinator", {
@@ -2398,14 +2574,46 @@ class RemoteHosts {
2398
2574
  return false;
2399
2575
  }
2400
2576
  }
2577
+ #allowConnection(address) {
2578
+ const rate = this.#options.connectionsPerSecond ?? 10;
2579
+ const now = Date.now();
2580
+ let bucket = this.#rates.get(address);
2581
+ if (!bucket) {
2582
+ bucket = { tokens: rate, at: now };
2583
+ this.#rates.set(address, bucket);
2584
+ if (this.#rates.size > 1e4)
2585
+ this.#rates.delete(this.#rates.keys().next().value);
2586
+ }
2587
+ bucket.tokens = Math.min(rate, bucket.tokens + (now - bucket.at) / 1000 * rate);
2588
+ bucket.at = now;
2589
+ if (bucket.tokens < 1)
2590
+ return false;
2591
+ bucket.tokens--;
2592
+ return true;
2593
+ }
2594
+ #evict(victim) {
2595
+ this.#uncount(victim);
2596
+ clearTimeout(victim.data.helloTimer);
2597
+ victim.close(RemoteClose.TooManyConnections, "Too many connections.");
2598
+ }
2401
2599
  #open(ws) {
2402
- if (this.#unauthenticated >= (this.#options.maxUnauthenticated ?? 16)) {
2403
- ws.close(RemoteClose.TooManyConnections, "Too many connections.");
2404
- return;
2600
+ const address = ws.data.address;
2601
+ if ((this.#pendingByAddress.get(address) ?? 0) >= (this.#options.maxUnauthenticatedPerAddress ?? 4)) {
2602
+ for (const other of this.#pending)
2603
+ if (other.data.address === address) {
2604
+ this.#evict(other);
2605
+ break;
2606
+ }
2607
+ }
2608
+ if (this.#pending.size >= (this.#options.maxUnauthenticated ?? 16)) {
2609
+ const oldest = this.#pending.values().next().value;
2610
+ if (oldest)
2611
+ this.#evict(oldest);
2405
2612
  }
2406
- this.#unauthenticated++;
2613
+ this.#pending.add(ws);
2614
+ this.#pendingByAddress.set(address, (this.#pendingByAddress.get(address) ?? 0) + 1);
2407
2615
  ws.data.counted = true;
2408
- ws.data.helloTimer = setTimeout(() => ws.close(RemoteClose.HelloTimeout, "No hello."), this.#options.helloTimeout ?? 1e4);
2616
+ ws.data.helloTimer = setTimeout(() => ws.close(RemoteClose.HelloTimeout, "No hello."), this.#options.helloTimeout ?? 3000);
2409
2617
  ws.send(JSON.stringify({
2410
2618
  t: "challenge",
2411
2619
  nonce: ws.data.nonce,
@@ -2485,6 +2693,7 @@ class RemoteHosts {
2485
2693
  children: new Map
2486
2694
  };
2487
2695
  this.#links.set(id, link);
2696
+ this.#droppedAt.delete(id);
2488
2697
  const lease = this.#options.lease;
2489
2698
  this.#post(link, {
2490
2699
  t: "welcome",
@@ -2496,10 +2705,15 @@ class RemoteHosts {
2496
2705
  waiter.check();
2497
2706
  }
2498
2707
  #uncount(ws) {
2499
- if (ws.data.counted) {
2500
- ws.data.counted = false;
2501
- this.#unauthenticated--;
2502
- }
2708
+ if (!ws.data.counted)
2709
+ return;
2710
+ ws.data.counted = false;
2711
+ this.#pending.delete(ws);
2712
+ const left = (this.#pendingByAddress.get(ws.data.address) ?? 1) - 1;
2713
+ if (left > 0)
2714
+ this.#pendingByAddress.set(ws.data.address, left);
2715
+ else
2716
+ this.#pendingByAddress.delete(ws.data.address);
2503
2717
  }
2504
2718
  #closed(ws, reason) {
2505
2719
  clearTimeout(ws.data.helloTimer);
@@ -2512,6 +2726,9 @@ class RemoteHosts {
2512
2726
  if (this.#links.get(link.id) !== link)
2513
2727
  return;
2514
2728
  this.#links.delete(link.id);
2729
+ this.#droppedAt.set(link.id, Date.now());
2730
+ if (this.#droppedAt.size > 1000)
2731
+ this.#droppedAt.delete(this.#droppedAt.keys().next().value);
2515
2732
  if (code !== undefined)
2516
2733
  try {
2517
2734
  link.ws.close(code, reason.slice(0, 100));
@@ -2646,7 +2863,13 @@ class ClusterManager {
2646
2863
  #stopPromise;
2647
2864
  #wasStarted = false;
2648
2865
  #stopping = false;
2866
+ #generation = 0;
2867
+ #leading = false;
2868
+ #renewTimer;
2869
+ #lastRenew = 0;
2870
+ #renewing = false;
2649
2871
  #restartTimers = new Set;
2872
+ #fencing = new Map;
2650
2873
  #startTimers = new Map;
2651
2874
  #pauses = new Set;
2652
2875
  #readyWaiters = new Set;
@@ -2694,6 +2917,11 @@ class ClusterManager {
2694
2917
  if (options.shardIds && typeof options.shardCount === "number")
2695
2918
  validateShardIds(options.shardIds, options.shardCount);
2696
2919
  this.#auto = options.shardCount === "auto";
2920
+ const ttl = options.leader?.ttl;
2921
+ if (options.leader && (typeof options.leader.lock?.acquire !== "function" || typeof options.leader.lock.release !== "function"))
2922
+ throw new TypeError("leader.lock must have acquire() and release().");
2923
+ if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 100))
2924
+ throw new RangeError("leader.ttl must be an integer of at least 100.");
2697
2925
  this.#options = { ...options };
2698
2926
  this.#supervisor = new ShardSupervisor({
2699
2927
  restartDelay: options.restartDelay,
@@ -2702,7 +2930,7 @@ class ClusterManager {
2702
2930
  this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
2703
2931
  if (options.remote)
2704
2932
  this.#remote = new RemoteHosts(options.remote, {
2705
- join: (id) => this.#emit("hostJoin", id),
2933
+ join: (id) => this.#hostJoined(id),
2706
2934
  leave: (id, reason) => this.#emit("hostLeave", id, reason),
2707
2935
  error: (error) => this.#emit("error", error)
2708
2936
  });
@@ -2776,8 +3004,15 @@ class ClusterManager {
2776
3004
  };
2777
3005
  }
2778
3006
  async#start() {
3007
+ const generation = this.#generation;
3008
+ const stale = () => this.#stopping || generation !== this.#generation;
3009
+ if (this.#options.leader && !this.#leading) {
3010
+ await this.#lead();
3011
+ if (stale())
3012
+ return;
3013
+ }
2779
3014
  const { count, maxConcurrency, budget } = await this.#resolveGateway();
2780
- if (this.#stopping)
3015
+ if (stale())
2781
3016
  return;
2782
3017
  const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
2783
3018
  validateShardIds(ids, count);
@@ -2785,11 +3020,11 @@ class ClusterManager {
2785
3020
  const chunks = allocateShards(ids, clusterCount);
2786
3021
  assertCompleteAllocation(chunks, ids);
2787
3022
  const placement = await this.#placeHosts().catch((error) => {
2788
- if (this.#stopping)
3023
+ if (stale())
2789
3024
  return;
2790
3025
  throw error;
2791
3026
  });
2792
- if (!placement || this.#stopping)
3027
+ if (!placement || stale())
2793
3028
  return;
2794
3029
  this.shardCount = count;
2795
3030
  this.#wasStarted = true;
@@ -2815,7 +3050,7 @@ class ClusterManager {
2815
3050
  for (const chunk of active)
2816
3051
  this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
2817
3052
  for (const [index, chunk] of active.entries()) {
2818
- if (this.#stopping)
3053
+ if (stale())
2819
3054
  return;
2820
3055
  for (const shard of chunk.shards)
2821
3056
  this.#owner.set(shard, chunk.id);
@@ -2824,7 +3059,7 @@ class ClusterManager {
2824
3059
  if (index < active.length - 1 && gap > 0)
2825
3060
  await this.#pause(gap);
2826
3061
  }
2827
- if (this.#stopping)
3062
+ if (stale())
2828
3063
  return;
2829
3064
  if (this.#options.waitForReady)
2830
3065
  await this.ready();
@@ -2834,6 +3069,104 @@ class ClusterManager {
2834
3069
  }, this.#options.autoScaleInterval);
2835
3070
  }
2836
3071
  }
3072
+ get isLeader() {
3073
+ return !this.#options.leader || this.#leading;
3074
+ }
3075
+ async#lead() {
3076
+ const { lock, ttl = 1e4 } = this.#options.leader;
3077
+ const generation = this.#generation;
3078
+ while (!this.#stopping && generation === this.#generation) {
3079
+ const started = Date.now();
3080
+ let held = false;
3081
+ try {
3082
+ held = await withTimeout(lock.acquire(ttl), ttl / 2);
3083
+ } catch (error) {
3084
+ this.#emit("error", asError(error));
3085
+ }
3086
+ if (this.#stopping || generation !== this.#generation) {
3087
+ if (held)
3088
+ lock.release().catch(() => {
3089
+ return;
3090
+ });
3091
+ return;
3092
+ }
3093
+ if (held) {
3094
+ this.#leading = true;
3095
+ this.#lastRenew = started;
3096
+ this.#startRenewing(ttl);
3097
+ this.#emit("leaderAcquired");
3098
+ const lease = this.#options.remote ? this.#options.remote.lease ?? 15000 : 0;
3099
+ if (lease > 0)
3100
+ await this.#pause(lease * 2);
3101
+ return;
3102
+ }
3103
+ await this.#pause(Math.floor(ttl / 2));
3104
+ }
3105
+ }
3106
+ #startRenewing(ttl) {
3107
+ this.#stopRenewing();
3108
+ const lock = this.#options.leader.lock;
3109
+ this.#renewTimer = setInterval(() => {
3110
+ if (Date.now() - this.#lastRenew > ttl / 2) {
3111
+ this.#standDown("The leader lock could not be renewed.");
3112
+ return;
3113
+ }
3114
+ if (this.#renewing)
3115
+ return;
3116
+ this.#renewing = true;
3117
+ const started = Date.now();
3118
+ lock.acquire(ttl).then((held) => {
3119
+ this.#renewing = false;
3120
+ if (!this.#leading)
3121
+ return;
3122
+ if (held)
3123
+ this.#lastRenew = started;
3124
+ else
3125
+ this.#standDown("Another coordinator holds the leader lock.");
3126
+ }, (error) => {
3127
+ this.#renewing = false;
3128
+ this.#emit("error", asError(error));
3129
+ });
3130
+ }, Math.max(10, Math.floor(ttl / 4)));
3131
+ this.#renewTimer.unref?.();
3132
+ }
3133
+ #stopRenewing() {
3134
+ if (this.#renewTimer)
3135
+ clearInterval(this.#renewTimer);
3136
+ this.#renewTimer = undefined;
3137
+ }
3138
+ async#resign() {
3139
+ this.#stopRenewing();
3140
+ if (!this.#leading)
3141
+ return;
3142
+ this.#leading = false;
3143
+ const { lock, ttl = 1e4 } = this.#options.leader;
3144
+ try {
3145
+ await withTimeout(lock.release(), ttl / 2);
3146
+ } catch (error) {
3147
+ this.#emit("error", asError(error));
3148
+ }
3149
+ }
3150
+ #standDown(reason) {
3151
+ if (!this.#leading)
3152
+ return;
3153
+ this.#stopRenewing();
3154
+ this.#leading = false;
3155
+ this.#stopping = true;
3156
+ this.#halt();
3157
+ for (const cluster of this.#infos.values()) {
3158
+ cluster.state = "stopped";
3159
+ try {
3160
+ cluster.process.kill("SIGKILL");
3161
+ } catch {}
3162
+ }
3163
+ this.clusters.clear();
3164
+ this.#startPromise = undefined;
3165
+ this.#remote?.stop();
3166
+ this.#stopping = false;
3167
+ this.#emit("leaderLost", reason);
3168
+ this.spawn().catch((error) => this.#emit("error", asError(error)));
3169
+ }
2837
3170
  async#placeHosts() {
2838
3171
  const remote = this.#remote;
2839
3172
  const options = this.#options.remote;
@@ -2901,6 +3234,9 @@ class ClusterManager {
2901
3234
  this.#infos.set(id, info);
2902
3235
  this.clusters.set(id, info);
2903
3236
  this.#allReady = false;
3237
+ child.on("error", (error) => this.#emit("error", new Error(`Cluster ${id} process error: ${error.message}`, {
3238
+ cause: error
3239
+ })));
2904
3240
  child.on("message", (frame) => this.#onFrame(info, child, frame));
2905
3241
  child.once("exit", (code, signal) => this.#onExit(info, child, code, signal));
2906
3242
  const timeout = this.#options.startTimeout;
@@ -2942,7 +3278,7 @@ class ClusterManager {
2942
3278
  this.#announceDown(info.id, info.shards);
2943
3279
  if (this.#options.restartOnExit === false) {
2944
3280
  info.state = "failed";
2945
- this.#checkReady();
3281
+ this.#rejectReady(info, "restartOnExit is false");
2946
3282
  return;
2947
3283
  }
2948
3284
  const delay = this.#supervisor.next(info.id);
@@ -2952,9 +3288,10 @@ class ClusterManager {
2952
3288
  this.#options.onGiveUp?.(info);
2953
3289
  } catch {}
2954
3290
  this.#emit("clusterGiveUp", info);
2955
- this.#checkReady();
2956
3291
  if (this.#options.reassignOnGiveUp)
2957
3292
  this.#reassign(info);
3293
+ else
3294
+ this.#rejectReady(info, "restarts exhausted");
2958
3295
  return;
2959
3296
  }
2960
3297
  info.state = "restarting";
@@ -2965,26 +3302,73 @@ class ClusterManager {
2965
3302
  attempt: info.restarts,
2966
3303
  delay
2967
3304
  });
3305
+ this.#restartLater(info, delay);
3306
+ }
3307
+ #restartLater(info, delay) {
2968
3308
  const timer = setTimeout(() => {
2969
3309
  this.#restartTimers.delete(timer);
3310
+ this.#fencing.delete(info.id);
2970
3311
  if (this.#stopping || info.state !== "restarting")
2971
3312
  return;
2972
- this.#launch(info.id, info.shards);
3313
+ const wait = this.#relocate(info);
3314
+ if (wait > 0)
3315
+ this.#fencing.set(info.id, this.#restartLater(info, wait));
3316
+ else
3317
+ this.#launch(info.id, info.shards);
2973
3318
  }, delay);
2974
3319
  timer.unref?.();
2975
3320
  this.#restartTimers.add(timer);
3321
+ return timer;
3322
+ }
3323
+ #hostJoined(hostId) {
3324
+ this.#emit("hostJoin", hostId);
3325
+ for (const [id, timer] of [...this.#fencing]) {
3326
+ const info = this.#infos.get(id);
3327
+ if (this.#hostOf.get(id) !== hostId || !info)
3328
+ continue;
3329
+ clearTimeout(timer);
3330
+ this.#restartTimers.delete(timer);
3331
+ this.#fencing.delete(id);
3332
+ if (!this.#stopping && info.state === "restarting")
3333
+ this.#launch(info.id, info.shards);
3334
+ }
3335
+ }
3336
+ #relocate(info) {
3337
+ const remote = this.#remote;
3338
+ const host = this.#hostOf.get(info.id) ?? "local";
3339
+ if (!remote || this.#options.remote?.moveClusters === false || host === "local" || remote.isConnected(host))
3340
+ return 0;
3341
+ const wait = remote.fencedAt(host) - Date.now();
3342
+ if (wait > 0)
3343
+ return wait;
3344
+ const places = this.#options.remote?.local ? ["local", ...remote.connected()] : remote.connected();
3345
+ if (places.length === 0)
3346
+ return 0;
3347
+ const load = new Map(places.map((place) => [place, 0]));
3348
+ for (const [cluster, place] of this.#hostOf) {
3349
+ const state = this.#infos.get(cluster)?.state;
3350
+ if (load.has(place) && state !== "failed" && state !== "stopped")
3351
+ load.set(place, load.get(place) + 1);
3352
+ }
3353
+ const target = places.reduce((best, place) => load.get(place) < load.get(best) ? place : best);
3354
+ this.#hostOf.set(info.id, target);
3355
+ info.host = target;
3356
+ this.#emit("clusterMoved", info, { from: host, to: target });
3357
+ return 0;
2976
3358
  }
2977
3359
  async#reassign(failed) {
2978
3360
  const shardIds = [...failed.shards];
2979
3361
  const target = [...this.#infos.values()].filter((cluster) => cluster.state === "ready").sort((a, b) => a.shards.length - b.shards.length)[0];
2980
3362
  if (!target) {
2981
3363
  this.#emit("error", new Error(`No ready cluster can take over shards [${shardIds}] of cluster ${failed.id}.`));
3364
+ this.#rejectReady(failed, "no ready cluster to take them");
2982
3365
  return;
2983
3366
  }
2984
3367
  try {
2985
3368
  await this.requestCluster(target.id, REQUEST_ASSIGN, { shardIds }, { timeoutMs: this.#options.startTimeout ?? 120000 });
2986
3369
  } catch (error) {
2987
3370
  this.#emit("error", new Error(`Cluster ${target.id} could not take over shards [${shardIds}]: ${error instanceof Error ? error.message : String(error)}`));
3371
+ this.#rejectReady(failed, "reassignment failed");
2988
3372
  return;
2989
3373
  }
2990
3374
  if (this.#stopping || failed.state !== "failed")
@@ -2998,6 +3382,7 @@ class ClusterManager {
2998
3382
  from: failed.id,
2999
3383
  to: target.id
3000
3384
  });
3385
+ this.#checkReady();
3001
3386
  }
3002
3387
  #clearStartTimer(id) {
3003
3388
  const timer = this.#startTimers.get(id);
@@ -3133,9 +3518,20 @@ class ClusterManager {
3133
3518
  throw new RangeError(`${what} is larger than maxMessageBytes (${this.#maxBytes}).`);
3134
3519
  }
3135
3520
  #onBus(from, raw) {
3136
- const message = raw;
3137
- if (!message || typeof message !== "object" || typeof message.type !== "string" || typeof message.source !== "number" || message.target !== null && !Number.isInteger(message.target))
3521
+ let message = raw;
3522
+ 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))
3138
3523
  return;
3524
+ if (!from.shards.includes(message.source))
3525
+ return;
3526
+ const { type, id, source, target, data, expectsReply } = message;
3527
+ message = {
3528
+ type,
3529
+ id,
3530
+ source,
3531
+ target,
3532
+ data,
3533
+ ...expectsReply === undefined ? {} : { expectsReply }
3534
+ };
3139
3535
  if (this.#tooBig(message.data)) {
3140
3536
  this.#emit("error", new Error(`Cluster ${from.id} sent a shard-bus message of more than ${this.#maxBytes} bytes; it was not relayed.`));
3141
3537
  return;
@@ -3279,12 +3675,21 @@ class ClusterManager {
3279
3675
  });
3280
3676
  }
3281
3677
  #isReady() {
3282
- if (this.#infos.size === 0)
3283
- return false;
3284
- for (const cluster of this.#infos.values())
3285
- if (cluster.state !== "ready")
3678
+ let ready = 0;
3679
+ for (const cluster of this.#infos.values()) {
3680
+ if (cluster.state === "ready")
3681
+ ready++;
3682
+ else if (!(cluster.state === "failed" && cluster.shards.length === 0))
3286
3683
  return false;
3287
- return true;
3684
+ }
3685
+ return ready > 0;
3686
+ }
3687
+ #rejectReady(failed, why) {
3688
+ if (failed.shards.length === 0)
3689
+ return;
3690
+ const error = new Error(`Cluster ${failed.id} failed and shards [${failed.shards}] have no owner (${why}).`);
3691
+ for (const waiter of [...this.#readyWaiters])
3692
+ waiter.reject(error);
3288
3693
  }
3289
3694
  #checkReady() {
3290
3695
  if (!this.#isReady())
@@ -3371,14 +3776,18 @@ class ClusterManager {
3371
3776
  cluster.state = "stopped";
3372
3777
  this.clusters.clear();
3373
3778
  this.#startPromise = undefined;
3374
- if (!keepRemote)
3779
+ if (!keepRemote) {
3375
3780
  this.#remote?.stop();
3781
+ await this.#resign();
3782
+ }
3376
3783
  this.#stopping = false;
3377
3784
  }
3378
3785
  #halt() {
3786
+ this.#generation++;
3379
3787
  for (const timer of this.#restartTimers)
3380
3788
  clearTimeout(timer);
3381
3789
  this.#restartTimers.clear();
3790
+ this.#fencing.clear();
3382
3791
  for (const id of [...this.#startTimers.keys()])
3383
3792
  this.#clearStartTimer(id);
3384
3793
  for (const wake of [...this.#pauses])
@@ -3433,9 +3842,20 @@ class ClusterManager {
3433
3842
  this.clusters.clear();
3434
3843
  this.#startPromise = undefined;
3435
3844
  this.#remote?.stop();
3845
+ this.#resign();
3436
3846
  this.#stopping = false;
3437
3847
  }
3438
3848
  }
3849
+ function withTimeout(promise, ms) {
3850
+ let timer;
3851
+ const timeout = new Promise((_, reject) => {
3852
+ timer = setTimeout(() => reject(new Error(`The leader lock did not answer within ${ms} ms.`)), ms);
3853
+ });
3854
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
3855
+ }
3856
+ function asError(error) {
3857
+ return error instanceof Error ? error : new Error(String(error));
3858
+ }
3439
3859
  // packages/sharding/src/host.ts
3440
3860
  import { fork as fork2 } from "child_process";
3441
3861
  class ClusterHost {
@@ -3443,11 +3863,16 @@ class ClusterHost {
3443
3863
  #running = new Map;
3444
3864
  #listeners = new Map;
3445
3865
  #socket;
3866
+ #urls;
3867
+ #urlIndex = 0;
3868
+ #unreachable = 0;
3869
+ #accepted = false;
3446
3870
  #lease = 15000;
3447
3871
  #lastRx = 0;
3448
3872
  #watchdog;
3449
3873
  #retry;
3450
3874
  #delay;
3875
+ #busyAttempts = 0;
3451
3876
  #closed = true;
3452
3877
  #connected = false;
3453
3878
  #firstConnect;
@@ -3458,18 +3883,24 @@ class ClusterHost {
3458
3883
  throw new RangeError("secret must be at least 16 characters.");
3459
3884
  if (!options.script?.trim())
3460
3885
  throw new TypeError("A worker script path is required.");
3461
- let url;
3462
- try {
3463
- url = new URL(options.url);
3464
- } catch {
3465
- throw new TypeError("url must be an absolute ws:// or wss:// URL.");
3886
+ const urls = Array.isArray(options.url) ? [...options.url] : [options.url];
3887
+ if (urls.length === 0 || urls.length > 16)
3888
+ throw new TypeError("url must list 1 to 16 coordinators.");
3889
+ for (const raw of urls) {
3890
+ let url;
3891
+ try {
3892
+ url = new URL(raw);
3893
+ } catch {
3894
+ throw new TypeError("url must be an absolute ws:// or wss:// URL.");
3895
+ }
3896
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
3897
+ throw new TypeError("url must start with ws:// or wss://.");
3898
+ if (url.username || url.password)
3899
+ throw new TypeError("url must not contain credentials.");
3900
+ if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
3901
+ throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
3466
3902
  }
3467
- if (url.protocol !== "ws:" && url.protocol !== "wss:")
3468
- throw new TypeError("url must start with ws:// or wss://.");
3469
- if (url.username || url.password)
3470
- throw new TypeError("url must not contain credentials.");
3471
- if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
3472
- throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
3903
+ this.#urls = urls;
3473
3904
  this.#options = options;
3474
3905
  this.#delay = options.reconnectDelay ?? 1000;
3475
3906
  }
@@ -3500,6 +3931,8 @@ class ClusterHost {
3500
3931
  if (!this.#closed)
3501
3932
  return Promise.resolve();
3502
3933
  this.#closed = false;
3934
+ this.#busyAttempts = 0;
3935
+ this.#unreachable = 0;
3503
3936
  return new Promise((resolve, reject) => {
3504
3937
  this.#firstConnect = { resolve, reject };
3505
3938
  this.#open();
@@ -3531,7 +3964,8 @@ class ClusterHost {
3531
3964
  });
3532
3965
  }
3533
3966
  #open() {
3534
- const socket = new WebSocket(this.#options.url);
3967
+ this.#accepted = false;
3968
+ const socket = new WebSocket(this.#urls[this.#urlIndex]);
3535
3969
  this.#socket = socket;
3536
3970
  socket.addEventListener("message", (event) => this.#message(socket, event.data));
3537
3971
  socket.addEventListener("error", () => {});
@@ -3550,12 +3984,31 @@ class ClusterHost {
3550
3984
  return false;
3551
3985
  }
3552
3986
  }
3987
+ #refuseSpawn(socket, data) {
3988
+ if (typeof data !== "string" || data.length > 65536)
3989
+ return;
3990
+ let frame;
3991
+ try {
3992
+ frame = JSON.parse(data);
3993
+ } catch {
3994
+ return;
3995
+ }
3996
+ if (frame?.t !== "spawn" || !Number.isInteger(frame.cluster) || !Number.isInteger(frame.epoch))
3997
+ return;
3998
+ this.#send(socket, {
3999
+ t: "error",
4000
+ cluster: frame.cluster,
4001
+ epoch: frame.epoch,
4002
+ message: "This host refused the spawn request (malformed or not allowed)."
4003
+ });
4004
+ }
3553
4005
  #message(socket, data) {
3554
4006
  if (this.#socket !== socket)
3555
4007
  return;
3556
4008
  const message = parseToHost(data);
3557
4009
  if (!message) {
3558
4010
  this.#emit("error", new Error("The coordinator sent a malformed frame."));
4011
+ this.#refuseSpawn(socket, data);
3559
4012
  return;
3560
4013
  }
3561
4014
  this.#lastRx = Date.now();
@@ -3574,6 +4027,7 @@ class ClusterHost {
3574
4027
  return;
3575
4028
  case "welcome":
3576
4029
  this.#lease = message.leaseMs;
4030
+ this.#accepted = true;
3577
4031
  this.#connected = true;
3578
4032
  this.#delay = this.#options.reconnectDelay ?? 1000;
3579
4033
  this.#startWatchdog();
@@ -3687,6 +4141,18 @@ class ClusterHost {
3687
4141
  return;
3688
4142
  const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
3689
4143
  const first = this.#firstConnect;
4144
+ const busy = code === RemoteClose.TooManyConnections || code === RemoteClose.HelloTimeout;
4145
+ const accepted = this.#accepted;
4146
+ if (this.#urls.length > 1)
4147
+ this.#urlIndex = (this.#urlIndex + 1) % this.#urls.length;
4148
+ if (first && !refused && !busy && !accepted && ++this.#unreachable < this.#urls.length) {
4149
+ this.#open();
4150
+ return;
4151
+ }
4152
+ if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
4153
+ this.#retryLater();
4154
+ return;
4155
+ }
3690
4156
  if (first) {
3691
4157
  this.#firstConnect = undefined;
3692
4158
  this.#closed = true;
@@ -3695,6 +4161,9 @@ class ClusterHost {
3695
4161
  }
3696
4162
  if (refused)
3697
4163
  this.#emit("error", new Error(`The coordinator refused this host (${code}: ${reason}).`));
4164
+ this.#retryLater();
4165
+ }
4166
+ #retryLater() {
3698
4167
  const wait = Math.round(this.#delay * (1 + Math.random() * 0.2));
3699
4168
  this.#delay = Math.min(this.#options.maxReconnectDelay ?? 30000, this.#delay * 2);
3700
4169
  this.#retry = setTimeout(() => {
@@ -3707,6 +4176,43 @@ class ClusterHost {
3707
4176
  function defaultFork(script, env) {
3708
4177
  return fork2(script, [], { env });
3709
4178
  }
4179
+ // packages/sharding/src/leader.ts
4180
+ var ACQUIRE = `local v = redis.call('GET', KEYS[1])
4181
+ if v == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
4182
+ if not v then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 end
4183
+ return 0`;
4184
+ var RELEASE = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
4185
+ return 0`;
4186
+
4187
+ class RedisLeaderLock {
4188
+ #client;
4189
+ #key;
4190
+ #token;
4191
+ constructor(options) {
4192
+ if (typeof options?.client?.send !== "function")
4193
+ throw new TypeError("RedisLeaderLock needs a client with send().");
4194
+ this.#client = options.client;
4195
+ this.#key = options.key ?? "lunibee:cluster:leader";
4196
+ this.#token = options.token ?? crypto.randomUUID();
4197
+ if (!this.#key || !this.#token)
4198
+ throw new TypeError("key and token must not be empty.");
4199
+ }
4200
+ async acquire(ttlMs) {
4201
+ if (!Number.isInteger(ttlMs) || ttlMs < 1)
4202
+ throw new RangeError("ttlMs must be a positive integer.");
4203
+ const reply = await this.#client.send("EVAL", [
4204
+ ACQUIRE,
4205
+ "1",
4206
+ this.#key,
4207
+ this.#token,
4208
+ String(ttlMs)
4209
+ ]);
4210
+ return Number(reply) === 1;
4211
+ }
4212
+ async release() {
4213
+ await this.#client.send("EVAL", [RELEASE, "1", this.#key, this.#token]);
4214
+ }
4215
+ }
3710
4216
 
3711
4217
  // packages/sharding/src/index.ts
3712
4218
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -3757,6 +4263,7 @@ class ShardManager {
3757
4263
  #resolved = false;
3758
4264
  #destroyed = false;
3759
4265
  #auto;
4266
+ #fixedCount;
3760
4267
  #autoScaleTimer;
3761
4268
  #total;
3762
4269
  #listeners = new Map;
@@ -3799,6 +4306,7 @@ class ShardManager {
3799
4306
  if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
3800
4307
  throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
3801
4308
  this.#total = count;
4309
+ this.#fixedCount = count;
3802
4310
  this.#auto = options.shardCount === "auto";
3803
4311
  this.#options = {
3804
4312
  spawnDelay: ShardManager.IDENTIFY_INTERVAL,
@@ -3830,7 +4338,7 @@ class ShardManager {
3830
4338
  }
3831
4339
  await this.#ensureInitialized();
3832
4340
  await this.#start([...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id));
3833
- if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
4341
+ if (this.#options.autoScaleInterval && this.#auto && !this.#autoScaleTimer) {
3834
4342
  this.#autoScaleTimer = setInterval(() => {
3835
4343
  this.checkAutoScale();
3836
4344
  }, this.#options.autoScaleInterval);
@@ -3905,6 +4413,8 @@ class ShardManager {
3905
4413
  this.#setStatus(id, "connecting");
3906
4414
  const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
3907
4415
  const failed = results.findIndex((result) => result.status === "rejected");
4416
+ if (limit)
4417
+ limit.remaining = Math.max(0, limit.remaining - results.filter((r) => r.status === "fulfilled").length);
3908
4418
  if (failed !== -1) {
3909
4419
  if (destroyOnFailure)
3910
4420
  this.destroy();
@@ -4041,7 +4551,7 @@ class ShardManager {
4041
4551
  return;
4042
4552
  if (this.#destroyed)
4043
4553
  this.#destroyed = false;
4044
- const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#options.shardCount ?? 1;
4554
+ const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#fixedCount;
4045
4555
  this.#initialize(count);
4046
4556
  }
4047
4557
  #initialize(count) {
@@ -4079,6 +4589,7 @@ class ShardManager {
4079
4589
  this.#setStatus(id, "unstable");
4080
4590
  this.#emit("shardUnstable", id, info);
4081
4591
  });
4592
+ this.#emit("shardCreate", id, gateway);
4082
4593
  return gateway;
4083
4594
  }
4084
4595
  }
@@ -4092,6 +4603,7 @@ export {
4092
4603
  IpcTransport,
4093
4604
  MAX_GUILDS_PER_SHARD,
4094
4605
  REMOTE_PROTOCOL_VERSION,
4606
+ RedisLeaderLock,
4095
4607
  RemoteClose,
4096
4608
  RequestTracker,
4097
4609
  ShardBus,
@@ -4105,7 +4617,9 @@ export {
4105
4617
  isClusterWorker,
4106
4618
  readClusterContext,
4107
4619
  shardsForGuilds,
4620
+ threadEndpoint,
4621
+ threadFork,
4108
4622
  validateShardIds
4109
4623
  };
4110
4624
 
4111
- //# debugId=7D2FE3134CAB1F3364756E2164756E21
4625
+ //# debugId=D3CF0E421E8E307864756E2164756E21