aqualink 2.15.0 → 2.16.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.
@@ -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
@@ -298,7 +298,7 @@ class Node {
298
298
  try {
299
299
  const ws = new WebSocket(this.wsUrl, {
300
300
  headers: this._headers,
301
- perMessageDeflate: false,
301
+ perMessageDeflate: true,
302
302
  handshakeTimeout: this.timeout,
303
303
  maxPayload: this.maxPayload,
304
304
  skipUTF8Validation: this.skipUTF8Validation
@@ -18,6 +18,9 @@ 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',
21
24
  LyricsNotFoundEvent: 'lyricsNotFound'
22
25
  })
23
26
 
@@ -301,7 +304,7 @@ class Player extends EventEmitter {
301
304
  this.playing = true
302
305
  this.paused = false
303
306
  this.position = 0
304
- await this.batchUpdatePlayer({guildId: this.guildId, encodedTrack: this.current.track}, true)
307
+ await this.batchUpdatePlayer({guildId: this.guildId, track: { encoded: this.current.track} }, true)
305
308
  return this
306
309
  } catch (error) {
307
310
  this.aqua.emit(AqualinkEvents.Error, error)
@@ -430,8 +433,8 @@ class Player extends EventEmitter {
430
433
  this._dataStore.clear()
431
434
  this._dataStore = null
432
435
  }
433
-
434
- if (this.current?.dispose) this.current.dispose()
436
+ // if autoResume is enabled, ig it will need to be keeped in case the connection dies somehow, so don't dispose it
437
+ if (this.current?.dispose && !this.aqua.options.autoResume) this.current.dispose()
435
438
  this.connection = this.filters = this.current = this.autoplaySeed = null
436
439
 
437
440
  if (!skipRemote) {
@@ -449,7 +452,7 @@ class Player extends EventEmitter {
449
452
  pause(paused) {
450
453
  if (this.destroyed || this.paused === !!paused) return this
451
454
  this.paused = !!paused
452
- this.batchUpdatePlayer({guildId: this.guildId, paused: this.paused}, true)
455
+ this.batchUpdatePlayer({guildId: this.guildId, paused: this.paused}, true).catch(() => {})
453
456
  return this
454
457
  }
455
458
 
@@ -459,7 +462,7 @@ class Player extends EventEmitter {
459
462
  const pos = position === 0 ? 0 : this.position + position
460
463
  const clamped = len ? Math.min(Math.max(pos, 0), len) : Math.max(pos, 0)
461
464
  this.position = clamped
462
- this.batchUpdatePlayer({guildId: this.guildId, position: clamped}, true)
465
+ this.batchUpdatePlayer({guildId: this.guildId, position: clamped}, true).catch(() => {})
463
466
  return this
464
467
  }
465
468
 
@@ -467,7 +470,7 @@ class Player extends EventEmitter {
467
470
  if (this.destroyed || !this.playing) return this
468
471
  this.playing = this.paused = false
469
472
  this.position = 0
470
- this.batchUpdatePlayer({guildId: this.guildId, encodedTrack: null}, true)
473
+ this.batchUpdatePlayer({guildId: this.guildId, track: {encoded: null}}, true).catch(() => {})
471
474
  return this
472
475
  }
473
476
 
@@ -475,7 +478,7 @@ class Player extends EventEmitter {
475
478
  const vol = _functions.clamp(volume)
476
479
  if (this.destroyed || this.volume === vol) return this
477
480
  this.volume = vol
478
- this.batchUpdatePlayer({guildId: this.guildId, volume: vol})
481
+ this.batchUpdatePlayer({guildId: this.guildId, volume: vol}).catch(() => {})
479
482
  return this
480
483
  }
481
484
 
@@ -492,7 +495,7 @@ class Player extends EventEmitter {
492
495
  const id = _functions.toId(channel)
493
496
  if (!id) throw new TypeError('Invalid text channel')
494
497
  this.textChannel = id
495
- this.batchUpdatePlayer({guildId: this.guildId, text_channel: id})
498
+ this.batchUpdatePlayer({guildId: this.guildId, text_channel: id}).catch(() => {})
496
499
  return this
497
500
  }
498
501
 
@@ -821,6 +824,21 @@ class Player extends EventEmitter {
821
824
  this.aqua.emit(AqualinkEvents.LyricsLine, this, track, payload)
822
825
  }
823
826
 
827
+ async volumeChanged(player, track, payload) {
828
+ if (this.destroyed) return
829
+ this.aqua.emit(AqualinkEvents.VolumeChanged, this, track, payload)
830
+ }
831
+
832
+ async filtersChanged(player, track, payload) {
833
+ if (this.destroyed) return
834
+ this.aqua.emit(AqualinkEvents.FiltersChanged, this, track, payload)
835
+ }
836
+
837
+ async seekEvent(player, track, payload) {
838
+ if (this.destroyed) return
839
+ this.aqua.emit(AqualinkEvents.Seek, this, track, payload)
840
+ }
841
+
824
842
  async lyricsFound(player, track, payload) {
825
843
  if (this.destroyed) return
826
844
  this.aqua.emit(AqualinkEvents.LyricsFound, this, track, payload)
@@ -876,4 +894,4 @@ class Player extends EventEmitter {
876
894
  }
877
895
  }
878
896
 
879
- module.exports = Player
897
+ module.exports = Player
@@ -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.0",
3
+ "version": "2.16.0",
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",