aqualink 2.7.1 → 2.7.2

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.
@@ -62,8 +62,16 @@ class Aqua extends EventEmitter {
62
62
  this._failoverQueue = new Map();
63
63
  this._lastFailoverAttempt = new Map();
64
64
 
65
+ this._brokenPlayers = new Map();
66
+ this._nodeReconnectHandlers = new Map();
67
+
65
68
  this._boundCleanupPlayer = this.cleanupPlayer.bind(this);
66
69
  this._boundHandlePlayerDestroy = this._handlePlayerDestroy.bind(this);
70
+ this._boundHandleNodeReconnect = this._handleNodeReconnect.bind(this);
71
+ this._boundHandleSocketClosed = this._handleSocketClosed.bind(this);
72
+
73
+ this.on('nodeConnect', (node) => this._handleNodeReconnect(node));
74
+ this.on('nodeDisconnect', (node) => this._handleSocketClosed(node));
67
75
  }
68
76
 
69
77
  defaultSendFunction(packet) {
@@ -128,6 +136,7 @@ class Aqua extends EventEmitter {
128
136
  this.destroyNode(nodeId);
129
137
 
130
138
  const node = new Node(this, options, this.options);
139
+ node.players = new Set();
131
140
  this.nodeMap.set(nodeId, node);
132
141
  this._invalidateCache();
133
142
 
@@ -144,6 +153,116 @@ class Aqua extends EventEmitter {
144
153
  }
145
154
  }
146
155
 
156
+ async _handleNodeReconnect(node) {
157
+ if (!this.autoResume) return;
158
+
159
+ const nodeId = node.name || node.host;
160
+ this.emit("debug", "Aqua", `Node ${nodeId} reconnected, attempting to rebuild broken players`);
161
+
162
+ this._nodeStates.set(nodeId, { connected: true, failoverInProgress: false });
163
+
164
+ await this._rebuildBrokenPlayers(node);
165
+ }
166
+
167
+ async _handleSocketClosed(node) {
168
+ if (!this.autoResume) return;
169
+
170
+ const nodeId = node.name || node.host;
171
+ this.emit("debug", "Aqua", `Socket closed for node ${nodeId}, storing broken players`);
172
+
173
+ this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
174
+
175
+ await this._storeBrokenPlayersForNode(node);
176
+ }
177
+
178
+ async _storeBrokenPlayersForNode(node) {
179
+ const nodeId = node.name || node.host;
180
+ const affectedPlayers = await this._getPlayersForNode(node);
181
+
182
+
183
+ for (const player of affectedPlayers) {
184
+ try {
185
+ const playerState = this._capturePlayerState(player);
186
+ if (playerState) {
187
+ playerState.originalNodeId = nodeId;
188
+ playerState.brokenAt = Date.now();
189
+ this._brokenPlayers.set(player.guildId, playerState);
190
+
191
+ this.emit("debug", "Aqua", `Stored broken player state for guild ${player.guildId}`);
192
+ }
193
+ } catch (error) {
194
+ this.emit("error", null, new Error(`Failed to store broken player state: ${error.message}`));
195
+ }
196
+ }
197
+ }
198
+
199
+ async _getPlayersForNode(node) {
200
+ const affectedPlayers = [];
201
+ for (const player of this.players.values()) {
202
+ if (player.nodes === node || player.nodes?.name === node.name) {
203
+ affectedPlayers.push(player);
204
+ }
205
+ }
206
+ return affectedPlayers;
207
+ }
208
+
209
+ async _rebuildBrokenPlayers(node) {
210
+ const nodeId = node.name || node.host;
211
+ const rebuiltCount = 0;
212
+
213
+
214
+
215
+ for (const [guildId, brokenState] of this._brokenPlayers.entries()) {
216
+ if (brokenState.originalNodeId !== nodeId) continue;
217
+ try {
218
+ await this._rebuildPlayer(brokenState, node);
219
+ this._brokenPlayers.delete(guildId);
220
+ rebuiltCount++;
221
+
222
+
223
+ this.emit("debug", "Aqua", `Successfully rebuilt player for guild ${guildId}`);
224
+ } catch (error) {
225
+ this.emit("error", null, new Error(`Failed to rebuild player for guild ${guildId}: ${error.message}`));
226
+
227
+ if (Date.now() - brokenState.brokenAt > 300000) {
228
+ this._brokenPlayers.delete(guildId);
229
+ }
230
+ }
231
+ }
232
+
233
+ if (rebuiltCount > 0) {
234
+ this.emit("playersRebuilt", node, rebuiltCount);
235
+ }
236
+ }
237
+
238
+ async _rebuildPlayer(brokenState, targetNode) {
239
+ const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = brokenState;
240
+
241
+ const connectionOptions = { guildId, textChannel, voiceChannel, defaultVolume: volume, deaf };
242
+ const existingPlayer = this.players.get(guildId);
243
+
244
+ if (existingPlayer) {
245
+ await existingPlayer.destroy();
246
+ }
247
+
248
+ setTimeout(async () => {
249
+ const newestPlayer = await this.createConnection(connectionOptions);
250
+ this.players.set(guildId, newestPlayer);
251
+
252
+ if (current) {
253
+ try {
254
+ await newestPlayer.queue.add(current);
255
+ await newestPlayer.play();
256
+ } catch (error) {
257
+ console.error("Error while playing track:", error);
258
+ }
259
+ }
260
+
261
+ this.emit("trackStart", newestPlayer, brokenState.current);
262
+ return newestPlayer;
263
+ }, 2000);
264
+ }
265
+
147
266
  destroyNode(identifier) {
148
267
  const node = this.nodeMap.get(identifier);
149
268
  if (!node) return;
@@ -153,6 +272,11 @@ class Aqua extends EventEmitter {
153
272
  }
154
273
 
155
274
  _cleanupNode(nodeId) {
275
+ const node = this.nodeMap.get(nodeId);
276
+ if (node) {
277
+ node.removeAllListeners();
278
+ }
279
+
156
280
  this.nodeMap.delete(nodeId);
157
281
  this._nodeStates.delete(nodeId);
158
282
  this._failoverQueue.delete(nodeId);
@@ -186,7 +310,7 @@ class Aqua extends EventEmitter {
186
310
  try {
187
311
  this.emit("nodeFailover", failedNode);
188
312
 
189
- const affectedPlayers = this._getPlayersForNode(failedNode);
313
+ const affectedPlayers = Array.from(failedNode.players);
190
314
  if (affectedPlayers.length === 0) {
191
315
  this._nodeStates.set(nodeId, { connected: false, failoverInProgress: false });
192
316
  return;
@@ -215,22 +339,6 @@ class Aqua extends EventEmitter {
215
339
  }
216
340
  }
217
341
 
218
- _getPlayersForNode(node) {
219
- const affectedPlayers = [];
220
- for (const player of this.players.values()) {
221
- if (player.nodes === node || player.nodes?.name === node.name) {
222
- affectedPlayers.push(player);
223
- }
224
- }
225
- return affectedPlayers;
226
- }
227
-
228
- _getAvailableNodesForFailover(failedNode) {
229
- return this.leastUsedNodes.filter(node =>
230
- node !== failedNode && node.name !== failedNode.name
231
- );
232
- }
233
-
234
342
  async _migratePlayersWithRetry(players, availableNodes) {
235
343
  const results = [];
236
344
 
@@ -275,7 +383,6 @@ class Aqua extends EventEmitter {
275
383
 
276
384
  await this._restorePlayerState(newPlayer, playerState);
277
385
 
278
- newPlayer.destroy();
279
386
  if (playerState.current) {
280
387
  newPlayer.queue.add(playerState.current);
281
388
  }
@@ -316,13 +423,12 @@ class Aqua extends EventEmitter {
316
423
  position: player.position || 0,
317
424
  current: player.current ? { ...player.current } : null,
318
425
  queue: player.queue?.tracks ? [...player.queue.tracks] : [],
319
- repeat: player.repeat,
426
+ repeat: player.loop,
320
427
  shuffle: player.shuffle,
321
428
  deaf: player.deaf || false,
322
429
  mute: player.mute || false,
323
- region: player.region,
324
- requester: player.requester,
325
- timestamp: Date.now()
430
+ timestamp: Date.now(),
431
+ connected: player.connected || false,
326
432
  };
327
433
  } catch (error) {
328
434
  return null;
@@ -347,22 +453,18 @@ class Aqua extends EventEmitter {
347
453
  if (!newPlayer || !playerState) return;
348
454
 
349
455
  try {
350
- // Batch operations where possible
351
456
  const operations = [];
352
457
 
353
458
  if (playerState.volume !== undefined) {
354
459
  operations.push(newPlayer.setVolume(playerState.volume));
355
460
  }
356
461
 
357
- // Restore queue efficiently
358
462
  if (playerState.queue?.length > 0) {
359
463
  newPlayer.queue.add(...playerState.queue);
360
464
  }
361
465
 
362
- // Wait for all operations
363
466
  await Promise.all(operations);
364
467
 
365
- // Handle current track restoration
366
468
  if (playerState.current && this.failoverOptions.preservePosition) {
367
469
  newPlayer.queue.unshift(playerState.current);
368
470
 
@@ -370,7 +472,7 @@ class Aqua extends EventEmitter {
370
472
  await newPlayer.play();
371
473
 
372
474
  if (playerState.position > 0) {
373
- await this._delay(300); // Reduced delay
475
+ await this._delay(300);
374
476
  await newPlayer.seek(playerState.position);
375
477
  }
376
478
 
@@ -380,7 +482,6 @@ class Aqua extends EventEmitter {
380
482
  }
381
483
  }
382
484
 
383
- // Restore other properties
384
485
  Object.assign(newPlayer, {
385
486
  repeat: playerState.repeat,
386
487
  shuffle: playerState.shuffle
@@ -395,7 +496,6 @@ class Aqua extends EventEmitter {
395
496
  return new Promise(resolve => setTimeout(resolve, ms));
396
497
  }
397
498
 
398
- // Optimized cleanup
399
499
  async cleanupPlayer(player) {
400
500
  if (!player) return;
401
501
  try {
@@ -434,7 +534,6 @@ class Aqua extends EventEmitter {
434
534
  }
435
535
  }
436
536
 
437
- // Optimized sorting with caching
438
537
  const loadCache = new Map();
439
538
  regionNodes.sort((a, b) => {
440
539
  const loadA = loadCache.get(a) ?? (loadCache.set(a, this._calculateLoad(a)), loadCache.get(a));
@@ -469,7 +568,6 @@ class Aqua extends EventEmitter {
469
568
  const player = new Player(this, node, options);
470
569
  this.players.set(options.guildId, player);
471
570
 
472
- // Use pre-bound method for better performance
473
571
  player.once("destroy", this._boundHandlePlayerDestroy);
474
572
  player.connect(options);
475
573
  this.emit("playerCreate", player);
@@ -477,6 +575,10 @@ class Aqua extends EventEmitter {
477
575
  }
478
576
 
479
577
  _handlePlayerDestroy(player) {
578
+ const node = player.nodes;
579
+ if (node && node.players) {
580
+ node.players.delete(player);
581
+ }
480
582
  this.players.delete(player.guildId);
481
583
  this.emit("playerDestroy", player);
482
584
  }
@@ -714,6 +816,44 @@ class Aqua extends EventEmitter {
714
816
  });
715
817
  }
716
818
 
819
+ // Auto-resume utility methods
820
+ getBrokenPlayersCount() {
821
+ return this._brokenPlayers.size;
822
+ }
823
+
824
+ getBrokenPlayers() {
825
+ return Array.from(this._brokenPlayers.entries()).map(([guildId, state]) => ({
826
+ guildId,
827
+ originalNodeId: state.originalNodeId,
828
+ brokenAt: state.brokenAt,
829
+ hasCurrentTrack: !!state.current,
830
+ queueSize: state.queue?.length || 0
831
+ }));
832
+ }
833
+
834
+ clearBrokenPlayers() {
835
+ this._brokenPlayers.clear();
836
+ }
837
+
838
+ async forceBrokenPlayerRebuild(guildId) {
839
+ const brokenState = this._brokenPlayers.get(guildId);
840
+ if (!brokenState) return false;
841
+
842
+ try {
843
+ const targetNode = this.nodeMap.get(brokenState.originalNodeId) || this.leastUsedNodes[0];
844
+ if (!targetNode || !targetNode.connected) {
845
+ throw new Error("No available nodes for rebuild");
846
+ }
847
+
848
+ await this._rebuildPlayer(brokenState, targetNode);
849
+ this._brokenPlayers.delete(guildId);
850
+ return true;
851
+ } catch (error) {
852
+ this.emit("error", null, new Error(`Failed to force rebuild player for guild ${guildId}: ${error.message}`));
853
+ return false;
854
+ }
855
+ }
856
+
717
857
  // Utility methods
718
858
  resetFailoverAttempts(nodeId) {
719
859
  this._failoverQueue.delete(nodeId);
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
-
3
2
  const WebSocket = require('ws');
4
3
  const Rest = require("./Rest");
5
4
 
@@ -30,6 +29,9 @@ class Node {
30
29
  this.sessionId = sessionId;
31
30
  this.regions = regions;
32
31
 
32
+ this.wsUrl = `ws${this.secure ? "s" : ""}://${this.host}:${this.port}/v4/websocket`;
33
+ this.rest = new Rest(aqua, this);
34
+
33
35
  const {
34
36
  resumeTimeout = 60,
35
37
  autoResume = false,
@@ -44,24 +46,19 @@ class Node {
44
46
  this.reconnectTries = reconnectTries;
45
47
  this.infiniteReconnects = infiniteReconnects;
46
48
 
47
- this.wsUrl = `ws${this.secure ? "s" : ""}://${this.host}:${this.port}/v4/websocket`;
48
- this.rest = new Rest(aqua, this);
49
- this._headers = this._constructHeaders();
50
-
51
49
  this.connected = false;
52
- this.isDestroyed = false;
53
50
  this.info = null;
54
51
  this.ws = null;
55
52
  this.reconnectAttempted = 0;
56
53
  this.reconnectTimeoutId = null;
57
- this.lastHealthCheck = Date.now();
54
+ this.isDestroyed = false;
58
55
 
59
- // Pre-bind event handlers for efficiency
60
56
  this._onOpen = this._onOpen.bind(this);
61
57
  this._onError = this._onError.bind(this);
62
58
  this._onMessage = this._onMessage.bind(this);
63
59
  this._onClose = this._onClose.bind(this);
64
60
 
61
+ this._headers = this._constructHeaders();
65
62
  this.initializeStats();
66
63
  }
67
64
 
@@ -94,11 +91,7 @@ class Node {
94
91
  async _onOpen() {
95
92
  this.connected = true;
96
93
  this.reconnectAttempted = 0;
97
- this.lastHealthCheck = Date.now();
98
94
  this.aqua.emit("debug", this.name, "WebSocket connection established");
99
-
100
- clearTimeout(this.reconnectTimeoutId);
101
- this.reconnectTimeoutId = null;
102
95
 
103
96
  if (this.aqua.bypassChecks?.nodeFetchInfo) return;
104
97
 
@@ -107,7 +100,7 @@ class Node {
107
100
  this.aqua.emit("nodeConnected", this);
108
101
 
109
102
  if (this.autoResume && this.sessionId) {
110
- await this.resumePlayers();
103
+ await this.aqua.loadPlayers();
111
104
  }
112
105
  } catch (err) {
113
106
  this.info = null;
@@ -131,8 +124,7 @@ class Node {
131
124
  const { op, guildId } = payload;
132
125
  if (!op) return;
133
126
 
134
- this.lastHealthCheck = Date.now();
135
-
127
+
136
128
  switch (op) {
137
129
  case "stats":
138
130
  this._updateStats(payload);
@@ -157,20 +149,22 @@ class Node {
157
149
  const reasonStr = reason?.toString() || "No reason provided";
158
150
 
159
151
  this.aqua.emit("nodeDisconnect", this, { code, reason: reasonStr });
152
+
160
153
  this.aqua.handleNodeFailover(this);
154
+
161
155
  this.scheduleReconnect(code);
162
156
  }
163
157
 
164
158
  scheduleReconnect(code) {
159
+ this.clearReconnectTimeout();
160
+
165
161
  if (code === Node.WS_CLOSE_NORMAL || this.isDestroyed) {
166
- this.aqua.emit("debug", this.name, "WebSocket closed normally, not reconnecting.");
162
+ this.aqua.emit("debug", this.name, "WebSocket closed normally, not reconnecting");
167
163
  return;
168
164
  }
169
165
 
170
- if (this.reconnectTimeoutId) return;
171
-
172
166
  if (this.infiniteReconnects) {
173
- this.aqua.emit("nodeReconnect", this, "Infinite reconnects enabled, trying again in 10 seconds.");
167
+ this.aqua.emit("nodeReconnect", this, "Infinite reconnects enabled, trying again in 10 seconds");
174
168
  this.reconnectTimeoutId = setTimeout(() => this.connect(), 10000);
175
169
  return;
176
170
  }
@@ -193,19 +187,26 @@ class Node {
193
187
  }
194
188
 
195
189
  calculateBackoff() {
196
- const baseBackoff = this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, this.reconnectAttempted - 1);
190
+ const baseBackoff = this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, this.reconnectAttempted);
197
191
  const jitter = Math.random() * Math.min(2000, baseBackoff * 0.2);
198
192
  return Math.min(baseBackoff + jitter, Node.MAX_BACKOFF);
199
193
  }
200
194
 
201
- connect() {
195
+ clearReconnectTimeout() {
196
+ if (this.reconnectTimeoutId) {
197
+ clearTimeout(this.reconnectTimeoutId);
198
+ this.reconnectTimeoutId = null;
199
+ }
200
+ }
201
+
202
+ async connect() {
202
203
  if (this.isDestroyed) return;
203
204
 
204
205
  if (this.ws && this.ws.readyState === Node.WS_OPEN) {
205
206
  this.aqua.emit("debug", this.name, "WebSocket already connected");
206
207
  return;
207
208
  }
208
-
209
+
209
210
  this.cleanupExistingConnection();
210
211
 
211
212
  this.ws = new WebSocket(this.wsUrl, {
@@ -217,31 +218,28 @@ class Node {
217
218
  this.ws.once("error", this._onError);
218
219
  this.ws.on("message", this._onMessage);
219
220
  this.ws.once("close", this._onClose);
221
+
220
222
  }
221
223
 
222
224
  cleanupExistingConnection() {
223
225
  if (!this.ws) return;
224
226
 
225
227
  this.ws.removeAllListeners();
226
- try {
227
- if (this.ws.readyState === Node.WS_OPEN) {
228
- this.ws.close(Node.WS_CLOSE_NORMAL, "Manual closure");
228
+
229
+ if (this.ws.readyState === Node.WS_OPEN) {
230
+ try {
231
+ this.ws.close();
232
+ } catch (err) {
233
+ this.emitError(`Failed to close WebSocket: ${err.message}`);
229
234
  }
230
- } catch (err) {
231
- this.emitError(`Failed to close WebSocket: ${err.message}`);
232
235
  }
236
+
233
237
  this.ws = null;
234
238
  }
235
239
 
236
240
  destroy(clean = false) {
237
- if (this.isDestroyed) return;
238
241
  this.isDestroyed = true;
239
-
240
- if (this.reconnectTimeoutId) {
241
- clearTimeout(this.reconnectTimeoutId);
242
- this.reconnectTimeoutId = null;
243
- }
244
-
242
+ this.clearReconnectTimeout();
245
243
  this.cleanupExistingConnection();
246
244
 
247
245
  if (!clean) {
@@ -253,29 +251,61 @@ class Node {
253
251
  this.aqua.emit("nodeDestroy", this);
254
252
  this.info = null;
255
253
  }
256
-
254
+
255
+ async getStats() {
256
+ if (this.connected && this.stats) {
257
+ return this.stats;
258
+ }
259
+
260
+ try {
261
+ const newStats = await this.rest.getStats();
262
+ 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
+ }
281
+ }
282
+ return this.stats;
283
+ } catch (err) {
284
+ this.emitError(`Failed to fetch node stats: ${err.message}`);
285
+ return this.stats;
286
+ }
287
+ }
288
+
257
289
  _updateStats(payload) {
258
290
  if (!payload) return;
259
-
260
- const { players, playingPlayers, uptime, memory, cpu, frameStats, ping } = payload;
261
-
262
- this.stats.players = players;
263
- this.stats.playingPlayers = playingPlayers;
264
- this.stats.uptime = uptime;
265
- this.stats.ping = ping ?? this.stats.ping;
266
-
267
- if (memory) {
268
- Object.assign(this.stats.memory, memory);
291
+
292
+ this.stats.players = payload.players;
293
+ this.stats.playingPlayers = payload.playingPlayers;
294
+ this.stats.uptime = payload.uptime;
295
+ this.stats.ping = payload.ping;
296
+
297
+ if (payload.memory) {
298
+ Object.assign(this.stats.memory, payload.memory);
269
299
  this._calculateMemoryPercentages();
270
300
  }
271
301
 
272
- if (cpu) {
273
- Object.assign(this.stats.cpu, cpu);
302
+ if (payload.cpu) {
303
+ Object.assign(this.stats.cpu, payload.cpu);
274
304
  this._calculateCpuPercentages();
275
305
  }
276
306
 
277
- if (frameStats) {
278
- Object.assign(this.stats.frameStats, frameStats);
307
+ if (payload.frameStats) {
308
+ Object.assign(this.stats.frameStats, payload.frameStats);
279
309
  }
280
310
  }
281
311
 
@@ -294,16 +324,16 @@ class Node {
294
324
  }
295
325
  }
296
326
 
297
- _handleReadyOp({ sessionId }) {
298
- if (!sessionId) {
327
+ _handleReadyOp(payload) {
328
+ if (!payload.sessionId) {
299
329
  this.emitError("Ready payload missing sessionId");
300
330
  return;
301
331
  }
302
332
 
303
- this.sessionId = sessionId;
304
- this.rest.setSessionId(sessionId);
333
+ this.sessionId = payload.sessionId;
334
+ this.rest.setSessionId(payload.sessionId);
305
335
  this._headers = this._constructHeaders();
306
- this.aqua.emit("nodeReady", this);
336
+ this.aqua.emit("nodeConnect", this);
307
337
  }
308
338
 
309
339
  async resumePlayers() {
@@ -314,7 +344,6 @@ class Node {
314
344
  this.emitError(`Failed to resume session: ${err.message}`);
315
345
  }
316
346
  }
317
-
318
347
  emitError(error) {
319
348
  const errorObj = error instanceof Error ? error : new Error(error);
320
349
  console.error(`[Aqua] [${this.name}] Error:`, errorObj);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",