aqualink 1.7.0-beta5 → 1.7.0-beta6

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.
@@ -6,28 +6,27 @@ const { version: pkgVersion } = require("../../package.json");
6
6
  const URL_REGEX = /^https?:\/\//;
7
7
 
8
8
  class Aqua extends EventEmitter {
9
- /**
10
- * @param {Object} client - The client instance.
11
- * @param {Array<Object>} nodes - An array of node configurations.
12
- * @param {Object} options - Configuration options for Aqua.
13
- * @param {Function} options.send - Function to send data.
14
- * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform. Options include:
15
- * - "youtube music": "ytmsearch"
16
- * - "youtube": "ytsearch"
17
- * - "spotify": "spsearch"
18
- * - "jiosaavn": "jssearch"
19
- * - "soundcloud": "scsearch"
20
- * - "deezer": "dzsearch"
21
- * - "tidal": "tdsearch"
22
- * - "applemusic": "amsearch"
23
- * - "bandcamp": "bcsearch"
24
- * @param {string} [options.restVersion="v4"] - Version of the REST API.
25
- * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
26
- * @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
27
- * @param {boolean} [options.autoResume=false] - Automatically resume tracks on reconnect.
28
- * @param {boolean} [options.infiniteReconnects=false] - Reconnect infinitely (default: false).
29
- */
30
- constructor(client, nodes, options) {
9
+ /**
10
+ * @param {Object} client - The client instance.
11
+ * @param {Array<Object>} nodes - An array of node configurations.
12
+ * @param {Object} options - Configuration options for Aqua.
13
+ * @param {Function} options.send - Function to send data.
14
+ * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform. Options include:
15
+ * - "youtube music": "ytmsearch"
16
+ * - "youtube": "ytsearch"
17
+ * - "spotify": "spsearch"
18
+ * - "jiosaavn": "jssearch"
19
+ * - "soundcloud": "scsearch"
20
+ * - "deezer": "dzsearch"
21
+ * - "tidal": "tdsearch"
22
+ * - "applemusic": "amsearch"
23
+ * - "bandcamp": "bcsearch"
24
+ * @param {string} [options.restVersion="v4"] - Version of the REST API.
25
+ * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
26
+ * @param {boolean} [options.autoResume=false] - Automatically resume tracks on reconnect.
27
+ * @param {boolean} [options.infiniteReconnects=false] - Reconnect infinitely (default: false).
28
+ */
29
+ constructor(client, nodes, options = {}) {
31
30
  super();
32
31
  this.validateInputs(client, nodes, options);
33
32
  this.client = client;
@@ -36,22 +35,33 @@ class Aqua extends EventEmitter {
36
35
  this.players = new Map();
37
36
  this.clientId = null;
38
37
  this.initiated = false;
39
- this.shouldDeleteMessage = options.shouldDeleteMessage || false;
40
- this.defaultSearchPlatform = options.defaultSearchPlatform || "ytsearch";
41
- this.restVersion = options.restVersion || "v4";
42
- this.plugins = options.plugins || [];
43
- this.version = pkgVersion;
44
38
  this.options = options;
45
- this.send = options.send;
46
- this.autoResume = options.autoResume || false;
47
- this.infiniteReconnects = options.infiniteReconnects || false;
39
+
40
+ this.shouldDeleteMessage = this.getOption(options, 'shouldDeleteMessage', false);
41
+ this.defaultSearchPlatform = this.getOption(options, 'defaultSearchPlatform', 'ytsearch');
42
+ this.leaveOnEnd = this.getOption(options, 'leaveOnEnd', true);
43
+ this.restVersion = this.getOption(options, 'restVersion', 'v4');
44
+ this.plugins = this.getOption(options, 'plugins', []);
45
+ this.version = pkgVersion;
46
+ this.send = options.send || this.defaultSendFunction;
47
+ this.autoResume = this.getOption(options, 'autoResume', false);
48
+ this.infiniteReconnects = this.getOption(options, 'infiniteReconnects', false);
49
+
48
50
  this.setMaxListeners(0);
49
51
  }
50
52
 
53
+ getOption(options, key, defaultValue) {
54
+ return options.hasOwnProperty(key) ? options[key] : defaultValue;
55
+ }
56
+
57
+ defaultSendFunction(payload) {
58
+ const guild = this.client.guilds.cache.get(payload.d.guild_id);
59
+ if (guild) guild.shard.send(payload);
60
+ }
61
+
51
62
  validateInputs(client, nodes, options) {
52
63
  if (!client) throw new Error("Client is required to initialize Aqua");
53
64
  if (!Array.isArray(nodes) || !nodes.length) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
54
- if (typeof options?.send !== "function") throw new Error("Send function is required to initialize Aqua");
55
65
  }
56
66
 
57
67
  get leastUsedNodes() {
@@ -3,7 +3,7 @@ const { Rest } = require("./Rest");
3
3
 
4
4
  class Node {
5
5
  #ws = null;
6
- #statsCache = {};
6
+ #statsCache = new Map();
7
7
  #lastStatsRequest = 0;
8
8
  #reconnectAttempted = 0;
9
9
  #reconnectTimeoutId = null;
@@ -37,7 +37,7 @@ class Node {
37
37
  this.infiniteReconnects = options?.infiniteReconnects ?? false;
38
38
  this.connected = false;
39
39
  this.info = null;
40
- this.stats = Object.freeze(this.#createStats());
40
+ this.stats = this.#createStats();
41
41
  }
42
42
 
43
43
  #createStats() {
@@ -47,24 +47,25 @@ class Node {
47
47
  uptime: 0,
48
48
  memory: { free: 0, used: 0, allocated: 0, reservable: 0, freePercentage: 0, usedPercentage: 0 },
49
49
  cpu: { cores: 0, systemLoad: 0, lavalinkLoad: 0, lavalinkLoadPercentage: 0 },
50
- frameStats: { sent: null, nulled: null, deficit: null },
50
+ frameStats: { sent: 0, nulled: 0, deficit: 0 },
51
51
  ping: 0
52
52
  };
53
53
  }
54
54
 
55
55
  async connect() {
56
56
  this.#cleanup();
57
- try {
58
- this.#ws = new WebSocket(this.wsUrl.href, {
59
- headers: this.#constructHeaders(),
60
- perMessageDeflate: false
61
- });
62
- this.#setupWebSocketListeners();
63
- this.aqua.emit('debug', this.name, 'Connecting...');
64
- } catch (err) {
65
- this.aqua.emit('debug', this.name, `Connection failed: ${err.message}`);
66
- this.#reconnect();
67
- }
57
+ this.#ws = new WebSocket(this.wsUrl.href, {
58
+ headers: this.#constructHeaders(),
59
+ perMessageDeflate: false,
60
+ handshakeTimeout: 5000
61
+ });
62
+
63
+ this.#ws.once('open', () => {
64
+ this.#onOpen();
65
+ });
66
+
67
+ this.#setupWebSocketListeners();
68
+ this.aqua.emit('debug', this.name, 'Connecting...');
68
69
  }
