aqualink 2.20.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -184
- package/build/handlers/autoplay.js +5 -1
- package/build/structures/Aqua.js +226 -539
- package/build/structures/AquaRecovery.js +901 -0
- package/build/structures/Connection.js +72 -261
- package/build/structures/ConnectionRecovery.js +398 -0
- package/build/structures/Filters.js +93 -12
- package/build/structures/Node.js +93 -54
- package/build/structures/Player.js +284 -337
- package/build/structures/PlayerLifecycle.js +575 -0
- package/build/structures/PlayerLifecycleState.js +42 -0
- package/build/structures/Reporting.js +32 -0
- package/build/structures/Rest.js +25 -2
- package/build/structures/Track.js +2 -2
- package/package.json +1 -1
package/build/structures/Aqua.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
const fs = require('node:fs')
|
|
2
|
-
const
|
|
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,41 @@ 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 (!this._traceBuffer || this._traceBuffer.length !== this.traceMaxEntries) {
|
|
195
|
+
this._traceBuffer = new Array(this.traceMaxEntries)
|
|
196
|
+
this._traceBufferCount = 0
|
|
197
|
+
this._traceBufferIndex = 0
|
|
198
|
+
}
|
|
199
|
+
const resolvedData = typeof data === 'function' ? data() : data
|
|
162
200
|
const entry = {
|
|
163
201
|
seq: ++this._traceSeq,
|
|
164
202
|
at: Date.now(),
|
|
165
203
|
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
|
|
204
|
+
data: resolvedData
|
|
172
205
|
}
|
|
173
206
|
this._traceBuffer[this._traceBufferIndex] = entry
|
|
174
207
|
this._traceBufferIndex = (this._traceBufferIndex + 1) % this.traceMaxEntries
|
|
@@ -181,6 +214,7 @@ class Aqua extends EventEmitter {
|
|
|
181
214
|
|
|
182
215
|
getTrace(limit = 300) {
|
|
183
216
|
const max = Math.max(1, Number(limit) || 300)
|
|
217
|
+
if (!this._traceBuffer) return []
|
|
184
218
|
const count = Math.min(max, this._traceBufferCount)
|
|
185
219
|
if (!count) return []
|
|
186
220
|
const out = new Array(count)
|
|
@@ -195,7 +229,7 @@ class Aqua extends EventEmitter {
|
|
|
195
229
|
}
|
|
196
230
|
|
|
197
231
|
clearTrace() {
|
|
198
|
-
this._traceBuffer.fill(undefined)
|
|
232
|
+
if (this._traceBuffer) this._traceBuffer.fill(undefined)
|
|
199
233
|
this._traceBufferCount = 0
|
|
200
234
|
this._traceBufferIndex = 0
|
|
201
235
|
}
|
|
@@ -214,27 +248,119 @@ class Aqua extends EventEmitter {
|
|
|
214
248
|
}
|
|
215
249
|
}
|
|
216
250
|
|
|
251
|
+
queueVoiceStateUpdate(data) {
|
|
252
|
+
const guildId = data?.guild_id ? String(data.guild_id) : null
|
|
253
|
+
if (!guildId) return false
|
|
254
|
+
|
|
255
|
+
this._voiceStatePending.set(guildId, data)
|
|
256
|
+
if (!this._voiceStateQueued.has(guildId)) {
|
|
257
|
+
this._voiceStateQueued.add(guildId)
|
|
258
|
+
this._voiceStateQueue.push(guildId)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this.debugTrace) {
|
|
262
|
+
this._trace('voice.queue.enqueue', {
|
|
263
|
+
guildId,
|
|
264
|
+
size: this._voiceStateQueued.size
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
this._scheduleVoiceStateFlush()
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_scheduleVoiceStateFlush(delay = 0) {
|
|
272
|
+
if (this._voiceStateFlushTimer) return
|
|
273
|
+
this._voiceStateFlushTimer = setTimeout(
|
|
274
|
+
() => {
|
|
275
|
+
this._voiceStateFlushTimer = null
|
|
276
|
+
this._flushVoiceStateQueue()
|
|
277
|
+
},
|
|
278
|
+
Math.max(0, delay)
|
|
279
|
+
)
|
|
280
|
+
this._voiceStateFlushTimer.unref?.()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_flushVoiceStateQueue() {
|
|
284
|
+
if (!this._voiceStateQueued.size) return
|
|
285
|
+
|
|
286
|
+
const now = Date.now()
|
|
287
|
+
const waitFor =
|
|
288
|
+
VOICE_STATE_QUEUE_INTERVAL - (now - this._lastVoiceStateSendAt)
|
|
289
|
+
if (waitFor > 0) {
|
|
290
|
+
this._scheduleVoiceStateFlush(waitFor)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let guildId = null
|
|
295
|
+
while (this._voiceStateQueueHead < this._voiceStateQueue.length) {
|
|
296
|
+
const candidate = this._voiceStateQueue[this._voiceStateQueueHead]
|
|
297
|
+
this._voiceStateQueue[this._voiceStateQueueHead] = undefined
|
|
298
|
+
this._voiceStateQueueHead++
|
|
299
|
+
if (candidate && this._voiceStateQueued.has(candidate)) {
|
|
300
|
+
guildId = candidate
|
|
301
|
+
this._voiceStateQueued.delete(candidate)
|
|
302
|
+
break
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
this._voiceStateQueueHead > 1024 ||
|
|
308
|
+
this._voiceStateQueueHead > this._voiceStateQueue.length / 2
|
|
309
|
+
) {
|
|
310
|
+
this._voiceStateQueue = this._voiceStateQueue.slice(
|
|
311
|
+
this._voiceStateQueueHead
|
|
312
|
+
)
|
|
313
|
+
this._voiceStateQueueHead = 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const data = guildId ? this._voiceStatePending.get(guildId) : null
|
|
317
|
+
if (guildId) this._voiceStatePending.delete(guildId)
|
|
318
|
+
|
|
319
|
+
if (data) {
|
|
320
|
+
this._lastVoiceStateSendAt = now
|
|
321
|
+
if (this.debugTrace) {
|
|
322
|
+
this._trace('voice.queue.send', {
|
|
323
|
+
guildId,
|
|
324
|
+
remaining: this._voiceStateQueued.size
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
_functions.safeCall(() => this.send({ op: 4, d: data }))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (this._voiceStateQueued.size) {
|
|
331
|
+
this._scheduleVoiceStateFlush(VOICE_STATE_QUEUE_INTERVAL)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
217
335
|
_bindEventHandlers() {
|
|
218
336
|
this._eventHandlers = {
|
|
219
337
|
onNodeConnect: (node) => {
|
|
220
|
-
this.
|
|
338
|
+
if (this.debugTrace)
|
|
339
|
+
this._trace('node.connect', { node: node?.name || node?.host })
|
|
221
340
|
this._invalidateCache()
|
|
222
341
|
this._performCleanup()
|
|
223
342
|
},
|
|
224
343
|
onNodeDisconnect: (node) => {
|
|
225
|
-
this.
|
|
344
|
+
if (this.debugTrace)
|
|
345
|
+
this._trace('node.disconnect', { node: node?.name || node?.host })
|
|
226
346
|
this._invalidateCache()
|
|
227
347
|
queueMicrotask(() => {
|
|
228
|
-
this._storeBrokenPlayers(node)
|
|
348
|
+
this._storeBrokenPlayers(node).catch((error) =>
|
|
349
|
+
reportSuppressedError(this, 'aqua.nodeDisconnect.storeBrokenPlayers', error, {
|
|
350
|
+
node: node?.name || node?.host
|
|
351
|
+
})
|
|
352
|
+
)
|
|
229
353
|
this._performCleanup()
|
|
230
354
|
})
|
|
231
355
|
},
|
|
232
356
|
onNodeReady: (node, { resumed }) => {
|
|
233
|
-
this.
|
|
234
|
-
node
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
357
|
+
if (this.debugTrace) {
|
|
358
|
+
this._trace('node.ready', {
|
|
359
|
+
node: node?.name || node?.host,
|
|
360
|
+
resumed: !!resumed,
|
|
361
|
+
players: this.players.size
|
|
362
|
+
})
|
|
363
|
+
}
|
|
238
364
|
if (resumed) {
|
|
239
365
|
const batch = []
|
|
240
366
|
for (const player of this.players.values()) {
|
|
@@ -249,7 +375,16 @@ class Aqua extends EventEmitter {
|
|
|
249
375
|
return
|
|
250
376
|
}
|
|
251
377
|
queueMicrotask(() => {
|
|
252
|
-
this._rebuildBrokenPlayers(node).catch(
|
|
378
|
+
this._rebuildBrokenPlayers(node).catch((error) =>
|
|
379
|
+
reportSuppressedError(
|
|
380
|
+
this,
|
|
381
|
+
'aqua.nodeReady.rebuildBrokenPlayers',
|
|
382
|
+
error,
|
|
383
|
+
{
|
|
384
|
+
node: node?.name || node?.host
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
)
|
|
253
388
|
})
|
|
254
389
|
}
|
|
255
390
|
}
|
|
@@ -269,19 +404,28 @@ class Aqua extends EventEmitter {
|
|
|
269
404
|
this._eventHandlers = null
|
|
270
405
|
}
|
|
271
406
|
this.removeAllListeners()
|
|
407
|
+
if (this._voiceStateFlushTimer) {
|
|
408
|
+
clearTimeout(this._voiceStateFlushTimer)
|
|
409
|
+
this._voiceStateFlushTimer = null
|
|
410
|
+
}
|
|
411
|
+
this._voiceStateQueue.length = 0
|
|
412
|
+
this._voiceStateQueueHead = 0
|
|
413
|
+
this._voiceStateQueued.clear()
|
|
414
|
+
this._voiceStatePending.clear()
|
|
272
415
|
|
|
273
416
|
for (const id of Array.from(this.nodeMap.keys())) this._destroyNode(id)
|
|
274
417
|
for (const player of Array.from(this.players.values()))
|
|
275
418
|
_functions.safeCall(() => player.destroy())
|
|
276
419
|
|
|
277
420
|
this.players.clear()
|
|
278
|
-
this.
|
|
279
|
-
this.
|
|
280
|
-
this._lastFailoverAttempt.clear()
|
|
421
|
+
this._failoverState = Object.create(null)
|
|
422
|
+
this._guildLifecycleLocks.clear()
|
|
281
423
|
this._brokenPlayers.clear()
|
|
282
424
|
this._rebuildLocks.clear()
|
|
283
425
|
this._nodeLoadCache.clear()
|
|
284
426
|
this._invalidateCache()
|
|
427
|
+
_functions.safeCall(() => this._recovery?.dispose?.())
|
|
428
|
+
this._recovery = null
|
|
285
429
|
}
|
|
286
430
|
|
|
287
431
|
get leastUsedNodes() {
|
|
@@ -361,7 +505,9 @@ class Aqua extends EventEmitter {
|
|
|
361
505
|
|
|
362
506
|
if (this.initiated) return this
|
|
363
507
|
if (!this.clientId) return this
|
|
364
|
-
await this._loadNodeSessions().catch(() =>
|
|
508
|
+
await this._loadNodeSessions().catch((error) =>
|
|
509
|
+
reportSuppressedError(this, 'aqua.init.loadNodeSessions', error)
|
|
510
|
+
)
|
|
365
511
|
const results = await Promise.allSettled(
|
|
366
512
|
this.nodes.map((n) =>
|
|
367
513
|
Promise.race([
|
|
@@ -389,10 +535,16 @@ class Aqua extends EventEmitter {
|
|
|
389
535
|
const node = new Node(this, options, this.options)
|
|
390
536
|
node.players = new Set()
|
|
391
537
|
this.nodeMap.set(id, node)
|
|
392
|
-
this.
|
|
538
|
+
this._failoverState[id] = {
|
|
539
|
+
connected: false,
|
|
540
|
+
failoverInProgress: false,
|
|
541
|
+
attempts: 0,
|
|
542
|
+
lastAttempt: 0
|
|
543
|
+
}
|
|
393
544
|
try {
|
|
394
545
|
await node.connect()
|
|
395
|
-
this.
|
|
546
|
+
this._failoverState[id].connected = true
|
|
547
|
+
this._failoverState[id].failoverInProgress = false
|
|
396
548
|
this._invalidateCache()
|
|
397
549
|
this.emit(AqualinkEvents.NodeCreate, node)
|
|
398
550
|
return node
|
|
@@ -416,361 +568,61 @@ class Aqua extends EventEmitter {
|
|
|
416
568
|
_functions.safeCall(() => node.players.clear())
|
|
417
569
|
this.nodeMap.delete(id)
|
|
418
570
|
}
|
|
419
|
-
this.
|
|
420
|
-
this._failoverQueue.delete(id)
|
|
421
|
-
this._lastFailoverAttempt.delete(id)
|
|
571
|
+
delete this._failoverState[id]
|
|
422
572
|
this._nodeLoadCache.delete(id)
|
|
423
573
|
this._invalidateCache()
|
|
424
574
|
}
|
|
425
575
|
|
|
426
576
|
_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
|
-
}
|
|
577
|
+
return this._recovery.storeBrokenPlayers(node)
|
|
438
578
|
}
|
|
439
579
|
|
|
440
580
|
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()
|
|
581
|
+
return this._recovery.rebuildBrokenPlayers(node)
|
|
469
582
|
}
|
|
470
583
|
|
|
471
584
|
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
|
-
}
|
|
585
|
+
return this._recovery.rebuildPlayer(state, targetNode)
|
|
505
586
|
}
|
|
506
587
|
|
|
507
588
|
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
|
-
}
|
|
589
|
+
return this._recovery.handleNodeFailover(failedNode)
|
|
548
590
|
}
|
|
549
591
|
|
|
550
592
|
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
|
|
593
|
+
return this._recovery.migratePlayersOptimized(players, nodes)
|
|
580
594
|
}
|
|
581
595
|
|
|
582
596
|
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
|
-
}
|
|
597
|
+
return this._recovery.migratePlayer(player, pickNode)
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
_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
|
|
601
|
+
return this._recovery.regionMatches(configuredRegion, extractedRegion)
|
|
606
602
|
}
|
|
607
603
|
|
|
608
604
|
_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)
|
|
605
|
+
return this._recovery.findBestNodeForRegion(region)
|
|
620
606
|
}
|
|
621
607
|
|
|
622
608
|
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
|
|
609
|
+
return this._recovery.movePlayerToNode(guildId, targetNode, reason)
|
|
700
610
|
}
|
|
701
611
|
|
|
702
612
|
_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
|
-
}
|
|
613
|
+
return this._recovery.capturePlayerState(player)
|
|
726
614
|
}
|
|
727
615
|
|
|
728
616
|
_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
|
-
})
|
|
617
|
+
return this._recovery.createPlayerOnNode(targetNode, state)
|
|
736
618
|
}
|
|
737
619
|
|
|
738
620
|
_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))
|
|
621
|
+
return this._recovery.seekAfterTrackStart(player, guildId, position, delay)
|
|
746
622
|
}
|
|
747
623
|
|
|
748
624
|
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)
|
|
625
|
+
return this._recovery.restorePlayerState(newPlayer, state)
|
|
774
626
|
}
|
|
775
627
|
|
|
776
628
|
updateVoiceState({ d, t }) {
|
|
@@ -781,13 +633,15 @@ class Aqua extends EventEmitter {
|
|
|
781
633
|
return
|
|
782
634
|
const player = this.players.get(String(d.guild_id))
|
|
783
635
|
if (!player) return
|
|
784
|
-
this.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
636
|
+
if (this.debugTrace) {
|
|
637
|
+
this._trace('voice.gateway', {
|
|
638
|
+
guildId: String(d.guild_id),
|
|
639
|
+
type: t,
|
|
640
|
+
hasSessionId: !!d.session_id,
|
|
641
|
+
hasEndpoint: !!d.endpoint,
|
|
642
|
+
hasChannelId: d.channel_id !== undefined
|
|
643
|
+
})
|
|
644
|
+
}
|
|
791
645
|
|
|
792
646
|
d.txId = player.txId
|
|
793
647
|
if (t === 'VOICE_STATE_UPDATE') {
|
|
@@ -833,11 +687,12 @@ class Aqua extends EventEmitter {
|
|
|
833
687
|
? this.fetchRegion(options.region)
|
|
834
688
|
: this.leastUsedNodes
|
|
835
689
|
if (!candidates.length) throw new Error('No nodes available')
|
|
836
|
-
return this.createPlayer(
|
|
690
|
+
return this.createPlayer(candidates[0], options)
|
|
837
691
|
}
|
|
838
692
|
|
|
839
693
|
createPlayer(node, options) {
|
|
840
|
-
const
|
|
694
|
+
const guildId = String(options.guildId)
|
|
695
|
+
const existing = this.players.get(guildId)
|
|
841
696
|
if (existing) {
|
|
842
697
|
_functions.safeCall(() =>
|
|
843
698
|
existing.destroy({
|
|
@@ -848,15 +703,16 @@ class Aqua extends EventEmitter {
|
|
|
848
703
|
)
|
|
849
704
|
}
|
|
850
705
|
const player = new Player(this, node, options)
|
|
851
|
-
const guildId = String(options.guildId)
|
|
852
706
|
this.players.set(guildId, player)
|
|
853
|
-
this.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
707
|
+
if (this.debugTrace) {
|
|
708
|
+
this._trace('player.create', {
|
|
709
|
+
guildId,
|
|
710
|
+
node: node?.name || node?.host,
|
|
711
|
+
voiceChannel: options.voiceChannel,
|
|
712
|
+
textChannel: options.textChannel,
|
|
713
|
+
resuming: !!options.resuming
|
|
714
|
+
})
|
|
715
|
+
}
|
|
860
716
|
node?.players?.add?.(player)
|
|
861
717
|
player.once('destroy', () => this._handlePlayerDestroy(player))
|
|
862
718
|
player.connect(options)
|
|
@@ -868,10 +724,12 @@ class Aqua extends EventEmitter {
|
|
|
868
724
|
player.nodes?.players?.delete?.(player)
|
|
869
725
|
const guildId = String(player.guildId)
|
|
870
726
|
if (this.players.get(guildId) === player) this.players.delete(guildId)
|
|
871
|
-
this.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
727
|
+
if (this.debugTrace) {
|
|
728
|
+
this._trace('player.destroyed', {
|
|
729
|
+
guildId,
|
|
730
|
+
node: player?.nodes?.name || player?.nodes?.host
|
|
731
|
+
})
|
|
732
|
+
}
|
|
875
733
|
this.emit(AqualinkEvents.PlayerDestroyed, player)
|
|
876
734
|
}
|
|
877
735
|
|
|
@@ -1079,194 +937,23 @@ class Aqua extends EventEmitter {
|
|
|
1079
937
|
}
|
|
1080
938
|
|
|
1081
939
|
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
|
-
}
|
|
940
|
+
return this._recovery.loadPlayers(filePath)
|
|
1124
941
|
}
|
|
1125
942
|
|
|
1126
943
|
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
|
-
}
|
|
944
|
+
return this._recovery.restorePlayer(p)
|
|
1190
945
|
}
|
|
1191
946
|
|
|
1192
947
|
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
|
-
})
|
|
948
|
+
return this._recovery.waitForFirstNode(timeout)
|
|
1218
949
|
}
|
|
1219
950
|
|
|
1220
951
|
_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
|
-
}
|
|
952
|
+
return this._recovery.performCleanup()
|
|
1238
953
|
}
|
|
1239
954
|
|
|
1240
955
|
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
|
-
}
|
|
956
|
+
return this._recovery.loadNodeSessions(filePath)
|
|
1270
957
|
}
|
|
1271
958
|
}
|
|
1272
959
|
|