aqualink 2.20.1 → 3.0.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()
@@ -399,126 +332,30 @@ class Connection {
399
332
  try {
400
333
  const now = Date.now()
401
334
  if (now - (this._lastStateReqAt || 0) < 1500) return false
402
- this._lastStateReqAt = now
403
335
 
404
336
  if (
405
337
  typeof this._player?.send !== 'function' ||
406
338
  !this._player.voiceChannel
407
339
  )
408
340
  return false
409
- this._player.send({
341
+ const queued = this._player.send({
410
342
  guild_id: this._guildId,
411
343
  channel_id: this._player.voiceChannel,
412
344
  self_deaf: this._player.deaf,
413
345
  self_mute: this._player.mute
414
346
  })
415
- return true
416
- } catch {
347
+ this._lastStateReqAt = now
348
+ return queued !== false
349
+ } catch (error) {
350
+ reportSuppressedError(this._aqua, 'connection.requestVoiceState', error, {
351
+ guildId: this._guildId
352
+ })
417
353
  return false
418
354
  }
419
355
  }
420
356
 
421
357
  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
- }
358
+ return this._recovery.attemptResume()
522
359
  }
523
360
 
524
361
  _handleReconnect() {
@@ -556,20 +393,24 @@ class Connection {
556
393
 
557
394
  _scheduleVoiceUpdate() {
558
395
  if (this._destroyed) {
559
- this._aqua?._trace?.('connection.update.skip', {
560
- guildId: this._guildId,
561
- reason: 'destroyed'
562
- })
396
+ if (this._aqua?.debugTrace) {
397
+ this._aqua._trace('connection.update.skip', {
398
+ guildId: this._guildId,
399
+ reason: 'destroyed'
400
+ })
401
+ }
563
402
  return
564
403
  }
565
404
  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
- })
405
+ if (this._aqua?.debugTrace) {
406
+ this._aqua._trace('connection.update.skip', {
407
+ guildId: this._guildId,
408
+ reason: 'invalid_voice_data',
409
+ hasSessionId: !!this.sessionId,
410
+ hasEndpoint: !!this.endpoint,
411
+ hasToken: !!this.token
412
+ })
413
+ }
573
414
  return
574
415
  }
575
416
 
@@ -596,9 +437,11 @@ class Connection {
596
437
 
597
438
  if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
598
439
  this._stateFlags |= STATE.UPDATE_SCHEDULED
599
- this._aqua?._trace?.('connection.update.scheduled', {
600
- guildId: this._guildId
601
- })
440
+ if (this._aqua?.debugTrace) {
441
+ this._aqua._trace('connection.update.scheduled', {
442
+ guildId: this._guildId
443
+ })
444
+ }
602
445
 
603
446
  this._voiceFlushTimer = setTimeout(
604
447
  () => this._executeVoiceUpdate(),
@@ -629,53 +472,20 @@ class Connection {
629
472
  this._lastSentVoiceKey = key
630
473
 
631
474
  this._sendUpdate(pending.payload)
632
- .catch(_functions.noop)
475
+ .catch((error) =>
476
+ reportSuppressedError(this._aqua, 'connection.update.execute', error, {
477
+ guildId: this._guildId
478
+ })
479
+ )
633
480
  .finally(() => sharedPool.release(pending.payload))
634
481
  }
635
482
 
636
- async _sendUpdate(payload) {
637
- if (this._destroyed) throw new Error('Connection destroyed')
638
- if (!this._rest) throw new Error('REST interface unavailable')
483
+ async _recoverMissingPlayer(isSessionError) {
484
+ return this._recovery.recoverMissingPlayer(isSessionError)
485
+ }
639
486
 
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
- }
487
+ async _sendUpdate(payload) {
488
+ return this._recovery.sendUpdate(payload)
679
489
  }
680
490
 
681
491
  destroy() {
@@ -699,6 +509,7 @@ class Connection {
699
509
  this._reconnectAttempts = 0
700
510
  this._consecutiveFailures = 0
701
511
  this._lastVoiceDataUpdate = 0
512
+ this._recovery = null
702
513
  }
703
514
  }
704
515