aqualink 2.19.1 → 2.20.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.
@@ -48,6 +48,17 @@ const _functions = {
48
48
  const label = (dot === -1 ? endpoint : endpoint.slice(0, dot)).toLowerCase()
49
49
  if (!label) return 'unknown'
50
50
 
51
+ // Discord voice hosts commonly look like: c-gru20-<hash>.discord.media
52
+ const cPrefix = /^c-([a-z]{3})(?:\d+)?(?:-|$)/
53
+ const m1 = cPrefix.exec(label)
54
+ if (m1?.[1]) return m1[1]
55
+
56
+ // Fallback for labels that still contain an iata-like token.
57
+ const token = /(?:^|-)([a-z]{3})(?:\d+)?(?:-|$)/
58
+ const m2 = token.exec(label)
59
+ if (m2?.[1]) return m2[1]
60
+
61
+ // Last fallback: strip trailing digits from first label.
51
62
  let i = label.length - 1
52
63
  while (i >= 0) {
53
64
  const c = label.charCodeAt(i)
@@ -56,15 +67,13 @@ const _functions = {
56
67
  }
57
68
  return label.slice(0, i + 1) || 'unknown'
58
69
  },
59
- fillVoicePayload: (payload, guildId, conn, player, resume) => {
70
+ fillVoicePayload: (payload, guildId, conn, player) => {
60
71
  payload.guildId = guildId
61
72
  const v = payload.data.voice
62
73
  v.token = conn.token
63
74
  v.endpoint = conn.endpoint
64
75
  v.sessionId = conn.sessionId
65
76
  v.channelId = player.voiceChannel
66
- v.resume = resume ? true : undefined
67
- v.sequence = resume ? conn.sequence : undefined
68
77
  payload.data.volume = player?.volume ?? 100
69
78
  return payload
70
79
  }
@@ -83,9 +92,7 @@ class PayloadPool {
83
92
  voice: {
84
93
  token: null,
85
94
  endpoint: null,
86
- sessionId: null,
87
- resume: undefined,
88
- sequence: undefined
95
+ sessionId: null
89
96
  },
90
97
  volume: null
91
98
  }
@@ -101,7 +108,6 @@ class PayloadPool {
101
108
  payload.guildId = null
102
109
  const v = payload.data.voice
103
110
  v.token = v.endpoint = v.sessionId = null
104
- v.resume = v.sequence = undefined
105
111
  payload.data.volume = null
106
112
  this._pool[this._size++] = payload
107
113
  }
@@ -151,6 +157,7 @@ class Connection {
151
157
 
152
158
  this._lastStateReqAt = 0
153
159
  this._stateGeneration = 0
160
+ this._regionMigrationAttempted = false
154
161
  }
155
162
 
156
163
  _hasValidVoiceData() {
@@ -201,6 +208,7 @@ class Connection {
201
208
  this._lastEndpoint = endpoint
202
209
  this._reconnectAttempts = 0
203
210
  this._consecutiveFailures = 0
211
+ this._regionMigrationAttempted = false
204
212
  }
205
213
 
206
214
  this.endpoint = endpoint
@@ -208,10 +216,71 @@ class Connection {
208
216
  this.token = data.token
209
217
  this.channelId = data.channel_id || this.channelId || this.voiceChannel
210
218
  this._lastVoiceDataUpdate = Date.now()
219
+ this._aqua?._trace?.('connection.serverUpdate', {
220
+ guildId: this._guildId,
221
+ endpoint: this.endpoint,
222
+ region: this.region,
223
+ txId: data.txId || null
224
+ })
211
225
  this._stateFlags &= ~STATE.VOICE_DATA_STALE
212
226
 
213
227
  if (this._player?.paused) this._player.pause(false)
228
+ const migrated = this._checkRegionMigration()
229
+ if (migrated) return
214
230
  this._scheduleVoiceUpdate()
231
+ this._player?._flushDeferredPlay?.()
232
+ }
233
+
234
+ _checkRegionMigration() {
235
+ if (this._destroyed || this._regionMigrationAttempted) return false
236
+ if (
237
+ !this._aqua?.autoRegionMigrate ||
238
+ !this.region ||
239
+ this.region === 'unknown'
240
+ )
241
+ return false
242
+ const player = this._player
243
+ if (!player || player.destroyed || player._resuming || player._reconnecting)
244
+ return false
245
+
246
+ const currentNode = player.nodes
247
+ if (!currentNode) return false
248
+
249
+ const currentRegions = Array.isArray(currentNode.regions)
250
+ ? currentNode.regions
251
+ : []
252
+ const alreadyMatching = currentRegions.some((r) =>
253
+ this._aqua._regionMatches?.(r, this.region)
254
+ )
255
+ if (alreadyMatching) {
256
+ this._regionMigrationAttempted = true
257
+ return false
258
+ }
259
+
260
+ const targetNode = this._aqua._findBestNodeForRegion?.(this.region)
261
+ if (!targetNode || targetNode === currentNode) return false
262
+
263
+ this._regionMigrationAttempted = true
264
+ this._aqua?._trace?.('connection.region.migrate', {
265
+ guildId: this._guildId,
266
+ region: this.region,
267
+ from: currentNode?.name || currentNode?.host,
268
+ to: targetNode?.name || targetNode?.host
269
+ })
270
+
271
+ queueMicrotask(() => {
272
+ this._aqua
273
+ .movePlayerToNode?.(this._guildId, targetNode, 'region')
274
+ .catch((err) => {
275
+ this._regionMigrationAttempted = false
276
+ this._aqua?._trace?.('connection.region.migrate.error', {
277
+ guildId: this._guildId,
278
+ region: this.region,
279
+ error: err?.message || String(err)
280
+ })
281
+ })
282
+ })
283
+ return true
215
284
  }
216
285
 
217
286
  resendVoiceUpdate(force = false) {
@@ -237,6 +306,10 @@ class Connection {
237
306
  if (data.txId && data.txId < this.txId) return
238
307
 
239
308
  if (!channelId) {
309
+ this._aqua?._trace?.('connection.stateUpdate.nullChannel', {
310
+ guildId: this._guildId,
311
+ txId: data.txId || null
312
+ })
240
313
  this.isWaitingForDisconnect = true
241
314
  if (!this._nullChannelTimer) {
242
315
  this._nullChannelTimer = setTimeout(() => {
@@ -249,6 +322,12 @@ class Connection {
249
322
  }
250
323
 
251
324
  this.isWaitingForDisconnect = false
325
+ this._aqua?._trace?.('connection.stateUpdate', {
326
+ guildId: this._guildId,
327
+ channelId,
328
+ sessionId,
329
+ txId: data.txId || null
330
+ })
252
331
 
253
332
  if (p && p.txId > this.txId) this.txId = p.txId
254
333
 
@@ -289,6 +368,9 @@ class Connection {
289
368
 
290
369
  this._stateFlags =
291
370
  (this._stateFlags | STATE.DISCONNECTING) & ~STATE.CONNECTED
371
+ this._aqua?._trace?.('connection.disconnect', {
372
+ guildId: this._guildId
373
+ })
292
374
  this._clearNullChannelTimer()
293
375
  this._clearPendingUpdate()
294
376
  this._clearReconnectTimer()
@@ -338,6 +420,13 @@ class Connection {
338
420
 
339
421
  async attemptResume() {
340
422
  if (!this._canAttemptResumeCore()) return false
423
+ this._aqua?._trace?.('connection.resume.attempt', {
424
+ guildId: this._guildId,
425
+ reconnectAttempts: this._reconnectAttempts,
426
+ hasSessionId: !!this.sessionId,
427
+ hasEndpoint: !!this.endpoint,
428
+ hasToken: !!this.token
429
+ })
341
430
 
342
431
  const currentGen = this._stateGeneration
343
432
 
@@ -382,6 +471,9 @@ class Connection {
382
471
  }
383
472
 
384
473
  await this._sendUpdate(payload)
474
+ this._aqua?._trace?.('connection.resume.success', {
475
+ guildId: this._guildId
476
+ })
385
477
 
386
478
  this._reconnectAttempts = 0
387
479
  this._consecutiveFailures = 0
@@ -399,6 +491,10 @@ class Connection {
399
491
  AqualinkEvents.Debug,
400
492
  `Resume failed for guild ${this._guildId}: ${e?.message || e}`
401
493
  )
494
+ this._aqua?._trace?.('connection.resume.error', {
495
+ guildId: this._guildId,
496
+ error: e?.message || String(e)
497
+ })
402
498
 
403
499
  if (
404
500
  this._reconnectAttempts < MAX_RECONNECT_ATTEMPTS &&
@@ -459,8 +555,23 @@ class Connection {
459
555
  }
460
556
 
461
557
  _scheduleVoiceUpdate() {
462
- if (this._destroyed) return
463
- if (!this._hasValidVoiceData()) return
558
+ if (this._destroyed) {
559
+ this._aqua?._trace?.('connection.update.skip', {
560
+ guildId: this._guildId,
561
+ reason: 'destroyed'
562
+ })
563
+ return
564
+ }
565
+ if (!this._hasValidVoiceData()) {
566
+ this._aqua?._trace?.('connection.update.skip', {
567
+ guildId: this._guildId,
568
+ reason: 'invalid_voice_data',
569
+ hasSessionId: !!this.sessionId,
570
+ hasEndpoint: !!this.endpoint,
571
+ hasToken: !!this.token
572
+ })
573
+ return
574
+ }
464
575
 
465
576
  if (!this._pendingUpdate) {
466
577
  const payload = sharedPool.acquire()
@@ -485,6 +596,9 @@ class Connection {
485
596
 
486
597
  if (this._stateFlags & STATE.UPDATE_SCHEDULED) return
487
598
  this._stateFlags |= STATE.UPDATE_SCHEDULED
599
+ this._aqua?._trace?.('connection.update.scheduled', {
600
+ guildId: this._guildId
601
+ })
488
602
 
489
603
  this._voiceFlushTimer = setTimeout(
490
604
  () => this._executeVoiceUpdate(),
@@ -524,8 +638,22 @@ class Connection {
524
638
  if (!this._rest) throw new Error('REST interface unavailable')
525
639
 
526
640
  try {
641
+ this._aqua?._trace?.('connection.update.send', {
642
+ guildId: this._guildId,
643
+ hasSessionId: !!this._rest?.sessionId,
644
+ hasVoice:
645
+ !!payload?.data?.voice?.sessionId && !!payload?.data?.voice?.endpoint
646
+ })
527
647
  await this._rest.updatePlayer(payload)
648
+ this._aqua?._trace?.('connection.update.ok', {
649
+ guildId: this._guildId
650
+ })
528
651
  } catch (e) {
652
+ this._aqua?._trace?.('connection.update.error', {
653
+ guildId: this._guildId,
654
+ statusCode: e?.statusCode || e?.response?.statusCode || null,
655
+ error: e?.message || String(e)
656
+ })
529
657
  if (e.statusCode === 404 || e.response?.statusCode === 404) {
530
658
  const isSessionError = e.body?.message?.includes('sessionId') || false
531
659
  if (this._aqua) {
@@ -574,4 +702,4 @@ class Connection {
574
702
  }
575
703
  }
576
704
 
577
- module.exports = Connection
705
+ module.exports = Connection
@@ -41,7 +41,7 @@ const EMPTY_ARRAY = Object.freeze([])
41
41
 
42
42
  const FILTER_POOL_SIZE = 16
43
43
  const filterPool = {
44
- pools: Object.fromEntries(Object.keys(FILTER_DEFAULTS).map(k => [k, []])),
44
+ pools: Object.fromEntries(Object.keys(FILTER_DEFAULTS).map((k) => [k, []])),
45
45
 
46
46
  acquire(type) {
47
47
  const pool = this.pools[type]
@@ -187,6 +187,10 @@ class Node {
187
187
  this._isConnecting = false
188
188
  this.reconnectAttempted = 0
189
189
  this._emitDebug('WebSocket connection established')
190
+ this.aqua?._trace?.('node.ws.open', {
191
+ node: this.name,
192
+ reconnectAttempted: this.reconnectAttempted
193
+ })
190
194
 
191
195
  if (!this.aqua?.bypassChecks?.nodeFetchInfo && !this.info) {
192
196
  const timeoutId = setTimeout(() => {
@@ -268,6 +272,12 @@ class Node {
268
272
  code,
269
273
  reason: _functions.reasonToString(reason)
270
274
  })
275
+ this.aqua?._trace?.('node.ws.close', {
276
+ node: this.name,
277
+ code,
278
+ reason: _functions.reasonToString(reason),
279
+ hasSessionId: !!this.sessionId
280
+ })
271
281
 
272
282
  if (this.isDestroyed) return
273
283
 
@@ -301,6 +311,11 @@ class Node {
301
311
  this._clearReconnectTimeout()
302
312
 
303
313
  const attempt = ++this.reconnectAttempted
314
+ this.aqua?._trace?.('node.ws.reconnect.scheduled', {
315
+ node: this.name,
316
+ attempt,
317
+ infinite: !!this.infiniteReconnects
318
+ })
304
319
 
305
320
  if (this.infiniteReconnects) {
306
321
  this.aqua.emit(AqualinkEvents.NodeReconnect, this, {
@@ -339,8 +354,7 @@ class Node {
339
354
 
340
355
  _calcBackoff(attempt) {
341
356
  const baseBackoff =
342
- this.reconnectTimeout *
343
- Node.BACKOFF_MULTIPLIER ** Math.min(attempt, 10)
357
+ this.reconnectTimeout * Node.BACKOFF_MULTIPLIER ** Math.min(attempt, 10)
344
358
  const maxJitter = Math.min(
345
359
  Node.JITTER_MAX,
346
360
  baseBackoff * Node.JITTER_FACTOR
@@ -370,6 +384,10 @@ class Node {
370
384
  this._isConnecting = true
371
385
  this.state = NODE_STATE.CONNECTING
372
386
  this._cleanup()
387
+ this.aqua?._trace?.('node.ws.connect', {
388
+ node: this.name,
389
+ url: this.wsUrl
390
+ })
373
391
 
374
392
  try {
375
393
  const h = this._boundHandlers
@@ -570,6 +588,12 @@ class Node {
570
588
  oldSessionId && oldSessionId !== sessionId && !payload.resumed
571
589
 
572
590
  this.sessionId = sessionId
591
+ this.aqua?._trace?.('node.ready.packet', {
592
+ node: this.name,
593
+ resumed: !!payload.resumed,
594
+ oldSessionId,
595
+ newSessionId: sessionId
596
+ })
573
597
  this.rest.setSessionId(sessionId)
574
598
  this._headers['Session-Id'] = sessionId
575
599
 
@@ -611,6 +635,11 @@ class Node {
611
635
 
612
636
  async _resumePlayers() {
613
637
  if (!this.sessionId) return
638
+ this.aqua?._trace?.('node.resume.begin', {
639
+ node: this.name,
640
+ sessionId: this.sessionId,
641
+ players: this.aqua?.players?.size || 0
642
+ })
614
643
 
615
644
  try {
616
645
  await this.rest.makeRequest('PATCH', `/v4/sessions/${this.sessionId}`, {
@@ -626,6 +655,11 @@ class Node {
626
655
  ) {
627
656
  try {
628
657
  this._emitDebug(`Rejoining voice for guild ${guildId} on resume`)
658
+ this.aqua?._trace?.('node.resume.rejoin', {
659
+ node: this.name,
660
+ guildId,
661
+ voiceChannel: player.voiceChannel
662
+ })
629
663
  player.connect({
630
664
  voiceChannel: player.voiceChannel,
631
665
  deaf: player.deaf,
@@ -644,6 +678,10 @@ class Node {
644
678
  await this.aqua.loadPlayers()
645
679
  }
646
680
  } catch (err) {
681
+ this.aqua?._trace?.('node.resume.error', {
682
+ node: this.name,
683
+ error: _functions.errMsg(err)
684
+ })
647
685
  this._emitError(`Failed to resume session: ${_functions.errMsg(err)}`)
648
686
  throw err
649
687
  }
@@ -664,4 +702,4 @@ class Node {
664
702
  }
665
703
  }
666
704
 
667
- module.exports = Node
705
+ module.exports = Node