// import { randomInt } from "./deps.ts"; const SERVER_VERSION = "0.1.0"; // TODO: version comparison type ID = string; // app state const allLobbies = new Map(); const allClients = new Map(); // TODO: client index by id interface DataMessage { type: string; } type ServerDataObject = Record; type ServerData = ServerDataObject[] | ServerDataObject | string; type Message = string | DataMessage; const broadcast = (message: Message) => allClients.forEach((client) => client.send(message)); const buildMessage = ( type: string, data: ServerData | ServerData[], ) => Object.assign( { type }, Array.isArray(data) ? { data } : (typeof data === "object" ? data : { data }), ); class Client { id: ID; peerId: number | null; name: string; socket: WebSocket; lobby: Lobby | null; constructor(socket: WebSocket) { this.id = crypto.randomUUID(); this.socket = socket; this.peerId = null; this.name = "Client"; this.lobby = null; allClients.set(this.id, this); } isConnected() { return this.socket.readyState == WebSocket.OPEN; } remove() { this.lobbyLeave(); if (this.isConnected()) { this.socket.close(); } allClients.delete(this.id); } send(message: Message) { try { if (this.isConnected()) { this.socket.send( typeof message === "object" ? ("json:" + JSON.stringify(message)) : message, ); } } catch (e) { console.error( `Failed to send on socket ${this.socket} to client ${this.id}. Disconnecting and removing...`, ); } } clientList() { if (!this.lobby) return; const netClients: { id: ID; name: string; peerId: number | null }[] = []; this.lobby.clients.forEach(({ id, name, peerId }) => netClients.push({ id, name, peerId }) ); // TODO: chunk async? } lobbyList() { const netLobbies: { id: ID; name: string }[] = []; allLobbies.forEach(({ id, name }) => netLobbies.push({ id, name })); // TODO: chunk async? this.send(buildMessage("lobby_list", netLobbies)); } lobbyNew({ id, name }: Lobby) { // if the client is already in a lobby, we don't care about new lobbies if (this.lobby) return; this.send(buildMessage("lobby_new", { id, name })); } lobbyDelete({ id }: Lobby) { if (this.lobby) return; this.send(buildMessage("lobby_delete", { id })); } lobbyCreate() { if (this.lobby) { this.send( `[info] cannot create lobby (already in lobby ${this.lobby.id})`, ); return; } new Lobby(this); } lobbyJoin(lobby: Lobby) { if (this.lobby) { this.send(`[info] cannot join lobby (already in lobby ${this.lobby.id})`); return; } this.lobby = lobby; lobby.addClient(this); this.send( buildMessage("lobby_joined", { id: lobby.id, peerId: this.peerId }), ); } lobbyLeave() { const leavingLobby = this.lobby; if (!leavingLobby) { this.send(`[info] cannot leave lobby (not in a lobby)`); return; } this.peerId = null; this.lobby = null; if (this.isConnected()) { this.send(buildMessage("lobby_left", { id: leavingLobby.id })); } leavingLobby.removeClient(this); } } class Lobby { id: ID; name: string; clients: Map; hostClientId: ID; constructor(host: Client, name?: string) { this.id = crypto.randomUUID(); this.hostClientId = host.id; this.clients = new Map(); this.name = name || this.id; allLobbies.set(this.id, this); host.peerId = 1; host.lobbyJoin(this); allClients.forEach((client) => client.lobbyNew(this)); } remove() { allClients.forEach((client) => client.lobbyDelete(this)); this.clients.forEach((client) => { client.lobbyLeave(); }); allLobbies.delete(this.id); } broadcast(message: Message) { this.clients.forEach((client) => client.send(message)); } addClient(client: Client) { if (!client.peerId) { const arr = new Int32Array(1); crypto.getRandomValues(arr); client.peerId = Math.abs(arr[0]); } client.send(buildMessage("your_peer_id", client.peerId.toString())); this.broadcast( buildMessage("peer_joined", { id: client.id, name: client.name, peerId: client.peerId, }), ); console.log("Sending peer_joined..."); client.send( buildMessage( "peer_joined", Array.from(this.clients.values()).map( ({ id, name, peerId }) => ({ id, name, peerId }), ), ), ); this.clients.set(client.id, client); } clientList() { return Array.from(this.clients.values()).map( ({ id, name, peerId }) => ({ id, name, peerId }), ); } removeClient({ id }: Client) { this.clients.delete(id); this.broadcast(buildMessage("peer_left", { id })); if (id === this.hostClientId) { console.warn("Host left!"); this.remove(); } } } interface ClientMessage { type: "candidate" | "offer" | "answer"; data: ServerDataObject; } // events function onMessage(client: Client, ev: MessageEvent) { // TODO: log who from? const msg = ev.data.trim(); console.log("Client Message Received", msg); if (msg === "init") client.send(buildMessage("your_id", client.id)); if (msg === "lobby_create") client.lobbyCreate(); if (msg === "lobby_leave") client.lobbyLeave(); if (msg === "request_lobby_list") client.lobbyList(); if (msg === "request_peer_list") { if (client.lobby == null) { client.send(`[info] not in a lobby`); } else { client.send(buildMessage("peer_joined", client.lobby.clientList())); } } if (msg.startsWith("lobby_join:")) { const id = msg.substr(11); const lobby = allLobbies.get(id); if (lobby) { client.lobbyJoin(lobby); client.clientList(); } else client.send(`[info] could not find lobby ${id}`); } if (msg.startsWith("json:")) { const data: ClientMessage = JSON.parse(msg.substr(5)); if (["candidate", "answer", "offer"].includes(data.type)) { console.log("Received WebRTC Negotiation Message..."); if (typeof data.data === "object") { const subdata = data.data; if (typeof subdata["peerId"] === "number") { const destPeerId: number = subdata["peerId"]; for (const iClient of client.lobby?.clients.values() || []) { if (iClient.peerId == destPeerId) { const payload = Object.assign({}, data); const srcPeerId = client.peerId; payload.data.peerId = srcPeerId; console.log( `Forwarding WebRTC Negotiation Message from peer ${srcPeerId} to peer ${destPeerId}...`, ); iClient.send(payload); break; } } } } } } } function onSocketOpen(client: Client, _ev: Event) { console.log("New Client", client.id); } function onClientLeave(client: Client) { client.remove(); } function onSocketClose(client: Client, _ev: Event) { console.log("Client Close"); onClientLeave(client); } function onSocketError(client: Client, _ev: Event) { console.log("Client Error"); onClientLeave(client); } const PORT = parseInt(Deno.env.get("PORT") || "80"); console.log("Listening on port", PORT); const listener = Deno.listen({ port: PORT }); for await (const conn of listener) { // console.debug("Connection received:", conn); (async () => { const server = Deno.serveHttp(conn); for await (const { respondWith, request } of server) { // console.debug("HTTP Request Received", request); try { // console.warn(JSON.stringify([allClients, allLobbies])); const { socket, response } = Deno.upgradeWebSocket(request); const client = new Client(socket); socket.onmessage = (ev) => onMessage(client, ev); socket.onopen = (ev) => onSocketOpen(client, ev); socket.onclose = (ev) => onSocketClose(client, ev); socket.onerror = (ev) => onSocketError(client, ev); respondWith(response); } catch (e) { console.log("Could not add client for unhandled reason:", e); respondWith( new Response("400 Bad Request", { status: 400, headers: { "content-type": "text/html" }, }), ); } } })(); }