magmastream 2.10.3-dev.0 → 2.10.3-dev.2

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.
@@ -102,11 +102,11 @@ class JsonQueue {
102
102
  const tracks = Array.isArray(track) ? track : [track];
103
103
  if (!tracks.length)
104
104
  return;
105
- const current = (await this.getPrevious()).filter((p) => p !== null);
106
- const validTracks = tracks.filter((t) => t !== null && typeof t.uri === "string");
105
+ const current = (await this.getPrevious()).filter((previousTrack) => previousTrack !== null);
106
+ const validTracks = tracks.filter((track) => track !== null && typeof track.uri === "string");
107
107
  if (!validTracks.length)
108
108
  return;
109
- const newTracks = validTracks.filter((t) => !current.some((p) => p.uri === t.uri));
109
+ const newTracks = validTracks.filter((track) => !current.some((previousTrack) => previousTrack.uri === track.uri));
110
110
  if (!newTracks.length)
111
111
  return;
112
112
  const updated = [...current, ...newTracks];
@@ -391,9 +391,9 @@ class JsonQueue {
391
391
  const users = [...userMap.keys()];
392
392
  const queues = users.map((id) => userMap.get(id));
393
393
  const shuffledQueue = [];
394
- while (queues.some((q) => q.length > 0)) {
395
- for (const q of queues) {
396
- const track = q.shift();
394
+ while (queues.some((queue) => queue.length > 0)) {
395
+ for (const queue of queues) {
396
+ const track = queue.shift();
397
397
  if (track)
398
398
  shuffledQueue.push(track);
399
399
  }
@@ -43,7 +43,7 @@ class MemoryQueue extends Array {
43
43
  const isArray = Array.isArray(track);
44
44
  const tracks = isArray ? [...track] : [track];
45
45
  // Get the track info as a string
46
- const trackInfo = isArray ? tracks.map((t) => Utils_1.JSONUtils.safe(t, 2)).join(", ") : Utils_1.JSONUtils.safe(track, 2);
46
+ const trackInfo = isArray ? tracks.map((track) => Utils_1.JSONUtils.safe(track, 2)).join(", ") : Utils_1.JSONUtils.safe(track, 2);
47
47
  // Emit a debug message
48
48
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] Added ${tracks.length} track(s) to queue: ${trackInfo}`);
49
49
  const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
@@ -129,13 +129,13 @@ class MemoryQueue extends Array {
129
129
  addPrevious(track) {
130
130
  try {
131
131
  const max = this.manager.options.maxPreviousTracks;
132
- this.previous = this.previous.filter((p) => p !== null);
132
+ this.previous = this.previous.filter((previousTrack) => previousTrack !== null);
133
133
  if (Array.isArray(track)) {
134
- const newTracks = track.filter((t) => !this.previous.some((p) => p?.identifier === t.identifier));
134
+ const newTracks = track.filter((track) => !this.previous.some((previousTrack) => previousTrack?.identifier === track.identifier));
135
135
  this.previous.push(...newTracks);
136
136
  }
137
137
  else {
138
- const exists = this.previous.some((p) => p?.identifier === track.identifier);
138
+ const exists = this.previous.some((previousTrack) => previousTrack?.identifier === track.identifier);
139
139
  if (!exists) {
140
140
  this.previous.push(track);
141
141
  }
@@ -261,7 +261,7 @@ class MemoryQueue extends Array {
261
261
  * @returns The previous tracks.
262
262
  */
263
263
  getPrevious() {
264
- this.previous = this.previous.map((t) => Utils_1.TrackUtils.revive(t));
264
+ this.previous = this.previous.map((track) => Utils_1.TrackUtils.revive(track));
265
265
  return [...this.previous];
266
266
  }
267
267
  /**
@@ -274,7 +274,7 @@ class MemoryQueue extends Array {
274
274
  * @returns The tracks in the queue.
275
275
  */
276
276
  getTracks() {
277
- this.forEach((t) => Utils_1.TrackUtils.revive(t));
277
+ this.forEach((track) => Utils_1.TrackUtils.revive(track));
278
278
  return [...this]; // clone to avoid direct mutation
279
279
  }
280
280
  /**
@@ -44,7 +44,7 @@ class RedisQueue {
44
44
  const isArray = Array.isArray(track);
45
45
  const tracks = isArray ? track : [track];
46
46
  // Serialize tracks
47
- const serialized = tracks.map((t) => this.serialize(t));
47
+ const serialized = tracks.map((track) => this.serialize(track));
48
48
  const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
49
49
  // Set current track if none exists
50
50
  if (!(await this.getCurrent())) {
@@ -524,9 +524,9 @@ class RedisQueue {
524
524
  const users = [...userMap.keys()];
525
525
  const queues = users.map((id) => userMap.get(id));
526
526
  const shuffledQueue = [];
527
- while (queues.some((q) => q.length > 0)) {
528
- for (const q of queues) {
529
- const track = q.shift();
527
+ while (queues.some((queue) => queue.length > 0)) {
528
+ for (const queue of queues) {
529
+ const track = queue.shift();
530
530
  if (track)
531
531
  shuffledQueue.push(track);
532
532
  }
@@ -216,8 +216,8 @@ class Manager extends events_1.EventEmitter {
216
216
  const search = isUrl ? _query.query : `${_source}:${_query.query}`;
217
217
  this.emit(Enums_1.ManagerEventTypes.Debug, isUrl ? `[MANAGER] Performing search for: ${_query.query}` : `[MANAGER] Performing ${_source} search for: ${_query.query}`);
218
218
  try {
219
- const res = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`));
220
- if (!res) {
219
+ const lavalinkResponse = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`));
220
+ if (!lavalinkResponse) {
221
221
  throw new MagmastreamError_1.MagmaStreamError({
222
222
  code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
223
223
  message: `No results returned from Lavalink for query "${search}".`,
@@ -225,16 +225,16 @@ class Manager extends events_1.EventEmitter {
225
225
  });
226
226
  }
227
227
  let result;
228
- switch (res.loadType) {
228
+ switch (lavalinkResponse.loadType) {
229
229
  case Enums_1.LoadTypes.Search: {
230
- const tracks = res.data.map((t) => Utils_1.TrackUtils.build(t, requester));
231
- result = { loadType: res.loadType, tracks };
230
+ const tracks = lavalinkResponse.data.map((track) => Utils_1.TrackUtils.build(track, requester));
231
+ result = { loadType: lavalinkResponse.loadType, tracks };
232
232
  break;
233
233
  }
234
234
  case Enums_1.LoadTypes.Short:
235
235
  case Enums_1.LoadTypes.Track: {
236
- const track = Utils_1.TrackUtils.build(res.data, requester);
237
- result = { loadType: res.loadType, tracks: [track] };
236
+ const track = Utils_1.TrackUtils.build(lavalinkResponse.data, requester);
237
+ result = { loadType: lavalinkResponse.loadType, tracks: [track] };
238
238
  break;
239
239
  }
240
240
  case Enums_1.LoadTypes.Album:
@@ -243,10 +243,10 @@ class Manager extends events_1.EventEmitter {
243
243
  case Enums_1.LoadTypes.Podcast:
244
244
  case Enums_1.LoadTypes.Show:
245
245
  case Enums_1.LoadTypes.Playlist: {
246
- const playlistData = res.data;
247
- const tracks = playlistData.tracks.map((t) => Utils_1.TrackUtils.build(t, requester));
246
+ const playlistData = lavalinkResponse.data;
247
+ const tracks = playlistData.tracks.map((track) => Utils_1.TrackUtils.build(track, requester));
248
248
  result = {
249
- loadType: res.loadType,
249
+ loadType: lavalinkResponse.loadType,
250
250
  tracks,
251
251
  playlist: {
252
252
  name: playlistData.info.name,
@@ -259,7 +259,7 @@ class Manager extends events_1.EventEmitter {
259
259
  break;
260
260
  }
261
261
  default:
262
- result = { loadType: res.loadType };
262
+ result = { loadType: lavalinkResponse.loadType, tracks: [] };
263
263
  }
264
264
  if (this.options.normalizeYouTubeTitles && "tracks" in result) {
265
265
  const processTrack = (track) => {
@@ -275,7 +275,9 @@ class Manager extends events_1.EventEmitter {
275
275
  result.playlist.tracks = result.tracks;
276
276
  }
277
277
  }
278
- const summary = "tracks" in result ? result.tracks.map((t) => Object.fromEntries(Object.entries(t).filter(([key]) => key !== "requester"))) : [];
278
+ const summary = "tracks" in result
279
+ ? result.tracks.map((track) => Object.fromEntries(Object.entries(track).filter(([key]) => key !== "requester")))
280
+ : [];
279
281
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Result search for ${_query.query}: ${Utils_1.JSONUtils.safe(summary, 2)}`);
280
282
  return result;
281
283
  }
@@ -412,14 +414,14 @@ class Manager extends events_1.EventEmitter {
412
414
  message: "No available nodes to decode tracks.",
413
415
  });
414
416
  }
