aqualink 2.20.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  const { AqualinkEvents } = require('./AqualinkEvents')
2
+ const ConnectionRecovery = require('./ConnectionRecovery')
3
+ const { reportSuppressedError } = require('./Reporting')
2
4
 
3
5
  const POOL_SIZE = 12
4
6
  const UPDATE_TIMEOUT = 4000
@@ -156,8 +158,18 @@ class Connection {
156
158
  this.isWaitingForDisconnect = false
157
159
 
158
160
  this._lastStateReqAt = 0
161
+ this._lastResumeBlockedLogAt = 0
159
162
  this._stateGeneration = 0
160
163
  this._regionMigrationAttempted = false
164
+ this._missingPlayerRecovering = false
165
+ this._lastMissingPlayerRecoverAt = 0
166
+ this._recovery = new ConnectionRecovery(this, {
167
+ _functions,
168
+ STATE,
169
+ RECONNECT_DELAY,
170
+ MAX_RECONNECT_ATTEMPTS,
171
+ RESUME_BACKOFF_MAX
172
+ })
161
173
  }
162
174
 
163
175
  _hasValidVoiceData() {
@@ -191,96 +203,11 @@ class Connection {
191
203
  }
192
204
 
193
205
  setServerUpdate(data) {
194
- if (this._destroyed || !data?.token) return
195
-
196
- const endpoint =
197
- typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
198
- if (!endpoint) return
199
-
200
- if (this._lastEndpoint === endpoint && this.token === data.token) return
201
-
202
- if (data.txId && data.txId < this.txId) return
203
-
204
- this._stateGeneration++
205
-
206
- if (this._lastEndpoint !== endpoint) {
207
- this.sequence = 0
208
- this._lastEndpoint = endpoint
209
- this._reconnectAttempts = 0
210
- this._consecutiveFailures = 0
211
- this._regionMigrationAttempted = false
212
- }
213
-
214
- this.endpoint = endpoint
215
- this.region = _functions.extractRegion(endpoint)
216
- this.token = data.token
217
- this.channelId = data.channel_id || this.channelId || this.voiceChannel
218
- this._lastVoiceDataUpdate = Date.now()
219
- this._aqua?._trace?.('connection.serverUpdate', {
220
- guildId: this._guildId,
221
- endpoint: this.endpoint,
222
- region: this.region,
223
- txId: data.txId || null
224
- })
225
- this._stateFlags &= ~STATE.VOICE_DATA_STALE
226
-
227
- if (this._player?.paused) this._player.pause(false)
228
- const migrated = this._checkRegionMigration()
229
- if (migrated) return
230
- this._scheduleVoiceUpdate()
231
- this._player?._flushDeferredPlay?.()
206
+ return this._recovery.setServerUpdate(data)
232
207
  }
233
208
 
234
209
  _checkRegionMigration() {
235
- if (this._destroyed || this._regionMigrationAttempted) return false
236
- if (
237
- !this._aqua?.autoRegionMigrate ||
238
- !this.region ||
239
- this.region === 'unknown'
240
- )
241
- return false
242
- const player = this._player
243
- if (!player || player.destroyed || player._resuming || player._reconnecting)
244
- return false
245
-
246
- const currentNode = player.nodes
247
- if (!currentNode) return false
248
-
249
- const currentRegions = Array.isArray(currentNode.regions)
250
- ? currentNode.regions
251
- : []
252
- const alreadyMatching = currentRegions.some((r) =>
253
- this._aqua._regionMatches?.(r, this.region)
254
- )
255
- if (alreadyMatching) {
256
- this._regionMigrationAttempted = true
257
- return false
258
- }
259
-
260
- const targetNode = this._aqua._findBestNodeForRegion?.(this.region)
261
- if (!targetNode || targetNode === currentNode) return false
262
-
263
- this._regionMigrationAttempted = true
264
- this._aqua?._trace?.('connection.region.migrate', {
265
- guildId: this._guildId,
266
- region: this.region,
267
- from: currentNode?.name || currentNode?.host,
268
- to: targetNode?.name || targetNode?.host
269
- })
270
-
271
- queueMicrotask(() => {
272
- this._aqua
273
- .movePlayerToNode?.(this._guildId, targetNode, 'region')
274
- .catch((err) => {
275
- this._regionMigrationAttempted = false
276
- this._aqua?._trace?.('connection.region.migrate.error', {
277
- guildId: this._guildId,
278
- region: this.region,
279
- error: err?.message || String(err)
280
- })
281
- })
282
- })
283
- return true
210
+ return this._recovery.checkRegionMigration()
284
211
  }
285
212
 
286
213
  resendVoiceUpdate(force = false) {
@@ -306,10 +233,12 @@ class Connection {
306
233
  if (data.txId && data.txId < this.txId) return
307
234
 
308
235
  if (!channelId) {
309
- this._aqua?._trace?.('connection.stateUpdate.nullChannel', {
310
- guildId: this._guildId,
311
- txId: data.txId || null
312
- })
236
+ if (this._aqua?.debugTrace) {
237
+ this._aqua._trace('connection.stateUpdate.nullChannel', {
238
+ guildId: this._guildId,
239
+ txId: data.txId || null
240
+ })
241
+ }
313
242
  this.isWaitingForDisconnect = true
314
243
  if (!this._nullChannelTimer) {
315
244
  this._nullChannelTimer = setTimeout(() => {
@@ -322,12 +251,14 @@ class Connection {
322
251
  }
323
252
 
324
253
  this.isWaitingForDisconnect = false
325
- this._aqua?._trace?.('connection.stateUpdate', {
326
- guildId: this._guildId,
327
- channelId,
328
- sessionId,
329
- txId: data.txId || null
330
- })
254
+ if (this._aqua?.debugTrace) {
255
+ this._aqua._trace('connection.stateUpdate', {
256
+ guildId: this._guildId,
257
+ channelId,
258
+ sessionId,
259
+ txId: data.txId || null
260
+ })
261
+ }
331
262
 
332
263
  if (p && p.txId > this.txId) this.txId = p.txId
333
264
 
@@ -368,9 +299,11 @@ class Connection {
368
299
 
369
300
  this._stateFlags =
370
301
  (this._stateFlags | STATE.DISCONNECTING) & ~STATE.CONNECTED
371
- this._aqua?._trace?.('connection.disconnect', {
372
- guildId: this._guildId
373
- })
302
+ if (this._aqua?.debugTrace) {
303
+ this._aqua._trace('connection.disconnect', {
304
+ guildId: this._guildId
305
+ })
306
+ }
374
307
  this._clearNullChannelTimer()
375
308
  this._clearPendingUpdate()
376
309
  this._clearReconnectTimer()
@@ -388,7 +321,9 @@ class Connection {
388
321
  } catch (e) {
389
322
  this._aqua?.emit?.(
390
323
  AqualinkEvents.Debug,
391
- new Error(`Player destroy failed: ${e?.message || e}`)
324
+ new Error(
325
+ `Player destroy failed (guild=${this._guildId}, sessionId=${this.sessionId || 'none'}): ${e?.message || e}`
326
+ )
392
327
  )
393
328
  } finally {
394
329
  this._stateFlags &= ~STATE.DISCONNECTING
@@ -399,126 +334,30 @@ class Connection {
399
334
  try {
400
335
  const now = Date.now()
401
336
  if (now - (this._lastStateReqAt || 0) < 1500) return false
402
- this._lastStateReqAt = now
403
337
 
404
338
  if (
405
339
  typeof this._player?.send !== 'function' ||
406
340
  !this._player.voiceChannel
407
341
  )
408
342
  return false
409
- this._player.send({
343
+ const queued = this._player.send({
410
344
  guild_id: this._guildId,
411
345
  channel_id: this._player.voiceChannel,
412
346
  self_deaf: this._player.deaf,
413
347
  self_mute: this._player.mute
414
348
  })
415
- return true
416
- } catch {
349
+ this._lastStateReqAt = now
350
+ return queued !== false
351
+ } catch (error) {
352
+ reportSuppressedError(this._aqua, 'connection.requestVoiceState', error, {
353
+ guildId: this._guildId
354
+ })
417
355
  return false
418
356
  }
419
357
  }
420
358
 
421
359
  async attemptResume() {
422
- if (!this._canAttemptResumeCore()) return false
423
- this._aqua?._trace?.('connection.resume.attempt', {
424
- guildId: this._guildId,
425
- reconnectAttempts: this._reconnectAttempts,
426
- hasSessionId: !!this.sessionId,
427
- hasEndpoint: !!this.endpoint,
428
- hasToken: !!this.token
429
- })
430
-
431
- const currentGen = this._stateGeneration
432
-
433
- if (
434
- !this.sessionId ||
435
- !this.endpoint ||
436
- !this.token ||
437
- this._stateFlags & STATE.VOICE_DATA_STALE
438
- ) {
439
- this._aqua.emit(
440
- AqualinkEvents.Debug,
441
- `Resume blocked: missing voice data for guild ${this._guildId}, requesting voice state`
442
- )
443
- this._requestVoiceState()
444
- return false
445
- }
446
-
447
- this.txId = this._player.txId || this.txId
448
- this._stateFlags |= STATE.ATTEMPTING_RESUME
449
- this._reconnectAttempts++
450
- this._aqua.emit(
451
- AqualinkEvents.Debug,
452
- `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`
453
- )
454
-
455
- const payload = sharedPool.acquire()
456
- try {
457
- _functions.fillVoicePayload(
458
- payload,
459
- this._guildId,
460
- this,
461
- this._player,
462
- true
463
- )
464
-
465
- if (this._stateGeneration !== currentGen) {
466
- this._aqua.emit(
467
- AqualinkEvents.Debug,
468
- `Resume aborted: State changed during attempt for guild ${this._guildId}`
469
- )
470
- return false
471
- }
472
-
473
- await this._sendUpdate(payload)
474
- this._aqua?._trace?.('connection.resume.success', {
475
- guildId: this._guildId
476
- })
477
-
478
- this._reconnectAttempts = 0
479
- this._consecutiveFailures = 0
480
- if (this._player) this._player._resuming = false
481
-
482
- this._aqua.emit(
483
- AqualinkEvents.Debug,
484
- `Resume PATCH sent for guild ${this._guildId}`
485
- )
486
- return true
487
- } catch (e) {
488
- if (this._destroyed || !this._aqua) throw e
489
- this._consecutiveFailures++
490
- this._aqua.emit(
491
- AqualinkEvents.Debug,
492
- `Resume failed for guild ${this._guildId}: ${e?.message || e}`
493
- )
494
- this._aqua?._trace?.('connection.resume.error', {
495
- guildId: this._guildId,
496
- error: e?.message || String(e)
497
- })
498
-
499
- if (
500
- this._reconnectAttempts < MAX_RECONNECT_ATTEMPTS &&
501
- !this._destroyed &&
502
- this._consecutiveFailures < 5
503
- ) {
504
- const delay = Math.min(
505
- RECONNECT_DELAY * (1 << (this._reconnectAttempts - 1)),
506
- RESUME_BACKOFF_MAX
507
- )
508
- this._setReconnectTimer(delay)
509
- } else {
510
- this._aqua.emit(
511
- AqualinkEvents.Debug,
512
- `Max reconnect attempts/failures reached for guild ${this._guildId}`
513
- )
514
- if (this._player) this._player._resuming = false
515
- this._handleDisconnect()
516
- }
517
- return false
518
- } finally {
519
- this._stateFlags &= ~STATE.ATTEMPTING_RESUME
520
- sharedPool.release(payload)
521
- }
360
+ return this._recovery.attemptResume()
522
361
  }
523
362
 
524
363
  _handleReconnect() {
@@ -556,20 +395,24 @@ class Connection {
556
395
 
557
396
  _scheduleVoiceUpdate() {
558
397
  if (this._destroyed) {
559
- this._aqua?._trace?.('connection.update.skip', {
560
- guildId: this._guildId,
561
- reason: 'destroyed'
562
- })
398
+ if (this._aqua?.debugTrace) {
399
+ this._aqua._trace('connection.update.skip', {
400
+ guildId: this._guildId,
401
+ reason: 'destroyed'
402
+ })
403
+ }
563
404
  return
564
405
  }
565
406
  if (!this._hasValidVoiceData()) {
566
- this._aqua?._trace?.('connection.update.skip', {
567
- guildId: this._guildId,
568
- reason: 'invalid_voice_data',
569
- hasSessionId: !!this.sessionId,
570
- hasEndpoint: !!this.endpoint,
571
- hasToken: !!this.token
572
- })
407
+ if (this._aqua?.debugTrace) {
408
+ this._aqua._trace('connection.update.skip', {
409
+ guildId: this._guildId,
410
+ reason: 'invalid_voice_data',
411
+ hasSessionId: !!this.sessionId,
412
+ hasEndpoint: !!this.endpoint,
413
+ hasToken: !!this.token
414
+ })
415
+ }
573
416
  return
574
417
  }
575
418
 
@@ -596,9 +439,11 @@ class Connection {
596
439
 
597
440
  if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
598
441
  this._stateFlags |= STATE.UPDATE_SCHEDULED
599
- this._aqua?._trace?.('connection.update.scheduled', {
600
- guildId: this._guildId
601
- })
442
+ if (this._aqua?.debugTrace) {
443
+ this._aqua._trace('connection.update.scheduled', {
444
+ guildId: this._guildId
445
+ })
446
+ }
602
447
 
603
448
  this._voiceFlushTimer = setTimeout(
604
449
  () => this._executeVoiceUpdate(),
@@ -609,6 +454,15 @@ class Connection {
609
454
 
610
455
  _executeVoiceUpdate() {
611
456
  if (this._destroyed) return
457
+ if (this._stateFlags & STATE.DISCONNECTING) {
458
+ this._stateFlags &= ~STATE.UPDATE_SCHEDULED
459
+ this._voiceFlushTimer = null
460
+ if (this._pendingUpdate) {
461
+ sharedPool.release(this._pendingUpdate.payload)
462
+ this._pendingUpdate = null
463
+ }
464
+ return
465
+ }
612
466
  this._stateFlags &= ~STATE.UPDATE_SCHEDULED
613
467
  this._voiceFlushTimer = null
614
468
 
@@ -629,53 +483,20 @@ class Connection {
629
483
  this._lastSentVoiceKey = key
630
484
 
631
485
  this._sendUpdate(pending.payload)
632
- .catch(_functions.noop)
486
+ .catch((error) =>
487
+ reportSuppressedError(this._aqua, 'connection.update.execute', error, {
488
+ guildId: this._guildId
489
+ })
490
+ )
633
491
  .finally(() => sharedPool.release(pending.payload))
634
492
  }
635
493
 
636
- async _sendUpdate(payload) {
637
- if (this._destroyed) throw new Error('Connection destroyed')
638
- if (!this._rest) throw new Error('REST interface unavailable')
494
+ async _recoverMissingPlayer(isSessionError) {
495
+ return this._recovery.recoverMissingPlayer(isSessionError)
496
+ }
639
497
 
640
- try {
641
- this._aqua?._trace?.('connection.update.send', {
642
- guildId: this._guildId,
643
- hasSessionId: !!this._rest?.sessionId,
644
- hasVoice:
645
- !!payload?.data?.voice?.sessionId && !!payload?.data?.voice?.endpoint
646
- })
647
- await this._rest.updatePlayer(payload)
648
- this._aqua?._trace?.('connection.update.ok', {
649
- guildId: this._guildId
650
- })
651
- } catch (e) {
652
- this._aqua?._trace?.('connection.update.error', {
653
- guildId: this._guildId,
654
- statusCode: e?.statusCode || e?.response?.statusCode || null,
655
- error: e?.message || String(e)
656
- })
657
- if (e.statusCode === 404 || e.response?.statusCode === 404) {
658
- const isSessionError = e.body?.message?.includes('sessionId') || false
659
- if (this._aqua) {
660
- this._aqua.emit(
661
- AqualinkEvents.Debug,
662
- `[Aqua/Connection] Player ${this._guildId} not found (404)${isSessionError ? ' - Session invalid' : ''}. Destroying.`
663
- )
664
- if (isSessionError && this._player?.nodes?._clearSession) {
665
- this._player.nodes._clearSession()
666
- }
667
- await this._aqua.destroyPlayer(this._guildId)
668
- }
669
- throw e
670
- }
671
- if (!_functions.isNetworkError(e)) {
672
- this._aqua.emit(
673
- AqualinkEvents.Debug,
674
- new Error(`Voice update failed: ${e?.message || e}`)
675
- )
676
- }
677
- throw e
678
- }
498
+ async _sendUpdate(payload) {
499
+ return this._recovery.sendUpdate(payload)
679
500
  }
680
501
 
681
502
  destroy() {
@@ -699,6 +520,7 @@ class Connection {
699
520
  this._reconnectAttempts = 0
700
521
  this._consecutiveFailures = 0
701
522
  this._lastVoiceDataUpdate = 0
523
+ this._recovery = null
702
524
  }
703
525
  }
704
526