aqualink 3.0.0 → 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.
- package/build/index.d.ts +1 -1
- package/build/structures/Aqua.js +13 -5
- package/build/structures/AquaRecovery.js +7 -3
- package/build/structures/Connection.js +12 -1
- package/build/structures/ConnectionRecovery.js +33 -6
- package/build/structures/Filters.js +3 -1
- package/build/structures/Node.js +108 -44
- package/build/structures/Player.js +92 -43
- package/build/structures/PlayerLifecycle.js +14 -5
- package/build/structures/Queue.js +5 -1
- package/build/structures/Rest.js +26 -9
- package/package.json +1 -1
package/build/index.d.ts
CHANGED
package/build/structures/Aqua.js
CHANGED
|
@@ -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')
|
|
@@ -191,7 +191,10 @@ class Aqua extends EventEmitter {
|
|
|
191
191
|
|
|
192
192
|
_trace(event, data = null) {
|
|
193
193
|
if (!this.debugTrace) return
|
|
194
|
-
if (
|
|
194
|
+
if (
|
|
195
|
+
!this._traceBuffer ||
|
|
196
|
+
this._traceBuffer.length !== this.traceMaxEntries
|
|
197
|
+
) {
|
|
195
198
|
this._traceBuffer = new Array(this.traceMaxEntries)
|
|
196
199
|
this._traceBufferCount = 0
|
|
197
200
|
this._traceBufferIndex = 0
|
|
@@ -346,9 +349,14 @@ class Aqua extends EventEmitter {
|
|
|
346
349
|
this._invalidateCache()
|
|
347
350
|
queueMicrotask(() => {
|
|
348
351
|
this._storeBrokenPlayers(node).catch((error) =>
|
|
349
|
-
reportSuppressedError(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
+
reportSuppressedError(
|
|
353
|
+
this,
|
|
354
|
+
'aqua.nodeDisconnect.storeBrokenPlayers',
|
|
355
|
+
error,
|
|
356
|
+
{
|
|
357
|
+
node: node?.name || node?.host
|
|
358
|
+
}
|
|
359
|
+
)
|
|
352
360
|
)
|
|
353
361
|
this._performCleanup()
|
|
354
362
|
})
|
|
@@ -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) => ({
|
|
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++) {
|
|
@@ -551,8 +554,9 @@ class AquaRecovery {
|
|
|
551
554
|
if (existing?.playing && !existing.destroyed) return true
|
|
552
555
|
if (existing?.destroyed) this.aqua.players.delete(gId)
|
|
553
556
|
|
|
554
|
-
const targetNode =
|
|
555
|
-
|
|
557
|
+
const targetNode = preferredNode?.connected
|
|
558
|
+
? preferredNode
|
|
559
|
+
: this.aqua.leastUsedNodes[0]
|
|
556
560
|
if (!targetNode?.connected) {
|
|
557
561
|
throw new Error(`No connected node available to restore guild ${gId}`)
|
|
558
562
|
}
|
|
@@ -321,7 +321,9 @@ class Connection {
|
|
|
321
321
|
} catch (e) {
|
|
322
322
|
this._aqua?.emit?.(
|
|
323
323
|
AqualinkEvents.Debug,
|
|
324
|
-
new Error(
|
|
324
|
+
new Error(
|
|
325
|
+
`Player destroy failed (guild=${this._guildId}, sessionId=${this.sessionId || 'none'}): ${e?.message || e}`
|
|
326
|
+
)
|
|
325
327
|
)
|
|
326
328
|
} finally {
|
|
327
329
|
this._stateFlags &= ~STATE.DISCONNECTING
|
|
@@ -452,6 +454,15 @@ class Connection {
|
|
|
452
454
|
|
|
453
455
|
_executeVoiceUpdate() {
|
|
454
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
|
+
}
|
|
455
466
|
this._stateFlags &= ~STATE.UPDATE_SCHEDULED
|
|
456
467
|
this._voiceFlushTimer = null
|
|
457
468
|
|
|
@@ -163,6 +163,14 @@ class ConnectionRecovery {
|
|
|
163
163
|
true
|
|
164
164
|
)
|
|
165
165
|
|
|
166
|
+
if (conn._destroyed || !conn._player || conn._player.destroyed) {
|
|
167
|
+
conn._aqua.emit(
|
|
168
|
+
AqualinkEvents.Debug,
|
|
169
|
+
`Resume aborted: player destroyed during attempt for guild ${conn._guildId}`
|
|
170
|
+
)
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
166
174
|
if (conn._stateGeneration !== currentGen) {
|
|
167
175
|
conn._aqua.emit(
|
|
168
176
|
AqualinkEvents.Debug,
|
|
@@ -178,6 +186,10 @@ class ConnectionRecovery {
|
|
|
178
186
|
})
|
|
179
187
|
}
|
|
180
188
|
|
|
189
|
+
if (conn._destroyed || conn._player?.destroyed) {
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
conn._reconnectAttempts = 0
|
|
182
194
|
conn._consecutiveFailures = 0
|
|
183
195
|
if (conn._player) conn._player._resuming = false
|
|
@@ -189,10 +201,17 @@ class ConnectionRecovery {
|
|
|
189
201
|
return true
|
|
190
202
|
} catch (error) {
|
|
191
203
|
if (conn._destroyed || !conn._aqua) throw error
|
|
204
|
+
if (conn._player?.destroyed) {
|
|
205
|
+
conn._aqua.emit(
|
|
206
|
+
AqualinkEvents.Debug,
|
|
207
|
+
`Resume aborted: player destroyed during retry for guild ${conn._guildId}`
|
|
208
|
+
)
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
192
211
|
conn._consecutiveFailures++
|
|
193
212
|
conn._aqua.emit(
|
|
194
213
|
AqualinkEvents.Debug,
|
|
195
|
-
`Resume failed for guild ${conn._guildId}: ${error?.message || error}`
|
|
214
|
+
`Resume failed for guild ${conn._guildId} (sessionId=${conn.sessionId || 'none'}, endpoint=${conn.endpoint || 'none'}): ${error?.message || error}`
|
|
196
215
|
)
|
|
197
216
|
if (conn._aqua?.debugTrace) {
|
|
198
217
|
conn._aqua._trace('connection.resume.error', {
|
|
@@ -204,7 +223,8 @@ class ConnectionRecovery {
|
|
|
204
223
|
if (
|
|
205
224
|
conn._reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS &&
|
|
206
225
|
!conn._destroyed &&
|
|
207
|
-
conn._consecutiveFailures < 5
|
|
226
|
+
conn._consecutiveFailures < 5 &&
|
|
227
|
+
!conn._player?.destroyed
|
|
208
228
|
) {
|
|
209
229
|
const delay = Math.min(
|
|
210
230
|
this.RECONNECT_DELAY * (1 << (conn._reconnectAttempts - 1)),
|
|
@@ -265,7 +285,10 @@ class ConnectionRecovery {
|
|
|
265
285
|
return false
|
|
266
286
|
})
|
|
267
287
|
if (resumed) {
|
|
268
|
-
conn._player?._clearVoiceRecovery?.(
|
|
288
|
+
conn._player?._clearVoiceRecovery?.(
|
|
289
|
+
recoveryToken,
|
|
290
|
+
'missing_player_resumed'
|
|
291
|
+
)
|
|
269
292
|
} else if (conn._player?._isVoiceRecoveryActive?.(recoveryToken)) {
|
|
270
293
|
conn.resendVoiceUpdate(true)
|
|
271
294
|
}
|
|
@@ -306,8 +329,12 @@ class ConnectionRecovery {
|
|
|
306
329
|
|
|
307
330
|
async sendUpdate(payload) {
|
|
308
331
|
const conn = this.connection
|
|
309
|
-
if (conn._destroyed)
|
|
310
|
-
|
|
332
|
+
if (conn._destroyed)
|
|
333
|
+
throw new Error(`Connection destroyed (guild=${conn._guildId})`)
|
|
334
|
+
if (!conn._rest)
|
|
335
|
+
throw new Error(
|
|
336
|
+
`REST interface unavailable (guild=${conn._guildId}, sessionId=${conn.sessionId || 'none'})`
|
|
337
|
+
)
|
|
311
338
|
|
|
312
339
|
try {
|
|
313
340
|
if (conn._aqua?.debugTrace) {
|
|
@@ -342,7 +369,7 @@ class ConnectionRecovery {
|
|
|
342
369
|
if (conn._aqua) {
|
|
343
370
|
conn._aqua.emit(
|
|
344
371
|
AqualinkEvents.Debug,
|
|
345
|
-
`[Aqua/Connection] Player ${conn._guildId} not found (404)${isSessionError ? ' - Session invalid' : ''}. Recovery failed, destroying.`
|
|
372
|
+
`[Aqua/Connection] Player ${conn._guildId} not found (404, sessionId=${conn.sessionId || 'none'}, endpoint=${conn.endpoint || 'none'})${isSessionError ? ' - Session invalid' : ''}. Recovery failed, destroying.`
|
|
346
373
|
)
|
|
347
374
|
await conn._aqua.destroyPlayer(conn._guildId)
|
|
348
375
|
}
|
|
@@ -202,7 +202,8 @@ class Filters {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
_scheduleUpdate() {
|
|
205
|
-
if (this._pendingUpdate || !this.player
|
|
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]
|
package/build/structures/Node.js
CHANGED
|
@@ -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
|
|
|
@@ -205,7 +215,15 @@ class Node {
|
|
|
205
215
|
this.isNodelink = !!this.info?.isNodelink
|
|
206
216
|
} catch (err) {
|
|
207
217
|
this.info = null
|
|
208
|
-
|
|
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
|
+
}
|
|
209
227
|
} finally {
|
|
210
228
|
clearTimeout(timeoutId)
|
|
211
229
|
}
|
|
@@ -592,8 +610,8 @@ class Node {
|
|
|
592
610
|
}
|
|
593
611
|
|
|
594
612
|
const oldSessionId = this.sessionId
|
|
595
|
-
const
|
|
596
|
-
|
|
613
|
+
const sessionInvalidated = !payload.resumed && !!oldSessionId
|
|
614
|
+
const sessionChanged = sessionInvalidated && oldSessionId !== sessionId
|
|
597
615
|
|
|
598
616
|
this.sessionId = sessionId
|
|
599
617
|
if (this.aqua?.debugTrace) {
|
|
@@ -607,9 +625,9 @@ class Node {
|
|
|
607
625
|
this.rest.setSessionId(sessionId)
|
|
608
626
|
this._headers['Session-Id'] = sessionId
|
|
609
627
|
|
|
610
|
-
if (
|
|
628
|
+
if (sessionInvalidated && this.aqua?.players) {
|
|
611
629
|
this._emitDebug(
|
|
612
|
-
`Session
|
|
630
|
+
`Session invalidated (resumed=${!!payload.resumed}, old=${oldSessionId}, new=${sessionId}), invalidating stale players`
|
|
613
631
|
)
|
|
614
632
|
try {
|
|
615
633
|
await this.aqua._storeBrokenPlayers?.(this)
|
|
@@ -618,6 +636,15 @@ class Node {
|
|
|
618
636
|
`Failed to snapshot stale players before invalidation: ${e?.message || e}`
|
|
619
637
|
)
|
|
620
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
|
+
|
|
621
648
|
const playersToDestroy = []
|
|
622
649
|
for (const [guildId, player] of this.aqua.players) {
|
|
623
650
|
if (player?.nodes === this || player?.nodes?.name === this.name) {
|
|
@@ -650,7 +677,8 @@ class Node {
|
|
|
650
677
|
|
|
651
678
|
this.aqua.emit(AqualinkEvents.NodeReady, this, {
|
|
652
679
|
resumed: !!payload.resumed,
|
|
653
|
-
sessionChanged
|
|
680
|
+
sessionChanged,
|
|
681
|
+
sessionInvalidated
|
|
654
682
|
})
|
|
655
683
|
|
|
656
684
|
if (this.autoResume) {
|
|
@@ -672,57 +700,93 @@ class Node {
|
|
|
672
700
|
})
|
|
673
701
|
}
|
|
674
702
|
|
|
703
|
+
let resumeSupported = true
|
|
675
704
|
try {
|
|
676
705
|
await this.rest.makeRequest('PATCH', `/v4/sessions/${this.sessionId}`, {
|
|
677
706
|
resuming: true,
|
|
678
707
|
timeout: this.resumeTimeout
|
|
679
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
|
+
}
|
|
726
|
+
|
|
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
|
+
}
|
|
680
739
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
)
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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 }) => {
|
|
757
|
+
try {
|
|
758
|
+
const recoveryToken = player._claimVoiceRecovery?.(
|
|
759
|
+
resumeSupported
|
|
760
|
+
? 'node_resume_rejoin'
|
|
761
|
+
: 'node_rejoin_after_resume_404'
|
|
762
|
+
)
|
|
763
|
+
this._emitDebug(`Rejoining voice for guild ${guildId} on resume`)
|
|
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
|
+
})
|
|
705
778
|
} catch (e) {
|
|
706
779
|
this._emitDebug(
|
|
707
780
|
`Failed to rejoin voice for ${guildId}: ${e?.message || e}`
|
|
708
781
|
)
|
|
709
782
|
}
|
|
710
|
-
}
|
|
711
|
-
|
|
783
|
+
})
|
|
784
|
+
)
|
|
712
785
|
}
|
|
786
|
+
}
|
|
713
787
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
} catch (err) {
|
|
718
|
-
if (this.aqua?.debugTrace) {
|
|
719
|
-
this.aqua._trace('node.resume.error', {
|
|
720
|
-
node: this.name,
|
|
721
|
-
error: _functions.errMsg(err)
|
|
722
|
-
})
|
|
723
|
-
}
|
|
724
|
-
this._emitError(`Failed to resume session: ${_functions.errMsg(err)}`)
|
|
725
|
-
throw err
|
|
788
|
+
if (this.aqua.loadPlayers && this.aqua.players.size === 0) {
|
|
789
|
+
await this.aqua.loadPlayers()
|
|
726
790
|
}
|
|
727
791
|
}
|
|
728
792
|
|
|
@@ -122,7 +122,8 @@ class MicrotaskUpdateBatcher {
|
|
|
122
122
|
const { player: p, updates: u } = this
|
|
123
123
|
this.updates = null
|
|
124
124
|
this.scheduled = false
|
|
125
|
-
if (!u || !p
|
|
125
|
+
if (!u || !p || p.destroyed || p.state === PLAYER_STATE.DISCONNECTING)
|
|
126
|
+
return Promise.resolve()
|
|
126
127
|
return p.updatePlayer(u).catch((err) => {
|
|
127
128
|
_functions.emitAquaError(
|
|
128
129
|
p.aqua,
|
|
@@ -296,7 +297,9 @@ class Player extends EventEmitter {
|
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
_isVoiceRecoveryActive(token) {
|
|
299
|
-
return
|
|
300
|
+
return (
|
|
301
|
+
!!token && !this.destroyed && this._activeVoiceRecoveryToken === token
|
|
302
|
+
)
|
|
300
303
|
}
|
|
301
304
|
|
|
302
305
|
_clearVoiceRecovery(token = this._activeVoiceRecoveryToken, reason = null) {
|
|
@@ -352,6 +355,11 @@ class Player extends EventEmitter {
|
|
|
352
355
|
|
|
353
356
|
async play(track, options = {}) {
|
|
354
357
|
if (this.destroyed || !this.queue) return this
|
|
358
|
+
if (options != null && typeof options !== 'object') {
|
|
359
|
+
throw new TypeError(
|
|
360
|
+
`Player.play(): options must be an object, got ${typeof options}`
|
|
361
|
+
)
|
|
362
|
+
}
|
|
355
363
|
|
|
356
364
|
let item = track
|
|
357
365
|
if (!item) {
|
|
@@ -484,7 +492,8 @@ class Player extends EventEmitter {
|
|
|
484
492
|
}
|
|
485
493
|
|
|
486
494
|
connect(options = {}) {
|
|
487
|
-
if (this.destroyed)
|
|
495
|
+
if (this.destroyed)
|
|
496
|
+
throw new Error(`Cannot connect destroyed player (guild=${this.guildId})`)
|
|
488
497
|
|
|
489
498
|
const voiceChannel = _functions.toId(
|
|
490
499
|
options.voiceChannel || this.voiceChannel
|
|
@@ -542,38 +551,29 @@ class Player extends EventEmitter {
|
|
|
542
551
|
}
|
|
543
552
|
|
|
544
553
|
destroy(options = {}) {
|
|
554
|
+
if (this.destroyed) return this
|
|
555
|
+
|
|
545
556
|
const {
|
|
546
557
|
preserveClient = true,
|
|
547
558
|
skipRemote = false,
|
|
548
559
|
preserveMessage = false,
|
|
549
560
|
preserveReconnecting = false,
|
|
550
|
-
preserveTracks = false
|
|
561
|
+
preserveTracks = false,
|
|
562
|
+
abortSignal = null
|
|
551
563
|
} = options
|
|
552
|
-
if (this.destroyed && !this.queue) {
|
|
553
|
-
this._reconnectNonce++
|
|
554
|
-
if (this._reconnectTimers) {
|
|
555
|
-
_functions.clearTimers(this._reconnectTimers)
|
|
556
|
-
this._reconnectTimers = null
|
|
557
|
-
}
|
|
558
|
-
this._reconnecting = false
|
|
559
|
-
this._isActivelyReconnecting = false
|
|
560
|
-
return this
|
|
561
|
-
}
|
|
562
564
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
this.
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
})
|
|
574
|
-
}
|
|
575
|
-
this.emit('destroy')
|
|
565
|
+
this._reconnectNonce++
|
|
566
|
+
this.destroyed = true
|
|
567
|
+
this._clearVoiceRecovery(undefined, 'destroyed')
|
|
568
|
+
if (this.aqua?.debugTrace) {
|
|
569
|
+
this.aqua._trace('player.destroy', {
|
|
570
|
+
guildId: this.guildId,
|
|
571
|
+
skipRemote: !!skipRemote,
|
|
572
|
+
preserveTracks: !!preserveTracks,
|
|
573
|
+
preserveReconnecting: !!preserveReconnecting
|
|
574
|
+
})
|
|
576
575
|
}
|
|
576
|
+
this.emit('destroy')
|
|
577
577
|
|
|
578
578
|
if (this._voiceWatchdogTimer) {
|
|
579
579
|
clearInterval(this._voiceWatchdogTimer)
|
|
@@ -583,7 +583,6 @@ class Player extends EventEmitter {
|
|
|
583
583
|
_functions.clearTimers(this._pendingTimers)
|
|
584
584
|
this._pendingTimers = null
|
|
585
585
|
|
|
586
|
-
// Clear reconnection timers to prevent memory leaks when destroyed externally
|
|
587
586
|
if (this._reconnectTimers) {
|
|
588
587
|
_functions.clearTimers(this._reconnectTimers)
|
|
589
588
|
this._reconnectTimers = null
|
|
@@ -633,8 +632,23 @@ class Player extends EventEmitter {
|
|
|
633
632
|
this.previousTracks = null
|
|
634
633
|
this.previousIdentifiers?.clear()
|
|
635
634
|
this.previousIdentifiers = null
|
|
636
|
-
this.queue
|
|
635
|
+
if (this.queue) {
|
|
636
|
+
for (
|
|
637
|
+
let i = this.queue._head || 0;
|
|
638
|
+
i < (this.queue._items?.length || 0);
|
|
639
|
+
i++
|
|
640
|
+
) {
|
|
641
|
+
const t = this.queue._items[i]
|
|
642
|
+
if (t && typeof t.dispose === 'function') {
|
|
643
|
+
try {
|
|
644
|
+
t.dispose()
|
|
645
|
+
} catch {}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
this.queue.clear()
|
|
649
|
+
}
|
|
637
650
|
this.queue = null
|
|
651
|
+
// ML-1: Clear _dataStore to prevent unbounded growth
|
|
638
652
|
this._dataStore?.clear()
|
|
639
653
|
this._dataStore = null
|
|
640
654
|
|
|
@@ -662,14 +676,25 @@ class Player extends EventEmitter {
|
|
|
662
676
|
|
|
663
677
|
if (!skipRemote) {
|
|
664
678
|
try {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
guildId: this.guildId
|
|
679
|
+
if (abortSignal?.aborted) {
|
|
680
|
+
if (this.aqua?.debugTrace) {
|
|
681
|
+
this.aqua._trace('player.destroy.aborted', {
|
|
682
|
+
guildId: this.guildId,
|
|
683
|
+
reason: 'abort_signal_already_set'
|
|
671
684
|
})
|
|
672
|
-
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
this.send({ guild_id: this.guildId, channel_id: null })
|
|
688
|
+
this.aqua?.destroyPlayer?.(this.guildId)
|
|
689
|
+
if (this.nodes?.connected)
|
|
690
|
+
this.nodes.rest
|
|
691
|
+
?.destroyPlayer(this.guildId, abortSignal)
|
|
692
|
+
.catch((error) => {
|
|
693
|
+
reportSuppressedError(this, 'player.destroy.remote', error, {
|
|
694
|
+
guildId: this.guildId
|
|
695
|
+
})
|
|
696
|
+
})
|
|
697
|
+
}
|
|
673
698
|
} catch (error) {
|
|
674
699
|
reportSuppressedError(this, 'player.destroy.gateway', error, {
|
|
675
700
|
guildId: this.guildId
|
|
@@ -677,12 +702,23 @@ class Player extends EventEmitter {
|
|
|
677
702
|
}
|
|
678
703
|
}
|
|
679
704
|
|
|
680
|
-
if (!preserveClient)
|
|
705
|
+
if (!preserveClient) {
|
|
706
|
+
try {
|
|
707
|
+
this.aqua?.removeListener?.('playerUpdate', this._boundPlayerUpdate)
|
|
708
|
+
} catch {}
|
|
709
|
+
this.aqua = this.nodes = null
|
|
710
|
+
}
|
|
681
711
|
return this
|
|
682
712
|
}
|
|
683
713
|
|
|
684
714
|
pause(paused) {
|
|
685
|
-
if (this.destroyed
|
|
715
|
+
if (this.destroyed) return this
|
|
716
|
+
if (paused != null && typeof paused !== 'boolean') {
|
|
717
|
+
throw new TypeError(
|
|
718
|
+
`Player.pause(): paused must be a boolean, got ${typeof paused}`
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
if (this.paused === !!paused) return this
|
|
686
722
|
this.paused = !!paused
|
|
687
723
|
this.batchUpdatePlayer({ paused: this.paused }, true).catch((error) =>
|
|
688
724
|
reportSuppressedError(this, 'player.pause', error, {
|
|
@@ -694,8 +730,12 @@ class Player extends EventEmitter {
|
|
|
694
730
|
}
|
|
695
731
|
|
|
696
732
|
seek(position) {
|
|
697
|
-
if (
|
|
698
|
-
|
|
733
|
+
if (position == null || !_functions.isNum(position)) {
|
|
734
|
+
throw new TypeError(
|
|
735
|
+
`Player.seek(): position must be a non-negative number, got ${typeof position}`
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
if (this.destroyed || !this.playing) return this
|
|
699
739
|
const len = this.current?.info?.length || 0
|
|
700
740
|
const clamped = len
|
|
701
741
|
? Math.min(Math.max(position, 0), len)
|
|
@@ -769,6 +809,14 @@ class Player extends EventEmitter {
|
|
|
769
809
|
}
|
|
770
810
|
|
|
771
811
|
setVolume(volume) {
|
|
812
|
+
if (
|
|
813
|
+
volume == null ||
|
|
814
|
+
(typeof volume !== 'number' && typeof volume !== 'string')
|
|
815
|
+
) {
|
|
816
|
+
throw new TypeError(
|
|
817
|
+
`Player.setVolume(): volume must be a number, got ${typeof volume}`
|
|
818
|
+
)
|
|
819
|
+
}
|
|
772
820
|
const vol = _functions.clamp(volume)
|
|
773
821
|
if (this.destroyed || this.volume === vol) return this
|
|
774
822
|
this.volume = vol
|
|
@@ -1029,6 +1077,7 @@ class Player extends EventEmitter {
|
|
|
1029
1077
|
const isReplaced = reason === 'replaced'
|
|
1030
1078
|
|
|
1031
1079
|
if (track) this.previousTracks.push(track)
|
|
1080
|
+
if (isReplaced) return
|
|
1032
1081
|
if (this.shouldDeleteMessage && !this._reconnecting && !this._resuming)
|
|
1033
1082
|
_functions.safeDel(this.nowPlayingMessage)
|
|
1034
1083
|
if (!isReplaced) this.current = null
|
|
@@ -1123,8 +1172,8 @@ class Player extends EventEmitter {
|
|
|
1123
1172
|
_functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload)
|
|
1124
1173
|
}
|
|
1125
1174
|
|
|
1126
|
-
async _attemptVoiceResume() {
|
|
1127
|
-
return this._lifecycleController.attemptVoiceResume()
|
|
1175
|
+
async _attemptVoiceResume(abortSignal) {
|
|
1176
|
+
return this._lifecycleController.attemptVoiceResume(abortSignal)
|
|
1128
1177
|
}
|
|
1129
1178
|
|
|
1130
1179
|
async socketClosed(_player, _track, payload) {
|
|
@@ -1142,7 +1191,7 @@ class Player extends EventEmitter {
|
|
|
1142
1191
|
} catch (err) {
|
|
1143
1192
|
_functions.emitAquaError(
|
|
1144
1193
|
this.aqua,
|
|
1145
|
-
new Error(`Send fail: ${err.message}`)
|
|
1194
|
+
new Error(`Send fail (guild=${this.guildId}): ${err.message}`)
|
|
1146
1195
|
)
|
|
1147
1196
|
return false
|
|
1148
1197
|
}
|
|
@@ -30,6 +30,8 @@ class PlayerLifecycle {
|
|
|
30
30
|
player.ping = this._functions.isNum(s.ping) ? s.ping : 0
|
|
31
31
|
player.timestamp = this._functions.isNum(s.time) ? s.time : Date.now()
|
|
32
32
|
|
|
33
|
+
if (player.destroyed) return
|
|
34
|
+
|
|
33
35
|
if (!player.connected) {
|
|
34
36
|
if (wasConnected || !player._voiceDownSince) {
|
|
35
37
|
if (player.aqua?.debugTrace) {
|
|
@@ -148,7 +150,9 @@ class PlayerLifecycle {
|
|
|
148
150
|
if (!hasVoiceData) {
|
|
149
151
|
const downFor = Date.now() - player._voiceDownSince
|
|
150
152
|
if (downFor > this.VOICE_DOWN_THRESHOLD * this.VOICE_ABANDON_MULTIPLIER) {
|
|
151
|
-
const recoveryToken = player._claimVoiceRecovery(
|
|
153
|
+
const recoveryToken = player._claimVoiceRecovery(
|
|
154
|
+
'watchdog_voice_refresh'
|
|
155
|
+
)
|
|
152
156
|
if (player._isVoiceRecoveryActive(recoveryToken))
|
|
153
157
|
player.connection?._requestVoiceState?.()
|
|
154
158
|
if (player._isVoiceRecoveryActive(recoveryToken))
|
|
@@ -215,11 +219,15 @@ class PlayerLifecycle {
|
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
async attemptVoiceResume() {
|
|
222
|
+
async attemptVoiceResume(abortSignal) {
|
|
219
223
|
const player = this.player
|
|
220
|
-
if (!player.connection?.sessionId)
|
|
224
|
+
if (!player.connection?.sessionId)
|
|
225
|
+
throw new Error(`No session (guild=${player.guildId})`)
|
|
226
|
+
if (abortSignal?.aborted) throw new Error('Resume aborted by signal')
|
|
221
227
|
if (!(await player.connection.attemptResume()))
|
|
222
|
-
throw new Error(
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Resume failed (guild=${player.guildId}, endpoint=${player.connection.endpoint || 'none'})`
|
|
230
|
+
)
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
async socketClosed(_player, _track, payload) {
|
|
@@ -342,7 +350,8 @@ class PlayerLifecycle {
|
|
|
342
350
|
return false
|
|
343
351
|
})
|
|
344
352
|
}
|
|
345
|
-
if (resumed)
|
|
353
|
+
if (resumed)
|
|
354
|
+
player._clearVoiceRecovery(recoveryToken, 'socket_soft_resumed')
|
|
346
355
|
if (
|
|
347
356
|
resumed ||
|
|
348
357
|
player.connected ||
|
|
@@ -2,6 +2,7 @@ class Queue {
|
|
|
2
2
|
constructor() {
|
|
3
3
|
this._items = []
|
|
4
4
|
this._head = 0
|
|
5
|
+
this._compactThreshold = 64
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
get size() {
|
|
@@ -40,13 +41,16 @@ class Queue {
|
|
|
40
41
|
|
|
41
42
|
_compact(force = false) {
|
|
42
43
|
if (this._head <= 0) return
|
|
43
|
-
if (!force && this._head
|
|
44
|
+
if (!force && this._head < this._compactThreshold) return
|
|
44
45
|
const len = this._items.length - this._head
|
|
45
46
|
for (let i = 0; i < len; i++) {
|
|
46
47
|
this._items[i] = this._items[this._head + i]
|
|
47
48
|
}
|
|
48
49
|
this._items.length = len
|
|
49
50
|
this._head = 0
|
|
51
|
+
if (this._compactThreshold < len) {
|
|
52
|
+
this._compactThreshold = Math.min(len * 2, 1024)
|
|
53
|
+
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
shuffle() {
|
package/build/structures/Rest.js
CHANGED
|
@@ -134,6 +134,7 @@ class Rest {
|
|
|
134
134
|
this.aqua = aqua
|
|
135
135
|
this.node = node
|
|
136
136
|
this.sessionId = node.sessionId
|
|
137
|
+
this._sessionGeneration = 0
|
|
137
138
|
this.timeout = node.timeout || 30000
|
|
138
139
|
|
|
139
140
|
const protocol = node.ssl ? 'https:' : 'http:'
|
|
@@ -236,10 +237,18 @@ class Rest {
|
|
|
236
237
|
|
|
237
238
|
setSessionId(sessionId) {
|
|
238
239
|
this.sessionId = sessionId
|
|
240
|
+
this._sessionGeneration++
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
_getSessionPath() {
|
|
243
|
+
_getSessionPath(generation) {
|
|
242
244
|
if (!this.sessionId) throw ERRORS.NO_SESSION
|
|
245
|
+
if (generation != null && generation !== this._sessionGeneration) {
|
|
246
|
+
const staleErr = new Error(
|
|
247
|
+
`Stale session: sessionId was updated (expected gen ${generation}, current ${this._sessionGeneration}) for session ${this.sessionId}`
|
|
248
|
+
)
|
|
249
|
+
staleErr.statusCode = 404
|
|
250
|
+
throw staleErr
|
|
251
|
+
}
|
|
243
252
|
return `${this._apiBase}/sessions/${this.sessionId}`
|
|
244
253
|
}
|
|
245
254
|
|
|
@@ -639,28 +648,33 @@ class Rest {
|
|
|
639
648
|
}
|
|
640
649
|
|
|
641
650
|
async updatePlayer({ guildId, data, noReplace = false }) {
|
|
651
|
+
const gen = this._sessionGeneration
|
|
642
652
|
return this.makeRequest(
|
|
643
653
|
'PATCH',
|
|
644
|
-
`${this._getSessionPath()}/players/${guildId}?noReplace=${noReplace}`,
|
|
654
|
+
`${this._getSessionPath(gen)}/players/${guildId}?noReplace=${noReplace}`,
|
|
645
655
|
data
|
|
646
656
|
)
|
|
647
657
|
}
|
|
648
658
|
|
|
649
659
|
async getPlayer(guildId) {
|
|
660
|
+
const gen = this._sessionGeneration
|
|
650
661
|
return this.makeRequest(
|
|
651
662
|
'GET',
|
|
652
|
-
`${this._getSessionPath()}/players/${guildId}`
|
|
663
|
+
`${this._getSessionPath(gen)}/players/${guildId}`
|
|
653
664
|
)
|
|
654
665
|
}
|
|
655
666
|
|
|
656
667
|
async getPlayers() {
|
|
657
|
-
|
|
668
|
+
const gen = this._sessionGeneration
|
|
669
|
+
return this.makeRequest('GET', `${this._getSessionPath(gen)}/players`)
|
|
658
670
|
}
|
|
659
671
|
|
|
660
|
-
async destroyPlayer(guildId) {
|
|
672
|
+
async destroyPlayer(guildId, abortSignal) {
|
|
673
|
+
const gen = this._sessionGeneration
|
|
674
|
+
if (abortSignal?.aborted) return null
|
|
661
675
|
return this.makeRequest(
|
|
662
676
|
'DELETE',
|
|
663
|
-
`${this._getSessionPath()}/players/${guildId}`
|
|
677
|
+
`${this._getSessionPath(gen)}/players/${guildId}`
|
|
664
678
|
)
|
|
665
679
|
}
|
|
666
680
|
|
|
@@ -733,9 +747,10 @@ class Rest {
|
|
|
733
747
|
|
|
734
748
|
if (guildId) {
|
|
735
749
|
try {
|
|
750
|
+
const gen = this._sessionGeneration
|
|
736
751
|
const lyrics = await this.makeRequest(
|
|
737
752
|
'GET',
|
|
738
|
-
`${this._getSessionPath()}/players/${guildId}/track/lyrics?skipTrackSource=${skip}`
|
|
753
|
+
`${this._getSessionPath(gen)}/players/${guildId}/track/lyrics?skipTrackSource=${skip}`
|
|
739
754
|
)
|
|
740
755
|
if (this._validLyrics(lyrics)) return lyrics
|
|
741
756
|
} catch {}
|
|
@@ -776,10 +791,11 @@ class Rest {
|
|
|
776
791
|
|
|
777
792
|
async subscribeLiveLyrics(guildId, skipTrackSource = false) {
|
|
778
793
|
try {
|
|
794
|
+
const gen = this._sessionGeneration
|
|
779
795
|
return (
|
|
780
796
|
(await this.makeRequest(
|
|
781
797
|
'POST',
|
|
782
|
-
`${this._getSessionPath()}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource ? 'true' : 'false'}`
|
|
798
|
+
`${this._getSessionPath(gen)}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource ? 'true' : 'false'}`
|
|
783
799
|
)) === null
|
|
784
800
|
)
|
|
785
801
|
} catch {
|
|
@@ -789,10 +805,11 @@ class Rest {
|
|
|
789
805
|
|
|
790
806
|
async unsubscribeLiveLyrics(guildId) {
|
|
791
807
|
try {
|
|
808
|
+
const gen = this._sessionGeneration
|
|
792
809
|
return (
|
|
793
810
|
(await this.makeRequest(
|
|
794
811
|
'DELETE',
|
|
795
|
-
`${this._getSessionPath()}/players/${guildId}/lyrics/subscribe`
|
|
812
|
+
`${this._getSessionPath(gen)}/players/${guildId}/lyrics/subscribe`
|
|
796
813
|
)) === null
|
|
797
814
|
)
|
|
798
815
|
} catch {
|