415
- const res = (await node.rest.post("/v4/decodetracks", Utils_1.JSONUtils.safe(tracks, 2)).catch((err) => reject(err)));
416
- if (!res) {
417
+ const decodedTrackData = (await node.rest.post("/v4/decodetracks", Utils_1.JSONUtils.safe(tracks, 2)).catch((err) => reject(err)));
418
+ if (!decodedTrackData) {
417
419
  throw new MagmastreamError_1.MagmaStreamError({
418
420
  code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
419
421
  message: "No decoded tracks returned from node.",
420
422
  });
421
423
  }
422
- return resolve(res);
424
+ return resolve(decodedTrackData);
423
425
  });
424
426
  }
425
427
  /**
@@ -429,9 +431,9 @@ class Manager extends events_1.EventEmitter {
429
431
  * @throws Will throw an error if no nodes are available or if the API request fails.
430
432
  */
431
433
  async decodeTrack(track) {
432
- const res = await this.decodeTracks([track]);
434
+ const decodedTracks = await this.decodeTracks([track]);
433
435
  // Since we're only decoding one track, we can just return the first element of the array
434
- return res[0];
436
+ return decodedTracks[0];
435
437
  }
436
438
  /**
437
439
  * Saves player states.
@@ -508,7 +510,7 @@ class Manager extends events_1.EventEmitter {
508
510
  const player = this.create(playerOptions);
509
511
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Recreating player: ${guildId}`);
510
512
  if (state.isAutoplay) {
511
- const savedUser = state.data?.clientUser;
513
+ const savedUser = (state.data?.Internal_AutoplayUser || state.data?.clientUser);
512
514
  if (savedUser) {
513
515
  const autoPlayUser = await player.manager.resolveUser(savedUser);
514
516
  player.setAutoplay(true, autoPlayUser, state.autoplayTries);
@@ -577,7 +579,7 @@ class Manager extends events_1.EventEmitter {
577
579
  }
578
580
  async restorePreviousQueue(player, state) {
579
581
  if (state.queue.previous.length > 0) {
580
- const validPrevious = state.queue.previous.filter((t) => t !== null && typeof t.identifier === "string");
582
+ const validPrevious = state.queue.previous.filter((track) => track !== null && typeof track.identifier === "string");
581
583
  if (validPrevious.length > 0)
582
584
  await player.queue.addPrevious(validPrevious);
583
585
  }
@@ -604,31 +606,31 @@ class Manager extends events_1.EventEmitter {
604
606
  restoreFilters(player, state) {
605
607
  const filterActions = {
606
608
  bassboost: () => player.filters.bassBoost(state.filters.bassBoostlevel),
607
- distort: (e) => player.filters.distort(e),
609
+ distort: (isEnabled) => player.filters.distort(isEnabled),
608
610
  setDistortion: () => player.filters.setDistortion(state.filters.distortion),
609
- eightD: (e) => player.filters.eightD(e),
611
+ eightD: (isEnabled) => player.filters.eightD(isEnabled),
610
612
  setKaraoke: () => player.filters.setKaraoke(state.filters.karaoke),
611
- nightcore: (e) => player.filters.nightcore(e),
612
- slowmo: (e) => player.filters.slowmo(e),
613
- soft: (e) => player.filters.soft(e),
614
- trebleBass: (e) => player.filters.trebleBass(e),
613
+ nightcore: (isEnabled) => player.filters.nightcore(isEnabled),
614
+ slowmo: (isEnabled) => player.filters.slowmo(isEnabled),
615
+ soft: (isEnabled) => player.filters.soft(isEnabled),
616
+ trebleBass: (isEnabled) => player.filters.trebleBass(isEnabled),
615
617
  setTimescale: () => player.filters.setTimescale(state.filters.timescale),
616
- tv: (e) => player.filters.tv(e),
618
+ tv: (isEnabled) => player.filters.tv(isEnabled),
617
619
  vibrato: () => player.filters.setVibrato(state.filters.vibrato),
618
- vaporwave: (e) => player.filters.vaporwave(e),
619
- pop: (e) => player.filters.pop(e),
620
- party: (e) => player.filters.party(e),
621
- earrape: (e) => player.filters.earrape(e),
622
- electronic: (e) => player.filters.electronic(e),
623
- radio: (e) => player.filters.radio(e),
620
+ vaporwave: (isEnabled) => player.filters.vaporwave(isEnabled),
621
+ pop: (isEnabled) => player.filters.pop(isEnabled),
622
+ party: (isEnabled) => player.filters.party(isEnabled),
623
+ earrape: (isEnabled) => player.filters.earrape(isEnabled),
624
+ electronic: (isEnabled) => player.filters.electronic(isEnabled),
625
+ radio: (isEnabled) => player.filters.radio(isEnabled),
624
626
  setRotation: () => player.filters.setRotation(state.filters.rotation),
625
- tremolo: (e) => player.filters.tremolo(e),
626
- china: (e) => player.filters.china(e),
627
- chipmunk: (e) => player.filters.chipmunk(e),
628
- darthvader: (e) => player.filters.darthvader(e),
629
- daycore: (e) => player.filters.daycore(e),
630
- doubletime: (e) => player.filters.doubletime(e),
631
- demon: (e) => player.filters.demon(e),
627
+ tremolo: (isEnabled) => player.filters.tremolo(isEnabled),
628
+ china: (isEnabled) => player.filters.china(isEnabled),
629
+ chipmunk: (isEnabled) => player.filters.chipmunk(isEnabled),
630
+ darthvader: (isEnabled) => player.filters.darthvader(isEnabled),
631
+ daycore: (isEnabled) => player.filters.daycore(isEnabled),
632
+ doubletime: (isEnabled) => player.filters.doubletime(isEnabled),
633
+ demon: (isEnabled) => player.filters.demon(isEnabled),
632
634
  };
633
635
  for (const [filter, isEnabled] of Object.entries(state.filters.filterStatus)) {
634
636
  if (isEnabled && filterActions[filter])
@@ -23,7 +23,8 @@ export declare class Node {
23
23
  private reconnectTimeout?;
24
24
  private reconnectAttempts;
25
25
  private redisPrefix?;
26
- private sessionIdsMap;
26
+ /** Session ID sent in the reconnect header for resumption — cleared once the ready op is received. */
27
+ private pendingResumeSessionId;
27
28
  /**
28
29
  * Creates an instance of Node.
29
30
  * @param manager - The manager for the node.
@@ -149,7 +150,7 @@ export declare class Node {
149
150
  * @emits {nodeRaw} - Emits a nodeRaw event with the raw message received from the WebSocket connection.
150
151
  * @private
151
152
  */
