Skip to content

Commit 6902184

Browse files
committed
Introduce a reconnection queue with reconnection state times + minor adjustments for that
1 parent 101041a commit 6902184

File tree

3 files changed

+76
-34
lines changed

3 files changed

+76
-34
lines changed

docs/src/content/docs/extra/version-log.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ Logs about the changed features.
99

1010
---
1111

12+
## **Version 2.7.2**
13+
14+
Make the node auto-reconnection more solid by a reconnection queue, so we can trigger reconnection tries much easier.
15+
You can check the current state of reconnection by accessing **`node.reconnectionState`** (`"IDLE"` / `"RECONNECTING"` / `"PENDING"` / `"DESTROYING"`)
16+
Alterantivly you can use the getter property to check wether it's pending to reconnect or reconnecting: **`node.isNodeReconnecting`** (easier to use)
17+
18+
Huge thanks to **[@ductridev](https://github.com/ductridev)** who introduced this change in: [Pull #154](https://github.com/Tomato6966/lavalink-client/pull/154)
19+
Enhances the resilience of the LavalinkNode by introducing automatic reconnection attempts on WebSocket errors instead of only emitting error events. The implementation adds a state machine using a new ReconnectionState enum to prevent duplicate reconnection attempts.
20+
21+
Key Changes:
22+
23+
- Added ReconnectionState enum to track reconnection status (IDLE, RECONNECTING, PENDING)
24+
- Refactored reconnect() method to use state guards preventing concurrent reconnection attempts
25+
- Modified error() handler to trigger reconnection directly instead of emitting error events
26+
- there is now a new getter: `node.reconnectionAttemptCount` (which allows you to get the count of the current reconnection attempts, considering the new option `node.options.retryTimespan`)
27+
- Before the `node.reconnectAttempts` was a number-counter. Now it's just an array of Date-Times, to be able to allow Retrys per a timespan (e.g. you can have 20 retries every hour.) - setup able through `node.options.retryTimespan`, on default it's -1. so it behaves as before.
28+
- FIX: Now instead of counting from "1 retry up" it counts from 0 up, means if you set `node.retryAmount` to "1" it actually tries 1 time to retry, and not 0. (issue before)
29+
30+
1231
## **Version 2.7.1**
1332

1433
Fixes:

src/structures/Node.ts

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isAbsolute } from "path";
22
import WebSocket from "ws";
33

44
import { DebugEvents, DestroyReasons, validSponsorBlocks } from "./Constants";
5+
import { ReconnectionState } from "./Types/Node";
56
import { NodeSymbol, queueTrackEnd, safeStringify } from "./Utils";
67

