lavalink-client 2.10.2 → 2.10.4

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.cjs CHANGED
@@ -1270,6 +1270,11 @@ function safeStringify(obj, padding = 0) {
1270
1270
  }
1271
1271
 
1272
1272
  // src/structures/Node.ts
1273
+ var normalizeInt = (value, fallback, min, max) => {
1274
+ const parsed = typeof value === "number" ? value : Number(value);
1275
+ if (!Number.isFinite(parsed)) return fallback;
1276
+ return Math.min(max, Math.max(min, Math.floor(parsed)));
1277
+ };
1273
1278
  var LavalinkNode = class _LavalinkNode {
1274
1279
  heartBeatPingTimestamp = 0;
1275
1280
  heartBeatPongTimestamp = 0;
@@ -1704,7 +1709,12 @@ var LavalinkNode = class _LavalinkNode {
1704
1709
  { headers }
1705
1710
  );
1706
1711
  this.socket.on("open", this.open.bind(this));
1707
- this.socket.on("close", (code, reason) => this.close(code, reason?.toString()));
1712
+ this.socket.on(
1713
+ "close",
1714
+ (code, reason) => void this.close(code, reason?.toString()).catch((error) => {
1715
+ this.NodeManager.emit("error", this, error);
1716
+ })
1717
+ );
1708
1718
  this.socket.on("message", this.message.bind(this));
1709
1719
  this.socket.on("error", this.error.bind(this));
1710
1720
  }
@@ -1796,43 +1806,7 @@ var LavalinkNode = class _LavalinkNode {
1796
1806
  )
1797
1807
  );
1798
1808
  }
1799
- const nodeToMove = Array.from(this.NodeManager.leastUsedNodes("playingPlayers")).find(
1800
- (n) => n.connected && n.options.id !== this.id
1801
- );
1802
- if (!nodeToMove) {
1803
- return Promise.allSettled(
1804
- Array.from(players.values()).map(
1805
- (player) => player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((error) => {
1806
- this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
1807
- state: "error",
1808
- message: `Failed to destroy player ${player.guildId}: ${error.message}`,
1809
- error,
1810
- functionLayer: "Node > destroy() > movePlayers"
1811
- });
1812
- })
1813
- )
1814
- );
1815
- }
1816
- return Promise.allSettled(
1817
- Array.from(players.values()).map(
1818
- (player) => player.changeNode(nodeToMove.options.id).catch((error) => {
1819
- this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1820
- state: "error",
1821
- message: `Failed to move player ${player.guildId}: ${error.message}`,
1822
- error,
1823
- functionLayer: "Node > destroy() > movePlayers"
1824
- });
1825
- return player.destroy(error.message ?? "PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
1826
- this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
1827
- state: "error",
1828
- message: `Failed to destroy player ${player.guildId} after move failure: ${destroyError.message}`,
1829
- error: destroyError,
1830
- functionLayer: "Node > destroy() > movePlayers"
1831
- });
1832
- });
1833
- })
1834
- )
1835
- );
1809
+ return this.movePlayersToReplacementNode(Array.from(players.values()), "destroy");
1836
1810
  };
