@stella_project/stellalib 1.0.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.
Files changed (64) hide show
  1. package/LICENSE +213 -0
  2. package/README.md +285 -0
  3. package/THIRD-PARTY-NOTICES.md +84 -0
  4. package/dist/Structures/Filters.d.ts +140 -0
  5. package/dist/Structures/Filters.d.ts.map +1 -0
  6. package/dist/Structures/Filters.js +315 -0
  7. package/dist/Structures/Filters.js.map +1 -0
  8. package/dist/Structures/LRUCache.d.ts +36 -0
  9. package/dist/Structures/LRUCache.d.ts.map +1 -0
  10. package/dist/Structures/LRUCache.js +94 -0
  11. package/dist/Structures/LRUCache.js.map +1 -0
  12. package/dist/Structures/Manager.d.ts +146 -0
  13. package/dist/Structures/Manager.d.ts.map +1 -0
  14. package/dist/Structures/Manager.js +503 -0
  15. package/dist/Structures/Manager.js.map +1 -0
  16. package/dist/Structures/Node.d.ts +118 -0
  17. package/dist/Structures/Node.d.ts.map +1 -0
  18. package/dist/Structures/Node.js +927 -0
  19. package/dist/Structures/Node.js.map +1 -0
  20. package/dist/Structures/Player.d.ts +193 -0
  21. package/dist/Structures/Player.d.ts.map +1 -0
  22. package/dist/Structures/Player.js +598 -0
  23. package/dist/Structures/Player.js.map +1 -0
  24. package/dist/Structures/Queue.d.ts +48 -0
  25. package/dist/Structures/Queue.d.ts.map +1 -0
  26. package/dist/Structures/Queue.js +114 -0
  27. package/dist/Structures/Queue.js.map +1 -0
  28. package/dist/Structures/Rest.d.ts +105 -0
  29. package/dist/Structures/Rest.d.ts.map +1 -0
  30. package/dist/Structures/Rest.js +343 -0
  31. package/dist/Structures/Rest.js.map +1 -0
  32. package/dist/Structures/SessionStore.d.ts +42 -0
  33. package/dist/Structures/SessionStore.d.ts.map +1 -0
  34. package/dist/Structures/SessionStore.js +94 -0
  35. package/dist/Structures/SessionStore.js.map +1 -0
  36. package/dist/Structures/Types.d.ts +450 -0
  37. package/dist/Structures/Types.d.ts.map +1 -0
  38. package/dist/Structures/Types.js +13 -0
  39. package/dist/Structures/Types.js.map +1 -0
  40. package/dist/Structures/Utils.d.ts +61 -0
  41. package/dist/Structures/Utils.d.ts.map +1 -0
  42. package/dist/Structures/Utils.js +204 -0
  43. package/dist/Structures/Utils.js.map +1 -0
  44. package/dist/Utils/FiltersEqualizers.d.ts +20 -0
  45. package/dist/Utils/FiltersEqualizers.d.ts.map +1 -0
  46. package/dist/Utils/FiltersEqualizers.js +96 -0
  47. package/dist/Utils/FiltersEqualizers.js.map +1 -0
  48. package/dist/Utils/ManagerCheck.d.ts +9 -0
  49. package/dist/Utils/ManagerCheck.d.ts.map +1 -0
  50. package/dist/Utils/ManagerCheck.js +45 -0
  51. package/dist/Utils/ManagerCheck.js.map +1 -0
  52. package/dist/Utils/NodeCheck.d.ts +9 -0
  53. package/dist/Utils/NodeCheck.d.ts.map +1 -0
  54. package/dist/Utils/NodeCheck.js +31 -0
  55. package/dist/Utils/NodeCheck.js.map +1 -0
  56. package/dist/Utils/PlayerCheck.d.ts +9 -0
  57. package/dist/Utils/PlayerCheck.d.ts.map +1 -0
  58. package/dist/Utils/PlayerCheck.js +23 -0
  59. package/dist/Utils/PlayerCheck.js.map +1 -0
  60. package/dist/index.d.ts +12 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +28 -0
  63. package/dist/index.js.map +1 -0
  64. package/package.json +64 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @license