152
- protected message(d: Buffer | string): Promise<void>;
153
+ protected message(messagePayload: Buffer | string): Promise<void>;
153
154
  /**
154
155
  * Handles an event emitted from the Lavalink node.
155
156
  * @param {PlayerEvent & PlayerEvents} payload The event emitted from the node.
@@ -10,7 +10,7 @@ const fs_1 = tslib_1.__importDefault(require("fs"));
10
10
  const path_1 = tslib_1.__importDefault(require("path"));
11
11
  const Enums_1 = require("./Enums");
12
12
  const MagmastreamError_1 = require("./MagmastreamError");
13
- const validSponsorBlocks = Object.values(Enums_1.SponsorBlockSegment).map((v) => v.toLowerCase());
13
+ const validSponsorBlocks = Object.values(Enums_1.SponsorBlockSegment).map((segment) => segment.toLowerCase());
14
14
  class Node {
15
15
  manager;
16
16
  options;
@@ -29,9 +29,10 @@ class Node {
29
29
  /** Whether the node is a NodeLink. */
30
30
  isNodeLink = false;
31
31
  reconnectTimeout;
32
- reconnectAttempts = 1;
32
+ reconnectAttempts = 0;
33
33
  redisPrefix;
34
- sessionIdsMap = new Map();
34
+ /** Session ID sent in the reconnect header for resumption — cleared once the ready op is received. */
35
+ pendingResumeSessionId = null;
35
36
  /**
36
37
  * Creates an instance of Node.
37
38
  * @param manager - The manager for the node.
@@ -153,8 +154,6 @@ class Node {
153
154
  try {
154
155
  const raw = fs_1.default.readFileSync(filePath, "utf-8").trim();
155
156
  this.sessionId = raw.length ? raw : null;
156
- if (this.sessionId)
157
- this.sessionIdsMap.set(this.getCompositeKey(), this.sessionId);
158
157
  }
159
158
  catch {
160
159
  this.sessionId = null;
@@ -169,7 +168,6 @@ class Node {
169
168
  const sid = await this.manager.redis.hget(key, compositeKey);
170
169
  this.sessionId = sid ?? null;
171
170
  if (this.sessionId) {
172
- this.sessionIdsMap.set(compositeKey, this.sessionId);
173
171
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Restored sessionId for ${compositeKey}: ${this.sessionId}`);
174
172
  }
175
173
  }
@@ -210,7 +208,6 @@ class Node {
210
208
  if (this.sessionId) {
211
209
  fs_1.default.writeFileSync(tmpPath, this.sessionId, "utf-8");
212
210
  fs_1.default.renameSync(tmpPath, filePath);
213
- this.sessionIdsMap.set(this.getCompositeKey(), this.sessionId);
214
211
  }
215
212
  else {
216
213
  try {
@@ -218,7 +215,6 @@ class Node {
218
215
  fs_1.default.unlinkSync(filePath);
219
216
  }
220
217
  catch { }
221
- this.sessionIdsMap.delete(this.getCompositeKey());
222
218
  }
223
219
  }
224
220
  async updateSessionIdRedis() {
@@ -228,11 +224,9 @@ class Node {
228
224
  try {
229
225
  if (this.sessionId) {
230
226
  await this.manager.redis.hset(key, compositeKey, this.sessionId);
231
- this.sessionIdsMap.set(compositeKey, this.sessionId);
232
227
  }
233
228
  else {
234
229
  await this.manager.redis.hdel(key, compositeKey);
235
- this.sessionIdsMap.delete(compositeKey);
236
230
  }
237
231
  }
238
232
  catch (err) {
@@ -262,8 +256,16 @@ class Node {
262
256
  "User-Id": this.manager.options.clientId,
263
257
  "Client-Name": this.manager.options.clientName,
264
258
  };
265
- if (typeof this.sessionId === "string" && this.sessionId.length > 0) {
266
- headers["Session-Id"] = this.sessionId;
259
+ // Capture resume session ID for the WS header, then clear this.sessionId.
260
+ // REST calls guard on this.sessionId being non-null — keeping the stale value
261
+ // would let updatePlayer/destroyPlayer fire with an invalid session during the
262
+ // reconnect window (between connect() and the 'ready' op).
263
+ // pendingResumeSessionId is kept so the 'ready' handler can still compute
264
+ // hadPreviousSession correctly. this.sessionId is re-set by 'ready'.
265
+ this.pendingResumeSessionId = this.sessionId;
266
+ this.sessionId = null;
267
+ if (typeof this.pendingResumeSessionId === "string" && this.pendingResumeSessionId.length > 0) {
268
+ headers["Session-Id"] = this.pendingResumeSessionId;
267
269
  }
268
270
  this.socket = new ws_1.default(`ws${this.options.useSSL ? "s" : ""}://${this.address}/v4/websocket`, { headers });
269
271
  this.socket.on("open", this.open.bind(this));
@@ -274,7 +276,7 @@ class Node {
274
276
  const debugInfo = {
275
277
  connected: this.connected,
276
278
  address: this.address,
277
- sessionId: this.sessionId,
279
+ pendingResumeSessionId: this.pendingResumeSessionId,
278
280
  options: {
279
281
  clientId: this.manager.options.clientId,
280
282
  clientName: this.manager.options.clientName,
@@ -300,18 +302,18 @@ class Node {
300
302
  identifier: this.options.identifier,
301
303
  address: this.address,
302
304
  sessionId: this.sessionId,
303
- playerCount: this.manager.players.filter((p) => p.node == this).size,
305
+ playerCount: this.manager.players.filter((player) => player.node == this).size,
304
306
  };
305
307
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Destroying node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
306
308
  // Automove all players connected to that node
307
- const players = this.manager.players.filter((p) => p.node == this);
309
+ const players = this.manager.players.filter((player) => player.node == this);
308
310
  if (players.size) {
309
311
  await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
310
312
  }
311
313
  // Always clear reconnect state regardless of connection status
312
314
  clearTimeout(this.reconnectTimeout);
313
315
  this.reconnectTimeout = undefined;
314
- this.reconnectAttempts = 1;
316
+ this.reconnectAttempts = 0;
315
317
  // Only close the socket if it is actually open
316
318
  if (this.connected) {
317
319
  this.socket.close(1000, "destroy");
@@ -347,7 +349,8 @@ class Node {
347
349
  };
348
350
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Reconnecting node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
349
351
  this.reconnectTimeout = setTimeout(async () => {
350
- if (this.reconnectAttempts >= this.options.maxRetryAttempts) {
352
+ this.reconnectAttempts++;
353
+ if (this.reconnectAttempts > this.options.maxRetryAttempts) {
351
354
  const error = new MagmastreamError_1.MagmaStreamError({
352
355
  code: Enums_1.MagmaStreamErrorCode.NODE_RECONNECT_FAILED,
353
356
  message: `Unable to reconnect after ${this.options.maxRetryAttempts} attempts.`,
@@ -360,7 +363,6 @@ class Node {
360
363
  this.socket = null;
361
364
  this.manager.emit(Enums_1.ManagerEventTypes.NodeReconnect, this);
362
365
  await this.connect();
363
- this.reconnectAttempts++;
364
366
  }, this.options.retryDelayMs);
365
367
  }
366
368
  /**
@@ -382,13 +384,15 @@ class Node {
382
384
  open() {
383
385
  if (this.reconnectTimeout)
384
386
  clearTimeout(this.reconnectTimeout);
387
+ this.reconnectTimeout = undefined;
388
+ this.reconnectAttempts = 0;
385
389
  const debugInfo = {
386
390
  identifier: this.options.identifier,
387
391
  connected: this.connected,
388
392
  };
389
393
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connected node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
390
394
  this.manager.emit(Enums_1.ManagerEventTypes.NodeConnect, this);
391
- const playersOnBackupNode = this.manager.players.filter((p) => p.node.options.isBackup);
395
+ const playersOnBackupNode = this.manager.players.filter((player) => player.node.options.isBackup);
392
396
  if (playersOnBackupNode.size) {
393
397
  Promise.all(Array.from(playersOnBackupNode.values(), (player) => player.moveNode(this.options.identifier)));
394
398
  }
@@ -414,8 +418,12 @@ class Node {
414
418
  };
415
419
  this.manager.emit(Enums_1.ManagerEventTypes.NodeDisconnect, this, { code, reason });
416
420
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
421
+ // Null the session ID immediately so REST calls during the reconnect delay
422
+ // window don't fire with a stale session. connect() reloads it from storage
423
+ // into pendingResumeSessionId for the WS Session-Id header.
424
+ this.sessionId = null;
417
425
  if (this.manager.useableNode) {
418
- const players = this.manager.players.filter((p) => p.node.options.identifier == this.options.identifier);
426
+ const players = this.manager.players.filter((player) => player.node.options.identifier == this.options.identifier);
419
427
  if (players.size) {
420
428
  await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
421
429
  }
@@ -457,12 +465,12 @@ class Node {
457
465
  * @emits {nodeRaw} - Emits a nodeRaw event with the raw message received from the WebSocket connection.
458
466
  * @private
459
467
  */
