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