Skip to content

Commit

Permalink
Fix issues with client packets and add tests for it
Browse files Browse the repository at this point in the history
  • Loading branch information
Ancocodet committed Dec 28, 2023
1 parent c66228b commit cb5cf33
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 90 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"scripts": {
"start": "bun run --bun src/index.ts",
"build": "bun build src/index.ts --outdir ./build"
"build": "bun build src/index.ts --outdir ./build",
"test": "bun test"
}
}
72 changes: 2 additions & 70 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,9 @@
import {Server, ServerWebSocket} from "bun";
import {ClientData} from "@/types.ts";
import {handleHealthEndpoints} from "@/routes/health.ts";
import {Logger} from "@/lib/logger.ts";
import {ClientPacket, JoinPacket, Packet, QuitPacket, ServerPacket, WelcomePacket} from "@/lib/packet.ts";
import RoomManager from "@/lib/room_manager.ts";
import startServer from "@/lib/server.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("/")
// Rooms will be available via /room/<room-uuid>
if (routes[1].toLowerCase() === 'server') {

if (routes.length < 3) {
return new Response(JSON.stringify({message: 'Page not found'}), {status: 404});
}

if (routes[2].toLowerCase() === 'room') {
if (routes.length < 4) {
return new Response(JSON.stringify({message: "No roomId provided"}), {status: 400})
}

let roomId = routes[3].toLowerCase();
if (!roomId.match(/[a-z\d-]{6,}/)) {
return new Response(JSON.stringify({message: "Invalid roomId provided"}), {status: 400})
}

const success = server.upgrade(request, {
data: {
roomId: roomId,
uuid: crypto.randomUUID()
}
});
return success ? undefined : new Response(JSON.stringify({message: "WebSocket upgrade error"}), {status: 400});
}
} else if(routes[1].toLowerCase() === 'health') {
return handleHealthEndpoints(routes)
}
return new Response(JSON.stringify({message: 'Page not found'}), {status: 404});
},
websocket: {
open(webSocket: ServerWebSocket<ClientData>): void | Promise<void> {
if (roomManager.joinRoom(webSocket.data.roomId, webSocket.data.uuid)) {
logger.info(`${webSocket.data.uuid} connected to room ${webSocket.data.roomId}`)

server.publish(webSocket.data.roomId, new JoinPacket(webSocket.data.uuid).toString())
webSocket.subscribe(webSocket.data.roomId)
webSocket.send(new WelcomePacket(webSocket.data.uuid, roomManager.getRoom(webSocket.data.roomId)).toString())
} else {
webSocket.close(1011, new ServerPacket("LIMIT", webSocket.data.roomId).toString())
}
},
message: function (webSocket: ServerWebSocket<ClientData>, message: string): void | Promise<void> {
let packet = JSON.parse(message) as Packet;
if(packet !== undefined && packet.getType().startsWith("CLIENT_")) {
let clientPacket = new ClientPacket(webSocket.data.uuid, packet.getType(), packet.getData());
webSocket.publish(webSocket.data.roomId, clientPacket.toString())
} else {
webSocket.send(new ServerPacket("ERROR", {'message': `Invalid packet type: ${packet.getType()}`}).toString())
}
},
close(webSocket: ServerWebSocket<ClientData>, code: number, reason: string): void | Promise<void> {
roomManager.leaveRoom(webSocket.data.roomId, webSocket.data.uuid)
logger.info(`${webSocket.data.uuid} disconnected from room ${webSocket.data.roomId}`)

webSocket.unsubscribe(webSocket.data.roomId)
server.publish(webSocket.data.roomId, new QuitPacket(webSocket.data.uuid).toString())
}
}
});

