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.js CHANGED
@@ -1204,6 +1204,11 @@ function safeStringify(obj, padding = 0) {
1204
1204
  }
1205
1205
 
1206
1206
  // src/structures/Node.ts
1207
+ var normalizeInt = (value, fallback, min, max) => {
1208
+ const parsed = typeof value === "number" ? value : Number(value);
1209
+ if (!Number.isFinite(parsed)) return fallback;
1210
+ return Math.min(max, Math.max(min, Math.floor(parsed)));
1211
+ };
1207
1212
  var LavalinkNode = class _LavalinkNode {
1208
1213
  heartBeatPingTimestamp = 0;
1209
1214
  heartBeatPongTimestamp = 0;
@@ -1638,7 +1643,12 @@ var LavalinkNode = class _LavalinkNode {
1638
1643
  { headers }
1639
1644
  );
1640
1645
  this.socket.on("open", this.open.bind(this));
1641
- this.socket.on("close", (code, reason) => this.close(code, reason?.toString()));
1646
+ this.socket.on(
1647
+ "close",
1648
+ (code, reason) => void this.close(code, reason?.toString()).catch((error) => {
1649
+ this.NodeManager.emit("error", this, error);
1650
+ })
1651
+ );
1642
1652
  this.socket.on("message", this.message.bind(this));
1643
1653
  this.socket.on("error", this.error.bind(this));
1644
1654
  }
@@ -1730,43 +1740,7 @@ var LavalinkNode = class _LavalinkNode {
1730
1740
  )
1731
1741
  );
1732
1742
  }
1733
- const nodeToMove = Array.from(this.NodeManager.leastUsedNodes("playingPlayers")).find(
1734
- (n) => n.connected && n.options.id !== this.id
1735
- );
1736
- if (!nodeToMove) {
1737
- return Promise.allSettled(
1738
- Array.from(players.values()).map(
1739
- (player) => player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((error) => {
1740
- this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
1741
- state: "error",
1742
- message: `Failed to destroy player ${player.guildId}: ${error.message}`,
1743
- error,
1744
- functionLayer: "Node > destroy() > movePlayers"
1745
- });
1746
- })
1747
- )
1748
- );
1749
- }
1750
- return Promise.allSettled(
1751
- Array.from(players.values()).map(
1752
- (player) => player.changeNode(nodeToMove.options.id).catch((error) => {
1753
- this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1754
- state: "error",
1755
- message: `Failed to move player ${player.guildId}: ${error.message}`,
1756
- error,
1757
- functionLayer: "Node > destroy() > movePlayers"
1758
- });
1759
- return player.destroy(error.message ?? "PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
1760
- this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
1761
- state: "error",
1762
- message: `Failed to destroy player ${player.guildId} after move failure: ${destroyError.message}`,
1763
- error: destroyError,
1764
- functionLayer: "Node > destroy() > movePlayers"
1765
- });
1766
- });
1767
- })
1768
- )
1769
- );
1743
+ return this.movePlayersToReplacementNode(Array.from(players.values()), "destroy");
1770
1744
  };
