= writable(null);
+export const list: Writable<{ id: string, name: string, count: number }[]|null> = writable(null);
\ No newline at end of file
diff --git a/client/src/routes/index.svelte b/client/src/routes/index.svelte
new file mode 100644
index 0000000..5982b0a
--- /dev/null
+++ b/client/src/routes/index.svelte
@@ -0,0 +1,2 @@
+Welcome to SvelteKit
+Visit kit.svelte.dev to read the documentation
diff --git a/client/static/favicon.png b/client/static/favicon.png
new file mode 100644
index 0000000..825b9e6
Binary files /dev/null and b/client/static/favicon.png differ
diff --git a/client/svelte.config.js b/client/svelte.config.js
new file mode 100644
index 0000000..892f0c4
--- /dev/null
+++ b/client/svelte.config.js
@@ -0,0 +1,15 @@
+import adapter from '@sveltejs/adapter-auto';
+import preprocess from 'svelte-preprocess';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://github.com/sveltejs/svelte-preprocess
+ // for more information about preprocessors
+ preprocess: preprocess(),
+
+ kit: {
+ adapter: adapter()
+ }
+};
+
+export default config;
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..0f47472
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true
+ }
+}
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 0000000..8747050
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,8 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+
+/** @type {import('vite').UserConfig} */
+const config = {
+ plugins: [sveltekit()]
+};
+
+export default config;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..f816c5b
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,17 @@
+lockfileVersion: 5.4
+
+importers:
+
+ server:
+ specifiers:
+ uWebSockets.js: github:uNetworking/uWebSockets.js#v20.10.0
+ dependencies:
+ uWebSockets.js: github.com/uNetworking/uWebSockets.js/806df48c9da86af7b3341f3e443388c7cd15c3de
+
+packages:
+
+ github.com/uNetworking/uWebSockets.js/806df48c9da86af7b3341f3e443388c7cd15c3de:
+ resolution: {tarball: https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/806df48c9da86af7b3341f3e443388c7cd15c3de}
+ name: uWebSockets.js
+ version: 20.10.0
+ dev: false
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..2d4d8a1
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+packages:
+ - client
+ - server
\ No newline at end of file
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..eee8bc9
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.10.0"
+ }
+}
diff --git a/server/src/index.js b/server/src/index.js
new file mode 100644
index 0000000..d8292a0
--- /dev/null
+++ b/server/src/index.js
@@ -0,0 +1,168 @@
+const decoder = new TextDecoder();
+const encoder = new TextEncoder();
+const PORT = 8080;
+
+let i = 0;
+function uuid() {
+ return (++i).toString();
+}
+
+/**
+ * @typedef Room
+ * @property {string} id
+ * @property {string} name
+ * @property {Client} host
+ * @property {Client[]} clients
+ */
+/**
+ * @typedef Client
+ * @property {string} name
+ * @property {WebSocket} connection
+ * @property {Room?} room
+ */
+
+/**
+ * @type {Map}
+ */
+const rooms = new Map();
+/**
+ * @type {Map}
+ */
+const clients = new Map();
+
+require("uWebSockets.js")
+ .App({})
+ .ws("/", {
+ idleTimeout: 30,
+ maxPayloadLength: 16 * 1024 * 1024,
+ upgrade: (res, req, context) => {
+ console.log(
+ `An Http connection wants to become WebSocket, URL: ${req.getUrl()}!`
+ );
+
+ const url = req.getUrl();
+ const parsed = new URL(url);
+ let name = parsed.searchParams.get("name");
+ if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.end("invalid_name");
+ name = name.trim();
+ if (clients.forEach(client => client.name === name)) return res.end("name_used");
+ /* This immediately calls open handler, you must not use res after this call */
+ res.upgrade(
+ {
+ url,
+ 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.url,
+ ws.name,
+ decoder.decode(ws.getRemoteAddressAsText())
+ );
+ clients.set(ws, {
+ connection: ws,
+ name: ws.name,
+ room: null
+ });
+ },
+ message: (ws, message, isBinary) => {
+ if (isBinary) return ws.end();
+ try {
+ const data = JSON.parse(decoder.decode(message));
+ if(data.t === "ping") return ws.ping();
+ switch(data.t) {
+ case "ping": {
+ return ws.ping();
+ }
+ case "create": {
+ const client = clients.get(ws);
+ if(client.room) return ws.send(JSON.stringify({t: "error", m: "already_in_room"}));
+ const name = data.name.trim();
+ if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.send(JSON.stringify({t: "error", m: "invalid_room_name"}));
+ const room = {
+ name: name,
+ host: ws,
+ clients: [client],
+ id: uuid()
+ };
+ rooms.set(room.id, room);
+ client.room = room;
+ return ws.send(JSON.stringify({ t: "create", id: room.id, name: name }));
+ }
+ case "leave": {
+ const client = clients.get(ws);
+ const room = client.room;
+ if (!room) return ws.send(JSON.stringify({ t: "error", m: "room_not_found" }));
+ if (!room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", m: "not_in_room" }));
+ room.clients.splice(room.clients.indexOf(client), 1);
+ if (room.clients.length === 0) {
+ rooms.delete(room.id);
+ } 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", id: room.id, name: client.name })));
+ ws.send(JSON.stringify({ t: "left", id: room.id, name: client.name }));
+ break;
+ }
+ case "join": {
+ const client = clients.get(ws);
+ if (!client) return ws.end();
+ 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: "already_in_room" }));
+ room.clients.push(ws);
+ ws.room = room;
+ room.clients.slice(0, -2).forEach(client => client.connection.send(JSON.stringify({ t: "join", id: room.id, client: client.name })));
+ ws.send(JSON.stringify({ t: "joined", id: room.id, client: client.name, clients: room.clients.map(t => t.name) }));
+ break;
+ }
+ case "cand":
+ case "desc": {
+ const client = clients.get(ws);
+ if (!client) return ws.end();
+ 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" }));
+ const targetClient = room.clients.find(t => t.name === msg.target);
+ if(!targetClient) return ws.send(JSON.stringify({ t: "error", e: "target_not_found" }));
+ if(!room.clients.includes(targetClient)) return ws.send(JSON.stringify({ t: "error", e: "target_not_in_room" }));
+ targetClient.connection.send(JSON.stringify({ t: msg.t, id: room.id, source: client.name, d: msg.d }));
+ }
+ case "list": {
+ ws.send(JSON.stringify({ t: "list", rooms: [...rooms.values()].map(t => ({ id: t.id, name: t.name, count: t.clients.length }))}));
+ break;
+ }
+ }
+ } catch (e) {
+ return ws.end();
+ }
+ },
+ close: (ws, code, message) => {
+ console.log("DIS", decoder.decode(ws.getRemoteAddressAsText()));
+ if (clients.get(ws)) {
+ let room = rooms.get(clients.get(ws));
+ if (room) {
+ room.clients.splice(room.clients.indexOf(ws), 1);
+ if (room.clients.length === 0) {
+ rooms.delete(room.id);
+ } else if(room.host == ws) {
+ room.host = room.clients[0];
+ room.clients.forEach(client => client.connection.send(JSON.stringify({ t: "host", host: client.name })));
+ }
+ }
+ }
+ clients.delete(ws);
+ },
+ })
+ .listen(PORT, () => {
+ console.log(`Listening on port ${PORT}`);
+ });