From 1ceab2fd0565d0304d77628ece5b40b49e1bff0e Mon Sep 17 00:00:00 2001 From: Schluffe Date: Wed, 16 Sep 2026 00:17:48 +0200 Subject: [PATCH] Socket fun --- server/client.test.ts | 97 +++++++++++++++++++++++-------------------- server/deno.ts | 84 +++++++++++++++++++++---------------- server/main.ts | 3 ++ 3 files changed, 104 insertions(+), 80 deletions(-) create mode 100644 server/main.ts diff --git a/server/client.test.ts b/server/client.test.ts index 2dcefc3..599791a 100644 --- a/server/client.test.ts +++ b/server/client.test.ts @@ -1,28 +1,20 @@ /// import { assertEquals } from "jsr:@std/assert"; +import { startServer } from "./deno.ts"; -const url = "ws://0.0.0.0:6969"; +const hostname = "0.0.0.0"; +const port = 8042; +const href = `ws://${hostname}:${port}`; -function connect(url: string): Promise { - return new Promise((resolve, reject) => { +async function connect(url: string): Promise<[WebSocket, string[]]> { + return new Promise((resolve) => { + const messages: string[] = []; const socket = new WebSocket(url); - socket.onopen = () => resolve(socket); - socket.onerror = (e) => reject(e); - }); -} -function nextMessage(socket: WebSocket): Promise { - return new Promise((resolve, reject) => { - const onMessage = (e: MessageEvent) => { - socket.removeEventListener("message", onMessage); - try { - resolve(JSON.parse(e.data)); - } catch { - resolve(e.data); - } + socket.onmessage = ({ data }) => { + messages.push(data); }; - socket.addEventListener("message", onMessage); - socket.addEventListener("error", reject, { once: true }); + socket.onopen = () => resolve([socket, messages]); }); } @@ -36,36 +28,51 @@ function close(socket: WebSocket): Promise { }); } -Deno.test("broadcasts a message on connect to all clients", async () => { - const s1 = await connect(url); - const s2 = await connect(url); - assertEquals(await nextMessage(s1), "2 has joined!"); +function setup( + fn: (ctx: Deno.TestContext) => Promise, +): (ctx: Deno.TestContext) => Promise { + return async (ctx) => { + const shutdown = startServer(hostname, port); + try { + await fn(ctx); + } finally { + await shutdown(); + } + }; +} - const s3 = connect(url); - Promise.all([nextMessage(s1), nextMessage(s2)]).then((messages) => { - messages.forEach((message) => { - assertEquals(message, "3 has joined!"); - }); - }); +Deno.test( + "broadcasts a message on connect to all clients", + setup(async () => { + const [s1, m1] = await connect(href); + const [s2, m2] = await connect(href); - await close(s1); - await close(s2); - await close(await s3); -}); + assertEquals(m1, ["1 has joined!", "2 has joined!"]); + assertEquals(m2, ["2 has joined!"]); -Deno.test("removes a client from broadcast after it disconnects", async () => { - const s4 = await connect(url); - const s5 = await connect(url); - const s6 = await connect(url); + await close(s2); + await close(s1); + }), +); - await close(s5); +Deno.test( + "removes a client from broadcast after it disconnects", + setup(async () => { + const [s1, m1] = await connect(href); + const [s2, m2] = await connect(href); + const [s3, m3] = await connect(href); - Promise.all([nextMessage(s4), nextMessage(s6)]).then((messages) => { - messages.forEach((message) => { - assertEquals(message, "5 has left!"); - }); - }); + m1.length = 0; + m2.length = 0; + m3.length = 0; - await close(s4); - await close(s6); -}); + await close(s3); + + assertEquals(m1, ["3 has left!"]); + assertEquals(m2, ["3 has left!"]); + assertEquals(m3, []); + + await close(s2); + await close(s1); + }), +); diff --git a/server/deno.ts b/server/deno.ts index acb792c..7cf3e3d 100644 --- a/server/deno.ts +++ b/server/deno.ts @@ -1,40 +1,54 @@ /// -const clientsMap = new Map(); -let nextId = 1; +export function startServer( + hostname: string = "localhost", + port: number = 8042, +): () => Promise { + let nextId = 1; + const clientsMap = new Map(); -const handleConnection = (socket: WebSocket): void => { - const id = nextId++; - - socket.onopen = () => { - clientsMap.set(id, socket); - broadcast(`${id} has joined!`); - }; - - socket.onclose = () => { - clientsMap.delete(id); - broadcast(`${id} has left!`); - }; - - socket.onmessage = console.log; - socket.onerror = console.error; -}; - -function broadcast(message: string): void { - for (const socket of clientsMap.values()) { - if (socket.readyState === WebSocket.OPEN) { - socket.send(message); + const broadcast = (message: string): void => { + for (const socket of clientsMap.values()) { + if (socket.readyState === WebSocket.OPEN) { + socket.send(message); + } } - } + }; + + const handleConnection = (socket: WebSocket): void => { + const id = nextId++; + + socket.onopen = () => { + clientsMap.set(id, socket); + broadcast(`${id} has joined!`); + }; + + socket.onclose = () => { + clientsMap.delete(id); + broadcast(`${id} has left!`); + }; + + socket.onmessage = console.log; + socket.onerror = console.error; + }; + + const ac = new AbortController(); + const server = Deno.serve( + { hostname, port, signal: ac.signal }, + (request) => { + if (request.headers.get("upgrade") !== "websocket") { + return new Response("Upgrade Required", { status: 426 }); + } + + const { socket, response } = Deno.upgradeWebSocket(request); + handleConnection(socket); + + return response; + }, + ); + + return () => { + ac.abort(); + return server.finished; + }; } - -Deno.serve({ port: 6969 }, (request) => { - if (request.headers.get("upgrade") !== "websocket") { - return new Response("Upgrade Required", { status: 426 }); - } - - const { socket, response } = Deno.upgradeWebSocket(request); - handleConnection(socket); - - return response; -}); diff --git a/server/main.ts b/server/main.ts new file mode 100644 index 0000000..8c2e840 --- /dev/null +++ b/server/main.ts @@ -0,0 +1,3 @@ +import { startServer } from "./deno.ts"; + +startServer();