78
import type {
@@ -20,7 +21,6 @@ import type { NodeManager } from "./NodeManager";
2021
import type {
2122
BaseNodeStats, LavalinkInfo, LavalinkNodeOptions, LyricsResult, ModifyRequest, NodeStats, SponsorBlockSegment
2223
} from "./Types/Node";
23-
import { ReconnectionState } from "./Types/Node";
2424
/**
2525
* Lavalink Node creator class
2626
*/
@@ -65,14 +65,14 @@ export class LavalinkNode {
6565
public resuming: { enabled: boolean, timeout: number | null } = { enabled: true, timeout: null };
6666
/** Actual Lavalink Information of the Node */
6767
public info: LavalinkInfo | null = null;
68+
/** current state of the Reconnections */
69+
public reconnectionState: ReconnectionState = ReconnectionState.IDLE;
6870
/** The Node Manager of this Node */
6971
private NodeManager: NodeManager | null = null;
7072
/** The Reconnection Timeout */
7173
private reconnectTimeout?: NodeJS.Timeout = undefined;
72-
/** The Reconnection Attempt counter */
73-
private reconnectAttempts = 1;
74-
/** Reconnection current state */
75-
private reconnectionState: ReconnectionState = ReconnectionState.IDLE;
74+
/** The Reconnection Attempt counter (array of datetimes when it tried it.) */
75+
private reconnectAttempts: number[] = [];
7676
/** The Socket of the Lavalink */
7777
private socket: WebSocket | null = null;
7878
/** Version of what the Lavalink Server should be */
@@ -98,6 +98,7 @@ export class LavalinkNode {
9898
secure: false,
9999
retryAmount: 5,
100100
retryDelay: 10e3,
101+
retryTimespan: -1,
101102
requestSignalTimeoutMS: 10000,
102103
heartBeatInterval: 30_000,
103104
closeOnError: true,
@@ -1013,60 +1014,81 @@ export class LavalinkNode {
10131014
return `http${this.options.secure ? "s" : ""}://${this.options.host}:${this.options.port}`;
10141015
}
10151016

1017+
1018+
/**
1019+
* If already trying to reconnect or pending, return
1020+
*/
1021+
public get isNodeReconnecting(): boolean {
1022+
return this.reconnectionState !== ReconnectionState.IDLE;
1023+
}
1024+
10161025
/**
10171026
* Reconnect to the lavalink node
1018-
* @param instaReconnect @default false wether to instantly try to reconnect
1027+
* @param force @default false Wether to instantly try to reconnect (force it)
10191028
* @returns void
10201029
*
10211030
* @example
10221031
* ```ts
10231032
* await player.node.reconnect();
10241033
* ```
10251034
*/
1026-
private reconnect(instaReconnect = false): void {
1035+
private reconnect(force = false): void {
10271036
// If already trying to reconnect or pending, return
1028-
if (this.reconnectionState !== ReconnectionState.IDLE) {
1037+
if (this.isNodeReconnecting) {
10291038
return;
10301039
}
10311040

10321041
// Set reconnection state to pending
10331042
this.reconnectionState = ReconnectionState.PENDING;
10341043
this.NodeManager.emit("reconnectinprogress", this);
10351044

1036-
const executeReconnect = () => {
1037-
if (this.reconnectAttempts >= this.options.retryAmount) {
1038-
const error = new Error(`Unable to connect after ${this.options.retryAmount} attempts.`)
1039-
1040-
this.NodeManager.emit("error", this, error);
1041-
1042-
this.reconnectionState = ReconnectionState.DESTROYING;
1043-
this.destroy(DestroyReasons.NodeReconnectFail);
1044-
1045-
this.reconnectionState = ReconnectionState.IDLE;
1046-
return;
1047-
}
1048-
1049-
this.NodeManager.emit("reconnecting", this);
1050-
this.reconnectionState = ReconnectionState.RECONNECTING;
1051-
this.connect();
1052-
this.reconnectAttempts++;
1053-
};
1054-
if (instaReconnect) {
1055-
executeReconnect();
1045+
if (force) {
1046+
this.executeReconnect();
10561047
return;
10571048
}
10581049
this.reconnectTimeout = setTimeout(() => {
10591050
this.reconnectTimeout = null;
1060-
executeReconnect();
1051+
this.executeReconnect();
10611052
}, this.options.retryDelay || 1000);
10621053
}
10631054

1055+
public get reconnectionAttemptCount(): number {
1056+
const maxAllowedTimestan = this.options.retryTimespan || -1;
1057+
if(maxAllowedTimestan <= 0) return this.reconnectAttempts.length;
1058+
return this.reconnectAttempts.filter(timestamp => Date.now() - timestamp <= maxAllowedTimestan).length;
1059+
}
1060+
1061+
/**
1062+
* Private Utility function to execute the reconnection
1063+
*/
1064+
private executeReconnect() {
1065+
if (this.reconnectionAttemptCount >= this.options.retryAmount) {
1066+
const error = new Error(`Unable to connect after ${this.options.retryAmount} attempts.`)
1067+
1068+
this.reconnectionState = ReconnectionState.DESTROYING;
1069+
1070+
this.NodeManager.emit("error", this, error);
1071+
this.destroy(DestroyReasons.NodeReconnectFail);
1072+
// the reconnection State should be set on idle inside of the destroy function
1073+
return;
1074+
}
1075+
1076+
// state's should be changed before emitting an event
1077+
this.reconnectAttempts.push(Date.now());
1078+
this.reconnectionState = ReconnectionState.RECONNECTING;
1079+
1080+
this.NodeManager.emit("reconnecting", this);
1081+
this.connect();
1082+
};
1083+
1084+
10641085
/**
10651086
* Private function to reset the reconnection attempts
10661087
* @returns
10671088
*/
10681089
private resetReconnectionAttempts(): void {
1069-
this.reconnectAttempts = 1;
1090+
this.reconnectionState = ReconnectionState.IDLE;
1091+
this.reconnectAttempts = [];
10701092
clearTimeout(this.reconnectTimeout);
10711093
this.reconnectTimeout = null;
10721094
return;
@@ -1095,7 +1117,6 @@ export class LavalinkNode {
10951117
this.isAlive = true;
10961118

10971119
// Reset reconnection state on successful connection
1098-
this.reconnectionState = ReconnectionState.IDLE;
10991120
this.resetReconnectionAttempts();
11001121

11011122
// trigger heartbeat-ping timeout - this is to check wether the client lost connection without knowing it

src/structures/Types/Node.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ export interface LavalinkNodeOptions {
2626
id?: string;
2727
/** Voice Regions of this Node */
2828
regions?: string[];
29-
/** The retryAmount for the node. */
29+
/** The max amount of retries for this node. */
3030
retryAmount?: number;
31-
/** The retryDelay for the node. */
31+
/** The delay of how often to retry a reconnection. */
3232
retryDelay?: number;
33+
/** How long a retry is a valid retry, it should be at least retryAmount*retryDelay. if <= 0 (default) then this won't be accounted. */
34+
retryTimespan?: number;
3335
/** signal for cancelling requests - default: AbortSignal.timeout(options.requestSignalTimeoutMS || 10000) - put <= 0 to disable */
3436
requestSignalTimeoutMS?: number;
3537
/** Close on error */
@@ -254,4 +256,4 @@ export enum ReconnectionState {
254256
RECONNECTING = "RECONNECTING",
255257
PENDING = "PENDING",
256258
DESTROYING = "DESTROYING"
257-
}
259+
}

0 commit comments

Comments
 (0)