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