magmastream 2.10.2-dev.3 → 2.10.3-alpha.0

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.
@@ -169,7 +169,6 @@ export declare class Manager extends EventEmitter {
169
169
  * @param string - The string to escape.
170
170
  * @returns The escaped string.
171
171
  */
172
- private escapeRegExp;
173
172
  /**
174
173
  * Checks if the given data is a voice update.
175
174
  * @param data The data to check.
@@ -16,6 +16,11 @@ const Enums_1 = require("./Enums");
16
16
  const package_json_1 = require("../../package.json");
17
17
  const MagmastreamError_1 = require("./MagmastreamError");
18
18
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
19
+ function escapeRegExp(value) {
20
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21
+ }
22
+ const YOUTUBE_URL_PATTERN = /(youtube\.com|youtu\.be)/;
23
+ const BLOCKED_WORDS_PATTERN = new RegExp(`\\b(${blockedWords_1.blockedWords.map(escapeRegExp).join("|")})\\b`, "gi");
19
24
  /**
20
25
  * The main hub for interacting with Lavalink and using Magmastream.
21
26
  */
@@ -258,7 +263,7 @@ class Manager extends events_1.EventEmitter {
258
263
  }
259
264
  if (this.options.normalizeYouTubeTitles && "tracks" in result) {
260
265
  const processTrack = (track) => {
261
- if (!/(youtube\.com|youtu\.be)/.test(track.uri))
266
+ if (!YOUTUBE_URL_PATTERN.test(track.uri))
262
267
  return track;
263
268
  const { cleanTitle, cleanAuthor } = this.parseYouTubeTitle(track.title, track.author);
264
269
  track.title = cleanTitle;
@@ -267,7 +272,7 @@ class Manager extends events_1.EventEmitter {
267
272
  };
268
273
  result.tracks = result.tracks.map(processTrack);
269
274
  if ("playlist" in result && result.playlist) {
270
- result.playlist.tracks = result.playlist.tracks.map(processTrack);
275
+ result.playlist.tracks = result.tracks;
271
276
  }
272
277
  }
273
278
  const summary = "tracks" in result ? result.tracks.map((t) => Object.fromEntries(Object.entries(t).filter(([key]) => key !== "requester"))) : [];
@@ -780,9 +785,7 @@ class Manager extends events_1.EventEmitter {
780
785
  const cleanAuthor = originalAuthor.replace("- Topic", "").trim();
781
786
  title = title.replace("Topic -", "").trim();
782
787
  // Remove blocked words and phrases
783
- const escapedBlockedWords = blockedWords_1.blockedWords.map((word) => this.escapeRegExp(word));
784
- const blockedWordsPattern = new RegExp(`\\b(${escapedBlockedWords.join("|")})\\b`, "gi");
785
- title = title.replace(blockedWordsPattern, "").trim();
788
+ title = title.replace(BLOCKED_WORDS_PATTERN, "").trim();
786
789
  // Remove empty brackets and balance remaining brackets
787
790
  title = title
788
791
  .replace(/[([{]\s*[)\]}]/g, "") // Empty brackets
@@ -847,10 +850,6 @@ class Manager extends events_1.EventEmitter {
847
850
  * @param string - The string to escape.
848
851
  * @returns The escaped string.
849
852
  */
850
- escapeRegExp(string) {
851
- // Replace special regex characters with their escaped counterparts
852
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
853
- }
854
853
  /**
855
854
  * Checks if the given data is a voice update.
856
855
  * @param data The data to check.
@@ -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.
@@ -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,8 +297,6 @@ 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,
@@ -310,10 +310,16 @@ class Node {
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
  }
@@ -487,7 +493,11 @@ class Node {
487
493
  case "ready":
488
494
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
489
495
  this.rest.setSessionId(payload.sessionId);
490
- const hadPreviousSession = this.sessionId && this.sessionId !== payload.sessionId;
496
+ // pendingResumeSessionId holds what we sent in Session-Id header (if anything).
497
+ // Use it — not this.sessionId which was nulled in connect() — to detect whether
498
+ // we attempted resumption with a different session than what Lavalink gave back.
499
+ const hadPreviousSession = this.pendingResumeSessionId && this.pendingResumeSessionId !== payload.sessionId;
500
+ this.pendingResumeSessionId = null;
491
501
  this.sessionId = payload.sessionId;
492
502
  await this.updateSessionId();
493
503
  this.info = await this.fetchInfo();
@@ -520,6 +530,11 @@ class Node {
520
530
  return;
521
531
  const track = await player.queue.getCurrent();
522
532
  const type = payload.type;
533
+ const TRACK_EVENTS = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
534
+ if (!track && TRACK_EVENTS.includes(type)) {
535
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[Node] Received ${type} for guild ${payload.guildId} but queue has no current track — ignoring.`);
536
+ return;
537
+ }
523
538
  let error;
524
539
  switch (type) {
525
540
  case "TrackStartEvent":
@@ -1125,7 +1140,7 @@ class Node {
1125
1140
  });
1126
1141
  }
1127
1142
  try {
1128
- await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, Utils_1.JSONUtils.safe(segments.map((v) => v.toLowerCase()), 2));
1143
+ await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((v) => v.toLowerCase()));
1129
1144
  }
1130
1145
  catch (err) {
1131
1146
  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);
@@ -104,8 +104,6 @@ class Rest {
104
104
  */
105
105
  async request(method, endpoint, body) {
106
106
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[REST] ${method} request to ${endpoint} with body: ${Utils_1.JSONUtils.safe(body, 2)}`);
107
- const controller = new AbortController();
108
- const timeoutHandle = setTimeout(() => controller.abort(), this.node.options.apiRequestTimeoutMs);
109
107
  let response = null;
110
108
  try {
111
109
  response = await (0, undici_1.fetch)(this.url + endpoint, {
@@ -115,11 +113,11 @@ class Rest {
115
113
  Authorization: this.password,
116
114
  },
117
115
  body: body !== undefined ? JSON.stringify(body) : undefined,
118
- signal: controller.signal,
116
+ signal: AbortSignal.timeout(this.node.options.apiRequestTimeoutMs),
119
117
  });
120
118
  }
121
119
  catch (e) {
122
- const isTimeout = e instanceof Error && e.name === "AbortError";
120
+ const isTimeout = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
123
121
  const message = e instanceof Error ? e.message : "Unknown error";
124
122
  const error = new MagmastreamError_1.MagmaStreamError({
125
123
  code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
@@ -128,9 +126,6 @@ class Rest {
128
126
  this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this.node, error);
129
127
  return null;
130
128
  }
131
- finally {
132
- clearTimeout(timeoutHandle);
133
- }
134
129
  const { status } = response;
135
130
  const data = status !== 204 ? await response.json() : null;
136
131
  if (status >= 200 && status < 300) {
@@ -15,7 +15,7 @@ class DiscordJSManager extends Manager_1.Manager {
15
15
  constructor(client, options) {
16
16
  super(options, true);
17
17
  this.client = client;
18
- const { version: djsVersion } = moduleRequire("discord.js/package.json");
18
+ const { version: djsVersion } = moduleRequire("discord.js");
19
19
  const [major, minor] = djsVersion.split(".").map(Number);
20
20
  if (!this.client.options.intents.has(GUILD_VOICE_STATES_INTENT)) {
21
21
  throw new MagmastreamError_1.MagmaStreamError({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.10.2-dev.3",
3
+ "version": "2.10.3-alpha.0",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,8 +16,10 @@
16
16
  "lint": "eslint \"src/**/*.{ts,js}\"",
17
17
  "lint:fix": "eslint --fix \"src/**/*.{ts,js}\"",
18
18
  "ci": "run-s format:check lint build types",
19
- "release:alpha": "npm run format && npm run lint:fix && npm run ci && npm version prerelease --preid=alpha && npm publish --tag alpha && git push && git push --follow-tags",
20
- "release:dev": "npm run format && npm run lint:fix && npm run ci && npm version prerelease --preid=dev && npm publish --tag dev && git push && git push --follow-tags"
19
+ "version:alpha": "node scripts/prerelease-version.cjs alpha",
20
+ "version:dev": "node scripts/prerelease-version.cjs dev",
21
+ "release:alpha": "npm run format && npm run lint:fix && npm run ci && npm run version:alpha && npm publish --tag alpha && git push && git push --follow-tags",
22
+ "release:dev": "npm run format && npm run lint:fix && npm run ci && npm run version:dev && npm publish --tag dev && git push && git push --follow-tags"
21
23
  },
22
24
  "devDependencies": {
23
25
  "@favware/rollup-type-bundler": "^4.0.0",