aqualink 2.17.2 → 2.17.3

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.
@@ -2,12 +2,12 @@
2
2
 
3
3
  const fs = require('node:fs')
4
4
  const readline = require('node:readline')
5
- const {EventEmitter} = require('tseep')
6
- const {AqualinkEvents} = require('./AqualinkEvents')
5
+ const { EventEmitter } = require('tseep')
6
+ const { AqualinkEvents } = require('./AqualinkEvents')
7
7
  const Node = require('./Node')
8
8
  const Player = require('./Player')
9
9
  const Track = require('./Track')
10
- const {version: pkgVersion} = require('../../package.json')
10
+ const { version: pkgVersion } = require('../../package.json')
11
11
 
12
12
  const SEARCH_PREFIX = ':'
13
13
  const EMPTY_ARRAY = Object.freeze([])
@@ -33,7 +33,6 @@ const MAX_REBUILD_LOCKS = 100
33
33
  const WRITE_BUFFER_SIZE = 100
34
34
  const MAX_QUEUE_SAVE = 10
35
35
  const MAX_TRACKS_RESTORE = 20
36
- const URL_PATTERN = /^https?:\/\//i
37
36
 
38
37
  const DEFAULT_OPTIONS = Object.freeze({
39
38
  shouldDeleteMessage: false,
@@ -56,11 +55,17 @@ const DEFAULT_OPTIONS = Object.freeze({
56
55
  })
57
56
  })
58
57
 
59
- // Shared helper functions
60
58
  const _functions = {
61
- delay: ms => new Promise(r => setTimeout(r, ms)),
62
- noop: () => {},
63
- isUrl: query => typeof query === 'string' && query.length > 8 && URL_PATTERN.test(query),
59
+ delay: ms => new Promise(r => {
60
+ const t = setTimeout(r, ms)
61
+ t.unref?.()
62
+ }),
63
+ noop: () => { },
64
+ isUrl: query => {
65
+ if (typeof query !== 'string' || query.length <= 8) return false
66
+ const q = query.trimStart()
67
+ return q.startsWith('http://') || q.startsWith('https://')
68
+ },
64
69
  formatQuery(query, source) {
65
70
  return this.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
66
71
  },
@@ -69,12 +74,17 @@ const _functions = {
69
74
  try {
70
75
  const result = fn()
71
76
  return result?.then ? result.catch(this.noop) : result
72
- } catch {}
77
+ } catch { }
73
78
  },
74
79
  parseRequester(str) {
75
80
  if (!str || typeof str !== 'string') return null
76
81
  const i = str.indexOf(':')
77
- return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
82
+ return i > 0 ? { id: str.substring(0, i), username: str.substring(i + 1) } : null
83
+ },
84
+ unrefTimeout: (fn, ms) => {
85
+ const t = setTimeout(fn, ms)
86
+ t.unref?.()
87
+ return t
78
88
  }
79
89
  }
80
90
 
@@ -92,9 +102,9 @@ class Aqua extends EventEmitter {
92
102
  this.initiated = false
93
103
  this.version = pkgVersion
94
104
 
95
- const merged = {...DEFAULT_OPTIONS, ...options}
105
+ const merged = { ...DEFAULT_OPTIONS, ...options }
96
106
  this.options = merged
97
- this.failoverOptions = {...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions}
107
+ this.failoverOptions = { ...DEFAULT_OPTIONS.failoverOptions, ...options.failoverOptions }
98
108
 
99
109
  this.shouldDeleteMessage = merged.shouldDeleteMessage
100
110
  this.defaultSearchPlatform = merged.defaultSearchPlatform
@@ -149,13 +159,13 @@ class Aqua extends EventEmitter {
149
159
  this._performCleanup()
150
160
  })
151
161
  },
152
- onNodeReady: (node, {resumed}) => {
162
+ onNodeReady: (node, { resumed }) => {
153
163
  if (!resumed) return
154
164
  const batch = []
155
165
  for (const player of this.players.values()) {
156
166
  if (player.nodes === node && player.connection) batch.push(player)
157
167
  }
158
- if (batch.length) queueMicrotask(() => batch.forEach(p => p.connection.resendVoiceUpdate({resume: true})))
168
+ if (batch.length) queueMicrotask(() => batch.forEach(p => p.connection.resendVoiceUpdate()))
159
169
  }
160
170
  }
161
171
  this.on(AqualinkEvents.NodeConnect, this._eventHandlers.onNodeConnect)
@@ -171,8 +181,10 @@ class Aqua extends EventEmitter {
171
181
  this._eventHandlers = null
172
182
  }
173
183
  this.removeAllListeners()
174
- this.nodeMap.forEach(node => this._destroyNode(node.name || node.host))
175
- this.players.forEach(player => _functions.safeCall(() => player.destroy()))
184
+
185
+ for (const id of Array.from(this.nodeMap.keys())) this._destroyNode(id)
186
+ for (const player of Array.from(this.players.values())) _functions.safeCall(() => player.destroy())
187
+
176
188
  this.players.clear()
