aqualink 2.16.1 → 2.17.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.
@@ -9,7 +9,6 @@ const Player = require('./Player')
9
9
  const Track = require('./Track')
10
10
  const {version: pkgVersion} = require('../../package.json')
11
11
 
12
- // Constants
13
12
  const SEARCH_PREFIX = ':'
14
13
  const EMPTY_ARRAY = Object.freeze([])
15
14
  const EMPTY_TRACKS_RESPONSE = Object.freeze({
@@ -57,17 +56,26 @@ const DEFAULT_OPTIONS = Object.freeze({
57
56
  })
58
57
  })
59
58
 
60
- // Utility functions (moved to module scope for reuse)
61
- const _delay = ms => new Promise(r => setTimeout(r, ms))
62
- const _noop = () => {}
63
- const _isUrl = query => typeof query === 'string' && query.length > 8 && URL_PATTERN.test(query)
64
- const _formatQuery = (query, source) => _isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
65
- const _makeTrack = (t, requester, node) => new Track(t, requester, node)
66
- const _safeCall = fn => {
67
- try {
68
- const result = fn()
69
- return result?.then ? result.catch(_noop) : result
70
- } catch {}
59
+ // Shared helper functions
60
+ 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),
64
+ formatQuery(query, source) {
65
+ return this.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
66
+ },
67
+ makeTrack: (t, requester, node) => new Track(t, requester, node),
68
+ safeCall(fn) {
69
+ try {
70
+ const result = fn()
71
+ return result?.then ? result.catch(this.noop) : result
72
+ } catch {}
73
+ },
74
+ parseRequester(str) {
75
+ if (!str || typeof str !== 'string') return null
76
+ const i = str.indexOf(':')
77
+ return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
78
+ }
71
79
  }
72
80
 