69
70
 
70
71
  #cleanup() {
@@ -73,21 +74,28 @@ class Node {
73
74
  this.#ws.terminate();
74
75
  this.#ws = null;
75
76
  }
77
+ this.info = null;
78
+ this.#statsCache.clear();
76
79
  }
77
80
 
78
81
  #constructHeaders() {
79
- const headers = {
82
+ return {
80
83
  Authorization: this.password,
81
84
  "User-Id": this.aqua.clientId,
82
85
  "Client-Name": `Aqua/${this.aqua.version}`,
86
+ ...(this.sessionId && { "Session-Id": this.sessionId }),
87
+ ...(this.resumeKey && { "Resume-Key": this.resumeKey })
83
88
  };
84
- if (this.sessionId) headers["Session-Id"] = this.sessionId;
85
- if (this.resumeKey) headers["Resume-Key"] = this.resumeKey;
86
- return Object.freeze(headers);
87
89
  }
88
90
 
89
91
  #setupWebSocketListeners() {
90
92
  if (!this.#ws) return;
93
+
94
+ this.#ws.removeAllListeners('open');
95
+ this.#ws.removeAllListeners('error');
96
+ this.#ws.removeAllListeners('message');
97
+ this.#ws.removeAllListeners('close');
98
+
91
99
  this.#ws.once("open", this.#onOpen.bind(this));
92
100
  this.#ws.once("error", this.#onError.bind(this));
93
101
  this.#ws.on("message", this.#onMessage.bind(this));
@@ -107,18 +115,20 @@ class Node {
107
115
  }
108
116
  }
109
117
  }
