aqualink 2.2.0 → 2.3.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.
@@ -8,11 +8,9 @@ class Node {
8
8
  static WS_OPEN = WebSocket.OPEN;
9
9
 
10
10
  constructor(aqua, connOptions, options = {}) {
11
- const host = connOptions.host || "localhost";
12
-
13
11
  this.aqua = aqua;
14
- this.name = connOptions.name || host;
15
- this.host = host;
12
+ this.host = connOptions.host || "localhost";
13
+ this.name = connOptions.name || this.host;
16
14
  this.port = connOptions.port || 2333;
17
15
  this.password = connOptions.password || "youshallnotpass";
18
16
  this.secure = !!connOptions.secure;
@@ -20,16 +18,23 @@ class Node {
20
18
  this.regions = connOptions.regions || [];
21
19
 
22
20
  this.wsUrl = `ws${this.secure ? "s" : ""}://${this.host}:${this.port}/v4/websocket`;
23
-
24
21
  this.rest = new Rest(aqua, this);
25
22
  this.resumeTimeout = options.resumeTimeout || 60;
26
23
  this.autoResume = !!options.autoResume;
27
24
  this.reconnectTimeout = options.reconnectTimeout || 2000;
28
25
  this.reconnectTries = options.reconnectTries || 3;
29
26
  this.infiniteReconnects = !!options.infiniteReconnects;
27
+
30
28
  this.connected = false;
31
29
  this.info = null;
30
+ this.ws = null;
31
+ this.reconnectAttempted = 0;
32
+ this.reconnectTimeoutId = null;
32
33
 
34
+ this.initializeStats();
35
+ }
36
+
37
+ initializeStats() {
33
38
  this.stats = {
34
39
  players: 0,
35
40
  playingPlayers: 0,
@@ -39,189 +44,211 @@ class Node {
39
44
  frameStats: { sent: 0, nulled: 0, deficit: 0 },
40
45
  ping: 0
41
46
  };
42
-
43
- let ws = null;
44
- let reconnectAttempted = 0;
45
- let reconnectTimeoutId = null;
46
-
47
- this._onOpen = () => {
48
- this.connected = true;
49
- reconnectAttempted = 0;
50
- if (aqua.listenerCount('debug') > 0) {
51
- aqua.emit("debug", this.name, `Connected to ${this.wsUrl}`);
52
- }
47
+ }
53
48
 
54
- this.rest.makeRequest("GET", "/v4/info")
55
- .then(info => {
56
- this.info = info;
57
- if (this.autoResume && this.sessionId) {
58
- return this.resumePlayers();
59
- }
60
- })
61
- .catch(err => {
62
- this.info = null;
63
- if (!aqua.bypassChecks?.nodeFetchInfo && aqua.listenerCount('error') > 0) {
64
- aqua.emit("error", this, `Failed to fetch node info: ${err.message}`);
65
- }
66
- });
49
+ _constructHeaders() {
50
+ const headers = {
51
+ Authorization: this.password,
52
+ "User-Id": this.aqua.clientId,
53
+ "Client-Name": `Aqua/${this.aqua.version}`
67
54
  };
68
55
 
69
- this._onError = (error) => {
70
- if (aqua.listenerCount('nodeError') > 0) {
71
- aqua.emit("nodeError", this, error);
72
- }
73
- };
56
+ if (this.sessionId) {
57
+ headers["Session-Id"] = this.sessionId;
58
+ }
74
59
 
75
- this._onMessage = (msg) => {
76
- let payload;
77
- try {
78
- payload = JSON.parse(msg);
79
- } catch {
80
- return;
81
- }
60
+ return headers;
61
+ }
82
62
 
83
- const op = payload?.op;
84
- if (!op) return;
63
+ _onOpen() {
64
+ this.connected = true;
65
+ this.reconnectAttempted = 0;
66
+ this.emitDebug(`Connected to ${this.wsUrl}`);
85
67
 
86
- switch (op) {
87
- case "stats":
88
- this._updateStats(payload);
89
- break;
90
- case "ready":
91
- this._handleReadyOp(payload);
92
- break;
93
- default:
94
- if (payload.guildId) {
95
- const player = aqua.players.get(payload.guildId);
96
- if (player) player.emit(op, payload);
97
- }
98
- }
99
- };
100
-
101
- this._onClose = (code, reason) => {
102
- this.connected = false;
103
-
104
- if (aqua.listenerCount('nodeDisconnect') > 0) {
105
- aqua.emit("nodeDisconnect", this, {
106
- code,
107
- reason: reason?.toString() || "No reason provided"
108
- });
109
- }
110
-
111
- clearTimeout(reconnectTimeoutId);
112
-
113
- if (this.infiniteReconnects) {
114
- if (aqua.listenerCount('nodeReconnect') > 0) {
115
- aqua.emit("nodeReconnect", this, "Infinite reconnects enabled, trying again in 10 seconds");
68
+ this.rest.makeRequest("GET", "/v4/info")
69
+ .then(info => {
70
+ this.info = info;
71
+ if (this.autoResume && this.sessionId) {
72
+ return this.resumePlayers();
116
73
  }
117
- reconnectTimeoutId = setTimeout(() => this.connect(), 10000);
118
- return;
119
- }
74
+ })
75
+ .catch(err => {
76
+ this.info = null;
77
+ if (!this.aqua.bypassChecks?.nodeFetchInfo) {
78
+ this.emitError(`Failed to fetch node info: ${err.message}`);
79
+ }
80
+ });
81
+ }
82
+
83
+ _onError(error) {
84
+ this.aqua.emit("nodeError", this, error);
85
+ }
86
+
87
+ _onMessage(msg) {
88
+ let payload;
89
+ try {
90
+ payload = JSON.parse(msg);
91
+ } catch {
92
+ return;
93
+ }
120
94
 
121
- if (reconnectAttempted >= this.reconnectTries) {
122
- if (aqua.listenerCount('nodeError') > 0) {
123
- aqua.emit("nodeError", this,
124
- new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
95
+ const op = payload?.op;
96
+ if (!op) return;
97
+
98
+ switch (op) {
99
+ case "stats":
100
+ this._updateStats(payload);
101
+ break;
102
+ case "ready":
103
+ this._handleReadyOp(payload);
104
+ break;
105
+ default:
106
+ if (payload.guildId) {
107
+ const player = this.aqua.players.get(payload.guildId);
108
+ if (player) player.emit(op, payload);
125
109
  }
126
- this.destroy(true);
127
- return;
128
- }
110
+ }
111
+ }
112
+
113
+ _onClose(code, reason) {
114
+ this.connected = false;
115
+
116
+ this.aqua.emit("nodeDisconnect", this, {
117
+ code,
118
+ reason: reason?.toString() || "No reason provided"
119
+ });
120
+
121
+ this.scheduleReconnect(code);
122
+ }
123
+
124
+ scheduleReconnect(code) {
125
+ this.clearReconnectTimeout();
126
+
127
+ if (code === 1000) {
128
+ return;
129
+ }
130
+
131
+ if (this.infiniteReconnects) {
132
+ this.aqua.emit("nodeReconnect", this, "Infinite reconnects enabled, trying again in 10 seconds");
133
+ this.reconnectTimeoutId = setTimeout(() => this.connect(), 10000);
134
+ return;
135
+ }
129
136
 
130
- const baseBackoff = this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, reconnectAttempted);
131
- const jitter = Math.random() * Math.min(2000, baseBackoff * 0.2);
132
- const backoffTime = Math.min(baseBackoff + jitter, Node.MAX_BACKOFF);
137
+ if (this.reconnectAttempted >= this.reconnectTries) {
138
+ this.emitError(new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
139
+ this.destroy(true);
140
+ return;
141
+ }
133
142
 
134
- reconnectAttempted++;
135
-
136
- if (aqua.listenerCount('nodeReconnect') > 0) {
137
- aqua.emit("nodeReconnect", this, {
138
- attempt: reconnectAttempted,
139
- backoffTime
140
- });
141
- }
142
-
143
- reconnectTimeoutId = setTimeout(() => this.connect(), backoffTime);
144
- };
143
+ const backoffTime = this.calculateBackoff();
144
+ this.reconnectAttempted++;
145
145
 
146
- this.connect = async () => {
147
- ws = new WebSocket(this.wsUrl, {
148
- headers: this._constructHeaders(),
149
- perMessageDeflate: false,
150
- });
151
-
152
- ws.once("open", this._onOpen);
153
- ws.once("error", this._onError);
154
- ws.on("message", this._onMessage);
155
- ws.once("close", this._onClose);
156
- };
146
+ this.aqua.emit("nodeReconnect", this, {
147
+ attempt: this.reconnectAttempted,
148
+ backoffTime
149
+ });
157
150
 
158
- this.destroy = (clean = false) => {
159
- clearTimeout(reconnectTimeoutId);
151
+ this.reconnectTimeoutId = setTimeout(() => this.connect(), backoffTime);
152
+ }
160
153
 
161
- if (ws) {
162
- ws.removeAllListeners();
163
- if (ws.readyState === Node.WS_OPEN) {
164
- ws.close();
165
- }
166
- ws = null;
167
- }
154
+ calculateBackoff() {
155
+ const baseBackoff = this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, this.reconnectAttempted);
156
+ const jitter = Math.random() * Math.min(2000, baseBackoff * 0.2);
157
+ return Math.min(baseBackoff + jitter, Node.MAX_BACKOFF);
158
+ }
168
159
 
169
- if (clean) {
170
- if (aqua.listenerCount('nodeDestroy') > 0) {
171
- aqua.emit("nodeDestroy", this);
172
- }
173
- aqua.nodes.delete(this.name);
174
- return;
175
- }
160
+ clearReconnectTimeout() {
161
+ if (this.reconnectTimeoutId) {
162
+ clearTimeout(this.reconnectTimeoutId);
163
+ this.reconnectTimeoutId = null;
164
+ }
165
+ }
176
166
 
177
- if (this.connected) {
178
- for (const player of aqua.players.values()) {
179
- if (player.node === this) {
180
- player.destroy();
181
- }
182
- }
183
- }
167
+ async connect() {
168
+ this.cleanupExistingConnection();
169
+
170
+ this.ws = new WebSocket(this.wsUrl, {
171
+ headers: this._constructHeaders(),
172
+ perMessageDeflate: false,
173
+ });
174
+
175
+ this.ws.once("open", this._onOpen.bind(this));
176
+ this.ws.once("error", this._onError.bind(this));
177
+ this.ws.on("message", this._onMessage.bind(this));
178
+ this.ws.once("close", this._onClose.bind(this));
179
+ }
184
180
 
185
- this.connected = false;
186
- aqua.nodes.delete(this.name);
181
+ cleanupExistingConnection() {
182
+ if (this.ws) {
183
+ this.ws.removeAllListeners();
187
184
 
188
- if (aqua.listenerCount('nodeDestroy') > 0) {
189
- aqua.emit("nodeDestroy", this);
185
+ if (this.ws.readyState === Node.WS_OPEN) {
186
+ try {
187
+ this.ws.close();
188
+ } catch (err) {
189
+ this.emitDebug(`Error closing WebSocket: ${err.message}`);
190
+ }
190
191
  }
191
192
 
192
- this.info = null;
193
- };
193
+ this.ws = null;
194
+ }
194
195
  }
196
+
197
+ destroy(clean = false) {
198
+ this.clearReconnectTimeout();
199
+ this.cleanupExistingConnection();
195
200
 
196
- _constructHeaders() {
197
- const headers = {
198
- Authorization: this.password,
199
- "User-Id": this.aqua.clientId,
200
- "Client-Name": `Aqua/${this.aqua.version}`
201
- };
202
-
203
- if (this.sessionId) {
204
- headers["Session-Id"] = this.sessionId;
201
+ if (clean) {
202
+ this.aqua.emit("nodeDestroy", this);
203
+ this.aqua.nodes.delete(this.name);
204
+ return;
205
205
  }
206
-
207
- return headers;
206
+
207
+ if (this.connected) {
208
+ for (const player of this.aqua.players.values()) {
209
+ if (player.nodes === this) {
210
+ player.destroy();
211
+ }
212
+ }
213
+ }
214
+
215
+ this.connected = false;
216
+ this.aqua.nodes.delete(this.name);
217
+ this.aqua.emit("nodeDestroy", this);
218
+ this.info = null;
208
219
  }
209
220
 
210
221
  async getStats() {
211
- const newStats = await this.rest.getStats();
212
- Object.assign(this.stats, newStats);
213
- return this.stats;
222
+ try {
223
+ const newStats = await this.rest.getStats();
224
+ Object.assign(this.stats, newStats);
225
+ return this.stats;
226
+ } catch (err) {
227
+ this.emitError(`Failed to fetch node stats: ${err.message}`);
228
+ return this.stats;
229
+ }
214
230
  }
215
231
 
216
232
  _updateStats(payload) {
217
233
  if (!payload) return;
218
234
 
235
+ this._updateBasicStats(payload);
236
+
237
+ this._updateMemoryStats(payload.memory);
238
+
239
+ this._updateCpuStats(payload.cpu);
240
+
241
+ this._updateFrameStats(payload.frameStats);
242
+ }
243
+
244
+ _updateBasicStats(payload) {
219
245
  this.stats.players = payload.players || this.stats.players;
220
246
  this.stats.playingPlayers = payload.playingPlayers || this.stats.playingPlayers;
221
247
  this.stats.uptime = payload.uptime || this.stats.uptime;
222
248
  this.stats.ping = payload.ping || this.stats.ping;
223
-
224
- const memory = payload.memory || {};
249
+ }
250
+
251
+ _updateMemoryStats(memory = {}) {
225
252
  const allocated = memory.allocated || this.stats.memory.allocated;
226
253
  const free = memory.free || this.stats.memory.free;
227
254
  const used = memory.used || this.stats.memory.used;
@@ -235,8 +262,9 @@ class Node {
235
262
  this.stats.memory.freePercentage = (free / allocated) * 100;
236
263
  this.stats.memory.usedPercentage = (used / allocated) * 100;
237
264
  }
238
-
239
- const cpu = payload.cpu || {};
265
+ }
266
+
267
+ _updateCpuStats(cpu = {}) {
240
268
  const cores = cpu.cores || this.stats.cpu.cores;
241
269
 
242
270
  this.stats.cpu.cores = cores;
@@ -246,9 +274,10 @@ class Node {
246
274
  if (cores) {
247
275
  this.stats.cpu.lavalinkLoadPercentage = (cpu.lavalinkLoad / cores) * 100;
248
276
  }
249
-
250
- const frameStats = payload.frameStats || {};
251
-
277
+ }
278
+
279
+ _updateFrameStats(frameStats = {}) {
280
+ if (!frameStats) return;
252
281
  this.stats.frameStats.sent = frameStats.sent || this.stats.frameStats.sent;
253
282
  this.stats.frameStats.nulled = frameStats.nulled || this.stats.frameStats.nulled;
254
283
  this.stats.frameStats.deficit = frameStats.deficit || this.stats.frameStats.deficit;
@@ -256,18 +285,13 @@ class Node {
256
285
 
257
286
  _handleReadyOp(payload) {
258
287
  if (!payload.sessionId) {
259
- if (this.aqua.listenerCount('error') > 0) {
260
- this.aqua.emit("error", this, "Ready payload missing sessionId");
261
- }
288
+ this.emitError("Ready payload missing sessionId");
262
289
  return;
263
290
  }
264
291
 
265
292
  this.sessionId = payload.sessionId;
266
293
  this.rest.setSessionId(payload.sessionId);
267
-
268
- if (this.aqua.listenerCount('nodeConnect') > 0) {
269
- this.aqua.emit("nodeConnect", this);
270
- }
294
+ this.aqua.emit("nodeConnect", this);
271
295
  }
272
296
 
273
297
  async resumePlayers() {
@@ -277,13 +301,25 @@ class Node {
277
301
  timeout: this.resumeTimeout
278
302
  });
279
303
 
280
- if (this.aqua.listenerCount('debug') > 0) {
281
- this.aqua.emit("debug", this.name, `Successfully resumed session ${this.sessionId}`);
282
- }
304
+ this.emitDebug(`Successfully resumed session ${this.sessionId}`);
283
305
  } catch (err) {
284
- if (this.aqua.listenerCount('error') > 0) {
285
- this.aqua.emit("error", this, `Failed to resume session: ${err.message}`);
286
- }
306
+ this.emitError(`Failed to resume session: ${err.message}`);
307
+ }
308
+ }
309
+
310
+ emitDebug(message) {
311
+ if (this.aqua.listenerCount('debug') > 0) {
312
+ this.aqua.emit("debug", this.name, message);
313
+ }
314
+ }
315
+
316
+ emitError(error) {
317
+ const errorObj = error instanceof Error ? error : new Error(error);
318
+
319
+ console.error(`[Aqua] [${this.name}] Error:`, errorObj);
320
+
321
+ if (this.aqua.listenerCount('error') > 0) {
322
+ this.aqua.emit("error", this, errorObj);
287
323
  }
288
324
  }
289
325
  }
@@ -40,8 +40,10 @@ class Player extends EventEmitter {
40
40
  this.loop = Player.validModes.has(options.loop) ? options.loop : Player.LOOP_MODES.NONE;
41
41
 
42
42
  this.queue = new Queue();
43
- this.previousTracks = Array(50);
43
+ this.previousTracks = new Array(50);
44
+ this.previousTracksIndex = 0;
44
45
  this.previousTracksCount = 0;
46
+
45
47
  this.shouldDeleteMessage = Boolean(options.shouldDeleteMessage);
46
48
  this.leaveOnEnd = Boolean(options.leaveOnEnd);
47
49
 
@@ -56,15 +58,19 @@ class Player extends EventEmitter {
56
58
  this.isAutoplayEnabled = false;
57
59
  this.isAutoplay = false;
58
60
 
59
- this._handlePlayerUpdate = this._handlePlayerUpdate.bind(this);
60
- this._handleEvent = this._handleEvent.bind(this);
61
+ this._boundHandlers = {
62
+ playerUpdate: this._handlePlayerUpdate.bind(this),
63
+ event: this._handleEvent.bind(this)
64
+ };
65
+
66
+ this.on("playerUpdate", this._boundHandlers.playerUpdate);
67
+ this.on("event", this._boundHandlers.event);
61
68
 
62
- this.on("playerUpdate", this._handlePlayerUpdate);
63
- this.on("event", this._handleEvent);
69
+ this._dataStore = null;
64
70
  }
65
71
 
66
72
  get previous() {
67
- return this.previousTracksCount > 0 ? this.previousTracks[0] : null;
73
+ return this.previousTracksCount > 0 ? this.previousTracks[this.previousTracksIndex] : null;
68
74
  }
69
75
 
70
76
  get currenttrack() {
@@ -154,17 +160,11 @@ class Player extends EventEmitter {
154
160
  addToPreviousTrack(track) {
155
161
  if (!track) return;
156
162
 
163
+ this.previousTracks[this.previousTracksIndex] = track;
164
+ this.previousTracksIndex = (this.previousTracksIndex + 1) % 50;
165
+
157
166
  if (this.previousTracksCount < 50) {
158
- for (let i = this.previousTracksCount; i > 0; i--) {
159
- this.previousTracks[i] = this.previousTracks[i-1];
160
- }
161
- this.previousTracks[0] = track;
162
167
  this.previousTracksCount++;
163
- } else {
164
- for (let i = 49; i > 0; i--) {
165
- this.previousTracks[i] = this.previousTracks[i-1];
166
- }
167
- this.previousTracks[0] = track;
168
168
  }
169
169
  }
170
170
 
@@ -222,19 +222,34 @@ class Player extends EventEmitter {
222
222
 
223
223
  this.disconnect();
224
224
 
225
- if (this.nowPlayingMessage) {
226
- this.nowPlayingMessage.delete().catch(() => {});
227
- this.nowPlayingMessage = null;
228
- }
225
+ this._cleanupNowPlayingMessage();
229
226
 
230
227
  this.isAutoplay = false;
231
228
 
229
+ this.off("playerUpdate", this._boundHandlers.playerUpdate);
230
+ this.off("event", this._boundHandlers.event);
231
+
232
232
  this.aqua.destroyPlayer(this.guildId);
233
233
  this.nodes.rest.destroyPlayer(this.guildId);
234
+
234
235
  this.clearData();
235
236
  this.removeAllListeners();
237
+
238
+ this._boundHandlers = null;
239
+ this.queue = null;
240
+ this.previousTracks = null;
241
+ this.connection = null;
242
+ this.filters = null;
243
+
236
244
  return this;
237
245
  }
246
+
247
+ _cleanupNowPlayingMessage() {
248
+ if (this.nowPlayingMessage) {
249
+ this.nowPlayingMessage.delete().catch(() => {});
250
+ this.nowPlayingMessage = null;
251
+ }
252
+ }
238
253
 
239
254
  pause(paused) {
240
255
  if (this.paused === paused) return this;
@@ -312,6 +327,8 @@ class Player extends EventEmitter {
312
327
  }
313
328
 
314
329
  disconnect() {
330
+ if (!this.connected) return this;
331
+
315
332
  this.connected = false;
316
333
  this.send({ guild_id: this.guildId, channel_id: null });
317
334
  this.voiceChannel = null;
@@ -372,26 +389,34 @@ class Player extends EventEmitter {
372
389
  return;
373
390
  }
374
391
 
392
+ await this._handleTrackLooping(player, track);
393
+
394
+ if (player.queue.isEmpty()) {
395
+ await this._handleEmptyQueue(player);
396
+ } else {
397
+ this.aqua.emit("trackEnd", player, track, reason);
398
+ await player.play();
399
+ }
400
+ }
401
+
402
+ async _handleTrackLooping(player, track) {
375
403
  if (this.loop === Player.LOOP_MODES.TRACK) {
376
404
  player.queue.unshift(track);
377
405
  } else if (this.loop === Player.LOOP_MODES.QUEUE) {
378
406
  player.queue.push(track);
379
407
  }
380
-
381
- if (player.queue.isEmpty()) {
382
- if (this.isAutoplayEnabled) {
383
- await player.autoplay(player);
384
- } else {
385
- this.playing = false;
386
- if (this.leaveOnEnd) {
387
- this.clearData();
388
- this.cleanup();
389
- }
390
- this.aqua.emit("queueEnd", player);
391
- }
408
+ }
409
+
410
+ async _handleEmptyQueue(player) {
411
+ if (this.isAutoplayEnabled) {
412
+ await player.autoplay(player);
392
413
  } else {
393
- this.aqua.emit("trackEnd", player, track, reason);
394
- await player.play();
414
+ this.playing = false;
415
+ if (this.leaveOnEnd) {
416
+ this.clearData();
417
+ this.cleanup();
418
+ }
419
+ this.aqua.emit("queueEnd", player);
395
420
  }
396
421
  }
397
422