aqualink 2.19.0 → 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,14 +1,25 @@
1
1
  const https = require('https')
2
2
 
3
+ // Default agent config (used only if shared agent not provided)
3
4
  const AGENT_CONFIG = {
4
5
  keepAlive: true,
5
- maxSockets: 5,
6
- maxFreeSockets: 2,
6
+ maxSockets: 64,
7
+ maxFreeSockets: 32,
7
8
  timeout: 8000,
8
9
  freeSocketTimeout: 4000
9
10
  }
10
11
 
11
- const agent = new https.Agent(AGENT_CONFIG)
12
+ // Shared agent reference - can be set from Rest module
13
+ let sharedAgent = null
14
+ const getAgent = () => sharedAgent || (sharedAgent = new https.Agent(AGENT_CONFIG))
15
+
16
+ // Allow Rest module to inject its agent
17
+ const setSharedAgent = (agent) => {
18
+ if (agent && typeof agent.request === 'function') {
19
+ sharedAgent = agent
20
+ }
21
+ }
22
+
12
23
 
13
24
  const SC_LINK_RE = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g
14
25
  const MAX_REDIRECTS = 3
@@ -23,7 +34,7 @@ const fastFetch = (url, depth = 0) =>
23
34
 
24
35
  const req = https.get(
25
36
  url,
26
- { agent, timeout: DEFAULT_TIMEOUT_MS },
37
+ { agent: getAgent(), timeout: DEFAULT_TIMEOUT_MS },
27
38
  (res) => {
28
39
  const { statusCode, headers } = res
29
40
 
@@ -132,5 +143,6 @@ const spAutoPlay = async (seed, player, requester, excludedIds = []) => {
132
143
 
133
144
  module.exports = {
134
145
  scAutoPlay,
135
- spAutoPlay
146
+ spAutoPlay,
147
+ setSharedAgent
136
148
  }
package/build/index.d.ts CHANGED
@@ -50,16 +50,81 @@ declare module 'aqualink' {
50
50
  get leastUsedNodes(): Node[]
51
51
 
52
52
  // Core Methods
53
+ /**
54
+ * Initializes the specific client id and connects to all nodes
55
+ * @param clientId Client ID
56
+ * @example
57
+ * ```ts
58
+ * await aqua.init(client.user.id);
59
+ * ```
60
+ */
53
61
  init(clientId: string): Promise<Aqua>
62
+
63
+ /**
64
+ * Creates a new node connection
65
+ * @param options Modified node options
66
+ */
54
67
  createNode(options: NodeOptions): Promise<Node>
68
+
69
+ /**
70
+ * Destroys a node by identifier
71
+ * @param identifier Node identifier (name or host)
72
+ */
55
73
  destroyNode(identifier: string): void
74
+
75
+ /**
76
+ * Updates the voice state of a player
77
+ * @param data Voice state update packet
78
+ */
56
79
  updateVoiceState(data: VoiceStateUpdate | VoiceServerUpdate): void
80
+
81
+ /**
82
+ * Fetches nodes in a specific region
83
+ * @param region Region name
84
+ */
57
85
  fetchRegion(region: string): Node[]
86
+
87
+ /**
88
+ * Creates a connection for a player
89
+ * @param options Connection options
90
+ */
58
91
  createConnection(options: ConnectionOptions): Player
92
+
93
+ /**
94
+ * Creates a player on a specific node
95
+ * @param node Node to create player on
96
+ * @param options Player options
97
+ */
59
98
  createPlayer(node: Node, options: PlayerOptions): Player
99
+
100
+ /**
101
+ * Destroys a player
102
+ * @param guildId Guild ID
103
+ */
60
104
  destroyPlayer(guildId: string): Promise<void>
105
+
106
+ /**
107
+ * Resolves a track or playlist
108
+ * @param options Resolution options
109
+ * @example
110
+ * ```ts
111
+ * const result = await aqua.resolve({ query: 'https://...', requester: user });
112
+ * ```
113
+ */
61
114
  resolve(options: ResolveOptions): Promise<ResolveResponse>
115
+
116
+ /**
117
+ * Gets an existing player
118
+ * @param guildId Guild ID
119
+ */
62
120
  get(guildId: string): Player
121
+
122
+ /**
123
+ * Searches for tracks
124
+ * @param query Search query
125
+ * @param requester Requester object
126
+ * @param source Search source (ytsearch, scsearch, etc)
127
+ */
63
128
  search(
64
129
  query: string,
65
130
  requester: any,
@@ -68,12 +133,16 @@ declare module 'aqualink' {
68
133
 
69
134
  // Save/Load Methods
70
135
  savePlayer(filePath?: string): Promise<void>
136
+ savePlayerSync(filePath?: string): void
71
137
  loadPlayers(filePath?: string): Promise<void>
72
138
 
73
139
  // Failover and Migration Methods
74
140
  handleNodeFailover(failedNode: Node): Promise<void>
75
141
 
76
142
  // Utility Methods
143
+ /**
144
+ * Destroys the Aqua instance and all players
145
+ */
77
146
  destroy(): Promise<void>
78
147
 
79
148
  // Internal Methods
@@ -168,7 +237,7 @@ declare module 'aqualink' {
168
237
  _isConnecting: boolean
169
238
  _debugEnabled: boolean
170
239
  _headers: Record<string, string>
171
- _boundHandlers: Record<string, Function>
240
+ _boundHandlers: Record<string, ReturnType<typeof this._boundHandlers>>
172
241
 
173
242
  // Methods
174
243
  connect(): Promise<void>
@@ -244,29 +313,119 @@ declare module 'aqualink' {
244
313
  _boundPlayerUpdate: (packet: any) => void
245
314
  _boundEvent: (payload: any) => void
246
315
  _boundAquaPlayerMove: (oldChannel: string, newChannel: string) => void
316
+ _lastVoiceChannel: string | null
317
+ _lastTextChannel: string | null
247
318
 
248
319
  // Getters
249
320
  get previous(): Track | null
250
321
  get currenttrack(): Track | null
251
322
 
252
323
  // Core Methods
253
- play(): Promise<Player>
324
+ /**
325
+ * Plays a track. If no track is provided, plays the next track in the queue.
326
+ * @param track The track to play
327
+ * @param options Options for playback
328
+ * @example
329
+ * ```ts
330
+ * // Play the next track in the queue
331
+ * await player.play();
332
+ *
333
+ * // Play a specific track
334
+ * await player.play(track);
335
+ * ```
336
+ */
337
+ play(track?: Track | null, options?: { paused?: boolean; startTime?: number; noReplace?: boolean }): Promise<Player>
338
+
339
+ /**
340
+ * Connects the player to a voice channel
341
+ * @param options Connection options
342
+ * @example
343
+ * ```ts
344
+ * player.connect({
345
+ * guildId: '...',
346
+ * voiceChannel: '...',
347
+ * deaf: true
348
+ * });
349
+ * ```
350
+ */
254
351
  connect(options?: ConnectionOptions): Player
352
+
353
+ /**
354
+ * Destroys the player and optionally cleans up resources
355
+ * @param options Destruction options
356
+ */
255
357
  destroy(options?: {
256
358
  preserveClient?: boolean
257
359
  skipRemote?: boolean
360
+ preserveMessage?: boolean
361
+ preserveReconnecting?: boolean
362
+ preserveTracks?: boolean
258
363
  }): Player
364
+
365
+ /**
366
+ * Pauses or resumes the player
367
+ * @param paused Whether to pause
368
+ */
259
369
  pause(paused: boolean): Player
370
+
371
+ /**
372
+ * Seeks to a position in the current track
373
+ * @param position Position in milliseconds
374
+ */
260
375
  seek(position: number): Player
376
+
377
+ /**
378
+ * Stops the playback
379
+ */
261
380
  stop(): Player
381
+
382
+ /**
383
+ * Sets the player volume
384
+ * @param volume Volume (0-1000)
385
+ */
262
386
  setVolume(volume: number): Player
387
+
388
+ /**
389
+ * Sets the loop mode
390
+ * @param mode Loop mode (off, track, queue)
391
+ */
263
392
  setLoop(mode: LoopMode | LoopModeName): Player
393
+
394
+ /**
395
+ * Sets the text channel for the player
396
+ * @param channel Channel ID
397
+ */
264
398
  setTextChannel(channel: string): Player
399
+
400
+ /**
401
+ * Sets the voice channel and moves the player
402
+ * @param channel Channel ID
403
+ */
265
404
  setVoiceChannel(channel: string): Player
405
+
406
+ /**
407
+ * Disconnects the player from voice
408
+ */
266
409
  disconnect(): Player
410
+
411
+ /**
412
+ * Shuffles the queue
413
+ */
267
414
  shuffle(): Player
415
+
416
+ /**
417
+ * Gets the player queue
418
+ */
268
419
  getQueue(): Queue
420
+
421
+ /**
422
+ * Replays the current track from the beginning
423
+ */
269
424
  replay(): Player
425
+
426
+ /**
427
+ * Skips the current track
428
+ */
270
429
  skip(): Player
271
430
 
272
431
  // Advanced Methods
@@ -355,9 +514,24 @@ declare module 'aqualink' {
355
514
  get thumbnail(): string
356
515
 
357
516
  // Methods
517
+ /**
518
+ * Resolves local artwork/thumbnail
519
+ */
358
520
  resolveThumbnail(url?: string): string | null
521
+
522
+ /**
523
+ * Resolves the track if it needs (re)resolution
524
+ */
359
525
  resolve(aqua: Aqua, opts?: TrackResolutionOptions): Promise<Track | null>
526
+
527
+ /**
528
+ * Checks if the track is valid
529
+ */
360
530
  isValid(): boolean
531
+
532
+ /**
533
+ * Disposes the track and frees resources
534
+ */
361
535
  dispose(): void
362
536
 
363
537
  // Internal Methods
@@ -380,13 +554,54 @@ declare module 'aqualink' {
380
554
  useHttp2: boolean
381
555
 
382
556
  // Core Methods
557
+ /**
558
+ * Sets the session ID for the REST connection
559
+ * @param sessionId The session ID
560
+ */
383
561
  setSessionId(sessionId: string): void
562
+
563
+ /**
564
+ * Makes a generic request to the Lavalink REST API
565
+ * @param method HTTP method
566
+ * @param endpoint API endpoint
567
+ * @param body Request body
568
+ */
384
569
  makeRequest(method: HttpMethod, endpoint: string, body?: any): Promise<any>
570
+
571
+ /**
572
+ * Updates a player via REST
573
+ * @param options Update options
574
+ */
385
575
  updatePlayer(options: UpdatePlayerOptions): Promise<any>
576
+
577
+ /**
578
+ * Destroys a player via REST
579
+ * @param guildId Guild ID
580
+ */
386
581
  destroyPlayer(guildId: string): Promise<any>
582
+
583
+ /**
584
+ * Gets lyrics for a track
585
+ * @param options Lyrics options
586
+ */
387
587
  getLyrics(options: GetLyricsOptions): Promise<LyricsResponse>
588
+
589
+ /**
590
+ * Subscribes to live lyrics events
591
+ * @param guildId Guild ID
592
+ * @param sync Whether to sync with playback
593
+ */
388
594
  subscribeLiveLyrics(guildId: string, sync?: boolean): Promise<any>
595
+
596
+ /**
597
+ * Unsubscribes from live lyrics
598
+ * @param guildId Guild ID
599
+ */
389
600
  unsubscribeLiveLyrics(guildId: string): Promise<any>
601
+
602
+ /**
603
+ * Gets node statistics
604
+ */
390
605
  getStats(): Promise<NodeStats>
391
606
 
392
607
  // Additional REST Methods
@@ -421,14 +636,33 @@ declare module 'aqualink' {
421
636
  readonly last: Track | null
422
637
 
423
638
  // Methods
639
+ /**
640
+ * Adds a track to the queue
641
+ * @param track Track to add
642
+ */
424
643
  add(track: Track): Queue
644
+
645
+ /**
646
+ * Adds multiple tracks to the queue
647
+ * @param tracks Tracks to add
648
+ */
425
649
  add(...tracks: Track[]): Queue
650
+
426
651
  push(track: Track): number
427
652
  unshift(track: Track): number
428
653
  shift(): Track | undefined
429
654
  remove(track: Track): boolean
655
+
656
+ /**
657
+ * Clears the queue
658
+ */
430
659
  clear(): void
660
+
661
+ /**
662
+ * Shuffles the queue
663
+ */
431
664
  shuffle(): Queue
665
+
432
666
  peek(): Track | null
433
667
  isEmpty(): boolean
434
668
  toArray(): Track[]
@@ -509,6 +743,9 @@ declare module 'aqualink' {
509
743
  _updateTimer: NodeJS.Timeout | null
510
744
  _hasDebugListeners: boolean
511
745
  _hasMoveListeners: boolean
746
+ _lastSentVoiceKey: string
747
+ _lastVoiceDataUpdate: number
748
+ _stateFlags: number
512
749
 
513
750
  // Methods
514
751
  setServerUpdate(data: VoiceServerUpdate['d']): void
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const fs = require('node:fs')
4
2
  const readline = require('node:readline')
5
3
  const { EventEmitter } = require('tseep')
@@ -23,7 +21,6 @@ const MAX_CONCURRENT_OPS = 10
23
21
  const BROKEN_PLAYER_TTL = 300000
24
22
  const FAILOVER_CLEANUP_TTL = 600000
25
23
  const PLAYER_BATCH_SIZE = 20
26
- const SEEK_DELAY = 120
27
24
  const RECONNECT_DELAY = 400
28
25
  const CACHE_VALID_TIME = 12000
29
26
  const NODE_TIMEOUT = 30000
@@ -31,8 +28,6 @@ const MAX_CACHE_SIZE = 20
31
28
  const MAX_FAILOVER_QUEUE = 50
32
29
  const MAX_REBUILD_LOCKS = 100
33
30
  const WRITE_BUFFER_SIZE = 100
34
- const MAX_QUEUE_SAVE = 10
35
- const MAX_TRACKS_RESTORE = 20
36
31
 
37
32
  const DEFAULT_OPTIONS = Object.freeze({
38
33
  shouldDeleteMessage: false,
@@ -181,7 +176,9 @@ class Aqua extends EventEmitter {
181
176
  }
182
177
  if (batch.length)
183
178
  queueMicrotask(() =>
184
- batch.forEach((p) => p.connection.resendVoiceUpdate())
179
+ batch.forEach((p) => {
180
+ p.connection.resendVoiceUpdate()
181
+ })
185
182
  )
186
183
  }
187
184
  }
@@ -257,7 +254,11 @@ class Aqua extends EventEmitter {
257
254
  const id = node.name || node.host
258
255
  const now = Date.now()
259
256
  const cached = this._nodeLoadCache.get(id)
260
- if (cached && now - cached.time < 5000) return cached.load
257
+ if (cached && now - cached.time < 5000) {
258
+ this._nodeLoadCache.delete(id)
259
+ this._nodeLoadCache.set(id, cached)
260
+ return cached.load
261
+ }
261
262
  const stats = node?.stats
262
263
  if (!stats) return 0
263
264
  const cores = Math.max(1, stats.cpu?.cores || 1)
@@ -267,11 +268,15 @@ class Aqua extends EventEmitter {
267
268
  (stats.playingPlayers || 0) * 0.75 +
268
269
  (stats.memory ? stats.memory.used / reservable : 0) * 40 +
269
270
  (node.rest?.calls || 0) * 0.001
270
- this._nodeLoadCache.set(id, { load, time: now })
271
- if (this._nodeLoadCache.size > MAX_CACHE_SIZE) {
272
- const first = this._nodeLoadCache.keys().next().value
273
- this._nodeLoadCache.delete(first)
271
+ if (this._nodeLoadCache.size >= MAX_CACHE_SIZE) {
272
+ const iterator = this._nodeLoadCache.keys()
273
+ while (this._nodeLoadCache.size >= MAX_CACHE_SIZE) {
274
+ const oldest = iterator.next().value
275
+ if (!oldest) break
276
+ this._nodeLoadCache.delete(oldest)
277
+ }
274
278
  }
279
+ this._nodeLoadCache.set(id, { load, time: now })
275
280
  return load
276
281
  }
277
282
 
@@ -528,7 +533,7 @@ class Aqua extends EventEmitter {
528
533
  return newPlayer
529
534
  } catch (error) {
530
535
  if (retry === maxRetries - 1) throw error
531
- await _functions.delay(retryDelay * Math.pow(1.5, retry))
536
+ await _functions.delay(retryDelay * 1.5 ** retry)
532
537
  }
533
538
  }
534
539
  }
@@ -858,7 +863,7 @@ class Aqua extends EventEmitter {
858
863
  buffer.push(JSON.stringify(data))
859
864
 
860
865
  if (buffer.length >= WRITE_BUFFER_SIZE) {
861
- const chunk = buffer.join('\n') + '\n'
866
+ const chunk = `${buffer.join('\n')}\n`
862
867
  buffer.length = 0
863
868
  if (!ws.write(chunk)) {
864
869
  drainPromise = drainPromise.then(
@@ -868,7 +873,7 @@ class Aqua extends EventEmitter {
868
873
  }
869
874
  }
870
875
 
871
- if (buffer.length) ws.write(buffer.join('\n') + '\n')
876
+ if (buffer.length) ws.write(`${buffer.join('\n')}\n`)
872
877
  await drainPromise
873
878
  await new Promise((resolve, reject) =>
874
879
  ws.end((err) => (err ? reject(err) : resolve()))
@@ -935,7 +940,7 @@ class Aqua extends EventEmitter {
935
940
  try {
936
941
  const gId = String(p.g)
937
942
  const existing = this.players.get(gId)
938
- if (existing && existing.playing) return
943
+ if (existing?.playing) return
939
944
 
940
945
  const player =
941
946
  existing ||
@@ -1068,7 +1073,6 @@ class Aqua extends EventEmitter {
1068
1073
  break
1069
1074
  }
1070
1075
  } catch {
1071
- continue
1072
1076
  }
1073
1077
  }
1074
1078
  } catch {
@@ -1079,4 +1083,4 @@ class Aqua extends EventEmitter {
1079
1083
  }
1080
1084
  }
1081
1085
 
1082
- module.exports = Aqua
1086
+ module.exports = Aqua
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const AqualinkEvents = {
4
2
  TrackStart: 'trackStart',
5
3
  TrackEnd: 'trackEnd',
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const { AqualinkEvents } = require('./AqualinkEvents')
4
2
 
5
3
  const POOL_SIZE = 12
@@ -216,8 +214,9 @@ class Connection {
216
214
  this._scheduleVoiceUpdate()
217
215
  }
218
216
 
219
- resendVoiceUpdate() {
217
+ resendVoiceUpdate(force = false) {
220
218
  if (this._destroyed || !this._hasValidVoiceData()) return false
219
+ if (force) this._lastSentVoiceKey = ''
221
220
  this._scheduleVoiceUpdate()
222
221
  return true
223
222
  }
@@ -456,17 +455,7 @@ class Connection {
456
455
  _makeVoiceKey() {
457
456
  const p = this._player
458
457
  const vol = p?.volume ?? 100
459
- return (
460
- (this.sessionId || '') +
461
- '|' +
462
- (this.token || '') +
463
- '|' +
464
- (this.endpoint || '') +
465
- '|' +
466
- (p?.voiceChannel || '') +
467
- '|' +
468
- vol
469
- )
458
+ return `${this.sessionId || ''}|${this.token || ''}|${this.endpoint || ''}|${p?.voiceChannel || ''}|${vol}`
470
459
  }
471
460
 
472
461
  _scheduleVoiceUpdate() {
@@ -538,11 +527,15 @@ class Connection {
538
527
  await this._rest.updatePlayer(payload)
539
528
  } catch (e) {
540
529
  if (e.statusCode === 404 || e.response?.statusCode === 404) {
530
+ const isSessionError = e.body?.message?.includes('sessionId') || false
541
531
  if (this._aqua) {
542
532
  this._aqua.emit(
543
533
  AqualinkEvents.Debug,
544
- `Player ${this._guildId} not found (404). Destroying.`
534
+ `[Aqua/Connection] Player ${this._guildId} not found (404)${isSessionError ? ' - Session invalid' : ''}. Destroying.`
545
535
  )
536
+ if (isSessionError && this._player?.nodes?._clearSession) {
537
+ this._player.nodes._clearSession()
538
+ }
546
539
  await this._aqua.destroyPlayer(this._guildId)
547
540
  }
548
541
  throw e
@@ -581,4 +574,4 @@ class Connection {
581
574
  }
582
575
  }
583
576
 
584
- module.exports = Connection
577
+ module.exports = Connection
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const FILTER_DEFAULTS = Object.freeze({
4
2
  karaoke: Object.freeze({
5
3
  level: 1,
@@ -41,6 +39,27 @@ const FILTER_KEYS = Object.freeze(
41
39
 
42
40
  const EMPTY_ARRAY = Object.freeze([])
43
41
 
42
+ const FILTER_POOL_SIZE = 16
43
+ const filterPool = {
44
+ pools: Object.fromEntries(Object.keys(FILTER_DEFAULTS).map(k => [k, []])),
45
+
46
+ acquire(type) {
47
+ const pool = this.pools[type]
48
+ if (pool && pool.length > 0) {
49
+ return pool.pop()
50
+ }
51
+ return { ...FILTER_DEFAULTS[type] }
52
+ },
53
+
54
+ release(type, obj) {
55
+ if (!obj || !this.pools[type]) return
56
+ const pool = this.pools[type]
57
+ if (pool.length < FILTER_POOL_SIZE) {
58
+ pool.push(obj)
59
+ }
60
+ }
61
+ }
62
+
44
63
  const _utils = Object.freeze({
45
64
  shallowEqual(current, defaults, override, keys) {
46
65
  if (!current) return false
@@ -73,6 +92,19 @@ const _utils = Object.freeze({
73
92
  const out = new Array(len)
74
93
  for (let i = 0; i < len; i++) out[i] = { band: i, gain }
75
94
  return out
95
+ },
96
+
97
+ mutateFilter(target, defaults, options, keys) {
98
+ let changed = false
99
+ for (let i = 0; i < keys.length; i++) {
100
+ const k = keys[i]
101
+ const newVal = k in options ? options[k] : defaults[k]
102
+ if (target[k] !== newVal) {
103
+ target[k] = newVal
104
+ changed = true
105
+ }
106
+ }
107
+ return changed
76
108
  }
77
109
  })
78
110
 
@@ -106,6 +138,11 @@ class Filters {
106
138
  }
107
139
 
108
140
  destroy() {
141
+ for (const [key, value] of Object.entries(this.filters)) {
142
+ if (value && typeof value === 'object' && key !== 'equalizer') {
143
+ filterPool.release(key, value)
144
+ }
145
+ }
109
146
  this._pendingUpdate = false
110
147
  this.player = null
111
148
  }
@@ -114,16 +151,27 @@ class Filters {
114
151
  const current = this.filters[filterName]
115
152
  if (!enabled) {
116
153
  if (current === null) return this
154
+ filterPool.release(filterName, current)
117
155
  this.filters[filterName] = null
156
+ this._dirty.add(filterName)
118
157
  return this._scheduleUpdate()
119
158
  }
120
159
 
121
160
  const defaults = FILTER_DEFAULTS[filterName]
122
161
  const keys = FILTER_KEYS[filterName]
123
- if (current && _utils.shallowEqual(current, defaults, options, keys))
124
- return this
125
162
 
126
- this.filters[filterName] = Object.assign({}, defaults, options)
163
+ if (current) {
164
+ if (_utils.shallowEqual(current, defaults, options, keys)) {
165
+ return this
166
+ }
167
+ _utils.mutateFilter(current, defaults, options, keys)
168
+ this._dirty.add(filterName)
169
+ return this._scheduleUpdate()
170
+ }
171
+
172
+ const newFilter = filterPool.acquire(filterName)
173
+ _utils.mutateFilter(newFilter, defaults, options, keys)
174
+ this.filters[filterName] = newFilter
127
175
  this._dirty.add(filterName)
128
176
  return this._scheduleUpdate()
129
177
  }
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const IS_BUN = !!(process?.isBun || process?.versions?.bun || globalThis.Bun)
4
2
  if (process && typeof process.isBun !== 'boolean') process.isBun = IS_BUN
5
3
 
@@ -263,7 +261,6 @@ class Node {
263
261
 
264
262
  _handleClose(code, reason) {
265
263
  this.connected = false
266
- const wasReady = this.state === NODE_STATE.READY
267
264
  this.state = this.isDestroyed ? NODE_STATE.IDLE : NODE_STATE.RECONNECTING
268
265
  this._isConnecting = false
269
266
 
@@ -343,7 +340,7 @@ class Node {
343
340
  _calcBackoff(attempt) {
344
341
  const baseBackoff =
345
342
  this.reconnectTimeout *
346
- Math.pow(Node.BACKOFF_MULTIPLIER, Math.min(attempt, 10))
343
+ Node.BACKOFF_MULTIPLIER ** Math.min(attempt, 10)
347
344
  const maxJitter = Math.min(
348
345
  Node.JITTER_MAX,
349
346
  baseBackoff * Node.JITTER_FACTOR
@@ -667,4 +664,4 @@ class Node {
667
664
  }
668
665
  }
669
666
 
670
- module.exports = Node
667
+ module.exports = Node
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const { EventEmitter } = require('tseep')
4
2
  const { AqualinkEvents } = require('./AqualinkEvents')
5
3
  const Connection = require('./Connection')
@@ -41,7 +39,6 @@ const WATCHDOG_INTERVAL = 15000
41
39
  const VOICE_DOWN_THRESHOLD = 10000
42
40
  const VOICE_ABANDON_MULTIPLIER = 12
43
41
  const RECONNECT_MAX = 15
44
- const RESUME_TIMEOUT = 5000
45
42
  const MUTE_TOGGLE_DELAY = 300
46
43
  const SEEK_DELAY = 800
47
44
  const PAUSE_DELAY = 1200
@@ -263,10 +260,16 @@ class Player extends EventEmitter {
263
260
  this.timestamp = _functions.isNum(s.time) ? s.time : Date.now()
264
261
 
265
262
  if (!this.connected) {
266
- if (!this._voiceDownSince) {
263
+ if (!this._voiceDownSince && !this._reconnecting && !this._voiceRecovering) {
267
264
  this._voiceDownSince = Date.now()
268
265
  this._createTimer(() => {
269
- if (this.connected || this.destroyed || this.nodes?.info?.isNodelink)
266
+ if (
267
+ this.connected ||
268
+ this.destroyed ||
269
+ this._reconnecting ||
270
+ this._voiceRecovering ||
271
+ this.nodes?.info?.isNodelink
272
+ )
270
273
  return
271
274
  this.connection.attemptResume()
272
275
  }, 1000)
@@ -387,7 +390,9 @@ class Player extends EventEmitter {
387
390
  this.nodes?.info?.isNodelink ||
388
391
  this.destroyed ||
389
392
  !this.voiceChannel ||
390
- this.connected
393
+ this.connected ||
394
+ this._reconnecting ||
395
+ this._voiceRecovering
391
396
  )
392
397
  return false
393
398
  if (
@@ -395,7 +400,7 @@ class Player extends EventEmitter {
395
400
  Date.now() - this._voiceDownSince < VOICE_DOWN_THRESHOLD
396
401
  )
397
402
  return false
398
- return !this._voiceRecovering && this.reconnectionRetries < RECONNECT_MAX
403
+ return this.reconnectionRetries < RECONNECT_MAX
399
404
  }
400
405
 
401
406
  async _voiceWatchdog() {
@@ -479,6 +484,7 @@ class Player extends EventEmitter {
479
484
  this.autoplayRetries = this.reconnectionRetries = 0
480
485
  if (!preserveReconnecting) this._reconnecting = false
481
486
  this._lastVoiceChannel = this.voiceChannel
487
+ this._lastTextChannel = this.textChannel
482
488
  this.voiceChannel = null
483
489
 
484
490
  if (
@@ -728,7 +734,7 @@ class Player extends EventEmitter {
728
734
  this.destroyed ||
729
735
  !this.isAutoplayEnabled ||
730
736
  !this.previous ||
731
- (this.queue && this.queue.size)
737
+ (this.queue?.size)
732
738
  )
733
739
  return this
734
740
  const prev = this.previous
@@ -947,18 +953,21 @@ class Player extends EventEmitter {
947
953
  }
948
954
 
949
955
  async socketClosed(player, track, payload) {
950
- if (this.destroyed) return
956
+ if (this.destroyed || this._reconnecting) return
957
+
951
958
  const code = payload?.code
952
959
  let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
953
960
  if (code === 4014 && this.connection?.isWaitingForDisconnect)
954
961
  isRecoverable = false
955
962
 
956
963
  if (code === 4015 && !this.nodes?.info?.isNodelink) {
964
+ this._reconnecting = true
957
965
  try {
958
966
  await this._attemptVoiceResume()
967
+ this._reconnecting = false
959
968
  return
960
969
  } catch {
961
- /* ignore */
970
+ this._reconnecting = false
962
971
  }
963
972
  }
964
973
 
@@ -974,8 +983,6 @@ class Player extends EventEmitter {
974
983
  if (code === 4022) this._suppressResumeUntil = Date.now() + 3000
975
984
  }
976
985
 
977
- if (this._reconnecting) return
978
-
979
986
  const aqua = this.aqua
980
987
  const vcId = _functions.toId(this.voiceChannel)
981
988
  const tcId = _functions.toId(this.textChannel)
@@ -1102,7 +1109,10 @@ class Player extends EventEmitter {
1102
1109
 
1103
1110
  set(key, value) {
1104
1111
  if (this.destroyed) return
1105
- ;(this._dataStore || (this._dataStore = new Map())).set(key, value)
1112
+ if (!this._dataStore) {
1113
+ this._dataStore = new Map()
1114
+ }
1115
+ this._dataStore.set(key, value)
1106
1116
  }
1107
1117
 
1108
1118
  get(key) {
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  class Queue {
4
2
  constructor() {
5
3
  this._items = []
@@ -41,10 +39,15 @@ class Queue {
41
39
  }
42
40
 
43
41
  shuffle() {
44
- // Compact first if needed
45
42
  if (this._head > 0) {
46
- this._items = this._items.slice(this._head)
47
- this._head = 0
43
+ if (this._head > 0) {
44
+ const len = this._items.length - this._head
45
+ for (let i = 0; i < len; i++) {
46
+ this._items[i] = this._items[this._head + i]
47
+ }
48
+ this._items.length = len
49
+ this._head = 0
50
+ }
48
51
  }
49
52
  for (let i = this._items.length - 1; i > 0; i--) {
50
53
  const j = Math.floor(Math.random() * (i + 1))
@@ -103,9 +106,12 @@ class Queue {
103
106
  const item = this._items[this._head]
104
107
  this._items[this._head] = undefined // Allow GC
105
108
  this._head++
106
- // Compact when head is > 50% of array to prevent unbounded growth
107
109
  if (this._head > 0 && this._head > this._items.length / 2) {
108
- this._items = this._items.slice(this._head)
110
+ const len = this._items.length - this._head
111
+ for (let i = 0; i < len; i++) {
112
+ this._items[i] = this._items[this._head + i]
113
+ }
114
+ this._items.length = len
109
115
  this._head = 0
110
116
  }
111
117
  return item
@@ -1,9 +1,7 @@
1
- 'use strict'
2
-
3
- const { Buffer } = require('buffer')
4
- const { Agent: HttpsAgent, request: httpsRequest } = require('https')
5
- const { Agent: HttpAgent, request: httpRequest } = require('http')
6
- const http2 = require('http2')
1
+ const { Buffer } = require('node:buffer')
2
+ const { Agent: HttpsAgent, request: httpsRequest } = require('node:https')
3
+ const { Agent: HttpAgent, request: httpRequest } = require('node:http')
4
+ const http2 = require('node:http2')
7
5
  const {
8
6
  createBrotliDecompress,
9
7
  createUnzip,
@@ -11,7 +9,12 @@ const {
11
9
  unzipSync,
12
10
  createZstdDecompress,
13
11
  zstdDecompressSync
14
- } = require('zlib')
12
+ } = require('node:zlib')
13
+
14
+ let autoplayModule = null
15
+ try {
16
+ autoplayModule = require('../handlers/autoplay')
17
+ } catch {}
15
18
 
16
19
  const unrefTimer = (t) => {
17
20
  try {
@@ -203,6 +206,10 @@ class Rest {
203
206
  this.agent = new (node.ssl ? HttpsAgent : HttpAgent)(opts)
204
207
  this.request = node.ssl ? httpsRequest : httpRequest
205
208
 
209
+ if (node.ssl && autoplayModule?.setSharedAgent) {
210
+ autoplayModule.setSharedAgent(this.agent)
211
+ }
212
+
206
213
  const origCreate = this.agent.createConnection.bind(this.agent)
207
214
  this.agent.createConnection = (options, cb) => {
208
215
  const socket = origCreate(options, cb)
@@ -847,4 +854,4 @@ class Rest {
847
854
  }
848
855
  }
849
856
 
850
- module.exports = Rest
857
+ module.exports = Rest
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const YT_ID_REGEX =
4
2
  /(?:[?&]v=|youtu\.be\/|\/embed\/|\/shorts\/)([A-Za-z0-9_-]{11})/
5
3
 
@@ -30,10 +28,12 @@ class Track {
30
28
  this.nodes = data.nodes || null
31
29
  this.requester = requester || null
32
30
  this._infoCache = null
31
+ this._artworkCache = undefined // undefined = not computed, null = computed but no artwork
33
32
  }
34
33
 
35
34
  get info() {
36
- return (this._infoCache ||= Object.freeze({
35
+ if (this._infoCache) return this._infoCache
36
+ this._infoCache = Object.freeze({
37
37
  identifier: this.identifier,
38
38
  isSeekable: this.isSeekable,
39
39
  position: this.position,
@@ -44,7 +44,8 @@ class Track {
44
44
  uri: this.uri,
45
45
  sourceName: this.sourceName,
46
46
  artworkUrl: this.artworkUrl || this._computeArtwork()
47
- }))
47
+ })
48
+ return this._infoCache
48
49
  }
49
50
 
50
51
  get length() {
@@ -52,7 +53,10 @@ class Track {
52
53
  }
53
54
 
54
55
  get thumbnail() {
55
- return this.artworkUrl || this._computeArtwork()
56
+ if (this.artworkUrl) return this.artworkUrl
57
+ if (this._artworkCache !== undefined) return this._artworkCache
58
+ this._artworkCache = this._computeArtwork()
59
+ return this._artworkCache
56
60
  }
57
61
 
58
62
  async resolve(aqua, opts = {}) {
@@ -140,10 +144,13 @@ class Track {
140
144
 
141
145
  _computeArtwork() {
142
146
  if (this.artworkUrl) return this.artworkUrl
147
+ if (this._artworkCache !== undefined) return this._artworkCache
143
148
  const id = this.identifier || (this.uri && YT_ID_REGEX.exec(this.uri)?.[1])
144
149
  if (id && this.sourceName?.includes('youtube')) {
145
- return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`
150
+ this._artworkCache = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`
151
+ return this._artworkCache
146
152
  }
153
+ this._artworkCache = null
147
154
  return null
148
155
  }
149
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.19.0",
3
+ "version": "2.19.1",
4
4
  "description": "An Lavalink/Nodelink client, focused in pure performance and features",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",