magmastream 2.10.2-dev.4 → 2.10.3-alpha.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 };
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.
@@ -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;
@@ -31,7 +31,8 @@ class Node {
31
31
  reconnectTimeout;
32
32
  reconnectAttempts = 1;
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,
@@ -295,25 +297,29 @@ class Node {
295
297
  * @returns {Promise<void>} A promise that resolves when the node and its resources have been destroyed.
296
298
  */
297
299
  async destroy() {
298
- if (!this.connected)
299
- return;
300
300
  const debugInfo = {
301
301
  connected: this.connected,
302
302
  identifier: this.options.identifier,
303
303
  address: this.address,
304
304
  sessionId: this.sessionId,
305
- playerCount: this.manager.players.filter((p) => p.node == this).size,
305
+ playerCount: this.manager.players.filter((player) => player.node == this).size,
306
306
  };
307
307
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Destroying node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
308
308
  // Automove all players connected to that node
309
- const players = this.manager.players.filter((p) => p.node == this);
309
+ const players = this.manager.players.filter((player) => player.node == this);
310
310
  if (players.size) {
311
311
  await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
312
312
  }
313
- this.socket.close(1000, "destroy");
314
- this.socket.removeAllListeners();
315
- this.reconnectAttempts = 1;
313
+ // Always clear reconnect state regardless of connection status
316
314
  clearTimeout(this.reconnectTimeout);
315
+ this.reconnectTimeout = undefined;
316
+ this.reconnectAttempts = 1;
317
+ // Only close the socket if it is actually open
318
+ if (this.connected) {
319
+ this.socket.close(1000, "destroy");
320
+ this.socket.removeAllListeners();
321
+ }
322
+ this.socket = null;
317
323
  this.manager.emit(Enums_1.ManagerEventTypes.NodeDestroy, this);
318
324
  this.manager.nodes.delete(this.options.identifier);
319
325
  }
@@ -384,7 +390,7 @@ class Node {
384
390
  };
385
391
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connected node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
386
392
  this.manager.emit(Enums_1.ManagerEventTypes.NodeConnect, this);
387
- const playersOnBackupNode = this.manager.players.filter((p) => p.node.options.isBackup);
393
+ const playersOnBackupNode = this.manager.players.filter((player) => player.node.options.isBackup);
388
394
  if (playersOnBackupNode.size) {
389
395
  Promise.all(Array.from(playersOnBackupNode.values(), (player) => player.moveNode(this.options.identifier)));
390
396
  }
@@ -410,8 +416,12 @@ class Node {
410
416
  };
411
417
  this.manager.emit(Enums_1.ManagerEventTypes.NodeDisconnect, this, { code, reason });
412
418
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
419
+ // Null the session ID immediately so REST calls during the reconnect delay
420
+ // window don't fire with a stale session. connect() reloads it from storage
421
+ // into pendingResumeSessionId for the WS Session-Id header.
422
+ this.sessionId = null;
413
423
  if (this.manager.useableNode) {
414
- const players = this.manager.players.filter((p) => p.node.options.identifier == this.options.identifier);
424
+ const players = this.manager.players.filter((player) => player.node.options.identifier == this.options.identifier);
415
425
  if (players.size) {
416
426
  await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
417
427
  }
@@ -453,12 +463,12 @@ class Node {
453
463
  * @emits {nodeRaw} - Emits a nodeRaw event with the raw message received from the WebSocket connection.
454
464
  * @private
455
465
  */
456
- async message(d) {
457
- if (Array.isArray(d))
458
- d = Buffer.concat(d);
459
- else if (d instanceof ArrayBuffer)
460
- d = Buffer.from(d);
461
- const payload = JSON.parse(d.toString());
466
+ async message(messagePayload) {
467
+ if (Array.isArray(messagePayload))
468
+ messagePayload = Buffer.concat(messagePayload);
469
+ else if (messagePayload instanceof ArrayBuffer)
470
+ messagePayload = Buffer.from(messagePayload);
471
+ const payload = JSON.parse(messagePayload.toString());
462
472
  if (!payload.op)
463
473
  return;
464
474
  this.manager.emit(Enums_1.ManagerEventTypes.NodeRaw, payload);
@@ -487,7 +497,11 @@ class Node {
487
497
  case "ready":
488
498
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
489
499
  this.rest.setSessionId(payload.sessionId);
490
- const hadPreviousSession = this.sessionId && this.sessionId !== payload.sessionId;
500
+ // pendingResumeSessionId holds what we sent in Session-Id header (if anything).
501
+ // Use it — not this.sessionId which was nulled in connect() — to detect whether
502
+ // we attempted resumption with a different session than what Lavalink gave back.
503
+ const hadPreviousSession = this.pendingResumeSessionId && this.pendingResumeSessionId !== payload.sessionId;
504
+ this.pendingResumeSessionId = null;
491
505
  this.sessionId = payload.sessionId;
492
506
  await this.updateSessionId();
493
507
  this.info = await this.fetchInfo();
@@ -520,6 +534,11 @@ class Node {
520
534
  return;
521
535
  const track = await player.queue.getCurrent();
522
536
  const type = payload.type;
537
+ const TRACK_EVENTS = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
538
+ if (!track && TRACK_EVENTS.includes(type)) {
539
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[Node] Received ${type} for guild ${payload.guildId} but queue has no current track — ignoring.`);
540
+ return;
541
+ }
523
542
  let error;
524
543
  switch (type) {
525
544
  case "TrackStartEvent":
@@ -1117,15 +1136,15 @@ class Node {
1117
1136
  context: { identifier: this.options.identifier, guildId: player.guildId },
1118
1137
  });
1119
1138
  }
1120
- if (segments.some((v) => !validSponsorBlocks.includes(v.toLowerCase()))) {
1139
+ if (segments.some((segment) => !validSponsorBlocks.includes(segment.toLowerCase()))) {
1121
1140
  throw new MagmastreamError_1.MagmaStreamError({
1122
1141
  code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1123
- message: `Invalid SponsorBlock segments provided. Valid ones are: ${validSponsorBlocks.map((v) => `'${v}'`).join(", ")}`,
1142
+ message: `Invalid SponsorBlock segments provided. Valid ones are: ${validSponsorBlocks.map((segment) => `'${segment}'`).join(", ")}`,
1124
1143
  context: { identifier: this.options.identifier, guildId: player.guildId, invalidSegments: segments },
1125
1144
  });
1126
1145
  }
1127
1146
  try {
1128
- await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((v) => v.toLowerCase()));
1147
+ await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((segment) => segment.toLowerCase()));
1129
1148
  }
1130
1149
  catch (err) {
1131
1150
  throw err instanceof MagmastreamError_1.MagmaStreamError
@@ -943,7 +943,12 @@ class Player {
943
943
  context: { guildId: this.guildId },
944
944
  });
945
945
  }
946
- await this.node.rest.destroyPlayer(this.guildId).catch(() => { });
946
+ // Only destroy the player on the old node if it is still reachable.
947
+ // If the server is down the REST call would fail anyway; skipping it
948
+ // also prevents a spurious error when switching nodes during an outage.
949
+ if (this.node.connected) {
950
+ await this.node.rest.destroyPlayer(this.guildId).catch(() => { });
951
+ }
947
952
  this.manager.players.delete(this.guildId);
948
953
  this.node = node;
949
954
  this.manager.players.set(this.guildId, this);
@@ -1038,7 +1043,7 @@ class Player {
1038
1043
  this.voiceReceiverWsClient = new ws_1.WebSocket(`${useSSL ? "wss" : "ws"}://${host}:${port}/connection/data`, { headers });
1039
1044
  this.voiceReceiverWsClient.on("open", () => this.openVoiceReceiver());
1040
1045
  this.voiceReceiverWsClient.on("error", (err) => this.onVoiceReceiverError(err));
1041
- this.voiceReceiverWsClient.on("message", (data) => this.onVoiceReceiverMessage(data.toString()));
1046
+ this.voiceReceiverWsClient.on("message", (rawMessage) => this.onVoiceReceiverMessage(rawMessage.toString()));
1042
1047
  this.voiceReceiverWsClient.on("close", (code, reason) => this.closeVoiceReceiver(code, reason.toString()));
1043
1048
  }
1044
1049
  /**
@@ -1161,18 +1166,18 @@ class Player {
1161
1166
  * @returns {Promise<void>} - A promise that resolves when the voice state is updated.
1162
1167
  */
1163
1168
  async updateVoice() {
1164
- const vs = this.voiceState;
1165
- const ev = vs?.event;
1166
- 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)
1167
1172
  return;
1168
1173
  await this.node.rest.updatePlayer({
1169
1174
  guildId: this.options.guildId,
1170
1175
  data: {
1171
1176
  voice: {
1172
- token: ev.token,
1173
- endpoint: ev.endpoint,
1174
- sessionId: vs.sessionId,
1175
- channelId: vs.channelId,
1177
+ token: voiceEvent.token,
1178
+ endpoint: voiceEvent.endpoint,
1179
+ sessionId: voiceState.sessionId,
1180
+ channelId: voiceState.channelId,
1176
1181
  },
1177
1182
  },
1178
1183
  });
@@ -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 {
@@ -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.2-dev.4",
3
+ "version": "2.10.3-alpha.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",