From 1c6f2368a1140e5a6f879ed5de6ce9fb40dfc9c9 Mon Sep 17 00:00:00 2001 From: Schluffe Date: Fri, 18 Sep 2026 11:41:18 +0200 Subject: [PATCH] Refactor server stuff --- deno.lock | 4 ++ engine/lib/assertNever.ts | 3 ++ server/client.test.ts | 103 ++------------------------------------ server/handler.ts | 21 ++++++++ server/main.ts | 5 +- server/ref/deno.test.ts | 64 +++++++++++++++++++++++ server/{ => ref}/deno.ts | 28 +++-------- server/server.ts | 15 ++++++ server/testSetup.ts | 46 +++++++++++++++++ 9 files changed, 169 insertions(+), 120 deletions(-) create mode 100644 engine/lib/assertNever.ts create mode 100644 server/handler.ts create mode 100644 server/ref/deno.test.ts rename server/{ => ref}/deno.ts (75%) create mode 100644 server/server.ts create mode 100644 server/testSetup.ts diff --git a/deno.lock b/deno.lock index 7b2008d..2cadff8 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,7 @@ "version": "5", "specifiers": { "jsr:@std/assert@*": "1.0.19", + "jsr:@std/http@*": "1.1.3", "jsr:@std/internal@^1.0.12": "1.0.14" }, "jsr": { @@ -11,6 +12,9 @@ "jsr:@std/internal" ] }, + "@std/http@1.1.3": { + "integrity": "73a98e05ff58aa18bea562d286172e6c815b1eddd4fcab194beda19e3683b88c" + }, "@std/internal@1.0.14": { "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" } diff --git a/engine/lib/assertNever.ts b/engine/lib/assertNever.ts new file mode 100644 index 0000000..dd4d548 --- /dev/null +++ b/engine/lib/assertNever.ts @@ -0,0 +1,3 @@ +export function assertNever(): never { + throw new Error("never"); +} diff --git a/server/client.test.ts b/server/client.test.ts index ae7e99b..88d5dce 100644 --- a/server/client.test.ts +++ b/server/client.test.ts @@ -1,107 +1,14 @@ /// import { assertEquals } from "jsr:@std/assert"; -import { startServer } from "./deno.ts"; - -const hostname = "0.0.0.0"; -const port = 8042; -const href = `ws://${hostname}:${port}`; - -function connect(url: string): Promise<[WebSocket, string[]]> { - return new Promise((resolve, reject) => { - const messages: string[] = []; - const socket = new WebSocket(url); - - socket.onopen = () => resolve([socket, messages]); - socket.onerror = (error) => reject(error); - socket.onmessage = ({ data }) => { - messages.push(data); - }; - }); -} - -function close(socket: WebSocket): Promise { - return new Promise((resolve) => { - if (socket.readyState === WebSocket.CLOSED) { - return resolve(); - } - socket.onclose = () => resolve(); - socket.close(); - }); -} - -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(); - } - }; -} - -function flush(): Promise { - return new Promise((resolve) => setTimeout(resolve, 0)); -} +import handler from "./handler.ts"; +import { close, connect, flush, href, setup } from "./testSetup.ts"; Deno.test( - "Welcome all clients on connect.", + "test", setup(async () => { const [s1, m1] = await connect(href); - const [s2, m2] = await connect(href); - await flush(); - - assertEquals(m1, ["Welcome 1, type /join ."]); - assertEquals(m2, ["Welcome 2, type /join ."]); - - await close(s2); + assertEquals(m1, []); await close(s1); - }), -); - -Deno.test( - "Messages all clients in a room on joining.", - setup(async () => { - const [s1, m1] = await connect(href); - const [s2, m2] = await connect(href); - - await flush(); - m1.length = 0; - m2.length = 0; - - s1.send("/join meh"); - s2.send("/join meh"); - await flush(); - - assertEquals(m1, ["1 has joined!", "2 has joined!"]); - assertEquals(m2, ["2 has joined!"]); - - await close(s2); - await close(s1); - }), -); - -Deno.test( - "Messages remaining members if one left the room.", - setup(async () => { - const [s1, m1] = await connect(href); - const [s2, m2] = await connect(href); - - s1.send("/join meh"); - s2.send("/join meh"); - await flush(); - - m1.length = 0; - m2.length = 0; - await close(s2); - await flush(); - - assertEquals(m1, ["2 has left!"]); - assertEquals(m2, []); - - await close(s1); - }), + }, handler), ); diff --git a/server/handler.ts b/server/handler.ts new file mode 100644 index 0000000..793b036 --- /dev/null +++ b/server/handler.ts @@ -0,0 +1,21 @@ +import { getCookies } from "jsr:@std/http/cookie"; + +function getSessionId(request: Request): null | string { + getCookies(request.headers); + return null; +} + +export default function (request: Request): Response { + if (request.headers.get("upgrade") !== "websocket") { + return new Response("Upgrade required.", { status: 426 }); + } + + const sessonId = getSessionId(request); + if (!sessonId) { + return new Response("Session requierd.", { status: 401 }); + } + + const { socket, response } = Deno.upgradeWebSocket(request); + + return response; +} diff --git a/server/main.ts b/server/main.ts index 8c2e840..8796ee3 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,3 +1,4 @@ -import { startServer } from "./deno.ts"; +import handler from "./handler.ts"; +import { startServer } from "./server.ts"; -startServer(); +startServer(handler); diff --git a/server/ref/deno.test.ts b/server/ref/deno.test.ts new file mode 100644 index 0000000..2292d19 --- /dev/null +++ b/server/ref/deno.test.ts @@ -0,0 +1,64 @@ +/// +import { assertEquals } from "jsr:@std/assert"; +import { close, connect, flush, href, setup } from "../testSetup.ts"; +import { getHandler } from "./deno.ts"; + +Deno.test( + "Welcome all clients on connect.", + setup(async () => { + const [s1, m1] = await connect(href); + const [s2, m2] = await connect(href); + + await flush(); + + assertEquals(m1, ["Welcome 1, type /join ."]); + assertEquals(m2, ["Welcome 2, type /join ."]); + + await close(s2); + await close(s1); + }, getHandler()), +); + +Deno.test( + "Messages all clients in a room on joining.", + setup(async () => { + const [s1, m1] = await connect(href); + const [s2, m2] = await connect(href); + + await flush(); + m1.length = 0; + m2.length = 0; + + s1.send("/join meh"); + s2.send("/join meh"); + await flush(); + + assertEquals(m1, ["1 has joined!", "2 has joined!"]); + assertEquals(m2, ["2 has joined!"]); + + await close(s2); + await close(s1); + }, getHandler()), +); + +Deno.test( + "Messages remaining members if one left the room.", + setup(async () => { + const [s1, m1] = await connect(href); + const [s2, m2] = await connect(href); + + s1.send("/join meh"); + s2.send("/join meh"); + await flush(); + + m1.length = 0; + m2.length = 0; + await close(s2); + await flush(); + + assertEquals(m1, ["2 has left!"]); + assertEquals(m2, []); + + await close(s1); + }, getHandler()), +); diff --git a/server/deno.ts b/server/ref/deno.ts similarity index 75% rename from server/deno.ts rename to server/ref/deno.ts index 38df654..5695eb4 100644 --- a/server/deno.ts +++ b/server/ref/deno.ts @@ -1,9 +1,6 @@ /// -export function startServer( - hostname: string = "localhost", - port: number = 8042, -): () => Promise { +export function getHandler(): Deno.ServeHandler { let nextId = 1; const clientsMap = new Map(); @@ -70,23 +67,14 @@ export function startServer( 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 }); - } + return (request) => { + if (request.headers.get("upgrade") !== "websocket") { + return new Response("Upgrade Required", { status: 426 }); + } - const { socket, response } = Deno.upgradeWebSocket(request); - handleConnection(socket); + const { socket, response } = Deno.upgradeWebSocket(request); + handleConnection(socket); - return response; - }, - ); - - return () => { - ac.abort(); - return server.finished; + return response; }; } diff --git a/server/server.ts b/server/server.ts new file mode 100644 index 0000000..733d9f2 --- /dev/null +++ b/server/server.ts @@ -0,0 +1,15 @@ +/// + +export function startServer( + handler: Deno.ServeHandler, + hostname: string = "localhost", + port: number = 8042, +): () => Promise { + const ac = new AbortController(); + const server = Deno.serve({ hostname, port, signal: ac.signal }, handler); + + return () => { + ac.abort(); + return server.finished; + }; +} diff --git a/server/testSetup.ts b/server/testSetup.ts new file mode 100644 index 0000000..8c11808 --- /dev/null +++ b/server/testSetup.ts @@ -0,0 +1,46 @@ +import { startServer } from "./server.ts"; + +export const hostname = "0.0.0.0"; +export const port = 8042; +export const href = `ws://${hostname}:${port}`; + +export function setup( + fn: (ctx: Deno.TestContext) => Promise, + handler: Deno.ServeHandler, +): (ctx: Deno.TestContext) => Promise { + return async (ctx) => { + const shutdown = startServer(handler, hostname, port); + try { + await fn(ctx); + } finally { + await shutdown(); + } + }; +} + +export function connect(url: string): Promise<[WebSocket, string[]]> { + return new Promise((resolve, reject) => { + const messages: string[] = []; + const socket = new WebSocket(url); + + socket.onopen = () => resolve([socket, messages]); + socket.onerror = (error) => reject(error); + socket.onmessage = ({ data }) => { + messages.push(data); + }; + }); +} + +export function close(socket: WebSocket): Promise { + return new Promise((resolve) => { + if (socket.readyState === WebSocket.CLOSED) { + return resolve(); + } + socket.onclose = () => resolve(); + socket.close(); + }); +} + +export function flush(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)); +}