3
+ * StellaLib — Copyright (c) 2026 AntonyZ, x2sadddDM, SynX, Astel (OSL-3.0)
4
+ * Derived from LithiumX — Copyright (c) 2025 Anantix Network (MIT)
5
+ * See LICENSE and THIRD-PARTY-NOTICES.md for full license details.
6
+ */
7
+ import type { NodeOptions, PlayerOptions, TrackData, VoicePacket, VoiceServer, VoiceStateUpdate, SearchPlatform, SearchQuery, SearchResult, ManagerOptions, ManagerEvents } from "./Types";
8
+ import type { StellaNode } from "./Node";
9
+ import type { StellaPlayer } from "./Player";
10
+ import { LRUCache } from "./LRUCache";
11
+ import { TypedEmitter } from "tiny-typed-emitter";
12
+ /**
13
+ * The main hub for interacting with Lavalink using StellaLib.
14
+ */
15
+ declare class StellaManager extends TypedEmitter<ManagerEvents> {
16
+ static readonly DEFAULT_SOURCES: Record<SearchPlatform, string>;
17
+ /** The map of players. */
18
+ readonly players: Map<string, StellaPlayer>;
19
+ /** The map of nodes. */
20
+ readonly nodes: Map<string, StellaNode>;
21
+ /** The options that were set. */
22
+ readonly options: ManagerOptions;
23
+ private initiated;
24
+ /** The search result LRU cache (bounded, TTL-based). */
25
+ caches: LRUCache<string, SearchResult>;
26
+ /** Whether the manager is shutting down. */
27
+ private shuttingDown;
28
+ /** Returns the nodes sorted by least CPU load. */
29
+ get leastLoadNode(): Map<string, StellaNode>;
30
+ /** Returns the nodes sorted by least amount of players. */
31
+ private get leastPlayersNode();
32
+ /** Returns the node with the lowest penalty score (best performance). */
33
+ get leastPenaltyNode(): StellaNode | undefined;
34
+ /** Returns a node based on priority. */
35
+ private get priorityNode();
36
+ /** Returns the best node to use based on configuration. */
37
+ get useableNodes(): StellaNode;
38
+ /**
39
+ * Initiates the Manager class.
40
+ * @param options
41
+ */
42
+ constructor(options: ManagerOptions);
43
+ /**
44
+ * Initiates the Manager.
45
+ * @param clientId
46
+ */
47
+ init(clientId?: string): this;
48
+ /**
49
+ * Searches the enabled sources based off the URL or the `source` property.
50
+ * @param query
51
+ * @param requester The user who requested the search.
52
+ */
53
+ search(query: string | SearchQuery, requester?: string): Promise<SearchResult>;
54
+ /**
55
+ * Returns the available source managers and plugins on a connected node.
56
+ * Useful for checking which search platforms the Lavalink server supports.
57
+ */
58
+ getAvailableSources(): Promise<{
59
+ sourceManagers: string[];
60
+ plugins: {
61
+ name: string;
62
+ version: string;
63
+ }[];
64
+ }>;
65
+ /**
66
+ * Decodes the base64 encoded tracks and returns a TrackData array.
67
+ * @param tracks
68
+ */
69
+ decodeTracks(tracks: string[]): Promise<TrackData[]>;
70
+ /**
71
+ * Decodes the base64 encoded track and returns a TrackData.
72
+ * @param track
73
+ */
74
+ decodeTrack(track: string): Promise<TrackData>;
75
+ /**
76
+ * Creates a player or returns one if it already exists.
77
+ * @param options
78
+ */
79
+ create(options: PlayerOptions): StellaPlayer;
80
+ /**
81
+ * Returns a player or undefined if it does not exist.
82
+ * @param guild
83
+ */
84
+ get(guild: string): StellaPlayer | undefined;
85
+ /**
86
+ * Destroys a player if it exists.
87
+ * @param guild
88
+ */
89
+ destroy(guild: string): void;
90
+ /**
91
+ * Creates a node or returns one if it already exists.
92
+ * @param options
93
+ */
94
+ createNode(options: NodeOptions): StellaNode;
95
+ /**
96
+ * Destroys a node if it exists.
97
+ * @param identifier
98
+ */
99
+ destroyNode(identifier: string): void;
100
+ /**
101
+ * Sends voice data to the Lavalink server.
102
+ * Handles both VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE from Discord.
103
+ * Includes channelId in voice state to satisfy Lavalink v4 requirements.
104
+ * @param data
105
+ */
106
+ updateVoiceState(data: VoicePacket | VoiceServer | VoiceStateUpdate): Promise<void>;
107
+ /**
108
+ * Gracefully shuts down the Manager: persists sessions, closes all nodes, and cleans up.
109
+ * Call this before your bot exits to enable seamless session resume on restart.
110
+ *
111
+ * Usage:
112
+ * ```ts
113
+ * process.on("SIGINT", async () => {
114
+ * await manager.shutdown();
115
+ * process.exit(0);
116
+ * });
117
+ * ```
118
+ */
119
+ shutdown(): Promise<void>;
120
+ /**
121
+ * Returns memory and performance statistics for monitoring.
122
+ */
123
+ getStats(): {
124
+ nodes: {
125
+ identifier: string;
126
+ connected: boolean;
127
+ players: number;
128
+ playingPlayers: number;
129
+ penalties: number;
130
+ uptime: number;
131
+ memory: {
132
+ used: number;
133
+ free: number;
134
+ allocated: number;
135
+ };
136
+ restRequests: number;
137
+ restFailed: number;
138
+ }[];
139
+ totalPlayers: number;
140
+ totalPlayingPlayers: number;
141
+ cacheSize: number;
142
+ cacheMemoryEstimate: number;
143
+ };
144
+ }
145
+ export { StellaManager };
146
+ //# sourceMappingURL=Manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Manager.d.ts","sourceRoot":"","sources":["../../src/Structures/Manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,503 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StellaManager = void 0;
7
+ const Utils_1 = require("./Utils");
8
+ const LRUCache_1 = require("./LRUCache");
9
+ const tiny_typed_emitter_1 = require("tiny-typed-emitter");
10
+ const ManagerCheck_1 = __importDefault(require("../Utils/ManagerCheck"));
11
+ /**
12
+ * The main hub for interacting with Lavalink using StellaLib.
13
+ */
14
+ class StellaManager extends tiny_typed_emitter_1.TypedEmitter {
15
+ static DEFAULT_SOURCES = {
16
+ "youtube music": "ytmsearch",
17
+ youtube: "ytsearch",
18
+ spotify: "spsearch",
19
+ jiosaavn: "jssearch",
20
+ soundcloud: "scsearch",
21
+ deezer: "dzsearch",
22
+ tidal: "tdsearch",
23
+ applemusic: "amsearch",
24
+ bandcamp: "bcsearch",
25
+ };
26
+ /** The map of players. */
27
+ players = new Map();
28
+ /** The map of nodes. */
29
+ nodes = new Map();
30
+ /** The options that were set. */
31
+ options;
32
+ initiated = false;
33
+ /** The search result LRU cache (bounded, TTL-based). */
34
+ caches;
35
+ /** Whether the manager is shutting down. */
36
+ shuttingDown = false;
37
+ /** Returns the nodes sorted by least CPU load. */
38
+ get leastLoadNode() {
39
+ const sorted = [...this.nodes.entries()]
40
+ .filter(([, node]) => node.connected)
41
+ .sort(([, a], [, b]) => {
42
+ const aload = a.stats.cpu
43
+ ? (a.stats.cpu.lavalinkLoad / a.stats.cpu.cores) * 100
44
+ : 0;
45
+ const bload = b.stats.cpu
46
+ ? (b.stats.cpu.lavalinkLoad / b.stats.cpu.cores) * 100
47
+ : 0;
48
+ return aload - bload;
49
+ });
50
+ return new Map(sorted);
51
+ }
52
+ /** Returns the nodes sorted by least amount of players. */
53
+ get leastPlayersNode() {
54
+ const sorted = [...this.nodes.entries()]
55
+ .filter(([, node]) => node.connected)
56
+ .sort(([, a], [, b]) => a.stats.players - b.stats.players);
57
+ return new Map(sorted);
58
+ }
59
+ /** Returns the node with the lowest penalty score (best performance). */
60
+ get leastPenaltyNode() {
61
+ const connected = [...this.nodes.values()].filter((n) => n.connected);
62
+ if (!connected.length)
63
+ return undefined;
64
+ return connected.reduce((best, node) => node.penalties < best.penalties ? node : best);
65
+ }
66
+ /** Returns a node based on priority. */
67
+ get priorityNode() {
68
+ const filteredNodes = [...this.nodes.values()].filter((node) => node.connected && (node.options.priority ?? 0) > 0);
69
+ const totalWeight = filteredNodes.reduce((total, node) => total + (node.options.priority ?? 0), 0);
70
+ const weightedNodes = filteredNodes.map((node) => ({
71
+ node,
72
+ weight: (node.options.priority ?? 0) / totalWeight,
73
+ }));
74
+ const randomNumber = Math.random();
75
+ let cumulativeWeight = 0;
76
+ for (const { node, weight } of weightedNodes) {
77
+ cumulativeWeight += weight;
78
+ if (randomNumber <= cumulativeWeight)
79
+ return node;
80
+ }
81
+ return this.leastPenaltyNode;
82
+ }
83
+ /** Returns the best node to use based on configuration. */
84
+ get useableNodes() {
85
+ if (this.options.usePriority)
86
+ return this.priorityNode;
87
+ switch (this.options.useNode) {
88
+ case "leastLoad":
89
+ return this.leastLoadNode.values().next().value;
90
+ case "leastPlayers":
91
+ return this.leastPlayersNode.values().next().value;
92
+ default:
93
+ // Default: use penalty-based selection (best overall)
94
+ return this.leastPenaltyNode ?? this.leastLoadNode.values().next().value;
95
+ }
96
+ }
97
+ /**
98
+ * Initiates the Manager class.
99
+ * @param options
100
+ */
101
+ constructor(options) {
102
+ super();
103
+ (0, ManagerCheck_1.default)(options);
104
+ Utils_1.Structure.get("Player").init(this);
105
+ Utils_1.Structure.get("Node").init(this);
106
+ Utils_1.TrackUtils.init(this);
107
+ if (options.trackPartial) {
108
+ Utils_1.TrackUtils.setTrackPartial(options.trackPartial);
109
+ delete options.trackPartial;
110
+ }
111
+ this.options = {
112
+ plugins: [],
113
+ nodes: [
114
+ {
115
+ identifier: "default",
116
+ host: "localhost",
117
+ resumeStatus: true,
118
+ resumeTimeout: 60,
119
+ },
120
+ ],
121
+ shards: 1,
122
+ autoPlay: true,
123
+ usePriority: false,
124
+ clientName: "StellaLib/0.0.1 (https://github.com/Roki-Stella-Projects/StellaLib)",
125
+ defaultSearchPlatform: "youtube",
126
+ useNode: "leastPlayers",
127
+ caches: { enabled: false, time: 0, maxSize: 200 },
128
+ ...options,
129
+ };
130
+ // Initialize LRU cache for search results
131
+ const cacheOpts = this.options.caches;
132
+ this.caches = new LRUCache_1.LRUCache(cacheOpts?.maxSize ?? 200, cacheOpts?.enabled ? (cacheOpts.time || 0) : 0);
133
+ if (this.options.plugins) {
134
+ for (const [index, plugin] of this.options.plugins.entries()) {
135
+ if (!(plugin instanceof Utils_1.Plugin))
136
+ throw new RangeError(`Plugin at index ${index} does not extend Plugin.`);
137
+ plugin.load(this);
138
+ }
139
+ }
140
+ if (this.options.nodes) {
141
+ for (const nodeOptions of this.options.nodes) {
142
+ const node = new (Utils_1.Structure.get("Node"))(nodeOptions);
143
+ this.nodes.set(node.options.identifier, node);
144
+ }
145
+ }
146
+ // Periodic LRU cache pruning (removes expired entries)
147
+ if (cacheOpts?.enabled && cacheOpts.time > 0) {
148
+ setInterval(() => {
149
+ const pruned = this.caches.prune();
150
+ if (pruned > 0) {
151
+ this.emit("Debug", `[Cache] Pruned ${pruned} expired entries (${this.caches.size} remaining)`);
152
+ }
153
+ }, Math.max(cacheOpts.time, 30000));
154
+ }
155
+ }
156
+ /**
157
+ * Initiates the Manager.
158
+ * @param clientId
159
+ */
160
+ init(clientId) {
161
+ if (this.initiated)
162
+ return this;
163
+ if (typeof clientId !== "undefined")
164
+ this.options.clientId = clientId;
165
+ if (typeof this.options.clientId !== "string")
166
+ throw new Error('"clientId" set is not type of "string"');
167
+ if (!this.options.clientId)
168
+ throw new Error('"clientId" is not set. Pass it in Manager#init() or as an option in the constructor.');
169
+ for (const node of this.nodes.values()) {
170
+ Promise.resolve(node.connect()).catch((err) => {
171
+ this.emit("NodeError", node, err);
172
+ });
173
+ }
174
+ this.initiated = true;
175
+ this.emit("Debug", "[Manager] Initialized");
176
+ return this;
177
+ }
178
+ /**
179
+ * Searches the enabled sources based off the URL or the `source` property.
180
+ * @param query
181
+ * @param requester The user who requested the search.
182
+ */
183
+ async search(query, requester) {
184
+ const node = this.useableNodes;
185
+ if (!node)
186
+ throw new Error("No available nodes.");
187
+ if (this.options.caches?.enabled && typeof query === "string") {
188
+ const cached = this.caches.get(query);
189
+ if (cached)
190
+ return cached;
191
+ }
192
+ const _query = typeof query === "string" ? { query } : query;
193
+ const rawQuery = _query.query;
194
+ const isURL = /^https?:\/\//.test(rawQuery);
195
+ // Build the list of platforms to try: primary + fallbacks
196
+ const primarySource = (_query.source ?? this.options.defaultSearchPlatform ?? "youtube");
197
+ const platformsToTry = [primarySource];
198
+ if (!isURL && this.options.searchFallback?.length) {
199
+ for (const fb of this.options.searchFallback) {
200
+ if (fb !== primarySource)
201
+ platformsToTry.push(fb);
202
+ }
203
+ }
204
+ let lastError = null;
205
+ for (const platform of platformsToTry) {
206
+ const prefix = StellaManager.DEFAULT_SOURCES[platform] ?? platform;
207
+ const search = isURL ? rawQuery : `${prefix}:${rawQuery}`;
208
+ try {
209
+ const res = await node.rest.loadTracks(search);
210
+ if (!res)
211
+ continue;
212
+ // If empty or error, try next fallback
213
+ if (res.loadType === "empty" || res.loadType === "error") {
214
+ if (platformsToTry.length > 1) {
215
+ this.emit("Debug", `[Search] "${platform}" returned ${res.loadType} for "${rawQuery}", trying next fallback...`);
216
+ }
217
+ continue;
218
+ }
219
+ let searchData = [];
220
+ let playlistData;
221
+ switch (res.loadType) {
222
+ case "search":
223
+ searchData = res.data;
224
+ break;
225
+ case "track":
226
+ searchData = [res.data];
227
+ break;
228
+ case "playlist":
229
+ playlistData = res.data;
230
+ break;
231
+ }
232
+ const tracks = searchData.map((track) => Utils_1.TrackUtils.build(track, requester));
233
+ let playlist;
234
+ if (res.loadType === "playlist" && playlistData) {
235
+ playlist = {
236
+ name: playlistData.info.name,
237
+ tracks: playlistData.tracks.map((track) => Utils_1.TrackUtils.build(track, requester)),
238
+ duration: playlistData.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0),
239
+ };
240
+ }
241
+ const result = {
242
+ loadType: res.loadType,
243
+ tracks,
244
+ playlist,
245
+ };
246
+ if (this.options.replaceYouTubeCredentials) {
247
+ let tracksToReplace = [];
248
+ if (result.loadType === "playlist" && result.playlist) {
249
+ tracksToReplace = result.playlist.tracks;
250
+ }
251
+ else {
252
+ tracksToReplace = result.tracks;
253
+ }
254
+ for (const track of tracksToReplace) {
255
+ if (isYouTubeURL(track.uri)) {
256
+ track.author = track.author.replace("- Topic", "").trim();
257
+ track.title = track.title.replace("Topic -", "").trim();
258
+ }
259
+ if (track.title.includes("-")) {
260
+ const [author, title] = track.title
261
+ .split("-")
262
+ .map((str) => str.trim());
263
+ track.author = author;
264
+ track.title = title;
265
+ }
266
+ }
267
+ }
268
+ if (this.options.caches?.enabled) {
269
+ this.caches.set(search, result);
270
+ }
271
+ if (platform !== primarySource) {
272
+ this.emit("Debug", `[Search] Found results via fallback "${platform}" for "${rawQuery}"`);
273
+ }
274
+ return result;
275
+ }
276
+ catch (err) {
277
+ lastError = err;
278
+ this.emit("Debug", `[Search] Error on "${platform}" for "${rawQuery}": ${err.message}`);
279
+ }
280
+ }
281
+ // All platforms exhausted — return empty result
282
+ return {
283
+ loadType: "empty",
284
+ tracks: [],
285
+ playlist: undefined,
286
+ };
287
+ function isYouTubeURL(uri) {
288
+ return uri?.includes("youtube.com") || uri?.includes("youtu.be");
289
+ }
290
+ }
291
+ /**
292
+ * Returns the available source managers and plugins on a connected node.
293
+ * Useful for checking which search platforms the Lavalink server supports.
294
+ */
295
+ async getAvailableSources() {
296
+ const node = this.useableNodes;
297
+ if (!node)
298
+ throw new Error("No available nodes.");
299
+ if (!node.info) {
300
+ await node.fetchInfo();
301
+ }
302
+ return {
303
+ sourceManagers: node.info?.sourceManagers ?? [],
304
+ plugins: node.info?.plugins ?? [],
305
+ };
306
+ }
307
+ /**
308
+ * Decodes the base64 encoded tracks and returns a TrackData array.
309
+ * @param tracks
310
+ */
311
+ async decodeTracks(tracks) {
312
+ const node = this.nodes.values().next().value;
313
+ if (!node)
314
+ throw new Error("No available nodes.");
315
+ const res = await node.rest.decodeTracks(tracks);
316
+ if (!res)
317
+ throw new Error("No data returned from query.");
318
+ return res;
319
+ }
320
+ /**
321
+ * Decodes the base64 encoded track and returns a TrackData.
322
+ * @param track
323
+ */
324
+ async decodeTrack(track) {
325
+ const res = await this.decodeTracks([track]);
326
+ return res[0];
327
+ }
328
+ /**
329
+ * Creates a player or returns one if it already exists.
330
+ * @param options
331
+ */
332
+ create(options) {
333
+ if (this.players.has(options.guild)) {
334
+ return this.players.get(options.guild);
335
+ }
336
+ return new (Utils_1.Structure.get("Player"))(options);
337
+ }
338
+ /**
339
+ * Returns a player or undefined if it does not exist.
340
+ * @param guild
341
+ */
342
+ get(guild) {
343
+ return this.players.get(guild);
344
+ }
345
+ /**
346
+ * Destroys a player if it exists.
347
+ * @param guild
348
+ */
349
+ destroy(guild) {
350
+ this.players.delete(guild);
351
+ }
352
+ /**
353
+ * Creates a node or returns one if it already exists.
354
+ * @param options
355
+ */
356
+ createNode(options) {
357
+ if (this.nodes.has(options.identifier || options.host)) {
358
+ return this.nodes.get(options.identifier || options.host);
359
+ }
360
+ return new (Utils_1.Structure.get("Node"))(options);
361
+ }
362
+ /**
363
+ * Destroys a node if it exists.
364
+ * @param identifier
365
+ */
366
+ destroyNode(identifier) {
367
+ const node = this.nodes.get(identifier);
368
+ if (!node)
369
+ return;
370
+ node.destroy();
371
+ this.nodes.delete(identifier);
372
+ }
373
+ /**
374
+ * Sends voice data to the Lavalink server.
375
+ * Handles both VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE from Discord.
376
+ * Includes channelId in voice state to satisfy Lavalink v4 requirements.
377
+ * @param data
378
+ */
379
+ async updateVoiceState(data) {
380
+ if ("t" in data &&
381
+ !["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t))
382
+ return;
383
+ const update = "d" in data ? data.d : data;
384
+ if (!update || (!("token" in update) && !("session_id" in update)))
385
+ return;
386
+ const player = this.players.get(update.guild_id ?? update.guild_id);
387
+ if (!player)
388
+ return;
389
+ // VOICE_SERVER_UPDATE — contains token & endpoint
390
+ if ("token" in update) {
391
+ player.voiceState.event = update;
392
+ const { sessionId, event: { token, endpoint }, } = player.voiceState;
393
+ // Include channelId in voice state (required by Lavalink v4)
394
+ await player.node.rest
395
+ .updatePlayer({
396
+ guildId: player.guild,
397
+ data: {
398
+ voice: {
399
+ token,
400
+ endpoint,
401
+ sessionId: sessionId,
402
+ channelId: player.voiceChannel ?? undefined,
403
+ },
404
+ },
405
+ })
406
+ .then(() => {
407
+ player.resolveVoiceReady();
408
+ this.emit("Debug", `[Player:${player.guild}] Voice state flushed to Lavalink`);
409
+ })
410
+ .catch((err) => {
411
+ this.emit("NodeError", player.node, err instanceof Error ? err : new Error(String(err)));
412
+ });
413
+ return;
414
+ }
415
+ // VOICE_STATE_UPDATE — contains session_id & channel_id
416
+ const voiceUpdate = update;
417
+ if (voiceUpdate.user_id !== this.options.clientId)
418
+ return;
419
+ if (voiceUpdate.channel_id) {
420
+ if (player.voiceChannel !== voiceUpdate.channel_id) {
421
+ this.emit("PlayerMove", player, player.voiceChannel, voiceUpdate.channel_id);
422
+ }
423
+ player.voiceState.sessionId = voiceUpdate.session_id;
424
+ player.voiceState.channelId = voiceUpdate.channel_id;
425
+ player.voiceChannel = voiceUpdate.channel_id;
426
+ return;
427
+ }
428
+ // Channel is null — user disconnected
429
+ this.emit("PlayerDisconnect", player, player.voiceChannel);
430
+ player.voiceChannel = null;
431
+ player.voiceState = Object.assign({
432
+ op: "voiceUpdate",
433
+ guildId: player.guild,
434
+ });
435
+ player.destroy();
436
+ }
437
+ /**
438
+ * Gracefully shuts down the Manager: persists sessions, closes all nodes, and cleans up.
439
+ * Call this before your bot exits to enable seamless session resume on restart.
440
+ *
441
+ * Usage:
442
+ * ```ts
443
+ * process.on("SIGINT", async () => {
444
+ * await manager.shutdown();
445
+ * process.exit(0);
446
+ * });
447
+ * ```
448
+ */
449
+ async shutdown() {
450
+ if (this.shuttingDown)
451
+ return;
452
+ this.shuttingDown = true;
453
+ this.emit("Debug", "[Manager] Graceful shutdown initiated...");
454
+ // Gracefully close all nodes (persists session IDs for resume)
455
+ const closePromises = [];
456
+ for (const node of this.nodes.values()) {
457
+ closePromises.push(node.gracefulClose());
458
+ }
459
+ await Promise.allSettled(closePromises);
460
+ // Flush session store if it supports it
461
+ const store = this.options.sessionStore;
462
+ if (store && "destroy" in store && typeof store.destroy === "function") {
463
+ try {
464
+ store.destroy();
465
+ }
466
+ catch {
467
+ // Ignore
468
+ }
469
+ }
470
+ // Clear caches
471
+ this.caches.clear();
472
+ this.emit("Debug", `[Manager] Shutdown complete. ${this.nodes.size} nodes closed, sessions persisted.`);
473
+ }
474
+ /**
475
+ * Returns memory and performance statistics for monitoring.
476
+ */
477
+ getStats() {
478
+ const nodes = [...this.nodes.values()].map((node) => ({
479
+ identifier: node.options.identifier,
480
+ connected: node.connected,
481
+ players: node.stats.players,
482
+ playingPlayers: node.stats.playingPlayers,
483
+ penalties: node.penalties,
484
+ uptime: node.uptime,
485
+ memory: {
486
+ used: node.stats.memory.used,
487
+ free: node.stats.memory.free,
488
+ allocated: node.stats.memory.allocated,
489
+ },
490
+ restRequests: node.rest.requestCount,
491
+ restFailed: node.rest.failedCount,
492
+ }));
493
+ return {
494
+ nodes,
495
+ totalPlayers: this.players.size,
496
+ totalPlayingPlayers: [...this.players.values()].filter((p) => p.playing).length,
497
+ cacheSize: this.caches.size,
498
+ cacheMemoryEstimate: this.caches.memoryEstimate,
499
+ };
500
+ }
501
+ }
502
+ exports.StellaManager = StellaManager;
503
+ //# sourceMappingURL=Manager.js.map