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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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
|
-
|
|
747
|
+
return;
|
|
758
748
|
const state = JSON.parse(data);
|
|
759
749
|
if (!state || typeof state !== "object")
|
|
760
|
-
|
|
750
|
+
return;
|
|
761
751
|
const guildId = key.split(":").pop();
|
|
762
752
|
if (!guildId)
|
|
763
|
-
|
|
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.
|
package/dist/structures/Node.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
241
|
+
action,
|
|
246
242
|
previousConnection: oldPlayer.state === Enums_1.StateTypes.Connected,
|
|
247
|
-
currentConnection:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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}\"",
|