110
-
111
118
  async getStats() {
119
+ if (!this.connected) return this.stats;
120
+
112
121
  const now = Date.now();
113
122
  const STATS_COOLDOWN = 60000;
123
+
114
124
  if (now - this.#lastStatsRequest < STATS_COOLDOWN) {
115
- return this.#statsCache[this.name] ?? this.stats;
125
+ return this.stats;
116
126
  }
127
+
117
128
  try {
118
129
  const stats = await this.rest.makeRequest("GET", "/v4/stats");
119
- this.#updateStats(stats);
130
+ this.stats = { ...this.#createStats(), ...stats };
120
131
  this.#lastStatsRequest = now;
121
- this.#statsCache[this.name] = this.stats;
122
132
  return this.stats;
123
133
  } catch (err) {
124
134
  this.aqua.emit('debug', `Stats fetch error: ${err.message}`);
@@ -128,60 +138,58 @@ class Node {
128
138
 
129
139
  #updateStats(payload) {
130
140
  if (!payload) return;
131
- this.stats = Object.freeze({
132
- players: payload.players ?? 0,
133
- playingPlayers: payload.playingPlayers ?? 0,
134
- uptime: payload.uptime ?? 0,
135
- ping: payload.ping ?? 0,
141
+ this.stats = {
142
+ ...this.stats,
143
+ ...payload,
136
144
  memory: this.#updateMemoryStats(payload.memory),
137
145
  cpu: this.#updateCpuStats(payload.cpu),
138
146
  frameStats: this.#updateFrameStats(payload.frameStats)
139
- });
147
+ };
140
148
  }
141
149
 
142
150
  #updateMemoryStats(memory = {}) {
143
151
  const allocated = memory.allocated ?? 0;
144
152
  const free = memory.free ?? 0;
145
153
  const used = memory.used ?? 0;
146
- return Object.freeze({
154
+ return {
147
155
  free,
148
156
  used,
149
157
  allocated,
150
158
  reservable: memory.reservable ?? 0,
151
159
  freePercentage: allocated ? (free / allocated) * 100 : 0,
152
160
  usedPercentage: allocated ? (used / allocated) * 100 : 0
153
- });
161
+ };
154
162
  }
155
163
 
156
164
  #updateCpuStats(cpu = {}) {
157
165
  const cores = cpu.cores ?? 0;
158
- return Object.freeze({
166
+ return {
159
167
  cores,
160
168
  systemLoad: cpu.systemLoad ?? 0,
161
169
  lavalinkLoad: cpu.lavalinkLoad ?? 0,
162
170
  lavalinkLoadPercentage: cores ? (cpu.lavalinkLoad / cores) * 100 : 0
163
- });
171
+ };
164
172
  }
165
173
 
166
174
  #updateFrameStats(frameStats = {}) {
167
- if (!frameStats) {
168
- return Object.freeze({
169
- sent: 0,
170
- nulled: 0,
171
- deficit: 0
172
- });
173
- }
175
+ if (!frameStats) return {};
176
+ return {
177
+ sent: frameStats.sent ?? 0,
178
+ nulled: frameStats.nulled ?? 0,
179
+ deficit: frameStats.deficit ?? 0
180
+ };
174
181
  }
175
182
 
176
183
  #onMessage(msg) {
177
184
  try {
178
185
  const payload = JSON.parse(msg.toString());
179
186
  if (!payload?.op) return;
187
+
180
188
  switch (payload.op) {
181
- case "stats":
189
+ case 'stats':
182
190
  this.#updateStats(payload);
183
191
  break;
184
- case "ready":
192
+ case 'ready':
185
193
  this.#handleReadyOp(payload);
186
194
  break;
187
195
  default:
@@ -222,16 +230,28 @@ class Node {
222
230
  return;
223
231
  }
224
232
 
225
- clearTimeout(this.#reconnectTimeoutId);
226
- if (++this.#reconnectAttempted >= this.reconnectTries) {
233
+ const jitter = Math.random() * 10000;
234
+ const backoffTime = Math.min(
235
+ this.reconnectTimeout * Math.pow(1.5, this.#reconnectAttempted) + jitter,
236
+ 30000
237
+ );
238
+
239
+ if (this.#reconnectAttempted >= this.reconnectTries && !this.infiniteReconnects) {
227
240
  this.aqua.emit("nodeError", this, new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
228
- return this.destroy();
241
+ this.destroy(true);
242
+ return;
229
243
  }
230
244
 
245
+ clearTimeout(this.#reconnectTimeoutId);
231
246
  this.#reconnectTimeoutId = setTimeout(() => {
232
- this.aqua.emit("nodeReconnect", this, this.#reconnectAttempted);
247
+ this.#reconnectAttempted++;
248
+ this.aqua.emit("nodeReconnect", {
249
+ nodeName: this.name,
250
+ attempt: this.#reconnectAttempted,
251
+ backoffTime
252
+ });
233
253
  this.connect();
234
- }, this.reconnectTimeout * Math.pow(2, this.#reconnectAttempted)); // Exponential backoff
254
+ }, backoffTime);
235
255
  }
236
256
 
237
257
  get penalties() {
@@ -264,8 +284,8 @@ class Node {
264
284
  this.aqua.nodeMap.delete(this.name);
265
285
  this.aqua.emit("nodeDestroy", this);
266
286
  this.info = null;
267
- this.#statsCache = {};
268
- this.stats = Object.freeze(this.#createStats());
287
+ this.#statsCache.clear();
288
+ this.stats = this.#createStats();
269
289
  }
270
290
  }
271
291
 
@@ -33,7 +33,8 @@ class Player extends EventEmitter {
33
33
  this.ping = 0;
34
34
  this.nowPlayingMessage = null;
35
35
  this.previousTracks = [];
36
- this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
36
+ this.shouldDeleteMessage = options.shouldDeleteMessage;
37
+ this.leaveOnEnd = options.leaveOnEnd
37
38
 
38
39
  this.boundHandleEvent = this.handleEvent.bind(this);
39
40
  this.boundOnPlayerUpdate = this.onPlayerUpdate.bind(this);
@@ -277,9 +278,12 @@ class Player extends EventEmitter {
277
278
 
278
279
  if (player.queue.isEmpty()) {
279
280
  this.playing = false;
280
- this.aqua.emit("queueEnd", player);
281
- return this.cleanup();
281
+ if (this.leaveOnEnd) {
282
+ await this.cleanup();
283
+ }
284
+ return this.aqua.emit("queueEnd", player);
282
285
  }
286
+
283
287
 
284
288
  return player.play();
285
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.7.0-beta5",
3
+ "version": "1.7.0-beta6",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "scripts": {