1837
1811
  return void handlePlayerOperations().finally(() => {
1838
1812
  this.socket?.close(1e3, "Node-Destroy");
@@ -1871,6 +1845,247 @@ var LavalinkNode = class _LavalinkNode {
1871
1845
  this.resetReconnectionAttempts();
1872
1846
  this.NodeManager.emit("disconnect", this, { code: 1e3, reason: disconnectReason });
1873
1847
  }
1848
+ /**
1849
+ * Pick the best ready node to move players to.
1850
+ * A node must be connected and have a sessionId before a player can be migrated to it.
1851
+ */
1852
+ getReadyMigrationNode(excludeNodeId) {
1853
+ return this.selectMigrationTarget(/* @__PURE__ */ new Set([excludeNodeId]), Number.MAX_SAFE_INTEGER);
1854
+ }
1855
+ getMigrationErrorMessage(error) {
1856
+ if (error instanceof Error) return error.message;
1857
+ if (typeof error === "string") return error;
1858
+ try {
1859
+ return safeStringify(error);
1860
+ } catch {
1861
+ return String(error);
1862
+ }
1863
+ }
1864
+ getMigrationConfig() {
1865
+ const migration = this._LManager.options?.advancedOptions?.playerMigration;
1866
+ return {
1867
+ concurrency: normalizeInt(migration?.concurrency, 5, 1, 32),
1868
+ perTargetConcurrency: normalizeInt(migration?.perTargetConcurrency, 2, 1, 8),
1869
+ backpressureDelayMs: normalizeInt(migration?.backpressureDelayMs, 50, 0, 1e3)
1870
+ };
1871
+ }
1872
+ getMigrationReservationCount(nodeId) {
1873
+ return this.NodeManager.playerMigration.reservations.get(nodeId) || 0;
1874
+ }
1875
+ reserveMigrationTarget(nodeId) {
1876
+ this.NodeManager.playerMigration.reservations.set(nodeId, this.getMigrationReservationCount(nodeId) + 1);
1877
+ }
1878
+ releaseMigrationTarget(nodeId) {
1879
+ const current = this.getMigrationReservationCount(nodeId);
1880
+ if (current <= 1) this.NodeManager.playerMigration.reservations.delete(nodeId);
1881
+ else this.NodeManager.playerMigration.reservations.set(nodeId, current - 1);
1882
+ this.NodeManager.signalPlayerMigrationCapacityChange();
1883
+ }
1884
+ getEffectiveMigrationLoad(node) {
1885
+ return (node.stats?.players || 0) + this.getMigrationReservationCount(node.id);
1886
+ }
1887
+ selectMigrationTarget(excludeNodeIds, perTargetConcurrency) {
1888
+ let selectedNode = null;
1889
+ let selectedEffectiveLoad = Number.POSITIVE_INFINITY;
1890
+ let selectedLiveLoad = Number.POSITIVE_INFINITY;
1891
+ for (const node of this.NodeManager.nodes.values()) {
1892
+ if (!node.connected || !node.sessionId) continue;
1893
+ if (excludeNodeIds.has(node.id)) continue;
1894
+ const reservationCount = this.getMigrationReservationCount(node.id);
1895
+ if (reservationCount >= perTargetConcurrency) continue;
1896
+ const liveLoad = node.stats?.players || 0;
1897
+ const effectiveLoad = this.getEffectiveMigrationLoad(node);
1898
+ if (!selectedNode || effectiveLoad < selectedEffectiveLoad || effectiveLoad === selectedEffectiveLoad && (liveLoad < selectedLiveLoad || liveLoad === selectedLiveLoad && node.id.localeCompare(selectedNode.id) < 0)) {
1899
+ selectedNode = node;
1900
+ selectedEffectiveLoad = effectiveLoad;
1901
+ selectedLiveLoad = liveLoad;
1902
+ }
1903
+ }
1904
+ return selectedNode;
1905
+ }
1906
+ hasAnyHealthyMigrationNode(excludeNodeIds) {
1907
+ for (const node of this.NodeManager.nodes.values()) {
1908
+ if (!node.connected || !node.sessionId) continue;
1909
+ if (excludeNodeIds.has(node.id)) continue;
1910
+ return true;
1911
+ }
1912
+ return false;
1913
+ }
1914
+ isMigrationNodeHealthy(nodeId) {
1915
+ const node = this.NodeManager.nodes.get(nodeId);
1916
+ return !!node?.connected && !!node.sessionId;
1917
+ }
1918
+ async waitForMigrationCapacity(excludeNodeIds, perTargetConcurrency, timeoutMs) {
1919
+ if (this.selectMigrationTarget(excludeNodeIds, perTargetConcurrency)) return;
1920
+ if (timeoutMs <= 0) {
1921
+ await new Promise((resolve) => setImmediate(resolve));
1922
+ return;
1923
+ }
1924
+ await new Promise((resolve) => {
1925
+ let settled = false;
1926
+ let timer;
1927
+ const onSignal = () => {
1928
+ if (settled) return;
1929
+ settled = true;
1930
+ if (timer) clearTimeout(timer);
1931
+ this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
1932
+ resolve();
1933
+ };
1934
+ timer = setTimeout(() => {
1935
+ if (settled) return;
1936
+ settled = true;
1937
+ this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
1938
+ resolve();
1939
+ }, timeoutMs);
1940
+ this.NodeManager.playerMigration.capacityWaiters.add(onSignal);
1941
+ });
1942
+ }
1943
+ /**
1944
+ * Move a batch of players to a ready node with bounded concurrency.
1945
+ * This keeps migration responsive without flooding the replacement node with requests.
1946
+ */
1947
+ async movePlayersToReplacementNode(players, onNoEligibleNode) {
1948
+ const migrationQueue = players.filter((player) => {
1949
+ if (player.getData("internal_nodeMigrating") === true) return false;
1950
+ if (player.getData("internal_destroystatus") === true) return false;
1951
+ player.setData("internal_nodeMigrating", true);
1952
+ return true;
1953
+ });
1954
+ const { concurrency, perTargetConcurrency, backpressureDelayMs } = this.getMigrationConfig();
1955
+ let nextQueueIndex = 0;
1956
+ const workerCount = Math.min(concurrency, migrationQueue.length);
1957
+ const migratePlayer = async (player) => {
1958
+ const excludedNodes = /* @__PURE__ */ new Set();
1959
+ try {
1960
+ for (let attempt = 0; attempt < 2; attempt++) {
1961
+ if (player.getData("internal_destroystatus") === true) return;
1962
+ if (player.node?.options?.id !== this.id) return;
1963
+ let targetNode = null;
1964
+ while (!(targetNode = this.selectMigrationTarget(excludedNodes, perTargetConcurrency))) {
1965
+ if (!this.hasAnyHealthyMigrationNode(excludedNodes)) {
1966
+ this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
1967
+ state: "warn",
1968
+ message: `No eligible ready node was available for player ${player.guildId}`,
1969
+ functionLayer: "Node > movePlayersToReplacementNode()"
1970
+ });
1971
+ if (onNoEligibleNode === "destroy") {
1972
+ await player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((destroyError) => {
1973
+ this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
1974
+ state: "error",
1975
+ message: `Failed to destroy player ${player.guildId}: ${this.getMigrationErrorMessage(destroyError)}`,
1976
+ error: destroyError,
1977
+ functionLayer: "Node > movePlayersToReplacementNode()"
1978
+ });
1979
+ });
1980
+ } else {
1981
+ player.playing = false;
1982
+ }
1983
+ return;
1984
+ }
1985
+ await this.waitForMigrationCapacity(excludedNodes, perTargetConcurrency, backpressureDelayMs);
1986
+ }
1987
+ const targetNodeId = targetNode.id;
1988
+ if (player.getData("internal_destroystatus") === true) return;
1989
+ if (player.node?.options?.id !== this.id) return;
1990
+ this.reserveMigrationTarget(targetNodeId);
1991
+ try {
1992
+ await player.changeNode(targetNodeId);
1993
+ return;
1994
+ } catch (error) {
1995
+ const errorMessage = this.getMigrationErrorMessage(error);
1996
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1997
+ state: "error",
1998
+ message: `Failed to move player ${player.guildId}: ${errorMessage}`,
1999
+ error,
2000
+ functionLayer: "Node > movePlayersToReplacementNode()"
2001
+ });
2002
+ const targetStillHealthy = this.isMigrationNodeHealthy(targetNodeId);
2003
+ excludedNodes.add(targetNodeId);
2004
+ if (attempt === 0 && !targetStillHealthy) {
2005
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
2006
+ state: "warn",
2007
+ message: `Migration target ${targetNodeId} became unavailable while moving player ${player.guildId}; retrying on another ready node`,
2008
+ error,
2009
+ functionLayer: "Node > movePlayersToReplacementNode()"
2010
+ });
2011
+ if (player.node?.options?.id === targetNodeId) player.node = this;
2012
+ continue;
2013
+ }
2014
+ if (attempt === 0 && this.hasAnyHealthyMigrationNode(excludedNodes)) {
2015
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
2016
+ state: "warn",
2017
+ message: `Retrying migration for player ${player.guildId} on a different ready node`,
2018
+ error,
2019
+ functionLayer: "Node > movePlayersToReplacementNode()"
2020
+ });
2021
+ if (player.node?.options?.id === targetNodeId) player.node = this;
2022
+ continue;
2023
+ }
2024
+ if (onNoEligibleNode === "destroy") {
2025
+ await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
2026
+ this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
2027
+ state: "error",
2028
+ message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
2029
+ error: destroyError,
2030
+ functionLayer: "Node > movePlayersToReplacementNode()"
2031
+ });
2032
+ });
2033
+ } else {
2034
+ player.playing = false;
2035
+ }
2036
+ return;
2037
+ } finally {
2038
+ this.releaseMigrationTarget(targetNodeId);
2039
+ }
2040
+ }
2041
+ } catch (error) {
2042
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
2043
+ state: "error",
2044
+ message: `Unexpected error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
2045
+ error,
2046
+ functionLayer: "Node > movePlayersToReplacementNode()"
2047
+ });
2048
+ if (onNoEligibleNode === "destroy") {
2049
+ await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
2050
+ this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
2051
+ state: "error",
2052
+ message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
2053
+ error: destroyError,
2054
+ functionLayer: "Node > movePlayersToReplacementNode()"
2055
+ });
2056
+ });
2057
+ } else {
2058
+ player.playing = false;
2059
+ }
2060
+ }
2061
+ };
2062
+ const workers = Array.from({ length: workerCount }, async () => {
2063
+ while (true) {
2064
+ const queueIndex = nextQueueIndex++;
2065
+ if (queueIndex >= migrationQueue.length) return;
2066
+ const player = migrationQueue[queueIndex];
2067
+ if (!player) continue;
2068
+ try {
2069
+ if (player.getData("internal_nodeMigrating") !== true) {
2070
+ player.setData("internal_nodeMigrating", true);
2071
+ }
2072
+ if (player.getData("internal_destroystatus") === true) continue;
2073
+ if (player.node?.options?.id !== this.id) continue;
2074
+ await migratePlayer(player);
2075
+ } catch (error) {
2076
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
2077
+ state: "error",
2078
+ message: `Unexpected worker error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
2079
+ error,
2080
+ functionLayer: "Node > movePlayersToReplacementNode()"
2081
+ });
2082
+ } finally {
2083
+ player.setData("internal_nodeMigrating", void 0);
2084
+ }
2085
+ }
2086
+ });
2087
+ await Promise.all(workers);
2088
+ }
1874
2089
  /**
1875
2090
  * Gets all Players of a Node
1876
2091
  * @returns array of players inside of lavalink
@@ -2583,9 +2798,10 @@ var LavalinkNode = class _LavalinkNode {
2583
2798
  }
2584
2799
  this.info.isNodelink = !!this.info.isNodelink;
2585
2800
  this.NodeManager.emit("connect", this);
2801
+ this.NodeManager.signalPlayerMigrationCapacityChange();
2586
2802
  }
2587
2803
  /** @private util function for handling closing events from websocket */
