aqualink 3.1.0 → 3.2.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.
package/build/index.js CHANGED
@@ -51,9 +51,9 @@ const VoiceRegion = Object.freeze({
51
51
  CanadaToronto: 'yyz',
52
52
  CanadaMontreal: 'ymq',
53
53
 
54
- Brazil: 'gru' || 'brazil', // i only know this endpoint lol.
55
- Chile: 'scl' || 'chile',
56
- Argentina: 'eze' || 'argentina'
54
+ Brazil: 'gru', // i only know this endpoint lol.
55
+ Chile: 'scl',
56
+ Argentina: 'eze'
57
57
  })
58
58
 
59
59
  module.exports = {
@@ -1,4 +1,4 @@
1
- const fs = require('node:fs')
1
+ const fs = require('node:fs')
2
2
  const path = require('node:path')
3
3
  const _readline = require('node:readline')
4
4
  const { EventEmitter } = require('node:events')
@@ -7,7 +7,7 @@ const AquaRecovery = require('./AquaRecovery')
7
7
  const Node = require('./Node')
8
8
  const Player = require('./Player')
9
9
  const Track = require('./Track')
10
- const { reportSuppressedError } = require('./Reporting')
10
+ const { emitOperationalError, reportSuppressedError } = require('./Reporting')
11
11
  const { version: pkgVersion } = require('../../package.json')
12
12
 
13
13
  const SEARCH_PREFIX = ':'
@@ -73,10 +73,12 @@ const _functions = {
73
73
  isUrl: (query) => {
74
74
  if (typeof query !== 'string' || query.length <= 8) return false
75
75
  const q = query.trimStart()
76
- return q.startsWith('http://') || q.startsWith('https://')
76
+ return (
77
+ q.startsWith('http://') || q.startsWith('https://') || q.includes(':')
78
+ )
77
79
  },
78
80
  formatQuery(query, source) {
79
- return this.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
81
+ return _functions.isUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
80
82
  },
81
83
  makeTrack: (t, requester, node) => new Track(t, requester, node),
82
84
  safeCall(fn) {
@@ -492,12 +494,7 @@ class Aqua extends EventEmitter {
492
494
  (stats.memory ? stats.memory.used / reservable : 0) * 40 +
493
495
  (node.rest?.calls || 0) * 0.001
494
496
  if (this._nodeLoadCache.size >= MAX_CACHE_SIZE) {
495
- const iterator = this._nodeLoadCache.keys()
496
- while (this._nodeLoadCache.size >= MAX_CACHE_SIZE) {
497
- const oldest = iterator.next().value
498
- if (!oldest) break
499
- this._nodeLoadCache.delete(oldest)
500
- }
497
+ this._nodeLoadCache.delete(this._nodeLoadCache.keys().next().value)
501
498
  }
502
499
  this._nodeLoadCache.set(id, { load, time: now })
503
500
  return load
@@ -549,17 +546,12 @@ class Aqua extends EventEmitter {
549
546
  attempts: 0,
550
547
  lastAttempt: 0
551
548
  }
552
- try {
553
- await node.connect()
554
- this._failoverState[id].connected = true
555
- this._failoverState[id].failoverInProgress = false
556
- this._invalidateCache()
557
- this.emit(AqualinkEvents.NodeCreate, node)
558
- return node
559
- } catch (error) {
560
- this._cleanupNode(id)
561
- throw error
562
- }
549
+ await node.connect()
550
+ this._failoverState[id].connected = true
551
+ this._failoverState[id].failoverInProgress = false
552
+ this._invalidateCache()
553
+ this.emit(AqualinkEvents.NodeCreate, node)
554
+ return node
563
555
  }
564
556
 
565
557
  _destroyNode(id) {
@@ -762,9 +754,8 @@ class Aqua extends EventEmitter {
762
754
  query,
763
755
  source || this.defaultSearchPlatform
764
756
  )
765
- const endpoint = `/${this.restVersion}/loadtracks?identifier=${encodeURIComponent(formatted)}`
766
757
  try {
767
- const response = await node.rest.makeRequest('GET', endpoint)
758
+ const response = await node.rest.loadTracks(formatted)
768
759
  if (
769
760
  !response ||
770
761
  response.loadType === 'empty' ||
@@ -773,11 +764,13 @@ class Aqua extends EventEmitter {
773
764
  return EMPTY_TRACKS_RESPONSE
774
765
  return this._constructResponse(response, requester, node)
775
766
  } catch (error) {
776
- throw new Error(
777
- error?.name === 'AbortError'
778
- ? 'Request timeout'
779
- : `Resolve failed: ${error?.message || error}`
780
- )
767
+ if (error?.name === 'AbortError') throw new Error('Request timeout')
768
+ const err = new Error(`Resolve failed: ${error?.message || error}`)
769
+ if (error?.statusCode != null) err.statusCode = error.statusCode
770
+ if (error?.body) err.body = error.body
771
+ if (error?.url) err.url = error.url
772
+ err.cause = error
773
+ throw err
781
774
  }
782
775
  }
783
776
 
@@ -882,11 +875,35 @@ class Aqua extends EventEmitter {
882
875
  const lockFile = `${filePath}.lock`
883
876
  const tempFile = `${filePath}.tmp`
884
877
  let ws = null
878
+ let lockAcquired = false
885
879
  try {
886
880
  await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
881
+ lockAcquired = true
887
882
  ws = fs.createWriteStream(tempFile, { encoding: 'utf8', flags: 'w' })
883
+ let streamError = null
884
+ ws.on('error', (error) => {
885
+ streamError = error
886
+ })
888
887
  const buffer = []
889
- let drainPromise = Promise.resolve()
888
+ const write = (chunk) => {
889
+ if (ws.write(chunk)) return Promise.resolve()
890
+ return new Promise((resolve, reject) => {
891
+ const onDrain = () => {
892
+ cleanup()
893
+ resolve()
894
+ }
895
+ const onError = (error) => {
896
+ cleanup()
897
+ reject(error)
898
+ }
899
+ const cleanup = () => {
900
+ ws.off('drain', onDrain)
901
+ ws.off('error', onError)
902
+ }
903
+ ws.once('drain', onDrain)
904
+ ws.once('error', onError)
905
+ })
906
+ }
890
907
 
891
908
  const nodeSessions = {}
892
909
  for (const node of this.nodeMap.values()) {
@@ -918,29 +935,36 @@ class Aqua extends EventEmitter {
918
935
  if (buffer.length >= WRITE_BUFFER_SIZE) {
919
936
  const chunk = `${buffer.join('\n')}\n`
920
937
  buffer.length = 0
921
- if (!ws.write(chunk)) {
922
- drainPromise = drainPromise.then(
923
- () => new Promise((r) => ws.once('drain', r))
924
- )
925
- }
938
+ await write(chunk)
926
939
  }
927
940
  }
928
941
 
929
- if (buffer.length) ws.write(`${buffer.join('\n')}\n`)
930
- await drainPromise
931
- await new Promise((resolve, reject) =>
932
- ws.end((err) => (err ? reject(err) : resolve()))
933
- )
942
+ if (buffer.length) await write(`${buffer.join('\n')}\n`)
943
+ if (streamError) throw streamError
944
+ await new Promise((resolve, reject) => {
945
+ const onError = (error) => {
946
+ ws.off('finish', onFinish)
947
+ reject(error)
948
+ }
949
+ const onFinish = () => {
950
+ ws.off('error', onError)
951
+ resolve()
952
+ }
953
+ ws.once('error', onError)
954
+ ws.once('finish', onFinish)
955
+ ws.end()
956
+ })
934
957
  ws = null
935
958
  await fs.promises.rename(tempFile, filePath)
936
959
  } catch (error) {
937
960
  console.error(`[Aqua/Autoresume]Error saving players:`, error)
938
- this.emit(AqualinkEvents.Error, null, error)
961
+ emitOperationalError(this, null, error)
939
962
  if (ws) _functions.safeCall(() => ws.destroy())
940
963
  await fs.promises.unlink(tempFile).catch(_functions.noop)
941
964
  } finally {
942
965
  if (ws) _functions.safeCall(() => ws.destroy())
943
- await fs.promises.unlink(lockFile).catch(_functions.noop)
966
+ if (lockAcquired)
967
+ await fs.promises.unlink(lockFile).catch(_functions.noop)
944
968
  }
945
969
  }
946
970
 
@@ -2,7 +2,7 @@ const fs = require('node:fs')
2
2
  const path = require('node:path')
3
3
  const readline = require('node:readline')
4
4
  const { AqualinkEvents } = require('./AqualinkEvents')
5
- const { reportSuppressedError } = require('./Reporting')
5
+ const { emitOperationalError, reportSuppressedError } = require('./Reporting')
6
6
 
7
7
  class AquaRecovery {
8
8
  constructor(aqua, deps) {
@@ -221,7 +221,7 @@ class AquaRecovery {
221
221
  this.performCleanup()
222
222
  }
223
223
  } catch (error) {
224
- this.aqua.emit(AqualinkEvents.Error, null, error)
224
+ emitOperationalError(this.aqua, null, error)
225
225
  } finally {
226
226
  state.failoverInProgress = false
227
227
  }
@@ -267,12 +267,32 @@ class AquaRecovery {
267
267
  async () => {
268
268
  const state = this.capturePlayerState(player)
269
269
  if (!state) throw new Error('Failed to capture state')
270
+ const oldMessage = player.nowPlayingMessage || null
271
+ const oldVoice = player.connection
272
+ ? {
273
+ sid: player.connection.sessionId,
274
+ ep: player.connection.endpoint,
275
+ tok: player.connection.token,
276
+ reg: player.connection.region,
277
+ cid: player.connection.channelId
278
+ }
279
+ : null
280
+ player.destroy({
281
+ preserveClient: true,
282
+ skipRemote: true,
283
+ preserveMessage: true,
284
+ preserveTracks: true,
285
+ preserveReconnecting: true
286
+ })
270
287
  const { maxRetries, retryDelay } = this.aqua.failoverOptions
271
288
  for (let retry = 0; retry < maxRetries; retry++) {
289
+ let newPlayer = null
272
290
  try {
273
291
  const targetNode = pickNode()
274
- const newPlayer = this.createPlayerOnNode(targetNode, state)
292
+ newPlayer = this.createPlayerOnNode(targetNode, state)
293
+ this._applyVoiceBootstrap(newPlayer, oldVoice)
275
294
  await this.restorePlayerState(newPlayer, state)
295
+ if (oldMessage) newPlayer.nowPlayingMessage = oldMessage
276
296
  this.aqua.emit(
277
297
  AqualinkEvents.PlayerMigrated,
278
298
  player,
@@ -281,6 +301,13 @@ class AquaRecovery {
281
301
  )
282
302
  return newPlayer
283
303
  } catch (error) {
304
+ newPlayer?.destroy?.({
305
+ preserveClient: true,
306
+ skipRemote: true,
307
+ preserveMessage: true,
308
+ preserveTracks: true,
309
+ preserveReconnecting: true
310
+ })
284
311
  if (retry === maxRetries - 1) throw error
285
312
  await this._functions.delay(retryDelay * 1.5 ** retry)
286
313
  }
@@ -446,12 +473,13 @@ class AquaRecovery {
446
473
  if (!player || !guildId || !(position > 0)) return
447
474
  const seekOnce = (startedPlayer) => {
448
475
  if (startedPlayer.guildId !== guildId) return
476
+ this.aqua.off(AqualinkEvents.TrackStart, seekOnce)
477
+ player.removeListener('destroy', cleanup)
449
478
  this._functions.unrefTimeout(() => player.seek?.(position), delay)
450
479
  }
451
- this.aqua.once(AqualinkEvents.TrackStart, seekOnce)
452
- player.once('destroy', () =>
453
- this.aqua.off(AqualinkEvents.TrackStart, seekOnce)
454
- )
480
+ const cleanup = () => this.aqua.off(AqualinkEvents.TrackStart, seekOnce)
481
+ this.aqua.on(AqualinkEvents.TrackStart, seekOnce)
482
+ player.once('destroy', cleanup)
455
483
  }
456
484
 
457
485
  async restorePlayerState(newPlayer, state) {
@@ -487,10 +515,12 @@ class AquaRecovery {
487
515
  this.aqua._loading = true
488
516
  const lockFile = `${filePath}.lock`
489
517
  let stream = null,
490
- rl = null
518
+ rl = null,
519
+ lockAcquired = false
491
520
  try {
492
521
  await fs.promises.access(filePath)
493
522
  await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
523
+ lockAcquired = true
494
524
  await this.waitForFirstNode()
495
525
 
496
526
  stream = fs.createReadStream(filePath, { encoding: 'utf8' })
@@ -536,13 +566,14 @@ class AquaRecovery {
536
566
  } catch (error) {
537
567
  if (error.code !== 'ENOENT') {
538
568
  console.error(`[Aqua/Autoresume]Error loading players:`, error)
539
- this.aqua.emit(AqualinkEvents.Error, null, error)
569
+ emitOperationalError(this.aqua, null, error)
540
570
  }
541
571
  } finally {
542
572
  this.aqua._loading = false
543
573
  if (rl) this._functions.safeCall(() => rl.close())
544
574
  if (stream) this._functions.safeCall(() => stream.destroy())
545
- await fs.promises.unlink(lockFile).catch(this._functions.noop)
575
+ if (lockAcquired)
576
+ await fs.promises.unlink(lockFile).catch(this._functions.noop)
546
577
  }
547
578
  }
548
579
 
@@ -36,6 +36,8 @@ const AqualinkEvents = {
36
36
  PlayerDestroyed: 'playerDestroy',
37
37
  PlayerMigrated: 'playerMigrated',
38
38
  PauseEvent: 'pauseEvent',
39
+ PlayerReconnectingEvent: 'PlayerReconnectingEvent',
40
+ PlayerReconnect: 'playerReconnect',
39
41
  MixStarted: 'mixStarted',
40
42
  MixEnded: 'mixEnded'
41
43
  }
@@ -76,6 +76,7 @@ const _functions = {
76
76
  v.endpoint = conn.endpoint
77
77
  v.sessionId = conn.sessionId
78
78
  v.channelId = player.voiceChannel
79
+ v.resuming = player?._resuming ?? false
79
80
  payload.data.volume = player?.volume ?? 100
80
81
  return payload
81
82
  }
@@ -168,7 +169,8 @@ class Connection {
168
169
  STATE,
169
170
  RECONNECT_DELAY,
170
171
  MAX_RECONNECT_ATTEMPTS,
171
- RESUME_BACKOFF_MAX
172
+ RESUME_BACKOFF_MAX,
173
+ sharedPool
172
174
  })
173
175
  }
174
176
 
@@ -240,6 +242,12 @@ class Connection {
240
242
  })
241
243
  }
242
244
  this.isWaitingForDisconnect = true
245
+ if (this.voiceChannel !== null) {
246
+ const oldChannel = this.voiceChannel
247
+ this.voiceChannel = null
248
+ if (p) p.voiceChannel = null
249
+ this._aqua.emit(AqualinkEvents.PlayerMove, p, oldChannel, null)
250
+ }
243
251
  if (!this._nullChannelTimer) {
244
252
  this._nullChannelTimer = setTimeout(() => {
245
253
  this._nullChannelTimer = null
@@ -9,6 +9,7 @@ class ConnectionRecovery {
9
9
  this.RECONNECT_DELAY = deps.RECONNECT_DELAY
10
10
  this.MAX_RECONNECT_ATTEMPTS = deps.MAX_RECONNECT_ATTEMPTS
11
11
  this.RESUME_BACKOFF_MAX = deps.RESUME_BACKOFF_MAX
12
+ this.sharedPool = deps.sharedPool
12
13
  }
13
14
 
14
15
  setServerUpdate(data) {
@@ -153,7 +154,7 @@ class ConnectionRecovery {
153
154
  `Attempt resume: guild=${conn._guildId} endpoint=${conn.endpoint} session=${conn.sessionId}`
154
155
  )
155
156
 
156
- const payload = sharedPool.acquire()
157
+ const payload = this.sharedPool.acquire()
157
158
  try {
158
159
  this._functions.fillVoicePayload(
159
160
  payload,
@@ -242,7 +243,7 @@ class ConnectionRecovery {
242
243
  return false
243
244
  } finally {
244
245
  conn._stateFlags &= ~this.STATE.ATTEMPTING_RESUME
245
- sharedPool.release(payload)
246
+ this.sharedPool.release(payload)
246
247
  }
247
248
  }
248
249
 
@@ -386,40 +387,4 @@ class ConnectionRecovery {
386
387
  }
387
388
  }
388
389
 
389
- class PayloadPool {
390
- constructor() {
391
- this._pool = []
392
- this._size = 0
393
- }
394
-
395
- _create() {
396
- return {
397
- guildId: null,
398
- data: {
399
- voice: {
400
- token: null,
401
- endpoint: null,
402
- sessionId: null
403
- },
404
- volume: null
405
- }
406
- }
407
- }
408
-
409
- acquire() {
410
- return this._size > 0 ? this._pool[--this._size] : this._create()
411
- }
412
-
413
- release(payload) {
414
- if (!payload || this._size >= 12) return
415
- payload.guildId = null
416
- const v = payload.data.voice
417
- v.token = v.endpoint = v.sessionId = null
418
- payload.data.volume = null
419
- this._pool[this._size++] = payload
420
- }
421
- }
422
-
423
- const sharedPool = new PayloadPool()
424
-
425
390
  module.exports = ConnectionRecovery