305 lines
7.9 KiB
TypeScript
305 lines
7.9 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>();
|
|
// TODO: client index by id
|
|
|
|
interface DataMessage {
|
|
type: string;
|
|
}
|
|
|
|
type ServerDataObject = Record<string, string | number | symbol | null>;
|
|
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<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.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,
|
|
}),
|
|
);
|
|
client.send(
|
|
buildMessage(
|
|
"peer_joined",
|
|
Array.from(this.clients.values()).map(
|
|
({ id, name, peerId }) => ({ id, name, peerId }),
|
|
),
|
|
),
|
|
);
|
|
this.clients.set(client.id, client);
|
|
}
|
|
|
|
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.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" },
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
})();
|
|
}
|