2588
- close(code, reason) {
2804
+ async close(code, reason) {
2589
2805
  this.resetAckTimeouts(true, true);
2590
2806
  try {
2591
2807
  if (this.socket) {
@@ -2604,19 +2820,22 @@ var LavalinkNode = class _LavalinkNode {
2604
2820
  this.isAlive = false;
2605
2821
  if (code === 1006 && !reason) reason = "Socket got terminated due to no ping connection";
2606
2822
  if (code === 1e3 && reason === "Node-Disconnect") return;
2823
+ const players = Array.from(
2824
+ this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).values()
2825
+ );
2607
2826
  this.NodeManager.emit("disconnect", this, { code, reason });
2608
2827
  if (code !== 1e3 || reason !== "Node-Destroy") {
2609
2828
  if (this.NodeManager.nodes.has(this.id)) {
2610
2829
  this.reconnect();
2611
2830
  }
2612
2831
  }
2613
- this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).forEach((p) => {
2614
- if (!this._LManager.options.autoMove) return p.playing = false;
2615
- if (this._LManager.options.autoMove) {
2616
- if (this.NodeManager.nodes.filter((n) => n.connected).size === 0) return p.playing = false;
2617
- p.moveNode();
2618
- }
2619
- });
2832
+ if (this._LManager.options.autoMove && players.length) {
2833
+ await this.movePlayersToReplacementNode(players, "freeze");
2834
+ } else if (!this._LManager.options.autoMove) {
2835
+ players.forEach((p) => p.playing = false);
2836
+ } else if (this.NodeManager.nodes.filter((n) => n.connected && !!n.sessionId).size === 0) {
2837
+ players.forEach((p) => p.playing = false);
2838
+ }
2620
2839
  }
2621
2840
  /** @private util function for handling error events from websocket */
2622
2841
  error(error) {
@@ -3694,6 +3913,16 @@ var NodeManager = class extends import_node_events.EventEmitter {
3694
3913
  * A map of all nodes in the nodeManager
3695
3914
  */
3696
3915
  nodes = new MiniMap();
3916
+ /** @internal Coordinates player migrations between nodes owned by this manager. */
3917
+ playerMigration = {
3918
+ reservations: /* @__PURE__ */ new Map(),
3919
+ capacityWaiters: /* @__PURE__ */ new Set()
3920
+ };
3921
+ /** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
3922
+ signalPlayerMigrationCapacityChange() {
3923
+ for (const resolve of this.playerMigration.capacityWaiters) resolve();
3924
+ this.playerMigration.capacityWaiters.clear();
3925
+ }
3697
3926
  /**
3698
3927
  * @param LavalinkManager The LavalinkManager that created this NodeManager
3699
3928
  */
@@ -6073,8 +6302,10 @@ var Player = class {
6073
6302
  * @returns
6074
6303
  */
6075
6304
  async connect() {
6076
- if (!this.options.voiceChannelId)
6077
- throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6305
+ if (!this.options.voiceChannelId) {
6306
+ if (this.voiceChannelId) this.options.voiceChannelId = this.voiceChannelId;
6307
+ else throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6308
+ }
6078
6309
  await this.LavalinkManager.options.sendToShard(this.guildId, {
6079
6310
  op: 4,
6080
6311
  d: {
@@ -6111,8 +6342,10 @@ var Player = class {
6111
6342
  * @returns
6112
6343
  */
6113
6344
  async disconnect(force = false) {
6114
- if (!force && !this.options.voiceChannelId)
6115
- throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6345
+ if (!force && !this.options.voiceChannelId) {
6346
+ if (this.voiceChannelId) this.options.voiceChannelId = this.voiceChannelId;
6347
+ else throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6348
+ }
6116
6349
  await this.LavalinkManager.options.sendToShard(this.guildId, {
6117
6350
  op: 4,
6118
6351
  d: {
@@ -6341,8 +6574,8 @@ var Player = class {
6341
6574
  try {
6342
6575
  if (!node)
6343
6576
  node = Array.from(this.LavalinkManager.nodeManager.leastUsedNodes("playingPlayers")).find(
6344
- (n) => n.connected && n.options.id !== this.node.options.id
6345
- ).id;
6577
+ (n) => n.connected && !!n.sessionId && n.options.id !== this.node.options.id
6578
+ )?.id;
6346
6579
  if (!node || !this.LavalinkManager.nodeManager.nodes.get(node))
6347
6580
  throw new RangeError("No nodes are available.");
6348
6581
  if (this.node.options.id === node) return this;
@@ -6497,6 +6730,11 @@ var LavalinkManager = class _LavalinkManager extends import_node_events2.EventEm
6497
6730
  advancedOptions: {
6498
6731
  enableDebugEvents: options?.advancedOptions?.enableDebugEvents ?? false,
6499
6732
  maxFilterFixDuration: options?.advancedOptions?.maxFilterFixDuration ?? 6e5,
6733
+ playerMigration: {
6734
+ concurrency: options?.advancedOptions?.playerMigration?.concurrency ?? 5,
6735
+ perTargetConcurrency: options?.advancedOptions?.playerMigration?.perTargetConcurrency ?? 2,
6736
+ backpressureDelayMs: options?.advancedOptions?.playerMigration?.backpressureDelayMs ?? 50
6737
+ },
6500
6738
  debugOptions: {
6501
6739
  logCustomSearches: options?.advancedOptions?.debugOptions?.logCustomSearches ?? false,
6502
6740
  noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
package/dist/index.d.cts CHANGED
@@ -3306,6 +3306,26 @@ declare class LavalinkNode {
3306
3306
  * ```
3307
3307
  */
3308
3308
  disconnect(disconnectReason?: DisconnectReasonsType): void;
3309
+ /**
3310
+ * Pick the best ready node to move players to.
3311
+ * A node must be connected and have a sessionId before a player can be migrated to it.
3312
+ */
3313
+ private getReadyMigrationNode;
3314
+ private getMigrationErrorMessage;
3315
+ private getMigrationConfig;
3316
+ private getMigrationReservationCount;
3317
+ private reserveMigrationTarget;
3318
+ private releaseMigrationTarget;
3319
+ private getEffectiveMigrationLoad;
3320
+ private selectMigrationTarget;
3321
+ private hasAnyHealthyMigrationNode;
3322
+ private isMigrationNodeHealthy;
3323
+ private waitForMigrationCapacity;
3324
+ /**
3325
+ * Move a batch of players to a ready node with bounded concurrency.
3326
+ * This keeps migration responsive without flooding the replacement node with requests.
3327
+ */
3328
+ private movePlayersToReplacementNode;
3309
3329
  /**
3310
3330
  * Gets all Players of a Node
3311
3331
  * @returns array of players inside of lavalink
@@ -3741,6 +3761,13 @@ declare class NodeManager extends EventEmitter {
3741
3761
  * A map of all nodes in the nodeManager
3742
3762
  */
3743
3763
  nodes: MiniMap<string, LavalinkNode | NodeLinkNode>;
3764
+ /** @internal Coordinates player migrations between nodes owned by this manager. */
3765
+ readonly playerMigration: {
3766
+ reservations: Map<string, number>;
3767
+ capacityWaiters: Set<() => void>;
3768
+ };
3769
+ /** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
3770
+ signalPlayerMigrationCapacityChange(): void;
3744
3771
  /**
3745
3772
  * @param LavalinkManager The LavalinkManager that created this NodeManager
3746
3773
  */
@@ -4103,6 +4130,15 @@ interface ManagerOptions<CustomPlayerT extends Player = Player> {
4103
4130
  dontThrowError?: boolean;
4104
4131
  };
4105
4132
  };
4133
+ /** Player migration configuration */
4134
+ playerMigration?: {
4135
+ /** Maximum number of players migrated concurrently. Default: 5 */
4136
+ concurrency?: number;
4137
+ /** Maximum number of concurrent migrations to a single target node. Default: 2 */
4138
+ perTargetConcurrency?: number;
4139
+ /** Maximum time to wait for migration capacity before retrying. Default: 50ms */
4140
+ backpressureDelayMs?: number;
4141
+ };
4106
4142
  };
4107
4143
  }
4108
4144
 
package/dist/index.d.ts CHANGED
@@ -3306,6 +3306,26 @@ declare class LavalinkNode {
3306
3306
  * ```
3307
3307
  */
3308
3308
  disconnect(disconnectReason?: DisconnectReasonsType): void;
3309
+ /**
3310
+ * Pick the best ready node to move players to.
3311
+ * A node must be connected and have a sessionId before a player can be migrated to it.
3312
+ */
3313
+ private getReadyMigrationNode;
3314
+ private getMigrationErrorMessage;
3315
+ private getMigrationConfig;
3316
+ private getMigrationReservationCount;
3317
+ private reserveMigrationTarget;
3318
+ private releaseMigrationTarget;
3319
+ private getEffectiveMigrationLoad;
3320
+ private selectMigrationTarget;
3321
+ private hasAnyHealthyMigrationNode;
3322
+ private isMigrationNodeHealthy;
3323
+ private waitForMigrationCapacity;
3324
+ /**
3325
+ * Move a batch of players to a ready node with bounded concurrency.
3326
+ * This keeps migration responsive without flooding the replacement node with requests.
3327
+ */
3328
+ private movePlayersToReplacementNode;
3309
3329
  /**
3310
3330
  * Gets all Players of a Node
3311
3331
  * @returns array of players inside of lavalink
@@ -3741,6 +3761,13 @@ declare class NodeManager extends EventEmitter {
3741
3761
  * A map of all nodes in the nodeManager
3742
3762
  */
3743
3763
  nodes: MiniMap<string, LavalinkNode | NodeLinkNode>;
3764
+ /** @internal Coordinates player migrations between nodes owned by this manager. */
3765
+ readonly playerMigration: {
3766
+ reservations: Map<string, number>;
3767
+ capacityWaiters: Set<() => void>;
3768
+ };
3769
+ /** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
3770
+ signalPlayerMigrationCapacityChange(): void;
3744
3771
  /**
3745
3772
  * @param LavalinkManager The LavalinkManager that created this NodeManager
3746
3773
  */
@@ -4103,6 +4130,15 @@ interface ManagerOptions<CustomPlayerT extends Player = Player> {
4103
4130
  dontThrowError?: boolean;
4104
4131
  };
4105
4132
  };
4133
+ /** Player migration configuration */
4134
+ playerMigration?: {
4135
+ /** Maximum number of players migrated concurrently. Default: 5 */
4136
+ concurrency?: number;
4137
+ /** Maximum number of concurrent migrations to a single target node. Default: 2 */
4138
+ perTargetConcurrency?: number;
4139
+ /** Maximum time to wait for migration capacity before retrying. Default: 50ms */
4140
+ backpressureDelayMs?: number;
4141
+ };
4106
4142
  };
4107
4143
  }
4108
4144