aqualink 2.11.5 → 2.11.7
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/structures/Aqua.js +25 -8
- package/build/structures/Connection.js +36 -50
- package/build/structures/Player.js +127 -37
- package/package.json +1 -1
package/build/structures/Aqua.js
CHANGED
|
@@ -28,6 +28,7 @@ const DEFAULT_OPTIONS = Object.freeze({
|
|
|
28
28
|
plugins: [],
|
|
29
29
|
autoResume: false,
|
|
30
30
|
infiniteReconnects: false,
|
|
31
|
+
loadBancer: 'leastLoad', // cpu, memory, rest: leastLoad, only rest: leastRest, no check: random
|
|
31
32
|
failoverOptions: Object.freeze({
|
|
32
33
|
enabled: true,
|
|
33
34
|
maxRetries: 3,
|
|
@@ -142,21 +143,37 @@ class Aqua extends EventEmitter {
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
get leastUsedNodes() {
|
|
145
|
-
const now = Date.now()
|
|
146
|
+
const now = Date.now();
|
|
146
147
|
if (this._leastUsedNodesCache && (now - this._leastUsedNodesCacheTime) < CACHE_VALID_TIME) {
|
|
147
|
-
return this._leastUsedNodesCache
|
|
148
|
+
return this._leastUsedNodesCache;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
const connected = []
|
|
151
|
+
const connected = [];
|
|
151
152
|
for (const node of this.nodeMap.values()) {
|
|
152
|
-
if (node.connected) connected.push(node)
|
|
153
|
+
if (node.connected) connected.push(node);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
let sorted;
|
|
157
|
+
switch (this.options.loadBancer) {
|
|
158
|
+
case 'leastRest':
|
|
159
|
+
sorted = connected.slice().sort((a, b) => {
|
|
160
|
+
const restA = a?.rest?.calls || 0;
|
|
161
|
+
const restB = b?.rest?.calls || 0;
|
|
162
|
+
return restA - restB;
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
case 'random':
|
|
166
|
+
sorted = Array.from(connected);
|
|
167
|
+
break;
|
|
168
|
+
case 'leastLoad':
|
|
169
|
+
default:
|
|
170
|
+
sorted = connected.slice().sort((a, b) => this._getCachedNodeLoad(a) - this._getCachedNodeLoad(b));
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
156
173
|
|
|
157
|
-
this._leastUsedNodesCache = Object.freeze(
|
|
158
|
-
this._leastUsedNodesCacheTime = now
|
|
159
|
-
return this._leastUsedNodesCache
|
|
174
|
+
this._leastUsedNodesCache = Object.freeze(sorted);
|
|
175
|
+
this._leastUsedNodesCacheTime = now;
|
|
176
|
+
return this._leastUsedNodesCache;
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
_invalidateCache() {
|
|
@@ -142,8 +142,8 @@ class Connection {
|
|
|
142
142
|
|
|
143
143
|
setServerUpdate(data) {
|
|
144
144
|
if (!data?.endpoint || !data.token ||
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
typeof data.endpoint !== 'string' ||
|
|
146
|
+
typeof data.token !== 'string') {
|
|
147
147
|
return
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -175,18 +175,18 @@ class Connection {
|
|
|
175
175
|
this.token = data.token
|
|
176
176
|
|
|
177
177
|
if (this._player.paused) {
|
|
178
|
-
this._player.
|
|
178
|
+
this._player.pause(false)
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
this._scheduleVoiceUpdate()
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
resendVoiceUpdate({
|
|
184
|
+
resendVoiceUpdate() { // Remove the parameter
|
|
185
185
|
if (!(this.sessionId && this.endpoint && this.token)) {
|
|
186
186
|
return false
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
this._scheduleVoiceUpdate(
|
|
189
|
+
this._scheduleVoiceUpdate() // No parameter needed
|
|
190
190
|
return true
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -201,9 +201,7 @@ class Connection {
|
|
|
201
201
|
let needsUpdate = false
|
|
202
202
|
|
|
203
203
|
if (this.voiceChannel !== channel_id) {
|
|
204
|
-
|
|
205
|
-
this._aqua.emit('playerMove', this.voiceChannel, channel_id)
|
|
206
|
-
}
|
|
204
|
+
this._aqua.emit('playerMove', this.voiceChannel, channel_id)
|
|
207
205
|
this.voiceChannel = channel_id
|
|
208
206
|
this._player.voiceChannel = channel_id
|
|
209
207
|
needsUpdate = true
|
|
@@ -249,32 +247,29 @@ class Connection {
|
|
|
249
247
|
|
|
250
248
|
async attemptResume() {
|
|
251
249
|
if (!(this.sessionId && this.endpoint && this.token)) {
|
|
252
|
-
|
|
250
|
+
return false;
|
|
253
251
|
}
|
|
254
252
|
|
|
255
|
-
const payload = this._payloadPool.acquire()
|
|
253
|
+
const payload = this._payloadPool.acquire(); // USE THE POOL
|
|
256
254
|
|
|
257
255
|
try {
|
|
258
|
-
payload.guildId = this._guildId
|
|
259
|
-
payload.data.voice.token = this.token
|
|
260
|
-
payload.data.voice.endpoint = this.endpoint
|
|
261
|
-
payload.data.voice.sessionId = this.sessionId
|
|
262
|
-
payload.data.voice.resume = true
|
|
263
|
-
payload.data.volume = this._player?.volume
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
await this._sendUpdate(payload)
|
|
270
|
-
return true
|
|
256
|
+
payload.guildId = this._guildId;
|
|
257
|
+
payload.data.voice.token = this.token;
|
|
258
|
+
payload.data.voice.endpoint = this.endpoint;
|
|
259
|
+
payload.data.voice.sessionId = this.sessionId;
|
|
260
|
+
payload.data.voice.resume = true;
|
|
261
|
+
payload.data.volume = this._player?.volume;
|
|
262
|
+
|
|
263
|
+
await this._sendUpdate(payload);
|
|
264
|
+
return true;
|
|
271
265
|
} catch (error) {
|
|
266
|
+
console.log(`Voice connection resume failed in guild ${this._guildId}: ${error?.message}`);
|
|
272
267
|
if (this._stateFlags & STATE_FLAGS.HAS_DEBUG_LISTENERS) {
|
|
273
|
-
this._aqua.emit('debug', `[Player ${this._guildId}]
|
|
268
|
+
this._aqua.emit('debug', `[Player ${this._guildId}] Voice update failed: ${error?.message}`);
|
|
274
269
|
}
|
|
275
|
-
return false
|
|
270
|
+
return false;
|
|
276
271
|
} finally {
|
|
277
|
-
this._payloadPool.release(payload)
|
|
272
|
+
this._payloadPool.release(payload); // ALWAYS RELEASE
|
|
278
273
|
}
|
|
279
274
|
}
|
|
280
275
|
|
|
@@ -294,40 +289,31 @@ class Connection {
|
|
|
294
289
|
this._pendingUpdate = null
|
|
295
290
|
}
|
|
296
291
|
|
|
297
|
-
_scheduleVoiceUpdate(
|
|
292
|
+
_scheduleVoiceUpdate() {
|
|
298
293
|
if (!(this.sessionId && this.endpoint && this.token)) {
|
|
299
|
-
return
|
|
294
|
+
return;
|
|
300
295
|
}
|
|
301
296
|
|
|
302
297
|
if (this._stateFlags & STATE_FLAGS.UPDATE_SCHEDULED) {
|
|
303
|
-
return
|
|
298
|
+
return;
|
|
304
299
|
}
|
|
305
300
|
|
|
306
|
-
this._clearPendingUpdate()
|
|
307
|
-
|
|
308
|
-
const payload = this._payloadPool.acquire()
|
|
301
|
+
this._clearPendingUpdate();
|
|
309
302
|
|
|
310
|
-
payload
|
|
311
|
-
|
|
312
|
-
voice.token = this.token
|
|
313
|
-
voice.endpoint = this.endpoint
|
|
314
|
-
voice.sessionId = this.sessionId
|
|
315
|
-
payload.data.volume = this._player.volume
|
|
316
|
-
|
|
317
|
-
if (isResume) {
|
|
318
|
-
voice.resume = true
|
|
319
|
-
voice.sequence = this.sequence
|
|
320
|
-
}
|
|
303
|
+
const payload = this._payloadPool.acquire();
|
|
304
|
+
payload.guildId = this._guildId;
|
|
305
|
+
payload.data.voice.token = this.token;
|
|
306
|
+
payload.data.voice.endpoint = this.endpoint;
|
|
307
|
+
payload.data.voice.sessionId = this.sessionId;
|
|
308
|
+
payload.data.volume = this._player.volume;
|
|
321
309
|
|
|
322
310
|
this._pendingUpdate = {
|
|
323
|
-
isResume,
|
|
324
311
|
payload,
|
|
325
312
|
timestamp: Date.now()
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
this._stateFlags |= STATE_FLAGS.UPDATE_SCHEDULED
|
|
313
|
+
};
|
|
329
314
|
|
|
330
|
-
|
|
315
|
+
this._stateFlags |= STATE_FLAGS.UPDATE_SCHEDULED;
|
|
316
|
+
queueMicrotask(this._executeVoiceUpdate);
|
|
331
317
|
}
|
|
332
318
|
|
|
333
319
|
_executeVoiceUpdate() {
|
|
@@ -366,8 +352,8 @@ class Connection {
|
|
|
366
352
|
await this._nodes.rest.updatePlayer(payload)
|
|
367
353
|
} catch (error) {
|
|
368
354
|
if (error.code !== 'ECONNREFUSED' &&
|
|
369
|
-
|
|
370
|
-
|
|
355
|
+
error.code !== 'ENOTFOUND' &&
|
|
356
|
+
(this._stateFlags & STATE_FLAGS.HAS_DEBUG_LISTENERS)) {
|
|
371
357
|
this._aqua.emit('debug', `[Player ${this._guildId}] Update failed: ${error.message}`)
|
|
372
358
|
}
|
|
373
359
|
throw error
|
|
@@ -107,7 +107,7 @@ class MicrotaskUpdateBatcher {
|
|
|
107
107
|
|
|
108
108
|
try {
|
|
109
109
|
this.player?.aqua?.emit?.('error', new Error(`Update player error: ${err.message}`))
|
|
110
|
-
} catch {}
|
|
110
|
+
} catch { }
|
|
111
111
|
throw err
|
|
112
112
|
})
|
|
113
113
|
}
|
|
@@ -235,6 +235,12 @@ class Player extends EventEmitter {
|
|
|
235
235
|
this._boundEvent = this._handleEvent.bind(this)
|
|
236
236
|
this.on('playerUpdate', this._boundPlayerUpdate)
|
|
237
237
|
this.on('event', this._boundEvent)
|
|
238
|
+
this._boundAquaPlayerMove = this._handleAquaPlayerMove.bind(this)
|
|
239
|
+
this.aqua.on('playerMove', this._boundAquaPlayerMove)
|
|
240
|
+
this._voiceDownSince = 0
|
|
241
|
+
this._voiceRecovering = false
|
|
242
|
+
this._voiceWatchdogTimer = setInterval(() => this._voiceWatchdog(), 15000)
|
|
243
|
+
this._voiceWatchdogTimer.unref?.()
|
|
238
244
|
}
|
|
239
245
|
|
|
240
246
|
_handlePlayerUpdate(packet) {
|
|
@@ -246,6 +252,20 @@ class Player extends EventEmitter {
|
|
|
246
252
|
this.connected = !!state.connected
|
|
247
253
|
this.ping = typeof state.ping === 'number' ? state.ping : 0
|
|
248
254
|
this.timestamp = typeof state.time === 'number' ? state.time : Date.now()
|
|
255
|
+
|
|
256
|
+
if (!this.connected) {
|
|
257
|
+
if (!this._voiceDownSince) {
|
|
258
|
+
this._voiceDownSince = Date.now()
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
if (!this.connected && !this.destroyed) {
|
|
261
|
+
this.connection.attemptResume()
|
|
262
|
+
}
|
|
263
|
+
}, 1000)
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
this._voiceDownSince = 0
|
|
267
|
+
}
|
|
268
|
+
|
|
249
269
|
this.aqua.emit('playerUpdate', this, packet)
|
|
250
270
|
}
|
|
251
271
|
|
|
@@ -336,9 +356,55 @@ class Player extends EventEmitter {
|
|
|
336
356
|
return this
|
|
337
357
|
}
|
|
338
358
|
|
|
359
|
+
async _voiceWatchdog() {
|
|
360
|
+
if (this.destroyed || !this.voiceChannel || this.connected) return
|
|
361
|
+
if (!this._voiceDownSince || (Date.now() - this._voiceDownSince) < 10000) return
|
|
362
|
+
if (this._voiceRecovering) return
|
|
363
|
+
|
|
364
|
+
this._voiceRecovering = true
|
|
365
|
+
try {
|
|
366
|
+
try {
|
|
367
|
+
await this.connection.attemptResume()
|
|
368
|
+
this.aqua.emit('debug', `[Player ${this.guildId}] Watchdog: resume sent`)
|
|
369
|
+
return
|
|
370
|
+
} catch { }
|
|
371
|
+
|
|
372
|
+
// 2) Force a new Discord voice update to re-trigger VSU/VSS
|
|
373
|
+
const toggleMute = !this.mute
|
|
374
|
+
this.send({
|
|
375
|
+
guild_id: this.guildId,
|
|
376
|
+
channel_id: this.voiceChannel,
|
|
377
|
+
self_deaf: this.deaf,
|
|
378
|
+
self_mute: toggleMute
|
|
379
|
+
})
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
if (!this.destroyed) {
|
|
382
|
+
this.send({
|
|
383
|
+
guild_id: this.guildId,
|
|
384
|
+
channel_id: this.voiceChannel,
|
|
385
|
+
self_deaf: this.deaf,
|
|
386
|
+
self_mute: this.mute
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
}, 300)
|
|
390
|
+
|
|
391
|
+
this.connection.resendVoiceUpdate({ resume: false })
|
|
392
|
+
this.aqua.emit('debug', `[Player ${this.guildId}] Watchdog: forced voice update/rejoin`)
|
|
393
|
+
} catch (err) {
|
|
394
|
+
this.aqua.emit('debug', `[Player ${this.guildId}] Watchdog recover failed: ${err?.message || err}`)
|
|
395
|
+
} finally {
|
|
396
|
+
this._voiceRecovering = false
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
339
400
|
destroy({ preserveClient = true, skipRemote = false } = {}) {
|
|
340
401
|
if (this.destroyed) return this
|
|
341
402
|
|
|
403
|
+
if (this._voiceWatchdogTimer) {
|
|
404
|
+
clearInterval(this._voiceWatchdogTimer)
|
|
405
|
+
this._voiceWatchdogTimer = null
|
|
406
|
+
}
|
|
407
|
+
|
|
342
408
|
this.destroyed = true
|
|
343
409
|
this.connected = false
|
|
344
410
|
this.playing = false
|
|
@@ -360,6 +426,11 @@ class Player extends EventEmitter {
|
|
|
360
426
|
this._updateBatcher = null
|
|
361
427
|
}
|
|
362
428
|
|
|
429
|
+
if (this._boundAquaPlayerMove) {
|
|
430
|
+
try { this.aqua.off('playerMove', this._boundAquaPlayerMove) } catch { }
|
|
431
|
+
this._boundAquaPlayerMove = null
|
|
432
|
+
}
|
|
433
|
+
|
|
363
434
|
if (!skipRemote) {
|
|
364
435
|
try {
|
|
365
436
|
this.send({ guild_id: this.guildId, channel_id: null })
|
|
@@ -634,10 +705,10 @@ class Player extends EventEmitter {
|
|
|
634
705
|
_isInvalidResponse(response) {
|
|
635
706
|
|
|
636
707
|
return !response?.tracks?.length ||
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
708
|
+
response.loadType === 'error' ||
|
|
709
|
+
response.loadType === 'empty' ||
|
|
710
|
+
response.loadType === 'LOAD_FAILED' ||
|
|
711
|
+
response.loadType === 'NO_MATCHES'
|
|
641
712
|
}
|
|
642
713
|
|
|
643
714
|
async trackStart(player, track) {
|
|
@@ -718,54 +789,56 @@ class Player extends EventEmitter {
|
|
|
718
789
|
this.aqua.emit('trackChange', this, track, payload)
|
|
719
790
|
}
|
|
720
791
|
|
|
721
|
-
async _attemptVoiceResume() {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const ok = await this.connection.attemptResume()
|
|
727
|
-
if (!ok) throw new Error('Resume request failed')
|
|
728
|
-
return new Promise((resolve, reject) => {
|
|
729
|
-
const timeout = setTimeout(() => {
|
|
730
|
-
this.off('playerUpdate', onUpdate)
|
|
731
|
-
reject(new Error('No resume confirmation'))
|
|
732
|
-
}, 5000)
|
|
792
|
+
async _attemptVoiceResume() {
|
|
793
|
+
if (!this.connection || !this.connection.sessionId) {
|
|
794
|
+
throw new Error('Missing connection or sessionId')
|
|
795
|
+
}
|
|
733
796
|
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
797
|
+
const ok = await this.connection.attemptResume()
|
|
798
|
+
if (!ok) throw new Error('Resume request failed')
|
|
799
|
+
return new Promise((resolve, reject) => {
|
|
800
|
+
const timeout = setTimeout(() => {
|
|
737
801
|
this.off('playerUpdate', onUpdate)
|
|
738
|
-
|
|
802
|
+
reject(new Error('No resume confirmation'))
|
|
803
|
+
}, 5000)
|
|
804
|
+
|
|
805
|
+
const onUpdate = (payload) => {
|
|
806
|
+
if (payload?.state?.connected || typeof payload?.state?.time === 'number') {
|
|
807
|
+
clearTimeout(timeout)
|
|
808
|
+
this.off('playerUpdate', onUpdate)
|
|
809
|
+
resolve()
|
|
810
|
+
}
|
|
739
811
|
}
|
|
740
|
-
}
|
|
741
812
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
813
|
+
this.on('playerUpdate', onUpdate)
|
|
814
|
+
})
|
|
815
|
+
}
|
|
745
816
|
|
|
746
817
|
async socketClosed(player, track, payload) {
|
|
747
818
|
if (this.destroyed) return
|
|
748
819
|
|
|
749
820
|
const code = payload?.code
|
|
750
821
|
|
|
751
|
-
|
|
822
|
+
// playerMove is handled by a single listener registered in constructor
|
|
823
|
+
|
|
824
|
+
if (code === 4022) {
|
|
752
825
|
this.aqua.emit('socketClosed', this, payload)
|
|
753
826
|
this.destroy()
|
|
754
827
|
return
|
|
755
828
|
}
|
|
756
829
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
830
|
+
if (code === 4015) {
|
|
831
|
+
this.aqua.emit('debug', `[Player ${this.guildId}] Voice server crashed (4015), attempting resume...`)
|
|
832
|
+
try {
|
|
833
|
+
// Race resume with timeout inside _attemptVoiceResume
|
|
834
|
+
await this._attemptVoiceResume()
|
|
835
|
+
this.aqua.emit('debug', `[Player ${this.guildId}] Voice resume succeeded`)
|
|
836
|
+
return
|
|
837
|
+
} catch (err) {
|
|
838
|
+
this.aqua.emit('debug', `[Player ${this.guildId}] Resume failed: ${err.message}. Falling back to reconnect`)
|
|
839
|
+
// fall through to your existing reconnect flow below
|
|
840
|
+
}
|
|
767
841
|
}
|
|
768
|
-
}
|
|
769
842
|
|
|
770
843
|
if (code !== 4015 && code !== 4009 && code !== 4006) {
|
|
771
844
|
this.aqua.emit('socketClosed', this, payload)
|
|
@@ -884,6 +957,23 @@ async _attemptVoiceResume() {
|
|
|
884
957
|
this.aqua.emit('lyricsNotFound', this, track, payload)
|
|
885
958
|
}
|
|
886
959
|
|
|
960
|
+
_handleAquaPlayerMove(oldChannel, newChannel) {
|
|
961
|
+
try {
|
|
962
|
+
if (fnToId(oldChannel) === fnToId(this.voiceChannel)) {
|
|
963
|
+
this.voiceChannel = fnToId(newChannel)
|
|
964
|
+
this.connected = !!newChannel
|
|
965
|
+
this.send({
|
|
966
|
+
guild_id: options.guildId || this.guildId,
|
|
967
|
+
channel_id: this.voiceChannel,
|
|
968
|
+
self_deaf: this.deaf,
|
|
969
|
+
self_mute: this.mute
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
} catch (err) {
|
|
973
|
+
// don't let playerMove errors propagate
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
887
977
|
send(data) {
|
|
888
978
|
try {
|
|
889
979
|
this.aqua.send({ op: 4, d: data })
|