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.
@@ -2,7 +2,7 @@
2
2
  // packages/sharding/package.json
3
3
  var package_default = {
4
4
  name: "@lunibee/sharding",
5
- version: "0.3.2",
5
+ version: "0.3.3",
6
6
  type: "module",
7
7
  module: "./src/index.ts",
8
8
  types: "./src/index.ts",
@@ -1479,14 +1479,109 @@ function parseParentFrame(value) {
1479
1479
  }
1480
1480
  }
1481
1481
 
1482
+ // packages/sharding/src/thread.ts
1483
+ import { EventEmitter } from "events";
1484
+ import {
1485
+ isMainThread,
1486
+ parentPort,
1487
+ Worker,
1488
+ workerData
1489
+ } from "worker_threads";
1490
+ var THREAD_MARKER = "__lunibeeClusterThread";
1491
+
1492
+ class ThreadChild extends EventEmitter {
1493
+ pid;
1494
+ exitCode = null;
1495
+ signalCode = null;
1496
+ connected = true;
1497
+ #worker;
1498
+ #signal;
1499
+ constructor(worker) {
1500
+ super();
1501
+ this.#worker = worker;
1502
+ this.pid = worker.threadId;
1503
+ worker.on("message", (frame) => this.emit("message", frame));
1504
+ worker.on("error", (error) => this.emit("error", error));
1505
+ worker.once("exit", (code) => {
1506
+ this.connected = false;
1507
+ if (this.#signal) {
1508
+ this.signalCode = this.#signal;
1509
+ this.emit("exit", null, this.#signal);
1510
+ } else {
1511
+ this.exitCode = code;
1512
+ this.emit("exit", code, null);
1513
+ }
1514
+ });
1515
+ }
1516
+ send(frame) {
1517
+ if (!this.connected)
1518
+ return false;
1519
+ try {
1520
+ this.#worker.postMessage(frame);
1521
+ return true;
1522
+ } catch {
1523
+ return false;
1524
+ }
1525
+ }
1526
+ kill(signal = "SIGTERM") {
1527
+ if (!this.connected)
1528
+ return false;
1529
+ this.#signal ??= signal;
1530
+ this.#worker.terminate();
1531
+ return true;
1532
+ }
1533
+ }
1534
+ function threadFork(options = {}) {
1535
+ return (script, env) => {
1536
+ const defined = {};
1537
+ for (const [key, value] of Object.entries(env))
1538
+ if (value !== undefined)
1539
+ defined[key] = value;
1540
+ const worker = new Worker(script, {
1541
+ env: defined,
1542
+ workerData: { [THREAD_MARKER]: true },
1543
+ ...options.resourceLimits ? { resourceLimits: options.resourceLimits } : {}
1544
+ });
1545
+ return new ThreadChild(worker);
1546
+ };
1547
+ }
1548
+ var endpoint;
1549
+ function threadEndpoint() {
1550
+ const port = parentPort;
1551
+ if (isMainThread || !port || !workerData?.[THREAD_MARKER])
1552
+ return;
1553
+ endpoint ??= portEndpoint(port);
1554
+ return endpoint;
1555
+ }
1556
+ function portEndpoint(port) {
1557
+ const wrapped = {
1558
+ send: (message) => {
1559
+ port.postMessage(message);
1560
+ return true;
1561
+ },
1562
+ on: (event, listener) => {
1563
+ if (event === "message")
1564
+ port.on("message", listener);
1565
+ return wrapped;
1566
+ },
1567
+ off: (event, listener) => {
1568
+ if (event === "message")
1569
+ port.off("message", listener);
1570
+ return wrapped;
1571
+ }
1572
+ };
1573
+ return wrapped;
1574
+ }
1575
+
1482
1576
  // packages/sharding/src/worker.ts
1483
1577
  class ClusterWorker {
1484
1578
  static #current;
1485
1579
  static current() {
1486
1580
  if (!ClusterWorker.#current) {
1487
1581
  const context = readClusterContext();
1488
- if (context && typeof process.send === "function")
1489
- ClusterWorker.#current = new ClusterWorker(context, process);
1582
+ const endpoint = typeof process.send === "function" ? process : threadEndpoint();
1583
+ if (context && endpoint)
1584
+ ClusterWorker.#current = new ClusterWorker(context, endpoint);
1490
1585
  }
1491
1586
  return ClusterWorker.#current;
1492
1587
  }
@@ -1671,8 +1766,13 @@ class IpcTransport {
1671
1766
  }
1672
1767
  }
1673
1768
  function defaultTransport(name) {
1674
- if (process.env.LUNIBEE_SHARD_BUS === "ipc" && typeof process.send === "function")
1675
- return new IpcTransport;
1769
+ if (process.env.LUNIBEE_SHARD_BUS === "ipc") {
1770
+ if (typeof process.send === "function")
1771
+ return new IpcTransport;
1772
+ const thread = threadEndpoint();
1773
+ if (thread)
1774
+ return new IpcTransport(thread);
1775
+ }
1676
1776
  return new BroadcastChannelTransport(name);
1677
1777
  }
1678
1778
 
@@ -2119,7 +2219,7 @@ class IdentifyCoordinator {
2119
2219
  }
2120
2220
 
2121
2221
  // packages/sharding/src/remote.ts
2122
- import { EventEmitter } from "events";
2222
+ import { EventEmitter as EventEmitter2 } from "events";
2123
2223
  import { randomBytes } from "crypto";
2124
2224
 
2125
2225
  // packages/sharding/src/remote-protocol.ts
@@ -2270,7 +2370,7 @@ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
2270
2370
  }
