aqualink 2.15.1 → 2.16.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.
@@ -29,7 +29,6 @@ const RECONNECT_DELAY = 400
29
29
  const CACHE_VALID_TIME = 12000
30
30
  const NODE_TIMEOUT = 30000
31
31
  const MAX_CACHE_SIZE = 20
32
- const MAX_BROKEN_PLAYERS = 50
33
32
  const MAX_FAILOVER_QUEUE = 50
34
33
  const MAX_REBUILD_LOCKS = 100
35
34
  const WRITE_BUFFER_SIZE = 100
@@ -279,7 +278,6 @@ class Aqua extends EventEmitter {
279
278
  this._brokenPlayers.set(player.guildId, state)
280
279
  }
281
280
  }
282
- this._trimBrokenPlayers()
283
281
  }
284
282
 
285
283
  async _rebuildBrokenPlayers(node) {
@@ -786,7 +784,6 @@ class Aqua extends EventEmitter {
786
784
  }
787
785
  }
788
786
 
789
- this._trimBrokenPlayers()
790
787
  if (this._failoverQueue.size > MAX_FAILOVER_QUEUE) this._failoverQueue.clear()
791
788
  if (this._rebuildLocks.size > MAX_REBUILD_LOCKS) this._rebuildLocks.clear()
792
789
 
@@ -795,12 +792,6 @@ class Aqua extends EventEmitter {
795
792
  if (!this.nodeMap.has(id)) this._nodeStates.delete(id)
796
793
  }
797
794
  }
798
-
799
- _trimBrokenPlayers() {
800
- if (this._brokenPlayers.size <= MAX_BROKEN_PLAYERS) return
801
- const sorted = [...this._brokenPlayers.entries()].sort((a, b) => a[1].brokenAt - b[1].brokenAt)
802
- sorted.slice(0, sorted.length - MAX_BROKEN_PLAYERS).forEach(([id]) => this._brokenPlayers.delete(id))
803
- }
804
795
  }
805
796
 
806
797
  module.exports = Aqua
@@ -31,7 +31,13 @@ const AqualinkEvents = {
31
31
  PlayerCreate: 'playerCreate',
32
32
  PlayerDestroy: 'playerDestroy',
33
33
  PlayersRebuilt: 'playersRebuilt',
34
+ VolumeChanged: 'volumeChanged',
35
+ FiltersChanged: 'filtersChanged',
36
+ Seek: 'seek',
37
+ PlayerCreated: 'playerCreated',
38
+ PlayerConnected: 'playerConnected',
39
+ PlayerDestroyed: 'playerDestroyed',
34
40
  PlayerMigrated: 'playerMigrated'
35
41
  };
36
42
 
37
- module.exports = { AqualinkEvents };
43
+ module.exports = { AqualinkEvents };
@@ -18,6 +18,12 @@ const EVENT_HANDLERS = Object.freeze({
18
18
  WebSocketClosedEvent: 'socketClosed',
19
19
  LyricsLineEvent: 'lyricsLine',
20
20
  LyricsFoundEvent: 'lyricsFound',
21
+ VolumeChangedEvent: 'volumeChanged',
22
+ FiltersChangedEvent: 'filtersChanged',
23
+ SeekEvent: 'seekEvent',
24
+ PlayerCreatedEvent: 'playerCreated',
25
+ PlayerConnectedEvent: 'playerConnected',
26
+ PlayerDestroyedEvent: 'playerDestroyed',
21
27
  LyricsNotFoundEvent: 'lyricsNotFound'
22
28
  })
23
29
 
