aqualink 3.0.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 = {
@@ -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) {
@@ -191,7 +193,10 @@ class Aqua extends EventEmitter {
191
193
 
192
194
  _trace(event, data = null) {
193
195
  if (!this.debugTrace) return
194
- if (!this._traceBuffer || this._traceBuffer.length !== this.traceMaxEntries) {
196
+ if (
197
+ !this._traceBuffer ||
198
+ this._traceBuffer.length !== this.traceMaxEntries
199
+ ) {
195
200
  this._traceBuffer = new Array(this.traceMaxEntries)
196
201
  this._traceBufferCount = 0
197
202
  this._traceBufferIndex = 0
@@ -346,9 +351,14 @@ class Aqua extends EventEmitter {
346
351
  this._invalidateCache()
347
352
  queueMicrotask(() => {
348
353
  this._storeBrokenPlayers(node).catch((error) =>
349
- reportSuppressedError(this, 'aqua.nodeDisconnect.storeBrokenPlayers', error, {
350
- node: node?.name || node?.host
351
- })
354
+ reportSuppressedError(
355
+ this,
356
+ 'aqua.nodeDisconnect.storeBrokenPlayers',
357
+ error,
358
+ {
359
+ node: node?.name || node?.host
360
+ }
361
+ )
352
362
  )
353
363
  this._performCleanup()
354
364
  })
@@ -484,12 +494,7 @@ class Aqua extends EventEmitter {
484
494
  (stats.memory ? stats.memory.used / reservable : 0) * 40 +
485
495
  (node.rest?.calls || 0) * 0.001
486
496
  if (this._nodeLoadCache.size >= MAX_CACHE_SIZE) {
487
- const iterator = this._nodeLoadCache.keys()
488
- while (this._nodeLoadCache.size >= MAX_CACHE_SIZE) {
489
- const oldest = iterator.next().value
490
- if (!oldest) break
491
- this._nodeLoadCache.delete(oldest)
492
- }
497
+ this._nodeLoadCache.delete(this._nodeLoadCache.keys().next().value)
493
498
  }
494
499
  this._nodeLoadCache.set(id, { load, time: now })
495
500
  return load
@@ -541,17 +546,12 @@ class Aqua extends EventEmitter {
541
546
  attempts: 0,
542
547
  lastAttempt: 0
543
548
  }
544
- try {
545
- await node.connect()
546
- this._failoverState[id].connected = true
547
- this._failoverState[id].failoverInProgress = false
548
- this._invalidateCache()
549
- this.emit(AqualinkEvents.NodeCreate, node)
550
- return node
551
- } catch (error) {
552
- this._cleanupNode(id)
553
- throw error
554
- }
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
555
555
  }
556
556
 
557
557
  _destroyNode(id) {
@@ -754,9 +754,8 @@ class Aqua extends EventEmitter {
754
754
  query,
755
755
  source || this.defaultSearchPlatform
756
756
  )
757
- const endpoint = `/${this.restVersion}/loadtracks?identifier=${encodeURIComponent(formatted)}`
758
757
  try {
759
- const response = await node.rest.makeRequest('GET', endpoint)
758
+ const response = await node.rest.loadTracks(formatted)
760
759
  if (
761
760
  !response ||
762
761
  response.loadType === 'empty' ||
@@ -765,11 +764,13 @@ class Aqua extends EventEmitter {
765
764
  return EMPTY_TRACKS_RESPONSE
766
765
  return this._constructResponse(response, requester, node)
767
766
  } catch (error) {
768
- throw new Error(
769
- error?.name === 'AbortError'
770
- ? 'Request timeout'
771
- : `Resolve failed: ${error?.message || error}`
772
- )
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
773
774
  }
774
775
  }
775
776
 
@@ -874,11 +875,35 @@ class Aqua extends EventEmitter {
874
875
  const lockFile = `${filePath}.lock`
875
876
  const tempFile = `${filePath}.tmp`
876
877
  let ws = null
878
+ let lockAcquired = false
877
879
  try {
878
880
  await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
881
+ lockAcquired = true
879
882
  ws = fs.createWriteStream(tempFile, { encoding: 'utf8', flags: 'w' })
883
+ let streamError = null
884
+ ws.on('error', (error) => {
885
+ streamError = error
886
+ })
880
887
  const buffer = []
881
- 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
+ }
882
907
 
883
908
  const nodeSessions = {}
884
909
  for (const node of this.nodeMap.values()) {
@@ -910,29 +935,36 @@ class Aqua extends EventEmitter {
910
935
  if (buffer.length >= WRITE_BUFFER_SIZE) {
911
936
  const chunk = `${buffer.join('\n')}\n`
912
937
  buffer.length = 0
913
- if (!ws.write(chunk)) {
914
- drainPromise = drainPromise.then(
915
- () => new Promise((r) => ws.once('drain', r))
916
- )
917
- }
938
+ await write(chunk)
918
939
  }
919
940
  }
920
941
 
921
- if (buffer.length) ws.write(`${buffer.join('\n')}\n`)
922
- await drainPromise
923
- await new Promise((resolve, reject) =>
924
- ws.end((err) => (err ? reject(err) : resolve()))
925
- )
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
+ })
926
957
  ws = null
927
958
  await fs.promises.rename(tempFile, filePath)
928
959
  } catch (error) {
929
960
  console.error(`[Aqua/Autoresume]Error saving players:`, error)
930
- this.emit(AqualinkEvents.Error, null, error)
961
+ emitOperationalError(this, null, error)
931
962
  if (ws) _functions.safeCall(() => ws.destroy())
932
963
  await fs.promises.unlink(tempFile).catch(_functions.noop)
933
964
  } finally {
934
965
  if (ws) _functions.safeCall(() => ws.destroy())
935
- await fs.promises.unlink(lockFile).catch(_functions.noop)
966
+ if (lockAcquired)
967
+ await fs.promises.unlink(lockFile).catch(_functions.noop)
936
968
  }
937
969
  }
938
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) {
@@ -112,7 +112,10 @@ class AquaRecovery {
112
112
  const batch = rebuilds.slice(i, i + this.MAX_CONCURRENT_OPS)
113
113
  const results = await Promise.allSettled(
114
114
  batch.map((state) =>
115
- this.restorePlayer(state, node).then((ok) => ({ ok, guildId: state.g }))
115
+ this.restorePlayer(state, node).then((ok) => ({
116
+ ok,
117
+ guildId: state.g
118
+ }))
116
119
  )
117
120
  )
118
121
  for (let j = 0; j < results.length; j++) {
@@ -218,7 +221,7 @@ class AquaRecovery {
218
221
  this.performCleanup()
219
222
  }
220
223
  } catch (error) {
221
- this.aqua.emit(AqualinkEvents.Error, null, error)
224
+ emitOperationalError(this.aqua, null, error)
222
225
  } finally {
223
226
  state.failoverInProgress = false
224
227
  }
@@ -264,12 +267,32 @@ class AquaRecovery {
264
267
  async () => {
265
268
  const state = this.capturePlayerState(player)
266
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
+ })
267
287
  const { maxRetries, retryDelay } = this.aqua.failoverOptions
268
288
  for (let retry = 0; retry < maxRetries; retry++) {
289
+ let newPlayer = null
269
290
  try {
270
291
  const targetNode = pickNode()
271
- const newPlayer = this.createPlayerOnNode(targetNode, state)
292
+ newPlayer = this.createPlayerOnNode(targetNode, state)
293
+ this._applyVoiceBootstrap(newPlayer, oldVoice)
272
294
  await this.restorePlayerState(newPlayer, state)
295
+ if (oldMessage) newPlayer.nowPlayingMessage = oldMessage
273
296
  this.aqua.emit(
274
297
  AqualinkEvents.PlayerMigrated,
275
298
  player,
@@ -278,6 +301,13 @@ class AquaRecovery {
278
301
  )
279
302
  return newPlayer
280
303
  } catch (error) {
304
+ newPlayer?.destroy?.({
305
+ preserveClient: true,
306
+ skipRemote: true,
307
+ preserveMessage: true,
308
+ preserveTracks: true,
309
+ preserveReconnecting: true
310
+ })
281
311
  if (retry === maxRetries - 1) throw error
282
312
  await this._functions.delay(retryDelay * 1.5 ** retry)
283
313
  }
@@ -443,12 +473,13 @@ class AquaRecovery {
443
473
  if (!player || !guildId || !(position > 0)) return
444
474
  const seekOnce = (startedPlayer) => {
445
475
  if (startedPlayer.guildId !== guildId) return
476
+ this.aqua.off(AqualinkEvents.TrackStart, seekOnce)
477
+ player.removeListener('destroy', cleanup)
446
478
  this._functions.unrefTimeout(() => player.seek?.(position), delay)
447
479
  }
448
- this.aqua.once(AqualinkEvents.TrackStart, seekOnce)
449
- player.once('destroy', () =>
450
- this.aqua.off(AqualinkEvents.TrackStart, seekOnce)
451
- )
480
+ const cleanup = () => this.aqua.off(AqualinkEvents.TrackStart, seekOnce)
481
+ this.aqua.on(AqualinkEvents.TrackStart, seekOnce)
482
+ player.once('destroy', cleanup)
452
483
  }
453
484
 
454
485
  async restorePlayerState(newPlayer, state) {
@@ -484,10 +515,12 @@ class AquaRecovery {
484
515
  this.aqua._loading = true
485
516
  const lockFile = `${filePath}.lock`
486
517
  let stream = null,
487
- rl = null
518
+ rl = null,
519
+ lockAcquired = false
488
520
  try {
489
521
  await fs.promises.access(filePath)
490
522
  await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
523
+ lockAcquired = true
491
524
  await this.waitForFirstNode()
492
525
 
493
526
  stream = fs.createReadStream(filePath, { encoding: 'utf8' })
@@ -533,13 +566,14 @@ class AquaRecovery {
533
566
  } catch (error) {
534
567
  if (error.code !== 'ENOENT') {
535
568
  console.error(`[Aqua/Autoresume]Error loading players:`, error)
536
- this.aqua.emit(AqualinkEvents.Error, null, error)
569
+ emitOperationalError(this.aqua, null, error)
537
570
  }
538
571
  } finally {
539
572
  this.aqua._loading = false
540
573
  if (rl) this._functions.safeCall(() => rl.close())
541
574
  if (stream) this._functions.safeCall(() => stream.destroy())
542
- await fs.promises.unlink(lockFile).catch(this._functions.noop)
575
+ if (lockAcquired)
576
+ await fs.promises.unlink(lockFile).catch(this._functions.noop)
543
577
  }
544
578
  }
545
579
 
@@ -551,8 +585,9 @@ class AquaRecovery {
551
585
  if (existing?.playing && !existing.destroyed) return true
552
586
  if (existing?.destroyed) this.aqua.players.delete(gId)
553
587
 
554
- const targetNode =
555
- preferredNode?.connected ? preferredNode : this.aqua.leastUsedNodes[0]
588
+ const targetNode = preferredNode?.connected
589
+ ? preferredNode
590
+ : this.aqua.leastUsedNodes[0]
556
591
  if (!targetNode?.connected) {
557
592
  throw new Error(`No connected node available to restore guild ${gId}`)
558
593
  }
@@ -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
@@ -321,7 +329,9 @@ class Connection {
321
329
  } catch (e) {
322
330
  this._aqua?.emit?.(
323
331
  AqualinkEvents.Debug,
324
- new Error(`Player destroy failed: ${e?.message || e}`)
332
+ new Error(
333
+ `Player destroy failed (guild=${this._guildId}, sessionId=${this.sessionId || 'none'}): ${e?.message || e}`
334
+ )
325
335
  )
326
336
  } finally {
327
337
  this._stateFlags &= ~STATE.DISCONNECTING
@@ -452,6 +462,15 @@ class Connection {
452
462
 
453
463
  _executeVoiceUpdate() {
454
464
  if (this._destroyed) return
465
+ if (this._stateFlags & STATE.DISCONNECTING) {
466
+ this._stateFlags &= ~STATE.UPDATE_SCHEDULED
467
+ this._voiceFlushTimer = null
468
+ if (this._pendingUpdate) {
469
+ sharedPool.release(this._pendingUpdate.payload)
470
+ this._pendingUpdate = null
471
+ }
472
+ return
473
+ }
455
474
  this._stateFlags &= ~STATE.UPDATE_SCHEDULED
456
475
  this._voiceFlushTimer = null
457
476
 
@@ -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,
@@ -163,6 +164,14 @@ class ConnectionRecovery {
163
164
  true
164
165
  )
165
166
 
167
+ if (conn._destroyed || !conn._player || conn._player.destroyed) {
168
+ conn._aqua.emit(
169
+ AqualinkEvents.Debug,
170
+ `Resume aborted: player destroyed during attempt for guild ${conn._guildId}`
171
+ )
172
+ return false
173
+ }
174
+
166
175
  if (conn._stateGeneration !== currentGen) {
167
176
  conn._aqua.emit(
168
177
  AqualinkEvents.Debug,
@@ -178,6 +187,10 @@ class ConnectionRecovery {
178
187
  })
179
188
  }
180
189
 
190
+ if (conn._destroyed || conn._player?.destroyed) {
191
+ return false
192
+ }
193
+
181
194
  conn._reconnectAttempts = 0
182
195
  conn._consecutiveFailures = 0
183
196
  if (conn._player) conn._player._resuming = false
@@ -189,10 +202,17 @@ class ConnectionRecovery {
189
202
  return true
190
203
  } catch (error) {
191
204
  if (conn._destroyed || !conn._aqua) throw error
205
+ if (conn._player?.destroyed) {
206
+ conn._aqua.emit(
207
+ AqualinkEvents.Debug,
208
+ `Resume aborted: player destroyed during retry for guild ${conn._guildId}`
209
+ )
210
+ return false
211
+ }
192
212
  conn._consecutiveFailures++
193
213
  conn._aqua.emit(
194
214
  AqualinkEvents.Debug,
195
- `Resume failed for guild ${conn._guildId}: ${error?.message || error}`
215
+ `Resume failed for guild ${conn._guildId} (sessionId=${conn.sessionId || 'none'}, endpoint=${conn.endpoint || 'none'}): ${error?.message || error}`
196
216
  )
197
217
  if (conn._aqua?.debugTrace) {
198
218
  conn._aqua._trace('connection.resume.error', {
@@ -204,7 +224,8 @@ class ConnectionRecovery {
204
224
  if (
205
225
  conn._reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS &&
206
226
  !conn._destroyed &&
207
- conn._consecutiveFailures < 5
227
+ conn._consecutiveFailures < 5 &&
228
+ !conn._player?.destroyed
208
229
  ) {
209
230
  const delay = Math.min(
210
231
  this.RECONNECT_DELAY * (1 << (conn._reconnectAttempts - 1)),
@@ -222,7 +243,7 @@ class ConnectionRecovery {
222
243
  return false
223
244
  } finally {
224
245
  conn._stateFlags &= ~this.STATE.ATTEMPTING_RESUME
225
- sharedPool.release(payload)
246
+ this.sharedPool.release(payload)
226
247
  }
227
248
  }
228
249
 
@@ -265,7 +286,10 @@ class ConnectionRecovery {
265
286
  return false
266
287
  })
267
288
  if (resumed) {
268
- conn._player?._clearVoiceRecovery?.(recoveryToken, 'missing_player_resumed')
289
+ conn._player?._clearVoiceRecovery?.(
290
+ recoveryToken,
291
+ 'missing_player_resumed'
292
+ )
269
293
  } else if (conn._player?._isVoiceRecoveryActive?.(recoveryToken)) {
270
294
  conn.resendVoiceUpdate(true)
271
295
  }
@@ -306,8 +330,12 @@ class ConnectionRecovery {
306
330
 
307
331
  async sendUpdate(payload) {
308
332
  const conn = this.connection
309
- if (conn._destroyed) throw new Error('Connection destroyed')
310
- if (!conn._rest) throw new Error('REST interface unavailable')
333
+ if (conn._destroyed)
334
+ throw new Error(`Connection destroyed (guild=${conn._guildId})`)
335
+ if (!conn._rest)
336
+ throw new Error(
337
+ `REST interface unavailable (guild=${conn._guildId}, sessionId=${conn.sessionId || 'none'})`
338
+ )
311
339
 
312
340
  try {
313
341
  if (conn._aqua?.debugTrace) {
@@ -342,7 +370,7 @@ class ConnectionRecovery {
342
370
  if (conn._aqua) {
343
371
  conn._aqua.emit(
344
372
  AqualinkEvents.Debug,
345
- `[Aqua/Connection] Player ${conn._guildId} not found (404)${isSessionError ? ' - Session invalid' : ''}. Recovery failed, destroying.`
373
+ `[Aqua/Connection] Player ${conn._guildId} not found (404, sessionId=${conn.sessionId || 'none'}, endpoint=${conn.endpoint || 'none'})${isSessionError ? ' - Session invalid' : ''}. Recovery failed, destroying.`
346
374
  )
347
375
  await conn._aqua.destroyPlayer(conn._guildId)
348
376
  }
@@ -359,40 +387,4 @@ class ConnectionRecovery {
359
387
  }
360
388
  }
361
389
 
362
- class PayloadPool {
363
- constructor() {
364
- this._pool = []
365
- this._size = 0
366
- }
367
-
368
- _create() {
369
- return {
370
- guildId: null,
371
- data: {
372
- voice: {
373
- token: null,
374
- endpoint: null,
375
- sessionId: null
376
- },
377
- volume: null
378
- }
379
- }
380
- }
381
-
382
- acquire() {
383
- return this._size > 0 ? this._pool[--this._size] : this._create()
384
- }
385
-
386
- release(payload) {
387
- if (!payload || this._size >= 12) return
388
- payload.guildId = null
389
- const v = payload.data.voice
390
- v.token = v.endpoint = v.sessionId = null
391
- payload.data.volume = null
392
- this._pool[this._size++] = payload
393
- }
394
- }
395
-
396
- const sharedPool = new PayloadPool()
397
-
398
390
  module.exports = ConnectionRecovery
@@ -202,7 +202,8 @@ class Filters {
202
202
  }
203
203
 
204
204
  _scheduleUpdate() {
205
- if (this._pendingUpdate || !this.player) return this
205
+ if (this._pendingUpdate || !this.player || this.player.destroyed)
206
+ return this
206
207
  this._pendingUpdate = true
207
208
  queueMicrotask(() => {
208
209
  this._pendingUpdate = false
@@ -398,6 +399,7 @@ class Filters {
398
399
  }
399
400
 
400
401
  async updateFilters() {
402
+ this._pendingUpdate = false
401
403
  if (!this.player || !this._dirty.size) return this
402
404
 
403
405
  const dirtyKeys = [...this._dirty]