177
189
  this._nodeStates.clear()
178
190
  this._failoverQueue.clear()
@@ -220,7 +232,7 @@ class Aqua extends EventEmitter {
220
232
  (stats.playingPlayers || 0) * 0.75 +
221
233
  (stats.memory ? stats.memory.used / reservable : 0) * 40 +
222
234
  (node.rest?.calls || 0) * 0.001
223
- this._nodeLoadCache.set(id, {load, time: now})
235
+ this._nodeLoadCache.set(id, { load, time: now })
224
236
  if (this._nodeLoadCache.size > MAX_CACHE_SIZE) {
225
237
  const first = this._nodeLoadCache.keys().next().value
226
238
  this._nodeLoadCache.delete(first)
@@ -233,7 +245,7 @@ class Aqua extends EventEmitter {
233
245
  this.clientId = clientId
234
246
  if (!this.clientId) return this
235
247
  const results = await Promise.allSettled(
236
- this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => {throw new Error('Timeout')})]))
248
+ this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => { throw new Error('Timeout') })]))
237
249
  )
238
250
  if (!results.some(r => r.status === 'fulfilled')) throw new Error('No nodes connected')
239
251
  if (this.plugins?.length) {
@@ -249,10 +261,10 @@ class Aqua extends EventEmitter {
249
261
  const node = new Node(this, options, this.options)
250
262
  node.players = new Set()
251
263
  this.nodeMap.set(id, node)
252
- this._nodeStates.set(id, {connected: false, failoverInProgress: false})
264
+ this._nodeStates.set(id, { connected: false, failoverInProgress: false })
253
265
  try {
254
266
  await node.connect()
255
- this._nodeStates.set(id, {connected: true, failoverInProgress: false})
267
+ this._nodeStates.set(id, { connected: true, failoverInProgress: false })
256
268
  this._invalidateCache()
257
269
  this.emit(AqualinkEvents.NodeCreate, node)
258
270
  return node
@@ -304,7 +316,7 @@ class Aqua extends EventEmitter {
304
316
  const now = Date.now()
305
317
  for (const [guildId, state] of this._brokenPlayers) {
306
318
  if (state.originalNodeId === id && (now - state.brokenAt) < BROKEN_PLAYER_TTL) {
307
- rebuilds.push({guildId, state})
319
+ rebuilds.push({ guildId, state })
308
320
  }
309
321
  }
310
322
  if (!rebuilds.length) return
@@ -312,7 +324,7 @@ class Aqua extends EventEmitter {
312
324
  for (let i = 0; i < rebuilds.length; i += MAX_CONCURRENT_OPS) {
313
325
  const batch = rebuilds.slice(i, i + MAX_CONCURRENT_OPS)
314
326
  const results = await Promise.allSettled(
315
- batch.map(({guildId, state}) => this._rebuildPlayer(state, node).then(() => guildId))
327
+ batch.map(({ guildId, state }) => this._rebuildPlayer(state, node).then(() => guildId))
316
328
  )
317
329
  for (const r of results) {
318
330
  if (r.status === 'fulfilled') successes.push(r.value)
@@ -324,7 +336,7 @@ class Aqua extends EventEmitter {
324
336
  }
325
337
 
326
338
  async _rebuildPlayer(state, targetNode) {
327
- const {guildId, textChannel, voiceChannel, current, volume = 65, deaf = true} = state
339
+ const { guildId, textChannel, voiceChannel, current, volume = 65, deaf = true } = state
328
340
  const lockKey = `rebuild_${guildId}`
329
341
  if (this._rebuildLocks.has(lockKey)) return
330
342
  this._rebuildLocks.add(lockKey)
@@ -333,11 +345,11 @@ class Aqua extends EventEmitter {
333
345
  await this.destroyPlayer(guildId)
334
346
  await _functions.delay(RECONNECT_DELAY)
335
347
  }
336
- const player = this.createPlayer(targetNode, {guildId, textChannel, voiceChannel, defaultVolume: volume, deaf})
348
+ const player = this.createPlayer(targetNode, { guildId, textChannel, voiceChannel, defaultVolume: volume, deaf })
337
349
  if (current && player?.queue?.add) {
338
350
  player.queue.add(current)
339
351
  await player.play()
340
- if (state.position > 0) setTimeout(() => player.seek?.(state.position), SEEK_DELAY)
352
+ if (state.position > 0) _functions.unrefTimeout(() => player.seek?.(state.position), SEEK_DELAY)
341
353
  if (state.paused) player.pause(true)
342
354
  }
343
355
  return player
@@ -357,7 +369,7 @@ class Aqua extends EventEmitter {
357
369
  const attempts = this._failoverQueue.get(id) || 0
358
370
  if (attempts >= this.failoverOptions.maxFailoverAttempts) return
359
371
 
360
- this._nodeStates.set(id, {connected: false, failoverInProgress: true})
372
+ this._nodeStates.set(id, { connected: false, failoverInProgress: true })
361
373
  this._lastFailoverAttempt.set(id, now)
362
374
  this._failoverQueue.set(id, attempts + 1)
363
375
 
@@ -379,7 +391,7 @@ class Aqua extends EventEmitter {
379
391
  } catch (error) {
380
392
  this.emit(AqualinkEvents.Error, null, error)
381
393
  } finally {
382
- this._nodeStates.set(id, {connected: false, failoverInProgress: false})
394
+ this._nodeStates.set(id, { connected: false, failoverInProgress: false })
383
395
  }
384
396
  }
385
397
 
@@ -403,7 +415,7 @@ class Aqua extends EventEmitter {
403
415
  for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPS) {
404
416
  const batch = players.slice(i, i + MAX_CONCURRENT_OPS)
405
417
  const batchResults = await Promise.allSettled(batch.map(p => this._migratePlayer(p, pickNode)))
406
- for (const r of batchResults) results.push({success: r.status === 'fulfilled', error: r.reason})
418
+ for (const r of batchResults) results.push({ success: r.status === 'fulfilled', error: r.reason })
407
419
  }
408
420
  return results
409
421
  }
@@ -411,7 +423,7 @@ class Aqua extends EventEmitter {
411
423
  async _migratePlayer(player, pickNode) {
412
424
  const state = this._capturePlayerState(player)
413
425
  if (!state) throw new Error('Failed to capture state')
414
- const {maxRetries, retryDelay} = this.failoverOptions
426
+ const { maxRetries, retryDelay } = this.failoverOptions
415
427
  for (let retry = 0; retry < maxRetries; retry++) {
416
428
  try {
417
429
  const targetNode = pickNode()
@@ -462,10 +474,10 @@ class Aqua extends EventEmitter {
462
474
  }
463
475
  if (state.queue?.length && newPlayer.queue?.add) newPlayer.queue.add(...state.queue)
464
476
  if (state.current && this.failoverOptions.preservePosition) {
465
- newPlayer.queue?.add?.(state.current, {toFront: true})
477
+ newPlayer.queue?.add?.(state.current, { toFront: true })
466
478
  if (this.failoverOptions.resumePlayback) {
467
479
  ops.push(newPlayer.play())
468
- if (state.position > 0) setTimeout(() => newPlayer.seek?.(state.position), SEEK_DELAY)
480
+ if (state.position > 0) _functions.unrefTimeout(() => newPlayer.seek?.(state.position), SEEK_DELAY)
469
481
  if (state.paused) ops.push(newPlayer.pause(true))
470
482
  }
471
483
  }
@@ -474,7 +486,7 @@ class Aqua extends EventEmitter {
474
486
  await Promise.allSettled(ops)
475
487
  }
476
488
 
477
- updateVoiceState({d, t}) {
489
+ updateVoiceState({ d, t }) {
478
490
  if (!d?.guild_id || (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE')) return
479
491
  const player = this.players.get(d.guild_id)
480
492
  if (!player || !player.nodes?.connected) return
@@ -540,7 +552,7 @@ class Aqua extends EventEmitter {
540
552
  await _functions.safeCall(() => player.destroy())
541
553
  }
542
554
 
543
- async resolve({query, source, requester, nodes}) {
555
+ async resolve({ query, source, requester, nodes }) {
544
556
  if (!this.initiated) throw new Error('Aqua not initialized')
545
557
  const node = this._getRequestNode(nodes)
546
558
  if (!node) throw new Error('No nodes available')
@@ -581,8 +593,8 @@ class Aqua extends EventEmitter {
581
593
  }
582
594
 
583
595
  _constructResponse(response, requester, node) {
584
- const {loadType, data, pluginInfo: rootPlugin} = response || {}
585
- const base = {loadType, exception: null, playlistInfo: null, pluginInfo: rootPlugin || {}, tracks: []}
596
+ const { loadType, data, pluginInfo: rootPlugin } = response || {}
597
+ const base = { loadType, exception: null, playlistInfo: null, pluginInfo: rootPlugin || {}, tracks: [] }
586
598
  if (loadType === 'error' || loadType === 'LOAD_FAILED') {
587
599
  base.exception = data || response.exception || null
588
600
  return base
@@ -599,7 +611,7 @@ class Aqua extends EventEmitter {
599
611
  ...info
600
612
  }
601
613
  }
602
- base.pluginInfo = data.pluginInfo || base.pluginInfo
614
+ base.pluginInfo = data.pluginInfo || rootPlugin || base.pluginInfo
603
615
  base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _functions.makeTrack(t, requester, node)) : []
604
616
  } else if (loadType === 'search') {
605
617
  base.tracks = Array.isArray(data) ? data.map(t => _functions.makeTrack(t, requester, node)) : []
@@ -616,7 +628,7 @@ class Aqua extends EventEmitter {
616
628
  async search(query, requester, source) {
617
629
  if (!query || !requester) return null
618
630
  try {
619
- const {tracks} = await this.resolve({query, source: source || this.defaultSearchPlatform, requester})
631
+ const { tracks } = await this.resolve({ query, source: source || this.defaultSearchPlatform, requester })
620
632
  return tracks || null
621
633
  } catch {
622
634
  return null
@@ -628,8 +640,8 @@ class Aqua extends EventEmitter {
628
640
  const tempFile = `${filePath}.tmp`
629
641
  let ws = null
630
642
  try {
631
- await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
632
- ws = fs.createWriteStream(tempFile, {encoding: 'utf8', flags: 'w'})
643
+ await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
644
+ ws = fs.createWriteStream(tempFile, { encoding: 'utf8', flags: 'w' })
633
645
  const buffer = []
634
646
  let drainPromise = Promise.resolve()
635
647
 
@@ -680,11 +692,11 @@ class Aqua extends EventEmitter {
680
692
  let stream = null, rl = null
681
693
  try {
682
694
  await fs.promises.access(filePath)
683
- await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
695
+ await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
684
696
  await this._waitForFirstNode()
685
697
 
686
- stream = fs.createReadStream(filePath, {encoding: 'utf8'})
687
- rl = readline.createInterface({input: stream, crlfDelay: Infinity})
698
+ stream = fs.createReadStream(filePath, { encoding: 'utf8' })
699
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
688
700
 
689
701
  const batch = []
690
702
  for await (const line of rl) {
@@ -719,7 +731,7 @@ class Aqua extends EventEmitter {
719
731
  player._resuming = !!p.resuming
720
732
  const requester = _functions.parseRequester(p.r)
721
733
  const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, MAX_TRACKS_RESTORE)
722
- const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({query: uri, requester}).catch(() => null)))
734
+ const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({ query: uri, requester }).catch(() => null)))
723
735
  const validTracks = resolved.flatMap(r => r?.tracks || [])
724
736
  if (validTracks.length && player.queue?.add) {
725
737
  if (player.queue.length <= 2) player.queue.length = 0
@@ -731,14 +743,14 @@ class Aqua extends EventEmitter {
731
743
  else player.volume = p.vol
732
744
  }
733
745
  await player.play()
734
- if (p.p > 0) setTimeout(() => player.seek?.(p.p), SEEK_DELAY)
746
+ if (p.p > 0) _functions.unrefTimeout(() => player.seek?.(p.p), SEEK_DELAY)
735
747
  if (p.pa) await player.pause(true)
736
748
  }
737
749
  if (p.nw && p.t) {
738
750
  const channel = this.client.channels?.cache?.get(p.t)
739
751
  if (channel?.messages) player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
740
752
  }
741
- } catch {}
753
+ } catch { }
742
754
  }
743
755
 
744
756
  async _waitForFirstNode(timeout = NODE_TIMEOUT) {
@@ -756,6 +768,7 @@ class Aqua extends EventEmitter {
756
768
  if (this.leastUsedNodes.length) { cleanup(); resolve() }
757
769
  }
758
770
  const timer = setTimeout(() => { cleanup(); reject(new Error('Timeout waiting for first node')) }, timeout)
771
+ timer.unref?.()
759
772
  this.on(AqualinkEvents.NodeConnect, onReady)
760
773
  this.on(AqualinkEvents.NodeCreate, onReady)
761
774
  onReady()
@@ -4,11 +4,17 @@ const { AqualinkEvents } = require('./AqualinkEvents')
4
4
 
5
5
  const POOL_SIZE = 12
6
6
  const UPDATE_TIMEOUT = 4000
7
+
7
8
  const RECONNECT_DELAY = 1000
8
9
  const MAX_RECONNECT_ATTEMPTS = 3
9
10
  const RESUME_BACKOFF_MAX = 8000
11
+
10
12
  const VOICE_DATA_TIMEOUT = 30000
11
13
 
14
+ const VOICE_FLUSH_DELAY = 50
15
+
16
+ const NULL_CHANNEL_GRACE_MS = 1500
17
+
12
18
  const STATE = {
13
19
  CONNECTED: 1,
14
20
  UPDATE_SCHEDULED: 64,
@@ -53,6 +59,7 @@ const _functions = {
53
59
  v.token = conn.token
54
60
  v.endpoint = conn.endpoint
55
61
  v.sessionId = conn.sessionId
62
+ v.channelId = player.voiceChannel
56
63
  v.resume = resume ? true : undefined
57
64
  v.sequence = resume ? conn.sequence : undefined
58
65
  payload.data.volume = player?.volume ?? 100
@@ -110,19 +117,28 @@ class Connection {
110
117
 
111
118
  this.voiceChannel = player.voiceChannel
112
119
  this.sessionId = null
120
+ this.channelId = null
113
121
  this.endpoint = null
114
122
  this.token = null
115
123
  this.region = null
116
124
  this.sequence = 0
117
125
 
118
126
  this._lastEndpoint = null
119
- this._pendingUpdate = null
120
127
  this._stateFlags = 0
121
128
  this._reconnectAttempts = 0
122
129
  this._destroyed = false
123
130
  this._reconnectTimer = null
124
131
  this._lastVoiceDataUpdate = 0
125
132
  this._consecutiveFailures = 0
133
+
134
+ this._voiceFlushTimer = null
135
+ this._pendingUpdate = null
136
+ this._lastSentVoiceKey = ''
137
+
138
+ this._nullChannelTimer = null
139
+
140
+ this._lastStateReqAt = 0
141
+ this._stateGeneration = 0
126
142
  }
127
143
 
128
144
  _hasValidVoiceData() {
@@ -134,10 +150,17 @@ class Connection {
134
150
  return true
135
151
  }
136
152
 
137
- _canAttemptResume() {
138
- if (this._destroyed || this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return false
153
+ _clearNullChannelTimer() {
154
+ if (!this._nullChannelTimer) return
155
+ clearTimeout(this._nullChannelTimer)
156
+ this._nullChannelTimer = null
157
+ }
158
+
159
+ _canAttemptResumeCore() {
160
+ if (this._destroyed) return false
161
+ if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return false
139
162
  if (this._stateFlags & (STATE.ATTEMPTING_RESUME | STATE.DISCONNECTING)) return false
140
- return this._hasValidVoiceData() || !!this._player?._resuming
163
+ return true
141
164
  }
142
165
 
143
166
  _setReconnectTimer(delay) {
@@ -148,12 +171,15 @@ class Connection {
148
171
  }
149
172
 
150
173
  setServerUpdate(data) {
151
- if (this._destroyed || !data?.endpoint || !data.token) return
174
+ if (this._destroyed || !data?.token) return
152
175
 
153
176
  const endpoint = typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
154
- if (!endpoint || typeof data.token !== 'string' || !data.token) return
177
+ if (!endpoint) return
178
+
155
179
  if (this._lastEndpoint === endpoint && this.token === data.token) return
156
180
 
181
+ this._stateGeneration++
182
+
157
183
  if (this._lastEndpoint !== endpoint) {
158
184
  this.sequence = 0
159
185
  this._lastEndpoint = endpoint
@@ -164,10 +190,11 @@ class Connection {
164
190
  this.endpoint = endpoint
165
191
  this.region = _functions.extractRegion(endpoint)
166
192
  this.token = data.token
193
+ this.channelId = data.channel_id || this.channelId || this.voiceChannel
167
194
  this._lastVoiceDataUpdate = Date.now()
168
195
  this._stateFlags &= ~STATE.VOICE_DATA_STALE
169
196
 
170
- if (this._player.paused) this._player.pause(false)
197
+ if (this._player?.paused) this._player.pause(false)
171
198
  this._scheduleVoiceUpdate()
172
199
  }
173
200
 
@@ -181,15 +208,38 @@ class Connection {
181
208
  if (this._destroyed || !data || data.user_id !== this._clientId) return
182
209
 
183
210
  const { session_id: sessionId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute } = data
211
+ const p = this._player
184
212
 
185
- if (!channelId) return this._handleDisconnect()
213
+ if (channelId) this._clearNullChannelTimer()
214
+
215
+ const reqCh = p?._voiceRequestChannel
216
+ const reqFresh = !!(reqCh && (Date.now() - (p._voiceRequestAt || 0)) < 5000)
217
+
218
+ if (!channelId) {
219
+ if (reqFresh) return
220
+
221
+ if (!this._nullChannelTimer) {
222
+ this._nullChannelTimer = setTimeout(() => {
223
+ this._nullChannelTimer = null
224
+ this._handleDisconnect()
225
+ }, NULL_CHANNEL_GRACE_MS)
226
+ _functions.safeUnref(this._nullChannelTimer)
227
+ }
228
+ return
229
+ }
230
+
231
+ if (reqFresh && channelId !== reqCh) return
232
+
233
+ if (reqCh && channelId === reqCh) {
234
+ p._voiceRequestChannel = null
235
+ }
186
236
 
187
237
  let needsUpdate = false
188
238
 
189
239
  if (this.voiceChannel !== channelId) {
190
240
  this._aqua.emit(AqualinkEvents.PlayerMove, this.voiceChannel, channelId)
191
241
  this.voiceChannel = channelId
192
- this._player.voiceChannel = channelId
242
+ p.voiceChannel = channelId
193
243
  needsUpdate = true
194
244
  }
195
245
 
@@ -202,30 +252,29 @@ class Connection {
202
252
  needsUpdate = true
203
253
  }
204
254
 
205
- this._player.connection.sessionId = sessionId || this._player.connection.sessionId
206
- this._player.self_deaf = this._player.selfDeaf = !!selfDeaf
207
- this._player.self_mute = this._player.selfMute = !!selfMute
208
- this._player.connected = true
255
+ p.self_deaf = p.selfDeaf = !!selfDeaf
256
+ p.self_mute = p.selfMute = !!selfMute
209
257
  this._stateFlags |= STATE.CONNECTED
210
258
 
211
259
  if (needsUpdate) this._scheduleVoiceUpdate()
212
260
  }
213
261
 
214
262
  _handleDisconnect() {
215
- if (this._destroyed || !(this._stateFlags & STATE.CONNECTED)) return
263
+ if (this._destroyed) return
216
264
 
217
265
  this._stateFlags = (this._stateFlags | STATE.DISCONNECTING) & ~STATE.CONNECTED
218
- this._player.connected = false
266
+ this._clearNullChannelTimer()
219
267
  this._clearPendingUpdate()
220
268
  this._clearReconnectTimer()
221
269
 
222
- this.voiceChannel = this.sessionId = null
270
+ this.voiceChannel = null
271
+ this.sessionId = null
223
272
  this.sequence = 0
224
273
  this._lastVoiceDataUpdate = 0
225
274
  this._stateFlags |= STATE.VOICE_DATA_STALE
226
275
 
227
276
  try {
228
- this._player.destroy?.()
277
+ this._player?.destroy?.()
229
278
  } catch (e) {
230
279
  this._aqua.emit(AqualinkEvents.Debug, new Error(`Player destroy failed: ${e?.message || e}`))
231
280
  } finally {
@@ -235,6 +284,10 @@ class Connection {
235
284
 
236
285
  _requestVoiceState() {
237
286
  try {
287
+ const now = Date.now()
288
+ if (now - (this._lastStateReqAt || 0) < 1500) return false
289
+ this._lastStateReqAt = now
290
+
238
291
  if (typeof this._player?.send !== 'function' || !this._player.voiceChannel) return false
239
292
  this._player.send({
240
293
  guild_id: this._guildId,
@@ -242,7 +295,6 @@ class Connection {
242
295
  self_deaf: this._player.deaf,
243
296
  self_mute: this._player.mute
244
297
  })
245
- this._setReconnectTimer(1500)
246
298
  return true
247
299
  } catch {
248
300
  return false
@@ -250,47 +302,38 @@ class Connection {
250
302
  }
251
303
 
252
304
  async attemptResume() {
253
- if (!this._canAttemptResume()) {
254
- this._aqua.emit(
255
- AqualinkEvents.Debug,
256
- `Resume blocked: destroyed=${this._destroyed}, hasValidData=${this._hasValidVoiceData()}, attempts=${this._reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`
257
- )
258
-
259
- const isResuming = this._player?._resuming
260
- const isStale = this._stateFlags & STATE.VOICE_DATA_STALE
261
- const needsVoiceData = !this.sessionId || !this.endpoint || !this.token
262
-
263
- if ((isStale || needsVoiceData) && isResuming) {
264
- this._aqua.emit(AqualinkEvents.Debug, `Requesting fresh voice state for guild ${this._guildId}`)
265
- this._requestVoiceState()
266
- } else if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
267
- this._handleDisconnect()
268
- }
269
- return false
270
- }
305
+ if (!this._canAttemptResumeCore()) return false
306
+
307
+ const currentGen = this._stateGeneration
271
308
 
272
- if ((!this.sessionId || !this.endpoint || !this.token) && this._player?._resuming) {
273
- this._aqua.emit(AqualinkEvents.Debug, `Resuming player but voice data missing for guild ${this._guildId}`)
309
+
310
+ if (!this.sessionId || !this.endpoint || !this.token || (this._stateFlags & STATE.VOICE_DATA_STALE)) {
311
+ this._aqua.emit(AqualinkEvents.Debug, `Resume blocked: missing voice data for guild ${this._guildId}, requesting voice state`)
274
312
  this._requestVoiceState()
275
313
  return false
276
314
  }
277
315
 
278
316
  this._stateFlags |= STATE.ATTEMPTING_RESUME
279
317
  this._reconnectAttempts++
280
- this._aqua.emit(
281
- AqualinkEvents.Debug,
282
- `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`
283
- )
318
+ this._aqua.emit(AqualinkEvents.Debug, `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`)
284
319
 
285
320
  const payload = sharedPool.acquire()
286
321
  try {
287
322
  _functions.fillVoicePayload(payload, this._guildId, this, this._player, true)
323
+
324
+ if (this._stateGeneration !== currentGen) {
325
+ this._aqua.emit(AqualinkEvents.Debug, `Resume aborted: State changed during attempt for guild ${this._guildId}`)
326
+ return false
327
+ }
328
+
329
+
288
330
  await this._sendUpdate(payload)
289
331
 
290
332
  this._reconnectAttempts = 0
291
333
  this._consecutiveFailures = 0
292
334
  if (this._player) this._player._resuming = false
293
- this._aqua.emit(AqualinkEvents.Debug, `Resume successful for guild ${this._guildId}`)
335
+
336
+ this._aqua.emit(AqualinkEvents.Debug, `Resume PATCH sent for guild ${this._guildId}`)
294
337
  return true
295
338
  } catch (e) {
296
339
  this._consecutiveFailures++
@@ -300,7 +343,7 @@ class Connection {
300
343
  const delay = Math.min(RECONNECT_DELAY * (1 << (this._reconnectAttempts - 1)), RESUME_BACKOFF_MAX)
301
344
  this._setReconnectTimer(delay)
302
345
  } else {
303
- this._aqua.emit(AqualinkEvents.Debug, `Max reconnect attempts or failures reached for guild ${this._guildId}`)
346
+ this._aqua.emit(AqualinkEvents.Debug, `Max reconnect attempts/failures reached for guild ${this._guildId}`)
304
347
  if (this._player) this._player._resuming = false
305
348
  this._handleDisconnect()
306
349
  }
@@ -330,36 +373,64 @@ class Connection {
330
373
  this._stateFlags &= ~STATE.UPDATE_SCHEDULED
331
374
  if (this._pendingUpdate?.payload) sharedPool.release(this._pendingUpdate.payload)
332
375
  this._pendingUpdate = null
376
+ if (this._voiceFlushTimer) {
377
+ clearTimeout(this._voiceFlushTimer)
378
+ this._voiceFlushTimer = null
379
+ }
333
380
  }
334
381
 
335
- _scheduleVoiceUpdate() {
336
- if (this._destroyed || !this._hasValidVoiceData() || (this._stateFlags & STATE.UPDATE_SCHEDULED)) return
337
-
338
- this._clearPendingUpdate()
382
+ _makeVoiceKey() {
383
+ const p = this._player
384
+ const vol = p?.volume ?? 100
385
+ return (this.sessionId || '') + '|' +
386
+ (this.token || '') + '|' +
387
+ (this.endpoint || '') + '|' +
388
+ (p?.voiceChannel || '') + '|' +
389
+ vol
390
+ }
339
391
 
340
- const payload = sharedPool.acquire()
341
- _functions.fillVoicePayload(payload, this._guildId, this, this._player, false)
392
+ _scheduleVoiceUpdate() {
393
+ if (this._destroyed) return
394
+ if (!this._hasValidVoiceData()) return
395
+
396
+ if (!this._pendingUpdate) {
397
+ const payload = sharedPool.acquire()
398
+ _functions.fillVoicePayload(payload, this._guildId, this, this._player, false)
399
+ this._pendingUpdate = { payload, timestamp: Date.now() }
400
+ } else {
401
+ this._pendingUpdate.timestamp = Date.now()
402
+ _functions.fillVoicePayload(this._pendingUpdate.payload, this._guildId, this, this._player, false)
403
+ }
342
404
 
343
- this._pendingUpdate = { payload, timestamp: Date.now() }
405
+ if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
344
406
  this._stateFlags |= STATE.UPDATE_SCHEDULED
345
- queueMicrotask(() => this._executeVoiceUpdate())
407
+
408
+ this._voiceFlushTimer = setTimeout(() => this._executeVoiceUpdate(), VOICE_FLUSH_DELAY)
409
+ _functions.safeUnref(this._voiceFlushTimer)
346
410
  }
347
411
 
348
412
  _executeVoiceUpdate() {
349
413
  if (this._destroyed) return
350
414
  this._stateFlags &= ~STATE.UPDATE_SCHEDULED
415
+ this._voiceFlushTimer = null
351
416
 
352
417
  const pending = this._pendingUpdate
353
- if (!pending) return
354
418
  this._pendingUpdate = null
355
419
 
420
+ if (!pending) return
356
421
  if (Date.now() - pending.timestamp > UPDATE_TIMEOUT) {
357
422
  sharedPool.release(pending.payload)
358
423
  return
359
424
  }
360
425
 
361
- const payload = pending.payload
362
- this._sendUpdate(payload).finally(() => sharedPool.release(payload))
426
+ const key = this._makeVoiceKey()
427
+ if (key === this._lastSentVoiceKey) {
428
+ sharedPool.release(pending.payload)
429
+ return
430
+ }
431
+ this._lastSentVoiceKey = key
432
+
433
+ this._sendUpdate(pending.payload).finally(() => sharedPool.release(pending.payload))
363
434
  }
364
435
 
365
436
  async _sendUpdate(payload) {
@@ -380,6 +451,7 @@ class Connection {
380
451
  if (this._destroyed) return
381
452
  this._destroyed = true
382
453
 
454
+ this._clearNullChannelTimer()
383
455
  this._clearPendingUpdate()
384
456
  this._clearReconnectTimer()
385
457
 
@@ -456,4 +456,4 @@ class Node {
456
456
  }
457
457
  }
458
458
 
459
- module.exports = Node
459
+ module.exports = Node
@@ -193,6 +193,9 @@ class Player extends EventEmitter {
193
193
  this.previousTracks = new CircularBuffer(PREVIOUS_TRACKS_SIZE)
194
194
  this._updateBatcher = batcherPool.acquire(this)
195
195
 
196
+ this._voiceRequestAt = 0
197
+ this._voiceRequestChannel = null
198
+ this._suppressResumeUntil = 0
196
199
  this._bindEvents()
197
200
  this._startWatchdog()
198
201
  }
@@ -241,6 +244,7 @@ class Player extends EventEmitter {
241
244
  this._voiceDownSince = Date.now()
242
245
  this._createTimer(() => {
243
246
  if (this.connected || this.destroyed || this.nodes?.info?.isNodelink) return
247
+ if (Date.now() < (this._suppressResumeUntil || 0)) return
244
248
  this.connection.attemptResume()
245
249
  }, 1000)
246
250
  }
@@ -337,13 +341,24 @@ class Player extends EventEmitter {
337
341
 
338
342
  connect(options = {}) {
339
343
  if (this.destroyed) throw new Error('Cannot connect destroyed player')
344
+
340
345
  const voiceChannel = _functions.toId(options.voiceChannel || this.voiceChannel)
341
346
  if (!voiceChannel) throw new TypeError('Voice channel required')
347
+
342
348
  this.deaf = options.deaf !== undefined ? !!options.deaf : true
343
349
  this.mute = !!options.mute
344
350
  this.destroyed = false
351
+
352
+ this._voiceRequestAt = Date.now()
353
+ this._voiceRequestChannel = voiceChannel
354
+
345
355
  this.voiceChannel = voiceChannel
346
- this.send({ guild_id: this.guildId, channel_id: voiceChannel, self_deaf: this.deaf, self_mute: this.mute })
356
+ this.send({
357
+ guild_id: this.guildId,
358
+ channel_id: voiceChannel,
359
+ self_deaf: this.deaf,
360
+ self_mute: this.mute
361
+ })
347
362
  return this
348
363
  }
349
364
 
@@ -432,6 +447,9 @@ class Player extends EventEmitter {
432
447
  this._dataStore = null
433
448
 
434
449
  if (this.current?.dispose && !this.aqua?.options?.autoResume) this.current.dispose()
450
+ if (this.connection) {
451
+ try { this.connection.destroy() } catch { }
452
+ }
435
453
  this.connection = this.filters = this.current = this.autoplaySeed = null
436
454
 
437
455
  if (!skipRemote) {
@@ -757,14 +775,20 @@ class Player extends EventEmitter {
757
775
  if (this.destroyed) return
758
776
  const code = payload?.code
759
777
 
760
- if (code === 4022) {
778
+ if (code === 4014 || code === 4022) {
761
779
  this.aqua.emit(AqualinkEvents.SocketClosed, this, payload)
762
- this.destroy()
780
+
781
+ this.connected = false
782
+ if (!this._voiceDownSince) this._voiceDownSince = Date.now()
783
+
784
+ if (code !== 4014) {
785
+ this._suppressResumeUntil = Date.now() + 3000
786
+ }
763
787
  return
764
788
  }
765
789
 
766
790
  if (code === 4015 && !this.nodes?.info?.isNodelink) {
767
- try { await this._attemptVoiceResume(); return } catch { }
791
+ try { await this._attemptVoiceResume(); return } catch { /* ignore */ }
768
792
  }
769
793
 
770
794
  if (![4015, 4009, 4006].includes(code)) {
@@ -851,8 +875,6 @@ class Player extends EventEmitter {
851
875
  _handleAquaPlayerMove(oldChannel, newChannel) {
852
876
  if (_functions.toId(oldChannel) !== _functions.toId(this.voiceChannel)) return
853
877
  this.voiceChannel = _functions.toId(newChannel)
854
- this.connected = !!newChannel
855
- this.send({ guild_id: this.guildId, channel_id: this.voiceChannel, self_deaf: this.deaf, self_mute: this.mute })
856
878
  }
857
879
 
858
880
  send(data) {
@@ -570,4 +570,4 @@ class Rest {
570
570
  }
571
571
  }
572
572
 
573
- module.exports = Rest
573
+ module.exports = Rest
@@ -22,6 +22,7 @@ class Track {
22
22
  this.uri = _h.str(info.uri)
23
23
  this.sourceName = _h.str(info.sourceName)
24
24
  this.artworkUrl = _h.str(info.artworkUrl)
25
+ this.pluginInfo = info.pluginInfo || data.pluginInfo || {}
25
26
 
26
27
  this.playlist = data.playlist || null
27
28
  this.node = node || data.node || null
@@ -92,6 +93,7 @@ class Track {
92
93
  this.uri = fi.uri ?? this.uri
93
94
  this.sourceName = fi.sourceName ?? this.sourceName
94
95
  this.artworkUrl = fi.artworkUrl ?? this.artworkUrl
96
+ this.pluginInfo = fi.pluginInfo ?? found.pluginInfo ?? this.pluginInfo
95
97
  this.isSeekable = fi.isSeekable ?? this.isSeekable
96
98
  this.isStream = fi.isStream ?? this.isStream
97
99
  this.position = _h.num(fi.position, this.position)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.17.2",
3
+ "version": "2.17.3",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "homepage": "https://aqualink-6006388d.mintlify.app/",
47
47
  "dependencies": {
48
- "ws": "^8.18.3",
48
+ "ws": "^8.19.0",
49
49
  "tseep": "^1.3.1"
50
50
  },
51
51
  "contributors": [