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/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.partial ?? false;
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
- Object.assign(existing, resource);
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.2",
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
- Object.assign(existing, message);
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
- Object.assign(existing, emoji);
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
- Object.assign(existing, channel);
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 channel of this.values())
9882
- if (channel.guildId === guildId && this.delete(channel.id))
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.2",
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
- if (context && typeof process.send === "function")
12501
- ClusterWorker.#current = new ClusterWorker(context, process);
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" && typeof process.send === "function")
12687
- return new IpcTransport;
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 EventEmitter3 } from "events";
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 EventEmitter3 {
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.#emit("hostJoin", id),
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 (this.#stopping)
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 (this.#stopping)
14052
+ if (stale())
13906
14053
  return;
13907
14054
  throw error;
13908
14055
  });
13909
- if (!placement || this.#stopping)
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 (this.#stopping)
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 (this.#stopping)
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.#launch(info.id, info.shards);
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
- let url;
14607
- try {
14608
- url = new URL(options.url);
14609
- } catch {
14610
- throw new TypeError("url must be an absolute ws:// or wss:// URL.");
14611
- }
14612
- if (url.protocol !== "ws:" && url.protocol !== "wss:")
14613
- throw new TypeError("url must start with ws:// or wss://.");
14614
- if (url.username || url.password)
14615
- throw new TypeError("url must not contain credentials.");
14616
- if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
14617
- throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
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
- const socket = new WebSocket(this.#options.url);
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=42483B6395CE63EF64756E2164756E21
16329
+ //# debugId=15BE02BC328888A864756E2164756E21