aqualink 2.18.1 → 2.19.1

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.
@@ -1,484 +1,577 @@
1
- 'use strict'
2
-
3
- const { AqualinkEvents } = require('./AqualinkEvents')
4
-
5
- const POOL_SIZE = 12
6
- const UPDATE_TIMEOUT = 4000
7
-
8
- const RECONNECT_DELAY = 1000
9
- const MAX_RECONNECT_ATTEMPTS = 3
10
- const RESUME_BACKOFF_MAX = 60000
11
-
12
- const VOICE_DATA_TIMEOUT = 90000
13
-
14
- const VOICE_FLUSH_DELAY = 50
15
-
16
- const NULL_CHANNEL_GRACE_MS = 15000
17
-
18
- const STATE = {
19
- CONNECTED: 1,
20
- UPDATE_SCHEDULED: 64,
21
- DISCONNECTING: 128,
22
- ATTEMPTING_RESUME: 256,
23
- VOICE_DATA_STALE: 512
24
- }
25
-
26
- const _functions = {
27
- safeUnref: t => (typeof t?.unref === 'function' ? t.unref() : undefined),
28
- isValidNumber: n => typeof n === 'number' && n >= 0 && Number.isFinite(n),
29
- isNetworkError: e => !!e && (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND' || e.code === 'ETIMEDOUT'),
30
- noop: () => {},
31
- extractRegion: endpoint => {
32
- if (typeof endpoint !== 'string') return 'unknown'
33
- endpoint = endpoint.trim()
34
- if (!endpoint) return 'unknown'
35
-
36
- const proto = endpoint.indexOf('://')
37
- if (proto !== -1) endpoint = endpoint.slice(proto + 3)
38
-
39
- const slash = endpoint.indexOf('/')
40
- if (slash !== -1) endpoint = endpoint.slice(0, slash)
41
-
42
- const colon = endpoint.indexOf(':')
43
- if (colon !== -1) endpoint = endpoint.slice(0, colon)
44
-
45
- const dot = endpoint.indexOf('.')
46
- const label = (dot === -1 ? endpoint : endpoint.slice(0, dot)).toLowerCase()
47
- if (!label) return 'unknown'
48
-
49
- let i = label.length - 1
50
- while (i >= 0) {
51
- const c = label.charCodeAt(i)
52
- if (c >= 48 && c <= 57) i--
53
- else break
54
- }
55
- return label.slice(0, i + 1) || 'unknown'
56
- },
57
- fillVoicePayload: (payload, guildId, conn, player, resume) => {
58
- payload.guildId = guildId
59
- const v = payload.data.voice
60
- v.token = conn.token
61
- v.endpoint = conn.endpoint
62
- v.sessionId = conn.sessionId
63
- v.channelId = player.voiceChannel
64
- v.resume = resume ? true : undefined
65
- v.sequence = resume ? conn.sequence : undefined
66
- payload.data.volume = player?.volume ?? 100
67
- return payload
68
- }
69
- }
70
-
71
- class PayloadPool {
72
- constructor() {
73
- this._pool = []
74
- this._size = 0
75
- }
76
-
77
- _create() {
78
- return {
79
- guildId: null,
80
- data: {
81
- voice: { token: null, endpoint: null, sessionId: null, resume: undefined, sequence: undefined },
82
- volume: null
83
- }
84
- }
85
- }
86
-
87
- acquire() {
88
- return this._size > 0 ? this._pool[--this._size] : this._create()
89
- }
90
-
91
- release(payload) {
92
- if (!payload || this._size >= POOL_SIZE) return
93
- payload.guildId = null
94
- const v = payload.data.voice
95
- v.token = v.endpoint = v.sessionId = null
96
- v.resume = v.sequence = undefined
97
- payload.data.volume = null
98
- this._pool[this._size++] = payload
99
- }
100
-
101
- destroy() {
102
- this._pool.length = 0
103
- this._size = 0
104
- }
105
- }
106
-
107
- const sharedPool = new PayloadPool()
108
-
109
- class Connection {
110
- constructor(player) {
111
- if (!player?.aqua?.clientId || !player.nodes?.rest) throw new TypeError('Invalid player configuration')
112
-
113
- this._player = player
114
- this._aqua = player.aqua
115
- this._rest = player.nodes.rest
116
- this._guildId = player.guildId
117
- this._clientId = player.aqua.clientId
118
-
119
- this.voiceChannel = player.voiceChannel
120
- this.sessionId = null
121
- this.channelId = null
122
- this.endpoint = null
123
- this.token = null
124
- this.region = null
125
- this.sequence = 0
126
- this.txId = 0
127
-
128
- this._lastEndpoint = null
129
- this._stateFlags = 0
130
- this._reconnectAttempts = 0
131
- this._destroyed = false
132
- this._reconnectTimer = null
133
- this._lastVoiceDataUpdate = 0
134
- this._consecutiveFailures = 0
135
-
136
- this._voiceFlushTimer = null
137
- this._pendingUpdate = null
138
- this._lastSentVoiceKey = ''
139
-
140
- this._nullChannelTimer = null
141
- this.isWaitingForDisconnect = false
142
-
143
- this._lastStateReqAt = 0
144
- this._stateGeneration = 0
145
- }
146
-
147
- _hasValidVoiceData() {
148
- if (!this.sessionId || !this.endpoint || !this.token) return false
149
- if (Date.now() - this._lastVoiceDataUpdate > VOICE_DATA_TIMEOUT) {
150
- this._stateFlags |= STATE.VOICE_DATA_STALE
151
- return false
152
- }
153
- return true
154
- }
155
-
156
- _clearNullChannelTimer() {
157
- if (!this._nullChannelTimer) return
158
- clearTimeout(this._nullChannelTimer)
159
- this._nullChannelTimer = null
160
- }
161
-
162
- _canAttemptResumeCore() {
163
- if (this._destroyed) return false
164
- if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return false
165
- if (this._stateFlags & (STATE.ATTEMPTING_RESUME | STATE.DISCONNECTING)) return false
166
- return true
167
- }
168
-
169
- _setReconnectTimer(delay) {
170
- if (this._destroyed) return
171
- this._clearReconnectTimer()
172
- this._reconnectTimer = setTimeout(() => this._handleReconnect(), delay)
173
- _functions.safeUnref(this._reconnectTimer)
174
- }
175
-
176
- setServerUpdate(data) {
177
- if (this._destroyed || !data?.token) return
178
-
179
- const endpoint = typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
180
- if (!endpoint) return
181
-
182
- if (this._lastEndpoint === endpoint && this.token === data.token) return
183
-
184
- if (data.txId && data.txId < this.txId) return
185
-
186
- this._stateGeneration++
187
-
188
- if (this._lastEndpoint !== endpoint) {
189
- this.sequence = 0
190
- this._lastEndpoint = endpoint
191
- this._reconnectAttempts = 0
192
- this._consecutiveFailures = 0
193
- }
194
-
195
- this.endpoint = endpoint
196
- this.region = _functions.extractRegion(endpoint)
197
- this.token = data.token
198
- this.channelId = data.channel_id || this.channelId || this.voiceChannel
199
- this._lastVoiceDataUpdate = Date.now()
200
- this._stateFlags &= ~STATE.VOICE_DATA_STALE
201
-
202
- if (this._player?.paused) this._player.pause(false)
203
- this._scheduleVoiceUpdate()
204
- }
205
-
206
- resendVoiceUpdate() {
207
- if (this._destroyed || !this._hasValidVoiceData()) return false
208
- this._scheduleVoiceUpdate()
209
- return true
210
- }
211
-
212
- setStateUpdate(data) {
213
- if (this._destroyed || !data || data.user_id !== this._clientId) return
214
-
215
- const { session_id: sessionId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute } = data
216
- const p = this._player
217
-
218
- if (channelId) this._clearNullChannelTimer()
219
-
220
- if (data.txId && data.txId < this.txId) return
221
-
222
- if (!channelId) {
223
- this.isWaitingForDisconnect = true
224
- if (!this._nullChannelTimer) {
225
- this._nullChannelTimer = setTimeout(() => {
226
- this._nullChannelTimer = null
227
- this._handleDisconnect()
228
- }, NULL_CHANNEL_GRACE_MS)
229
- _functions.safeUnref(this._nullChannelTimer)
230
- }
231
- return
232
- }
233
-
234
- this.isWaitingForDisconnect = false
235
-
236
- if (p && p.txId > this.txId) this.txId = p.txId
237
-
238
- let needsUpdate = false
239
-
240
- if (this.voiceChannel !== channelId) {
241
- p._reconnecting = true
242
- p._resuming = true
243
- this._aqua.emit(AqualinkEvents.PlayerMove, p, this.voiceChannel, channelId)
244
- this.voiceChannel = channelId
245
- p.voiceChannel = channelId
246
- needsUpdate = true
247
- }
248
-
249
- if (this.sessionId !== sessionId) {
250
- this.sessionId = sessionId
251
- this._lastVoiceDataUpdate = Date.now()
252
- this._stateFlags &= ~STATE.VOICE_DATA_STALE
253
- this._reconnectAttempts = 0
254
- this._consecutiveFailures = 0
255
- needsUpdate = true
256
- }
257
-
258
- p.self_deaf = p.selfDeaf = !!selfDeaf
259
- p.self_mute = p.selfMute = !!selfMute
260
- this._stateFlags |= STATE.CONNECTED
261
-
262
- if (needsUpdate) this._scheduleVoiceUpdate()
263
- }
264
-
265
- async _handleDisconnect() {
266
- if (this._destroyed) return
267
-
268
- this._stateFlags = (this._stateFlags | STATE.DISCONNECTING) & ~STATE.CONNECTED
269
- this._clearNullChannelTimer()
270
- this._clearPendingUpdate()
271
- this._clearReconnectTimer()
272
-
273
- this.voiceChannel = null
274
- this.sessionId = null
275
- this.sequence = 0
276
- this._lastVoiceDataUpdate = 0
277
- this._stateFlags |= STATE.VOICE_DATA_STALE
278
-
279
- try {
280
- if (this._aqua && this._guildId) {
281
- await this._aqua.destroyPlayer(this._guildId)
282
- }
283
- } catch (e) {
284
- this._aqua?.emit?.(AqualinkEvents.Debug, new Error(`Player destroy failed: ${e?.message || e}`))
285
- } finally {
286
- this._stateFlags &= ~STATE.DISCONNECTING
287
- }
288
- }
289
-
290
- _requestVoiceState() {
291
- try {
292
- const now = Date.now()
293
- if (now - (this._lastStateReqAt || 0) < 1500) return false
294
- this._lastStateReqAt = now
295
-
296
- if (typeof this._player?.send !== 'function' || !this._player.voiceChannel) return false
297
- this._player.send({
298
- guild_id: this._guildId,
299
- channel_id: this._player.voiceChannel,
300
- self_deaf: this._player.deaf,
301
- self_mute: this._player.mute
302
- })
303
- return true
304
- } catch {
305
- return false
306
- }
307
- }
308
-
309
- async attemptResume() {
310
- if (!this._canAttemptResumeCore()) return false
311
-
312
- const currentGen = this._stateGeneration
313
-
314
-
315
- if (!this.sessionId || !this.endpoint || !this.token || (this._stateFlags & STATE.VOICE_DATA_STALE)) {
316
- this._aqua.emit(AqualinkEvents.Debug, `Resume blocked: missing voice data for guild ${this._guildId}, requesting voice state`)
317
- this._requestVoiceState()
318
- return false
319
- }
320
-
321
- this.txId = this._player.txId || this.txId
322
- this._stateFlags |= STATE.ATTEMPTING_RESUME
323
- this._reconnectAttempts++
324
- this._aqua.emit(AqualinkEvents.Debug, `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`)
325
-
326
- const payload = sharedPool.acquire()
327
- try {
328
- _functions.fillVoicePayload(payload, this._guildId, this, this._player, true)
329
-
330
- if (this._stateGeneration !== currentGen) {
331
- this._aqua.emit(AqualinkEvents.Debug, `Resume aborted: State changed during attempt for guild ${this._guildId}`)
332
- return false
333
- }
334
-
335
-
336
- await this._sendUpdate(payload)
337
-
338
- this._reconnectAttempts = 0
339
- this._consecutiveFailures = 0
340
- if (this._player) this._player._resuming = false
341
-
342
- this._aqua.emit(AqualinkEvents.Debug, `Resume PATCH sent for guild ${this._guildId}`)
343
- return true
344
- } catch (e) {
345
- if (this._destroyed || !this._aqua) throw e
346
- this._consecutiveFailures++
347
- this._aqua.emit(AqualinkEvents.Debug, `Resume failed for guild ${this._guildId}: ${e?.message || e}`)
348
-
349
- if (this._reconnectAttempts < MAX_RECONNECT_ATTEMPTS && !this._destroyed && this._consecutiveFailures < 5) {
350
- const delay = Math.min(RECONNECT_DELAY * (1 << (this._reconnectAttempts - 1)), RESUME_BACKOFF_MAX)
351
- this._setReconnectTimer(delay)
352
- } else {
353
- this._aqua.emit(AqualinkEvents.Debug, `Max reconnect attempts/failures reached for guild ${this._guildId}`)
354
- if (this._player) this._player._resuming = false
355
- this._handleDisconnect()
356
- }
357
- return false
358
- } finally {
359
- this._stateFlags &= ~STATE.ATTEMPTING_RESUME
360
- sharedPool.release(payload)
361
- }
362
- }
363
-
364
- _handleReconnect() {
365
- this._reconnectTimer = null
366
- if (!this._destroyed) this.attemptResume()
367
- }
368
-
369
- updateSequence(seq) {
370
- if (_functions.isValidNumber(seq) && seq > this.sequence) this.sequence = seq
371
- }
372
-
373
- _clearReconnectTimer() {
374
- if (!this._reconnectTimer) return
375
- clearTimeout(this._reconnectTimer)
376
- this._reconnectTimer = null
377
- }
378
-
379
- _clearPendingUpdate() {
380
- this._stateFlags &= ~STATE.UPDATE_SCHEDULED
381
- if (this._pendingUpdate?.payload) sharedPool.release(this._pendingUpdate.payload)
382
- this._pendingUpdate = null
383
- if (this._voiceFlushTimer) {
384
- clearTimeout(this._voiceFlushTimer)
385
- this._voiceFlushTimer = null
386
- }
387
- }
388
-
389
- _makeVoiceKey() {
390
- const p = this._player
391
- const vol = p?.volume ?? 100
392
- return (this.sessionId || '') + '|' +
393
- (this.token || '') + '|' +
394
- (this.endpoint || '') + '|' +
395
- (p?.voiceChannel || '') + '|' +
396
- vol
397
- }
398
-
399
- _scheduleVoiceUpdate() {
400
- if (this._destroyed) return
401
- if (!this._hasValidVoiceData()) return
402
-
403
- if (!this._pendingUpdate) {
404
- const payload = sharedPool.acquire()
405
- _functions.fillVoicePayload(payload, this._guildId, this, this._player, false)
406
- this._pendingUpdate = { payload, timestamp: Date.now() }
407
- } else {
408
- this._pendingUpdate.timestamp = Date.now()
409
- _functions.fillVoicePayload(this._pendingUpdate.payload, this._guildId, this, this._player, false)
410
- }
411
-
412
- if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
413
- this._stateFlags |= STATE.UPDATE_SCHEDULED
414
-
415
- this._voiceFlushTimer = setTimeout(() => this._executeVoiceUpdate(), VOICE_FLUSH_DELAY)
416
- _functions.safeUnref(this._voiceFlushTimer)
417
- }
418
-
419
- _executeVoiceUpdate() {
420
- if (this._destroyed) return
421
- this._stateFlags &= ~STATE.UPDATE_SCHEDULED
422
- this._voiceFlushTimer = null
423
-
424
- const pending = this._pendingUpdate
425
- this._pendingUpdate = null
426
-
427
- if (!pending) return
428
- if (Date.now() - pending.timestamp > UPDATE_TIMEOUT) {
429
- sharedPool.release(pending.payload)
430
- return
431
- }
432
-
433
- const key = this._makeVoiceKey()
434
- if (key === this._lastSentVoiceKey) {
435
- sharedPool.release(pending.payload)
436
- return
437
- }
438
- this._lastSentVoiceKey = key
439
-
440
- this._sendUpdate(pending.payload)
441
- .catch(_functions.noop)
442
- .finally(() => sharedPool.release(pending.payload))
443
- }
444
-
445
- async _sendUpdate(payload) {
446
- if (this._destroyed) throw new Error('Connection destroyed')
447
- if (!this._rest) throw new Error('REST interface unavailable')
448
-
449
- try {
450
- await this._rest.updatePlayer(payload)
451
- } catch (e) {
452
- if (e.statusCode === 404 || e.response?.statusCode === 404) {
453
- if (this._aqua) {
454
- this._aqua.emit(AqualinkEvents.Debug, `Player ${this._guildId} not found (404). Destroying.`)
455
- await this._aqua.destroyPlayer(this._guildId)
456
- }
457
- throw e
458
- }
459
- if (!_functions.isNetworkError(e)) {
460
- this._aqua.emit(AqualinkEvents.Debug, new Error(`Voice update failed: ${e?.message || e}`))
461
- }
462
- throw e
463
- }
464
- }
465
-
466
- destroy() {
467
- if (this._destroyed) return
468
- this._destroyed = true
469
-
470
- this._clearNullChannelTimer()
471
- this._clearPendingUpdate()
472
- this._clearReconnectTimer()
473
-
474
- this._player = this._aqua = this._rest = null
475
- this.voiceChannel = this.sessionId = this.endpoint = this.token = this.region = this._lastEndpoint = null
476
- this._stateFlags = 0
477
- this.sequence = 0
478
- this._reconnectAttempts = 0
479
- this._consecutiveFailures = 0
480
- this._lastVoiceDataUpdate = 0
481
- }
482
- }
483
-
1
+ const { AqualinkEvents } = require('./AqualinkEvents')
2
+
3
+ const POOL_SIZE = 12
4
+ const UPDATE_TIMEOUT = 4000
5
+
6
+ const RECONNECT_DELAY = 1000
7
+ const MAX_RECONNECT_ATTEMPTS = 3
8
+ const RESUME_BACKOFF_MAX = 60000
9
+
10
+ const VOICE_DATA_TIMEOUT = 90000
11
+
12
+ const VOICE_FLUSH_DELAY = 50
13
+
14
+ const NULL_CHANNEL_GRACE_MS = 15000
15
+
16
+ const STATE = {
17
+ CONNECTED: 1,
18
+ UPDATE_SCHEDULED: 64,
19
+ DISCONNECTING: 128,
20
+ ATTEMPTING_RESUME: 256,
21
+ VOICE_DATA_STALE: 512
22
+ }
23
+
24
+ const _functions = {
25
+ safeUnref: (t) => (typeof t?.unref === 'function' ? t.unref() : undefined),
26
+ isValidNumber: (n) => typeof n === 'number' && n >= 0 && Number.isFinite(n),
27
+ isNetworkError: (e) =>
28
+ !!e &&
29
+ (e.code === 'ECONNREFUSED' ||
30
+ e.code === 'ENOTFOUND' ||
31
+ e.code === 'ETIMEDOUT'),
32
+ noop: () => {},
33
+ extractRegion: (endpoint) => {
34
+ if (typeof endpoint !== 'string') return 'unknown'
35
+ endpoint = endpoint.trim()
36
+ if (!endpoint) return 'unknown'
37
+
38
+ const proto = endpoint.indexOf('://')
39
+ if (proto !== -1) endpoint = endpoint.slice(proto + 3)
40
+
41
+ const slash = endpoint.indexOf('/')
42
+ if (slash !== -1) endpoint = endpoint.slice(0, slash)
43
+
44
+ const colon = endpoint.indexOf(':')
45
+ if (colon !== -1) endpoint = endpoint.slice(0, colon)
46
+
47
+ const dot = endpoint.indexOf('.')
48
+ const label = (dot === -1 ? endpoint : endpoint.slice(0, dot)).toLowerCase()
49
+ if (!label) return 'unknown'
50
+
51
+ let i = label.length - 1
52
+ while (i >= 0) {
53
+ const c = label.charCodeAt(i)
54
+ if (c >= 48 && c <= 57) i--
55
+ else break
56
+ }
57
+ return label.slice(0, i + 1) || 'unknown'
58
+ },
59
+ fillVoicePayload: (payload, guildId, conn, player, resume) => {
60
+ payload.guildId = guildId
61
+ const v = payload.data.voice
62
+ v.token = conn.token
63
+ v.endpoint = conn.endpoint
64
+ v.sessionId = conn.sessionId
65
+ v.channelId = player.voiceChannel
66
+ v.resume = resume ? true : undefined
67
+ v.sequence = resume ? conn.sequence : undefined
68
+ payload.data.volume = player?.volume ?? 100
69
+ return payload
70
+ }
71
+ }
72
+
73
+ class PayloadPool {
74
+ constructor() {
75
+ this._pool = []
76
+ this._size = 0
77
+ }
78
+
79
+ _create() {
80
+ return {
81
+ guildId: null,
82
+ data: {
83
+ voice: {
84
+ token: null,
85
+ endpoint: null,
86
+ sessionId: null,
87
+ resume: undefined,
88
+ sequence: undefined
89
+ },
90
+ volume: null
91
+ }
92
+ }
93
+ }
94
+
95
+ acquire() {
96
+ return this._size > 0 ? this._pool[--this._size] : this._create()
97
+ }
98
+
99
+ release(payload) {
100
+ if (!payload || this._size >= POOL_SIZE) return
101
+ payload.guildId = null
102
+ const v = payload.data.voice
103
+ v.token = v.endpoint = v.sessionId = null
104
+ v.resume = v.sequence = undefined
105
+ payload.data.volume = null
106
+ this._pool[this._size++] = payload
107
+ }
108
+
109
+ destroy() {
110
+ this._pool.length = 0
111
+ this._size = 0
112
+ }
113
+ }
114
+
115
+ const sharedPool = new PayloadPool()
116
+
117
+ class Connection {
118
+ constructor(player) {
119
+ if (!player?.aqua?.clientId || !player.nodes?.rest)
120
+ throw new TypeError('Invalid player configuration')
121
+
122
+ this._player = player
123
+ this._aqua = player.aqua
124
+ this._rest = player.nodes.rest
125
+ this._guildId = player.guildId
126
+ this._clientId = player.aqua.clientId
127
+
128
+ this.voiceChannel = player.voiceChannel
129
+ this.sessionId = null
130
+ this.channelId = null
131
+ this.endpoint = null
132
+ this.token = null
133
+ this.region = null
134
+ this.sequence = 0
135
+ this.txId = 0
136
+
137
+ this._lastEndpoint = null
138
+ this._stateFlags = 0
139
+ this._reconnectAttempts = 0
140
+ this._destroyed = false
141
+ this._reconnectTimer = null
142
+ this._lastVoiceDataUpdate = 0
143
+ this._consecutiveFailures = 0
144
+
145
+ this._voiceFlushTimer = null
146
+ this._pendingUpdate = null
147
+ this._lastSentVoiceKey = ''
148
+
149
+ this._nullChannelTimer = null
150
+ this.isWaitingForDisconnect = false
151
+
152
+ this._lastStateReqAt = 0
153
+ this._stateGeneration = 0
154
+ }
155
+
156
+ _hasValidVoiceData() {
157
+ if (!this.sessionId || !this.endpoint || !this.token) return false
158
+ if (Date.now() - this._lastVoiceDataUpdate > VOICE_DATA_TIMEOUT) {
159
+ this._stateFlags |= STATE.VOICE_DATA_STALE
160
+ return false
161
+ }
162
+ return true
163
+ }
164
+
165
+ _clearNullChannelTimer() {
166
+ if (!this._nullChannelTimer) return
167
+ clearTimeout(this._nullChannelTimer)
168
+ this._nullChannelTimer = null
169
+ }
170
+
171
+ _canAttemptResumeCore() {
172
+ if (this._destroyed) return false
173
+ if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return false
174
+ if (this._stateFlags & (STATE.ATTEMPTING_RESUME | STATE.DISCONNECTING))
175
+ return false
176
+ return true
177
+ }
178
+
179
+ _setReconnectTimer(delay) {
180
+ if (this._destroyed) return
181
+ this._clearReconnectTimer()
182
+ this._reconnectTimer = setTimeout(() => this._handleReconnect(), delay)
183
+ _functions.safeUnref(this._reconnectTimer)
184
+ }
185
+
186
+ setServerUpdate(data) {
187
+ if (this._destroyed || !data?.token) return
188
+
189
+ const endpoint =
190
+ typeof data.endpoint === 'string' ? data.endpoint.trim() : ''
191
+ if (!endpoint) return
192
+
193
+ if (this._lastEndpoint === endpoint && this.token === data.token) return
194
+
195
+ if (data.txId && data.txId < this.txId) return
196
+
197
+ this._stateGeneration++
198
+
199
+ if (this._lastEndpoint !== endpoint) {
200
+ this.sequence = 0
201
+ this._lastEndpoint = endpoint
202
+ this._reconnectAttempts = 0
203
+ this._consecutiveFailures = 0
204
+ }
205
+
206
+ this.endpoint = endpoint
207
+ this.region = _functions.extractRegion(endpoint)
208
+ this.token = data.token
209
+ this.channelId = data.channel_id || this.channelId || this.voiceChannel
210
+ this._lastVoiceDataUpdate = Date.now()
211
+ this._stateFlags &= ~STATE.VOICE_DATA_STALE
212
+
213
+ if (this._player?.paused) this._player.pause(false)
214
+ this._scheduleVoiceUpdate()
215
+ }
216
+
217
+ resendVoiceUpdate(force = false) {
218
+ if (this._destroyed || !this._hasValidVoiceData()) return false
219
+ if (force) this._lastSentVoiceKey = ''
220
+ this._scheduleVoiceUpdate()
221
+ return true
222
+ }
223
+
224
+ setStateUpdate(data) {
225
+ if (this._destroyed || !data || data.user_id !== this._clientId) return
226
+
227
+ const {
228
+ session_id: sessionId,
229
+ channel_id: channelId,
230
+ self_deaf: selfDeaf,
231
+ self_mute: selfMute
232
+ } = data
233
+ const p = this._player
234
+
235
+ if (channelId) this._clearNullChannelTimer()
236
+
237
+ if (data.txId && data.txId < this.txId) return
238
+
239
+ if (!channelId) {
240
+ this.isWaitingForDisconnect = true
241
+ if (!this._nullChannelTimer) {
242
+ this._nullChannelTimer = setTimeout(() => {
243
+ this._nullChannelTimer = null
244
+ this._handleDisconnect()
245
+ }, NULL_CHANNEL_GRACE_MS)
246
+ _functions.safeUnref(this._nullChannelTimer)
247
+ }
248
+ return
249
+ }
250
+
251
+ this.isWaitingForDisconnect = false
252
+
253
+ if (p && p.txId > this.txId) this.txId = p.txId
254
+
255
+ let needsUpdate = false
256
+
257
+ if (this.voiceChannel !== channelId) {
258
+ p._reconnecting = true
259
+ p._resuming = true
260
+ this._aqua.emit(
261
+ AqualinkEvents.PlayerMove,
262
+ p,
263
+ this.voiceChannel,
264
+ channelId
265
+ )
266
+ this.voiceChannel = channelId
267
+ p.voiceChannel = channelId
268
+ needsUpdate = true
269
+ }
270
+
271
+ if (this.sessionId !== sessionId) {
272
+ this.sessionId = sessionId
273
+ this._lastVoiceDataUpdate = Date.now()
274
+ this._stateFlags &= ~STATE.VOICE_DATA_STALE
275
+ this._reconnectAttempts = 0
276
+ this._consecutiveFailures = 0
277
+ needsUpdate = true
278
+ }
279
+
280
+ p.self_deaf = p.selfDeaf = !!selfDeaf
281
+ p.self_mute = p.selfMute = !!selfMute
282
+ this._stateFlags |= STATE.CONNECTED
283
+
284
+ if (needsUpdate) this._scheduleVoiceUpdate()
285
+ }
286
+
287
+ async _handleDisconnect() {
288
+ if (this._destroyed) return
289
+
290
+ this._stateFlags =
291
+ (this._stateFlags | STATE.DISCONNECTING) & ~STATE.CONNECTED
292
+ this._clearNullChannelTimer()
293
+ this._clearPendingUpdate()
294
+ this._clearReconnectTimer()
295
+
296
+ this.voiceChannel = null
297
+ this.sessionId = null
298
+ this.sequence = 0
299
+ this._lastVoiceDataUpdate = 0
300
+ this._stateFlags |= STATE.VOICE_DATA_STALE
301
+
302
+ try {
303
+ if (this._aqua && this._guildId) {
304
+ await this._aqua.destroyPlayer(this._guildId)
305
+ }
306
+ } catch (e) {
307
+ this._aqua?.emit?.(
308
+ AqualinkEvents.Debug,
309
+ new Error(`Player destroy failed: ${e?.message || e}`)
310
+ )
311
+ } finally {
312
+ this._stateFlags &= ~STATE.DISCONNECTING
313
+ }
314
+ }
315
+
316
+ _requestVoiceState() {
317
+ try {
318
+ const now = Date.now()
319
+ if (now - (this._lastStateReqAt || 0) < 1500) return false
320
+ this._lastStateReqAt = now
321
+
322
+ if (
323
+ typeof this._player?.send !== 'function' ||
324
+ !this._player.voiceChannel
325
+ )
326
+ return false
327
+ this._player.send({
328
+ guild_id: this._guildId,
329
+ channel_id: this._player.voiceChannel,
330
+ self_deaf: this._player.deaf,
331
+ self_mute: this._player.mute
332
+ })
333
+ return true
334
+ } catch {
335
+ return false
336
+ }
337
+ }
338
+
339
+ async attemptResume() {
340
+ if (!this._canAttemptResumeCore()) return false
341
+
342
+ const currentGen = this._stateGeneration
343
+
344
+ if (
345
+ !this.sessionId ||
346
+ !this.endpoint ||
347
+ !this.token ||
348
+ this._stateFlags & STATE.VOICE_DATA_STALE
349
+ ) {
350
+ this._aqua.emit(
351
+ AqualinkEvents.Debug,
352
+ `Resume blocked: missing voice data for guild ${this._guildId}, requesting voice state`
353
+ )
354
+ this._requestVoiceState()
355
+ return false
356
+ }
357
+
358
+ this.txId = this._player.txId || this.txId
359
+ this._stateFlags |= STATE.ATTEMPTING_RESUME
360
+ this._reconnectAttempts++
361
+ this._aqua.emit(
362
+ AqualinkEvents.Debug,
363
+ `Attempt resume: guild=${this._guildId} endpoint=${this.endpoint} session=${this.sessionId}`
364
+ )
365
+
366
+ const payload = sharedPool.acquire()
367
+ try {
368
+ _functions.fillVoicePayload(
369
+ payload,
370
+ this._guildId,
371
+ this,
372
+ this._player,
373
+ true
374
+ )
375
+
376
+ if (this._stateGeneration !== currentGen) {
377
+ this._aqua.emit(
378
+ AqualinkEvents.Debug,
379
+ `Resume aborted: State changed during attempt for guild ${this._guildId}`
380
+ )
381
+ return false
382
+ }
383
+
384
+ await this._sendUpdate(payload)
385
+
386
+ this._reconnectAttempts = 0
387
+ this._consecutiveFailures = 0
388
+ if (this._player) this._player._resuming = false
389
+
390
+ this._aqua.emit(
391
+ AqualinkEvents.Debug,
392
+ `Resume PATCH sent for guild ${this._guildId}`
393
+ )
394
+ return true
395
+ } catch (e) {
396
+ if (this._destroyed || !this._aqua) throw e
397
+ this._consecutiveFailures++
398
+ this._aqua.emit(
399
+ AqualinkEvents.Debug,
400
+ `Resume failed for guild ${this._guildId}: ${e?.message || e}`
401
+ )
402
+
403
+ if (
404
+ this._reconnectAttempts < MAX_RECONNECT_ATTEMPTS &&
405
+ !this._destroyed &&
406
+ this._consecutiveFailures < 5
407
+ ) {
408
+ const delay = Math.min(
409
+ RECONNECT_DELAY * (1 << (this._reconnectAttempts - 1)),
410
+ RESUME_BACKOFF_MAX
411
+ )
412
+ this._setReconnectTimer(delay)
413
+ } else {
414
+ this._aqua.emit(
415
+ AqualinkEvents.Debug,
416
+ `Max reconnect attempts/failures reached for guild ${this._guildId}`
417
+ )
418
+ if (this._player) this._player._resuming = false
419
+ this._handleDisconnect()
420
+ }
421
+ return false
422
+ } finally {
423
+ this._stateFlags &= ~STATE.ATTEMPTING_RESUME
424
+ sharedPool.release(payload)
425
+ }
426
+ }
427
+
428
+ _handleReconnect() {
429
+ this._reconnectTimer = null
430
+ if (!this._destroyed) this.attemptResume()
431
+ }
432
+
433
+ updateSequence(seq) {
434
+ if (_functions.isValidNumber(seq) && seq > this.sequence)
435
+ this.sequence = seq
436
+ }
437
+
438
+ _clearReconnectTimer() {
439
+ if (!this._reconnectTimer) return
440
+ clearTimeout(this._reconnectTimer)
441
+ this._reconnectTimer = null
442
+ }
443
+
444
+ _clearPendingUpdate() {
445
+ this._stateFlags &= ~STATE.UPDATE_SCHEDULED
446
+ if (this._pendingUpdate?.payload)
447
+ sharedPool.release(this._pendingUpdate.payload)
448
+ this._pendingUpdate = null
449
+ if (this._voiceFlushTimer) {
450
+ clearTimeout(this._voiceFlushTimer)
451
+ this._voiceFlushTimer = null
452
+ }
453
+ }
454
+
455
+ _makeVoiceKey() {
456
+ const p = this._player
457
+ const vol = p?.volume ?? 100
458
+ return `${this.sessionId || ''}|${this.token || ''}|${this.endpoint || ''}|${p?.voiceChannel || ''}|${vol}`
459
+ }
460
+
461
+ _scheduleVoiceUpdate() {
462
+ if (this._destroyed) return
463
+ if (!this._hasValidVoiceData()) return
464
+
465
+ if (!this._pendingUpdate) {
466
+ const payload = sharedPool.acquire()
467
+ _functions.fillVoicePayload(
468
+ payload,
469
+ this._guildId,
470
+ this,
471
+ this._player,
472
+ false
473
+ )
474
+ this._pendingUpdate = { payload, timestamp: Date.now() }
475
+ } else {
476
+ this._pendingUpdate.timestamp = Date.now()
477
+ _functions.fillVoicePayload(
478
+ this._pendingUpdate.payload,
479
+ this._guildId,
480
+ this,
481
+ this._player,
482
+ false
483
+ )
484
+ }
485
+
486
+ if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
487
+ this._stateFlags |= STATE.UPDATE_SCHEDULED
488
+
489
+ this._voiceFlushTimer = setTimeout(
490
+ () => this._executeVoiceUpdate(),
491
+ VOICE_FLUSH_DELAY
492
+ )
493
+ _functions.safeUnref(this._voiceFlushTimer)
494
+ }
495
+
496
+ _executeVoiceUpdate() {
497
+ if (this._destroyed) return
498
+ this._stateFlags &= ~STATE.UPDATE_SCHEDULED
499
+ this._voiceFlushTimer = null
500
+
501
+ const pending = this._pendingUpdate
502
+ this._pendingUpdate = null
503
+
504
+ if (!pending) return
505
+ if (Date.now() - pending.timestamp > UPDATE_TIMEOUT) {
506
+ sharedPool.release(pending.payload)
507
+ return
508
+ }
509
+
510
+ const key = this._makeVoiceKey()
511
+ if (key === this._lastSentVoiceKey) {
512
+ sharedPool.release(pending.payload)
513
+ return
514
+ }
515
+ this._lastSentVoiceKey = key
516
+
517
+ this._sendUpdate(pending.payload)
518
+ .catch(_functions.noop)
519
+ .finally(() => sharedPool.release(pending.payload))
520
+ }
521
+
522
+ async _sendUpdate(payload) {
523
+ if (this._destroyed) throw new Error('Connection destroyed')
524
+ if (!this._rest) throw new Error('REST interface unavailable')
525
+
526
+ try {
527
+ await this._rest.updatePlayer(payload)
528
+ } catch (e) {
529
+ if (e.statusCode === 404 || e.response?.statusCode === 404) {
530
+ const isSessionError = e.body?.message?.includes('sessionId') || false
531
+ if (this._aqua) {
532
+ this._aqua.emit(
533
+ AqualinkEvents.Debug,
534
+ `[Aqua/Connection] Player ${this._guildId} not found (404)${isSessionError ? ' - Session invalid' : ''}. Destroying.`
535
+ )
536
+ if (isSessionError && this._player?.nodes?._clearSession) {
537
+ this._player.nodes._clearSession()
538
+ }
539
+ await this._aqua.destroyPlayer(this._guildId)
540
+ }
541
+ throw e
542
+ }
543
+ if (!_functions.isNetworkError(e)) {
544
+ this._aqua.emit(
545
+ AqualinkEvents.Debug,
546
+ new Error(`Voice update failed: ${e?.message || e}`)
547
+ )
548
+ }
549
+ throw e
550
+ }
551
+ }
552
+
553
+ destroy() {
554
+ if (this._destroyed) return
555
+ this._destroyed = true
556
+
557
+ this._clearNullChannelTimer()
558
+ this._clearPendingUpdate()
559
+ this._clearReconnectTimer()
560
+
561
+ this._player = this._aqua = this._rest = null
562
+ this.voiceChannel =
563
+ this.sessionId =
564
+ this.endpoint =
565
+ this.token =
566
+ this.region =
567
+ this._lastEndpoint =
568
+ null
569
+ this._stateFlags = 0
570
+ this.sequence = 0
571
+ this._reconnectAttempts = 0
572
+ this._consecutiveFailures = 0
573
+ this._lastVoiceDataUpdate = 0
574
+ }
575
+ }
576
+
484
577
  module.exports = Connection