From c0e7c0810618853e8edfbeae99f9caa15e7e824b Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Wed, 25 Mar 2026 21:08:30 +0000 Subject: [PATCH] Integrate buzzer functionality into Bun server and update workflows Reverts Express server setup for the buzzer, migrates all functionality to a Bun server, and updates the workflow to execute the Bun server directly. Replit-Commit-Author: Agent Replit-Commit-Session-Id: f3ac8eb3-f610-4678-ab6e-ebf900098be4 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: c1942b0a-0cf4-4ca9-9b5f-f80287c102f2 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/d4b7863b-f7b2-425c-a9b5-ad7bd1885e9d/f3ac8eb3-f610-4678-ab6e-ebf900098be4/N1qS6qo Replit-Helium-Checkpoint-Created: true --- .replit | 5 +- server/buzzer-rooms.ts | 99 ------------------- server/buzzer-ws.ts | 214 ----------------------------------------- server/routes.ts | 39 +------- src/server.ts | 25 +++-- 5 files changed, 22 insertions(+), 360 deletions(-) delete mode 100644 server/buzzer-rooms.ts delete mode 100644 server/buzzer-ws.ts diff --git a/.replit b/.replit index 5cca7a0..19ed886 100644 --- a/.replit +++ b/.replit @@ -35,5 +35,8 @@ author = "agent" [[workflows.workflow.tasks]] task = "shell.exec" -args = "npm run dev" +args = "bun run src/server.ts" waitForPort = 5000 + +[workflows.workflow.metadata] +outputType = "webview" diff --git a/server/buzzer-rooms.ts b/server/buzzer-rooms.ts deleted file mode 100644 index 2ff9f33..0000000 --- a/server/buzzer-rooms.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { WebSocket } from "ws"; - -export interface RoomSettings { - mode: "individual" | "teams"; - numTeams: number; - teamNames: string[]; - playerPickTeam: boolean; - showBuzzOrder: boolean; - buzzerLockout: boolean; - timerSeconds: number; -} - -export interface Player { - id: string; - name: string; - teamIndex: number | null; - ws: WebSocket | null; - isConnected: boolean; -} - -export interface BuzzerState { - roundOpen: boolean; - buzzOrder: string[]; - buzzTimes: Map; -} - -export interface Room { - id: string; - moderatorSecret: string; - modWs: WebSocket | null; - settings: RoomSettings; - players: Map; - buzzerState: BuzzerState; - locked: boolean; - teamLocked: boolean; -} - -export const rooms = new Map(); -export const wsToPlayer = new Map(); - -const GREEK = ["Alpha","Beta","Gamma","Delta","Epsilon","Zeta","Eta","Theta","Iota","Kappa","Lambda","Mu","Nu","Xi","Omicron","Pi","Rho","Sigma","Tau","Upsilon","Phi","Chi","Psi","Omega"]; -export function greekName(i: number): string { - const cycle = Math.floor(i / GREEK.length); - const name = GREEK[i % GREEK.length]; - return cycle === 0 ? name : `${name} ${toRoman(cycle + 1)}`; -} -function toRoman(n: number): string { - const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1]; - const syms = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]; - let out = ""; - for (let i = 0; i < vals.length; i++) while (n >= vals[i]) { out += syms[i]; n -= vals[i]; } - return out; -} - -export function genId(len = 8): string { - const c = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - let s = ""; - for (let i = 0; i < len; i++) s += c[Math.floor(Math.random() * c.length)]; - return s; -} - -export function sanitize(v: unknown, max = 32): string { - return String(v ?? "").trim().replace(/[<>"'`]/g, "").slice(0, max); -} - -export function freshBuzzer(): BuzzerState { - return { roundOpen: false, buzzOrder: [], buzzTimes: new Map() }; -} - -export function publicRoom(room: Room) { - return { - id: room.id, - settings: room.settings, - locked: room.locked, - teamLocked: room.teamLocked, - modOnline: room.modWs !== null, - buzzerState: { - roundOpen: room.buzzerState.roundOpen, - buzzOrder: room.buzzerState.buzzOrder, - }, - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - name: p.name, - teamIndex: p.teamIndex, - isConnected: p.isConnected, - })), - }; -} - -export function broadcast(room: Room, msg: object, exclude?: WebSocket) { - const d = JSON.stringify(msg); - for (const p of room.players.values()) - if (p.ws && p.isConnected && p.ws !== exclude) try { p.ws.send(d); } catch {} - if (room.modWs && room.modWs !== exclude) try { room.modWs.send(d); } catch {} -} - -export function toMod(room: Room, msg: object) { - if (room.modWs) try { room.modWs.send(JSON.stringify(msg)); } catch {} -} diff --git a/server/buzzer-ws.ts b/server/buzzer-ws.ts deleted file mode 100644 index 0586219..0000000 --- a/server/buzzer-ws.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { WebSocket } from "ws"; -import { - rooms, wsToPlayer, - Room, Player, - genId, greekName, sanitize, freshBuzzer, publicRoom, broadcast, toMod, -} from "./buzzer-rooms"; - -const tx = (ws: WebSocket, msg: object) => { try { ws.send(JSON.stringify(msg)); } catch {} }; -const er = (ws: WebSocket, m: string) => tx(ws, { type: "error", message: m }); -const modOf = (ws: WebSocket): Room | null => { for (const r of rooms.values()) if (r.modWs === ws) return r; return null; }; - -export function handleMessage(ws: WebSocket, raw: string) { - let msg: any; - try { msg = JSON.parse(raw); } catch { return; } - const type: string = msg?.type ?? ""; - - if (type === "create_room") { - const s = msg.settings ?? {}; - const numTeams = Math.max(2, Math.min(64, (s.numTeams ?? 2) | 0)); - const teamNames: string[] = []; - for (let i = 0; i < numTeams; i++) - teamNames.push(s.teamNames?.[i] ? sanitize(s.teamNames[i], 32) : greekName(i)); - - const room: Room = { - id: genId(6), - moderatorSecret: genId(24), - modWs: ws, - settings: { - mode: s.mode === "teams" ? "teams" : "individual", - numTeams, - teamNames, - playerPickTeam: s.playerPickTeam === true, - showBuzzOrder: s.showBuzzOrder !== false, - buzzerLockout: s.buzzerLockout !== false, - timerSeconds: Math.min(600, Math.max(0, (s.timerSeconds ?? 0) | 0)), - }, - players: new Map(), - buzzerState: freshBuzzer(), - locked: false, - teamLocked: false, - }; - rooms.set(room.id, room); - tx(ws, { type: "room_created", roomId: room.id, modSecret: room.moderatorSecret, room: publicRoom(room) }); - return; - } - - if (type === "mod_rejoin") { - const room = rooms.get(sanitize(msg.roomId, 10)); - if (!room || room.moderatorSecret !== sanitize(msg.modSecret, 32)) { - er(ws, "Invalid credentials"); return; - } - room.modWs = ws; - tx(ws, { type: "mod_joined", room: publicRoom(room) }); - broadcast(room, { type: "room_update", room: publicRoom(room) }, ws); - return; - } - - if (type === "join_room") { - if (msg.modSecret) { er(ws, "Unauthorized"); return; } - const room = rooms.get(sanitize(msg.roomId ?? "", 10)); - if (!room) { er(ws, "Room not found"); return; } - if (room.locked) { er(ws, "Room is locked"); return; } - - const name = sanitize(msg.playerName ?? "Player", 24) || "Player"; - const existingId = sanitize(msg.playerId ?? "", 12); - let player: Player | undefined; - - if (existingId && room.players.has(existingId)) { - player = room.players.get(existingId)!; - player.ws = ws; player.isConnected = true; player.name = name; - } else { - player = { id: genId(), name, teamIndex: null, ws, isConnected: true }; - room.players.set(player.id, player); - } - - wsToPlayer.set(ws, { roomId: room.id, playerId: player.id }); - tx(ws, { type: "joined", playerId: player.id, room: publicRoom(room) }); - broadcast(room, { type: "room_update", room: publicRoom(room) }, ws); - return; - } - - if (type === "pick_team") { - const ctx = wsToPlayer.get(ws); - if (!ctx) return; - const room = rooms.get(ctx.roomId); - if (!room) return; - if (room.teamLocked || !room.settings.playerPickTeam || room.settings.mode !== "teams") { - er(ws, "Team selection not allowed"); return; - } - const p = room.players.get(ctx.playerId); - if (!p) return; - const ti = typeof msg.teamIndex === "number" ? msg.teamIndex : null; - p.teamIndex = ti !== null ? Math.max(0, Math.min(room.settings.numTeams - 1, ti | 0)) : null; - broadcast(room, { type: "room_update", room: publicRoom(room) }); - return; - } - - if (type === "buzz") { - const ctx = wsToPlayer.get(ws); - if (!ctx) return; - const room = rooms.get(ctx.roomId); - if (!room) return; - const bz = room.buzzerState; - - if (!bz.roundOpen) { tx(ws, { type: "buzz_rejected", reason: "Round not open" }); return; } - if (bz.buzzOrder.includes(ctx.playerId)) { tx(ws, { type: "buzz_rejected", reason: "Already buzzed" }); return; } - if (room.settings.buzzerLockout && bz.buzzOrder.length > 0) { tx(ws, { type: "buzz_rejected", reason: "Locked out" }); return; } - - bz.buzzOrder.push(ctx.playerId); - bz.buzzTimes.set(ctx.playerId, Date.now()); - - const p = room.players.get(ctx.playerId)!; - const pubOrder = room.settings.showBuzzOrder ? bz.buzzOrder : [bz.buzzOrder[0]]; - - broadcast(room, { type: "buzz_event", playerId: ctx.playerId, playerName: p.name, teamIndex: p.teamIndex, buzzOrder: pubOrder, room: publicRoom(room) }); - toMod(room, { type: "buzz_event", playerId: ctx.playerId, playerName: p.name, teamIndex: p.teamIndex, buzzOrder: bz.buzzOrder, buzzTimes: Object.fromEntries(bz.buzzTimes), room: publicRoom(room) }); - return; - } - - const room = modOf(ws); - if (!room) { er(ws, "Not authorized"); return; } - - switch (type) { - case "open_round": - room.buzzerState = freshBuzzer(); - room.buzzerState.roundOpen = true; - broadcast(room, { type: "round_open", room: publicRoom(room) }); - break; - case "close_round": - room.buzzerState.roundOpen = false; - broadcast(room, { type: "round_closed", room: publicRoom(room) }); - break; - case "reset_buzzer": - room.buzzerState = freshBuzzer(); - broadcast(room, { type: "buzzer_reset", room: publicRoom(room) }); - break; - case "update_settings": { - const s = msg.settings ?? {}; - const st = room.settings; - if (s.mode === "individual" || s.mode === "teams") st.mode = s.mode; - if (typeof s.numTeams === "number") { - const newN = Math.max(2, Math.min(64, s.numTeams | 0)); - while (st.teamNames.length < newN) st.teamNames.push(greekName(st.teamNames.length)); - st.numTeams = newN; - } - if (Array.isArray(s.teamNames)) st.teamNames = s.teamNames.slice(0, st.numTeams).map((n: unknown) => sanitize(n, 32)); - if (typeof s.playerPickTeam === "boolean") st.playerPickTeam = s.playerPickTeam; - if (typeof s.showBuzzOrder === "boolean") st.showBuzzOrder = s.showBuzzOrder; - if (typeof s.buzzerLockout === "boolean") st.buzzerLockout = s.buzzerLockout; - if (typeof s.timerSeconds === "number") st.timerSeconds = Math.min(600, Math.max(0, s.timerSeconds | 0)); - broadcast(room, { type: "room_update", room: publicRoom(room) }); - tx(ws, { type: "settings_updated", room: publicRoom(room) }); - break; - } - case "assign_team": { - const p = room.players.get(sanitize(msg.playerId, 12)); - if (p) { - p.teamIndex = typeof msg.teamIndex === "number" ? Math.max(0, Math.min(room.settings.numTeams - 1, msg.teamIndex | 0)) : null; - broadcast(room, { type: "room_update", room: publicRoom(room) }); - } - break; - } - case "kick_player": { - const p = room.players.get(sanitize(msg.playerId, 12)); - if (p) { - if (p.ws) { - try { p.ws.send(JSON.stringify({ type: "kicked" })); } catch {} - wsToPlayer.delete(p.ws); - } - room.players.delete(p.id); - broadcast(room, { type: "room_update", room: publicRoom(room) }); - } - break; - } - case "lock_room": - room.locked = msg.locked === true; - broadcast(room, { type: "room_update", room: publicRoom(room) }); - break; - case "lock_teams": - room.teamLocked = msg.locked === true; - broadcast(room, { type: "room_update", room: publicRoom(room) }); - break; - case "reset_teams": - for (const p of room.players.values()) p.teamIndex = null; - broadcast(room, { type: "room_update", room: publicRoom(room) }); - break; - case "end_room": - broadcast(room, { type: "room_ended" }); - for (const p of room.players.values()) if (p.ws) wsToPlayer.delete(p.ws); - room.modWs = null; - rooms.delete(room.id); - break; - } -} - -export function handleClose(ws: WebSocket) { - for (const room of rooms.values()) { - if (room.modWs === ws) { - room.modWs = null; - broadcast(room, { type: "mod_disconnected" }); - return; - } - } - const ctx = wsToPlayer.get(ws); - if (ctx) { - wsToPlayer.delete(ws); - const room = rooms.get(ctx.roomId); - if (room) { - const p = room.players.get(ctx.playerId); - if (p) { p.isConnected = false; p.ws = null; } - broadcast(room, { type: "room_update", room: publicRoom(room) }); - } - } -} diff --git a/server/routes.ts b/server/routes.ts index edeb9ae..1742eae 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,46 +1,9 @@ import type { Express } from "express"; import { createServer, type Server } from "http"; -import { WebSocketServer } from "ws"; -import fs from "fs"; -import path from "path"; -import { handleMessage, handleClose } from "./buzzer-ws"; export async function registerRoutes( httpServer: Server, - app: Express + _app: Express ): Promise { - // Serve buzzer static files - const publicDir = path.resolve(process.cwd(), "src/public"); - - app.get("/", (_req, res) => { - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.send(fs.readFileSync(path.join(publicDir, "index.html"), "utf-8")); - }); - - app.get("/styles.css", (_req, res) => { - res.setHeader("Content-Type", "text/css"); - res.send(fs.readFileSync(path.join(publicDir, "styles.css"), "utf-8")); - }); - - app.get("/script.js", (_req, res) => { - res.setHeader("Content-Type", "text/javascript"); - res.send(fs.readFileSync(path.join(publicDir, "script.js"), "utf-8")); - }); - - // WebSocket server for buzzer - const wss = new WebSocketServer({ server: httpServer, path: "/ws" }); - - wss.on("connection", (ws) => { - ws.on("message", (data) => { - if (typeof data === "string") { - handleMessage(ws, data); - } else { - handleMessage(ws, data.toString()); - } - }); - ws.on("close", () => handleClose(ws)); - ws.on("error", () => { try { ws.close(); } catch {} }); - }); - return httpServer; } diff --git a/src/server.ts b/src/server.ts index a23c5de..46e07e3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,21 +1,30 @@ import { readFileSync } from "fs"; import { handleMessage, handleClose } from "./ws-handler"; -const HTML = readFileSync("./src/public/index.html", "utf-8"); -const CSS = readFileSync("./src/public/styles.css", "utf-8"); -const JS = readFileSync("./src/public/script.js", "utf-8"); +const PORT = parseInt(process.env.PORT || "5000", 10); const server = Bun.serve({ - port: 3009, + port: PORT, + hostname: "0.0.0.0", fetch(req, server) { const url = new URL(req.url); if (url.pathname === "/ws") { if (!server.upgrade(req)) return new Response("WS upgrade failed", { status: 400 }); return undefined as any; } - if (url.pathname === "/styles.css") return new Response(CSS, { headers: { "Content-Type": "text/css" } }); - if (url.pathname === "/script.js") return new Response(JS, { headers: { "Content-Type": "text/javascript" } }); - return new Response(HTML, { headers: { "Content-Type": "text/html; charset=utf-8" } }); + if (url.pathname === "/styles.css") { + return new Response(readFileSync("./src/public/styles.css", "utf-8"), { + headers: { "Content-Type": "text/css" }, + }); + } + if (url.pathname === "/script.js") { + return new Response(readFileSync("./src/public/script.js", "utf-8"), { + headers: { "Content-Type": "text/javascript" }, + }); + } + return new Response(readFileSync("./src/public/index.html", "utf-8"), { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); }, websocket: { perMessageDeflate: true, @@ -26,4 +35,4 @@ const server = Bun.serve({ }, }); -console.log(`\x1b[32m[BUZZER]\x1b[0m → \x1b[36mhttp://localhost:${server.port}\x1b[0m`); \ No newline at end of file +console.log(`\x1b[32m[BUZZER]\x1b[0m → \x1b[36mhttp://localhost:${server.port}\x1b[0m`);