@@ -301,7 +307,7 @@ class Player extends EventEmitter {
301
307
  this.playing = true
302
308
  this.paused = false
303
309
  this.position = 0
304
- await this.batchUpdatePlayer({guildId: this.guildId, encodedTrack: this.current.track}, true)
310
+ await this.batchUpdatePlayer({guildId: this.guildId, track: { encoded: this.current.track} }, true)
305
311
  return this
306
312
  } catch (error) {
307
313
  this.aqua.emit(AqualinkEvents.Error, error)
@@ -430,8 +436,8 @@ class Player extends EventEmitter {
430
436
  this._dataStore.clear()
431
437
  this._dataStore = null
432
438
  }
433
-
434
- if (this.current?.dispose) this.current.dispose()
439
+ // if autoResume is enabled, ig it will need to be keeped in case the connection dies somehow, so don't dispose it
440
+ if (this.current?.dispose && !this.aqua.options.autoResume) this.current.dispose()
435
441
  this.connection = this.filters = this.current = this.autoplaySeed = null
436
442
 
437
443
  if (!skipRemote) {
@@ -449,17 +455,16 @@ class Player extends EventEmitter {
449
455
  pause(paused) {
450
456
  if (this.destroyed || this.paused === !!paused) return this
451
457
  this.paused = !!paused
452
- this.batchUpdatePlayer({guildId: this.guildId, paused: this.paused}, true)
458
+ this.batchUpdatePlayer({guildId: this.guildId, paused: this.paused}, true).catch(() => {})
453
459
  return this
454
460
  }
455
461
 
456
462
  seek(position) {
457
463
  if (this.destroyed || !this.playing || !_functions.isNum(position)) return this
458
464
  const len = this.current?.info?.length || 0
459
- const pos = position === 0 ? 0 : this.position + position
460
- const clamped = len ? Math.min(Math.max(pos, 0), len) : Math.max(pos, 0)
465
+ const clamped = len ? Math.min(Math.max(position, 0), len) : Math.max(position, 0)
461
466
  this.position = clamped
462
- this.batchUpdatePlayer({guildId: this.guildId, position: clamped}, true)
467
+ this.batchUpdatePlayer({guildId: this.guildId, position: clamped}, true).catch(() => {})
463
468
  return this
464
469
  }
465
470
 
@@ -467,7 +472,7 @@ class Player extends EventEmitter {
467
472
  if (this.destroyed || !this.playing) return this
468
473
  this.playing = this.paused = false
469
474
  this.position = 0
470
- this.batchUpdatePlayer({guildId: this.guildId, encodedTrack: null}, true)
475
+ this.batchUpdatePlayer({guildId: this.guildId, track: {encoded: null}}, true).catch(() => {})
471
476
  return this
472
477
  }
473
478
 
@@ -475,7 +480,7 @@ class Player extends EventEmitter {
475
480
  const vol = _functions.clamp(volume)
476
481
  if (this.destroyed || this.volume === vol) return this
477
482
  this.volume = vol
478
- this.batchUpdatePlayer({guildId: this.guildId, volume: vol})
483
+ this.batchUpdatePlayer({guildId: this.guildId, volume: vol}).catch(() => {})
479
484
  return this
480
485
  }
481
486
 
@@ -492,7 +497,7 @@ class Player extends EventEmitter {
492
497
  const id = _functions.toId(channel)
493
498
  if (!id) throw new TypeError('Invalid text channel')
494
499
  this.textChannel = id
495
- this.batchUpdatePlayer({guildId: this.guildId, text_channel: id})
500
+ this.batchUpdatePlayer({guildId: this.guildId, text_channel: id}).catch(() => {})
496
501
  return this
497
502
  }
498
503
 
@@ -552,6 +557,11 @@ class Player extends EventEmitter {
552
557
  return null
553
558
  }
554
559
 
560
+ async getLoadLyrics(encodedTrack) {
561
+ if (this.destroyed || !this.nodes?.rest) return null
562
+ return this.nodes.rest.getLoadLyrics(encodedTrack)
563
+ }
564
+
555
565
  subscribeLiveLyrics() {
556
566
  return this.destroyed ? Promise.reject(new Error('Player destroyed')) : this.nodes?.rest?.subscribeLiveLyrics(this.guildId, false)
557
567
  }
@@ -821,6 +831,21 @@ class Player extends EventEmitter {
821
831
  this.aqua.emit(AqualinkEvents.LyricsLine, this, track, payload)
822
832
  }
823
833
 
834
+ async volumeChanged(player, track, payload) {
835
+ if (this.destroyed) return
836
+ this.aqua.emit(AqualinkEvents.VolumeChanged, this, track, payload)
837
+ }
838
+
839
+ async filtersChanged(player, track, payload) {
840
+ if (this.destroyed) return
841
+ this.aqua.emit(AqualinkEvents.FiltersChanged, this, track, payload)
842
+ }
843
+
844
+ async seekEvent(player, track, payload) {
845
+ if (this.destroyed) return
846
+ this.aqua.emit(AqualinkEvents.Seek, this, track, payload)
847
+ }
848
+
824
849
  async lyricsFound(player, track, payload) {
825
850
  if (this.destroyed) return
826
851
  this.aqua.emit(AqualinkEvents.LyricsFound, this, track, payload)
@@ -831,6 +856,20 @@ class Player extends EventEmitter {
831
856
  this.aqua.emit(AqualinkEvents.LyricsNotFound, this, track, payload)
832
857
  }
833
858
 
859
+ async playerCreated(player, track, payload) {
860
+ if (this.destroyed) return
861
+ this.aqua.emit(AqualinkEvents.PlayerCreated, this, payload)
862
+ }
863
+
864
+ async playerConnected(player, track, payload) {
865
+ if (this.destroyed) return
866
+ this.aqua.emit(AqualinkEvents.PlayerConnected, this, payload)
867
+ }
868
+ async playerDestroyed(player, track, payload) {
869
+ if (this.destroyed) return
870
+ this.aqua.emit(AqualinkEvents.PlayerDestroyed, this, payload)
871
+ }
872
+
834
873
  _handleAquaPlayerMove(oldChannel, newChannel) {
835
874
  if (_functions.toId(oldChannel) !== _functions.toId(this.voiceChannel)) return
836
875
  this.voiceChannel = _functions.toId(newChannel)
@@ -1,33 +1,28 @@
1
- 'use strict'
2
-
3
- const { Buffer } = require('node:buffer')
4
- const { Agent: HttpsAgent, request: httpsRequest } = require('node:https')
5
- const { Agent: HttpAgent, request: httpRequest } = require('node:http')
6
- const http2 = require('node:http2')
7
- const { createBrotliDecompress, createUnzip } = require('node:zlib')
8
-
9
- const privateData = new WeakMap()
10
-
11
- const isValidBase64Char = code =>
12
- (code >= 65 && code <= 90) || (code >= 97 && code <= 122) ||
13
- (code >= 48 && code <= 57) || code === 43 || code === 47 ||
14
- code === 61 || code === 95 || code === 45
15
-
16
- const isValidBase64 = str => {
17
- if (typeof str !== 'string' || !str) return false
18
- const len = str.length
19
- if (len % 4 === 1) return false
20
- for (let i = 0; i < len; i++) {
21
- if (!isValidBase64Char(str.charCodeAt(i))) return false
22
- }
23
- return true
24
- }
1
+ const { Buffer } = require('buffer')
2
+ const { Agent: HttpsAgent, request: httpsRequest } = require('https')
3
+ const { Agent: HttpAgent, request: httpRequest } = require('http')
4
+ const http2 = require('http2')
5
+ const { createBrotliDecompress, createUnzip, brotliDecompressSync, unzipSync } = require('zlib')
6
+
7
+ const BASE64_LOOKUP = new Uint8Array(256)
8
+ for (let i = 65; i <= 90; i++) BASE64_LOOKUP[i] = 1
9
+ for (let i = 97; i <= 122; i++) BASE64_LOOKUP[i] = 1
10
+ for (let i = 48; i <= 57; i++) BASE64_LOOKUP[i] = 1
11
+ BASE64_LOOKUP[43] = BASE64_LOOKUP[47] = BASE64_LOOKUP[61] = BASE64_LOOKUP[95] = BASE64_LOOKUP[45] = 1
12
+
13
+ const ENCODING_BR = 1
14
+ const ENCODING_GZIP = 2
15
+ const ENCODING_DEFLATE = 3
16
+ const ENCODING_NONE = 0
25
17
 
26
18
  const MAX_RESPONSE_SIZE = 10485760
19
+ const SMALL_RESPONSE_THRESHOLD = 512
20
+ const COMPRESSION_MIN_SIZE = 1024
27
21
  const API_VERSION = 'v4'
28
22
  const UTF8_ENCODING = 'utf8'
29
23
  const JSON_CONTENT_TYPE = 'application/json'
30
24
  const HTTP2_THRESHOLD = 1024
25
+ const MAX_HEADER_POOL = 10
31
26
 
32
27
  const ERRORS = Object.freeze({
33
28
  NO_SESSION: new Error('Session ID required'),
@@ -37,6 +32,25 @@ const ERRORS = Object.freeze({
37
32
  RESPONSE_ABORTED: new Error('Response aborted')
38
33
  })
39
34
 
35
+ const _isValidBase64 = (str) => {
36
+ if (typeof str !== 'string' || !str) return false
37
+ const len = str.length
38
+ if (len % 4 === 1) return false
39
+ for (let i = 0; i < len; i++) {
40
+ if (!BASE64_LOOKUP[str.charCodeAt(i)]) return false
41
+ }
42
+ return true
43
+ }
44
+
45
+ const _getEncodingType = (encodingHeader) => {
46
+ if (!encodingHeader) return ENCODING_NONE
47
+ const firstChar = encodingHeader.charCodeAt(0)
48
+ if (firstChar === 98 && encodingHeader.startsWith('br')) return ENCODING_BR
49
+ if (firstChar === 103 && encodingHeader.startsWith('gzip')) return ENCODING_GZIP
50
+ if (firstChar === 100 && encodingHeader.startsWith('deflate')) return ENCODING_DEFLATE
51
+ return ENCODING_NONE
52
+ }
53
+
40
54
  class Rest {
41
55
  constructor(aqua, node) {
42
56
  this.aqua = aqua
@@ -48,7 +62,6 @@ class Rest {
48
62
  const host = node.host.includes(':') && !node.host.startsWith('[') ? `[${node.host}]` : node.host
49
63
  this.baseUrl = `${protocol}//${host}:${node.port}`
50
64
  this._apiBase = `/${API_VERSION}`
51
- this._sessionPath = this.sessionId ? `${this._apiBase}/sessions/${this.sessionId}` : null
52
65
 
53
66
  this._endpoints = Object.freeze({
54
67
  loadtracks: `${this._apiBase}/loadtracks?identifier=`,
@@ -68,7 +81,7 @@ class Rest {
68
81
  this.defaultHeaders = Object.freeze({
69
82
  Authorization: String(node.auth || node.password || ''),
70
83
  Accept: 'application/json, */*;q=0.5',
71
- 'Accept-Encoding': 'gzip, deflate, br',
84
+ 'Accept-Encoding': 'br, gzip, deflate',
72
85
  'User-Agent': `Aqualink/${aqua?.version || '1.0'} (Node.js ${process.version})`
73
86
  })
74
87
 
@@ -77,10 +90,6 @@ class Rest {
77
90
  this.useHttp2 = !!(aqua?.options?.useHttp2)
78
91
  this._h2 = null
79
92
  this._h2CleanupTimer = null
80
-
81
- privateData.set(this, {
82
- cleanupCallbacks: new Set()
83
- })
84
93
  }
85
94
 
86
95
  _setupAgent(node) {
@@ -105,26 +114,31 @@ class Rest {
105
114
 
106
115
  this.agent = new (node.ssl ? HttpsAgent : HttpAgent)(agentOpts)
107
116
  this.request = node.ssl ? httpsRequest : httpRequest
117
+
118
+ const origCreateConnection = this.agent.createConnection
119
+ this.agent.createConnection = (options, callback) => {
120
+ const socket = origCreateConnection.call(this.agent, options, callback)
121
+ socket.setNoDelay(true)
122
+ socket.setKeepAlive(true, 500)
123
+ socket.unref?.()
124
+ return socket
125
+ }
108
126
  }
109
127
 
110
128
  setSessionId(sessionId) {
111
129
  this.sessionId = sessionId
112
- this._sessionPath = sessionId ? `${this._apiBase}/sessions/${sessionId}` : null
113
130
  }
114
131
 
115
132
  _getSessionPath() {
116
- if (!this._sessionPath) {
117
- if (!this.sessionId) throw ERRORS.NO_SESSION
118
- this._sessionPath = `${this._apiBase}/sessions/${this.sessionId}`
119
- }
120
- return this._sessionPath
133
+ if (!this.sessionId) throw ERRORS.NO_SESSION
134
+ return `${this._apiBase}/sessions/${this.sessionId}`
121
135
  }
122
136
 
123
137
  _buildHeaders(hasPayload, payloadLength) {
124
138
  if (!hasPayload) return this.defaultHeaders
125
139
 
126
140
  let headers = this._headerPool.pop()
127
- if (!headers) headers = {}
141
+ if (!headers) headers = Object.create(null)
128
142
 
129
143
  headers.Authorization = this.defaultHeaders.Authorization
130
144
  headers.Accept = this.defaultHeaders.Accept
@@ -137,11 +151,13 @@ class Rest {
137
151
  }
138
152
 
139
153
  _returnHeadersToPool(headers) {
140
- if (headers !== this.defaultHeaders && this._headerPool.length < 10) {
141
- const keys = Object.keys(headers)
142
- for (let i = 0; i < keys.length; i++) {
143
- delete headers[keys[i]]
144
- }
154
+ if (headers !== this.defaultHeaders && this._headerPool.length < MAX_HEADER_POOL) {
155
+ headers.Authorization = null
156
+ headers.Accept = null
157
+ headers['Accept-Encoding'] = null
158
+ headers['User-Agent'] = null
159
+ headers['Content-Type'] = null
160
+ headers['Content-Length'] = null
145
161
  this._headerPool.push(headers)
146
162
  }
147
163
  }
@@ -151,7 +167,6 @@ class Rest {
151
167
  const payload = body === undefined ? undefined : (typeof body === 'string' ? body : JSON.stringify(body))
152
168
  const payloadLength = payload ? Buffer.byteLength(payload, UTF8_ENCODING) : 0
153
169
  const headers = this._buildHeaders(!!payload, payloadLength)
154
-
155
170
  const useHttp2 = this.useHttp2 && payloadLength >= HTTP2_THRESHOLD
156
171
 
157
172
  try {
@@ -165,29 +180,31 @@ class Rest {
165
180
 
166
181
  _makeHttp1Request(method, url, headers, payload) {
167
182
  return new Promise((resolve, reject) => {
168
- let req
169
- let timeoutId
183
+ let req = null
184
+ let timeoutId = null
170
185
  let resolved = false
171
- const chunks = []
186
+ let chunks = []
172
187
  let totalSize = 0
188
+ let preallocatedBuffer = null
173
189
 
174
190
  const cleanup = () => {
175
191
  if (timeoutId) {
176
192
  clearTimeout(timeoutId)
177
193
  timeoutId = null
178
194
  }
179
- chunks.length = 0
195
+ chunks = null
196
+ preallocatedBuffer = null
180
197
  }
181
198
 
182
199
  const complete = (isSuccess, value) => {
183
- if (resolved) return
200
+ if (resolved) return;
184
201
  resolved = true
185
202
  cleanup()
186
203
  if (req && !isSuccess) req.destroy()
187
204
  isSuccess ? resolve(value) : reject(value)
188
205
  }
189
206
 
190
- req = this.request(url, { method, headers, agent: this.agent, timeout: this.timeout }, res => {
207
+ req = this.request(url, { method, headers, agent: this.agent, timeout: this.timeout }, (res) => {
191
208
  cleanup()
192
209
 
193
210
  const status = res.statusCode
@@ -195,38 +212,123 @@ class Rest {
195
212
 
196
213
  const contentLength = res.headers['content-length']
197
214
  if (contentLength === '0') return res.resume(), complete(true, null)
198
- if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
215
+
216
+ const clInt = contentLength ? parseInt(contentLength, 10) : 0
217
+ if (clInt > MAX_RESPONSE_SIZE) {
218
+ res.resume()
199
219
  return complete(false, ERRORS.RESPONSE_TOO_LARGE)
200
220
  }
201
221
 
202
- const encoding = (res.headers['content-encoding'] || '').split(',')[0].trim()
222
+ const encodingType = _getEncodingType(res.headers['content-encoding'])
223
+
224
+ if (clInt > 0 && clInt < SMALL_RESPONSE_THRESHOLD && encodingType === ENCODING_NONE) {
225
+ res.once('data', (chunk) => {
226
+ const text = chunk.toString(UTF8_ENCODING)
227
+ let result = text
228
+ const contentType = res.headers['content-type'] || ''
229
+
230
+ if (contentType.charCodeAt(0) === 97 && contentType.includes('application/json')) {
231
+ try {
232
+ result = JSON.parse(text)
233
+ } catch (err) {
234
+ return complete(false, new Error(`JSON parse error: ${err.message}`))
235
+ }
236
+ }
237
+
238
+ if (status >= 400) {
239
+ const error = new Error(`HTTP ${status} ${method} ${url}`)
240
+ error.statusCode = status
241
+ error.statusMessage = res.statusMessage
242
+ error.headers = res.headers
243
+ error.body = result
244
+ error.url = url
245
+ return complete(false, error)
246
+ }
247
+
248
+ complete(true, result)
249
+ })
250
+ res.once('error', (err) => complete(false, err))
251
+ return;
252
+ }
253
+
254
+ if (clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
255
+ preallocatedBuffer = Buffer.allocUnsafe(clInt)
256
+ chunks = []
257
+ } else {
258
+ chunks = []
259
+ }
260
+
203
261
  let stream = res
204
262
 
205
- if (encoding === 'br' || encoding === 'gzip' || encoding === 'deflate') {
206
- const decompressor = encoding === 'br' ? createBrotliDecompress() : createUnzip()
207
- decompressor.once('error', err => complete(false, err))
208
- res.pipe(decompressor)
209
- stream = decompressor
263
+ if (encodingType !== ENCODING_NONE) {
264
+ if (clInt > 0 && clInt < COMPRESSION_MIN_SIZE) {
265
+ const compressedChunks = []
266
+ res.on('data', (chunk) => compressedChunks.push(chunk))
267
+ res.once('end', () => {
268
+ try {
269
+ const compressed = Buffer.concat(compressedChunks)
270
+ const decompressed = encodingType === ENCODING_BR
271
+ ? brotliDecompressSync(compressed)
272
+ : unzipSync(compressed)
273
+ const text = decompressed.toString(UTF8_ENCODING)
274
+ let result = text
275
+ const contentType = res.headers['content-type'] || ''
276
+
277
+ if (contentType.charCodeAt(0) === 97 && contentType.includes('application/json')) {
278
+ result = JSON.parse(text)
279
+ }
280
+
281
+ if (status >= 400) {
282
+ const error = new Error(`HTTP ${status} ${method} ${url}`)
283
+ error.statusCode = status
284
+ error.statusMessage = res.statusMessage
285
+ error.headers = res.headers
286
+ error.body = result
287
+ error.url = url
288
+ return complete(false, error)
289
+ }
290
+
291
+ complete(true, result)
292
+ } catch (err) {
293
+ complete(false, err)
294
+ }
295
+ })
296
+ res.once('error', (err) => complete(false, err))
297
+ return;
298
+ } else {
299
+ const decompressor = encodingType === ENCODING_BR ? createBrotliDecompress() : createUnzip()
300
+ decompressor.once('error', (err) => complete(false, err))
301
+ res.pipe(decompressor)
302
+ stream = decompressor
303
+ }
210
304
  }
211
305
 
212
306
  res.once('aborted', () => complete(false, ERRORS.RESPONSE_ABORTED))
213
- res.once('error', err => complete(false, err))
214
-
215
- stream.on('data', chunk => {
216
- totalSize += chunk.length
217
- if (totalSize > MAX_RESPONSE_SIZE) return complete(false, ERRORS.RESPONSE_TOO_LARGE)
218
- chunks.push(chunk)
307
+ res.once('error', (err) => complete(false, err))
308
+
309
+ stream.on('data', (chunk) => {
310
+ if (preallocatedBuffer) {
311
+ chunk.copy(preallocatedBuffer, totalSize)
312
+ totalSize += chunk.length
313
+ } else {
314
+ totalSize += chunk.length
315
+ if (totalSize > MAX_RESPONSE_SIZE) return complete(false, ERRORS.RESPONSE_TOO_LARGE)
316
+ chunks.push(chunk)
317
+ }
219
318
  })
220
319
 
221
320
  stream.once('end', () => {
222
321
  if (totalSize === 0) return complete(true, null)
223
322
 
224
- const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize)
323
+ const buffer = preallocatedBuffer
324
+ ? preallocatedBuffer.slice(0, totalSize)
325
+ : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize))
225
326
  const text = buffer.toString(UTF8_ENCODING)
226
-
227
327
  let result = text
228
328
  const contentType = res.headers['content-type'] || ''
229
- if (contentType.includes('application/json')) {try {
329
+
330
+ if (contentType.charCodeAt(0) === 97 && contentType.includes('application/json')) {
331
+ try {
230
332
  result = JSON.parse(text)
231
333
  } catch (err) {
232
334
  return complete(false, new Error(`JSON parse error: ${err.message}`))
@@ -247,41 +349,43 @@ class Rest {
247
349
  })
248
350
  })
249
351
 
250
- req.once('error', err => complete(false, err))
251
- req.once('socket', socket => {
252
- socket.setNoDelay(true)
253
- socket.setKeepAlive(true, 500)
254
- socket.unref?.()
255
- })
256
-
352
+ req.once('error', (err) => complete(false, err))
257
353
  timeoutId = setTimeout(() => complete(false, new Error(`Request timeout: ${this.timeout}ms`)), this.timeout)
258
354
  payload ? req.end(payload) : req.end()
259
355
  })
260
356
  }
261
357
 
262
- async _makeHttp2Request(method, path, headers, payload) {
358
+ _getOrCreateHttp2Session() {
263
359
  if (!this._h2 || this._h2.closed || this._h2.destroyed) {
264
- this._closeHttp2Session()
360
+ if (this._h2) this._h2 = null
265
361
 
266
362
  this._h2 = http2.connect(this.baseUrl)
267
363
 
268
364
  if (this._h2CleanupTimer) clearTimeout(this._h2CleanupTimer)
269
365
  this._h2CleanupTimer = setTimeout(() => this._closeHttp2Session(), 60000)
366
+ this._h2CleanupTimer.unref()
270
367
 
271
- this._h2.once('error', () => this._closeHttp2Session())
272
- this._h2.once('close', () => this._closeHttp2Session())
368
+ this._h2.once('error', () => { this._h2 = null })
369
+ this._h2.once('close', () => { this._h2 = null })
273
370
  this._h2.socket?.unref?.()
274
371
  }
275
372
 
373
+ return this._h2
374
+ }
375
+
376
+ async _makeHttp2Request(method, path, headers, payload) {
377
+ const session = this._getOrCreateHttp2Session()
378
+
276
379
  return new Promise((resolve, reject) => {
277
- let req
278
- let timeoutId
380
+ let req = null
381
+ let timeoutId = null
279
382
  let resolved = false
280
- const chunks = []
383
+ let chunks = []
281
384
  let totalSize = 0
385
+ let preallocatedBuffer = null
282
386
 
283
387
  const complete = (isSuccess, value) => {
284
- if (resolved) return
388
+ if (resolved) return;
285
389
  resolved = true
286
390
 
287
391
  if (timeoutId) {
@@ -289,12 +393,13 @@ class Rest {
289
393
  timeoutId = null
290
394
  }
291
395
 
292
- chunks.length = 0
396
+ chunks = null
397
+ preallocatedBuffer = null
293
398
  if (req && !isSuccess) req.close(http2.constants.NGHTTP2_CANCEL)
294
399
  isSuccess ? resolve(value) : reject(value)
295
400
  }
296
401
 
297
- const h = {
402
+ const h2Headers = {
298
403
  ':method': method,
299
404
  ':path': path,
300
405
  Authorization: headers.Authorization,
@@ -303,12 +408,12 @@ class Rest {
303
408
  'User-Agent': headers['User-Agent']
304
409
  }
305
410
 
306
- if (headers['Content-Type']) h['Content-Type'] = headers['Content-Type']
307
- if (headers['Content-Length']) h['Content-Length'] = headers['Content-Length']
411
+ if (headers['Content-Type']) h2Headers['Content-Type'] = headers['Content-Type']
412
+ if (headers['Content-Length']) h2Headers['Content-Length'] = headers['Content-Length']
308
413
 
309
- req = this._h2.request(h)
414
+ req = session.request(h2Headers)
310
415
 
311
- req.once('response', respHeaders => {
416
+ req.once('response', (respHeaders) => {
312
417
  if (timeoutId) {
313
418
  clearTimeout(timeoutId)
314
419
  timeoutId = null
@@ -316,33 +421,48 @@ class Rest {
316
421
 
317
422
  const status = respHeaders[':status'] || 0
318
423
  const cl = respHeaders['content-length']
424
+
319
425
  if (status === 204 || cl === '0') return req.resume(), complete(true, null)
320
- if (cl && parseInt(cl, 10) > MAX_RESPONSE_SIZE) {
321
- return req.resume(), complete(false, ERRORS.RESPONSE_TOO_LARGE)
426
+
427
+ const clInt = cl ? parseInt(cl, 10) : 0
428
+ if (clInt > MAX_RESPONSE_SIZE) {
429
+ req.resume()
430
+ return complete(false, ERRORS.RESPONSE_TOO_LARGE)
431
+ }
432
+
433
+ if (clInt > 0 && clInt <= MAX_RESPONSE_SIZE) {
434
+ preallocatedBuffer = Buffer.allocUnsafe(clInt)
322
435
  }
323
436
 
324
- const enc = (respHeaders['content-encoding'] || '').split(',')[0].trim()
325
- const decompressor = (enc === 'br' || enc === 'gzip' || enc === 'deflate')
326
- ? (enc === 'br' ? createBrotliDecompress() : createUnzip())
437
+ const encodingType = _getEncodingType(respHeaders['content-encoding'])
438
+ const decompressor = encodingType !== ENCODING_NONE
439
+ ? (encodingType === ENCODING_BR ? createBrotliDecompress() : createUnzip())
327
440
  : null
328
441
  const stream = decompressor ? req.pipe(decompressor) : req
329
442
 
330
- if (decompressor) decompressor.once('error', err => complete(false, err))
331
- req.once('error', err => complete(false, err))
332
-
333
- stream.on('data', chunk => {
334
- totalSize += chunk.length
335
- if (totalSize > MAX_RESPONSE_SIZE) return complete(false, ERRORS.RESPONSE_TOO_LARGE)
336
- chunks.push(chunk)
443
+ if (decompressor) decompressor.once('error', (err) => complete(false, err))
444
+ req.once('error', (err) => complete(false, err))
445
+
446
+ stream.on('data', (chunk) => {
447
+ if (preallocatedBuffer) {
448
+ chunk.copy(preallocatedBuffer, totalSize)
449
+ totalSize += chunk.length
450
+ } else {
451
+ totalSize += chunk.length
452
+ if (totalSize > MAX_RESPONSE_SIZE) return complete(false, ERRORS.RESPONSE_TOO_LARGE)
453
+ chunks.push(chunk)
454
+ }
337
455
  })
338
456
 
339
457
  stream.once('end', () => {
340
458
  if (totalSize === 0) return complete(true, null)
341
459
 
342
- const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize)
460
+ const buffer = preallocatedBuffer
461
+ ? preallocatedBuffer.slice(0, totalSize)
462
+ : (chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize))
343
463
  const text = buffer.toString(UTF8_ENCODING)
464
+ let result = null
344
465
 
345
- let result
346
466
  try {
347
467
  result = JSON.parse(text)
348
468
  } catch (err) {
@@ -403,14 +523,14 @@ class Rest {
403
523
  }
404
524
 
405
525
  async decodeTrack(encodedTrack) {
406
- if (!isValidBase64(encodedTrack)) throw ERRORS.INVALID_TRACK
526
+ if (!_isValidBase64(encodedTrack)) throw ERRORS.INVALID_TRACK
407
527
  return this.makeRequest('GET', `${this._endpoints.decodetrack}${encodeURIComponent(encodedTrack)}`)
408
528
  }
409
529
 
410
530
  async decodeTracks(encodedTracks) {
411
531
  if (!Array.isArray(encodedTracks) || encodedTracks.length === 0) throw ERRORS.INVALID_TRACKS
412
532
  for (let i = 0; i < encodedTracks.length; i++) {
413
- if (!isValidBase64(encodedTracks[i])) throw ERRORS.INVALID_TRACKS
533
+ if (!_isValidBase64(encodedTracks[i])) throw ERRORS.INVALID_TRACKS
414
534
  }
415
535
  return this.makeRequest('POST', this._endpoints.decodetracks, encodedTracks)
416
536
  }
@@ -442,7 +562,7 @@ class Rest {
442
562
  async getLyrics({ track, skipTrackSource = false }) {
443
563
  const guildId = track?.guild_id ?? track?.guildId
444
564
  const encoded = track?.encoded
445
- const hasEncoded = typeof encoded === 'string' && encoded.length > 0 && isValidBase64(encoded)
565
+ const hasEncoded = typeof encoded === 'string' && encoded.length > 0 && _isValidBase64(encoded)
446
566
  const title = track?.info?.title
447
567
 
448
568
  if (!track || (!guildId && !hasEncoded && !title)) {
@@ -505,15 +625,6 @@ class Rest {
505
625
  }
506
626
 
507
627
  destroy() {
508
- const privateInfo = privateData.get(this)
509
-
510
- if (privateInfo?.cleanupCallbacks) {
511
- for (const callback of privateInfo.cleanupCallbacks) {
512
- try { callback() } catch {}
513
- }
514
- privateInfo.cleanupCallbacks.clear()
515
- }
516
-
517
628
  if (this.agent) {
518
629
  this.agent.destroy()
519
630
  this.agent = null
@@ -531,8 +642,6 @@ class Rest {
531
642
  this.request = null
532
643
  this.defaultHeaders = null
533
644
  this._endpoints = null
534
-
535
- privateData.delete(this)
536
645
  }
537
646
  }
538
647
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.15.1",
3
+ "version": "2.16.1",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",