aqualink 2.11.4 → 2.11.5

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.
@@ -619,12 +619,11 @@ class Aqua extends EventEmitter {
619
619
  _handlePlayerDestroy(player) {
620
620
  const node = player.nodes
621
621
  node?.players?.delete?.(player)
622
-
623
622
  if (this.players.get(player.guildId) === player) {
624
623
  this.players.delete(player.guildId)
625
624
  }
626
625
 
627
- this.emit('playerDestroy', player)
626
+ this.emit("playerDestroy", player)
628
627
  }
629
628
 
630
629
  async destroyPlayer(guildId) {
@@ -646,11 +645,14 @@ class Aqua extends EventEmitter {
646
645
  if (!requestNode) throw new Error('No nodes available')
647
646
 
648
647
  const formattedQuery = isProbablyUrl(query) ? query : `${source}${SEARCH_PREFIX}${query}`
648
+ const now = performance.now()
649
+
649
650
 
650
651
  try {
651
652
  const endpoint = `/${this.restVersion}/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`
652
653
  const response = await requestNode.rest.makeRequest('GET', endpoint)
653
654
 
655
+
654
656
  if (!response || response.loadType === 'empty' || response.loadType === 'NO_MATCHES') {
655
657
  return EMPTY_TRACKS_RESPONSE
656
658
  }
@@ -1,47 +1,79 @@
1
1
  'use strict'
2
2
 
3
- const ENDPOINT_REGEX = /^([a-z-]+)/i
4
- const WHITESPACE_REGEX = /^\s+|\s+$/g
3
+ const POOL_SIZE = 10
4
+ const LISTENER_CHECK_INTERVAL = 5000
5
+ const UPDATE_TIMEOUT = 5000
6
+
7
+ const ENDPOINT_PATTERN = /^([a-z-]+)/i
8
+
9
+ const STATE_FLAGS = {
10
+ CONNECTED: 1 << 0,
11
+ PAUSED: 1 << 1,
12
+ SELF_DEAF: 1 << 2,
13
+ SELF_MUTE: 1 << 3,
14
+ HAS_DEBUG_LISTENERS: 1 << 4,
15
+ HAS_MOVE_LISTENERS: 1 << 5,
16
+ UPDATE_SCHEDULED: 1 << 6
17
+ }
5
18
 
6
19
  class UpdatePayloadPool {
7
20
  constructor() {
8
- this.pool = []
21
+ this.pool = new Array(POOL_SIZE)
22
+ this.size = 0
23
+
24
+ for (let i = 0; i < POOL_SIZE; i++) {
25
+ this.pool[i] = this._createPayload()
26
+ }
27
+ this.size = POOL_SIZE
9
28
  }
10
29
 
11
- acquire() {
12
- return this.pool.pop() || {
30
+ _createPayload() {
31
+ return {
13
32
  guildId: null,
14
33
  data: {
15
34
  voice: {
16
35
  token: null,
17
36
  endpoint: null,
18
- sessionId: null
37
+ sessionId: null,
38
+ resume: undefined,
39
+ sequence: undefined
19
40
  },
20
41
  volume: null
21
42
  }
22
43
  }
23
44
  }
24
45
 
46
+ acquire() {
47
+ if (this.size > 0) {
48
+ return this.pool[--this.size]
49
+ }
50
+ return this._createPayload()
51
+ }
52
+
25
53
  release(payload) {
54
+ if (!payload || this.size >= POOL_SIZE) return
55
+
26
56
  payload.guildId = null
27
- payload.data.voice.token = null
28
- payload.data.voice.endpoint = null
29
- payload.data.voice.sessionId = null
57
+ const voice = payload.data.voice
58
+ voice.token = null
59
+ voice.endpoint = null
60
+ voice.sessionId = null
61
+ voice.resume = undefined
62
+ voice.sequence = undefined
30
63
  payload.data.volume = null
31
- delete payload.data.voice.resume
32
- delete payload.data.voice.sequence
33
64
 
34
- if (this.pool.length < 10) {
35
- this.pool.push(payload)
36
- }
65
+ this.pool[this.size++] = payload
37
66
  }
38
67
  }
39
68
 
69
+ const sharedPayloadPool = new UpdatePayloadPool()
70
+
40
71
  class Connection {
41
72
  constructor(player) {
42
- if (!player) throw new TypeError('Player is required: CONNECTION')
43
- if (!player.aqua) throw new TypeError('Player.aqua is required: CONNECTION')
44
- if (!player.nodes) throw new TypeError('Player.nodes is required: CONNECTION')
73
+ // Validate once
74
+ if (!player?.aqua?.clientId || !player.nodes) {
75
+ throw new TypeError('Invalid player configuration')
76
+ }
45
77
 
46
78
  this._player = player
47
79
  this._aqua = player.aqua
@@ -49,62 +81,85 @@ class Connection {
49
81
  this._guildId = player.guildId
50
82
  this._clientId = player.aqua.clientId
51
83
 
52
- const state = Object.create(null)
53
- state.voiceChannel = player.voiceChannel
54
- state.sessionId = null
55
- state.endpoint = null
56
- state.token = null
57
- state.region = null
58
- state.sequence = 0
59
- state._lastEndpoint = null
60
- state._pendingUpdate = null
61
- state._updateTimer = null
62
-
63
- Object.assign(this, state)
64
-
65
- this._hasDebugListeners = false
66
- this._hasMoveListeners = false
84
+ this.voiceChannel = player.voiceChannel
85
+ this.sessionId = null
86
+ this.endpoint = null
87
+ this.token = null
88
+ this.region = null
89
+ this.sequence = 0
90
+ this._lastEndpoint = null
91
+ this._pendingUpdate = null
92
+
93
+ this._stateFlags = 0
67
94
  this._lastListenerCheck = 0
68
- this._listenerCheckInterval = 5000
69
95
 
70
- this._payloadPool = new UpdatePayloadPool()
96
+ this._payloadPool = sharedPayloadPool
97
+
98
+ this._executeVoiceUpdate = this._executeVoiceUpdate.bind(this)
71
99
 
72
100
  this._checkListeners()
73
101
  }
74
102
 
75
103
  _checkListeners() {
76
104
  const now = Date.now()
77
- if (now - this._lastListenerCheck < this._listenerCheckInterval) {
105
+ if (now - this._lastListenerCheck < LISTENER_CHECK_INTERVAL) {
78
106
  return
79
107
  }
80
108
 
81
- this._hasDebugListeners = this._aqua.listenerCount('debug') > 0
82
- this._hasMoveListeners = this._aqua.listenerCount('playerMove') > 0
109
+ let flags = this._stateFlags
110
+ flags = this._aqua.listenerCount('debug') > 0
111
+ ? flags | STATE_FLAGS.HAS_DEBUG_LISTENERS
112
+ : flags & ~STATE_FLAGS.HAS_DEBUG_LISTENERS
113
+
114
+ flags = this._aqua.listenerCount('playerMove') > 0
115
+ ? flags | STATE_FLAGS.HAS_MOVE_LISTENERS
116
+ : flags & ~STATE_FLAGS.HAS_MOVE_LISTENERS
117
+
118
+ this._stateFlags = flags
83
119
  this._lastListenerCheck = now
84
120
  }
85
121
 
86
122
  _extractRegion(endpoint) {
87
123
  if (!endpoint || typeof endpoint !== 'string') return null
88
124
 
89
- const match = ENDPOINT_REGEX.exec(endpoint)
90
- return match ? match[1] : null
125
+ const dashIndex = endpoint.indexOf('-')
126
+ if (dashIndex > 0) {
127
+ const region = endpoint.substring(0, dashIndex)
128
+ let isValid = true
129
+ for (let i = 0; i < region.length; i++) {
130
+ const code = region.charCodeAt(i)
131
+ if (!((code >= 65 && code <= 90) || (code >= 97 && code <= 122))) {
132
+ isValid = false
133
+ break
134
+ }
135
+ }
136
+ if (isValid) return region
137
+ }
138
+
139
+ const match = ENDPOINT_PATTERN.exec(endpoint)
140
+ return match?.[1] || null
91
141
  }
92
142
 
93
143
  setServerUpdate(data) {
94
- if (!data || typeof data !== 'object') return
95
- if (!data.endpoint || typeof data.endpoint !== 'string') return
96
- if (!data.token || typeof data.token !== 'string') return
144
+ if (!data?.endpoint || !data.token ||
145
+ typeof data.endpoint !== 'string' ||
146
+ typeof data.token !== 'string') {
147
+ return
148
+ }
149
+
150
+ const trimmedEndpoint = data.endpoint.trim()
97
151
 
98
- const newEndpoint = data.endpoint
99
- const hasWhitespace = WHITESPACE_REGEX.test(newEndpoint)
100
- const trimmedEndpoint = hasWhitespace ? newEndpoint.trim() : newEndpoint
152
+ if (this._lastEndpoint === trimmedEndpoint && this.token === data.token) {
153
+ return
154
+ }
101
155
 
102
156
  const newRegion = this._extractRegion(trimmedEndpoint)
157
+
103
158
  const hasRegionChange = this.region !== newRegion
104
159
  const hasEndpointChange = this._lastEndpoint !== trimmedEndpoint
105
160
 
106
161
  if (hasRegionChange || hasEndpointChange) {
107
- if (hasRegionChange && this._hasDebugListeners) {
162
+ if (hasRegionChange && (this._stateFlags & STATE_FLAGS.HAS_DEBUG_LISTENERS)) {
108
163
  this._aqua.emit('debug', `[Player ${this._guildId}] Region: ${this.region || 'none'} → ${newRegion}`)
109
164
  }
110
165
 
@@ -127,7 +182,7 @@ class Connection {
127
182
  }
128
183
 
129
184
  resendVoiceUpdate({ resume = false } = {}) {
130
- if (!this.sessionId || !this.endpoint || !this.token) {
185
+ if (!(this.sessionId && this.endpoint && this.token)) {
131
186
  return false
132
187
  }
133
188
 
@@ -136,44 +191,46 @@ class Connection {
136
191
  }
137
192
 
138
193
  setStateUpdate(data) {
139
- if (!data ||
140
- typeof data !== 'object' ||
141
- data.user_id !== this._clientId) {
194
+ if (!data || data.user_id !== this._clientId) {
142
195
  return
143
196
  }
144
197
 
145
198
  const { session_id, channel_id, self_deaf, self_mute } = data
146
199
 
147
200
  if (channel_id) {
201
+ let needsUpdate = false
202
+
148
203
  if (this.voiceChannel !== channel_id) {
149
- if (this._hasMoveListeners) {
204
+ if (this._stateFlags & STATE_FLAGS.HAS_MOVE_LISTENERS) {
150
205
  this._aqua.emit('playerMove', this.voiceChannel, channel_id)
151
206
  }
152
207
  this.voiceChannel = channel_id
153
208
  this._player.voiceChannel = channel_id
209
+ needsUpdate = true
154
210
  }
155
211
 
156
212
  if (this.sessionId !== session_id) {
157
213
  this.sessionId = session_id
214
+ needsUpdate = true
158
215
  }
159
216
 
160
- const playerUpdates = Object.create(null)
161
- playerUpdates.self_deaf = !!self_deaf
162
- playerUpdates.self_mute = !!self_mute
163
- playerUpdates.connected = true
164
217
 
165
- Object.assign(this._player, playerUpdates)
218
+ this._player.self_deaf = !!self_deaf
219
+ this._player.self_mute = !!self_mute
220
+ this._player.connected = true
166
221
 
167
- this._scheduleVoiceUpdate()
222
+ if (needsUpdate) {
223
+ this._scheduleVoiceUpdate()
224
+ }
168
225
  } else {
169
226
  this._handleDisconnect()
170
227
  }
171
228
  }
172
229
 
173
230
  _handleDisconnect() {
174
- if (!this._player.connected) return
231
+ if (!this._player?.connected) return
175
232
 
176
- if (this._hasDebugListeners) {
233
+ if (this._stateFlags & STATE_FLAGS.HAS_DEBUG_LISTENERS) {
177
234
  this._aqua.emit('debug', `[Player ${this._guildId}] Disconnected`)
178
235
  }
179
236
 
@@ -190,21 +247,47 @@ class Connection {
190
247
  }
191
248
  }
192
249
 
193
- updateSequence(seq) {
194
- if (typeof seq !== 'number' || seq < 0 || !Number.isFinite(seq)) {
195
- return
250
+ async attemptResume() {
251
+ if (!(this.sessionId && this.endpoint && this.token)) {
252
+ throw new Error('Missing required voice state')
196
253
  }
197
254
 
198
- this.sequence = Math.max(seq, this.sequence)
255
+ const payload = this._payloadPool.acquire()
256
+
257
+ try {
258
+ payload.guildId = this._guildId
259
+ payload.data.voice.token = this.token
260
+ payload.data.voice.endpoint = this.endpoint
261
+ payload.data.voice.sessionId = this.sessionId
262
+ payload.data.voice.resume = true
263
+ payload.data.volume = this._player?.volume
264
+
265
+ if (this.sequence >= 0 && Number.isFinite(this.sequence)) {
266
+ payload.data.voice.sequence = this.sequence
267
+ }
268
+
269
+ await this._sendUpdate(payload)
270
+ return true
271
+ } catch (error) {
272
+ if (this._stateFlags & STATE_FLAGS.HAS_DEBUG_LISTENERS) {
273
+ this._aqua.emit('debug', `[Player ${this._guildId}] Resume update failed: ${error?.message}`)
274
+ }
275
+ return false
276
+ } finally {
277
+ this._payloadPool.release(payload)
278
+ }
199
279
  }
200
280
 
201
- _clearPendingUpdate() {
202
- if (this._updateTimer) {
203
- clearTimeout(this._updateTimer)
204
- this._updateTimer = null
281
+ updateSequence(seq) {
282
+ if (typeof seq === 'number' && seq >= 0 && Number.isFinite(seq)) {
283
+ this.sequence = Math.max(seq, this.sequence)
205
284
  }
285
+ }
286
+
287
+ _clearPendingUpdate() {
288
+ this._stateFlags &= ~STATE_FLAGS.UPDATE_SCHEDULED
206
289
 
207
- if (this._pendingUpdate && this._pendingUpdate.payload) {
290
+ if (this._pendingUpdate?.payload) {
208
291
  this._payloadPool.release(this._pendingUpdate.payload)
209
292
  }
210
293
 
@@ -212,22 +295,28 @@ class Connection {
212
295
  }
213
296
 
214
297
  _scheduleVoiceUpdate(isResume = false) {
215
- if (!this.sessionId || !this.endpoint || !this.token) {
298
+ if (!(this.sessionId && this.endpoint && this.token)) {
299
+ return
300
+ }
301
+
302
+ if (this._stateFlags & STATE_FLAGS.UPDATE_SCHEDULED) {
216
303
  return
217
304
  }
218
305
 
219
306
  this._clearPendingUpdate()
220
307
 
221
308
  const payload = this._payloadPool.acquire()
309
+
222
310
  payload.guildId = this._guildId
223
- payload.data.voice.token = this.token
224
- payload.data.voice.endpoint = this.endpoint
225
- payload.data.voice.sessionId = this.sessionId
311
+ const voice = payload.data.voice
312
+ voice.token = this.token
313
+ voice.endpoint = this.endpoint
314
+ voice.sessionId = this.sessionId
226
315
  payload.data.volume = this._player.volume
227
316
 
228
317
  if (isResume) {
229
- payload.data.voice.resume = true
230
- payload.data.voice.sequence = this.sequence
318
+ voice.resume = true
319
+ voice.sequence = this.sequence
231
320
  }
232
321
 
233
322
  this._pendingUpdate = {
@@ -236,17 +325,18 @@ class Connection {
236
325
  timestamp: Date.now()
237
326
  }
238
327
 
239
- this._updateTimer = setImmediate(() => this._executeVoiceUpdate())
328
+ this._stateFlags |= STATE_FLAGS.UPDATE_SCHEDULED
329
+
330
+ queueMicrotask(this._executeVoiceUpdate)
240
331
  }
241
332
 
242
333
  _executeVoiceUpdate() {
334
+ this._stateFlags &= ~STATE_FLAGS.UPDATE_SCHEDULED
335
+
243
336
  const pending = this._pendingUpdate
244
337
  if (!pending) return
245
338
 
246
- this._updateTimer = null
247
-
248
- const age = Date.now() - pending.timestamp
249
- if (age > 5000) {
339
+ if (Date.now() - pending.timestamp > UPDATE_TIMEOUT) {
250
340
  this._payloadPool.release(pending.payload)
251
341
  this._pendingUpdate = null
252
342
  return
@@ -255,6 +345,12 @@ class Connection {
255
345
  const payload = pending.payload
256
346
  this._pendingUpdate = null
257
347
 
348
+ // to avoid any delay. Uncomment if needed:
349
+ // if (pending.isResume) {
350
+ // this._sendUpdateSync(payload)
351
+ // return
352
+ // }
353
+
258
354
  this._sendUpdate(payload)
259
355
  .finally(() => {
260
356
  this._payloadPool.release(payload)
@@ -262,7 +358,7 @@ class Connection {
262
358
  }
263
359
 
264
360
  async _sendUpdate(payload) {
265
- if (!this._nodes || !this._nodes.rest) {
361
+ if (!this._nodes?.rest) {
266
362
  throw new Error('Nodes or REST interface not available')
267
363
  }
268
364
 
@@ -271,10 +367,9 @@ class Connection {
271
367
  } catch (error) {
272
368
  if (error.code !== 'ECONNREFUSED' &&
273
369
  error.code !== 'ENOTFOUND' &&
274
- this._hasDebugListeners) {
370
+ (this._stateFlags & STATE_FLAGS.HAS_DEBUG_LISTENERS)) {
275
371
  this._aqua.emit('debug', `[Player ${this._guildId}] Update failed: ${error.message}`)
276
372
  }
277
-
278
373
  throw error
279
374
  }
280
375
  }
@@ -293,6 +388,7 @@ class Connection {
293
388
  this.token = null
294
389
  this.region = null
295
390
  this._lastEndpoint = null
391
+ this._stateFlags = 0
296
392
  }
297
393
  }
298
394