460
- async message(d) {
461
- if (Array.isArray(d))
462
- d = Buffer.concat(d);
463
- else if (d instanceof ArrayBuffer)
464
- d = Buffer.from(d);
465
- const payload = JSON.parse(d.toString());
468
+ async message(messagePayload) {
469
+ if (Array.isArray(messagePayload))
470
+ messagePayload = Buffer.concat(messagePayload);
471
+ else if (messagePayload instanceof ArrayBuffer)
472
+ messagePayload = Buffer.from(messagePayload);
473
+ const payload = JSON.parse(messagePayload.toString());
466
474
  if (!payload.op)
467
475
  return;
468
476
  this.manager.emit(Enums_1.ManagerEventTypes.NodeRaw, payload);
@@ -491,7 +499,11 @@ class Node {
491
499
  case "ready":
492
500
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
493
501
  this.rest.setSessionId(payload.sessionId);
494
- const hadPreviousSession = this.sessionId && this.sessionId !== payload.sessionId;
502
+ // pendingResumeSessionId holds what we sent in Session-Id header (if anything).
503
+ // Use it — not this.sessionId which was nulled in connect() — to detect whether
504
+ // we attempted resumption with a different session than what Lavalink gave back.
505
+ const hadPreviousSession = this.pendingResumeSessionId && this.pendingResumeSessionId !== payload.sessionId;
506
+ this.pendingResumeSessionId = null;
495
507
  this.sessionId = payload.sessionId;
496
508
  await this.updateSessionId();
497
509
  this.info = await this.fetchInfo();
@@ -524,6 +536,11 @@ class Node {
524
536
  return;
525
537
  const track = await player.queue.getCurrent();
526
538
  const type = payload.type;
539
+ const TRACK_EVENTS = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
540
+ if (!track && TRACK_EVENTS.includes(type)) {
541
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[Node] Received ${type} for guild ${payload.guildId} but queue has no current track — ignoring.`);
542
+ return;
543
+ }
527
544
  let error;
528
545
  switch (type) {
529
546
  case "TrackStartEvent":
@@ -1121,15 +1138,15 @@ class Node {
1121
1138
  context: { identifier: this.options.identifier, guildId: player.guildId },
1122
1139
  });
1123
1140
  }
1124
- if (segments.some((v) => !validSponsorBlocks.includes(v.toLowerCase()))) {
1141
+ if (segments.some((segment) => !validSponsorBlocks.includes(segment.toLowerCase()))) {
1125
1142
  throw new MagmastreamError_1.MagmaStreamError({
1126
1143
  code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1127
- message: `Invalid SponsorBlock segments provided. Valid ones are: ${validSponsorBlocks.map((v) => `'${v}'`).join(", ")}`,
1144
+ message: `Invalid SponsorBlock segments provided. Valid ones are: ${validSponsorBlocks.map((segment) => `'${segment}'`).join(", ")}`,
1128
1145
  context: { identifier: this.options.identifier, guildId: player.guildId, invalidSegments: segments },
1129
1146
  });
1130
1147
  }
1131
1148
  try {
1132
- await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((v) => v.toLowerCase()));
1149
+ await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((segment) => segment.toLowerCase()));
1133
1150
  }
1134
1151
  catch (err) {
1135
1152
  throw err instanceof MagmastreamError_1.MagmaStreamError
@@ -55,7 +55,7 @@ export declare class Player {
55
55
  /** Should only be used when the node is a NodeLink */
56
56
  protected voiceReceiverWsClient: WebSocket | null;
57
57
  protected isConnectToVoiceReceiver: boolean;
58
- protected voiceReceiverReconnectTimeout: NodeJS.Timeout | null;
58
+ protected voiceReceiverReconnectTimeout?: NodeJS.Timeout;
59
59
  protected voiceReceiverAttempt: number;
60
60
  protected voiceReceiverReconnectTries: number;
61
61
  /**
@@ -263,7 +263,7 @@ class Player {
263
263
  }
264
264
  if (this.voiceReceiverReconnectTimeout) {
265
265
  clearTimeout(this.voiceReceiverReconnectTimeout);
266
- this.voiceReceiverReconnectTimeout = null;
266
+ this.voiceReceiverReconnectTimeout = undefined;
267
267
  }
268
268
  if (this.voiceReceiverWsClient) {
269
269
  this.voiceReceiverWsClient.removeAllListeners();
@@ -1043,7 +1043,7 @@ class Player {
1043
1043
  this.voiceReceiverWsClient = new ws_1.WebSocket(`${useSSL ? "wss" : "ws"}://${host}:${port}/connection/data`, { headers });
1044
1044
  this.voiceReceiverWsClient.on("open", () => this.openVoiceReceiver());
1045
1045
  this.voiceReceiverWsClient.on("error", (err) => this.onVoiceReceiverError(err));
1046
- this.voiceReceiverWsClient.on("message", (data) => this.onVoiceReceiverMessage(data.toString()));
1046
+ this.voiceReceiverWsClient.on("message", (rawMessage) => this.onVoiceReceiverMessage(rawMessage.toString()));
1047
1047
  this.voiceReceiverWsClient.on("close", (code, reason) => this.closeVoiceReceiver(code, reason.toString()));
1048
1048
  }
1049
1049
  /**
@@ -1118,7 +1118,7 @@ class Player {
1118
1118
  async openVoiceReceiver() {
1119
1119
  if (this.voiceReceiverReconnectTimeout)
1120
1120
  clearTimeout(this.voiceReceiverReconnectTimeout);
1121
- this.voiceReceiverReconnectTimeout = null;
1121
+ this.voiceReceiverReconnectTimeout = undefined;
1122
1122
  this.isConnectToVoiceReceiver = true;
1123
1123
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Opened voice receiver for player ${this.guildId}`);
1124
1124
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverConnect, this);
@@ -1166,18 +1166,18 @@ class Player {
1166
1166
  * @returns {Promise<void>} - A promise that resolves when the voice state is updated.
1167
1167
  */
1168
1168
  async updateVoice() {
1169
- const vs = this.voiceState;
1170
- const ev = vs?.event;
1171
- if (!vs?.channelId || !vs?.sessionId || !ev?.token || !ev?.endpoint)
1169
+ const voiceState = this.voiceState;
1170
+ const voiceEvent = voiceState?.event;
1171
+ if (!voiceState?.channelId || !voiceState?.sessionId || !voiceEvent?.token || !voiceEvent?.endpoint)
1172
1172
  return;
1173
1173
  await this.node.rest.updatePlayer({
1174
1174
  guildId: this.options.guildId,
1175
1175
  data: {
1176
1176
  voice: {
1177
- token: ev.token,
1178
- endpoint: ev.endpoint,
1179
- sessionId: vs.sessionId,
1180
- channelId: vs.channelId,
1177
+ token: voiceEvent.token,
1178
+ endpoint: voiceEvent.endpoint,
1179
+ sessionId: voiceState.sessionId,
1180
+ channelId: voiceState.channelId,
1181
1181
  },
1182
1182
  },
1183
1183
  });
@@ -497,6 +497,8 @@ export interface LavaPlayer {
497
497
  export interface ErrorOrEmptySearchResult {
498
498
  /** The load type of the result. */
499
499
  loadType: LoadTypes.Empty | LoadTypes.Error;
500
+ /** Always an empty array for error/empty results. */
501
+ tracks: [];
500
502
  }
501
503
  /**
502
504
  * Track Search Result
@@ -62,8 +62,8 @@ class TrackUtils {
62
62
  static isTrack(track) {
63
63
  if (typeof track !== "object" || track === null)
64
64
  return false;
65
- const t = track;
66
- return REQUIRED_TRACK_KEYS.every((key) => typeof t[key] === "string");
65
+ const trackRecord = track;
66
+ return REQUIRED_TRACK_KEYS.every((key) => typeof trackRecord[key] === "string");
67
67
  }
68
68
  /**
69
69
  * Checks if the provided argument is a valid Track array.
@@ -295,7 +295,7 @@ class AutoPlayUtils {
295
295
  const resolvedTracks = await this.resolveTracksFromQuery(`${randomTrack.artist.name} - ${randomTrack.name}`, this.manager.options.defaultSearchPlatform, track.requester);
296
296
  if (!resolvedTracks.length)
297
297
  return [];
298
- const filteredTracks = resolvedTracks.filter((t) => t.uri !== track.uri);
298
+ const filteredTracks = resolvedTracks.filter((resolvedTrack) => resolvedTrack.uri !== track.uri);
299
299
  if (!filteredTracks.length) {
300
300
  return [];
301
301
  }
@@ -392,10 +392,10 @@ class AutoPlayUtils {
392
392
  const h2 = element.querySelector('h2[itemprop="name"]');
393
393
  if (!h2)
394
394
  return null;
395
- const a = h2.querySelector('a[itemprop="url"]');
396
- if (!a)
395
+ const anchorElement = h2.querySelector('a[itemprop="url"]');
396
+ if (!anchorElement)
397
397
  return null;
398
- const href = a.getAttribute("href");
398
+ const href = anchorElement.getAttribute("href");
399
399
  return href ? `https://soundcloud.com${href}` : null;
400
400
  })
401
401
  .filter(Boolean);
@@ -433,7 +433,7 @@ class AutoPlayUtils {
433
433
  searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}&index=${randomIndex}`;
434
434
  } while (track.uri.includes(searchURI));
435
435
  const resolvedTracks = await this.resolveTracksFromQuery(searchURI, Enums_1.SearchPlatform.YouTube, requester);
436
- const filteredTracks = resolvedTracks.filter((t) => t.uri !== track.uri);
436
+ const filteredTracks = resolvedTracks.filter((resolvedTrack) => resolvedTrack.uri !== track.uri);
437
437
  return filteredTracks;
438
438
  }
439
439
  case Enums_1.AutoPlayPlatform.Tidal: {
@@ -596,7 +596,7 @@ class AutoPlayUtils {
596
596
  context: { recommendedResult },
597
597
  });
598
598
  }
599
- return data.map((d) => TrackUtils.build(d, requester, true));
599
+ return data.map((trackData) => TrackUtils.build(trackData, requester, true));
600
600
  }
601
601
  case Enums_1.LoadTypes.Album:
602
602
  case Enums_1.LoadTypes.Artist:
@@ -606,7 +606,7 @@ class AutoPlayUtils {
606
606
  case Enums_1.LoadTypes.Playlist: {
607
607
  const data = recommendedResult.data;
608
608
  if (this.isPlaylistRawData(data)) {
609
- return data.tracks.map((d) => TrackUtils.build(d, requester, true));
609
+ return data.tracks.map((trackData) => TrackUtils.build(trackData, requester, true));
610
610
  }
611
611
  throw new MagmastreamError_1.MagmaStreamError({
612
612
  code: Enums_1.MagmaStreamErrorCode.UTILS_AUTOPLAY_BUILD_FAILED,
@@ -666,9 +666,9 @@ class PlayerUtils {
666
666
  if (!obj || typeof obj !== "object")
667
667
  return obj;
668
668
  const result = {};
669
- for (const [k, v] of Object.entries(obj)) {
670
- if (!isNonSerializable(v)) {
671
- result[k] = v;
669
+ for (const [k, entryValue] of Object.entries(obj)) {
670
+ if (!isNonSerializable(entryValue)) {
671
+ result[k] = entryValue;
672
672
  }
673
673
  }
674
674
  return result;
@@ -690,8 +690,8 @@ class PlayerUtils {
690
690
  }
691
691
  };
692
692
  const safeCurrent = current ? serializeTrack(current) : null;
693
- const safeTracks = tracks.map(serializeTrack).filter((t) => t !== null);
694
- const safePrevious = previous.map(serializeTrack).filter((t) => t !== null);
693
+ const safeTracks = tracks.map(serializeTrack).filter((serializedTrack) => serializedTrack !== null);
694
+ const safePrevious = previous.map(serializeTrack).filter((serializedTrack) => serializedTrack !== null);
695
695
  let safeNode = null;
696
696
  if (player.node) {
697
697
  try {
@@ -721,6 +721,8 @@ class PlayerUtils {
721
721
  paused: player.paused,
722
722
  playing: player.playing,
723
723
  position: player.position,
724
+ isAutoplay: player.isAutoplay,
725
+ autoplayTries: player.autoplayTries,
724
726
  trackRepeat: player.trackRepeat,
725
727
  queueRepeat: player.queueRepeat,
726
728
  dynamicRepeat: player.dynamicRepeat,
@@ -63,12 +63,12 @@ class CloudstormManager extends Manager_1.Manager {
63
63
  }
64
64
  // CloudStorm has no user/guild cache — return minimal portable info only.
65
65
  async resolveUser(user) {
66
- const id = typeof user === "string" ? user : String(user.id);
67
- const cached = this.getUserFromCache(id);
66
+ const userId = typeof user === "string" ? user : String(user.id);
67
+ const cached = this.getUserFromCache(userId);
68
68
  if (cached)
69
69
  return cached;
70
70
  return {
71
- id,
71
+ id: userId,
72
72
  username: typeof user === "string" ? undefined : user.username,
73
73
  };
74
74
  }
@@ -50,16 +50,16 @@ class DiscordJSManager extends Manager_1.Manager {
50
50
  guild.shard.send(packet);
51
51
  }
52
52
  async resolveUser(user) {
53
- const id = typeof user === "string" ? user : String(user.id);
54
- const cached = this.client.users.cache.get(id);
53
+ const userId = typeof user === "string" ? user : String(user.id);
54
+ const cached = this.client.users.cache.get(userId);
55
55
  if (cached)
56
56
  return cached;
57
57
  try {
58
- const fetched = await this.client.users.fetch(id);
58
+ const fetched = await this.client.users.fetch(userId);
59
59
  return fetched;
60
60
  }
61
61
  catch {
62
- return { id, username: typeof user === "string" ? undefined : user.username };
62
+ return { id: userId, username: typeof user === "string" ? undefined : user.username };
63
63
  }
64
64
  }
65
65
  resolveGuild(guildId) {
@@ -57,14 +57,14 @@ class DiscordenoManager extends Manager_1.Manager {
57
57
  * Uses user-provided cache getter if available, otherwise falls back to minimal info.
58
58
  */
59
59
  async resolveUser(user) {
60
- const id = typeof user === "string" ? user : String(user.id);
60
+ const userId = typeof user === "string" ? user : String(user.id);
61
61
  // Try user-provided cache getter
62
- const cached = this.getUserFromCache(id);
62
+ const cached = this.getUserFromCache(userId);
63
63
  if (cached)
64
64
  return cached;
65
65
  // Fallback: return minimal info
66
66
  return {
67
- id,
67
+ id: userId,
68
68
  username: typeof user === "string" ? undefined : user.username,
69
69
  };
70
70
  }
@@ -35,12 +35,12 @@ class ErisManager extends Manager_1.Manager {
35
35
  guild.shard.sendWS(packet.op, packet.d);
36
36
  }
37
37
  async resolveUser(user) {
38
- const id = typeof user === "string" ? user : String(user.id);
39
- const cached = this.client.users.get(id);
38
+ const userId = typeof user === "string" ? user : String(user.id);
39
+ const cached = this.client.users.get(userId);
40
40
  if (cached)
41
41
  return cached;
42
42
  return {
43
- id,
43
+ id: userId,
44
44
  username: typeof user === "string" ? undefined : user.username,
45
45
  };
46
46
  }
@@ -35,12 +35,12 @@ class OceanicManager extends Manager_1.Manager {
35
35
  guild.shard.send(packet.op, packet.d);
36
36
  }
37
37
  async resolveUser(user) {
38
- const id = typeof user === "string" ? user : String(user.id);
39
- const cached = this.client.users.get(id);
38
+ const userId = typeof user === "string" ? user : String(user.id);
39
+ const cached = this.client.users.get(userId);
40
40
  if (cached)
41
41
  return cached;
42
42
  return {
43
- id,
43
+ id: userId,
44
44
  username: typeof user === "string" ? undefined : user.username,
45
45
  };
46
46
  }
@@ -63,15 +63,15 @@ class SeyfertManager extends Manager_1.Manager {
63
63
  }
64
64
  }
65
65
  async resolveUser(user) {
66
- const id = typeof user === "string" ? user : String(user.id);
67
- const cached = this.client.cache.users?.get(id);
66
+ const userId = typeof user === "string" ? user : String(user.id);
67
+ const cached = this.client.cache.users?.get(userId);
68
68
  if (cached)
69
69
  return cached;
70
70
  try {
71
- return await this.client.users.fetch(id);
71
+ return await this.client.users.fetch(userId);
72
72
  }
73
73
  catch {
74
- return { id, username: typeof user === "string" ? undefined : user.username };
74
+ return { id: userId, username: typeof user === "string" ? undefined : user.username };
75
75
  }
76
76
  }
77
77
  resolveGuild(guildId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.10.3-dev.0",
3
+ "version": "2.10.3-dev.2",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",