godot-webrtc-mplayer-testing/server.ts

360 lines
9.3 KiB
TypeScript

import { randomInt } from "./deps.ts";
const SERVER_VERSION = "0.1.0";
// TODO: version comparison
type ID = string;
// app state
const allLobbies = new Map<ID, Lobby>();
const allClients = new Map<ID, Client>();
interface DataMessage {
type: string;
}
type ServerData = Record<string, string | number | symbol> | 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;
name: string;
socket: WebSocket;
lobby: Lobby | null;
constructor(socket: WebSocket) {
this.id = crypto.randomUUID();
this.socket = socket;
this.name = "Client";
this.lobby = null;
this.send(buildMessage("your-id", this.id));
console.log(this);
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 {
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 }[] = [];
this.lobby.clients.forEach(({ id, name }) => netClients.push({ id, name }));
// TODO: chunk async?
this.send(buildMessage("peer_list", netClients));
}
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;
this.send(buildMessage("lobby_joined", { id: lobby.id }));
lobby.addClient(this);
}
lobbyLeave() {
const leavingLobby = this.lobby;
if (!leavingLobby) {
this.send(`[info] cannot leave lobby (not in a lobby)`);
return;
}
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<ID, Client>;
hostClientId: ID;
constructor(host: Client, name?: string) {
this.id = crypto.randomUUID();
this.hostClientId = host.id;
this.clients = new Map<ID, Client>();
this.name = name || this.id;
allLobbies.set(this.id, this);
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) {
this.clients.set(client.id, client);
this.broadcast(
buildMessage("peer_joined", { id: client.id, name: client.name }),
);
}
removeClient({ id }: Client) {
this.clients.delete(id);
this.broadcast(buildMessage("peer_left", { id }));
if (id === this.hostClientId) {
console.warn("Host left!");
this.remove();
}
}
}
// events
function onMessage(client: Client, ev: MessageEvent) {
// TODO: log who from?
console.log("Client Message Received", ev.data);
if (ev.data === "lobby_create") client.lobbyCreate();
if (ev.data === "lobby_leave") client.lobbyLeave();
if (ev.data === "request_lobby_list") client.lobbyList();
if (ev.data === "request_peer_list") client.clientList();
if (ev.data.startsWith("lobby_join:")) {
const id = ev.data.substr(11);
const lobby = allLobbies.get(id);
if (lobby) client.lobbyJoin(lobby);
else client.send(`[info] could not find lobby ${id}`);
}
}
function onSocketOpen(_client: Client, _ev: Event) {
console.log("New Client");
}
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" },
}),
);
}
}
})();
}
/*
function peerCreatesLobby(peer: Peer, lobbyName: string) {
// TODO: ensure we can create a lobby
lobbies.add({
})
}
*/
/*
function peerJoinsLobby(peer: Peer, lobby: Lobby) {
lobbyName = randomSecret();
lobbies.set(lobbyName, new Lobby(lobbyName, this.id));
console.log(`Peer ${this.id} created lobby ${lobbyName}`);
console.log(`Open lobbies: ${lobbies.size}`);
}
const lobby = lobbies.get(lobbyName);
if (!lobby) throw new ProtoError(4000, STR_LOBBY_DOES_NOT_EXIST);
if (lobby.sealed) throw new ProtoError(4000, STR_LOBBY_IS_SEALED);
this.lobby = lobbyName;
console.log(
`Peer ${this.id} joining lobby ${lobbyName} ` +
`with ${lobby.peers.length} peers`,
);
lobby.join(this);
this.ws.send(`J: ${lobbyName}\n`);
}
// TODO: ensure peer not already in a lobby
const assigned = this.getPeerId(peer);
peer.ws.send(`I: ${assigned}\n`);
this.peers.forEach((p) => {
p.ws.send(`N: ${assigned}\n`);
peer.ws.send(`N: ${this.getPeerId(p)}\n`);
});
this.peers.push(peer);
}
*/
/*
function peerLeavesLobby(peer: Peer) {
const idx = this.peers.findIndex((p) => peer === p);
if (idx === -1) return false;
const assigned = this.getPeerId(peer);
const close = assigned === 1;
this.peers.forEach((p) => {
try {
// room host disconnected
if (close) p.ws.close(4000, STR_HOST_DISCONNECTED);
// notify peers
else p.ws.send(`D: ${assigned}\n`);
} catch (e) {
console.error(`Error when leaving: ${e}`);
}
});
this.peers.splice(idx, 1);
if (close && this.closeTimer >= 0) {
// we are closing already.
clearTimeout(this.closeTimer);
this.closeTimer = -1;
}
return close;
}
*/
/*
function graduateLobby(lobby: Lobby) {
// only host can seal
if (peer.id !== this.host) {
throw new ProtoError(4000, STR_ONLY_HOST_CAN_SEAL);
}
this.sealed = true;
this.peers.forEach((p) => {
p.ws.send("S: \n");
});
console.log(
`Peer ${peer.id} sealed lobby ${this.name} ` +
`with ${this.peers.length} peers`,
);
this.closeTimer = setTimeout(() => {
// close peer connection to host (and thus the lobby)
this.peers.forEach((p) => {
p.ws.close(1000, STR_SEAL_COMPLETE);
});
}, SEAL_CLOSE_TIMEOUT);
}
}
*/
/*
function parseMsg(peer: Peer, msg: string) {
// TODO: modify this?
// O: Client is sending an offer.
// A: Client is sending an answer.
// C: Client is sending a candidate.
let destId = parseInt(cmd.substr(3).trim());
// Dest is not an ID.
if (!destId) throw new ProtoError(4000, STR_INVALID_DEST);
if (destId === 1) destId = lobby.host;
const dest = lobby.peers.find((p: Peer) => p.id === destId);
// Dest is not in this room.
if (!dest) throw new ProtoError(4000, STR_INVALID_DEST);
function isCmd(what: string) {
return cmd.startsWith(`${what}: `);
}
if (isCmd("O") || isCmd("A") || isCmd("C")) {
dest.ws.send(cmd[0] + ": " + lobby.getPeerId(peer) + data);
return;
}
throw new ProtoError(4000, STR_INVALID_CMD);
}
*/