Socket fun

This commit is contained in:
Schluffe
2026-09-16 00:17:48 +02:00
parent b2e29eaa95
commit 1ceab2fd05
3 changed files with 104 additions and 80 deletions
+52 -45
View File
@@ -1,28 +1,20 @@
/// <reference lib="deno.ns" /> /// <reference lib="deno.ns" />
import { assertEquals } from "jsr:@std/assert"; 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<WebSocket> { async function connect(url: string): Promise<[WebSocket, string[]]> {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
const messages: string[] = [];
const socket = new WebSocket(url); const socket = new WebSocket(url);
socket.onopen = () => resolve(socket);
socket.onerror = (e) => reject(e);
});
}
function nextMessage(socket: WebSocket): Promise<unknown> { socket.onmessage = ({ data }) => {
return new Promise((resolve, reject) => { messages.push(data);
const onMessage = (e: MessageEvent) => {
socket.removeEventListener("message", onMessage);
try {
resolve(JSON.parse(e.data));
} catch {
resolve(e.data);
}
}; };
socket.addEventListener("message", onMessage); socket.onopen = () => resolve([socket, messages]);
socket.addEventListener("error", reject, { once: true });
}); });
} }
@@ -36,36 +28,51 @@ function close(socket: WebSocket): Promise<void> {
}); });
} }
Deno.test("broadcasts a message on connect to all clients", async () => { function setup(
const s1 = await connect(url); fn: (ctx: Deno.TestContext) => Promise<void>,
const s2 = await connect(url); ): (ctx: Deno.TestContext) => Promise<void> {
assertEquals(await nextMessage(s1), "2 has joined!"); return async (ctx) => {
const shutdown = startServer(hostname, port);
try {
await fn(ctx);
} finally {
await shutdown();
}
};
}
const s3 = connect(url); Deno.test(
Promise.all([nextMessage(s1), nextMessage(s2)]).then((messages) => { "broadcasts a message on connect to all clients",
messages.forEach((message) => { setup(async () => {
assertEquals(message, "3 has joined!"); const [s1, m1] = await connect(href);
}); const [s2, m2] = await connect(href);
});
await close(s1); assertEquals(m1, ["1 has joined!", "2 has joined!"]);
await close(s2); assertEquals(m2, ["2 has joined!"]);
await close(await s3);
});
Deno.test("removes a client from broadcast after it disconnects", async () => { await close(s2);
const s4 = await connect(url); await close(s1);
const s5 = await connect(url); }),
const s6 = await connect(url); );
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) => { m1.length = 0;
messages.forEach((message) => { m2.length = 0;
assertEquals(message, "5 has left!"); m3.length = 0;
});
});
await close(s4); await close(s3);
await close(s6);
}); assertEquals(m1, ["3 has left!"]);
assertEquals(m2, ["3 has left!"]);
assertEquals(m3, []);
await close(s2);
await close(s1);
}),
);
+49 -35
View File
@@ -1,40 +1,54 @@
/// <reference lib="deno.ns" /> /// <reference lib="deno.ns" />
const clientsMap = new Map<number, WebSocket>(); export function startServer(
let nextId = 1; hostname: string = "localhost",
port: number = 8042,
): () => Promise<void> {
let nextId = 1;
const clientsMap = new Map<number, WebSocket>();
const handleConnection = (socket: WebSocket): void => { const broadcast = (message: string): void => {
const id = nextId++; for (const socket of clientsMap.values()) {
if (socket.readyState === WebSocket.OPEN) {
socket.onopen = () => { socket.send(message);
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 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;
});
+3
View File
@@ -0,0 +1,3 @@
import { startServer } from "./deno.ts";
startServer();