Skip to content

Commit

Permalink
Refactor server logics and introduces RoomManager, UUID based user sy…
Browse files Browse the repository at this point in the history
…stem.

Refactored server logic in the source index file to now utilize a brand new RoomManager class and UUID based identification system, rather than relying on a username header. RoomManager class allows the system to easily manage joins and leaves of a room, as well optimal handling and removal of empty rooms. UUID based user identification was chosen over the previous username system to ensure uniqueness of each user and to avoid potential collisions. This will greatly improve the consistency and performance of the system.
  • Loading branch information
Ancocodet committed Dec 1, 2023
1 parent feb9539 commit 0ccdb11
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 14 deletions.
33 changes: 20 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import {Server, ServerWebSocket} from "bun";
import {ClientData} from "@/types.ts";
import {handleHealthEndpoints} from "@/routes/health.ts";
import {Logger} from "@/lib/logger.ts";
import {SimplePacket} from "@/lib/packet.ts";
import RoomManager from "@/lib/room_manager.ts";

const logger = new Logger();
const roomManager = new RoomManager(4)
const server = Bun.serve<ClientData>({
port: 3000,
development: false,
fetch(request: Request, server: Server): undefined | Response {
const url = new URL(request.url)
let routes = url.pathname.split("/")
Expand All @@ -25,16 +30,10 @@ const server = Bun.serve<ClientData>({
return new Response(JSON.stringify({message: "Invalid roomId provided"}), {status: 400})
}

// Check if a username is provided
if (!request.headers.has('X-Username')) {
logger.warning("Request with missing header was sent")
return new Response(JSON.stringify({message: "Username is required"}), {status: 400})
}

const success = server.upgrade(request, {
data: {
roomId: roomId,
username: request.headers.get('X-Username')
uuid: crypto.randomUUID()
}
});
return success ? undefined : new Response(JSON.stringify({message: "WebSocket upgrade error"}), {status: 400});
Expand All @@ -46,17 +45,25 @@ const server = Bun.serve<ClientData>({
},
websocket: {
open(webSocket: ServerWebSocket<ClientData>): void | Promise<void> {
logger.info(`${webSocket.data.username} connected to room ${webSocket.data.roomId}`)
webSocket.subscribe(webSocket.data.roomId)
webSocket.publish(webSocket.data.roomId, `${webSocket.data.username}: joined the room`)
if (roomManager.joinRoom(webSocket.data.roomId, webSocket.data.uuid)) {
logger.info(`${webSocket.data.uuid} connected to room ${webSocket.data.roomId}`)
webSocket.subscribe(webSocket.data.roomId)

webSocket.send(new SimplePacket("INFO", roomManager.getRoom(webSocket.data.roomId)).toString())
webSocket.publish(webSocket.data.roomId, new SimplePacket('JOIN', webSocket.data.uuid).toString())
} else {
webSocket.close(1011, new SimplePacket("LIMIT", webSocket.data.roomId).toString())
}
},
message: function (webSocket: ServerWebSocket<ClientData>, message: string | Buffer): void | Promise<void> {
webSocket.publish(webSocket.data.roomId, `${webSocket.data.username}: ${message}`)
webSocket.publish(webSocket.data.roomId, message)
},
close(webSocket: ServerWebSocket<ClientData>, code: number, reason: string): void | Promise<void> {
logger.info(`${webSocket.data.username} disconnected from room ${webSocket.data.roomId}`)
roomManager.leaveRoom(webSocket.data.roomId, webSocket.data.uuid)
logger.info(`${webSocket.data.uuid} disconnected from room ${webSocket.data.roomId}`)

webSocket.unsubscribe(webSocket.data.roomId)
webSocket.publish(webSocket.data.roomId, `${webSocket.data.username}: left the room`)
webSocket.publish(webSocket.data.roomId, new SimplePacket('QUIT', webSocket.data.uuid).toString())
}
}
});
Expand Down
18 changes: 18 additions & 0 deletions src/lib/packet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export class SimplePacket {

private readonly type: string;
private readonly data: unknown;

constructor(type: string, data: unknown) {
this.type = type;
this.data = data;
}

toString() : string {
return JSON.stringify({
'type': this.type,
'data': this.data
})
}

}
78 changes: 78 additions & 0 deletions src/lib/room_manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
export default class RoomManager {

private readonly maxClients: number;
private rooms: Map<string, Room> = new Map;

constructor(maxClients: number) {
this.maxClients = maxClients;
}

joinRoom(room: string, client: string) : boolean {
if ( ! this.rooms.has(room) ) {
this.rooms.set(room, new Room(client))
} else {
if(this.rooms.get(room)!.getClients() >= this.maxClients){
return false
}
this.rooms.get(room)?.join(client)
}
return true
}

leaveRoom(room: string, client: string) {
if( this.rooms.has(room) ) {
this.rooms.get(room)?.leave(client)
if(this.rooms.get(room)!.getClients() <= 0) {
this.rooms.delete(room)
}
}
}

getRoom(room: string) : string {
if( this.rooms.has(room) ) {
let roomData = this.rooms.get(room);
return JSON.stringify({
'host': roomData?.getHost(),
'clients': roomData?.getClientList()
});
}
return ""
}
}

class Room {

private clients: string[] = [];
private host: string;

constructor(host: string) {
this.host = host
this.clients.push(host)
}

getHost() : string {
return this.host
}

getClientList() : string[] {
return this.clients
}

getClients() : number {
return this.clients.length
}

join(client: string) {
if(!this.clients.includes(client))
this.clients.push(client)
}

leave(client: string) {
if(this.clients.includes(client))
this.clients = this.clients.filter(c => c !== client)
if(client === this.host && this.getClients() > 0) {
this.host = this.clients[0];
}
}

}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface ClientData {
roomId: string,
username: string
uuid: string
}

0 comments on commit 0ccdb11

Please sign in to comment.