1771
1745
  return void handlePlayerOperations().finally(() => {
1772
1746
  this.socket?.close(1e3, "Node-Destroy");
@@ -1805,6 +1779,247 @@ var LavalinkNode = class _LavalinkNode {
1805
1779
  this.resetReconnectionAttempts();
1806
1780
  this.NodeManager.emit("disconnect", this, { code: 1e3, reason: disconnectReason });
1807
1781
  }
1782
+ /**
1783
+ * Pick the best ready node to move players to.
1784
+ * A node must be connected and have a sessionId before a player can be migrated to it.
1785
+ */
1786
+ getReadyMigrationNode(excludeNodeId) {
1787
+ return this.selectMigrationTarget(/* @__PURE__ */ new Set([excludeNodeId]), Number.MAX_SAFE_INTEGER);
1788
+ }
1789
+ getMigrationErrorMessage(error) {
1790
+ if (error instanceof Error) return error.message;
1791
+ if (typeof error === "string") return error;
1792
+ try {
1793
+ return safeStringify(error);
1794
+ } catch {
1795
+ return String(error);
1796
+ }
1797
+ }
1798
+ getMigrationConfig() {
1799
+ const migration = this._LManager.options?.advancedOptions?.playerMigration;
1800
+ return {
1801
+ concurrency: normalizeInt(migration?.concurrency, 5, 1, 32),
1802
+ perTargetConcurrency: normalizeInt(migration?.perTargetConcurrency, 2, 1, 8),
1803
+ backpressureDelayMs: normalizeInt(migration?.backpressureDelayMs, 50, 0, 1e3)
1804
+ };
1805
+ }
1806
+ getMigrationReservationCount(nodeId) {
1807
+ return this.NodeManager.playerMigration.reservations.get(nodeId) || 0;
1808
+ }
1809
+ reserveMigrationTarget(nodeId) {
1810
+ this.NodeManager.playerMigration.reservations.set(nodeId, this.getMigrationReservationCount(nodeId) + 1);
1811
+ }
1812
+ releaseMigrationTarget(nodeId) {
1813
+ const current = this.getMigrationReservationCount(nodeId);
1814
+ if (current <= 1) this.NodeManager.playerMigration.reservations.delete(nodeId);
1815
+ else this.NodeManager.playerMigration.reservations.set(nodeId, current - 1);
1816
+ this.NodeManager.signalPlayerMigrationCapacityChange();
1817
+ }
1818
+ getEffectiveMigrationLoad(node) {
1819
+ return (node.stats?.players || 0) + this.getMigrationReservationCount(node.id);
1820
+ }
1821
+ selectMigrationTarget(excludeNodeIds, perTargetConcurrency) {
1822
+ let selectedNode = null;
1823
+ let selectedEffectiveLoad = Number.POSITIVE_INFINITY;
1824
+ let selectedLiveLoad = Number.POSITIVE_INFINITY;
1825
+ for (const node of this.NodeManager.nodes.values()) {
1826
+ if (!node.connected || !node.sessionId) continue;
1827
+ if (excludeNodeIds.has(node.id)) continue;
1828
+ const reservationCount = this.getMigrationReservationCount(node.id);
1829
+ if (reservationCount >= perTargetConcurrency) continue;
1830
+ const liveLoad = node.stats?.players || 0;
1831
+ const effectiveLoad = this.getEffectiveMigrationLoad(node);
1832
+ if (!selectedNode || effectiveLoad < selectedEffectiveLoad || effectiveLoad === selectedEffectiveLoad && (liveLoad < selectedLiveLoad || liveLoad === selectedLiveLoad && node.id.localeCompare(selectedNode.id) < 0)) {
1833
+ selectedNode = node;
1834
+ selectedEffectiveLoad = effectiveLoad;
1835
+ selectedLiveLoad = liveLoad;
1836
+ }
1837
+ }
1838
+ return selectedNode;
1839
+ }
1840
+ hasAnyHealthyMigrationNode(excludeNodeIds) {
1841
+ for (const node of this.NodeManager.nodes.values()) {
1842
+ if (!node.connected || !node.sessionId) continue;
1843
+ if (excludeNodeIds.has(node.id)) continue;
1844
+ return true;
1845
+ }
1846
+ return false;
1847
+ }
1848
+ isMigrationNodeHealthy(nodeId) {
1849
+ const node = this.NodeManager.nodes.get(nodeId);
1850
+ return !!node?.connected && !!node.sessionId;
1851
+ }
1852
+ async waitForMigrationCapacity(excludeNodeIds, perTargetConcurrency, timeoutMs) {
1853
+ if (this.selectMigrationTarget(excludeNodeIds, perTargetConcurrency)) return;
1854
+ if (timeoutMs <= 0) {
1855
+ await new Promise((resolve) => setImmediate(resolve));
1856
+ return;
1857
+ }
1858
+ await new Promise((resolve) => {
1859
+ let settled = false;
1860
+ let timer;
1861
+ const onSignal = () => {
1862
+ if (settled) return;
1863
+ settled = true;
1864
+ if (timer) clearTimeout(timer);
1865
+ this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
1866
+ resolve();
1867
+ };
1868
+ timer = setTimeout(() => {
1869
+ if (settled) return;
1870
+ settled = true;
1871
+ this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
1872
+ resolve();
1873
+ }, timeoutMs);
1874
+ this.NodeManager.playerMigration.capacityWaiters.add(onSignal);
1875
+ });
1876
+ }
1877
+ /**
1878
+ * Move a batch of players to a ready node with bounded concurrency.
1879
+ * This keeps migration responsive without flooding the replacement node with requests.
1880
+ */
1881
+ async movePlayersToReplacementNode(players, onNoEligibleNode) {
1882
+ const migrationQueue = players.filter((player) => {
1883
+ if (player.getData("internal_nodeMigrating") === true) return false;
1884
+ if (player.getData("internal_destroystatus") === true) return false;
1885
+ player.setData("internal_nodeMigrating", true);
1886
+ return true;
1887
+ });
1888
+ const { concurrency, perTargetConcurrency, backpressureDelayMs } = this.getMigrationConfig();
1889
+ let nextQueueIndex = 0;
1890
+ const workerCount = Math.min(concurrency, migrationQueue.length);
1891
+ const migratePlayer = async (player) => {
1892
+ const excludedNodes = /* @__PURE__ */ new Set();
1893
+ try {
1894
+ for (let attempt = 0; attempt < 2; attempt++) {
1895
+ if (player.getData("internal_destroystatus") === true) return;
1896
+ if (player.node?.options?.id !== this.id) return;
1897
+ let targetNode = null;
1898
+ while (!(targetNode = this.selectMigrationTarget(excludedNodes, perTargetConcurrency))) {
1899
+ if (!this.hasAnyHealthyMigrationNode(excludedNodes)) {
1900
+ this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
1901
+ state: "warn",
1902
+ message: `No eligible ready node was available for player ${player.guildId}`,
1903
+ functionLayer: "Node > movePlayersToReplacementNode()"
1904
+ });
1905
+ if (onNoEligibleNode === "destroy") {
1906
+ await player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((destroyError) => {
1907
+ this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
1908
+ state: "error",
1909
+ message: `Failed to destroy player ${player.guildId}: ${this.getMigrationErrorMessage(destroyError)}`,
1910
+ error: destroyError,
1911
+ functionLayer: "Node > movePlayersToReplacementNode()"
1912
+ });
1913
+ });
1914
+ } else {
1915
+ player.playing = false;
1916
+ }
1917
+ return;
1918
+ }
1919
+ await this.waitForMigrationCapacity(excludedNodes, perTargetConcurrency, backpressureDelayMs);
1920
+ }
1921
+ const targetNodeId = targetNode.id;
1922
+ if (player.getData("internal_destroystatus") === true) return;
1923
+ if (player.node?.options?.id !== this.id) return;
1924
+ this.reserveMigrationTarget(targetNodeId);
1925
+ try {
1926
+ await player.changeNode(targetNodeId);
1927
+ return;
1928
+ } catch (error) {
1929
+ const errorMessage = this.getMigrationErrorMessage(error);
1930
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1931
+ state: "error",
1932
+ message: `Failed to move player ${player.guildId}: ${errorMessage}`,
1933
+ error,
1934
+ functionLayer: "Node > movePlayersToReplacementNode()"
1935
+ });
1936
+ const targetStillHealthy = this.isMigrationNodeHealthy(targetNodeId);
1937
+ excludedNodes.add(targetNodeId);
1938
+ if (attempt === 0 && !targetStillHealthy) {
1939
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1940
+ state: "warn",
1941
+ message: `Migration target ${targetNodeId} became unavailable while moving player ${player.guildId}; retrying on another ready node`,
1942
+ error,
1943
+ functionLayer: "Node > movePlayersToReplacementNode()"
1944
+ });
1945
+ if (player.node?.options?.id === targetNodeId) player.node = this;
1946
+ continue;
1947
+ }
1948
+ if (attempt === 0 && this.hasAnyHealthyMigrationNode(excludedNodes)) {
1949
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1950
+ state: "warn",
1951
+ message: `Retrying migration for player ${player.guildId} on a different ready node`,
1952
+ error,
1953
+ functionLayer: "Node > movePlayersToReplacementNode()"
1954
+ });
1955
+ if (player.node?.options?.id === targetNodeId) player.node = this;
1956
+ continue;
1957
+ }
1958
+ if (onNoEligibleNode === "destroy") {
1959
+ await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
1960
+ this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
1961
+ state: "error",
1962
+ message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
1963
+ error: destroyError,
1964
+ functionLayer: "Node > movePlayersToReplacementNode()"
1965
+ });
1966
+ });
1967
+ } else {
1968
+ player.playing = false;
1969
+ }
1970
+ return;
1971
+ } finally {
1972
+ this.releaseMigrationTarget(targetNodeId);
1973
+ }
1974
+ }
1975
+ } catch (error) {
1976
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
1977
+ state: "error",
1978
+ message: `Unexpected error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
1979
+ error,
1980
+ functionLayer: "Node > movePlayersToReplacementNode()"
1981
+ });
1982
+ if (onNoEligibleNode === "destroy") {
1983
+ await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
1984
+ this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
1985
+ state: "error",
1986
+ message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
1987
+ error: destroyError,
1988
+ functionLayer: "Node > movePlayersToReplacementNode()"
1989
+ });
1990
+ });
1991
+ } else {
1992
+ player.playing = false;
1993
+ }
1994
+ }
1995
+ };
1996
+ const workers = Array.from({ length: workerCount }, async () => {
1997
+ while (true) {
1998
+ const queueIndex = nextQueueIndex++;
1999
+ if (queueIndex >= migrationQueue.length) return;
2000
+ const player = migrationQueue[queueIndex];
2001
+ if (!player) continue;
2002
+ try {
2003
+ if (player.getData("internal_nodeMigrating") !== true) {
2004
+ player.setData("internal_nodeMigrating", true);
2005
+ }
2006
+ if (player.getData("internal_destroystatus") === true) continue;
2007
+ if (player.node?.options?.id !== this.id) continue;
2008
+ await migratePlayer(player);
2009
+ } catch (error) {
2010
+ this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
2011
+ state: "error",
2012
+ message: `Unexpected worker error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
2013
+ error,
2014
+ functionLayer: "Node > movePlayersToReplacementNode()"
2015
+ });
2016
+ } finally {
2017
+ player.setData("internal_nodeMigrating", void 0);
2018
+ }
2019
+ }
2020
+ });
2021
+ await Promise.all(workers);
2022
+ }
1808
2023
  /**
1809
2024
  * Gets all Players of a Node
1810
2025
  * @returns array of players inside of lavalink
@@ -2517,9 +2732,10 @@ var LavalinkNode = class _LavalinkNode {
2517
2732
  }
2518
2733
  this.info.isNodelink = !!this.info.isNodelink;
2519
2734
  this.NodeManager.emit("connect", this);
2735
+ this.NodeManager.signalPlayerMigrationCapacityChange();
2520
2736
  }
2521
2737
  /** @private util function for handling closing events from websocket */
