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.
@@ -80,6 +80,16 @@ const _functions = {
80
80
 
81
81
  errMsg(err) {
82
82
  return err?.message || String(err)
83
+ },
84
+
85
+ statusCode(err) {
86
+ return (
87
+ err?.statusCode ||
88
+ err?.status ||
89
+ err?.response?.statusCode ||
90
+ err?.response?.status ||
91
+ null
92
+ )
83
93
  }
84
94
  }
85
95
 
@@ -102,10 +112,10 @@ class Node {
102
112
  this.host = connOptions.host || 'localhost'
103
113
  this.name = connOptions.name || this.host
104
114
  this.port = connOptions.port || 2333
105
- this.auth = connOptions.auth || 'youshallnotpass'
115
+ this.auth = connOptions.auth || connOptions.password || 'youshallnotpass'
106
116
  this.sessionId = connOptions.sessionId || null
107
117
  this.regions = connOptions.regions || []
108
- this.ssl = !!connOptions.ssl
118
+ this.ssl = !!connOptions.ssl || !!connOptions.secure || false
109
119
  this.wsUrl = _functions.buildWsUrl(this.host, this.port, this.ssl)
110
120
 
111
121
  this.rest = new Rest(aqua, this)
@@ -187,10 +197,12 @@ class Node {
187
197
  this._isConnecting = false
188
198
  this.reconnectAttempted = 0
189
199
  this._emitDebug('WebSocket connection established')
190
- this.aqua?._trace?.('node.ws.open', {
191
- node: this.name,
192
- reconnectAttempted: this.reconnectAttempted
193
- })
200
+ if (this.aqua?.debugTrace) {
201
+ this.aqua._trace('node.ws.open', {
202
+ node: this.name,
203
+ reconnectAttempted: this.reconnectAttempted
204
+ })
205
+ }
194
206
 
195
207
  if (!this.aqua?.bypassChecks?.nodeFetchInfo && !this.info) {
196
208
  const timeoutId = setTimeout(() => {
@@ -203,7 +215,15 @@ class Node {
203
215
  this.isNodelink = !!this.info?.isNodelink
204
216
  } catch (err) {
205
217
  this.info = null
206
- this._emitError(`Failed to fetch node info: ${_functions.errMsg(err)}`)
218
+ if (_functions.statusCode(err) === 404) {
219
+ this._emitDebug(
220
+ 'Node info endpoint unavailable (HTTP 404); continuing without remote info'
221
+ )
222
+ } else {
223
+ this._emitError(
224
+ `Failed to fetch node info: ${_functions.errMsg(err)}`
225
+ )
226
+ }
207
227
  } finally {
208
228
  clearTimeout(timeoutId)
209
229
  }
@@ -272,12 +292,14 @@ class Node {
272
292
  code,
273
293
  reason: _functions.reasonToString(reason)
274
294
  })
275
- this.aqua?._trace?.('node.ws.close', {
276
- node: this.name,
277
- code,
278
- reason: _functions.reasonToString(reason),
279
- hasSessionId: !!this.sessionId
280
- })
295
+ if (this.aqua?.debugTrace) {
296
+ this.aqua._trace('node.ws.close', {
297
+ node: this.name,
298
+ code,
299
+ reason: _functions.reasonToString(reason),
300
+ hasSessionId: !!this.sessionId
301
+ })
302
+ }
281
303
 
282
304
  if (this.isDestroyed) return
283
305
 
@@ -311,11 +333,13 @@ class Node {
311
333
  this._clearReconnectTimeout()
312
334
 
313
335
  const attempt = ++this.reconnectAttempted
314
- this.aqua?._trace?.('node.ws.reconnect.scheduled', {
315
- node: this.name,
316
- attempt,
317
- infinite: !!this.infiniteReconnects
318
- })
336
+ if (this.aqua?.debugTrace) {
337
+ this.aqua._trace('node.ws.reconnect.scheduled', {
338
+ node: this.name,
339
+ attempt,
340
+ infinite: !!this.infiniteReconnects
341
+ })
342
+ }
319
343
 
320
344
  if (this.infiniteReconnects) {
321
345
  this.aqua.emit(AqualinkEvents.NodeReconnect, this, {
@@ -384,10 +408,12 @@ class Node {
384
408
  this._isConnecting = true
385
409
  this.state = NODE_STATE.CONNECTING
386
410
  this._cleanup()
387
- this.aqua?._trace?.('node.ws.connect', {
388
- node: this.name,
389
- url: this.wsUrl
390
- })
411
+ if (this.aqua?.debugTrace) {
412
+ this.aqua._trace('node.ws.connect', {
413
+ node: this.name,
414
+ url: this.wsUrl
415
+ })
416
+ }
391
417
 
392
418
  try {
393
419
  const h = this._boundHandlers
@@ -453,7 +479,7 @@ class Node {
453
479
 
454
480
  const ws = new WebSocketImpl(this.wsUrl, {
455
481
  headers: this._headers,
456
- perMessageDeflate: true,
482
+ perMessageDeflate: false,
457
483
  handshakeTimeout: this.timeout,
458
484
  maxPayload: this.maxPayload,
459
485
  skipUTF8Validation: this.skipUTF8Validation
@@ -584,36 +610,66 @@ class Node {
584
610
  }
585
611
 
586
612
  const oldSessionId = this.sessionId
587
- const sessionChanged =
588
- oldSessionId && oldSessionId !== sessionId && !payload.resumed
613
+ const sessionInvalidated = !payload.resumed && !!oldSessionId
614
+ const sessionChanged = sessionInvalidated && oldSessionId !== sessionId
589
615
 
590
616
  this.sessionId = sessionId
591
- this.aqua?._trace?.('node.ready.packet', {
592
- node: this.name,
593
- resumed: !!payload.resumed,
594
- oldSessionId,
595
- newSessionId: sessionId
596
- })
617
+ if (this.aqua?.debugTrace) {
618
+ this.aqua._trace('node.ready.packet', {
619
+ node: this.name,
620
+ resumed: !!payload.resumed,
621
+ oldSessionId,
622
+ newSessionId: sessionId
623
+ })
624
+ }
597
625
  this.rest.setSessionId(sessionId)
598
626
  this._headers['Session-Id'] = sessionId
599
627
 
600
- if (sessionChanged && this.aqua?.players) {
628
+ if (sessionInvalidated && this.aqua?.players) {
601
629
  this._emitDebug(
602
- `Session changed from ${oldSessionId} to ${sessionId}, invalidating stale players`
630
+ `Session invalidated (resumed=${!!payload.resumed}, old=${oldSessionId}, new=${sessionId}), invalidating stale players`
603
631
  )
632
+ try {
633
+ await this.aqua._storeBrokenPlayers?.(this)
634
+ } catch (e) {
635
+ this._emitDebug(
636
+ `Failed to snapshot stale players before invalidation: ${e?.message || e}`
637
+ )
638
+ }
639
+ for (const [, player] of this.aqua.players) {
640
+ if (player?.nodes === this || player?.nodes?.name === this.name) {
641
+ if (player.connection) {
642
+ player.connection._lastEndpoint = null
643
+ player.connection._stateFlags |= 512
644
+ }
645
+ }
646
+ }
647
+
604
648
  const playersToDestroy = []
605
649
  for (const [guildId, player] of this.aqua.players) {
606
650
  if (player?.nodes === this || player?.nodes?.name === this.name) {
607
- playersToDestroy.push(guildId)
651
+ playersToDestroy.push({ guildId, player })
608
652
  }
609
653
  }
610
- for (const guildId of playersToDestroy) {
654
+ for (const { guildId, player } of playersToDestroy) {
611
655
  try {
612
- this._emitDebug(`Destroying stale player for guild ${guildId}`)
613
- await this.aqua.destroyPlayer(guildId)
656
+ this._emitDebug(
657
+ `Invalidating stale player for guild ${guildId} without voice disconnect`
658
+ )
659
+ player?.destroy?.({
660
+ preserveClient: true,
661
+ skipRemote: true,
662
+ preserveMessage: true,
663
+ preserveTracks: true,
664
+ preserveReconnecting: true
665
+ })
666
+ if (this.aqua.players.get(String(guildId)) === player) {
667
+ this.aqua.players.delete(String(guildId))
668
+ }
669
+ this.players?.delete?.(player)
614
670
  } catch (e) {
615
671
  this._emitDebug(
616
- `Failed to destroy stale player ${guildId}: ${e?.message || e}`
672
+ `Failed to invalidate stale player ${guildId}: ${e?.message || e}`
617
673
  )
618
674
  }
619
675
  }
@@ -621,7 +677,8 @@ class Node {
621
677
 
622
678
  this.aqua.emit(AqualinkEvents.NodeReady, this, {
623
679
  resumed: !!payload.resumed,
624
- sessionChanged
680
+ sessionChanged,
681
+ sessionInvalidated
625
682
  })
626
683
 
627
684
  if (this.autoResume) {
@@ -635,55 +692,101 @@ class Node {
635
692
 
636
693
  async _resumePlayers() {
637
694
  if (!this.sessionId) return
638
- this.aqua?._trace?.('node.resume.begin', {
639
- node: this.name,
640
- sessionId: this.sessionId,
641
- players: this.aqua?.players?.size || 0
642
- })
695
+ if (this.aqua?.debugTrace) {
696
+ this.aqua._trace('node.resume.begin', {
697
+ node: this.name,
698
+ sessionId: this.sessionId,
699
+ players: this.aqua?.players?.size || 0
700
+ })
701
+ }
643
702
 
703
+ let resumeSupported = true
644
704
  try {
645
705
  await this.rest.makeRequest('PATCH', `/v4/sessions/${this.sessionId}`, {
646
706
  resuming: true,
647
707
  timeout: this.resumeTimeout
648
708
  })
709
+ } catch (err) {
710
+ if (_functions.statusCode(err) === 404) {
711
+ resumeSupported = false
712
+ this._emitDebug(
713
+ 'Session resume endpoint unavailable (HTTP 404); falling back without server-side session resume'
714
+ )
715
+ } else {
716
+ if (this.aqua?.debugTrace) {
717
+ this.aqua._trace('node.resume.error', {
718
+ node: this.name,
719
+ error: _functions.errMsg(err)
720
+ })
721
+ }
722
+ this._emitError(`Failed to resume session: ${_functions.errMsg(err)}`)
723
+ throw err
724
+ }
725
+ }
649
726
 
650
- if (this.aqua?.players) {
651
- for (const [guildId, player] of this.aqua.players) {
652
- if (
653
- (player?.nodes === this || player?.nodes?.name === this.name) &&
654
- player.voiceChannel
655
- ) {
727
+ if (!resumeSupported) {
728
+ try {
729
+ const existingPlayers = await this.rest.getPlayers()
730
+ if (Array.isArray(existingPlayers) && existingPlayers.length === 0) {
731
+ this._emitDebug(
732
+ 'No players found on Lavalink for this session; will rejoin voice only'
733
+ )
734
+ }
735
+ } catch (_) {
736
+ // getPlayers may also 404 — that's fine, we already know session is invalid
737
+ }
738
+ }
739
+
740
+ if (this.aqua?.players) {
741
+ const PLAYER_BATCH_SIZE = 20
742
+ const playersToResume = []
743
+ for (const [guildId, player] of this.aqua.players) {
744
+ if (
745
+ (player?.nodes === this || player?.nodes?.name === this.name) &&
746
+ player.voiceChannel &&
747
+ !player.destroyed
748
+ ) {
749
+ playersToResume.push({ guildId, player })
750
+ }
751
+ }
752
+
753
+ for (let i = 0; i < playersToResume.length; i += PLAYER_BATCH_SIZE) {
754
+ const batch = playersToResume.slice(i, i + PLAYER_BATCH_SIZE)
755
+ await Promise.allSettled(
756
+ batch.map(async ({ guildId, player }) => {
656
757
  try {
758
+ const recoveryToken = player._claimVoiceRecovery?.(
759
+ resumeSupported
760
+ ? 'node_resume_rejoin'
761
+ : 'node_rejoin_after_resume_404'
762
+ )
657
763
  this._emitDebug(`Rejoining voice for guild ${guildId} on resume`)
658
- this.aqua?._trace?.('node.resume.rejoin', {
659
- node: this.name,
660
- guildId,
661
- voiceChannel: player.voiceChannel
662
- })
663
- player.connect({
664
- voiceChannel: player.voiceChannel,
665
- deaf: player.deaf,
666
- mute: player.mute
667
- })
764
+ if (this.aqua?.debugTrace) {
765
+ this.aqua._trace('node.resume.rejoin', {
766
+ node: this.name,
767
+ guildId,
768
+ voiceChannel: player.voiceChannel,
769
+ resumeSupported
770
+ })
771
+ }
772
+ if (player._isVoiceRecoveryActive?.(recoveryToken))
773
+ player.connect({
774
+ voiceChannel: player.voiceChannel,
775
+ deaf: player.deaf,
776
+ mute: player.mute
777
+ })
668
778
  } catch (e) {
669
779
  this._emitDebug(
670
780
  `Failed to rejoin voice for ${guildId}: ${e?.message || e}`
671
781
  )
672
782
  }
673
- }
674
- }
783
+ })
784
+ )
675
785
  }
786
+ }
676
787
 
677
- if (this.aqua.loadPlayers && this.aqua.players.size === 0) {
678
- await this.aqua.loadPlayers()
679
- }
680
- } catch (err) {
681
- this.aqua?._trace?.('node.resume.error', {
682
- node: this.name,
683
- error: _functions.errMsg(err)
684
- })
685
- this._emitError(`Failed to resume session: ${_functions.errMsg(err)}`)
686
- throw err
788
+ if (this.aqua.loadPlayers && this.aqua.players.size === 0) {
789
+ await this.aqua.loadPlayers()
687
790
  }
688
791
  }
689
792