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.
@@ -22,56 +22,58 @@ const EVENT_HANDLERS = Object.freeze({
22
22
  LyricsNotFoundEvent: "lyricsNotFound"
23
23
  });
24
24
 
25
-
25
+ // Use Sets for O(1) lookups
26
26
  const VALID_MODES = new Set(Object.values(LOOP_MODES));
27
27
  const FAILURE_REASONS = new Set(["LOAD_FAILED", "CLEANUP"]);
28
- const RECONNECT_CODES = new Set([4015, 4009, 4006, 1000]);
28
+ const RECONNECT_CODES = new Set([4015, 4009, 4006]);
29
29
  const FAIL_LOAD_TYPES = new Set(["error", "empty", "LOAD_FAILED", "NO_MATCHES"]);
30
30
 
31
- class UpdateBatcher {
31
+ class OptimizedUpdateBatcher {
32
32
  constructor(player) {
33
33
  this.player = player;
34
- this.pendingUpdates = {};
35
- this.timeout = null;
36
- this.hasUpdates = false;
34
+ this.updates = Object.create(null); // Faster than {}
35
+ this.timeoutId = 0;
36
+ this.hasPending = false;
37
37
  }
38
38
 
39
39
  batch(data, immediate = false) {
40
- Object.assign(this.pendingUpdates, data);
41
- this.hasUpdates = true;
40
+ // Merge updates efficiently
41
+ for (const key in data) {
42
+ this.updates[key] = data[key];
43
+ }
44
+ this.hasPending = true;
42
45
 
43
- if (this.timeout) {
44
- clearTimeout(this.timeout);
45
- this.timeout = null;
46
+ if (this.timeoutId) {
47
+ clearTimeout(this.timeoutId);
48
+ this.timeoutId = 0;
46
49
  }
47
50
 
48
51
  if (immediate || data.track) {
49
- const updates = this.pendingUpdates;
50
- this.pendingUpdates = {};
51
- this.hasUpdates = false;
52
- return this.player.updatePlayer(updates);
52
+ return this._flush();
53
53
  }
54
54
 
55
- this.timeout = setTimeout(() => {
56
- if (this.hasUpdates) {
57
- const updates = this.pendingUpdates;
58
- this.pendingUpdates = {};
59
- this.hasUpdates = false;
60
- this.player.updatePlayer(updates);
61
- }
62
- this.timeout = null;
63
- }, 32);
64
-
55
+ this.timeoutId = setTimeout(() => this._flush(), 32);
65
56
  return Promise.resolve();
66
57
  }
67
58
 
59
+ _flush() {
60
+ if (!this.hasPending) return Promise.resolve();
61
+
62
+ const updates = this.updates;
63
+ this.updates = Object.create(null);
64
+ this.hasPending = false;
65
+ this.timeoutId = 0;
66
+
67
+ return this.player.updatePlayer(updates);
68
+ }
69
+
68
70
  destroy() {
69
- if (this.timeout) {
70
- clearTimeout(this.timeout);
71
- this.timeout = null;
71
+ if (this.timeoutId) {
72
+ clearTimeout(this.timeoutId);
73
+ this.timeoutId = 0;
72
74
  }
73
- this.pendingUpdates = {};
74
- this.hasUpdates = false;
75
+ this.updates = Object.create(null);
76
+ this.hasPending = false;
75
77
  }
76
78
  }
77
79
 
@@ -82,74 +84,87 @@ class Player extends EventEmitter {
82
84
 
83
85
  constructor(aqua, nodes, options = {}) {
84
86
  super();
87
+
88
+ // Core references
85
89
  this.aqua = aqua;
86
90
  this.nodes = nodes;
87
91
  this.guildId = options.guildId;
88
92
  this.textChannel = options.textChannel;
89
93
  this.voiceChannel = options.voiceChannel;
90
94
 
95
+ // Initialize components
91
96
  this.connection = new Connection(this);
92
97
  this.filters = new Filters(this);
93
98
  this.queue = new Queue();
94
99
 
100
+ // Optimized volume clamping
95
101
  const vol = options.defaultVolume ?? 100;
96
- this.volume = vol < 0 ? 0 : vol > 200 ? 200 : vol;
102
+ this.volume = Math.max(0, Math.min(200, vol));
97
103
 
104
+ // Direct assignment with validation
98
105
  this.loop = VALID_MODES.has(options.loop) ? options.loop : LOOP_MODES.NONE;
99
- this.shouldDeleteMessage = Boolean(this.aqua.options.shouldDeleteMessage);
100
- this.leaveOnEnd = Boolean(this.aqua.options.leaveOnEnd);
106
+ this.shouldDeleteMessage = !!this.aqua.options.shouldDeleteMessage;
107
+ this.leaveOnEnd = !!this.aqua.options.leaveOnEnd;
101
108
 
109
+ // Circular buffer for previous tracks (more memory efficient)
102
110
  this.previousTracks = new Array(50);
103
- this.previousTracksIndex = 0;
104
- this.previousTracksCount = 0;
111
+ this.previousIndex = 0;
112
+ this.previousCount = 0;
105
113
 
114
+ // State flags - grouped for better cache locality
106
115
  this.playing = false;
107
116
  this.paused = false;
108
117
  this.connected = false;
118
+ this.isAutoplayEnabled = false;
119
+ this.isAutoplay = false;
120
+
121
+ // Track state
109
122
  this.current = null;
110
123
  this.position = 0;
111
124
  this.timestamp = 0;
112
125
  this.ping = 0;
113
126
  this.nowPlayingMessage = null;
114
- this.isAutoplayEnabled = false;
115
- this.isAutoplay = false;
116
127
 
117
- this._updateBatcher = new UpdateBatcher(this);
128
+ // Optimized components
129
+ this._updateBatcher = new OptimizedUpdateBatcher(this);
118
130
  this._dataStore = new Map();
119
131
 
120
- this._handlePlayerUpdate = this._handlePlayerUpdate.bind(this);
121
- this._handleEvent = this._handleEvent.bind(this);
132
+ // Bind methods once
133
+ this._boundPlayerUpdate = this._handlePlayerUpdate.bind(this);
134
+ this._boundEvent = this._handleEvent.bind(this);
122
135
 
123
- this.on("playerUpdate", this._handlePlayerUpdate);
124
- this.on("event", this._handleEvent);
136
+ this.on("playerUpdate", this._boundPlayerUpdate);
137
+ this.on("event", this._boundEvent);
125
138
  }
126
139
 
127
140
  _handlePlayerUpdate(packet) {
128
- this.position = packet.state.position;
129
- this.connected = packet.state.connected;
130
- this.ping = packet.state.ping;
131
- this.timestamp = packet.state.time;
141
+ const state = packet.state;
142
+ this.position = state.position;
143
+ this.connected = state.connected;
144
+ this.ping = state.ping;
145
+ this.timestamp = state.time;
132
146
  this.aqua.emit("playerUpdate", this, packet);
133
147
  }
134
148
 
135
149
  async _handleEvent(payload) {
136
- try {
137
- const handlerName = EVENT_HANDLERS[payload.type];
150
+ const handlerName = EVENT_HANDLERS[payload.type];
138
151
 
139
- console.log(`Event: ${payload.type} for Player ${handlerName}`);
140
- if (handlerName && typeof this[handlerName] === "function") {
141
- await this[handlerName](this, this.current, payload);
142
- } else {
143
- this.aqua.emit("nodeError", this, new Error(`Node encountered an unknown event: '${payload.type}'`));
144
- }
152
+ if (!handlerName || typeof this[handlerName] !== "function") {
153
+ this.aqua.emit("nodeError", this, new Error(`Unknown event: ${payload.type}`));
154
+ return;
155
+ }
156
+
157
+ try {
158
+ await this[handlerName](this, this.current, payload);
145
159
  } catch (error) {
146
- console.error(`Error handling event ${payload.type}:`, error);
147
160
  this.aqua.emit("error", error);
148
161
  }
149
162
  }
150
163
 
151
164
  get previous() {
152
- return this.previousTracksCount ? this.previousTracks[(this.previousTracksIndex - 1 + 50) % 50] : null;
165
+ return this.previousCount > 0
166
+ ? this.previousTracks[(this.previousIndex - 1 + 50) % 50]
167
+ : null;
153
168
  }
154
169
 
155
170
  get currenttrack() {
@@ -161,19 +176,13 @@ class Player extends EventEmitter {
161
176
  }
162
177
 
163
178
  async autoplay(player) {
164
- if (!player) throw new Error("Player is undefined. const player = aqua.players.get(guildId);");
165
- if (!this.isAutoplayEnabled) {
166
- this.aqua.emit("debug", this.guildId, "Autoplay is disabled.");
167
- return this;
168
- }
179
+ if (!this.isAutoplayEnabled || !this.previous) return this;
169
180
 
170
181
  this.isAutoplay = true;
171
- if (!this.previous) return this;
182
+ const prevInfo = this.previous.info;
183
+ const { sourceName, identifier, uri, requester } = prevInfo;
172
184
 
173
185
  try {
174
- const { sourceName, identifier, uri, requester } = this.previous.info;
175
- this.aqua.emit("debug", this.guildId, `Attempting autoplay for ${sourceName}`);
176
-
177
186
  let query, source;
178
187
 
179
188
  switch (sourceName) {
@@ -205,7 +214,10 @@ class Player extends EventEmitter {
205
214
 
206
215
  const tracks = response.tracks;
207
216
  const track = tracks[Math.floor(Math.random() * tracks.length)];
208
- if (!track?.info?.title) throw new Error("Invalid track object: missing title or info.");
217
+
218
+ if (!track?.info?.title) {
219
+ throw new Error("Invalid track object");
220
+ }
209
221
 
210
222
  track.requester = this.previous.requester || { id: "Unknown" };
211
223
  this.queue.push(track);
@@ -213,14 +225,12 @@ class Player extends EventEmitter {
213
225
 
214
226
  return this;
215
227
  } catch (error) {
216
- console.error("Autoplay error:", error);
217
228
  return this.stop();
218
229
  }
219
230
  }
220
231
 
221
232
  setAutoplay(enabled) {
222
- this.isAutoplayEnabled = Boolean(enabled);
223
- this.aqua.emit("debug", this.guildId, `Autoplay has been ${enabled ? "enabled" : "disabled"}.`);
233
+ this.isAutoplayEnabled = !!enabled;
224
234
  return this;
225
235
  }
226
236
 
@@ -232,36 +242,34 @@ class Player extends EventEmitter {
232
242
  this.playing = true;
233
243
  this.position = 0;
234
244
 
235
- this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
236
245
  return this.batchUpdatePlayer({ track: { encoded: this.current.track } }, true);
237
246
  }
238
247
 
239
248
  connect(options = this) {
240
249
  const { guildId, voiceChannel, deaf = true, mute = false } = options;
250
+
241
251
  this.deaf = deaf;
242
252
  this.mute = mute;
253
+ this.connected = true;
254
+
243
255
  this.send({
244
256
  guild_id: guildId,
245
257
  channel_id: voiceChannel,
246
258
  self_deaf: deaf,
247
259
  self_mute: mute,
248
260
  });
249
- this.connected = true;
250
- this.aqua.emit("debug", guildId, `Player connected to voice channel: ${voiceChannel}.`);
261
+
251
262
  return this;
252
263
  }
253
264
 
254
265
  destroy() {
255
266
  if (!this.connected) return this;
256
267
 
257
- const voiceChannelId = this.voiceChannel ? this.voiceChannel.id || this.voiceChannel : null;
258
268
  this._updateBatcher.destroy();
259
269
 
260
270
  this.send({ guild_id: this.guildId, channel_id: null });
261
- this._lastVoiceChannel = voiceChannelId;
262
- this.voiceChannel = null;
263
271
  this.connected = false;
264
- this.send({ guild_id: this.guildId, channel_id: null });
272
+ this.voiceChannel = null;
265
273
 
266
274
  if (this.nowPlayingMessage) {
267
275
  this.nowPlayingMessage.delete().catch(() => { });
@@ -276,15 +284,17 @@ class Player extends EventEmitter {
276
284
  this.nodes.rest.destroyPlayer(this.guildId);
277
285
  } catch (error) {
278
286
  if (!error.message.includes('ECONNREFUSED')) {
279
- console.error('Error destroying player on node:', error);
287
+ console.error('Error destroying player:', error);
280
288
  }
281
289
  }
282
290
  }
283
291
 
284
- this.previousTracksCount = 0;
292
+ // Clean up efficiently
293
+ this.previousCount = 0;
285
294
  this._dataStore.clear();
286
295
  this.removeAllListeners();
287
296
 
297
+ // Nullify references
288
298
  this.queue = null;
289
299
  this.previousTracks = null;
290
300
  this.connection = null;
@@ -301,20 +311,16 @@ class Player extends EventEmitter {
301
311
  }
302
312
 
303
313
  async getLyrics(options = {}) {
304
- const { query = null, useCurrentTrack = true, skipTrackSource = false } = options;
314
+ const { query, useCurrentTrack = true, skipTrackSource = false } = options;
305
315
 
306
316
  if (query) {
307
- this.aqua.emit("debug", `[Aqua/Player] Searching lyrics for query: "${query}"`);
308
317
  return this.nodes.rest.getLyrics({
309
- track: {
310
- info: { title: query }
311
- },
318
+ track: { info: { title: query } },
312
319
  skipTrackSource
313
320
  });
314
321
  }
315
322
 
316
323
  if (useCurrentTrack && this.playing && this.current?.info) {
317
- this.aqua.emit("debug", `[Aqua/Player] Getting lyrics for current track: "${this.current.info.title}"`);
318
324
  return this.nodes.rest.getLyrics({
319
325
  track: {
320
326
  info: this.current.info,
@@ -325,25 +331,24 @@ class Player extends EventEmitter {
325
331
  });
326
332
  }
327
333
 
328
- this.aqua.emit("debug", `[Aqua/Player] getLyrics called but no query was provided and no track is playing.`);
329
334
  return null;
330
335
  }
331
336
 
332
- async subscribeLiveLyrics() {
337
+ subscribeLiveLyrics() {
333
338
  return this.nodes.rest.subscribeLiveLyrics(this.guildId, false);
334
339
  }
335
340
 
336
- async unsubscribeLiveLyrics() {
341
+ unsubscribeLiveLyrics() {
337
342
  return this.nodes.rest.unsubscribeLiveLyrics(this.guildId);
338
343
  }
339
344
 
340
345
  seek(position) {
341
346
  if (!this.playing) return this;
342
- if (position < 0) position = 0;
343
- if (this.current?.info?.length && position > this.current.info.length) {
344
- position = this.current.info.length;
345
- }
346
- this.position = position;
347
+
348
+ // Clamp position efficiently
349
+ const maxPos = this.current?.info?.length;
350
+ this.position = Math.max(0, maxPos ? Math.min(position, maxPos) : position);
351
+
347
352
  this.batchUpdatePlayer({ position: this.position });
348
353
  return this;
349
354
  }
@@ -357,14 +362,18 @@ class Player extends EventEmitter {
357
362
  }
358
363
 
359
364
  setVolume(volume) {
360
- if (volume < 0 || volume > 200) throw new Error("Volume must be between 0 and 200.");
361
- this.volume = volume;
362
- this.batchUpdatePlayer({ volume });
365
+ const vol = Math.max(0, Math.min(200, volume));
366
+ if (this.volume === vol) return this;
367
+
368
+ this.volume = vol;
369
+ this.batchUpdatePlayer({ volume: vol });
363
370
  return this;
364
371
  }
365
372
 
366
373
  setLoop(mode) {
367
- if (!VALID_MODES.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
374
+ if (!VALID_MODES.has(mode)) {
375
+ throw new Error("Invalid loop mode");
376
+ }
368
377
  this.loop = mode;
369
378
  this.batchUpdatePlayer({ loop: mode });
370
379
  return this;
@@ -377,13 +386,16 @@ class Player extends EventEmitter {
377
386
  }
378
387
 
379
388
  setVoiceChannel(channel) {
380
- if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
381
- if (this.connected && channel === this.voiceChannel) throw new ReferenceError(`Player already connected to ${channel}.`);
389
+ if (!channel) throw new TypeError("Channel required");
390
+ if (this.connected && channel === this.voiceChannel) {
391
+ throw new ReferenceError(`Already connected to ${channel}`);
392
+ }
393
+
382
394
  this.voiceChannel = channel;
383
395
  this.connect({
384
396
  deaf: this.deaf,
385
397
  guildId: this.guildId,
386
- voiceChannel: this.voiceChannel,
398
+ voiceChannel: channel,
387
399
  textChannel: this.textChannel,
388
400
  mute: this.mute,
389
401
  });
@@ -393,41 +405,27 @@ class Player extends EventEmitter {
393
405
  disconnect() {
394
406
  if (!this.connected) return this;
395
407
  this.connected = false;
396
- this.send({ guild_id: this.guildId, channel_id: null });
397
408
  this.voiceChannel = null;
398
- this.aqua.emit("debug", this.guildId, "Player disconnected.");
409
+ this.send({ guild_id: this.guildId, channel_id: null });
399
410
  return this;
400
411
  }
401
412
 
413
+ // Optimized Fisher-Yates shuffle
402
414
  shuffle() {
403
415
  const queue = this.queue;
404
416
  const len = queue.length;
405
417
 
406
418
  if (len <= 1) return this;
407
419
 
408
- if (len < 200) {
409
- for (let i = len - 1; i > 0; i--) {
410
- const j = Math.floor(Math.random() * (i + 1));
411
- [queue[i], queue[j]] = [queue[j], queue[i]];
412
- }
413
- } else {
414
- this._shuffleAsync(queue, len - 1);
415
- }
416
-
417
- return this;
418
- }
419
-
420
- _shuffleAsync(queue, i, chunkSize = 100) {
421
- const end = Math.max(0, i - chunkSize);
422
-
423
- for (; i > end; i--) {
420
+ // Inline shuffle for better performance
421
+ for (let i = len - 1; i > 0; i--) {
424
422
  const j = Math.floor(Math.random() * (i + 1));
425
- [queue[i], queue[j]] = [queue[j], queue[i]];
423
+ const temp = queue[i];
424
+ queue[i] = queue[j];
425
+ queue[j] = temp;
426
426
  }
427
427
 
428
- if (i > 0) {
429
- setImmediate(() => this._shuffleAsync(queue, i, chunkSize));
430
- }
428
+ return this;
431
429
  }
432
430
 
433
431
  getQueue() {
@@ -435,7 +433,7 @@ class Player extends EventEmitter {
435
433
  }
436
434
 
437
435
  replay() {
438
- return this.seek(-this.position);
436
+ return this.seek(0);
439
437
  }
440
438
 
441
439
  skip() {
@@ -451,25 +449,20 @@ class Player extends EventEmitter {
451
449
 
452
450
  async trackEnd(player, track, payload) {
453
451
  if (track) {
454
- this.previousTracks[this.previousTracksIndex] = track;
455
- this.previousTracksIndex = (this.previousTracksIndex + 1) % 50;
456
- if (this.previousTracksCount < 50) this.previousTracksCount++;
452
+ this.previousTracks[this.previousIndex] = track;
453
+ this.previousIndex = (this.previousIndex + 1) % 50;
454
+ if (this.previousCount < 50) this.previousCount++;
457
455
  }
458
456
 
459
457
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
460
- try {
461
- await this.nowPlayingMessage.delete();
462
- } catch (error) {
463
- // Ignore
464
- } finally {
465
- this.nowPlayingMessage = null;
466
- }
458
+ this.nowPlayingMessage.delete().catch(() => { });
459
+ this.nowPlayingMessage = null;
467
460
  }
468
461
 
469
462
  const reason = payload.reason;
470
463
  if (FAILURE_REASONS.has(reason)) {
471
- if (!player.queue.length) {
472
- this.previousTracksCount = 0;
464
+ if (this.queue.length === 0) {
465
+ this.previousCount = 0;
473
466
  this._dataStore.clear();
474
467
  this.aqua.emit("queueEnd", player);
475
468
  } else {
@@ -485,14 +478,13 @@ class Player extends EventEmitter {
485
478
  player.queue.push(track);
486
479
  }
487
480
 
488
-
489
481
  if (player.queue.isEmpty()) {
490
482
  if (this.isAutoplayEnabled) {
491
483
  await player.autoplay(player);
492
484
  } else {
493
485
  this.playing = false;
494
486
  if (this.leaveOnEnd) {
495
- this.previousTracksCount = 0;
487
+ this.previousCount = 0;
496
488
  this._dataStore.clear();
497
489
  this.destroy();
498
490
  }
@@ -515,59 +507,51 @@ class Player extends EventEmitter {
515
507
  }
516
508
 
517
509
  async socketClosed(player, track, payload) {
518
- const { code, guildId, reason } = payload || {};
510
+ const { code, guildId } = payload || {};
519
511
 
520
512
  if (RECONNECT_CODES.has(code)) {
521
513
  try {
522
- this.connected = false;
523
-
524
514
  const voiceChannelId = this.voiceChannel?.id || this.voiceChannel;
515
+ const textChannelId = this.textChannel?.id || this.textChannel;
516
+ const currentTrack = this.current;
525
517
 
526
518
  if (!voiceChannelId) {
527
- console.error(`Cannot reconnect: No voice channel available for guild ${guildId}`);
528
519
  this.aqua.emit("socketClosed", player, payload);
529
520
  return;
530
521
  }
531
522
 
532
- this.aqua.emit("debug", guildId, `Attempting to reconnect to voice channel: ${voiceChannelId}`);
533
523
 
534
- this.send({
535
- guild_id: guildId,
536
- channel_id: voiceChannelId,
537
- self_mute: this.mute || false,
538
- self_deaf: this.deaf || true
539
- });
524
+ if (!player.destroyed) {
525
+ await player.destroy();
526
+ this.aqua.emit("playerDestroy", player);
527
+ }
540
528
 
541
- setTimeout(() => {
542
- if (!this.connected) {
543
- console.error(`Reconnection failed for guild ${guildId} after timeout`);
544
- this.aqua.emit("socketClosed", player, payload);
545
- }
546
- }, 5000);
529
+ const newPlayer = await this.aqua.createConnection({
530
+ guildId,
531
+ voiceChannel: voiceChannelId,
532
+ textChannel: textChannelId,
533
+ deaf: this.deaf,
534
+ mute: this.mute,
535
+ defaultVolume: this.volume
536
+ });
547
537
 
548
- this.aqua.emit("debug", guildId, `Reconnection attempt sent for voice channel: ${voiceChannelId}`);
538
+ if (track) {
539
+ newPlayer.queue.add(track);
540
+ await newPlayer.play();
541
+ }
549
542
  return;
550
543
 
551
544
  } catch (error) {
552
- console.error(`Failed to reconnect socket for guild ${guildId}:`, error);
553
- this.connected = false;
545
+ console.error("Reconnection failed:", error);
546
+ this.aqua.emit("socketClosed", player, payload);
554
547
  }
548
+ return;
555
549
  }
556
550
 
557
- this.connected = false;
558
551
  this.aqua.emit("socketClosed", player, payload);
559
-
560
- if (this.playing && this.current && this.queue.length > 0) {
561
- try {
562
- this.aqua.emit("debug", guildId, "Attempting to resume playback after socket close");
563
- await this.play();
564
- } catch (error) {
565
- console.error(`Failed to resume playback after socket close for guild ${guildId}:`, error);
566
- this.stop();
567
- }
568
- }
569
552
  }
570
553
 
554
+
571
555
  async lyricsLine(player, track, payload) {
572
556
  this.aqua.emit("lyricsLine", player, track, payload);
573
557
  }
@@ -593,7 +577,7 @@ class Player extends EventEmitter {
593
577
  }
594
578
 
595
579
  clearData() {
596
- this.previousTracksCount = 0;
580
+ this.previousCount = 0;
597
581
  this._dataStore.clear();
598
582
  return this;
599
583
  }
@@ -603,7 +587,9 @@ class Player extends EventEmitter {
603
587
  }
604
588
 
605
589
  async cleanup() {
606
- if (!this.playing && !this.paused && this.queue.isEmpty()) this.destroy();
590
+ if (!this.playing && !this.paused && this.queue.isEmpty()) {
591
+ this.destroy();
592
+ }
607
593
  }
608
594
  }
609
595