Skip to content

Commit

Permalink
chore: Update Queue class and RestEventEmitter interface
Browse files Browse the repository at this point in the history
  • Loading branch information
EvarinDev committed Jul 21, 2024
1 parent d3a781c commit 08d4752
Show file tree
Hide file tree
Showing 14 changed files with 529 additions and 28 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"scripts": {
"build:js": "npx babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline",
"lint": "npx x eslint src/**/*.ts",
"test:connect": "bun test/connect.ts"
"test:connect": "bun test/connect.ts",
"test:manager": "bun test/manager.ts",
"test:player": "bun test/player.ts"
},
"devDependencies": {
"@eslint/js": "^9.7.0",
Expand All @@ -30,7 +32,9 @@
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/preset-env": "^7.24.8",
"@babel/preset-typescript": "^7.24.7",
"@discordjs/collection": "^2.1.0",
"axios": "^1.7.2",
"discord.js": "^14.15.3",
"tiny-typed-emitter": "^2.1.0",
"ws": "^8.18.0"
}
Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export * from "./structures/Node";
export * from "./types/Node";
export * from "./types/Node";
export * from "./structures/Manager";
export * from "./types/Manager";
export * from "./structures/Player";
export * from "./types/Player";
export * from "./structures/Rest";
export * from "./types/Rest";
130 changes: 128 additions & 2 deletions src/structures/Manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,133 @@
import { TypedEmitter } from "tiny-typed-emitter";
import type { ManagerEventEmitter, ManagerOptions } from "../types/Manager";
import { Node } from "./Node";
import type { PlayerCreate } from "../types/Player";
import { Player } from "./Player";
import { Collection } from "@discordjs/collection";
import type { VoiceState } from "discord.js";
import type { VoicePacket, VoiceServer } from "../types/Discord";

