aqualink 2.7.3 → 2.8.0

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.
@@ -14,20 +14,22 @@ class Connection {
14
14
  this.endpoint = null;
15
15
  this.token = null;
16
16
  this.region = null;
17
- this.selfDeaf = false;
18
- this.selfMute = false;
19
17
  }
20
18
 
21
19
  setServerUpdate(data) {
22
- if (!data?.endpoint) return;
20
+ if (!data?.endpoint || !data.token) {
21
+ this.aqua.emit("debug", `[Player ${this.guildId}] Received incomplete server update.`);
22
+ return;
23
+ }
23
24
 
24
25
  const { endpoint, token } = data;
25
-
26
26
  const regionMatch = REGION_REGEX.exec(endpoint);
27
- if (!regionMatch) return;
28
-
27
+ if (!regionMatch) {
28
+ this.aqua.emit("debug", `[Player ${this.guildId}] Failed to extract region from endpoint: ${endpoint}`);
29
+ return;
30
+ }
29
31
  const newRegion = regionMatch[1];
30
-
32
+
31
33
  if (this.endpoint === endpoint && this.token === token && this.region === newRegion) {
32
34
  return;
33
35
  }
@@ -38,7 +40,7 @@ class Connection {
38
40
  this.region = newRegion;
39
41
 
40
42
  this.aqua.emit("debug",
41
- `[Player ${this.guildId} - CONNECTION] Voice Server: ${oldRegion ? `Changed from ${oldRegion} to ${newRegion}` : newRegion}`
43
+ `[Player ${this.guildId}] Voice server updated: ${oldRegion ? `Changed from ${oldRegion} to ${newRegion}` : newRegion}`
42
44
  );
43
45
 
44
46
  if (this.player.paused) {
@@ -49,13 +51,12 @@ class Connection {
49
51
  }
50
52
 
51
53
  setStateUpdate(data) {
52
- if (!data) {
53
- this._destroyPlayer();
54
+ if (!data || data.user_id !== this.aqua.clientId) {
54
55
  return;
55
56
  }
56
57
 
57
- const { channel_id, session_id, self_deaf, self_mute } = data;
58
-
58
+ const { session_id, channel_id, self_deaf, self_mute } = data;
59
+
59
60
  if (!channel_id || !session_id) {
60
61
  this._destroyPlayer();
61
62
  return;
@@ -67,27 +68,26 @@ class Connection {
67
68
  this.player.voiceChannel = channel_id;
68
69
  }
69
70
 
70
- this.selfDeaf = !!self_deaf;
71
- this.selfMute = !!self_mute;
71
+ this.player.selfDeaf = !!self_deaf;
72
+ this.player.selfMute = !!self_mute;
72
73
 
73
74
  if (this.sessionId !== session_id) {
74
75
  this.sessionId = session_id;
76
+ this.aqua.emit("debug", `[Player ${this.guildId}] Received new session ID.`);
75
77
  this._updatePlayerVoiceData();
76
78
  }
77
79
  }
78
80
 
79
81
  _destroyPlayer() {
80
- if (this.player) {
82
+ if (this.player && !this.player.destroyed) {
81
83
  this.player.destroy();
82
84
  this.aqua.emit("playerDestroy", this.player);
83
85
  }
84
86
  }
85
87
 
86
88
  _updatePlayerVoiceData() {
87
- if (!this.player) return;
88
-
89
- if (!this.sessionId || !this.endpoint || !this.token) {
90
- this.aqua.emit("debug", `[Player ${this.guildId}] Incomplete voice data, waiting for complete data`);
89
+ if (!this.player || !this.sessionId || !this.endpoint || !this.token) {
90
+ this.aqua.emit("debug", `[Player ${this.guildId}] Incomplete voice data, waiting...`);
91
91
  return;
92
92
  }
93
93
 
@@ -100,10 +100,7 @@ class Connection {
100
100
  try {
101
101
  this.nodes.rest.updatePlayer({
102
102
  guildId: this.guildId,
103
- data: {
104
- voice: voiceData,
105
- volume: this.player.volume
106
- }
103
+ data: { voice: voiceData, volume: this.player.volume },
107
104
  });
108
105
  } catch (error) {
109
106
  this.aqua.emit("apiError", "updatePlayer", {
@@ -113,7 +110,6 @@ class Connection {
113
110
  });
114
111
  }
115
112
  }
116
-
117
113
  }
118
114
 
119
115
  module.exports = Connection;
@@ -2,11 +2,18 @@
2
2
  const WebSocket = require('ws');
3
3
  const Rest = require("./Rest");
4
4
 
5
+ const LYRICS_OP_REGEX = /^Lyrics/;
6
+ const JSON_VALIDATION_REGEX = /^[\s]*[{\[]/;
7
+
5
8
  class Node {
6
9
  static BACKOFF_MULTIPLIER = 1.5;
7
10
  static MAX_BACKOFF = 60000;
8
11
  static WS_OPEN = WebSocket.OPEN;
9
12
  static WS_CLOSE_NORMAL = 1000;
13
+ static DEFAULT_RECONNECT_TIMEOUT = 2000;
14
+ static DEFAULT_RESUME_TIMEOUT = 60;
15
+ static JITTER_MAX = 2000;
16
+ static JITTER_FACTOR = 0.2;
10
17
 
11
18
  constructor(aqua, connOptions, options = {}) {
12
19
  this.aqua = aqua;
@@ -21,30 +28,26 @@ class Node {
21
28
  regions = []
22
29
  } = connOptions;
23
30
 
24
- this.host = host;
25
- this.name = name;
26
- this.port = port;
27
- this.password = password;
28
- this.secure = !!secure;
29
- this.sessionId = sessionId;
30
- this.regions = regions;
31
+ Object.assign(this, {
32
+ host, name, port, password, sessionId, regions,
33
+ secure: !!secure,
34
+ wsUrl: `ws${secure ? "s" : ""}://${host}:${port}/v4/websocket`
35
+ });
31
36
 
32
- this.wsUrl = `ws${this.secure ? "s" : ""}://${this.host}:${this.port}/v4/websocket`;
33
37
  this.rest = new Rest(aqua, this);
34
38
 
35
39
  const {
36
- resumeTimeout = 60,
40
+ resumeTimeout = Node.DEFAULT_RESUME_TIMEOUT,
37
41
  autoResume = false,
38
- reconnectTimeout = 2000,
42
+ reconnectTimeout = Node.DEFAULT_RECONNECT_TIMEOUT,
39
43
  reconnectTries = 3,
40
44
  infiniteReconnects = false
41
45
  } = options;
42
46
 
43
- this.resumeTimeout = resumeTimeout;
44
- this.autoResume = autoResume;
45
- this.reconnectTimeout = reconnectTimeout;
46
- this.reconnectTries = reconnectTries;
47
- this.infiniteReconnects = infiniteReconnects;
47
+ Object.assign(this, {
48
+ resumeTimeout, autoResume, reconnectTimeout,
49
+ reconnectTries, infiniteReconnects
50
+ });
48
51
 
49
52
  this.connected = false;
50
53
  this.info = null;
@@ -53,33 +56,40 @@ class Node {
53
56
  this.reconnectTimeoutId = null;
54
57
  this.isDestroyed = false;
55
58
 
56
- this._onOpen = this._onOpen.bind(this);
57
- this._onError = this._onError.bind(this);
58
- this._onMessage = this._onMessage.bind(this);
59
- this._onClose = this._onClose.bind(this);
59
+ this._boundHandlers = {
60
+ onOpen: () => this._onOpen(),
61
+ onError: (error) => this._onError(error),
62
+ onMessage: (msg) => this._onMessage(msg),
63
+ onClose: (code, reason) => this._onClose(code, reason)
64
+ };
60
65
 
61
66
  this._headers = this._constructHeaders();
62
- this.initializeStats();
67
+ this._initializeStats();
63
68
  }
64
69
 
65
- initializeStats() {
70
+ _initializeStats() {
66
71
  this.stats = {
67
72
  players: 0,
68
73
  playingPlayers: 0,
69
74
  uptime: 0,
70
- memory: { free: 0, used: 0, allocated: 0, reservable: 0, freePercentage: 0, usedPercentage: 0 },
71
- cpu: { cores: 0, systemLoad: 0, lavalinkLoad: 0, lavalinkLoadPercentage: 0 },
75
+ memory: {
76
+ free: 0, used: 0, allocated: 0, reservable: 0,
77
+ freePercentage: 0, usedPercentage: 0
78
+ },
79
+ cpu: {
80
+ cores: 0, systemLoad: 0, lavalinkLoad: 0,
81
+ lavalinkLoadPercentage: 0
82
+ },
72
83
  frameStats: { sent: 0, nulled: 0, deficit: 0 },
73
84
  ping: 0
74
85
  };
75
86
  }
76
87
 
77
88
  _constructHeaders() {
78
- const headers = {
79
- Authorization: this.password,
80
- "User-Id": this.aqua.clientId,
81
- "Client-Name": `Aqua/${this.aqua.version} (https://github.com/ToddyTheNoobDud/AquaLink)`
82
- };
89
+ const headers = Object.create(null);
90
+ headers.Authorization = this.password;
91
+ headers["User-Id"] = this.aqua.clientId;
92
+ headers["Client-Name"] = `Aqua/${this.aqua.version} (https://github.com/ToddyTheNoobDud/AquaLink)`;
83
93
 
84
94
  if (this.sessionId) {
85
95
  headers["Session-Id"] = this.sessionId;
@@ -104,7 +114,7 @@ class Node {
104
114
  }
105
115
  } catch (err) {
106
116
  this.info = null;
107
- this.emitError(`Failed to fetch node info: ${err.message}`);
117
+ this._emitError(`Failed to fetch node info: ${err.message}`);
108
118
  }
109
119
  }
110
120
 
@@ -113,18 +123,22 @@ class Node {
113
123
  }
114
124
 
115
125
  _onMessage(msg) {
126
+ if (!JSON_VALIDATION_REGEX.test(msg)) {
127
+ this.aqua.emit("debug", this.name, `Received invalid JSON format: ${msg.slice(0, 100)}...`);
128
+ return;
129
+ }
130
+
116
131
  let payload;
117
132
  try {
118
133
  payload = JSON.parse(msg);
119
134
  } catch {
120
- this.aqua.emit("debug", this.name, `Received invalid JSON: ${msg}`);
135
+ this.aqua.emit("debug", this.name, `JSON parse failed: ${msg.slice(0, 100)}...`);
121
136
  return;
122
137
  }
123
138
 
124
139
  const { op, guildId } = payload;
125
140
  if (!op) return;
126
141
 
127
-
128
142
  switch (op) {
129
143
  case "stats":
130
144
  this._updateStats(payload);
@@ -133,30 +147,32 @@ class Node {
133
147
  this._handleReadyOp(payload);
134
148
  break;
135
149
  default:
136
- if (op.startsWith("Lyrics")) {
137
- const player = guildId ? this.aqua.players.get(guildId) : null;
138
- this.aqua.emit(op, player, payload.track || null, payload);
139
- } else if (guildId) {
140
- const player = this.aqua.players.get(guildId);
141
- player?.emit(op, payload);
142
- }
150
+ this._handleCustomOp(op, guildId, payload);
143
151
  break;
144
152
  }
145
153
  }
146
154
 
155
+ _handleCustomOp(op, guildId, payload) {
156
+ if (LYRICS_OP_REGEX.test(op)) {
157
+ const player = guildId ? this.aqua.players.get(guildId) : null;
158
+ this.aqua.emit(op, player, payload.track || null, payload);
159
+ } else if (guildId) {
160
+ const player = this.aqua.players.get(guildId);
161
+ if (player) player.emit(op, payload);
162
+ }
163
+ }
164
+
147
165
  _onClose(code, reason) {
148
166
  this.connected = false;
149
167
  const reasonStr = reason?.toString() || "No reason provided";
150
168
 
151
169
  this.aqua.emit("nodeDisconnect", this, { code, reason: reasonStr });
152
-
153
170
  this.aqua.handleNodeFailover(this);
154
-
155
- this.scheduleReconnect(code);
171
+ this._scheduleReconnect(code);
156
172
  }
157
173
 
158
- scheduleReconnect(code) {
159
- this.clearReconnectTimeout();
174
+ _scheduleReconnect(code) {
175
+ this._clearReconnectTimeout();
160
176
 
161
177
  if (code === Node.WS_CLOSE_NORMAL || this.isDestroyed) {
162
178
  this.aqua.emit("debug", this.name, "WebSocket closed normally, not reconnecting");
@@ -170,12 +186,12 @@ class Node {
170
186
  }
171
187
 
172
188
  if (this.reconnectAttempted >= this.reconnectTries) {
173
- this.emitError(new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
189
+ this._emitError(new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
174
190
  this.destroy(true);
175
191
  return;
176
192
  }
177
193
 
178
- const backoffTime = this.calculateBackoff();
194
+ const backoffTime = this._calculateBackoff();
179
195
  this.reconnectAttempted++;
180
196
 
181
197
  this.aqua.emit("nodeReconnect", this, {
@@ -186,13 +202,14 @@ class Node {
186
202
  this.reconnectTimeoutId = setTimeout(() => this.connect(), backoffTime);
187
203
  }
188
204
 
189
- calculateBackoff() {
190
- const baseBackoff = this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, this.reconnectAttempted);
191
- const jitter = Math.random() * Math.min(2000, baseBackoff * 0.2);
205
+ _calculateBackoff() {
206
+ const baseBackoff = this.reconnectTimeout * (Node.BACKOFF_MULTIPLIER ** this.reconnectAttempted);
207
+ const maxJitter = Math.min(Node.JITTER_MAX, baseBackoff * Node.JITTER_FACTOR);
208
+ const jitter = Math.random() * maxJitter;
192
209
  return Math.min(baseBackoff + jitter, Node.MAX_BACKOFF);
193
210
  }
194
211
 
195
- clearReconnectTimeout() {
212
+ _clearReconnectTimeout() {
196
213
  if (this.reconnectTimeoutId) {
197
214
  clearTimeout(this.reconnectTimeoutId);
198
215
  this.reconnectTimeoutId = null;
@@ -202,26 +219,26 @@ class Node {
202
219
  async connect() {
203
220
  if (this.isDestroyed) return;
204
221
 
205
- if (this.ws && this.ws.readyState === Node.WS_OPEN) {
222
+ if (this.ws?.readyState === Node.WS_OPEN) {
206
223
  this.aqua.emit("debug", this.name, "WebSocket already connected");
207
224
  return;
208
225
  }
209
226
 
210
- this.cleanupExistingConnection();
227
+ this._cleanupExistingConnection();
211
228
 
212
229
  this.ws = new WebSocket(this.wsUrl, {
213
230
  headers: this._headers,
214
231
  perMessageDeflate: false
215
232
  });
216
233
 
217
- this.ws.once("open", this._onOpen);
218
- this.ws.once("error", this._onError);
219
- this.ws.on("message", this._onMessage);
220
- this.ws.once("close", this._onClose);
221
-
234
+ const handlers = this._boundHandlers;
235
+ this.ws.once("open", handlers.onOpen);
236
+ this.ws.once("error", handlers.onError);
237
+ this.ws.on("message", handlers.onMessage);
238
+ this.ws.once("close", handlers.onClose);
222
239
  }
223
240
 
224
- cleanupExistingConnection() {
241
+ _cleanupExistingConnection() {
225
242
  if (!this.ws) return;
226
243
 
227
244
  this.ws.removeAllListeners();
@@ -230,7 +247,7 @@ class Node {
230
247
  try {
231
248
  this.ws.close();
232
249
  } catch (err) {
233
- this.emitError(`Failed to close WebSocket: ${err.message}`);
250
+ this._emitError(`Failed to close WebSocket: ${err.message}`);
234
251
  }
235
252
  }
236
253
 
@@ -239,8 +256,8 @@ class Node {
239
256
 
240
257
  destroy(clean = false) {
241
258
  this.isDestroyed = true;
242
- this.clearReconnectTimeout();
243
- this.cleanupExistingConnection();
259
+ this._clearReconnectTimeout();
260
+ this._cleanupExistingConnection();
244
261
 
245
262
  if (!clean) {
246
263
  this.aqua.handleNodeFailover(this);
@@ -260,39 +277,45 @@ class Node {
260
277
  try {
261
278
  const newStats = await this.rest.getStats();
262
279
  if (newStats && this.stats) {
263
- this.stats.players = newStats.players ?? this.stats.players;
264
- this.stats.playingPlayers = newStats.playingPlayers ?? this.stats.playingPlayers;
265
- this.stats.uptime = newStats.uptime ?? this.stats.uptime;
266
- this.stats.ping = newStats.ping ?? this.stats.ping;
267
-
268
- if (newStats.memory) {
269
- Object.assign(this.stats.memory, newStats.memory);
270
- this._calculateMemoryPercentages();
271
- }
272
-
273
- if (newStats.cpu) {
274
- Object.assign(this.stats.cpu, newStats.cpu);
275
- this._calculateCpuPercentages();
276
- }
277
-
278
- if (newStats.frameStats) {
279
- Object.assign(this.stats.frameStats, newStats.frameStats);
280
- }
280
+ this._mergeStats(newStats);
281
281
  }
282
282
  return this.stats;
283
283
  } catch (err) {
284
- this.emitError(`Failed to fetch node stats: ${err.message}`);
284
+ this._emitError(`Failed to fetch node stats: ${err.message}`);
285
285
  return this.stats;
286
286
  }
287
287
  }
288
288
 
289
+ _mergeStats(newStats) {
290
+ this.stats.players = newStats.players ?? this.stats.players;
291
+ this.stats.playingPlayers = newStats.playingPlayers ?? this.stats.playingPlayers;
292
+ this.stats.uptime = newStats.uptime ?? this.stats.uptime;
293
+ this.stats.ping = newStats.ping ?? this.stats.ping;
294
+
295
+ if (newStats.memory) {
296
+ Object.assign(this.stats.memory, newStats.memory);
297
+ this._calculateMemoryPercentages();
298
+ }
299
+
300
+ if (newStats.cpu) {
301
+ Object.assign(this.stats.cpu, newStats.cpu);
302
+ this._calculateCpuPercentages();
303
+ }
304
+
305
+ if (newStats.frameStats) {
306
+ Object.assign(this.stats.frameStats, newStats.frameStats);
307
+ }
308
+ }
309
+
289
310
  _updateStats(payload) {
290
311
  if (!payload) return;
291
312
 
292
- this.stats.players = payload.players;
293
- this.stats.playingPlayers = payload.playingPlayers;
294
- this.stats.uptime = payload.uptime;
295
- this.stats.ping = payload.ping;
313
+ Object.assign(this.stats, {
314
+ players: payload.players,
315
+ playingPlayers: payload.playingPlayers,
316
+ uptime: payload.uptime,
317
+ ping: payload.ping
318
+ });
296
319
 
297
320
  if (payload.memory) {
298
321
  Object.assign(this.stats.memory, payload.memory);
@@ -312,8 +335,9 @@ class Node {
312
335
  _calculateMemoryPercentages() {
313
336
  const { memory } = this.stats;
314
337
  if (memory.allocated > 0) {
315
- memory.freePercentage = (memory.free / memory.allocated) * 100;
316
- memory.usedPercentage = (memory.used / memory.allocated) * 100;
338
+ const allocated = memory.allocated;
339
+ memory.freePercentage = (memory.free / allocated) * 100;
340
+ memory.usedPercentage = (memory.used / allocated) * 100;
317
341
  }
318
342
  }
319
343
 
@@ -326,7 +350,7 @@ class Node {
326
350
 
327
351
  _handleReadyOp(payload) {
328
352
  if (!payload.sessionId) {
329
- this.emitError("Ready payload missing sessionId");
353
+ this._emitError("Ready payload missing sessionId");
330
354
  return;
331
355
  }
332
356
 
@@ -341,10 +365,11 @@ class Node {
341
365
  await this.aqua.loadPlayers();
342
366
  this.aqua.emit("debug", this.name, "Session resumed successfully");
343
367
  } catch (err) {
344
- this.emitError(`Failed to resume session: ${err.message}`);
368
+ this._emitError(`Failed to resume session: ${err.message}`);
345
369
  }
346
370
  }
347
- emitError(error) {
371
+
372
+ _emitError(error) {
348
373
  const errorObj = error instanceof Error ? error : new Error(error);
349
374
  console.error(`[Aqua] [${this.name}] Error:`, errorObj);
350
375
  this.aqua.emit("error", this, errorObj);