lunibee 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.js +22 -10
- package/dist/core/index.js.map +8 -8
- package/dist/index.js +403 -38
- package/dist/index.js.map +17 -15
- package/dist/managers/base.d.ts +7 -0
- package/dist/managers/index.js +22 -10
- package/dist/managers/index.js.map +8 -8
- package/dist/rest/index.js +7 -2
- package/dist/rest/index.js.map +3 -3
- package/dist/sharding/cluster.d.ts +28 -0
- package/dist/sharding/host.d.ts +7 -2
- package/dist/sharding/index.d.ts +2 -0
- package/dist/sharding/index.js +376 -28
- package/dist/sharding/index.js.map +11 -9
- package/dist/sharding/leader.d.ts +34 -0
- package/dist/sharding/remote.d.ts +18 -0
- package/dist/sharding/thread.d.ts +30 -0
- package/dist/structures/index.js +3 -3
- package/dist/structures/index.js.map +3 -3
- package/package.json +16 -15
package/dist/index.js
CHANGED
|
@@ -324,7 +324,7 @@ class Channel extends BaseStructure {
|
|
|
324
324
|
permissionOverwrites;
|
|
325
325
|
partial;
|
|
326
326
|
#context;
|
|
327
|
-
constructor(data, context, options
|
|
327
|
+
constructor(data, context, options) {
|
|
328
328
|
super(data.id);
|
|
329
329
|
if (!Number.isInteger(data.type) || data.type < 0)
|
|
330
330
|
throw new RangeError("A Discord channel requires a valid channel type.");
|
|
@@ -337,7 +337,7 @@ class Channel extends BaseStructure {
|
|
|
337
337
|
this.rateLimitPerUser = data.rate_limit_per_user;
|
|
338
338
|
this.position = data.position;
|
|
339
339
|
this.permissionOverwrites = (data.permission_overwrites ?? []).map((overwrite) => ({ ...overwrite }));
|
|
340
|
-
this.partial = options
|
|
340
|
+
this.partial = options?.partial ?? false;
|
|
341
341
|
this.#context = context;
|
|
342
342
|
}
|
|
343
343
|
send(options) {
|
|
@@ -6460,6 +6460,13 @@ class Collection extends Map {
|
|
|
6460
6460
|
}
|
|
6461
6461
|
|
|
6462
6462
|
// packages/managers/src/base.ts
|
|
6463
|
+
function assignFields(into, from) {
|
|
6464
|
+
const target = into;
|
|
6465
|
+
const source = from;
|
|
6466
|
+
for (const key of Object.keys(source))
|
|
6467
|
+
target[key] = source[key];
|
|
6468
|
+
return into;
|
|
6469
|
+
}
|
|
6463
6470
|
function splitReason(options) {
|
|
6464
6471
|
const { reason, ...payload } = options;
|
|
6465
6472
|
return [payload, reason];
|
|
@@ -6560,7 +6567,7 @@ class ResourceManager extends Manager {
|
|
|
6560
6567
|
const key = this.#key(resource);
|
|
6561
6568
|
const existing = this.get(key);
|
|
6562
6569
|
if (existing !== undefined && existing !== resource && typeof existing === "object" && existing !== null) {
|
|
6563
|
-
|
|
6570
|
+
assignFields(existing, resource);
|
|
6564
6571
|
this.set(key, existing);
|
|
6565
6572
|
return existing;
|
|
6566
6573
|
}
|
|
@@ -6574,7 +6581,7 @@ class ResourceManager extends Manager {
|
|
|
6574
6581
|
// packages/rest/package.json
|
|
6575
6582
|
var package_default = {
|
|
6576
6583
|
name: "@lunibee/rest",
|
|
6577
|
-
version: "0.3.
|
|
6584
|
+
version: "0.3.3",
|
|
6578
6585
|
type: "module",
|
|
6579
6586
|
module: "./src/index.ts",
|
|
6580
6587
|
types: "./src/index.ts",
|
|
@@ -7908,6 +7915,7 @@ class RedisRateLimitStore {
|
|
|
7908
7915
|
#atomic;
|
|
7909
7916
|
#local = new MemoryRateLimitStore;
|
|
7910
7917
|
#lastError;
|
|
7918
|
+
#warnedSlots = false;
|
|
7911
7919
|
#lastFailureAt;
|
|
7912
7920
|
#failures = 0;
|
|
7913
7921
|
#mode;
|
|
@@ -8006,6 +8014,10 @@ class RedisRateLimitStore {
|
|
|
8006
8014
|
this.#lastError = error;
|
|
8007
8015
|
this.#lastFailureAt = Date.now();
|
|
8008
8016
|
this.#failures++;
|
|
8017
|
+
if (!this.#warnedSlots && /CROSSSLOT/.test(String(error))) {
|
|
8018
|
+
this.#warnedSlots = true;
|
|
8019
|
+
console.warn('[lunibee/rest] Redis Cluster refused a script whose keys hash to different slots (CROSSSLOT): use a hash-tagged prefix such as "{mybot}:rest:".');
|
|
8020
|
+
}
|
|
8009
8021
|
try {
|
|
8010
8022
|
this.#onErrorCallback?.(operation, error);
|
|
8011
8023
|
} catch {}
|
|
@@ -8803,7 +8815,7 @@ class MessageManager {
|
|
|
8803
8815
|
const existing = this.cache.peek(data.id);
|
|
8804
8816
|
const message = new Message(data, this.#context);
|
|
8805
8817
|
if (existing) {
|
|
8806
|
-
|
|
8818
|
+
assignFields(existing, message);
|
|
8807
8819
|
return existing;
|
|
8808
8820
|
}
|
|
8809
8821
|
if (this.#caching)
|
|
@@ -8980,7 +8992,7 @@ class EmojiManager extends Manager {
|
|
|
8980
8992
|
#merge(key, emoji) {
|
|
8981
8993
|
const existing = this.get(key);
|
|
8982
8994
|
if (existing) {
|
|
8983
|
-
|
|
8995
|
+
assignFields(existing, emoji);
|
|
8984
8996
|
return existing;
|
|
8985
8997
|
}
|
|
8986
8998
|
this.set(key, emoji);
|
|
@@ -9867,7 +9879,7 @@ class ChannelManager extends Manager {
|
|
|
9867
9879
|
#merge(channel) {
|
|
9868
9880
|
const existing = this.get(channel.id);
|
|
9869
9881
|
if (existing && existing.type === channel.type) {
|
|
9870
|
-
|
|
9882
|
+
assignFields(existing, channel);
|
|
9871
9883
|
return existing;
|
|
9872
9884
|
}
|
|
9873
9885
|
this.set(channel.id, channel);
|
|
@@ -9877,9 +9889,14 @@ class ChannelManager extends Manager {
|
|
|
9877
9889
|
return this.set(channel.id, channel);
|
|
9878
9890
|
}
|
|
9879
9891
|
deleteGuildChannels(guildId) {
|
|
9892
|
+
const ids = [];
|
|
9893
|
+
this.cache.forEach((channel, id) => {
|
|
9894
|
+
if (channel.guildId === guildId)
|
|
9895
|
+
ids.push(id);
|
|
9896
|
+
});
|
|
9880
9897
|
let removed = 0;
|
|
9881
|
-
for (const
|
|
9882
|
-
if (
|
|
9898
|
+
for (const id of ids)
|
|
9899
|
+
if (this.delete(id))
|
|
9883
9900
|
removed++;
|
|
9884
9901
|
return removed;
|
|
9885
9902
|
}
|
|
@@ -12314,7 +12331,7 @@ class HandlerRegistry {
|
|
|
12314
12331
|
// packages/sharding/package.json
|
|
12315
12332
|
var package_default2 = {
|
|
12316
12333
|
name: "@lunibee/sharding",
|
|
12317
|
-
version: "0.3.
|
|
12334
|
+
version: "0.3.3",
|
|
12318
12335
|
type: "module",
|
|
12319
12336
|
module: "./src/index.ts",
|
|
12320
12337
|
types: "./src/index.ts",
|
|
@@ -12491,14 +12508,109 @@ function parseParentFrame(value) {
|
|
|
12491
12508
|
}
|
|
12492
12509
|
}
|
|
12493
12510
|
|
|
12511
|
+
// packages/sharding/src/thread.ts
|
|
12512
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
12513
|
+
import {
|
|
12514
|
+
isMainThread,
|
|
12515
|
+
parentPort,
|
|
12516
|
+
Worker,
|
|
12517
|
+
workerData
|
|
12518
|
+
} from "worker_threads";
|
|
12519
|
+
var THREAD_MARKER = "__lunibeeClusterThread";
|
|
12520
|
+
|
|
12521
|
+
class ThreadChild extends EventEmitter3 {
|
|
12522
|
+
pid;
|
|
12523
|
+
exitCode = null;
|
|
12524
|
+
signalCode = null;
|
|
12525
|
+
connected = true;
|
|
12526
|
+
#worker;
|
|
12527
|
+
#signal;
|
|
12528
|
+
constructor(worker) {
|
|
12529
|
+
super();
|
|
12530
|
+
this.#worker = worker;
|
|
12531
|
+
this.pid = worker.threadId;
|
|
12532
|
+
worker.on("message", (frame) => this.emit("message", frame));
|
|
12533
|
+
worker.on("error", (error) => this.emit("error", error));
|
|
12534
|
+
worker.once("exit", (code) => {
|
|
12535
|
+
this.connected = false;
|
|
12536
|
+
if (this.#signal) {
|
|
12537
|
+
this.signalCode = this.#signal;
|
|
12538
|
+
this.emit("exit", null, this.#signal);
|
|
12539
|
+
} else {
|
|
12540
|
+
this.exitCode = code;
|
|
12541
|
+
this.emit("exit", code, null);
|
|
12542
|
+
}
|
|
12543
|
+
});
|
|
12544
|
+
}
|
|
12545
|
+
send(frame) {
|
|
12546
|
+
if (!this.connected)
|
|
12547
|
+
return false;
|
|
12548
|
+
try {
|
|
12549
|
+
this.#worker.postMessage(frame);
|
|
12550
|
+
return true;
|
|
12551
|
+
} catch {
|
|
12552
|
+
return false;
|
|
12553
|
+
}
|
|
12554
|
+
}
|
|
12555
|
+
kill(signal = "SIGTERM") {
|
|
12556
|
+
if (!this.connected)
|
|
12557
|
+
return false;
|
|
12558
|
+
this.#signal ??= signal;
|
|
12559
|
+
this.#worker.terminate();
|
|
12560
|
+
return true;
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
function threadFork(options = {}) {
|
|
12564
|
+
return (script, env) => {
|
|
12565
|
+
const defined = {};
|
|
12566
|
+
for (const [key, value] of Object.entries(env))
|
|
12567
|
+
if (value !== undefined)
|
|
12568
|
+
defined[key] = value;
|
|
12569
|
+
const worker = new Worker(script, {
|
|
12570
|
+
env: defined,
|
|
12571
|
+
workerData: { [THREAD_MARKER]: true },
|
|
12572
|
+
...options.resourceLimits ? { resourceLimits: options.resourceLimits } : {}
|
|
12573
|
+
});
|
|
12574
|
+
return new ThreadChild(worker);
|
|
12575
|
+
};
|
|
12576
|
+
}
|
|
12577
|
+
var endpoint;
|
|
12578
|
+
function threadEndpoint() {
|
|
12579
|
+
const port = parentPort;
|
|
12580
|
+
if (isMainThread || !port || !workerData?.[THREAD_MARKER])
|
|
12581
|
+
return;
|
|
12582
|
+
endpoint ??= portEndpoint(port);
|
|
12583
|
+
return endpoint;
|
|
12584
|
+
}
|
|
12585
|
+
function portEndpoint(port) {
|
|
12586
|
+
const wrapped = {
|
|
12587
|
+
send: (message) => {
|
|
12588
|
+
port.postMessage(message);
|
|
12589
|
+
return true;
|
|
12590
|
+
},
|
|
12591
|
+
on: (event, listener) => {
|
|
12592
|
+
if (event === "message")
|
|
12593
|
+
port.on("message", listener);
|
|
12594
|
+
return wrapped;
|
|
12595
|
+
},
|
|
12596
|
+
off: (event, listener) => {
|
|
12597
|
+
if (event === "message")
|
|
12598
|
+
port.off("message", listener);
|
|
12599
|
+
return wrapped;
|
|
12600
|
+
}
|
|
12601
|
+
};
|
|
12602
|
+
return wrapped;
|
|
12603
|
+
}
|
|
12604
|
+
|
|
12494
12605
|
// packages/sharding/src/worker.ts
|
|
12495
12606
|
class ClusterWorker {
|
|
12496
12607
|
static #current;
|
|
12497
12608
|
static current() {
|
|
12498
12609
|
if (!ClusterWorker.#current) {
|
|
12499
12610
|
const context = readClusterContext();
|
|
12500
|
-
|
|
12501
|
-
|
|
12611
|
+
const endpoint = typeof process.send === "function" ? process : threadEndpoint();
|
|
12612
|
+
if (context && endpoint)
|
|
12613
|
+
ClusterWorker.#current = new ClusterWorker(context, endpoint);
|
|
12502
12614
|
}
|
|
12503
12615
|
return ClusterWorker.#current;
|
|
12504
12616
|
}
|
|
@@ -12683,8 +12795,13 @@ class IpcTransport {
|
|
|
12683
12795
|
}
|
|
12684
12796
|
}
|
|
12685
12797
|
function defaultTransport(name) {
|
|
12686
|
-
if (process.env.LUNIBEE_SHARD_BUS === "ipc"
|
|
12687
|
-
|
|
12798
|
+
if (process.env.LUNIBEE_SHARD_BUS === "ipc") {
|
|
12799
|
+
if (typeof process.send === "function")
|
|
12800
|
+
return new IpcTransport;
|
|
12801
|
+
const thread = threadEndpoint();
|
|
12802
|
+
if (thread)
|
|
12803
|
+
return new IpcTransport(thread);
|
|
12804
|
+
}
|
|
12688
12805
|
return new BroadcastChannelTransport(name);
|
|
12689
12806
|
}
|
|
12690
12807
|
|
|
@@ -13131,7 +13248,7 @@ class IdentifyCoordinator {
|
|
|
13131
13248
|
}
|
|
13132
13249
|
|
|
13133
13250
|
// packages/sharding/src/remote.ts
|
|
13134
|
-
import { EventEmitter as
|
|
13251
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
13135
13252
|
import { randomBytes } from "crypto";
|
|
13136
13253
|
|
|
13137
13254
|
// packages/sharding/src/remote-protocol.ts
|
|
@@ -13282,7 +13399,7 @@ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
|
|
|
13282
13399
|
}
|
|
13283
13400
|
|
|
13284
13401
|
// packages/sharding/src/remote.ts
|
|
13285
|
-
class RemoteChild extends
|
|
13402
|
+
class RemoteChild extends EventEmitter4 {
|
|
13286
13403
|
host;
|
|
13287
13404
|
cluster;
|
|
13288
13405
|
epoch;
|
|
@@ -13328,6 +13445,7 @@ class RemoteChild extends EventEmitter3 {
|
|
|
13328
13445
|
class RemoteHosts {
|
|
13329
13446
|
#options;
|
|
13330
13447
|
#links = new Map;
|
|
13448
|
+
#droppedAt = new Map;
|
|
13331
13449
|
#waiters = new Set;
|
|
13332
13450
|
#hooks;
|
|
13333
13451
|
#server;
|
|
@@ -13365,6 +13483,13 @@ class RemoteHosts {
|
|
|
13365
13483
|
connected() {
|
|
13366
13484
|
return [...this.#links.keys()].sort();
|
|
13367
13485
|
}
|
|
13486
|
+
isConnected(hostId) {
|
|
13487
|
+
return this.#links.has(hostId);
|
|
13488
|
+
}
|
|
13489
|
+
fencedAt(hostId) {
|
|
13490
|
+
const dropped = this.#droppedAt.get(hostId);
|
|
13491
|
+
return dropped === undefined || this.#links.has(hostId) ? 0 : dropped + this.#options.lease * 2;
|
|
13492
|
+
}
|
|
13368
13493
|
start() {
|
|
13369
13494
|
if (this.#server)
|
|
13370
13495
|
return;
|
|
@@ -13597,6 +13722,7 @@ class RemoteHosts {
|
|
|
13597
13722
|
children: new Map
|
|
13598
13723
|
};
|
|
13599
13724
|
this.#links.set(id, link);
|
|
13725
|
+
this.#droppedAt.delete(id);
|
|
13600
13726
|
const lease = this.#options.lease;
|
|
13601
13727
|
this.#post(link, {
|
|
13602
13728
|
t: "welcome",
|
|
@@ -13629,6 +13755,9 @@ class RemoteHosts {
|
|
|
13629
13755
|
if (this.#links.get(link.id) !== link)
|
|
13630
13756
|
return;
|
|
13631
13757
|
this.#links.delete(link.id);
|
|
13758
|
+
this.#droppedAt.set(link.id, Date.now());
|
|
13759
|
+
if (this.#droppedAt.size > 1000)
|
|
13760
|
+
this.#droppedAt.delete(this.#droppedAt.keys().next().value);
|
|
13632
13761
|
if (code !== undefined)
|
|
13633
13762
|
try {
|
|
13634
13763
|
link.ws.close(code, reason.slice(0, 100));
|
|
@@ -13763,7 +13892,13 @@ class ClusterManager {
|
|
|
13763
13892
|
#stopPromise;
|
|
13764
13893
|
#wasStarted = false;
|
|
13765
13894
|
#stopping = false;
|
|
13895
|
+
#generation = 0;
|
|
13896
|
+
#leading = false;
|
|
13897
|
+
#renewTimer;
|
|
13898
|
+
#lastRenew = 0;
|
|
13899
|
+
#renewing = false;
|
|
13766
13900
|
#restartTimers = new Set;
|
|
13901
|
+
#fencing = new Map;
|
|
13767
13902
|
#startTimers = new Map;
|
|
13768
13903
|
#pauses = new Set;
|
|
13769
13904
|
#readyWaiters = new Set;
|
|
@@ -13811,6 +13946,11 @@ class ClusterManager {
|
|
|
13811
13946
|
if (options.shardIds && typeof options.shardCount === "number")
|
|
13812
13947
|
validateShardIds(options.shardIds, options.shardCount);
|
|
13813
13948
|
this.#auto = options.shardCount === "auto";
|
|
13949
|
+
const ttl = options.leader?.ttl;
|
|
13950
|
+
if (options.leader && (typeof options.leader.lock?.acquire !== "function" || typeof options.leader.lock.release !== "function"))
|
|
13951
|
+
throw new TypeError("leader.lock must have acquire() and release().");
|
|
13952
|
+
if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 100))
|
|
13953
|
+
throw new RangeError("leader.ttl must be an integer of at least 100.");
|
|
13814
13954
|
this.#options = { ...options };
|
|
13815
13955
|
this.#supervisor = new ShardSupervisor({
|
|
13816
13956
|
restartDelay: options.restartDelay,
|
|
@@ -13819,7 +13959,7 @@ class ClusterManager {
|
|
|
13819
13959
|
this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
|
|
13820
13960
|
if (options.remote)
|
|
13821
13961
|
this.#remote = new RemoteHosts(options.remote, {
|
|
13822
|
-
join: (id) => this.#
|
|
13962
|
+
join: (id) => this.#hostJoined(id),
|
|
13823
13963
|
leave: (id, reason) => this.#emit("hostLeave", id, reason),
|
|
13824
13964
|
error: (error) => this.#emit("error", error)
|
|
13825
13965
|
});
|
|
@@ -13893,8 +14033,15 @@ class ClusterManager {
|
|
|
13893
14033
|
};
|
|
13894
14034
|
}
|
|
13895
14035
|
async#start() {
|
|
14036
|
+
const generation = this.#generation;
|
|
14037
|
+
const stale = () => this.#stopping || generation !== this.#generation;
|
|
14038
|
+
if (this.#options.leader && !this.#leading) {
|
|
14039
|
+
await this.#lead();
|
|
14040
|
+
if (stale())
|
|
14041
|
+
return;
|
|
14042
|
+
}
|
|
13896
14043
|
const { count, maxConcurrency, budget } = await this.#resolveGateway();
|
|
13897
|
-
if (
|
|
14044
|
+
if (stale())
|
|
13898
14045
|
return;
|
|
13899
14046
|
const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
|
|
13900
14047
|
validateShardIds(ids, count);
|
|
@@ -13902,11 +14049,11 @@ class ClusterManager {
|
|
|
13902
14049
|
const chunks = allocateShards(ids, clusterCount);
|
|
13903
14050
|
assertCompleteAllocation(chunks, ids);
|
|
13904
14051
|
const placement = await this.#placeHosts().catch((error) => {
|
|
13905
|
-
if (
|
|
14052
|
+
if (stale())
|
|
13906
14053
|
return;
|
|
13907
14054
|
throw error;
|
|
13908
14055
|
});
|
|
13909
|
-
if (!placement ||
|
|
14056
|
+
if (!placement || stale())
|
|
13910
14057
|
return;
|
|
13911
14058
|
this.shardCount = count;
|
|
13912
14059
|
this.#wasStarted = true;
|
|
@@ -13932,7 +14079,7 @@ class ClusterManager {
|
|
|
13932
14079
|
for (const chunk of active)
|
|
13933
14080
|
this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
|
|
13934
14081
|
for (const [index, chunk] of active.entries()) {
|
|
13935
|
-
if (
|
|
14082
|
+
if (stale())
|
|
13936
14083
|
return;
|
|
13937
14084
|
for (const shard of chunk.shards)
|
|
13938
14085
|
this.#owner.set(shard, chunk.id);
|
|
@@ -13941,7 +14088,7 @@ class ClusterManager {
|
|
|
13941
14088
|
if (index < active.length - 1 && gap > 0)
|
|
13942
14089
|
await this.#pause(gap);
|
|
13943
14090
|
}
|
|
13944
|
-
if (
|
|
14091
|
+
if (stale())
|
|
13945
14092
|
return;
|
|
13946
14093
|
if (this.#options.waitForReady)
|
|
13947
14094
|
await this.ready();
|
|
@@ -13951,6 +14098,104 @@ class ClusterManager {
|
|
|
13951
14098
|
}, this.#options.autoScaleInterval);
|
|
13952
14099
|
}
|
|
13953
14100
|
}
|
|
14101
|
+
get isLeader() {
|
|
14102
|
+
return !this.#options.leader || this.#leading;
|
|
14103
|
+
}
|
|
14104
|
+
async#lead() {
|
|
14105
|
+
const { lock, ttl = 1e4 } = this.#options.leader;
|
|
14106
|
+
const generation = this.#generation;
|
|
14107
|
+
while (!this.#stopping && generation === this.#generation) {
|
|
14108
|
+
const started = Date.now();
|
|
14109
|
+
let held = false;
|
|
14110
|
+
try {
|
|
14111
|
+
held = await withTimeout(lock.acquire(ttl), ttl / 2);
|
|
14112
|
+
} catch (error) {
|
|
14113
|
+
this.#emit("error", asError(error));
|
|
14114
|
+
}
|
|
14115
|
+
if (this.#stopping || generation !== this.#generation) {
|
|
14116
|
+
if (held)
|
|
14117
|
+
lock.release().catch(() => {
|
|
14118
|
+
return;
|
|
14119
|
+
});
|
|
14120
|
+
return;
|
|
14121
|
+
}
|
|
14122
|
+
if (held) {
|
|
14123
|
+
this.#leading = true;
|
|
14124
|
+
this.#lastRenew = started;
|
|
14125
|
+
this.#startRenewing(ttl);
|
|
14126
|
+
this.#emit("leaderAcquired");
|
|
14127
|
+
const lease = this.#options.remote ? this.#options.remote.lease ?? 15000 : 0;
|
|
14128
|
+
if (lease > 0)
|
|
14129
|
+
await this.#pause(lease * 2);
|
|
14130
|
+
return;
|
|
14131
|
+
}
|
|
14132
|
+
await this.#pause(Math.floor(ttl / 2));
|
|
14133
|
+
}
|
|
14134
|
+
}
|
|
14135
|
+
#startRenewing(ttl) {
|
|
14136
|
+
this.#stopRenewing();
|
|
14137
|
+
const lock = this.#options.leader.lock;
|
|
14138
|
+
this.#renewTimer = setInterval(() => {
|
|
14139
|
+
if (Date.now() - this.#lastRenew > ttl / 2) {
|
|
14140
|
+
this.#standDown("The leader lock could not be renewed.");
|
|
14141
|
+
return;
|
|
14142
|
+
}
|
|
14143
|
+
if (this.#renewing)
|
|
14144
|
+
return;
|
|
14145
|
+
this.#renewing = true;
|
|
14146
|
+
const started = Date.now();
|
|
14147
|
+
lock.acquire(ttl).then((held) => {
|
|
14148
|
+
this.#renewing = false;
|
|
14149
|
+
if (!this.#leading)
|
|
14150
|
+
return;
|
|
14151
|
+
if (held)
|
|
14152
|
+
this.#lastRenew = started;
|
|
14153
|
+
else
|
|
14154
|
+
this.#standDown("Another coordinator holds the leader lock.");
|
|
14155
|
+
}, (error) => {
|
|
14156
|
+
this.#renewing = false;
|
|
14157
|
+
this.#emit("error", asError(error));
|
|
14158
|
+
});
|
|
14159
|
+
}, Math.max(10, Math.floor(ttl / 4)));
|
|
14160
|
+
this.#renewTimer.unref?.();
|
|
14161
|
+
}
|
|
14162
|
+
#stopRenewing() {
|
|
14163
|
+
if (this.#renewTimer)
|
|
14164
|
+
clearInterval(this.#renewTimer);
|
|
14165
|
+
this.#renewTimer = undefined;
|
|
14166
|
+
}
|
|
14167
|
+
async#resign() {
|
|
14168
|
+
this.#stopRenewing();
|
|
14169
|
+
if (!this.#leading)
|
|
14170
|
+
return;
|
|
14171
|
+
this.#leading = false;
|
|
14172
|
+
const { lock, ttl = 1e4 } = this.#options.leader;
|
|
14173
|
+
try {
|
|
14174
|
+
await withTimeout(lock.release(), ttl / 2);
|
|
14175
|
+
} catch (error) {
|
|
14176
|
+
this.#emit("error", asError(error));
|
|
14177
|
+
}
|
|
14178
|
+
}
|
|
14179
|
+
#standDown(reason) {
|
|
14180
|
+
if (!this.#leading)
|
|
14181
|
+
return;
|
|
14182
|
+
this.#stopRenewing();
|
|
14183
|
+
this.#leading = false;
|
|
14184
|
+
this.#stopping = true;
|
|
14185
|
+
this.#halt();
|
|
14186
|
+
for (const cluster of this.#infos.values()) {
|
|
14187
|
+
cluster.state = "stopped";
|
|
14188
|
+
try {
|
|
14189
|
+
cluster.process.kill("SIGKILL");
|
|
14190
|
+
} catch {}
|
|
14191
|
+
}
|
|
14192
|
+
this.clusters.clear();
|
|
14193
|
+
this.#startPromise = undefined;
|
|
14194
|
+
this.#remote?.stop();
|
|
14195
|
+
this.#stopping = false;
|
|
14196
|
+
this.#emit("leaderLost", reason);
|
|
14197
|
+
this.spawn().catch((error) => this.#emit("error", asError(error)));
|
|
14198
|
+
}
|
|
13954
14199
|
async#placeHosts() {
|
|
13955
14200
|
const remote = this.#remote;
|
|
13956
14201
|
const options = this.#options.remote;
|
|
@@ -14086,14 +14331,59 @@ class ClusterManager {
|
|
|
14086
14331
|
attempt: info.restarts,
|
|
14087
14332
|
delay
|
|
14088
14333
|
});
|
|
14334
|
+
this.#restartLater(info, delay);
|
|
14335
|
+
}
|
|
14336
|
+
#restartLater(info, delay) {
|
|
14089
14337
|
const timer = setTimeout(() => {
|
|
14090
14338
|
this.#restartTimers.delete(timer);
|
|
14339
|
+
this.#fencing.delete(info.id);
|
|
14091
14340
|
if (this.#stopping || info.state !== "restarting")
|
|
14092
14341
|
return;
|
|
14093
|
-
this.#
|
|
14342
|
+
const wait = this.#relocate(info);
|
|
14343
|
+
if (wait > 0)
|
|
14344
|
+
this.#fencing.set(info.id, this.#restartLater(info, wait));
|
|
14345
|
+
else
|
|
14346
|
+
this.#launch(info.id, info.shards);
|
|
14094
14347
|
}, delay);
|
|
14095
14348
|
timer.unref?.();
|
|
14096
14349
|
this.#restartTimers.add(timer);
|
|
14350
|
+
return timer;
|
|
14351
|
+
}
|
|
14352
|
+
#hostJoined(hostId) {
|
|
14353
|
+
this.#emit("hostJoin", hostId);
|
|
14354
|
+
for (const [id, timer] of [...this.#fencing]) {
|
|
14355
|
+
const info = this.#infos.get(id);
|
|
14356
|
+
if (this.#hostOf.get(id) !== hostId || !info)
|
|
14357
|
+
continue;
|
|
14358
|
+
clearTimeout(timer);
|
|
14359
|
+
this.#restartTimers.delete(timer);
|
|
14360
|
+
this.#fencing.delete(id);
|
|
14361
|
+
if (!this.#stopping && info.state === "restarting")
|
|
14362
|
+
this.#launch(info.id, info.shards);
|
|
14363
|
+
}
|
|
14364
|
+
}
|
|
14365
|
+
#relocate(info) {
|
|
14366
|
+
const remote = this.#remote;
|
|
14367
|
+
const host = this.#hostOf.get(info.id) ?? "local";
|
|
14368
|
+
if (!remote || this.#options.remote?.moveClusters === false || host === "local" || remote.isConnected(host))
|
|
14369
|
+
return 0;
|
|
14370
|
+
const wait = remote.fencedAt(host) - Date.now();
|
|
14371
|
+
if (wait > 0)
|
|
14372
|
+
return wait;
|
|
14373
|
+
const places = this.#options.remote?.local ? ["local", ...remote.connected()] : remote.connected();
|
|
14374
|
+
if (places.length === 0)
|
|
14375
|
+
return 0;
|
|
14376
|
+
const load = new Map(places.map((place) => [place, 0]));
|
|
14377
|
+
for (const [cluster, place] of this.#hostOf) {
|
|
14378
|
+
const state = this.#infos.get(cluster)?.state;
|
|
14379
|
+
if (load.has(place) && state !== "failed" && state !== "stopped")
|
|
14380
|
+
load.set(place, load.get(place) + 1);
|
|
14381
|
+
}
|
|
14382
|
+
const target = places.reduce((best, place) => load.get(place) < load.get(best) ? place : best);
|
|
14383
|
+
this.#hostOf.set(info.id, target);
|
|
14384
|
+
info.host = target;
|
|
14385
|
+
this.#emit("clusterMoved", info, { from: host, to: target });
|
|
14386
|
+
return 0;
|
|
14097
14387
|
}
|
|
14098
14388
|
async#reassign(failed) {
|
|
14099
14389
|
const shardIds = [...failed.shards];
|
|
@@ -14515,14 +14805,18 @@ class ClusterManager {
|
|
|
14515
14805
|
cluster.state = "stopped";
|
|
14516
14806
|
this.clusters.clear();
|
|
14517
14807
|
this.#startPromise = undefined;
|
|
14518
|
-
if (!keepRemote)
|
|
14808
|
+
if (!keepRemote) {
|
|
14519
14809
|
this.#remote?.stop();
|
|
14810
|
+
await this.#resign();
|
|
14811
|
+
}
|
|
14520
14812
|
this.#stopping = false;
|
|
14521
14813
|
}
|
|
14522
14814
|
#halt() {
|
|
14815
|
+
this.#generation++;
|
|
14523
14816
|
for (const timer of this.#restartTimers)
|
|
14524
14817
|
clearTimeout(timer);
|
|
14525
14818
|
this.#restartTimers.clear();
|
|
14819
|
+
this.#fencing.clear();
|
|
14526
14820
|
for (const id of [...this.#startTimers.keys()])
|
|
14527
14821
|
this.#clearStartTimer(id);
|
|
14528
14822
|
for (const wake of [...this.#pauses])
|
|
@@ -14577,9 +14871,20 @@ class ClusterManager {
|
|
|
14577
14871
|
this.clusters.clear();
|
|
14578
14872
|
this.#startPromise = undefined;
|
|
14579
14873
|
this.#remote?.stop();
|
|
14874
|
+
this.#resign();
|
|
14580
14875
|
this.#stopping = false;
|
|
14581
14876
|
}
|
|
14582
14877
|
}
|
|
14878
|
+
function withTimeout(promise, ms) {
|
|
14879
|
+
let timer;
|
|
14880
|
+
const timeout = new Promise((_, reject) => {
|
|
14881
|
+
timer = setTimeout(() => reject(new Error(`The leader lock did not answer within ${ms} ms.`)), ms);
|
|
14882
|
+
});
|
|
14883
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
14884
|
+
}
|
|
14885
|
+
function asError(error) {
|
|
14886
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
14887
|
+
}
|
|
14583
14888
|
// packages/sharding/src/host.ts
|
|
14584
14889
|
import { fork as fork2 } from "child_process";
|
|
14585
14890
|
class ClusterHost {
|
|
@@ -14587,6 +14892,10 @@ class ClusterHost {
|
|
|
14587
14892
|
#running = new Map;
|
|
14588
14893
|
#listeners = new Map;
|
|
14589
14894
|
#socket;
|
|
14895
|
+
#urls;
|
|
14896
|
+
#urlIndex = 0;
|
|
14897
|
+
#unreachable = 0;
|
|
14898
|
+
#accepted = false;
|
|
14590
14899
|
#lease = 15000;
|
|
14591
14900
|
#lastRx = 0;
|
|
14592
14901
|
#watchdog;
|
|
@@ -14603,18 +14912,24 @@ class ClusterHost {
|
|
|
14603
14912
|
throw new RangeError("secret must be at least 16 characters.");
|
|
14604
14913
|
if (!options.script?.trim())
|
|
14605
14914
|
throw new TypeError("A worker script path is required.");
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
url
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14915
|
+
const urls = Array.isArray(options.url) ? [...options.url] : [options.url];
|
|
14916
|
+
if (urls.length === 0 || urls.length > 16)
|
|
14917
|
+
throw new TypeError("url must list 1 to 16 coordinators.");
|
|
14918
|
+
for (const raw of urls) {
|
|
14919
|
+
let url;
|
|
14920
|
+
try {
|
|
14921
|
+
url = new URL(raw);
|
|
14922
|
+
} catch {
|
|
14923
|
+
throw new TypeError("url must be an absolute ws:// or wss:// URL.");
|
|
14924
|
+
}
|
|
14925
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:")
|
|
14926
|
+
throw new TypeError("url must start with ws:// or wss://.");
|
|
14927
|
+
if (url.username || url.password)
|
|
14928
|
+
throw new TypeError("url must not contain credentials.");
|
|
14929
|
+
if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
|
|
14930
|
+
throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
|
|
14931
|
+
}
|
|
14932
|
+
this.#urls = urls;
|
|
14618
14933
|
this.#options = options;
|
|
14619
14934
|
this.#delay = options.reconnectDelay ?? 1000;
|
|
14620
14935
|
}
|
|
@@ -14646,6 +14961,7 @@ class ClusterHost {
|
|
|
14646
14961
|
return Promise.resolve();
|
|
14647
14962
|
this.#closed = false;
|
|
14648
14963
|
this.#busyAttempts = 0;
|
|
14964
|
+
this.#unreachable = 0;
|
|
14649
14965
|
return new Promise((resolve, reject) => {
|
|
14650
14966
|
this.#firstConnect = { resolve, reject };
|
|
14651
14967
|
this.#open();
|
|
@@ -14677,7 +14993,8 @@ class ClusterHost {
|
|
|
14677
14993
|
});
|
|
14678
14994
|
}
|
|
14679
14995
|
#open() {
|
|
14680
|
-
|
|
14996
|
+
this.#accepted = false;
|
|
14997
|
+
const socket = new WebSocket(this.#urls[this.#urlIndex]);
|
|
14681
14998
|
this.#socket = socket;
|
|
14682
14999
|
socket.addEventListener("message", (event) => this.#message(socket, event.data));
|
|
14683
15000
|
socket.addEventListener("error", () => {});
|
|
@@ -14739,6 +15056,7 @@ class ClusterHost {
|
|
|
14739
15056
|
return;
|
|
14740
15057
|
case "welcome":
|
|
14741
15058
|
this.#lease = message.leaseMs;
|
|
15059
|
+
this.#accepted = true;
|
|
14742
15060
|
this.#connected = true;
|
|
14743
15061
|
this.#delay = this.#options.reconnectDelay ?? 1000;
|
|
14744
15062
|
this.#startWatchdog();
|
|
@@ -14853,6 +15171,13 @@ class ClusterHost {
|
|
|
14853
15171
|
const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
|
|
14854
15172
|
const first = this.#firstConnect;
|
|
14855
15173
|
const busy = code === RemoteClose.TooManyConnections || code === RemoteClose.HelloTimeout;
|
|
15174
|
+
const accepted = this.#accepted;
|
|
15175
|
+
if (this.#urls.length > 1)
|
|
15176
|
+
this.#urlIndex = (this.#urlIndex + 1) % this.#urls.length;
|
|
15177
|
+
if (first && !refused && !busy && !accepted && ++this.#unreachable < this.#urls.length) {
|
|
15178
|
+
this.#open();
|
|
15179
|
+
return;
|
|
15180
|
+
}
|
|
14856
15181
|
if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
|
|
14857
15182
|
this.#retryLater();
|
|
14858
15183
|
return;
|
|
@@ -14880,6 +15205,43 @@ class ClusterHost {
|
|
|
14880
15205
|
function defaultFork(script, env) {
|
|
14881
15206
|
return fork2(script, [], { env });
|
|
14882
15207
|
}
|
|
15208
|
+
// packages/sharding/src/leader.ts
|
|
15209
|
+
var ACQUIRE = `local v = redis.call('GET', KEYS[1])
|
|
15210
|
+
if v == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
|
|
15211
|
+
if not v then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 end
|
|
15212
|
+
return 0`;
|
|
15213
|
+
var RELEASE = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
|
|
15214
|
+
return 0`;
|
|
15215
|
+
|
|
15216
|
+
class RedisLeaderLock {
|
|
15217
|
+
#client;
|
|
15218
|
+
#key;
|
|
15219
|
+
#token;
|
|
15220
|
+
constructor(options) {
|
|
15221
|
+
if (typeof options?.client?.send !== "function")
|
|
15222
|
+
throw new TypeError("RedisLeaderLock needs a client with send().");
|
|
15223
|
+
this.#client = options.client;
|
|
15224
|
+
this.#key = options.key ?? "lunibee:cluster:leader";
|
|
15225
|
+
this.#token = options.token ?? crypto.randomUUID();
|
|
15226
|
+
if (!this.#key || !this.#token)
|
|
15227
|
+
throw new TypeError("key and token must not be empty.");
|
|
15228
|
+
}
|
|
15229
|
+
async acquire(ttlMs) {
|
|
15230
|
+
if (!Number.isInteger(ttlMs) || ttlMs < 1)
|
|
15231
|
+
throw new RangeError("ttlMs must be a positive integer.");
|
|
15232
|
+
const reply = await this.#client.send("EVAL", [
|
|
15233
|
+
ACQUIRE,
|
|
15234
|
+
"1",
|
|
15235
|
+
this.#key,
|
|
15236
|
+
this.#token,
|
|
15237
|
+
String(ttlMs)
|
|
15238
|
+
]);
|
|
15239
|
+
return Number(reply) === 1;
|
|
15240
|
+
}
|
|
15241
|
+
async release() {
|
|
15242
|
+
await this.#client.send("EVAL", [RELEASE, "1", this.#key, this.#token]);
|
|
15243
|
+
}
|
|
15244
|
+
}
|
|
14883
15245
|
|
|
14884
15246
|
// packages/sharding/src/index.ts
|
|
14885
15247
|
var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -15839,6 +16201,7 @@ export {
|
|
|
15839
16201
|
RateLimiter,
|
|
15840
16202
|
RedisCache,
|
|
15841
16203
|
RedisCollection,
|
|
16204
|
+
RedisLeaderLock,
|
|
15842
16205
|
RedisRateLimitStore,
|
|
15843
16206
|
RemoteClose,
|
|
15844
16207
|
RequestScheduler,
|
|
@@ -15952,6 +16315,8 @@ export {
|
|
|
15952
16315
|
stickerURL,
|
|
15953
16316
|
strikethrough,
|
|
15954
16317
|
subtext,
|
|
16318
|
+
threadEndpoint,
|
|
16319
|
+
threadFork,
|
|
15955
16320
|
timestamp,
|
|
15956
16321
|
toRequest,
|
|
15957
16322
|
tokenFingerprint,
|
|
@@ -15961,4 +16326,4 @@ export {
|
|
|
15961
16326
|
validateShardIds
|
|
15962
16327
|
};
|
|
15963
16328
|
|
|
15964
|
-
//# debugId=
|
|
16329
|
+
//# debugId=15BE02BC328888A864756E2164756E21
|