export class Manager extends TypedEmitter {
constructor() {
export class Manager extends TypedEmitter<ManagerEventEmitter> {
options: ManagerOptions;
public readonly players = new Collection<string, Player>();
public readonly nodes = new Collection<string, Node>();
constructor(options: ManagerOptions) {
super();
this.options = options;
for (const node of options.nodes) {
this.nodes.set(node.host, new Node(node));
}
}

private get priorityNode(): Node {
const filteredNodes = this.nodes.filter((node) => node.connected && node.options.priority > 0);
const totalWeight = filteredNodes.reduce((total, node) => total + node.options.priority, 0);
const weightedNodes = filteredNodes.map((node) => ({
node,
weight: node.options.priority / totalWeight,
}));
const randomNumber = Math.random();

let cumulativeWeight = 0;

for (const { node, weight } of weightedNodes) {
cumulativeWeight += weight;
if (randomNumber <= cumulativeWeight) {
return node;
}
}

return this.options.useNode === "leastLoad" ? this.leastLoadNode.first() : this.leastPlayersNode.first();
}

private get leastLoadNode(): Collection<string, Node> {
return this.nodes
.filter((node) => node.connected)
.sort((a, b) => {
const aload = a.stats.cpu ? (a.stats.cpu.lavalinkLoad / a.stats.cpu.cores) * 100 : 0;
const bload = b.stats.cpu ? (b.stats.cpu.lavalinkLoad / b.stats.cpu.cores) * 100 : 0;
return aload - bload;
});
}

private get leastPlayersNode(): Collection<string, Node> {
return this.nodes.filter((node) => node.connected).sort((a, b) => a.stats.players - b.stats.players);
}

public get useableNodes(): Node {
return this.options.usePriority ? this.priorityNode : this.options.useNode === "leastLoad" ? this.leastLoadNode.first() : this.leastPlayersNode.first();
}

public create(options: PlayerCreate): Player {
if (this.players.has(options.guild_id)) {
return this.players.get(options.guild_id);
}

return new Player(this, options);
}

public init() {
this.emit("raw", "Manager initialized");
for (const node of this.nodes.values()) {
try {
node.on("connect", () => {
this.emit("NodeConnect", node);
}).on("disconnect", () => {
this.emit("NodeDisconnect", node);
}).on("error", (err) => {
this.emit("NodeError", node, err);
}).on("stats", (stats) => {
this.emit("NodeStats", node, stats);
}).on("ready", () => {
this.emit("NodeReady", node);
}).on("raw", (data) => {
this.emit("NodeRaw", node, data);
});
node.connect();
} catch (err) {
this.emit("NodeError", node, err);
}
}
}
public async updateVoiceState(data: VoicePacket | VoiceServer | VoiceState): Promise<void> {
if ("t" in data && !["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t)) return;
const update = "d" in data ? data.d : data;
if (!update || (!("token" in update) && !("session_id" in update))) return;
const player = this.players.get(update.guild_id);
if (!player) return;
if ("token" in update) {
// @ts-ignore
if (!player.voiceState) player.voiceState = { event: {} };
player.voiceState.event = update;

const {
sessionId,
event: { token, endpoint },
} = player.voiceState;
console.log(player.voiceState)
await player.node.rest.updatePlayer({
guildId: player.guild_id,
data: { voice: { token, endpoint, sessionId } },
});

return;
}

if (update.user_id !== this.options.clientId) return;
if (update.channel_id) {
if (player.voiceChannel !== update.channel_id) {
this.emit("PlayerMove", player, player.voiceChannel, update.channel_id);
}

player.voiceState.sessionId = update.session_id;
player.voiceChannel = update.channel_id;
return;
}

this.emit("PlayerDisconnect", player, player.voiceChannel);
player.voiceChannel = null;
player.voiceState = Object.assign({});
player.destroy();
return;
}
}
37 changes: 25 additions & 12 deletions src/structures/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TypedEmitter } from "tiny-typed-emitter";
import { WebSocket } from "ws";
import type { NodeConfig, NodeEventEmitter, NodeStats } from "../types/Node";
import { Rest } from "./Rest";
import type { Manager } from "./Manager";

/* This TypeScript class extends TypedEmitter with a generic type of NodeEventEmitter. */
export class Node extends TypedEmitter<NodeEventEmitter> {
Expand All @@ -23,11 +24,14 @@ export class Node extends TypedEmitter<NodeEventEmitter> {
property can hold a value of type `NodeStats` (defined elsewhere in the codebase) or it can be
`null` if no value is assigned to it. */
public stats: NodeStats | null = null;
/* The line `session_id: string | null = null;` in the Node class is declaring a public property
named `session_id` with a type of `string` or `null`. This means that the `session_id` property
can hold a value of type `string`, representing a session identifier, or it can be `null` if no
value is assigned to it. */
session_id: string | null = null;
/* The line `public rest: Rest | null = null;` in the Node class is declaring a public property
named `rest` of type `Rest` or `null`. */
public rest: Rest | null = null;

/**
* The function checks if a WebSocket connection is open and returns a boolean value accordingly.
* @returns The `get connected` method returns a boolean value indicating whether the WebSocket
Expand All @@ -38,6 +42,8 @@ export class Node extends TypedEmitter<NodeEventEmitter> {
if (!this.socket) return false;
return this.socket.readyState === WebSocket.OPEN;
}
/* The line `private static _manager: Manager;` in the Node class is declaring a private static
property named `_manager` with a type of `Manager`. */
/**
* The constructor initializes a Rest object with the provided NodeConfig options.
* @param {NodeConfig} options - The `options` parameter is an object that contains configuration
Expand All @@ -47,11 +53,7 @@ export class Node extends TypedEmitter<NodeEventEmitter> {
constructor(options: NodeConfig) {
super();
this.options = options;
this.rest = new Rest({
host: options.host,
password: options.password,
port: options.port,
})
this.rest = new Rest(this);
}
/**
* The `connect` function establishes a WebSocket connection with specified headers and event
Expand All @@ -63,18 +65,18 @@ export class Node extends TypedEmitter<NodeEventEmitter> {
if (this.connected) return;
const headers = Object.assign({
"Authorization": this.options.password,
"Client-Name": "Lavalink",
"Client-Name": this.options.clientName || `Sunday.ts/${require("../../package.json").version}`,
"User-Id": "213",
})
this.socket = new WebSocket(`ws${this.options.secure ? "s" : ""}://${this.options.host}:${this.options.port}/v4/websocket`, { headers });
this.socket.on("open", () => {
this.emit("connect");
});
this.socket.on("close", () => {
console.log("Connection closed");
this.socket.on("close", (data) => {
this.emit("disconnect", data);
});
this.socket.on("error", (error) => {
console.error(error);
this.emit("error", error);
});
this.socket.on("message", this.onMessage.bind(this));
}
Expand All @@ -92,8 +94,16 @@ export class Node extends TypedEmitter<NodeEventEmitter> {
this.emit("raw", payload);
switch (payload?.op) {
case "ready": {
this.session_id = payload.session_id;
this.emit("ready");
this.rest.setSessionId(payload.sessionId);
this.session_id = payload.sessionId;

if (this.options.resumeStatus) {
this.rest.patch(`/v4/sessions/${this.session_id}`, {
resuming: this.options.resumeStatus,
timeout: this.options.resumeTimeout,
});
}
this.emit("ready")
break;
}
case "stats": {
Expand All @@ -103,4 +113,7 @@ export class Node extends TypedEmitter<NodeEventEmitter> {
}
}
}
private debug(message: string) {
return this.emit("raw", message);
}
}
106 changes: 105 additions & 1 deletion src/structures/Player.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,107 @@
import type { VoiceState } from "../types/Discord";
import type { PlayerCreate } from "../types/Player";
import type { Manager } from "./Manager";
import type { Node } from "./Node";
import { Queue } from "./Queue";

export class Player {

manager: Manager;
options: PlayerCreate;
node: Node;
voiceChannel: string;
state: "CONNECTING" | "CONNECTED" | "DISCONNECTED" | "DISCONNECTING" | "DESTROYING" = "DISCONNECTED";
guild_id: string;
public voiceState: VoiceState;
paused: boolean = false;
playing: boolean = false;
textChannel: string
public readonly queue = new Queue();
constructor(manager: Manager, options: PlayerCreate) {
this.manager = manager;
if (!this.manager) throw new RangeError("Manager has not been initiated.");
this.options = options;
this.voiceChannel = options.voiceChannel;
this.voiceState = Object.assign({
op: "voiceUpdate",
guild_id: options.guild_id,
});
if (options.voiceChannel) this.voiceChannel = options.voiceChannel;
if (options.textChannel) this.textChannel = options.textChannel;
const node = this.manager.nodes.get(options.node);
this.node = node || this.manager.useableNodes;
if (!this.node) throw new RangeError("No available nodes.");
this.guild_id = options.guild_id;
this.manager.players.set(options.guild_id, this);
this.manager.emit("PlayerCreate", this);
}

public connect(): this {
if (!this.voiceChannel) throw new RangeError("No voice channel has been set.");
this.state = "CONNECTING";

this.manager.options.send(this.guild_id, {
op: 4,
d: {
guild_id: this.guild_id,
channel_id: this.voiceChannel,
self_mute: this.options.selfMute || false,
self_deaf: this.options.selfDeafen || false,
},
});

this.state = "CONNECTED";
return this;
}
public disconnect(): this {
if (this.voiceChannel === null) return this;
this.state = "DISCONNECTING";

this.pause(true);
this.manager.options.send(this.guild_id, {
op: 4,
d: {
guild_id: this.guild_id,
channel_id: null,
self_mute: false,
self_deaf: false,
},
});

this.voiceChannel = null;
this.state = "DISCONNECTED";
return this;
}

/** Destroys the player. */
public destroy(disconnect = true): void {
this.state = "DESTROYING";

if (disconnect) {
this.disconnect();
}

//this.node.rest.destroyPlayer(this.guild);
this.manager.emit("PlayerDestroy", this);
this.manager.players.delete(this.guild_id);
}
public pause(pause: boolean): this {
if (typeof pause !== "boolean") throw new RangeError('Pause can only be "true" or "false".');

if (this.paused === pause || !this.queue.totalSize) return this;

const oldPlayer = { ...this };

this.playing = !pause;
this.paused = pause;

this.node.rest.updatePlayer({
guildId: this.guild_id,
data: {
paused: pause,
},
});

this.manager.emit("PlayerStateUpdate", oldPlayer, this);
return this;
}
}
6 changes: 6 additions & 0 deletions src/structures/Queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class Queue {
totalSize: number = 0;
constructor() {

}
}
Loading

0 comments on commit 08d4752

Please sign in to comment.