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.
- package/dist/builders/attachment.d.ts +1 -0
- package/dist/builders/clone.d.ts +10 -0
- package/dist/builders/commands.d.ts +1 -1
- package/dist/builders/components.d.ts +24 -5
- package/dist/builders/index.js +64 -17
- package/dist/builders/index.js.map +10 -9
- package/dist/collection/index.d.ts +1 -0
- package/dist/collection/index.js +297 -19
- package/dist/collection/index.js.map +5 -4
- package/dist/collection/redis.d.ts +98 -0
- package/dist/core/collector.d.ts +8 -0
- package/dist/core/events.d.ts +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +6242 -4733
- package/dist/core/index.js.map +47 -40
- package/dist/core/permissions.d.ts +3 -402
- package/dist/core/redis-cache.d.ts +85 -0
- package/dist/formatters/index.d.ts +5 -1
- package/dist/formatters/index.js +7 -3
- package/dist/formatters/index.js.map +3 -3
- package/dist/handlers/index.js +11 -2
- package/dist/handlers/index.js.map +3 -3
- package/dist/index.js +8688 -6136
- package/dist/index.js.map +67 -59
- package/dist/managers/application.d.ts +10 -6
- package/dist/managers/base.d.ts +11 -1
- package/dist/managers/guild.d.ts +3 -1
- package/dist/managers/index.d.ts +6 -1
- package/dist/managers/index.js +383 -109
- package/dist/managers/index.js.map +20 -23
- package/dist/managers/member.d.ts +3 -1
- package/dist/managers/message.d.ts +13 -4
- package/dist/managers/thread.d.ts +3 -3
- package/dist/rest/decoder.d.ts +2 -0
- package/dist/rest/errors.d.ts +5 -2
- package/dist/rest/headers.d.ts +31 -0
- package/dist/rest/identity.d.ts +31 -0
- package/dist/rest/index.d.ts +97 -6
- package/dist/rest/index.js +1355 -220
- package/dist/rest/index.js.map +16 -13
- package/dist/rest/limiter.d.ts +75 -10
- package/dist/rest/redis.d.ts +103 -22
- package/dist/rest/scheduler.d.ts +13 -0
- package/dist/rest/store.d.ts +135 -12
- package/dist/rest/telemetry.d.ts +151 -0
- package/dist/rest/webhook.d.ts +18 -6
- package/dist/sharding/bus.d.ts +4 -2
- package/dist/sharding/cluster.d.ts +29 -1
- package/dist/sharding/health.d.ts +1 -1
- package/dist/sharding/host.d.ts +13 -2
- package/dist/sharding/identify.d.ts +2 -0
- package/dist/sharding/index.d.ts +8 -0
- package/dist/sharding/index.js +579 -65
- package/dist/sharding/index.js.map +17 -15
- package/dist/sharding/leader.d.ts +34 -0
- package/dist/sharding/remote.d.ts +36 -2
- package/dist/sharding/supervisor.d.ts +1 -1
- package/dist/sharding/thread.d.ts +30 -0
- package/dist/sharding/transport.d.ts +6 -5
- package/dist/sharding/worker.d.ts +11 -1
- package/dist/structures/base.d.ts +16 -5
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +57 -91
- package/dist/structures/index.js.map +10 -18
- package/dist/structures/options.d.ts +3 -2
- package/dist/structures/permissions.d.ts +409 -0
- package/dist/structures/resources.d.ts +4 -1
- package/dist/types/gateway.d.ts +4 -0
- package/dist/types/index.d.ts +14 -4
- package/dist/types/index.js.map +3 -3
- package/dist/utils/args.d.ts +3 -1
- package/dist/utils/index.d.ts +6 -1
- package/dist/utils/index.js +17 -10
- package/dist/utils/index.js.map +4 -4
- package/dist/voice/index.d.ts +7 -1
- package/dist/voice/index.js +33 -10
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/index.d.ts +2 -0
- package/dist/ws/index.js +32 -4
- package/dist/ws/index.js.map +5 -5
- package/package.json +21 -17
package/dist/sharding/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// packages/sharding/package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@lunibee/sharding",
|
|
5
|
-
version: "0.3.
|
|
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
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
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
|
-
|
|
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"
|
|
1630
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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: {
|
|
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
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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.#
|
|
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 ??
|
|
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
|
-
|
|
2501
|
-
|
|
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.#
|
|
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 (
|
|
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 (
|
|
3023
|
+
if (stale())
|
|
2789
3024
|
return;
|
|
2790
3025
|
throw error;
|
|
2791
3026
|
});
|
|
2792
|
-
if (!placement ||
|
|
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 (
|
|
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 (
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
3137
|
-
if (!message || typeof message !== "object" || typeof message.type !== "string" || typeof message.
|
|
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
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3462
|
-
|
|
3463
|
-
url
|
|
3464
|
-
|
|
3465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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=
|
|
4625
|
+
//# debugId=D3CF0E421E8E307864756E2164756E21
|