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.
- package/README.md +174 -184
- package/build/handlers/autoplay.js +5 -1
- package/build/index.d.ts +1 -1
- package/build/structures/Aqua.js +235 -540
- package/build/structures/AquaRecovery.js +905 -0
- package/build/structures/Connection.js +84 -262
- package/build/structures/ConnectionRecovery.js +425 -0
- package/build/structures/Filters.js +96 -13
- package/build/structures/Node.js +175 -72
- package/build/structures/Player.js +344 -338
- package/build/structures/PlayerLifecycle.js +584 -0
- package/build/structures/PlayerLifecycleState.js +42 -0
- package/build/structures/Queue.js +5 -1
- package/build/structures/Reporting.js +32 -0
- package/build/structures/Rest.js +51 -11
- package/build/structures/Track.js +2 -2
- package/package.json +1 -1
package/build/structures/Aqua.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
const fs = require('node:fs')
|
|
2
|
-
const
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const _readline = require('node:readline')
|
|
3
4
|
const { EventEmitter } = require('node:events')
|
|
4
5
|
const { AqualinkEvents } = require('./AqualinkEvents')
|
|
6
|
+
const AquaRecovery = require('./AquaRecovery')
|
|
5
7
|
const Node = require('./Node')
|
|
6
8
|
const Player = require('./Player')
|
|
7
9
|
const Track = require('./Track')
|
|
10
|
+
const { reportSuppressedError } = require('./Reporting')
|
|
8
11
|
const { version: pkgVersion } = require('../../package.json')
|
|
9
12
|
|
|
10
13
|
const SEARCH_PREFIX = ':'
|
|
@@ -29,6 +32,7 @@ const MAX_FAILOVER_QUEUE = 50
|
|
|
29
32
|
const MAX_REBUILD_LOCKS = 100
|
|
30
33
|
const WRITE_BUFFER_SIZE = 100
|
|
31
34
|
const TRACE_BUFFER_SIZE = 3000
|
|
35
|
+
const VOICE_STATE_QUEUE_INTERVAL = 900
|
|
32
36
|
|
|
33
37
|
const DEFAULT_OPTIONS = Object.freeze({
|
|
34
38
|
shouldDeleteMessage: false,
|
|
@@ -54,7 +58,9 @@ const DEFAULT_OPTIONS = Object.freeze({
|
|
|
54
58
|
maxFailoverAttempts: 5
|
|
55
59
|
}),
|
|
56
60
|
maxQueueSave: 10,
|
|
57
|
-
maxTracksRestore: 20
|
|
61
|
+
maxTracksRestore: 20,
|
|
62
|
+
trackResolveConcurrency: 4,
|
|
63
|
+
brokenPlayerStorePath: null
|
|
58
64
|
})
|
|
59
65
|
|
|
60
66
|
const _functions = {
|
|
@@ -130,6 +136,15 @@ class Aqua extends EventEmitter {
|
|
|
130
136
|
this.useHttp2 = merged.useHttp2
|
|
131
137
|
this.maxQueueSave = merged.maxQueueSave
|
|
132
138
|
this.maxTracksRestore = merged.maxTracksRestore
|
|
139
|
+
this.trackResolveConcurrency = Math.max(
|
|
140
|
+
1,
|
|
141
|
+
Number(merged.trackResolveConcurrency) || 4
|
|
142
|
+
)
|
|
143
|
+
this.brokenPlayerStorePath =
|
|
144
|
+
typeof merged.brokenPlayerStorePath === 'string' &&
|
|
145
|
+
merged.brokenPlayerStorePath.trim()
|
|
146
|
+
? merged.brokenPlayerStorePath
|
|
147
|
+
: path.join(process.cwd(), `AquaBrokenPlayers.${process.pid}.jsonl`)
|
|
133
148
|
this.send = merged.send || this._createDefaultSend()
|
|
134
149
|
this.debugTrace = !!merged.debugTrace
|
|
135
150
|
this.traceMaxEntries = Math.max(
|
|
@@ -138,14 +153,13 @@ class Aqua extends EventEmitter {
|
|
|
138
153
|
)
|
|
139
154
|
this.traceSink =
|
|
140
155
|
typeof merged.traceSink === 'function' ? merged.traceSink : null
|
|
141
|
-
this._traceBuffer = new Array(this.traceMaxEntries)
|
|
156
|
+
this._traceBuffer = this.debugTrace ? new Array(this.traceMaxEntries) : null
|
|
142
157
|
this._traceBufferCount = 0
|
|
143
158
|
this._traceBufferIndex = 0
|
|
144
159
|
this._traceSeq = 0
|
|
145
160
|
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
148
|
-
this._lastFailoverAttempt = new Map()
|
|
161
|
+
this._failoverState = Object.create(null)
|
|
162
|
+
this._guildLifecycleLocks = new Map()
|
|
149
163
|
this._brokenPlayers = new Map()
|
|
150
164
|
this._rebuildLocks = new Set()
|
|
151
165
|
this._leastUsedNodesCache = null
|
|
@@ -153,22 +167,44 @@ class Aqua extends EventEmitter {
|
|
|
153
167
|
this._nodeLoadCache = new Map()
|
|
154
168
|
this._eventHandlers = null
|
|
155
169
|
this._loading = false
|
|
170
|
+
this._voiceStateQueue = []
|
|
171
|
+
this._voiceStateQueueHead = 0
|
|
172
|
+
this._voiceStateQueued = new Set()
|
|
173
|
+
this._voiceStatePending = new Map()
|
|
174
|
+
this._voiceStateFlushTimer = null
|
|
175
|
+
this._lastVoiceStateSendAt = 0
|
|
176
|
+
this._recovery = new AquaRecovery(this, {
|
|
177
|
+
_functions,
|
|
178
|
+
MAX_CONCURRENT_OPS,
|
|
179
|
+
BROKEN_PLAYER_TTL,
|
|
180
|
+
FAILOVER_CLEANUP_TTL,
|
|
181
|
+
MAX_FAILOVER_QUEUE,
|
|
182
|
+
MAX_REBUILD_LOCKS,
|
|
183
|
+
PLAYER_BATCH_SIZE,
|
|
184
|
+
RECONNECT_DELAY,
|
|
185
|
+
NODE_TIMEOUT,
|
|
186
|
+
EMPTY_ARRAY
|
|
187
|
+
})
|
|
156
188
|
|
|
157
189
|
if (this.autoResume) this._bindEventHandlers()
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
_trace(event, data = null) {
|
|
161
193
|
if (!this.debugTrace) return
|
|
194
|
+
if (
|
|
195
|
+
!this._traceBuffer ||
|
|
196
|
+
this._traceBuffer.length !== this.traceMaxEntries
|
|
197
|
+
) {
|
|
198
|
+
this._traceBuffer = new Array(this.traceMaxEntries)
|
|
199
|
+
this._traceBufferCount = 0
|
|
200
|
+
this._traceBufferIndex = 0
|
|
201
|
+
}
|
|
202
|
+
const resolvedData = typeof data === 'function' ? data() : data
|
|
162
203
|
const entry = {
|
|
163
204
|
seq: ++this._traceSeq,
|
|
164
205
|
at: Date.now(),
|
|
165
206
|
event,
|
|
166
|
-
data
|
|
167
|
-
}
|
|
168
|
-
if (this._traceBuffer.length !== this.traceMaxEntries) {
|
|
169
|
-
this._traceBuffer = new Array(this.traceMaxEntries)
|
|
170
|
-
this._traceBufferCount = 0
|
|
171
|
-
this._traceBufferIndex = 0
|
|
207
|
+
data: resolvedData
|
|
172
208
|
}
|
|
173
209
|
this._traceBuffer[this._traceBufferIndex] = entry
|
|
174
210
|
this._traceBufferIndex = (this._traceBufferIndex + 1) % this.traceMaxEntries
|
|
@@ -181,6 +217,7 @@ class Aqua extends EventEmitter {
|
|
|
181
217
|
|
|
182
218
|
getTrace(limit = 300) {
|
|
183
219
|
const max = Math.max(1, Number(limit) || 300)
|
|
220
|
+
if (!this._traceBuffer) return []
|
|
184
221
|
const count = Math.min(max, this._traceBufferCount)
|
|
185
222
|
if (!count) return []
|
|
186
223
|
const out = new Array(count)
|
|
@@ -195,7 +232,7 @@ class Aqua extends EventEmitter {
|
|
|
195
232
|
}
|
|
196
233
|
|
|
197
234
|
clearTrace() {
|
|
198
|
-
this._traceBuffer.fill(undefined)
|
|
235
|
+
if (this._traceBuffer) this._traceBuffer.fill(undefined)
|
|
199
236
|
this._traceBufferCount = 0
|
|
200
237
|
this._traceBufferIndex = 0
|
|
201
238
|
}
|
|
@@ -214,27 +251,124 @@ class Aqua extends EventEmitter {
|
|
|
214
251
|
}
|
|
215
252
|
}
|
|
216
253
|
|
|
254
|
+
queueVoiceStateUpdate(data) {
|
|
255
|
+
const guildId = data?.guild_id ? String(data.guild_id) : null
|
|
256
|
+
if (!guildId) return false
|
|
257
|
+
|
|
258
|
+
this._voiceStatePending.set(guildId, data)
|
|
259
|
+
if (!this._voiceStateQueued.has(guildId)) {
|
|
260
|
+
this._voiceStateQueued.add(guildId)
|
|
261
|
+
this._voiceStateQueue.push(guildId)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (this.debugTrace) {
|
|
265
|
+
this._trace('voice.queue.enqueue', {
|
|
266
|
+
guildId,
|
|
267
|
+
size: this._voiceStateQueued.size
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
this._scheduleVoiceStateFlush()
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_scheduleVoiceStateFlush(delay = 0) {
|
|
275
|
+
if (this._voiceStateFlushTimer) return
|
|
276
|
+
this._voiceStateFlushTimer = setTimeout(
|
|
277
|
+
() => {
|
|
278
|
+
this._voiceStateFlushTimer = null
|
|
279
|
+
this._flushVoiceStateQueue()
|
|
280
|
+
},
|
|
281
|
+
Math.max(0, delay)
|
|
282
|
+
)
|
|
283
|
+
this._voiceStateFlushTimer.unref?.()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_flushVoiceStateQueue() {
|
|
287
|
+
if (!this._voiceStateQueued.size) return
|
|
288
|
+
|
|
289
|
+
const now = Date.now()
|
|
290
|
+
const waitFor =
|
|
291
|
+
VOICE_STATE_QUEUE_INTERVAL - (now - this._lastVoiceStateSendAt)
|
|
292
|
+
if (waitFor > 0) {
|
|
293
|
+
this._scheduleVoiceStateFlush(waitFor)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let guildId = null
|
|
298
|
+
while (this._voiceStateQueueHead < this._voiceStateQueue.length) {
|
|
299
|
+
const candidate = this._voiceStateQueue[this._voiceStateQueueHead]
|
|
300
|
+
this._voiceStateQueue[this._voiceStateQueueHead] = undefined
|
|
301
|
+
this._voiceStateQueueHead++
|
|
302
|
+
if (candidate && this._voiceStateQueued.has(candidate)) {
|
|
303
|
+
guildId = candidate
|
|
304
|
+
this._voiceStateQueued.delete(candidate)
|
|
305
|
+
break
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (
|
|
310
|
+
this._voiceStateQueueHead > 1024 ||
|
|
311
|
+
this._voiceStateQueueHead > this._voiceStateQueue.length / 2
|
|
312
|
+
) {
|
|
313
|
+
this._voiceStateQueue = this._voiceStateQueue.slice(
|
|
314
|
+
this._voiceStateQueueHead
|
|
315
|
+
)
|
|
316
|
+
this._voiceStateQueueHead = 0
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const data = guildId ? this._voiceStatePending.get(guildId) : null
|
|
320
|
+
if (guildId) this._voiceStatePending.delete(guildId)
|
|
321
|
+
|
|
322
|
+
if (data) {
|
|
323
|
+
this._lastVoiceStateSendAt = now
|
|
324
|
+
if (this.debugTrace) {
|
|
325
|
+
this._trace('voice.queue.send', {
|
|
326
|
+
guildId,
|
|
327
|
+
remaining: this._voiceStateQueued.size
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
_functions.safeCall(() => this.send({ op: 4, d: data }))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (this._voiceStateQueued.size) {
|
|
334
|
+
this._scheduleVoiceStateFlush(VOICE_STATE_QUEUE_INTERVAL)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
217
338
|
_bindEventHandlers() {
|
|
218
339
|
this._eventHandlers = {
|
|
219
340
|
onNodeConnect: (node) => {
|
|
220
|
-
this.
|
|
341
|
+
if (this.debugTrace)
|
|
342
|
+
this._trace('node.connect', { node: node?.name || node?.host })
|
|
221
343
|
this._invalidateCache()
|
|
222
344
|
this._performCleanup()
|
|
223
345
|
},
|
|
224
346
|
onNodeDisconnect: (node) => {
|
|
225
|
-
this.
|
|
347
|
+
if (this.debugTrace)
|
|
348
|
+
this._trace('node.disconnect', { node: node?.name || node?.host })
|
|
226
349
|
this._invalidateCache()
|
|
227
350
|
queueMicrotask(() => {
|
|
228
|
-
this._storeBrokenPlayers(node)
|
|
351
|
+
this._storeBrokenPlayers(node).catch((error) =>
|
|
352
|
+
reportSuppressedError(
|
|
353
|
+
this,
|
|
354
|
+
'aqua.nodeDisconnect.storeBrokenPlayers',
|
|
355
|
+
error,
|
|
356
|
+
{
|
|
357
|
+
node: node?.name || node?.host
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
)
|
|
229
361
|
this._performCleanup()
|
|
230
362
|
})
|
|
231
363
|
},
|
|
232
364
|
onNodeReady: (node, { resumed }) => {
|
|
233
|
-
this.
|
|
234
|
-
node
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
365
|
+
if (this.debugTrace) {
|
|
366
|
+
this._trace('node.ready', {
|
|
367
|
+
node: node?.name || node?.host,
|
|
368
|
+
resumed: !!resumed,
|
|
369
|
+
players: this.players.size
|
|
370
|
+
})
|
|
371
|
+
}
|
|
238
372
|
if (resumed) {
|
|
239
373
|
const batch = []
|
|
240
374
|
for (const player of this.players.values()) {
|
|
@@ -249,7 +383,16 @@ class Aqua extends EventEmitter {
|
|
|
249
383
|
return
|
|
250
384
|
}
|
|
251
385
|
queueMicrotask(() => {
|
|
252
|
-
this._rebuildBrokenPlayers(node).catch(
|
|
386
|
+
this._rebuildBrokenPlayers(node).catch((error) =>
|
|
387
|
+
reportSuppressedError(
|
|
388
|
+
this,
|
|
389
|
+
'aqua.nodeReady.rebuildBrokenPlayers',
|
|
390
|
+
error,
|
|
391
|
+
{
|
|
392
|
+
node: node?.name || node?.host
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
)
|
|
253
396
|
})
|
|
254
397
|
}
|
|
255
398
|
}
|
|
@@ -269,19 +412,28 @@ class Aqua extends EventEmitter {
|
|
|
269
412
|
this._eventHandlers = null
|
|
270
413
|
}
|
|
271
414
|
this.removeAllListeners()
|
|
415
|
+
if (this._voiceStateFlushTimer) {
|
|
416
|
+
clearTimeout(this._voiceStateFlushTimer)
|
|
417
|
+
this._voiceStateFlushTimer = null
|
|
418
|
+
}
|
|
419
|
+
this._voiceStateQueue.length = 0
|
|
420
|
+
this._voiceStateQueueHead = 0
|
|
421
|
+
this._voiceStateQueued.clear()
|
|
422
|
+
this._voiceStatePending.clear()
|
|
272
423
|
|
|
273
424
|
for (const id of Array.from(this.nodeMap.keys())) this._destroyNode(id)
|
|
274
425
|
for (const player of Array.from(this.players.values()))
|
|
275
426
|
_functions.safeCall(() => player.destroy())
|
|
276
427
|
|
|
277
428
|
this.players.clear()
|
|
278
|
-
this.
|
|
279
|
-
this.
|
|
280
|
-
this._lastFailoverAttempt.clear()
|
|
429
|
+
this._failoverState = Object.create(null)
|
|
430
|
+
this._guildLifecycleLocks.clear()
|
|
281
431
|
this._brokenPlayers.clear()
|
|
282
432
|
this._rebuildLocks.clear()
|
|
283
433
|
this._nodeLoadCache.clear()
|
|
284
434
|
this._invalidateCache()
|
|
435
|
+
_functions.safeCall(() => this._recovery?.dispose?.())
|
|
436
|
+
this._recovery = null
|
|
285
437
|
}
|
|
286
438
|
|
|
287
439
|
get leastUsedNodes() {
|
|
@@ -361,7 +513,9 @@ class Aqua extends EventEmitter {
|
|
|
361
513
|
|
|
362
514
|
if (this.initiated) return this
|
|
363
515
|
if (!this.clientId) return this
|
|
364
|
-
await this._loadNodeSessions().catch(() =>
|
|
516
|
+
await this._loadNodeSessions().catch((error) =>
|
|
517
|
+
reportSuppressedError(this, 'aqua.init.loadNodeSessions', error)
|
|
518
|
+
)
|
|
365
519
|
const results = await Promise.allSettled(
|
|
366
520
|
this.nodes.map((n) =>
|
|
367
521
|
Promise.race([
|
|
@@ -389,10 +543,16 @@ class Aqua extends EventEmitter {
|
|
|
389
543
|
const node = new Node(this, options, this.options)
|
|
390
544
|
node.players = new Set()
|
|
391
545
|
this.nodeMap.set(id, node)
|
|
392
|
-
this.
|
|
546
|
+
this._failoverState[id] = {
|
|
547
|
+
connected: false,
|
|
548
|
+
failoverInProgress: false,
|
|
549
|
+
attempts: 0,
|
|
550
|
+
lastAttempt: 0
|
|
551
|
+
}
|
|
393
552
|
try {
|
|
394
553
|
await node.connect()
|
|
395
|
-
this.
|
|
554
|
+
this._failoverState[id].connected = true
|
|
555
|
+
this._failoverState[id].failoverInProgress = false
|
|
396
556
|
this._invalidateCache()
|
|
397
557
|
this.emit(AqualinkEvents.NodeCreate, node)
|
|
398
558
|
return node
|
|
@@ -416,361 +576,61 @@ class Aqua extends EventEmitter {
|
|
|
416
576
|
_functions.safeCall(() => node.players.clear())
|
|
417
577
|
this.nodeMap.delete(id)
|
|
418
578
|
}
|
|
419
|
-
this.
|
|
420
|
-
this._failoverQueue.delete(id)
|
|
421
|
-
this._lastFailoverAttempt.delete(id)
|
|
579
|
+
delete this._failoverState[id]
|
|
422
580
|
this._nodeLoadCache.delete(id)
|
|
423
581
|
this._invalidateCache()
|
|
424
582
|
}
|
|
425
583
|
|
|
426
584
|
_storeBrokenPlayers(node) {
|
|
427
|
-
|
|
428
|
-
const now = Date.now()
|
|
429
|
-
for (const player of this.players.values()) {
|
|
430
|
-
if (player.nodes !== node) continue
|
|
431
|
-
const state = this._capturePlayerState(player)
|
|
432
|
-
if (state) {
|
|
433
|
-
state.originalNodeId = id
|
|
434
|
-
state.brokenAt = now
|
|
435
|
-
this._brokenPlayers.set(String(player.guildId), state)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
585
|
+
return this._recovery.storeBrokenPlayers(node)
|
|
438
586
|
}
|
|
439
587
|
|
|
440
588
|
async _rebuildBrokenPlayers(node) {
|
|
441
|
-
|
|
442
|
-
const rebuilds = []
|
|
443
|
-
const now = Date.now()
|
|
444
|
-
for (const [guildId, state] of this._brokenPlayers) {
|
|
445
|
-
if (
|
|
446
|
-
state.originalNodeId === id &&
|
|
447
|
-
now - state.brokenAt < BROKEN_PLAYER_TTL
|
|
448
|
-
) {
|
|
449
|
-
rebuilds.push({ guildId, state })
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (!rebuilds.length) return
|
|
453
|
-
const successes = []
|
|
454
|
-
for (let i = 0; i < rebuilds.length; i += MAX_CONCURRENT_OPS) {
|
|
455
|
-
const batch = rebuilds.slice(i, i + MAX_CONCURRENT_OPS)
|
|
456
|
-
const results = await Promise.allSettled(
|
|
457
|
-
batch.map(({ guildId, state }) =>
|
|
458
|
-
this._rebuildPlayer(state, node).then(() => guildId)
|
|
459
|
-
)
|
|
460
|
-
)
|
|
461
|
-
for (const r of results) {
|
|
462
|
-
if (r.status === 'fulfilled') successes.push(r.value)
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
for (const guildId of successes) this._brokenPlayers.delete(guildId)
|
|
466
|
-
if (successes.length)
|
|
467
|
-
this.emit(AqualinkEvents.PlayersRebuilt, node, successes.length)
|
|
468
|
-
this._performCleanup()
|
|
589
|
+
return this._recovery.rebuildBrokenPlayers(node)
|
|
469
590
|
}
|
|
470
591
|
|
|
471
592
|
async _rebuildPlayer(state, targetNode) {
|
|
472
|
-
|
|
473
|
-
guildId,
|
|
474
|
-
textChannel,
|
|
475
|
-
voiceChannel,
|
|
476
|
-
current,
|
|
477
|
-
volume = 65,
|
|
478
|
-
deaf = true
|
|
479
|
-
} = state
|
|
480
|
-
const lockKey = `rebuild_${guildId}`
|
|
481
|
-
if (this._rebuildLocks.has(lockKey)) return
|
|
482
|
-
this._rebuildLocks.add(lockKey)
|
|
483
|
-
try {
|
|
484
|
-
if (this.players.has(guildId)) {
|
|
485
|
-
await this.destroyPlayer(guildId)
|
|
486
|
-
await _functions.delay(RECONNECT_DELAY)
|
|
487
|
-
}
|
|
488
|
-
const player = this.createPlayer(targetNode, {
|
|
489
|
-
guildId,
|
|
490
|
-
textChannel,
|
|
491
|
-
voiceChannel,
|
|
492
|
-
defaultVolume: volume,
|
|
493
|
-
deaf
|
|
494
|
-
})
|
|
495
|
-
if (current && player?.queue?.add) {
|
|
496
|
-
player.queue.add(current)
|
|
497
|
-
await player.play()
|
|
498
|
-
this._seekAfterTrackStart(player, guildId, state.position, 50)
|
|
499
|
-
if (state.paused) player.pause(true)
|
|
500
|
-
}
|
|
501
|
-
return player
|
|
502
|
-
} finally {
|
|
503
|
-
this._rebuildLocks.delete(lockKey)
|
|
504
|
-
}
|
|
593
|
+
return this._recovery.rebuildPlayer(state, targetNode)
|
|
505
594
|
}
|
|
506
595
|
|
|
507
596
|
async handleNodeFailover(failedNode) {
|
|
508
|
-
|
|
509
|
-
const id = failedNode.name || failedNode.host
|
|
510
|
-
const now = Date.now()
|
|
511
|
-
const state = this._nodeStates.get(id)
|
|
512
|
-
if (state?.failoverInProgress) return
|
|
513
|
-
const lastAttempt = this._lastFailoverAttempt.get(id)
|
|
514
|
-
if (lastAttempt && now - lastAttempt < this.failoverOptions.cooldownTime)
|
|
515
|
-
return
|
|
516
|
-
const attempts = this._failoverQueue.get(id) || 0
|
|
517
|
-
if (attempts >= this.failoverOptions.maxFailoverAttempts) return
|
|
518
|
-
|
|
519
|
-
this._nodeStates.set(id, { connected: false, failoverInProgress: true })
|
|
520
|
-
this._lastFailoverAttempt.set(id, now)
|
|
521
|
-
this._failoverQueue.set(id, attempts + 1)
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
this.emit(AqualinkEvents.NodeFailover, failedNode)
|
|
525
|
-
const players = Array.from(failedNode.players || [])
|
|
526
|
-
if (!players.length) return
|
|
527
|
-
const available = []
|
|
528
|
-
for (const n of this.nodeMap.values()) {
|
|
529
|
-
if (n !== failedNode && n.connected) available.push(n)
|
|
530
|
-
}
|
|
531
|
-
if (!available.length) throw new Error('No failover nodes')
|
|
532
|
-
const results = await this._migratePlayersOptimized(players, available)
|
|
533
|
-
const successful = results.filter((r) => r.success).length
|
|
534
|
-
if (successful) {
|
|
535
|
-
this.emit(
|
|
536
|
-
AqualinkEvents.NodeFailoverComplete,
|
|
537
|
-
failedNode,
|
|
538
|
-
successful,
|
|
539
|
-
results.length - successful
|
|
540
|
-
)
|
|
541
|
-
this._performCleanup()
|
|
542
|
-
}
|
|
543
|
-
} catch (error) {
|
|
544
|
-
this.emit(AqualinkEvents.Error, null, error)
|
|
545
|
-
} finally {
|
|
546
|
-
this._nodeStates.set(id, { connected: false, failoverInProgress: false })
|
|
547
|
-
}
|
|
597
|
+
return this._recovery.handleNodeFailover(failedNode)
|
|
548
598
|
}
|
|
549
599
|
|
|
550
600
|
async _migratePlayersOptimized(players, nodes) {
|
|
551
|
-
|
|
552
|
-
const counts = new Map()
|
|
553
|
-
for (const n of nodes) {
|
|
554
|
-
loads.set(n, this._getNodeLoad(n))
|
|
555
|
-
counts.set(n, 0)
|
|
556
|
-
}
|
|
557
|
-
const pickNode = () => {
|
|
558
|
-
let best = nodes[0],
|
|
559
|
-
bestScore = loads.get(best) + counts.get(best)
|
|
560
|
-
for (let i = 1; i < nodes.length; i++) {
|
|
561
|
-
const score = loads.get(nodes[i]) + counts.get(nodes[i])
|
|
562
|
-
if (score < bestScore) {
|
|
563
|
-
best = nodes[i]
|
|
564
|
-
bestScore = score
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
counts.set(best, counts.get(best) + 1)
|
|
568
|
-
return best
|
|
569
|
-
}
|
|
570
|
-
const results = []
|
|
571
|
-
for (let i = 0; i < players.length; i += MAX_CONCURRENT_OPS) {
|
|
572
|
-
const batch = players.slice(i, i + MAX_CONCURRENT_OPS)
|
|
573
|
-
const batchResults = await Promise.allSettled(
|
|
574
|
-
batch.map((p) => this._migratePlayer(p, pickNode))
|
|
575
|
-
)
|
|
576
|
-
for (const r of batchResults)
|
|
577
|
-
results.push({ success: r.status === 'fulfilled', error: r.reason })
|
|
578
|
-
}
|
|
579
|
-
return results
|
|
601
|
+
return this._recovery.migratePlayersOptimized(players, nodes)
|
|
580
602
|
}
|
|
581
603
|
|
|
582
604
|
async _migratePlayer(player, pickNode) {
|
|
583
|
-
|
|
584
|
-
if (!state) throw new Error('Failed to capture state')
|
|
585
|
-
const { maxRetries, retryDelay } = this.failoverOptions
|
|
586
|
-
for (let retry = 0; retry < maxRetries; retry++) {
|
|
587
|
-
try {
|
|
588
|
-
const targetNode = pickNode()
|
|
589
|
-
const newPlayer = this._createPlayerOnNode(targetNode, state)
|
|
590
|
-
await this._restorePlayerState(newPlayer, state)
|
|
591
|
-
this.emit(AqualinkEvents.PlayerMigrated, player, newPlayer, targetNode)
|
|
592
|
-
return newPlayer
|
|
593
|
-
} catch (error) {
|
|
594
|
-
if (retry === maxRetries - 1) throw error
|
|
595
|
-
await _functions.delay(retryDelay * 1.5 ** retry)
|
|
596
|
-
}
|
|
597
|
-
}
|
|
605
|
+
return this._recovery.migratePlayer(player, pickNode)
|
|
598
606
|
}
|
|
599
607
|
|
|
600
608
|
_regionMatches(configuredRegion, extractedRegion) {
|
|
601
|
-
|
|
602
|
-
const configured = String(configuredRegion).trim().toLowerCase()
|
|
603
|
-
const extracted = String(extractedRegion).trim().toLowerCase()
|
|
604
|
-
if (!configured || !extracted) return false
|
|
605
|
-
return configured === extracted
|
|
609
|
+
return this._recovery.regionMatches(configuredRegion, extractedRegion)
|
|
606
610
|
}
|
|
607
611
|
|
|
608
612
|
_findBestNodeForRegion(region) {
|
|
609
|
-
|
|
610
|
-
const candidates = []
|
|
611
|
-
for (const node of this.nodeMap.values()) {
|
|
612
|
-
if (!node?.connected) continue
|
|
613
|
-
const regions = Array.isArray(node.regions) ? node.regions : []
|
|
614
|
-
if (regions.some((r) => this._regionMatches(r, region))) {
|
|
615
|
-
candidates.push(node)
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
if (!candidates.length) return null
|
|
619
|
-
return this._chooseLeastBusyNode(candidates)
|
|
613
|
+
return this._recovery.findBestNodeForRegion(region)
|
|
620
614
|
}
|
|
621
615
|
|
|
622
616
|
async movePlayerToNode(guildId, targetNode, reason = 'region') {
|
|
623
|
-
|
|
624
|
-
const player = this.players.get(id)
|
|
625
|
-
if (!player || player.destroyed) throw new Error(`Player not found: ${id}`)
|
|
626
|
-
if (!targetNode?.connected) throw new Error('Target node is not connected')
|
|
627
|
-
if (player.nodes === targetNode || player.nodes?.name === targetNode.name)
|
|
628
|
-
return player
|
|
629
|
-
|
|
630
|
-
const state = this._capturePlayerState(player)
|
|
631
|
-
if (!state) throw new Error(`Failed to capture state for ${id}`)
|
|
632
|
-
const oldPlayer = player
|
|
633
|
-
const oldNode = oldPlayer.nodes
|
|
634
|
-
const oldMessage = oldPlayer.nowPlayingMessage || null
|
|
635
|
-
const oldConn = oldPlayer.connection
|
|
636
|
-
const oldVoice = oldConn
|
|
637
|
-
? {
|
|
638
|
-
sessionId: oldConn.sessionId || null,
|
|
639
|
-
endpoint: oldConn.endpoint || null,
|
|
640
|
-
token: oldConn.token || null,
|
|
641
|
-
region: oldConn.region || null,
|
|
642
|
-
channelId: oldConn.channelId || null
|
|
643
|
-
}
|
|
644
|
-
: null
|
|
645
|
-
|
|
646
|
-
oldPlayer.destroy({
|
|
647
|
-
preserveClient: true,
|
|
648
|
-
skipRemote: true,
|
|
649
|
-
preserveMessage: true,
|
|
650
|
-
preserveTracks: true,
|
|
651
|
-
preserveReconnecting: true
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
const newPlayer = this.createPlayer(targetNode, {
|
|
655
|
-
guildId: state.guildId,
|
|
656
|
-
textChannel: state.textChannel,
|
|
657
|
-
voiceChannel: state.voiceChannel,
|
|
658
|
-
defaultVolume: state.volume || 100,
|
|
659
|
-
deaf: state.deaf || false,
|
|
660
|
-
mute: oldPlayer.mute || false,
|
|
661
|
-
resuming: true,
|
|
662
|
-
preserveMessage: true
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
// Bootstrap voice on the new node using last known voice state to avoid
|
|
666
|
-
// "track queued, waiting for voice state" after region migration.
|
|
667
|
-
if (oldVoice && newPlayer.connection) {
|
|
668
|
-
if (oldVoice.sessionId)
|
|
669
|
-
newPlayer.connection.sessionId = oldVoice.sessionId
|
|
670
|
-
if (oldVoice.endpoint) newPlayer.connection.endpoint = oldVoice.endpoint
|
|
671
|
-
if (oldVoice.token) newPlayer.connection.token = oldVoice.token
|
|
672
|
-
if (oldVoice.region) newPlayer.connection.region = oldVoice.region
|
|
673
|
-
if (oldVoice.channelId)
|
|
674
|
-
newPlayer.connection.channelId = oldVoice.channelId
|
|
675
|
-
newPlayer.connection._lastVoiceDataUpdate = Date.now()
|
|
676
|
-
newPlayer.connection.resendVoiceUpdate(true)
|
|
677
|
-
this._trace('player.migrate.voiceBootstrap', {
|
|
678
|
-
guildId: id,
|
|
679
|
-
from: oldNode?.name || oldNode?.host,
|
|
680
|
-
to: targetNode?.name || targetNode?.host,
|
|
681
|
-
hasSessionId: !!newPlayer.connection.sessionId,
|
|
682
|
-
hasEndpoint: !!newPlayer.connection.endpoint,
|
|
683
|
-
hasToken: !!newPlayer.connection.token
|
|
684
|
-
})
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
await this._restorePlayerState(newPlayer, state)
|
|
688
|
-
if (oldMessage) newPlayer.nowPlayingMessage = oldMessage
|
|
689
|
-
|
|
690
|
-
this._trace('player.migrated', {
|
|
691
|
-
guildId: id,
|
|
692
|
-
reason,
|
|
693
|
-
from: oldNode?.name || oldNode?.host,
|
|
694
|
-
to: targetNode?.name || targetNode?.host,
|
|
695
|
-
region:
|
|
696
|
-
newPlayer?.connection?.region || oldPlayer?.connection?.region || null
|
|
697
|
-
})
|
|
698
|
-
this.emit(AqualinkEvents.PlayerMigrated, oldPlayer, newPlayer, targetNode)
|
|
699
|
-
return newPlayer
|
|
617
|
+
return this._recovery.movePlayerToNode(guildId, targetNode, reason)
|
|
700
618
|
}
|
|
701
619
|
|
|
702
620
|
_capturePlayerState(player) {
|
|
703
|
-
|
|
704
|
-
let position = player.position || 0
|
|
705
|
-
if (player.playing && !player.paused && player.timestamp) {
|
|
706
|
-
const elapsed = Date.now() - player.timestamp
|
|
707
|
-
position = Math.min(
|
|
708
|
-
position + elapsed,
|
|
709
|
-
player.current?.info?.length || position + elapsed
|
|
710
|
-
)
|
|
711
|
-
}
|
|
712
|
-
return {
|
|
713
|
-
guildId: player.guildId,
|
|
714
|
-
textChannel: player.textChannel,
|
|
715
|
-
voiceChannel: player.voiceChannel,
|
|
716
|
-
volume: player.volume ?? 100,
|
|
717
|
-
paused: !!player.paused,
|
|
718
|
-
position,
|
|
719
|
-
current: player.current || null,
|
|
720
|
-
queue: player.queue?.toArray?.() || EMPTY_ARRAY,
|
|
721
|
-
loop: player.loop,
|
|
722
|
-
shuffle: player.shuffle,
|
|
723
|
-
deaf: player.deaf ?? false,
|
|
724
|
-
connected: !!player.connected
|
|
725
|
-
}
|
|
621
|
+
return this._recovery.capturePlayerState(player)
|
|
726
622
|
}
|
|
727
623
|
|
|
728
624
|
_createPlayerOnNode(targetNode, state) {
|
|
729
|
-
return this.
|
|
730
|
-
guildId: state.guildId,
|
|
731
|
-
textChannel: state.textChannel,
|
|
732
|
-
voiceChannel: state.voiceChannel,
|
|
733
|
-
defaultVolume: state.volume || 100,
|
|
734
|
-
deaf: state.deaf || false
|
|
735
|
-
})
|
|
625
|
+
return this._recovery.createPlayerOnNode(targetNode, state)
|
|
736
626
|
}
|
|
737
627
|
|
|
738
628
|
_seekAfterTrackStart(player, guildId, position, delay = 50) {
|
|
739
|
-
|
|
740
|
-
const seekOnce = (p) => {
|
|
741
|
-
if (p.guildId !== guildId) return
|
|
742
|
-
_functions.unrefTimeout(() => player.seek?.(position), delay)
|
|
743
|
-
}
|
|
744
|
-
this.once(AqualinkEvents.TrackStart, seekOnce)
|
|
745
|
-
player.once('destroy', () => this.off(AqualinkEvents.TrackStart, seekOnce))
|
|
629
|
+
return this._recovery.seekAfterTrackStart(player, guildId, position, delay)
|
|
746
630
|
}
|
|
747
631
|
|
|
748
632
|
async _restorePlayerState(newPlayer, state) {
|
|
749
|
-
|
|
750
|
-
if (typeof state.volume === 'number') {
|
|
751
|
-
if (typeof newPlayer.setVolume === 'function')
|
|
752
|
-
ops.push(newPlayer.setVolume(state.volume))
|
|
753
|
-
else newPlayer.volume = state.volume
|
|
754
|
-
}
|
|
755
|
-
if (state.queue?.length && newPlayer.queue?.add)
|
|
756
|
-
newPlayer.queue.add(...state.queue)
|
|
757
|
-
if (state.current && this.failoverOptions.preservePosition) {
|
|
758
|
-
if (this.failoverOptions.resumePlayback) {
|
|
759
|
-
ops.push(newPlayer.play(state.current))
|
|
760
|
-
this._seekAfterTrackStart(
|
|
761
|
-
newPlayer,
|
|
762
|
-
newPlayer.guildId,
|
|
763
|
-
state.position,
|
|
764
|
-
50
|
|
765
|
-
)
|
|
766
|
-
if (state.paused) ops.push(newPlayer.pause(true))
|
|
767
|
-
} else if (newPlayer.queue?.add) {
|
|
768
|
-
newPlayer.queue.add(state.current)
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
newPlayer.loop = state.loop
|
|
772
|
-
newPlayer.shuffle = state.shuffle
|
|
773
|
-
await Promise.allSettled(ops)
|
|
633
|
+
return this._recovery.restorePlayerState(newPlayer, state)
|
|
774
634
|
}
|
|
775
635
|
|
|
776
636
|
updateVoiceState({ d, t }) {
|
|
@@ -781,13 +641,15 @@ class Aqua extends EventEmitter {
|
|
|
781
641
|
return
|
|
782
642
|
const player = this.players.get(String(d.guild_id))
|
|
783
643
|
if (!player) return
|
|
784
|
-
this.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
644
|
+
if (this.debugTrace) {
|
|
645
|
+
this._trace('voice.gateway', {
|
|
646
|
+
guildId: String(d.guild_id),
|
|
647
|
+
type: t,
|
|
648
|
+
hasSessionId: !!d.session_id,
|
|
649
|
+
hasEndpoint: !!d.endpoint,
|
|
650
|
+
hasChannelId: d.channel_id !== undefined
|
|
651
|
+
})
|
|
652
|
+
}
|
|
791
653
|
|
|
792
654
|
d.txId = player.txId
|
|
793
655
|
if (t === 'VOICE_STATE_UPDATE') {
|
|
@@ -833,11 +695,12 @@ class Aqua extends EventEmitter {
|
|
|
833
695
|
? this.fetchRegion(options.region)
|
|
834
696
|
: this.leastUsedNodes
|
|
835
697
|
if (!candidates.length) throw new Error('No nodes available')
|
|
836
|
-
return this.createPlayer(
|
|
698
|
+
return this.createPlayer(candidates[0], options)
|
|
837
699
|
}
|
|
838
700
|
|
|
839
701
|
createPlayer(node, options) {
|
|
840
|
-
const
|
|
702
|
+
const guildId = String(options.guildId)
|
|
703
|
+
const existing = this.players.get(guildId)
|
|
841
704
|
if (existing) {
|
|
842
705
|
_functions.safeCall(() =>
|
|
843
706
|
existing.destroy({
|
|
@@ -848,15 +711,16 @@ class Aqua extends EventEmitter {
|
|
|
848
711
|
)
|
|
849
712
|
}
|
|
850
713
|
const player = new Player(this, node, options)
|
|
851
|
-
const guildId = String(options.guildId)
|
|
852
714
|
this.players.set(guildId, player)
|
|
853
|
-
this.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
715
|
+
if (this.debugTrace) {
|
|
716
|
+
this._trace('player.create', {
|
|
717
|
+
guildId,
|
|
718
|
+
node: node?.name || node?.host,
|
|
719
|
+
voiceChannel: options.voiceChannel,
|
|
720
|
+
textChannel: options.textChannel,
|
|
721
|
+
resuming: !!options.resuming
|
|
722
|
+
})
|
|
723
|
+
}
|
|
860
724
|
node?.players?.add?.(player)
|
|
861
725
|
player.once('destroy', () => this._handlePlayerDestroy(player))
|
|
862
726
|
player.connect(options)
|
|
@@ -868,10 +732,12 @@ class Aqua extends EventEmitter {
|
|
|
868
732
|
player.nodes?.players?.delete?.(player)
|
|
869
733
|
const guildId = String(player.guildId)
|
|
870
734
|
if (this.players.get(guildId) === player) this.players.delete(guildId)
|
|
871
|
-
this.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
735
|
+
if (this.debugTrace) {
|
|
736
|
+
this._trace('player.destroyed', {
|
|
737
|
+
guildId,
|
|
738
|
+
node: player?.nodes?.name || player?.nodes?.host
|
|
739
|
+
})
|
|
740
|
+
}
|
|
875
741
|
this.emit(AqualinkEvents.PlayerDestroyed, player)
|
|
876
742
|
}
|
|
877
743
|
|
|
@@ -1079,194 +945,23 @@ class Aqua extends EventEmitter {
|
|
|
1079
945
|
}
|
|
1080
946
|
|
|
1081
947
|
async loadPlayers(filePath = './AquaPlayers.jsonl') {
|
|
1082
|
-
|
|
1083
|
-
this._loading = true
|
|
1084
|
-
const lockFile = `${filePath}.lock`
|
|
1085
|
-
let stream = null,
|
|
1086
|
-
rl = null
|
|
1087
|
-
try {
|
|
1088
|
-
await fs.promises.access(filePath)
|
|
1089
|
-
await fs.promises.writeFile(lockFile, String(process.pid), { flag: 'wx' })
|
|
1090
|
-
await this._waitForFirstNode()
|
|
1091
|
-
|
|
1092
|
-
stream = fs.createReadStream(filePath, { encoding: 'utf8' })
|
|
1093
|
-
rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
|
|
1094
|
-
|
|
1095
|
-
const batch = []
|
|
1096
|
-
for await (const line of rl) {
|
|
1097
|
-
if (!line.trim()) continue
|
|
1098
|
-
try {
|
|
1099
|
-
const parsed = JSON.parse(line)
|
|
1100
|
-
if (parsed.type === 'node_sessions') continue
|
|
1101
|
-
batch.push(parsed)
|
|
1102
|
-
} catch {
|
|
1103
|
-
continue
|
|
1104
|
-
}
|
|
1105
|
-
if (batch.length >= PLAYER_BATCH_SIZE) {
|
|
1106
|
-
await Promise.allSettled(batch.map((p) => this._restorePlayer(p)))
|
|
1107
|
-
batch.length = 0
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
if (batch.length)
|
|
1111
|
-
await Promise.allSettled(batch.map((p) => this._restorePlayer(p)))
|
|
1112
|
-
await fs.promises.writeFile(filePath, '')
|
|
1113
|
-
} catch (err) {
|
|
1114
|
-
if (err.code !== 'ENOENT') {
|
|
1115
|
-
console.error(`[Aqua/Autoresume]Error loading players:`, err)
|
|
1116
|
-
this.emit(AqualinkEvents.Error, null, err)
|
|
1117
|
-
}
|
|
1118
|
-
} finally {
|
|
1119
|
-
this._loading = false
|
|
1120
|
-
if (rl) _functions.safeCall(() => rl.close())
|
|
1121
|
-
if (stream) _functions.safeCall(() => stream.destroy())
|
|
1122
|
-
await fs.promises.unlink(lockFile).catch(_functions.noop)
|
|
1123
|
-
}
|
|
948
|
+
return this._recovery.loadPlayers(filePath)
|
|
1124
949
|
}
|
|
1125
950
|
|
|
1126
951
|
async _restorePlayer(p) {
|
|
1127
|
-
|
|
1128
|
-
const gId = String(p.g)
|
|
1129
|
-
const existing = this.players.get(gId)
|
|
1130
|
-
if (existing?.playing) return
|
|
1131
|
-
|
|
1132
|
-
const player =
|
|
1133
|
-
existing ||
|
|
1134
|
-
this.createPlayer(this._chooseLeastBusyNode(this.leastUsedNodes), {
|
|
1135
|
-
guildId: gId,
|
|
1136
|
-
textChannel: p.t,
|
|
1137
|
-
voiceChannel: p.v,
|
|
1138
|
-
defaultVolume: p.vol || 65,
|
|
1139
|
-
deaf: true,
|
|
1140
|
-
resuming: !!p.resuming
|
|
1141
|
-
})
|
|
1142
|
-
player._resuming = !!p.resuming
|
|
1143
|
-
const requester = _functions.parseRequester(p.r)
|
|
1144
|
-
const tracksToResolve = [p.u, ...(p.q || [])]
|
|
1145
|
-
.filter(Boolean)
|
|
1146
|
-
.slice(0, this.maxTracksRestore)
|
|
1147
|
-
const resolved = await Promise.all(
|
|
1148
|
-
tracksToResolve.map((uri) =>
|
|
1149
|
-
this.resolve({ query: uri, requester }).catch(() => null)
|
|
1150
|
-
)
|
|
1151
|
-
)
|
|
1152
|
-
const validTracks = resolved.flatMap((r) => r?.tracks || [])
|
|
1153
|
-
if (validTracks.length && player.queue?.add) {
|
|
1154
|
-
player.queue.add(...validTracks)
|
|
1155
|
-
}
|
|
1156
|
-
if (p.u && validTracks[0]) {
|
|
1157
|
-
if (p.vol != null) {
|
|
1158
|
-
if (typeof player.setVolume === 'function')
|
|
1159
|
-
await player.setVolume(p.vol)
|
|
1160
|
-
else player.volume = p.vol
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
this._seekAfterTrackStart(player, gId, p.p, 100)
|
|
1164
|
-
|
|
1165
|
-
await player.play(undefined, { startTime: p.p, paused: p.pa })
|
|
1166
|
-
}
|
|
1167
|
-
if (p.nw && p.t) {
|
|
1168
|
-
const channel = this.client.channels?.cache?.get?.(p.t)
|
|
1169
|
-
if (channel?.messages?.fetch) {
|
|
1170
|
-
player.nowPlayingMessage = await channel.messages
|
|
1171
|
-
.fetch(p.nw)
|
|
1172
|
-
.catch(() => null)
|
|
1173
|
-
} else if (this.client.messages?.fetch) {
|
|
1174
|
-
player.nowPlayingMessage = await this.client.messages
|
|
1175
|
-
.fetch(p.nw, p.t)
|
|
1176
|
-
.catch(() => null)
|
|
1177
|
-
}
|
|
1178
|
-
this._trace('player.nowPlaying.restore', {
|
|
1179
|
-
guildId: gId,
|
|
1180
|
-
messageId: p.nw,
|
|
1181
|
-
restored: !!player.nowPlayingMessage
|
|
1182
|
-
})
|
|
1183
|
-
}
|
|
1184
|
-
} catch (e) {
|
|
1185
|
-
console.error(
|
|
1186
|
-
`[Aqua/Autoresume]Failed to restore player for guild: ${p.g}`,
|
|
1187
|
-
e
|
|
1188
|
-
)
|
|
1189
|
-
}
|
|
952
|
+
return this._recovery.restorePlayer(p)
|
|
1190
953
|
}
|
|
1191
954
|
|
|
1192
955
|
async _waitForFirstNode(timeout = NODE_TIMEOUT) {
|
|
1193
|
-
|
|
1194
|
-
return new Promise((resolve, reject) => {
|
|
1195
|
-
let resolved = false
|
|
1196
|
-
const cleanup = () => {
|
|
1197
|
-
if (resolved) return
|
|
1198
|
-
resolved = true
|
|
1199
|
-
clearTimeout(timer)
|
|
1200
|
-
this.off(AqualinkEvents.NodeConnect, onReady)
|
|
1201
|
-
this.off(AqualinkEvents.NodeCreate, onReady)
|
|
1202
|
-
}
|
|
1203
|
-
const onReady = () => {
|
|
1204
|
-
if (this.leastUsedNodes.length) {
|
|
1205
|
-
cleanup()
|
|
1206
|
-
resolve()
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
const timer = setTimeout(() => {
|
|
1210
|
-
cleanup()
|
|
1211
|
-
reject(new Error('Timeout waiting for first node'))
|
|
1212
|
-
}, timeout)
|
|
1213
|
-
timer.unref?.()
|
|
1214
|
-
this.on(AqualinkEvents.NodeConnect, onReady)
|
|
1215
|
-
this.on(AqualinkEvents.NodeCreate, onReady)
|
|
1216
|
-
onReady()
|
|
1217
|
-
})
|
|
956
|
+
return this._recovery.waitForFirstNode(timeout)
|
|
1218
957
|
}
|
|
1219
958
|
|
|
1220
959
|
_performCleanup() {
|
|
1221
|
-
|
|
1222
|
-
for (const [guildId, state] of this._brokenPlayers) {
|
|
1223
|
-
if (now - state.brokenAt > BROKEN_PLAYER_TTL)
|
|
1224
|
-
this._brokenPlayers.delete(guildId)
|
|
1225
|
-
}
|
|
1226
|
-
for (const [id, ts] of this._lastFailoverAttempt) {
|
|
1227
|
-
if (now - ts > FAILOVER_CLEANUP_TTL) {
|
|
1228
|
-
this._lastFailoverAttempt.delete(id)
|
|
1229
|
-
this._failoverQueue.delete(id)
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
if (this._failoverQueue.size > MAX_FAILOVER_QUEUE)
|
|
1233
|
-
this._failoverQueue.clear()
|
|
1234
|
-
if (this._rebuildLocks.size > MAX_REBUILD_LOCKS) this._rebuildLocks.clear()
|
|
1235
|
-
for (const id of this._nodeStates.keys()) {
|
|
1236
|
-
if (!this.nodeMap.has(id)) this._nodeStates.delete(id)
|
|
1237
|
-
}
|
|
960
|
+
return this._recovery.performCleanup()
|
|
1238
961
|
}
|
|
1239
962
|
|
|
1240
963
|
async _loadNodeSessions(filePath = './AquaPlayers.jsonl') {
|
|
1241
|
-
|
|
1242
|
-
rl = null
|
|
1243
|
-
try {
|
|
1244
|
-
await fs.promises.access(filePath)
|
|
1245
|
-
stream = fs.createReadStream(filePath, { encoding: 'utf8' })
|
|
1246
|
-
rl = readline.createInterface({ input: stream, crlfDelay: Infinity })
|
|
1247
|
-
|
|
1248
|
-
for await (const line of rl) {
|
|
1249
|
-
if (!line.trim()) continue
|
|
1250
|
-
try {
|
|
1251
|
-
const parsed = JSON.parse(line)
|
|
1252
|
-
if (parsed.type === 'node_sessions') {
|
|
1253
|
-
for (const [name, sessionId] of Object.entries(parsed.data)) {
|
|
1254
|
-
const nodeOptions = this.nodes.find(
|
|
1255
|
-
(n) => (n.name || n.host) === name
|
|
1256
|
-
)
|
|
1257
|
-
if (nodeOptions) {
|
|
1258
|
-
nodeOptions.sessionId = sessionId
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
break
|
|
1262
|
-
}
|
|
1263
|
-
} catch {}
|
|
1264
|
-
}
|
|
1265
|
-
} catch {
|
|
1266
|
-
} finally {
|
|
1267
|
-
if (rl) _functions.safeCall(() => rl.close())
|
|
1268
|
-
if (stream) _functions.safeCall(() => stream.destroy())
|
|
1269
|
-
}
|
|
964
|
+
return this._recovery.loadNodeSessions(filePath)
|
|
1270
965
|
}
|
|
1271
966
|
}
|
|
1272
967
|
|