From eb6474b33a21d927d506919157d196f2bccf0116 Mon Sep 17 00:00:00 2001 From: PandaIN95 <95042588+PandaIN95@users.noreply.github.com> Date: Tue, 14 Jan 2025 03:12:13 +0530 Subject: [PATCH 1/2] Fix: node try to reconnect when its not on nodeManager.nodes list & remove timers on nodeDelete Added: before deleting any node now you Choose whether to move players to different node. --- src/structures/Constants.ts | 1 + src/structures/Node.ts | 103 ++++++++++++++++++++++++++++------ src/structures/NodeManager.ts | 6 +- 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/structures/Constants.ts b/src/structures/Constants.ts index 284c745..87cb927 100644 --- a/src/structures/Constants.ts +++ b/src/structures/Constants.ts @@ -61,6 +61,7 @@ export enum DestroyReasons { NodeReconnectFail = "NodeReconnectFail", Disconnected = "Disconnected", PlayerReconnectFail = "PlayerReconnectFail", + PlayerChangeNodeFail = "PlayerChangeNodeFail", ChannelDeleted = "ChannelDeleted", DisconnectAllNodes = "DisconnectAllNodes", ReconnectAllNodes = "ReconnectAllNodes", diff --git a/src/structures/Node.ts b/src/structures/Node.ts index ae04a7c..caa2076 100644 --- a/src/structures/Node.ts +++ b/src/structures/Node.ts @@ -475,6 +475,7 @@ export class LavalinkNode { * Destroys the Node-Connection (Websocket) and all player's of the node * @param destroyReason Destroy Reason to use when destroying the players * @param deleteNode wether to delete the nodte from the nodes list too, if false it will emit a disconnect. @default true + * @param movePlayers whether to movePlayers to different eligible connected node. If false players won't be moved @default false * @returns void * * @example @@ -482,26 +483,86 @@ export class LavalinkNode { * player.node.destroy("custom Player Destroy Reason", true); * ``` */ - public destroy(destroyReason?: DestroyReasonsType, deleteNode = true): void { + public destroy(destroyReason?: DestroyReasonsType, deleteNode: boolean = true, movePlayers: boolean = false): void { if (!this.connected) return; const players = this.NodeManager.LavalinkManager.players.filter(p => p.node.id === this.id); - if (players) players.forEach(p => { - p.destroy(destroyReason || DestroyReasons.NodeDestroy); - }); - - this.socket.close(1000, "Node-Destroy"); - this.socket.removeAllListeners(); - this.socket = null; - - this.reconnectAttempts = 1; - clearTimeout(this.reconnectTimeout); - - if (deleteNode) { - this.NodeManager.emit("destroy", this, destroyReason); - this.NodeManager.nodes.delete(this.id); - } else { - this.NodeManager.emit("disconnect", this, { code: 1000, reason: destroyReason }); + if (players.size) { + const enableDebugEvents = this.NodeManager.LavalinkManager.options?.advancedOptions?.enableDebugEvents; + const handlePlayerOperations = () => { + if (movePlayers) { + const nodeToMove = Array.from(this.NodeManager.nodes.values()) + .find(n => n.connected && n.options.id !== this.id); + + if (nodeToMove) { + return Promise.allSettled(Array.from(players.values()).map(player => + player.changeNode(nodeToMove.options.id) + .catch(error => { + if (enableDebugEvents) { + console.error(`Node > destroy() Failed to move player ${player.guildId}: ${error.message}`); + } + return player.destroy(error.message ?? DestroyReasons.PlayerChangeNodeFail) + .catch(destroyError => { + if (enableDebugEvents) { + console.error(`Node > destroy() Failed to destroy player ${player.guildId} after move failure: ${destroyError.message}`); + } + }); + }) + )); + } else { + return Promise.allSettled(Array.from(players.values()).map(player => + player.destroy("No eligible node found to move player.") + .catch(error => { + if (enableDebugEvents) { + console.error(`Node > destroy() Failed to destroy player ${player.guildId}: ${error.message}`); + } + }) + )); + } + } else { + return Promise.allSettled(Array.from(players.values()).map(player => + player.destroy(destroyReason || DestroyReasons.NodeDestroy) + .catch(error => { + if (enableDebugEvents) { + console.error(`Node > destroy() Failed to destroy player ${player.guildId}: ${error.message}`); + } + }) + )); + } + }; + + // Handle all player operations first, then clean up the socket + handlePlayerOperations().finally(() => { + this.socket.close(1000, "Node-Destroy"); + this.socket.removeAllListeners(); + this.socket = null; + this.reconnectAttempts = 1; + clearTimeout(this.reconnectTimeout); + + if (deleteNode) { + this.NodeManager.emit("destroy", this, destroyReason); + this.NodeManager.nodes.delete(this.id); + clearInterval(this.heartBeatInterval); + clearTimeout(this.pingTimeout); + } else { + this.NodeManager.emit("disconnect", this, { code: 1000, reason: destroyReason }); + } + }); + } else { // If no players, proceed with socket cleanup immediately + this.socket.close(1000, "Node-Destroy"); + this.socket.removeAllListeners(); + this.socket = null; + this.reconnectAttempts = 1; + clearTimeout(this.reconnectTimeout); + + if (deleteNode) { + this.NodeManager.emit("destroy", this, destroyReason); + this.NodeManager.nodes.delete(this.id); + clearInterval(this.heartBeatInterval); + clearTimeout(this.pingTimeout); + } else { + this.NodeManager.emit("disconnect", this, { code: 1000, reason: destroyReason }); + } } return; } @@ -1059,7 +1120,13 @@ export class LavalinkNode { this.NodeManager.emit("disconnect", this, { code, reason }); - if (code !== 1000 || reason !== "Node-Destroy") this.reconnect(); + if (code !== 1000 || reason !== "Node-Destroy") { + if (code !== 1000 || reason !== "Node-Destroy") { + if (this.NodeManager.nodes.has(this.id)) { // try to reconnect only when the node is still in the nodeManager.nodes list + this.reconnect(); + } + } + } } /** @private util function for handling error events from websocket */ diff --git a/src/structures/NodeManager.ts b/src/structures/NodeManager.ts index 4f03a4e..b095ee4 100644 --- a/src/structures/NodeManager.ts +++ b/src/structures/NodeManager.ts @@ -187,12 +187,14 @@ export class NodeManager extends EventEmitter { /** * Delete a node from the nodeManager and destroy it * @param node The node to delete + * @param movePlayers whether to movePlayers to different connected node before deletion. @default false * @returns */ - deleteNode(node: LavalinkNodeIdentifier | LavalinkNode): void { + deleteNode(node: LavalinkNodeIdentifier | LavalinkNode, movePlayers: boolean = false): void { const decodeNode = typeof node === "string" ? this.nodes.get(node) : node || this.leastUsedNodes()[0]; if (!decodeNode) throw new Error("Node was not found"); - decodeNode.destroy(DestroyReasons.NodeDeleted); + if (movePlayers) decodeNode.destroy(DestroyReasons.NodeDeleted, true, true); + else decodeNode.destroy(DestroyReasons.NodeDeleted); this.nodes.delete(decodeNode.id); return; } From ebced592f05a9a6f2b378c48dd5f95ae6dd3353f Mon Sep 17 00:00:00 2001 From: PandaIN95 <95042588+PandaIN95@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:12:30 +0530 Subject: [PATCH 2/2] added jsdocs examples --- src/structures/Constants.ts | 1 + src/structures/Node.ts | 15 +++++++++------ src/structures/NodeManager.ts | 10 ++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/structures/Constants.ts b/src/structures/Constants.ts index 87cb927..92f2779 100644 --- a/src/structures/Constants.ts +++ b/src/structures/Constants.ts @@ -62,6 +62,7 @@ export enum DestroyReasons { Disconnected = "Disconnected", PlayerReconnectFail = "PlayerReconnectFail", PlayerChangeNodeFail = "PlayerChangeNodeFail", + PlayerChangeNodeFailNoEligibleNode = "PlayerChangeNodeFailNoEligibleNode", ChannelDeleted = "ChannelDeleted", DisconnectAllNodes = "DisconnectAllNodes", ReconnectAllNodes = "ReconnectAllNodes", diff --git a/src/structures/Node.ts b/src/structures/Node.ts index caa2076..713fe4c 100644 --- a/src/structures/Node.ts +++ b/src/structures/Node.ts @@ -479,9 +479,14 @@ export class LavalinkNode { * @returns void * * @example + * Destroys node and its players * ```ts * player.node.destroy("custom Player Destroy Reason", true); * ``` + * destroys only the node and moves its players to different connected node. + * ```ts + * player.node.destroy("custom Player Destroy Reason", true, true); + * ``` */ public destroy(destroyReason?: DestroyReasonsType, deleteNode: boolean = true, movePlayers: boolean = false): void { if (!this.connected) return; @@ -491,7 +496,7 @@ export class LavalinkNode { const enableDebugEvents = this.NodeManager.LavalinkManager.options?.advancedOptions?.enableDebugEvents; const handlePlayerOperations = () => { if (movePlayers) { - const nodeToMove = Array.from(this.NodeManager.nodes.values()) + const nodeToMove = Array.from(this.NodeManager.leastUsedNodes("playingPlayers")) .find(n => n.connected && n.options.id !== this.id); if (nodeToMove) { @@ -511,7 +516,7 @@ export class LavalinkNode { )); } else { return Promise.allSettled(Array.from(players.values()).map(player => - player.destroy("No eligible node found to move player.") + player.destroy(DestroyReasons.PlayerChangeNodeFailNoEligibleNode) .catch(error => { if (enableDebugEvents) { console.error(`Node > destroy() Failed to destroy player ${player.guildId}: ${error.message}`); @@ -1121,10 +1126,8 @@ export class LavalinkNode { this.NodeManager.emit("disconnect", this, { code, reason }); if (code !== 1000 || reason !== "Node-Destroy") { - if (code !== 1000 || reason !== "Node-Destroy") { - if (this.NodeManager.nodes.has(this.id)) { // try to reconnect only when the node is still in the nodeManager.nodes list - this.reconnect(); - } + if (this.NodeManager.nodes.has(this.id)) { // try to reconnect only when the node is still in the nodeManager.nodes list + this.reconnect(); } } } diff --git a/src/structures/NodeManager.ts b/src/structures/NodeManager.ts index b095ee4..4dd38e8 100644 --- a/src/structures/NodeManager.ts +++ b/src/structures/NodeManager.ts @@ -189,6 +189,16 @@ export class NodeManager extends EventEmitter { * @param node The node to delete * @param movePlayers whether to movePlayers to different connected node before deletion. @default false * @returns + * + * @example + * Deletes the node + * ```ts + * client.lavalink.nodeManager.deleteNode("nodeId to delete"); + * ``` + * Moves players to a different node before deleting + * ```ts + * client.lavalink.nodeManager.deleteNode("nodeId to delete", true); + * ``` */ deleteNode(node: LavalinkNodeIdentifier | LavalinkNode, movePlayers: boolean = false): void { const decodeNode = typeof node === "string" ? this.nodes.get(node) : node || this.leastUsedNodes()[0];