magmastream 2.10.5-alpha.1 → 2.10.6-alpha.1

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.
@@ -117,12 +117,6 @@ export declare class Manager extends EventEmitter {
117
117
  * @param {string} guildId - The guild ID of the player to save
118
118
  */
119
119
  savePlayerState(guildId: string): Promise<void>;
120
- /**
121
- * Sleeps for a specified amount of time.
122
- * @param ms The amount of time to sleep in milliseconds.
123
- * @returns A promise that resolves after the specified amount of time.
124
- */
125
- private sleep;
126
120
  private restorePlayerFromState;
127
121
  private restoreQueue;
128
122
  private restorePreviousQueue;
@@ -529,14 +529,6 @@ class Manager extends events_1.EventEmitter {
529
529
  await player.queue.clearPrevious();
530
530
  await player.queue.setCurrent(null);
531
531
  }
532
- /**
533
- * Sleeps for a specified amount of time.
534
- * @param ms The amount of time to sleep in milliseconds.
535
- * @returns A promise that resolves after the specified amount of time.
536
- */
537
- async sleep(ms) {
538
- return new Promise((resolve) => setTimeout(resolve, ms));
539
- }
540
532
  async restorePlayerFromState(node, nodeId, guildId, state, cleanup) {
541
533
  if (!state.guildId || state.node?.options?.identifier !== nodeId)
542
534
  return;
@@ -584,7 +576,6 @@ class Manager extends events_1.EventEmitter {
584
576
  await player.pause(true);
585
577
  await cleanup();
586
578
  this.emit(Enums_1.ManagerEventTypes.PlayerRestored, player, node);
587
- await this.sleep(1000);
588
579
  }
589
580
  async restoreQueue(node, player, state, lavaPlayer) {
590
581
  const currentTrack = state.queue.current;
@@ -724,10 +715,9 @@ class Manager extends events_1.EventEmitter {
724
715
  await promises_1.default.mkdir(playersBaseDir, { recursive: true });
725
716
  });
726
717
  const guildDirs = await promises_1.default.readdir(playersBaseDir, { withFileTypes: true });
727
- for (const file of guildDirs) {
728
- if (!file.isDirectory())
729
- continue;
730
- const guildId = file.name;
718
+ await Promise.all(guildDirs
719
+ .filter((file) => file.isDirectory())
720
+ .map(async ({ name: guildId }) => {
731
721
  const stateFilePath = Utils_1.PlayerUtils.getPlayerStatePath(guildId);
732
722
  try {
733
723
  await promises_1.default.access(stateFilePath);
@@ -740,7 +730,7 @@ class Manager extends events_1.EventEmitter {
740
730
  catch (error) {
741
731
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing guild ${guildId}: ${error}`);
742
732
  }
743
- }
733
+ }));
744
734
  }
745
735
  catch (error) {
746
736
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error loading player states: ${error}`);
@@ -750,17 +740,17 @@ class Manager extends events_1.EventEmitter {
750
740
  case Enums_1.StateStorageType.Redis: {
751
741
  try {
752
742
  const keys = await this.redis.keys(`${Utils_1.PlayerUtils.getRedisKey()}playerstore:*`);
753
- for (const key of keys) {
743
+ await Promise.all(keys.map(async (key) => {
754
744
  try {
755
745
  const data = await this.redis.get(key);
756
746
  if (!data)
757
- continue;
747
+ return;
758
748
  const state = JSON.parse(data);
759
749
  if (!state || typeof state !== "object")
760
- continue;
750
+ return;
761
751
  const guildId = key.split(":").pop();
762
752
  if (!guildId)
763
- continue;
753
+ return;
764
754
  await this.restorePlayerFromState(node, nodeId, guildId, state, async () => {
765
755
  await this.redis.del(key);
766
756
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted Redis state: ${key}`);
@@ -769,7 +759,7 @@ class Manager extends events_1.EventEmitter {
769
759
  catch (error) {
770
760
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing Redis key ${key}: ${error}`);
771
761
  }
772
- }
762
+ }));
773
763
  }
774
764
  catch (error) {
775
765
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error loading player states from Redis: ${error}`);
@@ -23,7 +23,6 @@ export declare class Node {
23
23
  private reconnectTimeout?;
24
24
  private reconnectAttempts;
25
25
  private redisPrefix?;
26
- /** Session ID sent in the reconnect header for resumption — cleared once the ready op is received. */
27
26
  private pendingResumeSessionId;
28
27
  /**
29
28
  * Creates an instance of Node.
@@ -11,6 +11,7 @@ const path_1 = tslib_1.__importDefault(require("path"));
11
11
  const Enums_1 = require("./Enums");
12
12
  const MagmastreamError_1 = require("./MagmastreamError");
13
13
  const validSponsorBlocks = Object.values(Enums_1.SponsorBlockSegment).map((segment) => segment.toLowerCase());
14
+ const TRACK_REQUIRED_EVENTS = new Set(["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"]);
14
15
  class Node {
15
16
  manager;
16
17
  options;
@@ -31,7 +32,6 @@ class Node {
31
32
  reconnectTimeout;
32
33
  reconnectAttempts = 0;
33
34
  redisPrefix;
34
- /** Session ID sent in the reconnect header for resumption — cleared once the ready op is received. */
35
35
  pendingResumeSessionId = null;
36
36
  /**
37
37
  * Creates an instance of Node.
@@ -256,12 +256,7 @@ class Node {
256
256
  "User-Id": this.manager.options.clientId,
257
257
  "Client-Name": this.manager.options.clientName,
258
258
  };
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'.
259
+ // Keep the resume header while blocking REST calls until the next ready event.
265
260
  this.pendingResumeSessionId = this.sessionId;
266
261
  this.sessionId = null;
267
262
  if (typeof this.pendingResumeSessionId === "string" && this.pendingResumeSessionId.length > 0) {
@@ -310,11 +305,9 @@ class Node {
310
305
  if (players.size) {
311
306
  await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
312
307
  }
313
- // Always clear reconnect state regardless of connection status
314
308
  clearTimeout(this.reconnectTimeout);
315
309
  this.reconnectTimeout = undefined;
316
310
  this.reconnectAttempts = 0;
317
- // Only close the socket if it is actually open
318
311
  if (this.connected) {
319
312
  this.socket.close(1000, "destroy");
320
313
  this.socket.removeAllListeners();
@@ -419,9 +412,7 @@ class Node {
419
412
  };
420
413
  this.manager.emit(Enums_1.ManagerEventTypes.NodeDisconnect, this, { code, reason });
421
414
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${Utils_1.JSONUtils.safe(debugInfo, 2)}`);
422
- // Null the session ID immediately so REST calls during the reconnect delay
423
- // window don't fire with a stale session. connect() reloads it from storage
424
- // into pendingResumeSessionId for the WS Session-Id header.
415
+ // Prevent REST calls from using a stale session during reconnect.
425
416
  this.sessionId = null;
426
417
  if (this.manager.useableNode) {
427
418
  const players = this.manager.players.filter((player) => player.node.options.identifier == this.options.identifier);
@@ -494,15 +485,14 @@ class Node {
494
485
  this.handlePlayerUpdate(payload);
495
486
  break;
496
487
  case "event":
497
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
488
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
489
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
490
+ }
498
491
  await this.handleEvent(payload);
499
492
  break;
500
493
  case "ready":
501
494
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
502
495
  this.rest.setSessionId(payload.sessionId);
503
- // pendingResumeSessionId holds what we sent in Session-Id header (if anything).
504
- // Use it — not this.sessionId which was nulled in connect() — to detect whether
505
- // we attempted resumption with a different session than what Lavalink gave back.
506
496
  const hadPreviousSession = this.pendingResumeSessionId && this.pendingResumeSessionId !== payload.sessionId;
507
497
  this.pendingResumeSessionId = null;
508
498
  this.sessionId = payload.sessionId;
@@ -544,7 +534,6 @@ class Node {
544
534
  if (!player || player.node.options.identifier !== this.options.identifier)
545
535
  return;
546
536
  player.position = payload.state.position ?? 0;
547
- // Transport status can lag lifecycle events; expose it without overwriting player.state.
548
537
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerUpdate, player, payload);
549
538
  }
550
539
  /**
@@ -581,8 +570,7 @@ class Node {
581
570
  return;
582
571
  const track = await this.resolveEventTrack(player, payload);
583
572
  const type = payload.type;
584
- const trackRequiredEvents = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
585
- if (!track && trackRequiredEvents.includes(type)) {
573
+ if (!track && TRACK_REQUIRED_EVENTS.has(type)) {
586
574
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Received ${type} for guild ${payload.guildId}, but neither the payload nor local queue contains a usable track.`);
587
575
  return;
588
576
  }
@@ -1239,9 +1227,11 @@ class Node {
1239
1227
  * @param {WebSocketClosedEvent} payload - The event payload containing additional data about the WebSocket close event.
1240
1228
  */
1241
1229
  socketClosed(player, payload) {
1242
- // This may describe the old voice socket after a channel move, not a lifecycle disconnect.
1230
+ // A move can close the old socket after the new connection is ready.
1243
1231
  this.manager.emit(Enums_1.ManagerEventTypes.SocketClosed, player, payload);
1244
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1232
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
1233
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1234
+ }
1245
1235
  }
1246
1236
  /**
1247
1237
  * Emitted when the segments for a track are loaded.
@@ -101,6 +101,7 @@ export declare class Player {
101
101
  * @returns {this} The player instance.
102
102
  */
103
103
  disconnect(): Promise<this>;
104
+ private emitConnectionChange;
104
105
  /**
105
106
  * Destroys the player and clears the queue.
106
107
  * @param {boolean} disconnect - Whether to disconnect the player from the voice channel.
@@ -187,7 +187,7 @@ class Player {
187
187
  // Set the player state to connecting.
188
188
  this.state = Enums_1.StateTypes.Connecting;
189
189
  // Clone the current player state for comparison.
190
- const oldPlayer = this ? { ...this } : null;
190
+ const oldPlayer = { ...this };
191
191
  this.manager.sendPacket({
192
192
  op: 4,
193
193
  d: {
@@ -200,15 +200,7 @@ class Player {
200
200
  // Set the player state to connected.
201
201
  this.state = Enums_1.StateTypes.Connected;
202
202
  // Emit the player state update event.
203
- this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this, {
204
- changeType: Enums_1.PlayerStateEventTypes.ConnectionChange,
205
- details: {
206
- type: "connection",
207
- action: "connect",
208
- previousConnection: oldPlayer?.state === Enums_1.StateTypes.Connected,
209
- currentConnection: true,
210
- },
211
- });
203
+ this.emitConnectionChange(oldPlayer, "connect");
212
204
  }
213
205
  /**
214
206
  * Disconnects the player from the voice channel.
@@ -218,7 +210,7 @@ class Player {
218
210
  // Set the player state to disconnecting.
219
211
  this.state = Enums_1.StateTypes.Disconnecting;
220
212
  // Clone the current player state for comparison.
221
- const oldPlayer = this ? { ...this } : null;
213
+ const oldPlayer = { ...this };
222
214
  // Pause the player.
223
215
  if (this.options.pauseOnDisconnect) {
224
216
  await this.pause(true);
@@ -238,16 +230,19 @@ class Player {
238
230
  // Set the player state to disconnected.
239
231
  this.state = Enums_1.StateTypes.Disconnected;
240
232
  // Emit the player state update event.
233
+ this.emitConnectionChange(oldPlayer, "disconnect");
234
+ return this;
235
+ }
236
+ emitConnectionChange(oldPlayer, action) {
241
237
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this, {
242
238
  changeType: Enums_1.PlayerStateEventTypes.ConnectionChange,
243
239
  details: {
244
240
  type: "connection",
245
- action: "disconnect",
241
+ action,
246
242
  previousConnection: oldPlayer.state === Enums_1.StateTypes.Connected,
247
- currentConnection: false,
243
+ currentConnection: action === "connect",
248
244
  },
249
245
  });
250
- return this;
251
246
  }
252
247
  /**
253
248
  * Destroys the player and clears the queue.
@@ -1136,9 +1131,6 @@ class Player {
1136
1131
  context: { guildId: this.guildId },
1137
1132
  });
1138
1133
  }
1139
- // Only destroy the player on the old node if it is still reachable.
1140
- // If the server is down the REST call would fail anyway; skipping it
1141
- // also prevents a spurious error when switching nodes during an outage.
1142
1134
  if (this.node.connected) {
1143
1135
  await this.node.rest.destroyPlayer(this.guildId).catch(() => { });
1144
1136
  }
@@ -1343,7 +1335,9 @@ class Player {
1343
1335
  }
1344
1336
  if (!packet?.op)
1345
1337
  return;
1346
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1338
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
1339
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1340
+ }
1347
1341
  switch (packet.type) {
1348
1342
  case "startSpeakingEvent": {
1349
1343
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, packet.data);
@@ -1358,7 +1352,9 @@ class Player {
1358
1352
  break;
1359
1353
  }
1360
1354
  default: {
1361
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1355
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
1356
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1357
+ }
1362
1358
  break;
1363
1359
  }
1364
1360
  }
@@ -1411,9 +1407,12 @@ class Player {
1411
1407
  return;
1412
1408
  }
1413
1409
  if (frame.op === 3) {
1414
- const stream = this.voiceReceiverStreams.get(key) ?? { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] };
1410
+ let stream = this.voiceReceiverStreams.get(key);
1411
+ if (!stream) {
1412
+ stream = { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] };
1413
+ this.voiceReceiverStreams.set(key, stream);
1414
+ }
1415
1415
  stream.chunks.push(frame.data);
1416
- this.voiceReceiverStreams.set(key, stream);
1417
1416
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverData, this, {
1418
1417
  userId: frame.userId,
1419
1418
  guildId: frame.guildId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.10.5-alpha.1",
3
+ "version": "2.10.6-alpha.1",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "prepare": "npm run build",
12
12
  "build": "tsc",
13
13
  "types": "rtb --dist dist",
14
- "test": "npm run build && node --test tests/player-state.test.cjs",
14
+ "test": "npm run build && node --test tests/player-state.test.cjs tests/hot-paths.test.cjs tests/restore-performance.test.cjs",
15
15
  "format": "prettier --write .",
16
16
  "format:check": "prettier --check .",
17
17
  "lint": "eslint \"src/**/*.{ts,js}\"",