const server = startServer(logger, roomManager)
logger.info(`Listening on ${server.hostname}:${server.port}`)
22 changes: 3 additions & 19 deletions src/lib/packet.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
export class Packet {
private readonly type: string;
private readonly data: unknown;
public readonly type: string;
public readonly data: unknown;

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

getType() : string {
return this.type
}

getData() : unknown {
return this.data
}

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

export class ServerPacket extends Packet {
constructor(type: string, data: unknown) {
super("ROOM_" + type, data);
}

}

export class ClientPacket extends Packet {

private readonly sender: string;
public readonly sender: string;

constructor(sender: string, type: string, data: unknown) {
super(type, data);
Expand Down
76 changes: 76 additions & 0 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import RoomManager from "@/lib/room_manager.ts";
import {ClientData} from "@/types.ts";
import {Server, ServerWebSocket} from "bun";
import {handleHealthEndpoints} from "@/routes/health.ts";
import {ClientPacket, JoinPacket, Packet, QuitPacket, ServerPacket, WelcomePacket} from "@/lib/packet.ts";
import {Logger} from "@/lib/logger.ts";

export default function startServer(logger: Logger, roomManager: RoomManager, port: number = 3000) : Server {
const server = Bun.serve<ClientData>({
port: port,
development: false,
fetch(request: Request, server: Server): undefined | Response {
const url = new URL(request.url)
let routes = url.pathname.split("/")
// Rooms will be available via /room/<room-uuid>
if (routes[1].toLowerCase() === 'server') {

if (routes.length < 3) {
return new Response(JSON.stringify({message: 'Page not found'}), {status: 404});
}

if (routes[2].toLowerCase() === 'room') {
if (routes.length < 4) {
return new Response(JSON.stringify({message: "No roomId provided"}), {status: 400})
}

let roomId = routes[3].toLowerCase();
if (!roomId.match(/[a-z\d-]{6,}/)) {
return new Response(JSON.stringify({message: "Invalid roomId provided"}), {status: 400})
}

const success = server.upgrade(request, {
data: {
roomId: roomId,
uuid: crypto.randomUUID()
}
});
return success ? undefined : new Response(JSON.stringify({message: "WebSocket upgrade error"}), {status: 400});
}
} else if(routes[1].toLowerCase() === 'health') {
return handleHealthEndpoints(routes)
}
return new Response(JSON.stringify({message: 'Page not found'}), {status: 404});
},
websocket: {
open(webSocket: ServerWebSocket<ClientData>): void | Promise<void> {
if (roomManager.joinRoom(webSocket.data.roomId, webSocket.data.uuid)) {
logger.info(`${webSocket.data.uuid} connected to room ${webSocket.data.roomId}`)

server.publish(webSocket.data.roomId, JSON.stringify(new JoinPacket(webSocket.data.uuid)))
webSocket.subscribe(webSocket.data.roomId)
webSocket.sendText(JSON.stringify(new WelcomePacket(webSocket.data.uuid, roomManager.getRoom(webSocket.data.roomId))), true)
} else {
webSocket.close(1011, JSON.stringify(new ServerPacket("LIMIT", webSocket.data.roomId)))
}
},
message: function (webSocket: ServerWebSocket<ClientData>, message: string): void | Promise<void> {
let packet = JSON.parse(message) as Packet;
if(packet !== undefined && packet.type.startsWith("CLIENT_")) {
let clientPacket = new ClientPacket(webSocket.data.uuid, packet.type, packet.data);
webSocket.publish(webSocket.data.roomId, JSON.stringify(clientPacket))
} else {
webSocket.send(JSON.stringify(new ServerPacket("ERROR", {'message': `Invalid packet type: ${packet.type}`})))
}
},
close(webSocket: ServerWebSocket<ClientData>, code: number, reason: string): void | Promise<void> {
roomManager.leaveRoom(webSocket.data.roomId, webSocket.data.uuid)
logger.info(`${webSocket.data.uuid} disconnected from room ${webSocket.data.roomId}`)

webSocket.unsubscribe(webSocket.data.roomId)
server.publish(webSocket.data.roomId, JSON.stringify(new QuitPacket(webSocket.data.uuid)))
}
}
});
return server;
}
1 change: 1 addition & 0 deletions tests/health.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {describe, expect, test} from "bun:test";
import {handleHealthEndpoints} from "@/routes/health.ts";

describe("health-endpoints", () => {
test("invalid", () => {
expect(handleHealthEndpoints(["bla", "health", "invalid"])?.status).toBe(404)
Expand Down
73 changes: 73 additions & 0 deletions tests/join.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {afterAll, beforeAll, beforeEach, describe, expect, test} from "bun:test";
import {Logger} from "@/lib/logger.ts";
import RoomManager from "@/lib/room_manager.ts";
import startServer from "@/lib/server.ts";
import {Server} from "bun";
import {Packet} from "@/lib/packet.ts";

describe('join', () => {

let server: Server;

beforeAll(() => {
const logger = new Logger();
const roomManager = new RoomManager(4);
server = startServer(logger, roomManager, 3000)
})

afterAll(() => {
server.stop(true);
})

test('Server allows connection to room', async () => {
const clientOne: WebSocket = new WebSocket("ws://localhost:3000/server/room/abc-123")

let lastPacket = new Packet('ERROR', 'none')
clientOne.addEventListener("message", function (event) {
lastPacket = JSON.parse(<string>event.data) as Packet;
clientOne.terminate()
})

let error: number = 0;
clientOne.addEventListener("error", event => {
error += 1;
})

await waitForSocketState(clientOne, WebSocket.CLOSED);

expect(lastPacket.type).toBe("ROOM_WELCOME");
expect(error).toBe(0);
})

test('Join packet is broadcasted', async () => {
const clientOne: WebSocket = new WebSocket("ws://localhost:3000/server/room/abc-123")

let lastPacket= new Packet('ERROR', 'none')
clientOne.addEventListener("message", function (event) {
lastPacket = JSON.parse(<string>event.data) as Packet;
})

const clientTwo: WebSocket = new WebSocket("ws://localhost:3000/server/room/abc-123")
await waitForSocketState(clientTwo, WebSocket.OPEN)

clientOne.close()
clientTwo.close()

await waitForSocketState(clientOne, WebSocket.CLOSED);
await waitForSocketState(clientTwo, WebSocket.CLOSED);

expect(lastPacket.type).toBe("ROOM_JOIN")
})
})

function waitForSocketState(socket: WebSocket, state: WebSocketReadyState) {
return new Promise<void>(function (resolve) {
setTimeout(function () {
if (socket.readyState === state) {
resolve();
} else {
waitForSocketState(socket, state).then(resolve);
}
}, 5);
});
}

0 comments on commit cb5cf33

Please sign in to comment.