aqualink 2.19.1 → 2.20.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.
@@ -1,6 +1,6 @@
1
1
  const fs = require('node:fs')
2
2
  const readline = require('node:readline')
3
- const { EventEmitter } = require('tseep')
3
+ const { EventEmitter } = require('node:events')
4
4
  const { AqualinkEvents } = require('./AqualinkEvents')
5
5
  const Node = require('./Node')
6
6
  const Player = require('./Player')
@@ -28,6 +28,7 @@ const MAX_CACHE_SIZE = 20
28
28
  const MAX_FAILOVER_QUEUE = 50
29
29
  const MAX_REBUILD_LOCKS = 100
30
30
  const WRITE_BUFFER_SIZE = 100
31
+ const TRACE_BUFFER_SIZE = 3000
31
32
 
32
33
  const DEFAULT_OPTIONS = Object.freeze({
33
34
  shouldDeleteMessage: false,
@@ -39,6 +40,10 @@ const DEFAULT_OPTIONS = Object.freeze({
39
40
  infiniteReconnects: true,
40
41
  loadBalancer: 'leastLoad',
41
42
  useHttp2: false,
43
+ debugTrace: false,
44
+ traceMaxEntries: TRACE_BUFFER_SIZE,
45
+ traceSink: null,
46
+ autoRegionMigrate: false,
42
47
  failoverOptions: Object.freeze({
43
48
  enabled: true,
44
49
  maxRetries: 3,
@@ -121,10 +126,22 @@ class Aqua extends EventEmitter {
121
126
  this.restrictedDomains = merged.restrictedDomains || []
122
127
  this.allowedDomains = merged.allowedDomains || []
123
128
  this.loadBalancer = merged.loadBalancer
129
+ this.autoRegionMigrate = merged.autoRegionMigrate
124
130
  this.useHttp2 = merged.useHttp2
125
131
  this.maxQueueSave = merged.maxQueueSave
126
132
  this.maxTracksRestore = merged.maxTracksRestore
127
133
  this.send = merged.send || this._createDefaultSend()
134
+ this.debugTrace = !!merged.debugTrace
135
+ this.traceMaxEntries = Math.max(
136
+ 100,
137
+ Number(merged.traceMaxEntries) || TRACE_BUFFER_SIZE
138
+ )
139
+ this.traceSink =
140
+ typeof merged.traceSink === 'function' ? merged.traceSink : null
141
+ this._traceBuffer = new Array(this.traceMaxEntries)
142
+ this._traceBufferCount = 0
143
+ this._traceBufferIndex = 0
144
+ this._traceSeq = 0
128
145
 
129
146
  this._nodeStates = new Map()
130
147
  this._failoverQueue = new Map()
@@ -140,6 +157,49 @@ class Aqua extends EventEmitter {
140
157
  if (this.autoResume) this._bindEventHandlers()
141
158
  }
142
159
 
160
+ _trace(event, data = null) {
161
+ if (!this.debugTrace) return
162
+ const entry = {
163
+ seq: ++this._traceSeq,
164
+ at: Date.now(),
165
+ event,
166
+ data
167
+ }
168
+ if (this._traceBuffer.length !== this.traceMaxEntries) {
169
+ this._traceBuffer = new Array(this.traceMaxEntries)
170
+ this._traceBufferCount = 0
171
+ this._traceBufferIndex = 0
172
+ }
173
+ this._traceBuffer[this._traceBufferIndex] = entry
174
+ this._traceBufferIndex = (this._traceBufferIndex + 1) % this.traceMaxEntries
175
+ if (this._traceBufferCount < this.traceMaxEntries) this._traceBufferCount++
176
+ if (this.traceSink) _functions.safeCall(() => this.traceSink(entry))
177
+ if (this.listenerCount(AqualinkEvents.Debug) > 0) {
178
+ this.emit(AqualinkEvents.Debug, 'trace', JSON.stringify(entry))
179
+ }
180
+ }
181
+
182
+ getTrace(limit = 300) {
183
+ const max = Math.max(1, Number(limit) || 300)
184
+ const count = Math.min(max, this._traceBufferCount)
185
+ if (!count) return []
186
+ const out = new Array(count)
187
+ let start =
188
+ (this._traceBufferIndex - count + this.traceMaxEntries) %
189
+ this.traceMaxEntries
190
+ for (let i = 0; i < count; i++) {
191
+ out[i] = this._traceBuffer[start]
192
+ start = (start + 1) % this.traceMaxEntries
193
+ }
194
+ return out
195
+ }
196
+
197
+ clearTrace() {
198
+ this._traceBuffer.fill(undefined)
199
+ this._traceBufferCount = 0
200
+ this._traceBufferIndex = 0
201
+ }
202
+
143
203
  _createDefaultSend() {
144
204
  return (packet) => {
145
205
  const guildId = packet?.d?.guild_id
@@ -156,12 +216,13 @@ class Aqua extends EventEmitter {
156
216
 
157
217
  _bindEventHandlers() {
158
218
  this._eventHandlers = {
159
- onNodeConnect: async (node) => {
219
+ onNodeConnect: (node) => {
220
+ this._trace('node.connect', { node: node?.name || node?.host })
160
221
  this._invalidateCache()
161
- await this._rebuildBrokenPlayers(node)
162
222
  this._performCleanup()
163
223
  },
164
224
  onNodeDisconnect: (node) => {
225
+ this._trace('node.disconnect', { node: node?.name || node?.host })
165
226
  this._invalidateCache()
166
227
  queueMicrotask(() => {
167
228
  this._storeBrokenPlayers(node)
@@ -169,17 +230,27 @@ class Aqua extends EventEmitter {
169
230
  })
170
231
  },
171
232
  onNodeReady: (node, { resumed }) => {
172
- if (!resumed) return
173
- const batch = []
174
- for (const player of this.players.values()) {
175
- if (player.nodes === node && player.connection) batch.push(player)
233
+ this._trace('node.ready', {
234
+ node: node?.name || node?.host,
235
+ resumed: !!resumed,
236
+ players: this.players.size
237
+ })
238
+ if (resumed) {
239
+ const batch = []
240
+ for (const player of this.players.values()) {
241
+ if (player.nodes === node && player.connection) batch.push(player)
242
+ }
243
+ if (batch.length)
244
+ queueMicrotask(() =>
245
+ batch.forEach((p) => {
246
+ p.connection.resendVoiceUpdate()
247
+ })
248
+ )
249
+ return
176
250
  }
177
- if (batch.length)
178
- queueMicrotask(() =>
179
- batch.forEach((p) => {
180
- p.connection.resendVoiceUpdate()
181
- })
182
- )
251
+ queueMicrotask(() => {
252
+ this._rebuildBrokenPlayers(node).catch(_functions.noop)
253
+ })
183
254
  }
184
255
  }
185
256
  this.on(AqualinkEvents.NodeConnect, this._eventHandlers.onNodeConnect)
@@ -334,9 +405,8 @@ class Aqua extends EventEmitter {
334
405
  _destroyNode(id) {
335
406
  const node = this.nodeMap.get(id)
336
407
  if (!node) return
337
- _functions.safeCall(() => node.destroy())
408
+ _functions.safeCall(() => node.destroy(true))
338
409
  this._cleanupNode(id)
339
- this.emit(AqualinkEvents.NodeDestroy, node)
340
410
  }
341
411
 
342
412
  _cleanupNode(id) {
@@ -425,18 +495,7 @@ class Aqua extends EventEmitter {
425
495
  if (current && player?.queue?.add) {
426
496
  player.queue.add(current)
427
497
  await player.play()
428
- if (state.position > 0) {
429
- const seekOnce = (p) => {
430
- if (p.guildId === guildId) {
431
- this.off(AqualinkEvents.TrackStart, seekOnce)
432
- _functions.unrefTimeout(() => player.seek?.(state.position), 50)
433
- }
434
- }
435
- this.once(AqualinkEvents.TrackStart, seekOnce)
436
- player.once('destroy', () =>
437
- this.off(AqualinkEvents.TrackStart, seekOnce)
438
- )
439
- }
498
+ this._seekAfterTrackStart(player, guildId, state.position, 50)
440
499
  if (state.paused) player.pause(true)
441
500
  }
442
501
  return player
@@ -538,6 +597,108 @@ class Aqua extends EventEmitter {
538
597
  }
539
598
  }
540
599
 
600
+ _regionMatches(configuredRegion, extractedRegion) {
601
+ if (!configuredRegion || !extractedRegion) return false
602
+ const configured = String(configuredRegion).trim().toLowerCase()
603
+ const extracted = String(extractedRegion).trim().toLowerCase()
604
+ if (!configured || !extracted) return false
605
+ return configured === extracted
606
+ }
607
+
608
+ _findBestNodeForRegion(region) {
609
+ if (!region) return null
610
+ const candidates = []
611
+ for (const node of this.nodeMap.values()) {
612
+ if (!node?.connected) continue
613
+ const regions = Array.isArray(node.regions) ? node.regions : []
614
+ if (regions.some((r) => this._regionMatches(r, region))) {
615
+ candidates.push(node)
616
+ }
617
+ }
618
+ if (!candidates.length) return null
619
+ return this._chooseLeastBusyNode(candidates)
620
+ }
621
+
622
+ async movePlayerToNode(guildId, targetNode, reason = 'region') {
623
+ const id = String(guildId)
624
+ const player = this.players.get(id)
625
+ if (!player || player.destroyed) throw new Error(`Player not found: ${id}`)
626
+ if (!targetNode?.connected) throw new Error('Target node is not connected')
627
+ if (player.nodes === targetNode || player.nodes?.name === targetNode.name)
628
+ return player
629
+
630
+ const state = this._capturePlayerState(player)
631
+ if (!state) throw new Error(`Failed to capture state for ${id}`)
632
+ const oldPlayer = player
633
+ const oldNode = oldPlayer.nodes
634
+ const oldMessage = oldPlayer.nowPlayingMessage || null
635
+ const oldConn = oldPlayer.connection
636
+ const oldVoice = oldConn
637
+ ? {
638
+ sessionId: oldConn.sessionId || null,
639
+ endpoint: oldConn.endpoint || null,
640
+ token: oldConn.token || null,
641
+ region: oldConn.region || null,
642
+ channelId: oldConn.channelId || null
643
+ }
644
+ : null
645
+
646
+ oldPlayer.destroy({
647
+ preserveClient: true,
648
+ skipRemote: true,
649
+ preserveMessage: true,
650
+ preserveTracks: true,
651
+ preserveReconnecting: true
652
+ })
653
+
654
+ const newPlayer = this.createPlayer(targetNode, {
655
+ guildId: state.guildId,
656
+ textChannel: state.textChannel,
657
+ voiceChannel: state.voiceChannel,
658
+ defaultVolume: state.volume || 100,
659
+ deaf: state.deaf || false,
660
+ mute: oldPlayer.mute || false,
661
+ resuming: true,
662
+ preserveMessage: true
663
+ })
664
+
665
+ // Bootstrap voice on the new node using last known voice state to avoid
666
+ // "track queued, waiting for voice state" after region migration.
667
+ if (oldVoice && newPlayer.connection) {
668
+ if (oldVoice.sessionId)
669
+ newPlayer.connection.sessionId = oldVoice.sessionId
670
+ if (oldVoice.endpoint) newPlayer.connection.endpoint = oldVoice.endpoint
671
+ if (oldVoice.token) newPlayer.connection.token = oldVoice.token
672
+ if (oldVoice.region) newPlayer.connection.region = oldVoice.region
673
+ if (oldVoice.channelId)
674
+ newPlayer.connection.channelId = oldVoice.channelId
675
+ newPlayer.connection._lastVoiceDataUpdate = Date.now()
676
+ newPlayer.connection.resendVoiceUpdate(true)
677
+ this._trace('player.migrate.voiceBootstrap', {
678
+ guildId: id,
679
+ from: oldNode?.name || oldNode?.host,
680
+ to: targetNode?.name || targetNode?.host,
681
+ hasSessionId: !!newPlayer.connection.sessionId,
682
+ hasEndpoint: !!newPlayer.connection.endpoint,
683
+ hasToken: !!newPlayer.connection.token
684
+ })
685
+ }
686
+
687
+ await this._restorePlayerState(newPlayer, state)
688
+ if (oldMessage) newPlayer.nowPlayingMessage = oldMessage
689
+
690
+ this._trace('player.migrated', {
691
+ guildId: id,
692
+ reason,
693
+ from: oldNode?.name || oldNode?.host,
694
+ to: targetNode?.name || targetNode?.host,
695
+ region:
696
+ newPlayer?.connection?.region || oldPlayer?.connection?.region || null
697
+ })
698
+ this.emit(AqualinkEvents.PlayerMigrated, oldPlayer, newPlayer, targetNode)
699
+ return newPlayer
700
+ }
701
+
541
702
  _capturePlayerState(player) {
542
703
  if (!player) return null
543
704
  let position = player.position || 0
@@ -574,6 +735,16 @@ class Aqua extends EventEmitter {
574
735
  })
575
736
  }
576
737
 
738
+ _seekAfterTrackStart(player, guildId, position, delay = 50) {
739
+ if (!player || !guildId || !(position > 0)) return
740
+ const seekOnce = (p) => {
741
+ if (p.guildId !== guildId) return
742
+ _functions.unrefTimeout(() => player.seek?.(position), delay)
743
+ }
744
+ this.once(AqualinkEvents.TrackStart, seekOnce)
745
+ player.once('destroy', () => this.off(AqualinkEvents.TrackStart, seekOnce))
746
+ }
747
+
577
748
  async _restorePlayerState(newPlayer, state) {
578
749
  const ops = []
579
750
  if (typeof state.volume === 'number') {
@@ -584,26 +755,17 @@ class Aqua extends EventEmitter {
584
755
  if (state.queue?.length && newPlayer.queue?.add)
585
756
  newPlayer.queue.add(...state.queue)
586
757
  if (state.current && this.failoverOptions.preservePosition) {
587
- newPlayer.queue?.add?.(state.current, { toFront: true })
588
758
  if (this.failoverOptions.resumePlayback) {
589
- ops.push(newPlayer.play())
590
- if (state.position > 0) {
591
- const guildId = newPlayer.guildId
592
- const seekOnce = (p) => {
593
- if (p.guildId === guildId) {
594
- this.off(AqualinkEvents.TrackStart, seekOnce)
595
- _functions.unrefTimeout(
596
- () => newPlayer.seek?.(state.position),
597
- 50
598
- )
599
- }
600
- }
601
- this.once(AqualinkEvents.TrackStart, seekOnce)
602
- newPlayer.once('destroy', () =>
603
- this.off(AqualinkEvents.TrackStart, seekOnce)
604
- )
605
- }
759
+ ops.push(newPlayer.play(state.current))
760
+ this._seekAfterTrackStart(
761
+ newPlayer,
762
+ newPlayer.guildId,
763
+ state.position,
764
+ 50
765
+ )
606
766
  if (state.paused) ops.push(newPlayer.pause(true))
767
+ } else if (newPlayer.queue?.add) {
768
+ newPlayer.queue.add(state.current)
607
769
  }
608
770
  }
609
771
  newPlayer.loop = state.loop
@@ -619,6 +781,13 @@ class Aqua extends EventEmitter {
619
781
  return
620
782
  const player = this.players.get(String(d.guild_id))
621
783
  if (!player) return
784
+ this._trace('voice.gateway', {
785
+ guildId: String(d.guild_id),
786
+ type: t,
787
+ hasSessionId: !!d.session_id,
788
+ hasEndpoint: !!d.endpoint,
789
+ hasChannelId: d.channel_id !== undefined
790
+ })
622
791
 
623
792
  d.txId = player.txId
624
793
  if (t === 'VOICE_STATE_UPDATE') {
@@ -681,6 +850,13 @@ class Aqua extends EventEmitter {
681
850
  const player = new Player(this, node, options)
682
851
  const guildId = String(options.guildId)
683
852
  this.players.set(guildId, player)
853
+ this._trace('player.create', {
854
+ guildId,
855
+ node: node?.name || node?.host,
856
+ voiceChannel: options.voiceChannel,
857
+ textChannel: options.textChannel,
858
+ resuming: !!options.resuming
859
+ })
684
860
  node?.players?.add?.(player)
685
861
  player.once('destroy', () => this._handlePlayerDestroy(player))
686
862
  player.connect(options)
@@ -692,6 +868,10 @@ class Aqua extends EventEmitter {
692
868
  player.nodes?.players?.delete?.(player)
693
869
  const guildId = String(player.guildId)
694
870
  if (this.players.get(guildId) === player) this.players.delete(guildId)
871
+ this._trace('player.destroyed', {
872
+ guildId,
873
+ node: player?.nodes?.name || player?.nodes?.host
874
+ })
695
875
  this.emit(AqualinkEvents.PlayerDestroyed, player)
696
876
  }
697
877
 
@@ -699,9 +879,13 @@ class Aqua extends EventEmitter {
699
879
  const id = String(guildId)
700
880
  const player = this.players.get(id)
701
881
  if (!player) return
882
+
883
+ // Guard against recursive destroy calls triggered by Player.destroy().
702
884
  this.players.delete(id)
703
- _functions.safeCall(() => player.removeAllListeners())
704
885
  await _functions.safeCall(() => player.destroy())
886
+
887
+ // Fallback cleanup in case the player "destroy" listener was not attached.
888
+ if (player?.nodes?.players?.has?.(player)) this._handlePlayerDestroy(player)
705
889
  }
706
890
 
707
891
  async resolve({ query, source, requester, nodes }) {
@@ -779,7 +963,10 @@ class Aqua extends EventEmitter {
779
963
  }
780
964
  if (loadType === 'track' && data) {
781
965
  base.pluginInfo =
782
- data.info?.pluginInfo || data.pluginInfo || base.pluginInfo
966
+ data.pluginInfo ||
967
+ data.info?.pluginInfo ||
968
+ rootPlugin ||
969
+ base.pluginInfo
783
970
  base.tracks.push(_functions.makeTrack(data, requester, node))
784
971
  } else if (loadType === 'playlist' && data) {
785
972
  const info = data.info
@@ -973,25 +1160,26 @@ class Aqua extends EventEmitter {
973
1160
  else player.volume = p.vol
974
1161
  }
975
1162
 
976
- if (p.p > 0) {
977
- const seekOnce = (pl) => {
978
- if (pl.guildId === gId) {
979
- this.off(AqualinkEvents.TrackStart, seekOnce)
980
- _functions.unrefTimeout(() => player.seek?.(p.p), 100)
981
- }
982
- }
983
- this.on(AqualinkEvents.TrackStart, seekOnce)
984
- player.once('destroy', () => this.off(AqualinkEvents.TrackStart, seekOnce))
985
- }
1163
+ this._seekAfterTrackStart(player, gId, p.p, 100)
986
1164
 
987
1165
  await player.play(undefined, { startTime: p.p, paused: p.pa })
988
1166
  }
989
1167
  if (p.nw && p.t) {
990
- const channel = this.client.channels?.cache?.get(p.t)
991
- if (channel?.messages)
1168
+ const channel = this.client.channels?.cache?.get?.(p.t)
1169
+ if (channel?.messages?.fetch) {
992
1170
  player.nowPlayingMessage = await channel.messages
993
1171
  .fetch(p.nw)
994
1172
  .catch(() => null)
1173
+ } else if (this.client.messages?.fetch) {
1174
+ player.nowPlayingMessage = await this.client.messages
1175
+ .fetch(p.nw, p.t)
1176
+ .catch(() => null)
1177
+ }
1178
+ this._trace('player.nowPlaying.restore', {
1179
+ guildId: gId,
1180
+ messageId: p.nw,
1181
+ restored: !!player.nowPlayingMessage
1182
+ })
995
1183
  }
996
1184
  } catch (e) {
997
1185
  console.error(
@@ -1072,8 +1260,7 @@ class Aqua extends EventEmitter {
1072
1260
  }
1073
1261
  break
1074
1262
  }
1075
- } catch {
1076
- }
1263
+ } catch {}
1077
1264
  }
1078
1265
  } catch {
1079
1266
  } finally {
@@ -1083,4 +1270,4 @@ class Aqua extends EventEmitter {
1083
1270
  }
1084
1271
  }
1085
1272
 
1086
- module.exports = Aqua
1273
+ module.exports = Aqua