2271
2371
 
2272
2372
  // packages/sharding/src/remote.ts
2273
- class RemoteChild extends EventEmitter {
2373
+ class RemoteChild extends EventEmitter2 {
2274
2374
  host;
2275
2375
  cluster;
2276
2376
  epoch;
@@ -2316,6 +2416,7 @@ class RemoteChild extends EventEmitter {
2316
2416
  class RemoteHosts {
2317
2417
  #options;
2318
2418
  #links = new Map;
2419
+ #droppedAt = new Map;
2319
2420
  #waiters = new Set;
2320
2421
  #hooks;
2321
2422
  #server;
@@ -2353,6 +2454,13 @@ class RemoteHosts {
2353
2454
  connected() {
2354
2455
  return [...this.#links.keys()].sort();
2355
2456
  }
2457
+ isConnected(hostId) {
2458
+ return this.#links.has(hostId);
2459
+ }
2460
+ fencedAt(hostId) {
2461
+ const dropped = this.#droppedAt.get(hostId);
2462
+ return dropped === undefined || this.#links.has(hostId) ? 0 : dropped + this.#options.lease * 2;
2463
+ }
2356
2464
  start() {
2357
2465
  if (this.#server)
2358
2466
  return;
@@ -2585,6 +2693,7 @@ class RemoteHosts {
2585
2693
  children: new Map
2586
2694
  };
2587
2695
  this.#links.set(id, link);
2696
+ this.#droppedAt.delete(id);
2588
2697
  const lease = this.#options.lease;
2589
2698
  this.#post(link, {
2590
2699
  t: "welcome",
@@ -2617,6 +2726,9 @@ class RemoteHosts {
2617
2726
  if (this.#links.get(link.id) !== link)
2618
2727
  return;
2619
2728
  this.#links.delete(link.id);
2729
+ this.#droppedAt.set(link.id, Date.now());
2730
+ if (this.#droppedAt.size > 1000)
2731
+ this.#droppedAt.delete(this.#droppedAt.keys().next().value);
2620
2732
  if (code !== undefined)
2621
2733
  try {
2622
2734
  link.ws.close(code, reason.slice(0, 100));
@@ -2751,7 +2863,13 @@ class ClusterManager {
2751
2863
  #stopPromise;
2752
2864
  #wasStarted = false;
2753
2865
  #stopping = false;
2866
+ #generation = 0;
2867
+ #leading = false;
2868
+ #renewTimer;
2869
+ #lastRenew = 0;
2870
+ #renewing = false;
2754
2871
  #restartTimers = new Set;
2872
+ #fencing = new Map;
2755
2873
  #startTimers = new Map;
2756
2874
  #pauses = new Set;
2757
2875
  #readyWaiters = new Set;
@@ -2799,6 +2917,11 @@ class ClusterManager {
2799
2917
  if (options.shardIds && typeof options.shardCount === "number")
2800
2918
  validateShardIds(options.shardIds, options.shardCount);
2801
2919
  this.#auto = options.shardCount === "auto";
2920
+ const ttl = options.leader?.ttl;
2921
+ if (options.leader && (typeof options.leader.lock?.acquire !== "function" || typeof options.leader.lock.release !== "function"))
2922
+ throw new TypeError("leader.lock must have acquire() and release().");
2923
+ if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 100))
2924
+ throw new RangeError("leader.ttl must be an integer of at least 100.");
2802
2925
  this.#options = { ...options };
2803
2926
  this.#supervisor = new ShardSupervisor({
2804
2927
  restartDelay: options.restartDelay,
@@ -2807,7 +2930,7 @@ class ClusterManager {
2807
2930
  this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
2808
2931
  if (options.remote)
2809
2932
  this.#remote = new RemoteHosts(options.remote, {
2810
- join: (id) => this.#emit("hostJoin", id),
2933
+ join: (id) => this.#hostJoined(id),
2811
2934
  leave: (id, reason) => this.#emit("hostLeave", id, reason),
2812
2935
  error: (error) => this.#emit("error", error)
2813
2936
  });
@@ -2881,8 +3004,15 @@ class ClusterManager {
2881
3004
  };
2882
3005
  }
2883
3006
  async#start() {
3007
+ const generation = this.#generation;
3008
+ const stale = () => this.#stopping || generation !== this.#generation;
3009
+ if (this.#options.leader && !this.#leading) {
3010
+ await this.#lead();
3011
+ if (stale())
3012
+ return;
3013
+ }
2884
3014
  const { count, maxConcurrency, budget } = await this.#resolveGateway();
2885
- if (this.#stopping)
3015
+ if (stale())
2886
3016
  return;
2887
3017
  const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
2888
3018
  validateShardIds(ids, count);
@@ -2890,11 +3020,11 @@ class ClusterManager {
2890
3020
  const chunks = allocateShards(ids, clusterCount);
2891
3021
  assertCompleteAllocation(chunks, ids);
2892
3022
  const placement = await this.#placeHosts().catch((error) => {
2893
- if (this.#stopping)
3023
+ if (stale())
2894
3024
  return;
2895
3025
  throw error;
2896
3026
  });
2897
- if (!placement || this.#stopping)
3027
+ if (!placement || stale())
2898
3028
  return;
2899
3029
  this.shardCount = count;
2900
3030
  this.#wasStarted = true;
@@ -2920,7 +3050,7 @@ class ClusterManager {
2920
3050
  for (const chunk of active)
2921
3051
  this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
2922
3052
  for (const [index, chunk] of active.entries()) {
2923
- if (this.#stopping)
3053
+ if (stale())
2924
3054
  return;
2925
3055
  for (const shard of chunk.shards)
2926
3056
  this.#owner.set(shard, chunk.id);
@@ -2929,7 +3059,7 @@ class ClusterManager {
2929
3059
  if (index < active.length - 1 && gap > 0)
2930
3060
  await this.#pause(gap);
2931
3061
  }
2932
- if (this.#stopping)
3062
+ if (stale())
2933
3063
  return;
2934
3064
  if (this.#options.waitForReady)
2935
3065
  await this.ready();
@@ -2939,6 +3069,104 @@ class ClusterManager {
2939
3069
  }, this.#options.autoScaleInterval);
2940
3070
  }
2941
3071
  }
3072
+ get isLeader() {
3073
+ return !this.#options.leader || this.#leading;
3074
+ }
3075
+ async#lead() {
3076
+ const { lock, ttl = 1e4 } = this.#options.leader;
3077
+ const generation = this.#generation;
3078
+ while (!this.#stopping && generation === this.#generation) {
3079
+ const started = Date.now();
3080
+ let held = false;
3081
+ try {
3082
+ held = await withTimeout(lock.acquire(ttl), ttl / 2);
3083
+ } catch (error) {
3084
+ this.#emit("error", asError(error));
3085
+ }
3086
+ if (this.#stopping || generation !== this.#generation) {
3087
+ if (held)
3088
+ lock.release().catch(() => {
3089
+ return;
3090
+ });
3091
+ return;
3092
+ }
3093
+ if (held) {
3094
+ this.#leading = true;
3095
+ this.#lastRenew = started;
3096
+ this.#startRenewing(ttl);
3097
+ this.#emit("leaderAcquired");
3098
+ const lease = this.#options.remote ? this.#options.remote.lease ?? 15000 : 0;
3099
+ if (lease > 0)
3100
+ await this.#pause(lease * 2);
3101
+ return;
3102
+ }
3103
+ await this.#pause(Math.floor(ttl / 2));
3104
+ }
3105
+ }
3106
+ #startRenewing(ttl) {
3107
+ this.#stopRenewing();
3108
+ const lock = this.#options.leader.lock;
3109
+ this.#renewTimer = setInterval(() => {
3110
+ if (Date.now() - this.#lastRenew > ttl / 2) {
3111
+ this.#standDown("The leader lock could not be renewed.");
3112
+ return;
3113
+ }
3114
+ if (this.#renewing)
3115
+ return;
3116
+ this.#renewing = true;
3117
+ const started = Date.now();
3118
+ lock.acquire(ttl).then((held) => {
3119
+ this.#renewing = false;
3120
+ if (!this.#leading)
3121
+ return;
3122
+ if (held)
3123
+ this.#lastRenew = started;
3124
+ else
3125
+ this.#standDown("Another coordinator holds the leader lock.");
3126
+ }, (error) => {
3127
+ this.#renewing = false;
3128
+ this.#emit("error", asError(error));
3129
+ });
3130
+ }, Math.max(10, Math.floor(ttl / 4)));
3131
+ this.#renewTimer.unref?.();
3132
+ }
3133
+ #stopRenewing() {
3134
+ if (this.#renewTimer)
3135
+ clearInterval(this.#renewTimer);
3136
+ this.#renewTimer = undefined;
3137
+ }
3138
+ async#resign() {
3139
+ this.#stopRenewing();
3140
+ if (!this.#leading)
3141
+ return;
3142
+ this.#leading = false;
3143
+ const { lock, ttl = 1e4 } = this.#options.leader;
3144
+ try {
3145
+ await withTimeout(lock.release(), ttl / 2);
3146
+ } catch (error) {
3147
+ this.#emit("error", asError(error));
3148
+ }
3149
+ }
3150
+ #standDown(reason) {
3151
+ if (!this.#leading)
3152
+ return;
3153
+ this.#stopRenewing();
3154
+ this.#leading = false;
3155
+ this.#stopping = true;
3156
+ this.#halt();
3157
+ for (const cluster of this.#infos.values()) {
3158
+ cluster.state = "stopped";
3159
+ try {
3160
+ cluster.process.kill("SIGKILL");
3161
+ } catch {}
3162
+ }
3163
+ this.clusters.clear();
3164
+ this.#startPromise = undefined;
3165
+ this.#remote?.stop();
3166
+ this.#stopping = false;
3167
+ this.#emit("leaderLost", reason);
3168
+ this.spawn().catch((error) => this.#emit("error", asError(error)));
3169
+ }
2942
3170
  async#placeHosts() {
2943
3171
  const remote = this.#remote;
2944
3172
  const options = this.#options.remote;
@@ -3074,14 +3302,59 @@ class ClusterManager {
3074
3302
  attempt: info.restarts,
3075
3303
  delay
3076
3304
  });
3305
+ this.#restartLater(info, delay);
3306
+ }
3307
+ #restartLater(info, delay) {
3077
3308
  const timer = setTimeout(() => {
3078
3309
  this.#restartTimers.delete(timer);
3310
+ this.#fencing.delete(info.id);
3079
3311
  if (this.#stopping || info.state !== "restarting")
3080
3312
  return;
3081
- this.#launch(info.id, info.shards);
3313
+ const wait = this.#relocate(info);
3314
+ if (wait > 0)
3315
+ this.#fencing.set(info.id, this.#restartLater(info, wait));
3316
+ else
3317
+ this.#launch(info.id, info.shards);
3082
3318
  }, delay);
3083
3319
  timer.unref?.();
3084
3320
  this.#restartTimers.add(timer);
3321
+ return timer;
3322
+ }
3323
+ #hostJoined(hostId) {
3324
+ this.#emit("hostJoin", hostId);
3325
+ for (const [id, timer] of [...this.#fencing]) {
3326
+ const info = this.#infos.get(id);
3327
+ if (this.#hostOf.get(id) !== hostId || !info)
3328
+ continue;
3329
+ clearTimeout(timer);
3330
+ this.#restartTimers.delete(timer);
3331
+ this.#fencing.delete(id);
3332
+ if (!this.#stopping && info.state === "restarting")
3333
+ this.#launch(info.id, info.shards);
3334
+ }
3335
+ }
3336
+ #relocate(info) {
3337
+ const remote = this.#remote;
3338
+ const host = this.#hostOf.get(info.id) ?? "local";
3339
+ if (!remote || this.#options.remote?.moveClusters === false || host === "local" || remote.isConnected(host))
3340
+ return 0;
3341
+ const wait = remote.fencedAt(host) - Date.now();
3342
+ if (wait > 0)
3343
+ return wait;
3344
+ const places = this.#options.remote?.local ? ["local", ...remote.connected()] : remote.connected();
3345
+ if (places.length === 0)
3346
+ return 0;
3347
+ const load = new Map(places.map((place) => [place, 0]));
3348
+ for (const [cluster, place] of this.#hostOf) {
3349
+ const state = this.#infos.get(cluster)?.state;
3350
+ if (load.has(place) && state !== "failed" && state !== "stopped")
3351
+ load.set(place, load.get(place) + 1);
3352
+ }
3353
+ const target = places.reduce((best, place) => load.get(place) < load.get(best) ? place : best);
3354
+ this.#hostOf.set(info.id, target);
3355
+ info.host = target;
3356
+ this.#emit("clusterMoved", info, { from: host, to: target });
3357
+ return 0;
3085
3358
  }
3086
3359
  async#reassign(failed) {
3087
3360
  const shardIds = [...failed.shards];
@@ -3503,14 +3776,18 @@ class ClusterManager {
3503
3776
  cluster.state = "stopped";
3504
3777
  this.clusters.clear();
3505
3778
  this.#startPromise = undefined;
3506
- if (!keepRemote)
3779
+ if (!keepRemote) {
3507
3780
  this.#remote?.stop();
3781
+ await this.#resign();
3782
+ }
3508
3783
  this.#stopping = false;
3509
3784
  }
3510
3785
  #halt() {
3786
+ this.#generation++;
3511
3787
  for (const timer of this.#restartTimers)
3512
3788
  clearTimeout(timer);
3513
3789
  this.#restartTimers.clear();
3790
+ this.#fencing.clear();
3514
3791
  for (const id of [...this.#startTimers.keys()])
3515
3792
  this.#clearStartTimer(id);
3516
3793
  for (const wake of [...this.#pauses])
@@ -3565,9 +3842,20 @@ class ClusterManager {
3565
3842
  this.clusters.clear();
3566
3843
  this.#startPromise = undefined;
3567
3844
  this.#remote?.stop();
3845
+ this.#resign();
3568
3846
  this.#stopping = false;
3569
3847
  }
3570
3848
  }
3849
+ function withTimeout(promise, ms) {
3850
+ let timer;
3851
+ const timeout = new Promise((_, reject) => {
3852
+ timer = setTimeout(() => reject(new Error(`The leader lock did not answer within ${ms} ms.`)), ms);
3853
+ });
3854
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
3855
+ }
3856
+ function asError(error) {
3857
+ return error instanceof Error ? error : new Error(String(error));
3858
+ }
3571
3859
  // packages/sharding/src/host.ts
3572
3860
  import { fork as fork2 } from "child_process";
3573
3861
  class ClusterHost {
@@ -3575,6 +3863,10 @@ class ClusterHost {
3575
3863
  #running = new Map;
3576
3864
  #listeners = new Map;
3577
3865
  #socket;
3866
+ #urls;
3867
+ #urlIndex = 0;
3868
+ #unreachable = 0;
3869
+ #accepted = false;
3578
3870
  #lease = 15000;
3579
3871
  #lastRx = 0;
3580
3872
  #watchdog;
@@ -3591,18 +3883,24 @@ class ClusterHost {
3591
3883
  throw new RangeError("secret must be at least 16 characters.");
3592
3884
  if (!options.script?.trim())
3593
3885
  throw new TypeError("A worker script path is required.");
3594
- let url;
3595
- try {
3596
- url = new URL(options.url);
3597
- } catch {
3598
- throw new TypeError("url must be an absolute ws:// or wss:// URL.");
3886
+ const urls = Array.isArray(options.url) ? [...options.url] : [options.url];
3887
+ if (urls.length === 0 || urls.length > 16)
3888
+ throw new TypeError("url must list 1 to 16 coordinators.");
3889
+ for (const raw of urls) {
3890
+ let url;
3891
+ try {
3892
+ url = new URL(raw);
3893
+ } catch {
3894
+ throw new TypeError("url must be an absolute ws:// or wss:// URL.");
3895
+ }
3896
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
3897
+ throw new TypeError("url must start with ws:// or wss://.");
3898
+ if (url.username || url.password)
3899
+ throw new TypeError("url must not contain credentials.");
3900
+ if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
3901
+ throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
3599
3902
  }
3600
- if (url.protocol !== "ws:" && url.protocol !== "wss:")
3601
- throw new TypeError("url must start with ws:// or wss://.");
3602
- if (url.username || url.password)
3603
- throw new TypeError("url must not contain credentials.");
3604
- if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
3605
- throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
3903
+ this.#urls = urls;
3606
3904
  this.#options = options;
3607
3905
  this.#delay = options.reconnectDelay ?? 1000;
3608
3906
  }
@@ -3634,6 +3932,7 @@ class ClusterHost {
3634
3932
  return Promise.resolve();
3635
3933
  this.#closed = false;
3636
3934
  this.#busyAttempts = 0;
3935
+ this.#unreachable = 0;
3637
3936
  return new Promise((resolve, reject) => {
3638
3937
  this.#firstConnect = { resolve, reject };
3639
3938
  this.#open();
@@ -3665,7 +3964,8 @@ class ClusterHost {
3665
3964
  });
3666
3965
  }
3667
3966
  #open() {
3668
- const socket = new WebSocket(this.#options.url);
3967
+ this.#accepted = false;
3968
+ const socket = new WebSocket(this.#urls[this.#urlIndex]);
3669
3969
  this.#socket = socket;
3670
3970
  socket.addEventListener("message", (event) => this.#message(socket, event.data));
3671
3971
  socket.addEventListener("error", () => {});
@@ -3727,6 +4027,7 @@ class ClusterHost {
3727
4027
  return;
3728
4028
  case "welcome":
3729
4029
  this.#lease = message.leaseMs;
4030
+ this.#accepted = true;
3730
4031
  this.#connected = true;
3731
4032
  this.#delay = this.#options.reconnectDelay ?? 1000;
3732
4033
  this.#startWatchdog();
@@ -3841,6 +4142,13 @@ class ClusterHost {
3841
4142
  const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
3842
4143
  const first = this.#firstConnect;
3843
4144
  const busy = code === RemoteClose.TooManyConnections || code === RemoteClose.HelloTimeout;
4145
+ const accepted = this.#accepted;
4146
+ if (this.#urls.length > 1)
4147
+ this.#urlIndex = (this.#urlIndex + 1) % this.#urls.length;
4148
+ if (first && !refused && !busy && !accepted && ++this.#unreachable < this.#urls.length) {
4149
+ this.#open();
4150
+ return;
4151
+ }
3844
4152
  if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
3845
4153
  this.#retryLater();
3846
4154
  return;
@@ -3868,6 +4176,43 @@ class ClusterHost {
3868
4176
  function defaultFork(script, env) {
3869
4177
  return fork2(script, [], { env });
3870
4178
  }
4179
+ // packages/sharding/src/leader.ts
4180
+ var ACQUIRE = `local v = redis.call('GET', KEYS[1])
4181
+ if v == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
4182
+ if not v then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 end
4183
+ return 0`;
4184
+ var RELEASE = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
4185
+ return 0`;
4186
+
4187
+ class RedisLeaderLock {
4188
+ #client;
4189
+ #key;
4190
+ #token;
4191
+ constructor(options) {
4192
+ if (typeof options?.client?.send !== "function")
4193
+ throw new TypeError("RedisLeaderLock needs a client with send().");
4194
+ this.#client = options.client;
4195
+ this.#key = options.key ?? "lunibee:cluster:leader";
4196
+ this.#token = options.token ?? crypto.randomUUID();
4197
+ if (!this.#key || !this.#token)
4198
+ throw new TypeError("key and token must not be empty.");
4199
+ }
4200
+ async acquire(ttlMs) {
4201
+ if (!Number.isInteger(ttlMs) || ttlMs < 1)
4202
+ throw new RangeError("ttlMs must be a positive integer.");
4203
+ const reply = await this.#client.send("EVAL", [
4204
+ ACQUIRE,
4205
+ "1",
4206
+ this.#key,
4207
+ this.#token,
4208
+ String(ttlMs)
4209
+ ]);
4210
+ return Number(reply) === 1;
4211
+ }
4212
+ async release() {
4213
+ await this.#client.send("EVAL", [RELEASE, "1", this.#key, this.#token]);
4214
+ }
4215
+ }
3871
4216
 
3872
4217
  // packages/sharding/src/index.ts
3873
4218
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -4258,6 +4603,7 @@ export {
4258
4603
  IpcTransport,
4259
4604
  MAX_GUILDS_PER_SHARD,
4260
4605
  REMOTE_PROTOCOL_VERSION,
4606
+ RedisLeaderLock,
4261
4607
  RemoteClose,
4262
4608
  RequestTracker,
4263
4609
  ShardBus,
@@ -4271,7 +4617,9 @@ export {
4271
4617
  isClusterWorker,
4272
4618
  readClusterContext,
4273
4619
  shardsForGuilds,
4620
+ threadEndpoint,
4621
+ threadFork,
4274
4622
  validateShardIds
4275
4623
  };
4276
4624
 
4277
- //# debugId=8AD6CE4D87FD51CB64756E2164756E21
4625
+ //# debugId=D3CF0E421E8E307864756E2164756E21