slightlyComplicatedTicTacToe/server/src/index.js
2023-01-20 19:38:40 +01:00

330 lines
13 KiB
JavaScript

const decoder = new TextDecoder();
const PORT = 8080;
/**
* @typedef Room
* @property {string} name
* @property {Client} host
* @property {Client[]} clients
*/
/**
* @typedef Client
* @property {string} name
* @property {WebSocket} connection
* @property {Room?} room
*/
/**
* @type {Map<string, Room>}
*/
const rooms = new Map();
/**
* @type {Map<WebSocket, Client>}
*/
const clients = new Map();
function deleteRoom(room) {
rooms.delete(room);
for(let [, client] of clients) {
if(!client.room) {
client.connection.send(
JSON.stringify({ t: "room_deleted", name: room })
)
}
}
}
require("uWebSockets.js")
.App({})
.ws("/", {
idleTimeout: 32,
maxPayloadLength: 16 * 1024 * 1024,
upgrade: (res, req, context) => {
console.log(
`An Http connection wants to become WebSocket, URL: ${req.getUrl()}?${req.getQuery()}!`
);
let name = req.getQuery("name").trim().toLocaleLowerCase();
if (
!name ||
typeof name !== "string" ||
name.length < 2 ||
name.length > 64 ||
!name.trim()
) {
console.log("Invalid name");
return res.end("invalid_name");
}
name = name.trim();
if ([...clients.values()].find((client) => client.name === name)) {
console.log("Duplicate name");
return res.end("name_used");
}
/* This immediately calls open handler, you must not use res after this call */
res.upgrade(
{
name,
},
/* Spell these correctly */
req.getHeader("sec-websocket-key"),
req.getHeader("sec-websocket-protocol"),
req.getHeader("sec-websocket-extensions"),
context
);
},
open: (ws) => {
console.log(
"CON",
ws.name,
decoder.decode(ws.getRemoteAddressAsText())
);
clients.set(ws, {
connection: ws,
name: ws.name,
ipa: decoder.decode(ws.getRemoteAddressAsText()),
room: null,
});
},
message: (ws, message, isBinary) => {
if (isBinary) return ws.end(0, "invalid_message");
try {
const data = JSON.parse(decoder.decode(message));
const client = clients.get(ws);
console.log(client.name, data);
if (data.t === "ping") return ws.ping();
switch (data.t) {
case "ping": {
return ws.ping();
}
case "create": {
if (client.room)
return ws.send(
JSON.stringify({
t: "error",
e: "already_in_room",
})
);
const name = data.name.trim().toLocaleLowerCase();
if (
!name ||
typeof name !== "string" ||
name.length < 2 ||
name.length > 64 ||
!name.trim()
)
return res.send(
JSON.stringify({
t: "error",
e: "invalid_room_name",
})
);
if (
[...rooms.values()].find(
(room) => room.name === name
)
)
return ws.send(
JSON.stringify({
t: "error",
e: "room_name_used",
})
);
const room = {
name: name,
host: ws,
clients: [client],
};
rooms.set(room.name, room);
client.room = room;
for(let [, cclient] of clients) {
if(!cclient.room) {
cclient.connection.send(
JSON.stringify({ t: "room_created", name })
);
}
}
return ws.send(
JSON.stringify({ t: "create", name })
);
}
case "leave": {
const room = client.room;
if (!room)
return ws.send(
JSON.stringify({
t: "error",
e: "room_not_found",
})
);
if (!room.clients.includes(client))
return ws.send(
JSON.stringify({ t: "error", e: "not_in_room" })
);
room.clients.splice(room.clients.indexOf(client), 1);
if (room.clients.length === 0) {
deleteRoom(room.name);
} else if (room.host == ws) {
room.host = room.clients[0];
room.clients.forEach((client) =>
client.connection.send(
JSON.stringify({
t: "host",
host: client.name,
})
)
);
}
client.room = null;
room.clients.forEach((client) =>
client.connection.send(
JSON.stringify({
t: "leave",
client: client.name,
})
)
);
ws.send(
JSON.stringify({ t: "left", name: client.name })
);
break;
}
case "join": {
if (!client) return ws.end(0, "missing_client");
const room = rooms.get(data.name);
if (!room)
return ws.send(
JSON.stringify({
t: "error",
e: "room_not_found",
})
);
if (client.room)
return ws.send(
JSON.stringify({
t: "error",
e: "already_in_other_room",
})
);
if (room.clients.includes(client))
return ws.send(
JSON.stringify({
t: "error",
e: "already_in_room",
})
);
if (room.clients.length > 5)
return ws.send(
JSON.stringify({ t: "error", e: "room_full" })
);
room.clients.push(client);
client.room = room;
const srcclient = client;
room.clients
.filter((t) => t !== client)
.forEach((client) =>
client.connection.send(
JSON.stringify({
t: "join",
client: srcclient.name,
})
)
);
ws.send(
JSON.stringify({
t: "joined",
name: room.name,
client: client.name,
host: room.host.name,
clients: room.clients.map((t) => t.name),
})
);
break;
}
case "broadcast": {
if (!client) return ws.end(0, "missing_client");
const room = client.room;
if (!room)
return ws.send(
JSON.stringify({
t: "error",
e: "room_not_found",
})
);
if (!room.clients.includes(client))
return ws.send(
JSON.stringify({ t: "error", e: "not_in_room" })
);
let clientName = client.name
room.clients.forEach((client) => {
client.connection.send(
JSON.stringify({
t: "broadcast",
client: clientName,
d: data.d,
})
);
});
}
case "list": {
ws.send(
JSON.stringify({
t: "list",
rooms: [...rooms.values()]
.filter((t) => t.clients.length < 2)
.map((t) => ({
name: t.name,
host: t.host.name,
count: t.clients.length,
})),
})
);
break;
}
}
} catch (e) {
console.warn(e);
return ws.end(0, "internal");
}
},
close: (ws, code, message) => {
console.log("DIS1", ws, code, message);
try {
if (clients.get(ws)) {
const client = clients.get(ws);
console.log("DIS", client.name, client.ipa);
let room = client.room;
if (room) {
room.clients.splice(room.clients.indexOf(client), 1);
if (room.clients.length === 0) {
deleteRoom(room.name);
} else if (room.host == ws) {
room.host = room.clients[0];
room.clients.forEach((client) =>
client.connection.send(
JSON.stringify({
t: "host",
host: client.name,
})
)
);
}
const srcclient = client;
room.clients.forEach((client) =>
client.connection.send(JSON.stringify({ t: "leave", client: srcclient.name }))
);
}
}
} catch (e) {
console.warn("Error during closing", e);
}
clients.delete(ws);
},
})
.get("/*", (res, req) => {
res.writeStatus("200 OK").writeHeader("Content-Type", "text/plain").end("OK");
})
.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});