2522
- close(code, reason) {
2738
+ async close(code, reason) {
2523
2739
  this.resetAckTimeouts(true, true);
2524
2740
  try {
2525
2741
  if (this.socket) {
@@ -2538,19 +2754,22 @@ var LavalinkNode = class _LavalinkNode {
2538
2754
  this.isAlive = false;
2539
2755
  if (code === 1006 && !reason) reason = "Socket got terminated due to no ping connection";
2540
2756
  if (code === 1e3 && reason === "Node-Disconnect") return;
2757
+ const players = Array.from(
2758
+ this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).values()
2759
+ );
2541
2760
  this.NodeManager.emit("disconnect", this, { code, reason });
2542
2761
  if (code !== 1e3 || reason !== "Node-Destroy") {
2543
2762
  if (this.NodeManager.nodes.has(this.id)) {
2544
2763
  this.reconnect();
2545
2764
  }
2546
2765
  }
2547
- this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).forEach((p) => {
2548
- if (!this._LManager.options.autoMove) return p.playing = false;
2549
- if (this._LManager.options.autoMove) {
2550
- if (this.NodeManager.nodes.filter((n) => n.connected).size === 0) return p.playing = false;
2551
- p.moveNode();
2552
- }
2553
- });
2766
+ if (this._LManager.options.autoMove && players.length) {
2767
+ await this.movePlayersToReplacementNode(players, "freeze");
2768
+ } else if (!this._LManager.options.autoMove) {
2769
+ players.forEach((p) => p.playing = false);
2770
+ } else if (this.NodeManager.nodes.filter((n) => n.connected && !!n.sessionId).size === 0) {
2771
+ players.forEach((p) => p.playing = false);
2772
+ }
2554
2773
  }
2555
2774
  /** @private util function for handling error events from websocket */
2556
2775
  error(error) {
@@ -3628,6 +3847,16 @@ var NodeManager = class extends EventEmitter {
3628
3847
  * A map of all nodes in the nodeManager
3629
3848
  */
3630
3849
  nodes = new MiniMap();
3850
+ /** @internal Coordinates player migrations between nodes owned by this manager. */
3851
+ playerMigration = {
3852
+ reservations: /* @__PURE__ */ new Map(),
3853
+ capacityWaiters: /* @__PURE__ */ new Set()
3854
+ };
3855
+ /** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
3856
+ signalPlayerMigrationCapacityChange() {
3857
+ for (const resolve of this.playerMigration.capacityWaiters) resolve();
3858
+ this.playerMigration.capacityWaiters.clear();
3859
+ }
3631
3860
  /**
3632
3861
  * @param LavalinkManager The LavalinkManager that created this NodeManager
3633
3862
  */
@@ -6007,8 +6236,10 @@ var Player = class {
6007
6236
  * @returns
6008
6237
  */
6009
6238
  async connect() {
6010
- if (!this.options.voiceChannelId)
6011
- throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6239
+ if (!this.options.voiceChannelId) {
6240
+ if (this.voiceChannelId) this.options.voiceChannelId = this.voiceChannelId;
6241
+ else throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6242
+ }
6012
6243
  await this.LavalinkManager.options.sendToShard(this.guildId, {
6013
6244
  op: 4,
6014
6245
  d: {
@@ -6045,8 +6276,10 @@ var Player = class {
6045
6276
  * @returns
6046
6277
  */
6047
6278
  async disconnect(force = false) {
6048
- if (!force && !this.options.voiceChannelId)
6049
- throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6279
+ if (!force && !this.options.voiceChannelId) {
6280
+ if (this.voiceChannelId) this.options.voiceChannelId = this.voiceChannelId;
6281
+ else throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
6282
+ }
6050
6283
  await this.LavalinkManager.options.sendToShard(this.guildId, {
6051
6284
  op: 4,
6052
6285
  d: {
@@ -6275,8 +6508,8 @@ var Player = class {
6275
6508
  try {
6276
6509
  if (!node)
6277
6510
  node = Array.from(this.LavalinkManager.nodeManager.leastUsedNodes("playingPlayers")).find(
6278
- (n) => n.connected && n.options.id !== this.node.options.id
6279
- ).id;
6511
+ (n) => n.connected && !!n.sessionId && n.options.id !== this.node.options.id
6512
+ )?.id;
6280
6513
  if (!node || !this.LavalinkManager.nodeManager.nodes.get(node))
6281
6514
  throw new RangeError("No nodes are available.");
6282
6515
  if (this.node.options.id === node) return this;
@@ -6431,6 +6664,11 @@ var LavalinkManager = class _LavalinkManager extends EventEmitter2 {
6431
6664
  advancedOptions: {
6432
6665
  enableDebugEvents: options?.advancedOptions?.enableDebugEvents ?? false,
6433
6666
  maxFilterFixDuration: options?.advancedOptions?.maxFilterFixDuration ?? 6e5,
6667
+ playerMigration: {
6668
+ concurrency: options?.advancedOptions?.playerMigration?.concurrency ?? 5,
6669
+ perTargetConcurrency: options?.advancedOptions?.playerMigration?.perTargetConcurrency ?? 2,
6670
+ backpressureDelayMs: options?.advancedOptions?.playerMigration?.backpressureDelayMs ?? 50
6671
+ },
6434
6672
  debugOptions: {
6435
6673
  logCustomSearches: options?.advancedOptions?.debugOptions?.logCustomSearches ?? false,
6436
6674
  noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,