aqualink 2.20.1 → 3.1.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.
@@ -0,0 +1,584 @@
1
+ const { AqualinkEvents } = require('./AqualinkEvents')
2
+ const { reportSuppressedError } = require('./Reporting')
3
+
4
+ class PlayerLifecycle {
5
+ constructor(player, deps) {
6
+ this.player = player
7
+ this._functions = deps._functions
8
+ this.PLAYER_STATE = deps.PLAYER_STATE
9
+ this.VOICE_TRACE_INTERVAL = deps.VOICE_TRACE_INTERVAL
10
+ this.PLAYER_UPDATE_SILENCE_THRESHOLD = deps.PLAYER_UPDATE_SILENCE_THRESHOLD
11
+ this.VOICE_DOWN_THRESHOLD = deps.VOICE_DOWN_THRESHOLD
12
+ this.VOICE_ABANDON_MULTIPLIER = deps.VOICE_ABANDON_MULTIPLIER
13
+ this.VOICE_FORCE_DESTROY_MS = deps.VOICE_FORCE_DESTROY_MS
14
+ this.RECONNECT_MAX = deps.RECONNECT_MAX
15
+ this.MUTE_TOGGLE_DELAY = deps.MUTE_TOGGLE_DELAY
16
+ this.SEEK_DELAY = deps.SEEK_DELAY
17
+ this.PAUSE_DELAY = deps.PAUSE_DELAY
18
+ this.RETRY_BACKOFF_BASE = deps.RETRY_BACKOFF_BASE
19
+ this.RETRY_BACKOFF_MAX = deps.RETRY_BACKOFF_MAX
20
+ }
21
+
22
+ handlePlayerUpdate(packet) {
23
+ const player = this.player
24
+ if (player.destroyed || !packet?.state) return
25
+ const s = packet.state
26
+ player._lastPlayerUpdateAt = Date.now()
27
+ const wasConnected = player.connected
28
+ player.position = this._functions.isNum(s.position) ? s.position : 0
29
+ player.connected = !!s.connected
30
+ player.ping = this._functions.isNum(s.ping) ? s.ping : 0
31
+ player.timestamp = this._functions.isNum(s.time) ? s.time : Date.now()
32
+
33
+ if (player.destroyed) return
34
+
35
+ if (!player.connected) {
36
+ if (wasConnected || !player._voiceDownSince) {
37
+ if (player.aqua?.debugTrace) {
38
+ player.aqua._trace('player.voice.down', {
39
+ guildId: player.guildId,
40
+ reconnecting: !!player._reconnecting,
41
+ recovering: !!player._voiceRecovering
42
+ })
43
+ }
44
+ }
45
+ if (
46
+ !player._voiceDownSince &&
47
+ !player._reconnecting &&
48
+ !player._voiceRecovering
49
+ ) {
50
+ player._voiceDownSince = Date.now()
51
+ const recoveryToken = player._claimVoiceRecovery('player_update_resume')
52
+ player._createTimer(() => {
53
+ if (
54
+ !player._isVoiceRecoveryActive(recoveryToken) ||
55
+ player.connected ||
56
+ player.destroyed ||
57
+ player._reconnecting ||
58
+ player._voiceRecovering ||
59
+ player.nodes?.info?.isNodelink ||
60
+ !player.voiceChannel
61
+ )
62
+ return
63
+ player.connection.attemptResume()
64
+ }, 1000)
65
+ }
66
+ } else {
67
+ player._voiceDownSince = 0
68
+ player.state = this.PLAYER_STATE.READY
69
+ player._clearVoiceRecovery(undefined, 'connected')
70
+ player._voiceRecovering = false
71
+
72
+ if (player._reconnecting && !player._isActivelyReconnecting) {
73
+ player._reconnecting = false
74
+ }
75
+ if (player._resuming) {
76
+ player._resuming = false
77
+ }
78
+
79
+ const now = Date.now()
80
+ if (
81
+ !wasConnected ||
82
+ now - player._lastVoiceUpTraceAt >= this.VOICE_TRACE_INTERVAL
83
+ ) {
84
+ player._lastVoiceUpTraceAt = now
85
+ if (player.aqua?.debugTrace) {
86
+ player.aqua._trace('player.voice.up', {
87
+ guildId: player.guildId,
88
+ ping: player.ping
89
+ })
90
+ }
91
+ }
92
+ this.flushDeferredPlay()
93
+ }
94
+
95
+ player.aqua.emit(AqualinkEvents.PlayerUpdate, player, packet)
96
+ }
97
+
98
+ async voiceWatchdog() {
99
+ const player = this.player
100
+ if (player.destroyed || !player.connection) return
101
+
102
+ const now = Date.now()
103
+ const silentPlayer =
104
+ player.playing &&
105
+ !player.paused &&
106
+ !!player.voiceChannel &&
107
+ !player._reconnecting &&
108
+ !player._voiceRecovering &&
109
+ now - (player._lastPlayerUpdateAt || 0) >=
110
+ this.PLAYER_UPDATE_SILENCE_THRESHOLD
111
+
112
+ if (silentPlayer) {
113
+ const silenceMs = now - (player._lastPlayerUpdateAt || now)
114
+ if (!player._voiceDownSince)
115
+ player._voiceDownSince = now - this.VOICE_DOWN_THRESHOLD - 1
116
+ player._lastPlayerUpdateAt = now
117
+ player.connected = false
118
+ if (player.aqua?.debugTrace) {
119
+ player.aqua._trace('player.voice.silence', {
120
+ guildId: player.guildId,
121
+ silenceMs,
122
+ playing: !!player.playing,
123
+ paused: !!player.paused
124
+ })
125
+ }
126
+ }
127
+
128
+ if (player._voiceDownSince && !player.connected) {
129
+ const downFor = Date.now() - player._voiceDownSince
130
+ if (
131
+ downFor > this.VOICE_FORCE_DESTROY_MS &&
132
+ player.reconnectionRetries >= this.RECONNECT_MAX
133
+ ) {
134
+ if (player.aqua?.debugTrace) {
135
+ player.aqua._trace('player.forceDestroy', {
136
+ guildId: player.guildId
137
+ })
138
+ }
139
+ player.destroy()
140
+ return
141
+ }
142
+ }
143
+
144
+ if (!player._shouldAttemptVoiceRecovery()) return
145
+
146
+ const hasVoiceData =
147
+ player.connection?.sessionId &&
148
+ player.connection?.endpoint &&
149
+ player.connection?.token
150
+ if (!hasVoiceData) {
151
+ const downFor = Date.now() - player._voiceDownSince
152
+ if (downFor > this.VOICE_DOWN_THRESHOLD * this.VOICE_ABANDON_MULTIPLIER) {
153
+ const recoveryToken = player._claimVoiceRecovery(
154
+ 'watchdog_voice_refresh'
155
+ )
156
+ if (player._isVoiceRecoveryActive(recoveryToken))
157
+ player.connection?._requestVoiceState?.()
158
+ if (player._isVoiceRecoveryActive(recoveryToken))
159
+ player.connection?.resendVoiceUpdate(true)
160
+ player.reconnectionRetries = Math.min(
161
+ player.reconnectionRetries + 1,
162
+ 30
163
+ )
164
+ if (
165
+ downFor > this.VOICE_FORCE_DESTROY_MS &&
166
+ player.reconnectionRetries >= this.RECONNECT_MAX * 2
167
+ ) {
168
+ player.destroy()
169
+ }
170
+ }
171
+ return
172
+ }
173
+
174
+ const recoveryToken = player._claimVoiceRecovery('watchdog_resume')
175
+ player._voiceRecovering = true
176
+ try {
177
+ if (!player._isVoiceRecoveryActive(recoveryToken)) return
178
+ if (await player.connection.attemptResume()) {
179
+ player.reconnectionRetries = player._voiceDownSince = 0
180
+ player._clearVoiceRecovery(recoveryToken, 'resumed')
181
+ return
182
+ }
183
+ if (!player._isVoiceRecoveryActive(recoveryToken)) return
184
+ const originalMute = player.mute
185
+ player.send({
186
+ guild_id: player.guildId,
187
+ channel_id: player.voiceChannel,
188
+ self_deaf: player.deaf,
189
+ self_mute: !originalMute
190
+ })
191
+ await player._delay(this.MUTE_TOGGLE_DELAY)
192
+ if (!player.destroyed && player._isVoiceRecoveryActive(recoveryToken)) {
193
+ player.send({
194
+ guild_id: player.guildId,
195
+ channel_id: player.voiceChannel,
196
+ self_deaf: player.deaf,
197
+ self_mute: originalMute
198
+ })
199
+ }
200
+ if (player._isVoiceRecoveryActive(recoveryToken))
201
+ player.connection.resendVoiceUpdate()
202
+ player.reconnectionRetries++
203
+ } catch (error) {
204
+ player.reconnectionRetries++
205
+ reportSuppressedError(player, 'player.voiceWatchdog', error, {
206
+ guildId: player.guildId
207
+ })
208
+ if (player.reconnectionRetries >= this.RECONNECT_MAX) {
209
+ if (player._isVoiceRecoveryActive(recoveryToken))
210
+ player.connection?._requestVoiceState?.()
211
+ if (player._isVoiceRecoveryActive(recoveryToken))
212
+ player.connection?.resendVoiceUpdate(true)
213
+ player.reconnectionRetries = this.RECONNECT_MAX - 2
214
+ }
215
+ } finally {
216
+ if (player._isVoiceRecoveryActive(recoveryToken)) {
217
+ player._voiceRecovering = false
218
+ }
219
+ }
220
+ }
221
+
222
+ async attemptVoiceResume(abortSignal) {
223
+ const player = this.player
224
+ if (!player.connection?.sessionId)
225
+ throw new Error(`No session (guild=${player.guildId})`)
226
+ if (abortSignal?.aborted) throw new Error('Resume aborted by signal')
227
+ if (!(await player.connection.attemptResume()))
228
+ throw new Error(
229
+ `Resume failed (guild=${player.guildId}, endpoint=${player.connection.endpoint || 'none'})`
230
+ )
231
+ }
232
+
233
+ async socketClosed(_player, _track, payload) {
234
+ const player = this.player
235
+ if (player.destroyed || player._reconnecting) return
236
+ if (player.aqua?.debugTrace) {
237
+ player.aqua._trace('player.socketClosed', {
238
+ guildId: player.guildId,
239
+ code: payload?.code
240
+ })
241
+ }
242
+
243
+ const code = payload?.code
244
+ if (code === 4006 && player._resuming) {
245
+ if (player.aqua?.debugTrace) {
246
+ player.aqua._trace('player.socketClosed.ignored', {
247
+ guildId: player.guildId,
248
+ code,
249
+ reason: 'transient_while_resuming'
250
+ })
251
+ }
252
+ return
253
+ }
254
+
255
+ let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
256
+ if (code === 4014 && player.connection?.isWaitingForDisconnect)
257
+ isRecoverable = false
258
+
259
+ if (code === 4015 && !player.nodes?.info?.isNodelink) {
260
+ const recoveryToken = player._claimVoiceRecovery('socket_closed_resume')
261
+ player._reconnecting = true
262
+ player._isActivelyReconnecting = true
263
+ try {
264
+ if (!player._isVoiceRecoveryActive(recoveryToken)) return
265
+ await this.attemptVoiceResume()
266
+ player._clearVoiceRecovery(recoveryToken, 'socket_closed_resumed')
267
+ player._reconnecting = false
268
+ player._isActivelyReconnecting = false
269
+ return
270
+ } catch (error) {
271
+ player._reconnecting = false
272
+ reportSuppressedError(player, 'player.socketClosed.resume', error, {
273
+ guildId: player.guildId,
274
+ code
275
+ })
276
+ }
277
+ }
278
+
279
+ if (!isRecoverable) {
280
+ player.aqua.emit(AqualinkEvents.SocketClosed, player, payload)
281
+ player.destroy()
282
+ return
283
+ }
284
+
285
+ if (code === 4014 || code === 4022) {
286
+ player.connected = false
287
+ if (!player._voiceDownSince) player._voiceDownSince = Date.now()
288
+ player._suppressResumeUntil = Date.now() + (code === 4022 ? 3000 : 2000)
289
+ }
290
+
291
+ const aqua = player.aqua
292
+ const vcId = this._functions.toId(player.voiceChannel)
293
+ const tcId = this._functions.toId(player.textChannel)
294
+ const { guildId, deaf, mute } = player
295
+
296
+ if (!vcId) {
297
+ aqua?.emit?.(AqualinkEvents.SocketClosed, player, payload)
298
+ return
299
+ }
300
+
301
+ if (code === 4014 || code === 4022) {
302
+ const recoveryToken = player._claimVoiceRecovery('socket_closed_soft')
303
+ const now = Date.now()
304
+ if (
305
+ now - (player._voiceRequestAt || 0) >= 1200 &&
306
+ player._isVoiceRecoveryActive(recoveryToken)
307
+ ) {
308
+ player._voiceRequestAt = now
309
+ if (player._isVoiceRecoveryActive(recoveryToken))
310
+ player.connection?._requestVoiceState?.()
311
+ if (player._isVoiceRecoveryActive(recoveryToken))
312
+ player.connection?.resendVoiceUpdate?.(true)
313
+ if (player._isVoiceRecoveryActive(recoveryToken))
314
+ this._functions.safeCall(() =>
315
+ player.connect({
316
+ guildId,
317
+ voiceChannel: vcId,
318
+ deaf,
319
+ mute
320
+ })
321
+ )
322
+ }
323
+
324
+ if (player.aqua?.debugTrace) {
325
+ player.aqua._trace('player.socketClosed.softRecover', {
326
+ guildId: player.guildId,
327
+ code,
328
+ strategy: code === 4022 ? 'resume_after_voice_refresh' : '4014_retry'
329
+ })
330
+ }
331
+ const waitMs = Math.max(0, player._suppressResumeUntil - Date.now())
332
+ if (waitMs > 0) await player._delay(waitMs)
333
+
334
+ let resumed = false
335
+ if (
336
+ player._isVoiceRecoveryActive(recoveryToken) &&
337
+ !player.destroyed &&
338
+ !player.connected
339
+ ) {
340
+ resumed = await player.connection?.attemptResume?.().catch((error) => {
341
+ reportSuppressedError(
342
+ player,
343
+ 'player.socketClosed.softRecover',
344
+ error,
345
+ {
346
+ guildId: player.guildId,
347
+ code
348
+ }
349
+ )
350
+ return false
351
+ })
352
+ }
353
+ if (resumed)
354
+ player._clearVoiceRecovery(recoveryToken, 'socket_soft_resumed')
355
+ if (
356
+ resumed ||
357
+ player.connected ||
358
+ player.destroyed ||
359
+ player._reconnecting
360
+ ) {
361
+ if (player.aqua?.debugTrace) {
362
+ player.aqua._trace('player.socketClosed.softRecover.ok', {
363
+ guildId: player.guildId,
364
+ code,
365
+ resumed: !!resumed
366
+ })
367
+ }
368
+ return
369
+ }
370
+ if (player.aqua?.debugTrace) {
371
+ player.aqua._trace('player.socketClosed.softRecover.failed', {
372
+ guildId: player.guildId,
373
+ code
374
+ })
375
+ }
376
+ }
377
+
378
+ const state = {
379
+ volume: player.volume,
380
+ position: player.position,
381
+ paused: player.paused,
382
+ loop: player.loop,
383
+ isAutoplayEnabled: player.isAutoplayEnabled,
384
+ currentTrack: player.current,
385
+ queue: player.queue?.toArray() || [],
386
+ previousIdentifiers: Array.from(player.previousIdentifiers),
387
+ autoplaySeed: player.autoplaySeed,
388
+ nowPlayingMessage: player.nowPlayingMessage,
389
+ voiceState: player.connection
390
+ ? {
391
+ sessionId: player.connection.sessionId || null,
392
+ endpoint: player.connection.endpoint || null,
393
+ token: player.connection.token || null,
394
+ region: player.connection.region || null,
395
+ channelId: player.connection.channelId || null
396
+ }
397
+ : null
398
+ }
399
+
400
+ player._reconnecting = true
401
+ player._isActivelyReconnecting = true
402
+ player.destroy({
403
+ preserveClient: true,
404
+ skipRemote: true,
405
+ preserveMessage: true,
406
+ preserveReconnecting: true,
407
+ preserveTracks: true
408
+ })
409
+
410
+ const reconnectNonce = player._reconnectNonce
411
+ player._reconnectTimers = new Set()
412
+ const reconnectTimers = player._reconnectTimers
413
+ const tryReconnect = async (attempt) => {
414
+ if (aqua?.destroyed || player._reconnectNonce !== reconnectNonce) {
415
+ this._functions.clearTimers(reconnectTimers)
416
+ player._reconnectTimers = null
417
+ player._reconnecting = false
418
+ player._isActivelyReconnecting = false
419
+ return
420
+ }
421
+ const activePlayer = aqua?.players?.get?.(String(guildId))
422
+ if (activePlayer && activePlayer !== player && !activePlayer.destroyed) {
423
+ this._functions.clearTimers(reconnectTimers)
424
+ player._reconnectTimers = null
425
+ player._reconnecting = false
426
+ player._isActivelyReconnecting = false
427
+ return
428
+ }
429
+ try {
430
+ const np = await aqua.createConnection({
431
+ guildId,
432
+ voiceChannel: vcId,
433
+ textChannel: tcId,
434
+ deaf,
435
+ mute,
436
+ defaultVolume: state.volume,
437
+ preserveMessage: true,
438
+ resuming: true
439
+ })
440
+ if (!np) throw new Error('Failed to create player')
441
+ if (player._reconnectNonce !== reconnectNonce || aqua?.destroyed) {
442
+ try {
443
+ np.destroy?.()
444
+ } catch {}
445
+ this._functions.clearTimers(reconnectTimers)
446
+ player._reconnectTimers = null
447
+ player._reconnecting = false
448
+ player._isActivelyReconnecting = false
449
+ return
450
+ }
451
+ const latestActivePlayer = aqua?.players?.get?.(String(guildId))
452
+ if (
453
+ latestActivePlayer &&
454
+ latestActivePlayer !== player &&
455
+ !latestActivePlayer.destroyed
456
+ ) {
457
+ try {
458
+ np.destroy?.()
459
+ } catch {}
460
+ this._functions.clearTimers(reconnectTimers)
461
+ player._reconnectTimers = null
462
+ player._reconnecting = false
463
+ player._isActivelyReconnecting = false
464
+ return
465
+ }
466
+
467
+ np.reconnectionRetries = 0
468
+ np.loop = state.loop
469
+ np.isAutoplayEnabled = state.isAutoplayEnabled
470
+ np.autoplaySeed = state.autoplaySeed
471
+ np.previousIdentifiers = new Set(state.previousIdentifiers)
472
+ np.nowPlayingMessage = state.nowPlayingMessage
473
+ if (state.voiceState && np.connection) {
474
+ np.connection.sessionId =
475
+ state.voiceState.sessionId || np.connection.sessionId
476
+ np.connection.endpoint =
477
+ state.voiceState.endpoint || np.connection.endpoint
478
+ np.connection.token = state.voiceState.token || np.connection.token
479
+ np.connection.region = state.voiceState.region || np.connection.region
480
+ np.connection.channelId =
481
+ state.voiceState.channelId || np.connection.channelId
482
+ np.connection._lastEndpoint =
483
+ state.voiceState.endpoint || np.connection._lastEndpoint
484
+ if (
485
+ np.connection.sessionId &&
486
+ np.connection.endpoint &&
487
+ np.connection.token
488
+ ) {
489
+ np.connection._lastVoiceDataUpdate = Date.now()
490
+ np.connection.resendVoiceUpdate(true)
491
+ }
492
+ }
493
+
494
+ const ct = state.currentTrack
495
+ if (ct) np.queue.add(ct)
496
+ for (const q of state.queue) if (q !== ct) np.queue.add(q)
497
+
498
+ if (ct) {
499
+ await np.play()
500
+ if (state.position > 5000)
501
+ np._createTimer(
502
+ () => !np.destroyed && np.seek(state.position),
503
+ this.SEEK_DELAY
504
+ )
505
+ if (state.paused)
506
+ np._createTimer(
507
+ () => !np.destroyed && np.pause(true),
508
+ this.PAUSE_DELAY
509
+ )
510
+ }
511
+
512
+ this._functions.clearTimers(reconnectTimers)
513
+ player._reconnectTimers = null
514
+ player._reconnecting = false
515
+ player._isActivelyReconnecting = false
516
+ aqua.emit(AqualinkEvents.PlayerReconnected, np, {
517
+ oldPlayer: player,
518
+ restoredState: state
519
+ })
520
+ } catch (error) {
521
+ if (player._reconnectNonce !== reconnectNonce || aqua?.destroyed) {
522
+ this._functions.clearTimers(reconnectTimers)
523
+ player._reconnectTimers = null
524
+ player._reconnecting = false
525
+ player._isActivelyReconnecting = false
526
+ return
527
+ }
528
+ const retriesLeft = this.RECONNECT_MAX - attempt
529
+ aqua.emit(AqualinkEvents.ReconnectionFailed, player, {
530
+ error,
531
+ code,
532
+ payload,
533
+ retriesLeft
534
+ })
535
+
536
+ if (retriesLeft > 0) {
537
+ this._functions.createTimer(
538
+ () => tryReconnect(attempt + 1),
539
+ Math.min(this.RETRY_BACKOFF_BASE * attempt, this.RETRY_BACKOFF_MAX),
540
+ reconnectTimers
541
+ )
542
+ } else {
543
+ this._functions.clearTimers(reconnectTimers)
544
+ player._reconnectTimers = null
545
+ player._reconnecting = false
546
+ player._isActivelyReconnecting = false
547
+ aqua.emit(AqualinkEvents.SocketClosed, player, payload)
548
+ }
549
+ }
550
+ }
551
+
552
+ tryReconnect(1)
553
+ }
554
+
555
+ flushDeferredPlay() {
556
+ const player = this.player
557
+ if (
558
+ !player._deferredStart ||
559
+ player.destroyed ||
560
+ !player.current?.track ||
561
+ !player._updateBatcher
562
+ )
563
+ return
564
+ player._deferredStart = false
565
+ const updateData = {
566
+ track: { encoded: player.current.track },
567
+ paused: player.paused
568
+ }
569
+ if (player.position > 0) updateData.position = player.position
570
+ if (player.aqua?.debugTrace) {
571
+ player.aqua._trace('player.play.deferred.flush', {
572
+ guildId: player.guildId,
573
+ hasEndpoint: !!player.connection?.endpoint
574
+ })
575
+ }
576
+ player.batchUpdatePlayer(updateData, true).catch((error) =>
577
+ reportSuppressedError(player, 'player.deferredPlay.flush', error, {
578
+ guildId: player.guildId
579
+ })
580
+ )
581
+ }
582
+ }
583
+
584
+ module.exports = PlayerLifecycle
@@ -0,0 +1,42 @@
1
+ function defineLifecycleAccessor(player, prop, key) {
2
+ Object.defineProperty(player, prop, {
3
+ configurable: true,
4
+ enumerable: false,
5
+ get() {
6
+ return this._lifecycle[key]
7
+ },
8
+ set(value) {
9
+ this._lifecycle[key] = !!value
10
+ }
11
+ })
12
+ }
13
+
14
+ function attachPlayerLifecycleState(player, options = {}) {
15
+ Object.defineProperty(player, '_lifecycle', {
16
+ configurable: true,
17
+ enumerable: false,
18
+ writable: false,
19
+ value: {
20
+ voiceRecovering: false,
21
+ reconnecting: false,
22
+ activelyReconnecting: false,
23
+ resuming: !!options.resuming,
24
+ deferredStart: false
25
+ }
26
+ })
27
+
28
+ defineLifecycleAccessor(player, '_voiceRecovering', 'voiceRecovering')
29
+ defineLifecycleAccessor(player, '_reconnecting', 'reconnecting')
30
+ defineLifecycleAccessor(
31
+ player,
32
+ '_isActivelyReconnecting',
33
+ 'activelyReconnecting'
34
+ )
35
+ defineLifecycleAccessor(player, '_resuming', 'resuming')
36
+ defineLifecycleAccessor(player, '_deferredStart', 'deferredStart')
37
+ return player._lifecycle
38
+ }
39
+
40
+ module.exports = {
41
+ attachPlayerLifecycleState
42
+ }
@@ -2,6 +2,7 @@ class Queue {
2
2
  constructor() {
3
3
  this._items = []
4
4
  this._head = 0
5
+ this._compactThreshold = 64
5
6
  }
6
7
 
7
8
  get size() {
@@ -40,13 +41,16 @@ class Queue {
40
41
 
41
42
  _compact(force = false) {
42
43
  if (this._head <= 0) return
43
- if (!force && this._head <= this._items.length / 2) return
44
+ if (!force && this._head < this._compactThreshold) return
44
45
  const len = this._items.length - this._head
45
46
  for (let i = 0; i < len; i++) {
46
47
  this._items[i] = this._items[this._head + i]
47
48
  }
48
49
  this._items.length = len
49
50
  this._head = 0
51
+ if (this._compactThreshold < len) {
52
+ this._compactThreshold = Math.min(len * 2, 1024)
53
+ }
50
54
  }
51
55
 
52
56
  shuffle() {
@@ -0,0 +1,32 @@
1
+ const { AqualinkEvents } = require('./AqualinkEvents')
2
+
3
+ function normalizeError(error, fallback = 'Unknown error') {
4
+ if (error instanceof Error) return error
5
+ if (typeof error === 'string' && error) return new Error(error)
6
+ if (error && typeof error.message === 'string' && error.message) {
7
+ return new Error(error.message)
8
+ }
9
+ return new Error(fallback)
10
+ }
11
+
12
+ function getAqua(target) {
13
+ return target?.aqua || target || null
14
+ }
15
+
16
+ function reportSuppressedError(target, scope, error, data = null) {
17
+ const aqua = getAqua(target)
18
+ const err = normalizeError(error, `Suppressed error in ${scope}`)
19
+ if (aqua?.debugTrace) {
20
+ aqua._trace(`${scope}.suppressed`, {
21
+ ...(data || {}),
22
+ error: err.message
23
+ })
24
+ }
25
+ aqua?.emit?.(AqualinkEvents.Debug, err)
26
+ return err
27
+ }
28
+
29
+ module.exports = {
30
+ normalizeError,
31
+ reportSuppressedError
32
+ }