73
81
  class Aqua extends EventEmitter {
@@ -128,7 +136,6 @@ class Aqua extends EventEmitter {
128
136
  }
129
137
 
130
138
  _bindEventHandlers() {
131
- // Store handlers for cleanup
132
139
  this._eventHandlers = {
133
140
  onNodeConnect: async node => {
134
141
  this._invalidateCache()
@@ -156,7 +163,6 @@ class Aqua extends EventEmitter {
156
163
  this.on(AqualinkEvents.NodeReady, this._eventHandlers.onNodeReady)
157
164
  }
158
165
 
159
- // Cleanup event handlers to prevent memory leaks
160
166
  destroy() {
161
167
  if (this._eventHandlers) {
162
168
  this.off(AqualinkEvents.NodeConnect, this._eventHandlers.onNodeConnect)
@@ -166,8 +172,15 @@ class Aqua extends EventEmitter {
166
172
  }
167
173
  this.removeAllListeners()
168
174
  this.nodeMap.forEach(node => this._destroyNode(node.name || node.host))
169
- this.players.forEach(player => _safeCall(() => player.destroy()))
175
+ this.players.forEach(player => _functions.safeCall(() => player.destroy()))
170
176
  this.players.clear()
177
+ this._nodeStates.clear()
178
+ this._failoverQueue.clear()
179
+ this._lastFailoverAttempt.clear()
180
+ this._brokenPlayers.clear()
181
+ this._rebuildLocks.clear()
182
+ this._nodeLoadCache.clear()
183
+ this._invalidateCache()
171
184
  }
172
185
 
173
186
  get leastUsedNodes() {
@@ -175,7 +188,10 @@ class Aqua extends EventEmitter {
175
188
  if (this._leastUsedNodesCache && (now - this._leastUsedNodesCacheTime) < CACHE_VALID_TIME) {
176
189
  return this._leastUsedNodesCache
177
190
  }
178
- const connected = Array.from(this.nodeMap.values()).filter(n => n.connected)
191
+ const connected = []
192
+ for (const n of this.nodeMap.values()) {
193
+ if (n.connected) connected.push(n)
194
+ }
179
195
  const sorted = this.loadBalancer === 'leastRest'
180
196
  ? connected.sort((a, b) => (a.rest?.calls || 0) - (b.rest?.calls || 0))
181
197
  : this.loadBalancer === 'random'
@@ -193,14 +209,16 @@ class Aqua extends EventEmitter {
193
209
 
194
210
  _getNodeLoad(node) {
195
211
  const id = node.name || node.host
196
- const cached = this._nodeLoadCache.get(id)
197
212
  const now = Date.now()
213
+ const cached = this._nodeLoadCache.get(id)
198
214
  if (cached && (now - cached.time) < 5000) return cached.load
199
215
  const stats = node?.stats
200
216
  if (!stats) return 0
201
- const load = (stats.cpu ? stats.cpu.systemLoad / Math.max(1, stats.cpu.cores || 1) : 0) * 100 +
217
+ const cores = Math.max(1, stats.cpu?.cores || 1)
218
+ const reservable = Math.max(1, stats.memory?.reservable || 1)
219
+ const load = (stats.cpu ? stats.cpu.systemLoad / cores : 0) * 100 +
202
220
  (stats.playingPlayers || 0) * 0.75 +
203
- (stats.memory ? stats.memory.used / Math.max(1, stats.memory.reservable) : 0) * 40 +
221
+ (stats.memory ? stats.memory.used / reservable : 0) * 40 +
204
222
  (node.rest?.calls || 0) * 0.001
205
223
  this._nodeLoadCache.set(id, {load, time: now})
206
224
  if (this._nodeLoadCache.size > MAX_CACHE_SIZE) {
@@ -213,13 +231,13 @@ class Aqua extends EventEmitter {
213
231
  async init(clientId) {
214
232
  if (this.initiated) return this
215
233
  this.clientId = clientId
216
- if (!this.clientId) return
234
+ if (!this.clientId) return this
217
235
  const results = await Promise.allSettled(
218
- this.nodes.map(n => Promise.race([this._createNode(n), _delay(NODE_TIMEOUT).then(() => {throw new Error('Timeout')})]))
236
+ this.nodes.map(n => Promise.race([this._createNode(n), _functions.delay(NODE_TIMEOUT).then(() => {throw new Error('Timeout')})]))
219
237
  )
220
238
  if (!results.some(r => r.status === 'fulfilled')) throw new Error('No nodes connected')
221
239
  if (this.plugins?.length) {
222
- await Promise.allSettled(this.plugins.map(p => _safeCall(() => p.load(this))))
240
+ await Promise.allSettled(this.plugins.map(p => _functions.safeCall(() => p.load(this))))
223
241
  }
224
242
  this.initiated = true
225
243
  return this
@@ -247,7 +265,7 @@ class Aqua extends EventEmitter {
247
265
  _destroyNode(id) {
248
266
  const node = this.nodeMap.get(id)
249
267
  if (!node) return
250
- _safeCall(() => node.destroy())
268
+ _functions.safeCall(() => node.destroy())
251
269
  this._cleanupNode(id)
252
270
  this.emit(AqualinkEvents.NodeDestroy, node)
253
271
  }
@@ -255,8 +273,8 @@ class Aqua extends EventEmitter {
255
273
  _cleanupNode(id) {
256
274
  const node = this.nodeMap.get(id)
257
275
  if (node) {
258
- _safeCall(() => node.removeAllListeners())
259
- _safeCall(() => node.players.clear())
276
+ _functions.safeCall(() => node.removeAllListeners())
277
+ _functions.safeCall(() => node.players.clear())
260
278
  this.nodeMap.delete(id)
261
279
  }
262
280
  this._nodeStates.delete(id)
@@ -296,11 +314,11 @@ class Aqua extends EventEmitter {
296
314
  const results = await Promise.allSettled(
297
315
  batch.map(({guildId, state}) => this._rebuildPlayer(state, node).then(() => guildId))
298
316
  )
299
- results.forEach(r => {
317
+ for (const r of results) {
300
318
  if (r.status === 'fulfilled') successes.push(r.value)
301
- })
319
+ }
302
320
  }
303
- successes.forEach(guildId => this._brokenPlayers.delete(guildId))
321
+ for (const guildId of successes) this._brokenPlayers.delete(guildId)
304
322
  if (successes.length) this.emit(AqualinkEvents.PlayersRebuilt, node, successes.length)
305
323
  this._performCleanup()
306
324
  }
@@ -311,17 +329,16 @@ class Aqua extends EventEmitter {
311
329
  if (this._rebuildLocks.has(lockKey)) return
312
330
  this._rebuildLocks.add(lockKey)
313
331
  try {
314
- const existing = this.players.get(guildId)
315
- if (existing) {
332
+ if (this.players.has(guildId)) {
316
333
  await this.destroyPlayer(guildId)
317
- await _delay(RECONNECT_DELAY)
334
+ await _functions.delay(RECONNECT_DELAY)
318
335
  }
319
336
  const player = this.createPlayer(targetNode, {guildId, textChannel, voiceChannel, defaultVolume: volume, deaf})
320
337
  if (current && player?.queue?.add) {
321
338
  player.queue.add(current)
322
339
  await player.play()
323
340
  if (state.position > 0) setTimeout(() => player.seek?.(state.position), SEEK_DELAY)
324
- if (state.paused) await player.pause(true)
341
+ if (state.paused) player.pause(true)
325
342
  }
326
343
  return player
327
344
  } finally {
@@ -348,7 +365,10 @@ class Aqua extends EventEmitter {
348
365
  this.emit(AqualinkEvents.NodeFailover, failedNode)
349
366
  const players = Array.from(failedNode.players || [])
350
367
  if (!players.length) return
351
- const available = Array.from(this.nodeMap.values()).filter(n => n !== failedNode && n.connected)
368
+ const available = []
369
+ for (const n of this.nodeMap.values()) {
370
+ if (n !== failedNode && n.connected) available.push(n)
371
+ }
352
372
  if (!available.length) throw new Error('No failover nodes')
353
373
  const results = await this._migratePlayersOptimized(players, available)
354
374
  const successful = results.filter(r => r.success).length
@@ -366,24 +386,24 @@ class Aqua extends EventEmitter {
366
386
  async _migratePlayersOptimized(players, nodes) {
367
387
  const loads = new Map()
368
388
  const counts = new Map()
369
- nodes.forEach(n => {
389
+ for (const n of nodes) {
370
390
  loads.set(n, this._getNodeLoad(n))
371
391
  counts.set(n, 0)
372
- })
392
+ }
373
393
  const pickNode = () => {
374
- const n = nodes.reduce((best, node) => {
375
- const score = loads.get(node) + counts.get(node)
376
- const bestScore = loads.get(best) + counts.get(best)
377
- return score < bestScore ? node : best
378
- })
379
- counts.set(n, counts.get(n) + 1)
380
- return n
394
+ let best = nodes[0], bestScore = loads.get(best) + counts.get(best)
395
+ for (let i = 1; i < nodes.length; i++) {
396
+ const score = loads.get(nodes[i]) + counts.get(nodes[i])
397
+ if (score < bestScore) { best = nodes[i]; bestScore = score }
398
+ }
399
+ counts.set(best, counts.get(best) + 1)
400
+ return best
381
401
  }
382
402
  const results = []
383
403
  for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPS) {
384
404
  const batch = players.slice(i, i + MAX_CONCURRENT_OPS)
385
405
  const batchResults = await Promise.allSettled(batch.map(p => this._migratePlayer(p, pickNode)))
386
- results.push(...batchResults.map(r => ({success: r.status === 'fulfilled', error: r.reason})))
406
+ for (const r of batchResults) results.push({success: r.status === 'fulfilled', error: r.reason})
387
407
  }
388
408
  return results
389
409
  }
@@ -391,7 +411,8 @@ class Aqua extends EventEmitter {
391
411
  async _migratePlayer(player, pickNode) {
392
412
  const state = this._capturePlayerState(player)
393
413
  if (!state) throw new Error('Failed to capture state')
394
- for (let retry = 0; retry < this.failoverOptions.maxRetries; retry++) {
414
+ const {maxRetries, retryDelay} = this.failoverOptions
415
+ for (let retry = 0; retry < maxRetries; retry++) {
395
416
  try {
396
417
  const targetNode = pickNode()
397
418
  const newPlayer = this._createPlayerOnNode(targetNode, state)
@@ -399,14 +420,15 @@ class Aqua extends EventEmitter {
399
420
  this.emit(AqualinkEvents.PlayerMigrated, player, newPlayer, targetNode)
400
421
  return newPlayer
401
422
  } catch (error) {
402
- if (retry === this.failoverOptions.maxRetries - 1) throw error
403
- await _delay(this.failoverOptions.retryDelay * Math.pow(1.5, retry))
423
+ if (retry === maxRetries - 1) throw error
424
+ await _functions.delay(retryDelay * Math.pow(1.5, retry))
404
425
  }
405
426
  }
406
427
  }
407
428
 
408
429
  _capturePlayerState(player) {
409
- return player ? {
430
+ if (!player) return null
431
+ return {
410
432
  guildId: player.guildId,
411
433
  textChannel: player.textChannel,
412
434
  voiceChannel: player.voiceChannel,
@@ -419,7 +441,7 @@ class Aqua extends EventEmitter {
419
441
  shuffle: player.shuffle,
420
442
  deaf: player.deaf ?? false,
421
443
  connected: !!player.connected
422
- } : null
444
+ }
423
445
  }
424
446
 
425
447
  _createPlayerOnNode(targetNode, state) {
@@ -447,20 +469,18 @@ class Aqua extends EventEmitter {
447
469
  if (state.paused) ops.push(newPlayer.pause(true))
448
470
  }
449
471
  }
450
- Object.assign(newPlayer, {loop: state.loop, shuffle: state.shuffle})
472
+ newPlayer.loop = state.loop
473
+ newPlayer.shuffle = state.shuffle
451
474
  await Promise.allSettled(ops)
452
475
  }
453
476
 
454
477
  updateVoiceState({d, t}) {
455
478
  if (!d?.guild_id || (t !== 'VOICE_STATE_UPDATE' && t !== 'VOICE_SERVER_UPDATE')) return
456
479
  const player = this.players.get(d.guild_id)
457
- if (!player) return
480
+ if (!player || !player.nodes?.connected) return
458
481
  if (t === 'VOICE_STATE_UPDATE') {
459
482
  if (d.user_id !== this.clientId) return
460
- if (!d.channel_id) {
461
- this.destroyPlayer(d.guild_id)
462
- return
463
- }
483
+ if (!d.channel_id) return void this.destroyPlayer(d.guild_id)
464
484
  if (player.connection) {
465
485
  player.connection.sessionId = d.session_id
466
486
  player.connection.setStateUpdate(d)
@@ -473,7 +493,10 @@ class Aqua extends EventEmitter {
473
493
  fetchRegion(region) {
474
494
  if (!region) return this.leastUsedNodes
475
495
  const lower = region.toLowerCase()
476
- const filtered = Array.from(this.nodeMap.values()).filter(n => n.connected && n.regions?.includes(lower))
496
+ const filtered = []
497
+ for (const n of this.nodeMap.values()) {
498
+ if (n.connected && n.regions?.includes(lower)) filtered.push(n)
499
+ }
477
500
  return Object.freeze(filtered.sort((a, b) => this._getNodeLoad(a) - this._getNodeLoad(b)))
478
501
  }
479
502
 
@@ -482,7 +505,7 @@ class Aqua extends EventEmitter {
482
505
  const existing = this.players.get(options.guildId)
483
506
  if (existing) {
484
507
  if (options.voiceChannel && existing.voiceChannel !== options.voiceChannel) {
485
- _safeCall(() => existing.connect(options))
508
+ _functions.safeCall(() => existing.connect(options))
486
509
  }
487
510
  return existing
488
511
  }
@@ -493,7 +516,7 @@ class Aqua extends EventEmitter {
493
516
 
494
517
  createPlayer(node, options) {
495
518
  const existing = this.players.get(options.guildId)
496
- if (existing) _safeCall(() => existing.destroy())
519
+ if (existing) _functions.safeCall(() => existing.destroy())
497
520
  const player = new Player(this, node, options)
498
521
  this.players.set(options.guildId, player)
499
522
  node?.players?.add?.(player)
@@ -513,22 +536,22 @@ class Aqua extends EventEmitter {
513
536
  const player = this.players.get(guildId)
514
537
  if (!player) return
515
538
  this.players.delete(guildId)
516
- _safeCall(() => player.removeAllListeners())
517
- await _safeCall(() => player.destroy())
539
+ _functions.safeCall(() => player.removeAllListeners())
540
+ await _functions.safeCall(() => player.destroy())
518
541
  }
519
542
 
520
- async resolve({query, source = this.defaultSearchPlatform, requester, nodes}) {
543
+ async resolve({query, source, requester, nodes}) {
521
544
  if (!this.initiated) throw new Error('Aqua not initialized')
522
545
  const node = this._getRequestNode(nodes)
523
546
  if (!node) throw new Error('No nodes available')
524
- const formatted = _formatQuery(query, source)
547
+ const formatted = _functions.formatQuery(query, source || this.defaultSearchPlatform)
525
548
  const endpoint = `/${this.restVersion}/loadtracks?identifier=${encodeURIComponent(formatted)}`
526
549
  try {
527
550
  const response = await node.rest.makeRequest('GET', endpoint)
528
551
  if (!response || response.loadType === 'empty' || response.loadType === 'NO_MATCHES') return EMPTY_TRACKS_RESPONSE
529
552
  return this._constructResponse(response, requester, node)
530
553
  } catch (error) {
531
- throw new Error(error?.name === 'AbortError' ? 'Request timeout' : `Resolve failed: ${error?.message || String(error)}`)
554
+ throw new Error(error?.name === 'AbortError' ? 'Request timeout' : `Resolve failed: ${error?.message || error}`)
532
555
  }
533
556
  }
534
557
 
@@ -549,11 +572,12 @@ class Aqua extends EventEmitter {
549
572
  _chooseLeastBusyNode(nodes) {
550
573
  if (!nodes?.length) return null
551
574
  if (nodes.length === 1) return nodes[0]
552
- return nodes.reduce((best, node) => {
553
- const score = this._getNodeLoad(node)
554
- const bestScore = this._getNodeLoad(best)
555
- return score < bestScore ? node : best
556
- })
575
+ let best = nodes[0], bestScore = this._getNodeLoad(best)
576
+ for (let i = 1; i < nodes.length; i++) {
577
+ const score = this._getNodeLoad(nodes[i])
578
+ if (score < bestScore) { best = nodes[i]; bestScore = score }
579
+ }
580
+ return best
557
581
  }
558
582
 
559
583
  _constructResponse(response, requester, node) {
@@ -563,25 +587,22 @@ class Aqua extends EventEmitter {
563
587
  base.exception = data || response.exception || null
564
588
  return base
565
589
  }
566
- switch (loadType) {
567
- case 'track':
568
- if (data) {
569
- base.pluginInfo = data.info?.pluginInfo || data.pluginInfo || base.pluginInfo
570
- base.tracks.push(_makeTrack(data, requester, node))
571
- }
572
- break
573
- case 'playlist':
574
- if (data) {
575
- const info = data.info || null
576
- const thumbnail = data.pluginInfo?.artworkUrl || data.tracks?.[0]?.info?.artworkUrl || null
577
- if (info) base.playlistInfo = {name: info.name || info.title, thumbnail, ...info}
578
- base.pluginInfo = data.pluginInfo || base.pluginInfo
579
- base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _makeTrack(t, requester, node)) : []
590
+ if (loadType === 'track' && data) {
591
+ base.pluginInfo = data.info?.pluginInfo || data.pluginInfo || base.pluginInfo
592
+ base.tracks.push(_functions.makeTrack(data, requester, node))
593
+ } else if (loadType === 'playlist' && data) {
594
+ const info = data.info
595
+ if (info) {
596
+ base.playlistInfo = {
597
+ name: info.name || info.title,
598
+ thumbnail: data.pluginInfo?.artworkUrl || data.tracks?.[0]?.info?.artworkUrl || null,
599
+ ...info
580
600
  }
581
- break
582
- case 'search':
583
- base.tracks = Array.isArray(data) ? data.map(t => _makeTrack(t, requester, node)) : []
584
- break
601
+ }
602
+ base.pluginInfo = data.pluginInfo || base.pluginInfo
603
+ base.tracks = Array.isArray(data.tracks) ? data.tracks.map(t => _functions.makeTrack(t, requester, node)) : []
604
+ } else if (loadType === 'search') {
605
+ base.tracks = Array.isArray(data) ? data.map(t => _functions.makeTrack(t, requester, node)) : []
585
606
  }
586
607
  return base
587
608
  }
@@ -592,7 +613,7 @@ class Aqua extends EventEmitter {
592
613
  return player
593
614
  }
594
615
 
595
- async search(query, requester, source = this.defaultSearchPlatform) {
616
+ async search(query, requester, source) {
596
617
  if (!query || !requester) return null
597
618
  try {
598
619
  const {tracks} = await this.resolve({query, source: source || this.defaultSearchPlatform, requester})
@@ -610,11 +631,10 @@ class Aqua extends EventEmitter {
610
631
  await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
611
632
  ws = fs.createWriteStream(tempFile, {encoding: 'utf8', flags: 'w'})
612
633
  const buffer = []
613
- let writePromise = Promise.resolve()
634
+ let drainPromise = Promise.resolve()
614
635
 
615
636
  for (const player of this.players.values()) {
616
637
  const requester = player.requester || player.current?.requester
617
- console.log(`Saving player for guild ${player.guildId} to ${player}`)
618
638
  const data = {
619
639
  g: player.guildId,
620
640
  t: player.textChannel,
@@ -636,36 +656,28 @@ class Aqua extends EventEmitter {
636
656
  const chunk = buffer.join('\n') + '\n'
637
657
  buffer.length = 0
638
658
  if (!ws.write(chunk)) {
639
- writePromise = writePromise.then(() => new Promise(resolve => ws.once('drain', resolve)))
659
+ drainPromise = drainPromise.then(() => new Promise(r => ws.once('drain', r)))
640
660
  }
641
661
  }
642
662
  }
643
663
 
644
- if (buffer.length) {
645
- const chunk = buffer.join('\n') + '\n'
646
- ws.write(chunk)
647
- }
648
-
649
- await writePromise
650
- await new Promise((resolve, reject) => {
651
- ws.end(err => err ? reject(err) : resolve())
652
- })
664
+ if (buffer.length) ws.write(buffer.join('\n') + '\n')
665
+ await drainPromise
666
+ await new Promise((resolve, reject) => ws.end(err => err ? reject(err) : resolve()))
653
667
  ws = null
654
-
655
668
  await fs.promises.rename(tempFile, filePath)
656
669
  } catch (error) {
657
670
  this.emit(AqualinkEvents.Error, null, error)
658
- if (ws) _safeCall(() => ws.destroy())
659
- await fs.promises.unlink(tempFile).catch(_noop)
671
+ if (ws) _functions.safeCall(() => ws.destroy())
672
+ await fs.promises.unlink(tempFile).catch(_functions.noop)
660
673
  } finally {
661
- await fs.promises.unlink(lockFile).catch(_noop)
674
+ await fs.promises.unlink(lockFile).catch(_functions.noop)
662
675
  }
663
676
  }
664
677
 
665
678
  async loadPlayers(filePath = './AquaPlayers.jsonl') {
666
679
  const lockFile = `${filePath}.lock`
667
- let stream = null
668
- let rl = null
680
+ let stream = null, rl = null
669
681
  try {
670
682
  await fs.promises.access(filePath)
671
683
  await fs.promises.writeFile(lockFile, String(process.pid), {flag: 'wx'})
@@ -677,26 +689,20 @@ class Aqua extends EventEmitter {
677
689
  const batch = []
678
690
  for await (const line of rl) {
679
691
  if (!line.trim()) continue
680
- try {
681
- batch.push(JSON.parse(line))
682
- } catch {
683
- continue
684
- }
692
+ try { batch.push(JSON.parse(line)) } catch { continue }
685
693
  if (batch.length >= PLAYER_BATCH_SIZE) {
686
694
  await Promise.allSettled(batch.map(p => this._restorePlayer(p)))
687
695
  batch.length = 0
688
696
  }
689
697
  }
690
698
  if (batch.length) await Promise.allSettled(batch.map(p => this._restorePlayer(p)))
691
-
692
- // Clear file after successful load
693
699
  await fs.promises.writeFile(filePath, '')
694
700
  } catch (err) {
695
701
  if (err.code !== 'ENOENT') this.emit(AqualinkEvents.Error, null, err)
696
702
  } finally {
697
- if (rl) _safeCall(() => rl.close())
698
- if (stream) _safeCall(() => stream.destroy())
699
- await fs.promises.unlink(lockFile).catch(_noop)
703
+ if (rl) _functions.safeCall(() => rl.close())
704
+ if (stream) _functions.safeCall(() => stream.destroy())
705
+ await fs.promises.unlink(lockFile).catch(_functions.noop)
700
706
  }
701
707
  }
702
708
 
@@ -708,13 +714,10 @@ class Aqua extends EventEmitter {
708
714
  voiceChannel: p.v,
709
715
  defaultVolume: p.vol || 65,
710
716
  deaf: true,
711
- // indicate this player is being restored from persisted state so Connection
712
- // can behave accordingly (attempt resume even if voice data is stale)
713
717
  resuming: !!p.resuming
714
718
  })
715
- // Ensure flag is set on existing players as well
716
719
  player._resuming = !!p.resuming
717
- const requester = this._parseRequester(p.r)
720
+ const requester = _functions.parseRequester(p.r)
718
721
  const tracksToResolve = [p.u, ...(p.q || [])].filter(Boolean).slice(0, MAX_TRACKS_RESTORE)
719
722
  const resolved = await Promise.all(tracksToResolve.map(uri => this.resolve({query: uri, requester}).catch(() => null)))
720
723
  const validTracks = resolved.flatMap(r => r?.tracks || [])
@@ -733,35 +736,26 @@ class Aqua extends EventEmitter {
733
736
  }
734
737
  if (p.nw && p.t) {
735
738
  const channel = this.client.channels?.cache?.get(p.t)
736
- if (channel?.messages) {
737
- player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
738
- }
739
+ if (channel?.messages) player.nowPlayingMessage = await channel.messages.fetch(p.nw).catch(() => null)
739
740
  }
740
741
  } catch {}
741
742
  }
742
743
 
743
- _parseRequester(str) {
744
- if (!str || typeof str !== 'string') return null
745
- const i = str.indexOf(':')
746
- return i > 0 ? {id: str.substring(0, i), username: str.substring(i + 1)} : null
747
- }
748
-
749
744
  async _waitForFirstNode(timeout = NODE_TIMEOUT) {
750
745
  if (this.leastUsedNodes.length) return
751
746
  return new Promise((resolve, reject) => {
752
- const onReady = () => {
753
- if (this.leastUsedNodes.length) {
754
- clearTimeout(timer)
755
- this.off(AqualinkEvents.NodeConnect, onReady)
756
- this.off(AqualinkEvents.NodeCreate, onReady)
757
- resolve()
758
- }
759
- }
760
- const timer = setTimeout(() => {
747
+ let resolved = false
748
+ const cleanup = () => {
749
+ if (resolved) return
750
+ resolved = true
751
+ clearTimeout(timer)
761
752
  this.off(AqualinkEvents.NodeConnect, onReady)
762
753
  this.off(AqualinkEvents.NodeCreate, onReady)
763
- reject(new Error('Timeout waiting for first node'))
764
- }, timeout)
754
+ }
755
+ const onReady = () => {
756
+ if (this.leastUsedNodes.length) { cleanup(); resolve() }
757
+ }
758
+ const timer = setTimeout(() => { cleanup(); reject(new Error('Timeout waiting for first node')) }, timeout)
765
759
  this.on(AqualinkEvents.NodeConnect, onReady)
766
760
  this.on(AqualinkEvents.NodeCreate, onReady)
767
761
  onReady()
@@ -770,24 +764,17 @@ class Aqua extends EventEmitter {
770
764
 
771
765
  _performCleanup() {
772
766
  const now = Date.now()
773
-
774
- // Cleanup expired broken players
775
767
  for (const [guildId, state] of this._brokenPlayers) {
776
768
  if (now - state.brokenAt > BROKEN_PLAYER_TTL) this._brokenPlayers.delete(guildId)
777
769
  }
778
-
779
- // Cleanup old failover attempts
780
770
  for (const [id, ts] of this._lastFailoverAttempt) {
781
771
  if (now - ts > FAILOVER_CLEANUP_TTL) {
782
772
  this._lastFailoverAttempt.delete(id)
783
773
  this._failoverQueue.delete(id)
784
774
  }
785
775
  }
786
-
787
776
  if (this._failoverQueue.size > MAX_FAILOVER_QUEUE) this._failoverQueue.clear()
788
777
  if (this._rebuildLocks.size > MAX_REBUILD_LOCKS) this._rebuildLocks.clear()
789
-
790
- // Cleanup orphaned node states
791
778
  for (const id of this._nodeStates.keys()) {
792
779
  if (!this.nodeMap.has(id)) this._nodeStates.delete(id)
793
780
  }
@@ -37,7 +37,8 @@ const AqualinkEvents = {
37
37
  PlayerCreated: 'playerCreated',
38
38
  PlayerConnected: 'playerConnected',
39
39
  PlayerDestroyed: 'playerDestroyed',
40
- PlayerMigrated: 'playerMigrated'
40
+ PlayerMigrated: 'playerMigrated',
41
+ PauseEvent: 'pauseEvent'
41
42
  };
42
43
 
43
44
  module.exports = { AqualinkEvents };