magmastream 2.10.5 → 2.10.6-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.
@@ -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,19 +529,10 @@ 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;
543
- const hasGuild = this.resolveGuild(state.guildId);
544
- if (!hasGuild)
535
+ if (state.guildId !== guildId)
545
536
  return;
546
537
  const lavaPlayer = (await node.rest.get(`/v4/sessions/${state.node.sessionId}/players/${state.guildId}`));
547
538
  if (!lavaPlayer)
@@ -584,7 +575,6 @@ class Manager extends events_1.EventEmitter {
584
575
  await player.pause(true);
585
576
  await cleanup();
586
577
  this.emit(Enums_1.ManagerEventTypes.PlayerRestored, player, node);
587
- await this.sleep(1000);
588
578
  }
589
579
  async restoreQueue(node, player, state, lavaPlayer) {
590
580
  const currentTrack = state.queue.current;
@@ -724,12 +714,14 @@ class Manager extends events_1.EventEmitter {
724
714
  await promises_1.default.mkdir(playersBaseDir, { recursive: true });
725
715
  });
726
716
  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;
717
+ await Promise.all(guildDirs
718
+ .filter((file) => file.isDirectory())
719
+ .map(async ({ name: guildId }) => {
731
720
  const stateFilePath = Utils_1.PlayerUtils.getPlayerStatePath(guildId);
732
721
  try {
722
+ // Guilds outside this client's cache belong to another cluster; skip them before reading any file.
723
+ if (!this.resolveGuild(guildId))
724
+ return;
733
725
  await promises_1.default.access(stateFilePath);
734
726
  const state = JSON.parse(await promises_1.default.readFile(stateFilePath, "utf-8"));
735
727
  await this.restorePlayerFromState(node, nodeId, guildId, state, async () => {
@@ -740,7 +732,7 @@ class Manager extends events_1.EventEmitter {
740
732
  catch (error) {
741
733
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing guild ${guildId}: ${error}`);
742
734
  }
743
- }
735
+ }));
744
736
  }
745
737
  catch (error) {
746
738
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error loading player states: ${error}`);
@@ -750,17 +742,17 @@ class Manager extends events_1.EventEmitter {
750
742
  case Enums_1.StateStorageType.Redis: {
751
743
  try {
752
744
  const keys = await this.redis.keys(`${Utils_1.PlayerUtils.getRedisKey()}playerstore:*`);
753
- for (const key of keys) {
745
+ await Promise.all(keys.map(async (key) => {
754
746
  try {
747
+ const guildId = key.split(":").pop();
748
+ if (!guildId || !this.resolveGuild(guildId))
749
+ return;
755
750
  const data = await this.redis.get(key);
756
751
  if (!data)
757
- continue;
752
+ return;
758
753
  const state = JSON.parse(data);
759
754
  if (!state || typeof state !== "object")
760
- continue;
761
- const guildId = key.split(":").pop();
762
- if (!guildId)
763
- continue;
755
+ return;
764
756
  await this.restorePlayerFromState(node, nodeId, guildId, state, async () => {
765
757
  await this.redis.del(key);
766
758
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted Redis state: ${key}`);
@@ -769,7 +761,7 @@ class Manager extends events_1.EventEmitter {
769
761
  catch (error) {
770
762
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing Redis key ${key}: ${error}`);
771
763
  }
772
- }
764
+ }));
773
765
  }
774
766
  catch (error) {
775
767
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error loading player states from Redis: ${error}`);
@@ -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;
@@ -484,7 +485,9 @@ class Node {
484
485
  this.handlePlayerUpdate(payload);
485
486
  break;
486
487
  case "event":
487
- 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
+ }
488
491
  await this.handleEvent(payload);
489
492
  break;
490
493
  case "ready":
@@ -531,6 +534,7 @@ class Node {
531
534
  if (!player || player.node.options.identifier !== this.options.identifier)
532
535
  return;
533
536
  player.position = payload.state.position ?? 0;
537
+ player.ping = payload.state.ping ?? -1;
534
538
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerUpdate, player, payload);
535
539
  }
536
540
  /**
@@ -567,8 +571,7 @@ class Node {
567
571
  return;
568
572
  const track = await this.resolveEventTrack(player, payload);
569
573
  const type = payload.type;
570
- const trackRequiredEvents = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
571
- if (!track && trackRequiredEvents.includes(type)) {
574
+ if (!track && TRACK_REQUIRED_EVENTS.has(type)) {
572
575
  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.`);
573
576
  return;
574
577
  }
@@ -1227,7 +1230,9 @@ class Node {
1227
1230
  socketClosed(player, payload) {
1228
1231
  // A move can close the old socket after the new connection is ready.
1229
1232
  this.manager.emit(Enums_1.ManagerEventTypes.SocketClosed, player, payload);
1230
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1233
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
1234
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1235
+ }
1231
1236
  }
1232
1237
  /**
1233
1238
  * Emitted when the segments for a track are loaded.
@@ -18,6 +18,8 @@ export declare class Player {
18
18
  dynamicRepeat: boolean;
19
19
  /** The time the player is in the track. */
20
20
  position: number;
21
+ /** Last node-reported Discord voice ping in milliseconds; -1 when unavailable. */
22
+ ping: number;
21
23
  /** Whether the player is playing. */
22
24
  playing: boolean;
23
25
  /** Whether the player is paused. */
@@ -84,6 +86,8 @@ export declare class Player {
84
86
  * @returns {T} - The data associated with the key, cast to the specified type.
85
87
  */
86
88
  get<T>(key: string): T;
89
+ /** Returns the last node-reported Discord voice ping in milliseconds, or -1 when unavailable. */
90
+ getPing(): number;
87
91
  /**
88
92
  * Same as Manager#search() but a shortcut on the player itself.
89
93
  * @param query
@@ -101,6 +105,7 @@ export declare class Player {
101
105
  * @returns {this} The player instance.
102
106
  */
103
107
  disconnect(): Promise<this>;
108
+ private emitConnectionChange;
104
109
  /**
105
110
  * Destroys the player and clears the queue.
106
111
  * @param {boolean} disconnect - Whether to disconnect the player from the voice channel.
@@ -26,6 +26,8 @@ class Player {
26
26
  dynamicRepeat = false;
27
27
  /** The time the player is in the track. */
28
28
  position = 0;
29
+ /** Last node-reported Discord voice ping in milliseconds; -1 when unavailable. */
30
+ ping = -1;
29
31
  /** Whether the player is playing. */
30
32
  playing = false;
31
33
  /** Whether the player is paused. */
@@ -162,6 +164,10 @@ class Player {
162
164
  // Access the data object using the key and cast it to the specified type T.
163
165
  return this.data[key];
164
166
  }
167
+ /** Returns the last node-reported Discord voice ping in milliseconds, or -1 when unavailable. */
168
+ getPing() {
169
+ return this.ping;
170
+ }
165
171
  /**
166
172
  * Same as Manager#search() but a shortcut on the player itself.
167
173
  * @param query
@@ -187,7 +193,7 @@ class Player {
187
193
  // Set the player state to connecting.
188
194
  this.state = Enums_1.StateTypes.Connecting;
189
195
  // Clone the current player state for comparison.
190
- const oldPlayer = this ? { ...this } : null;
196
+ const oldPlayer = { ...this };
191
197
  this.manager.sendPacket({
192
198
  op: 4,
193
199
  d: {
@@ -200,15 +206,7 @@ class Player {
200
206
  // Set the player state to connected.
201
207
  this.state = Enums_1.StateTypes.Connected;
202
208
  // 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
- });
209
+ this.emitConnectionChange(oldPlayer, "connect");
212
210
  }
213
211
  /**
214
212
  * Disconnects the player from the voice channel.
@@ -218,7 +216,7 @@ class Player {
218
216
  // Set the player state to disconnecting.
219
217
  this.state = Enums_1.StateTypes.Disconnecting;
220
218
  // Clone the current player state for comparison.
221
- const oldPlayer = this ? { ...this } : null;
219
+ const oldPlayer = { ...this };
222
220
  // Pause the player.
223
221
  if (this.options.pauseOnDisconnect) {
224
222
  await this.pause(true);
@@ -238,16 +236,19 @@ class Player {
238
236
  // Set the player state to disconnected.
239
237
  this.state = Enums_1.StateTypes.Disconnected;
240
238
  // Emit the player state update event.
239
+ this.emitConnectionChange(oldPlayer, "disconnect");
240
+ return this;
241
+ }
242
+ emitConnectionChange(oldPlayer, action) {
241
243
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this, {
242
244
  changeType: Enums_1.PlayerStateEventTypes.ConnectionChange,
243
245
  details: {
244
246
  type: "connection",
245
- action: "disconnect",
247
+ action,
246
248
  previousConnection: oldPlayer.state === Enums_1.StateTypes.Connected,
247
- currentConnection: false,
249
+ currentConnection: action === "connect",
248
250
  },
249
251
  });
250
- return this;
251
252
  }
252
253
  /**
253
254
  * Destroys the player and clears the queue.
@@ -1340,7 +1341,9 @@ class Player {
1340
1341
  }
1341
1342
  if (!packet?.op)
1342
1343
  return;
1343
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1344
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
1345
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1346
+ }
1344
1347
  switch (packet.type) {
1345
1348
  case "startSpeakingEvent": {
1346
1349
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, packet.data);
@@ -1355,7 +1358,9 @@ class Player {
1355
1358
  break;
1356
1359
  }
1357
1360
  default: {
1358
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1361
+ if (this.manager.listenerCount(Enums_1.ManagerEventTypes.Debug)) {
1362
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1363
+ }
1359
1364
  break;
1360
1365
  }
1361
1366
  }
@@ -1408,9 +1413,12 @@ class Player {
1408
1413
  return;
1409
1414
  }
1410
1415
  if (frame.op === 3) {
1411
- const stream = this.voiceReceiverStreams.get(key) ?? { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] };
1416
+ let stream = this.voiceReceiverStreams.get(key);
1417
+ if (!stream) {
1418
+ stream = { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] };
1419
+ this.voiceReceiverStreams.set(key, stream);
1420
+ }
1412
1421
  stream.chunks.push(frame.data);
1413
- this.voiceReceiverStreams.set(key, stream);
1414
1422
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverData, this, {
1415
1423
  userId: frame.userId,
1416
1424
  guildId: frame.guildId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.10.5",
3
+ "version": "2.10.6-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",
@@ -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}\"",