"use strict" // TODOs // - measure/report latency // - use server reported time to find winner import config from "./config.js" const buzzer_key = { code: 0x20, name: "space bar", } const q = (selector, root) => (root || document).querySelector(selector) const on = (event, cb) => document.addEventListener(event, cb) function node(type, { appendTo, cls, text, data, ...attrs } = {}) { let elem = document.createElement(type) if (cls) { elem.className = cls } if (text) { elem.textContent = text } for (const name in data ?? {}) { elem.dataset[name] = data[name] } for (const name in attrs) { elem.setAttribute(name, attrs[name]) } if (appendTo) { elem = appendTo.appendChild(elem) } return elem } /** * Some duration conversion constants. */ const ms_ns = 1_000_000 // nanoseconds in a millisecond const s_ms = 1_000 // milliseconds in a second const s_ns = 1_000_000_000 // nanoseconds in a second let socket, servertime, toffset_ms, clients = [], me function hide(e) { q(`#${e}`).style.display = "none" } function show(e) { q(`#${e}`).style.display = "block" } function session_id() { const match = /^#?(.+)/.exec(document.location.hash) return match ? match[1] : null } function new_session_id() { if (!window.crypto) { return Math.random().toString(36).substr(2) } const data = new Uint8Array(10) crypto.getRandomValues(data) return Array.from(data, (v) => v.toString(36)).join("") } function setup_url() { const sid = session_id() || new_session_id() document.location.hash = sid } function send(type, value) { // console.debug('sending', value) socket.send(JSON.stringify({ type, value })) } function clear(container) { while (container.children.length > 0) { const child = container.children[0] child.remove() } } let ul function redraw_clients(me, clients) { if (!me) { return } clear(ul) for (const c of clients) { node("li", { text: c.name, data: { cid: c.id }, appendTo: ul, cls: c.id === me ? "me" : "", }) } } const highlights = {} function highlight(client_id, until_ns) { if (highlights[client_id]) { return } const timeout_ms = (until_ns - servertime_now_ns()) / ms_ns if (timeout_ms <= 10) { console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms) return } for (const li of ul.children) { if (li.dataset.cid === client_id) { li.classList.add("buzzing") if (Object.keys(highlights).length) { li.classList.add("too-late") } else { li.classList.add("first") } highlights[client_id] = setTimeout(() => { delete highlights[client_id] li.classList.remove("buzzing") li.classList.remove("too-late") li.classList.remove("first") }, timeout_ms) return } } } /** * Guess the exact current server time. */ function servertime_now_ns() { const now_ms = performance.now() const delta_ns = ms_ns * (now_ms - toffset_ms) return servertime + delta_ns } function setup_ui() { on("focus", (event) => { hide("active") hide("inactive") show("ready") }) on("blur", (event) => { hide("active") show("inactive") hide("ready") }) if (document.hasFocus()) { hide("inactive") } else { hide("ready") } q("#bkey").textContent = buzzer_key.name let buzzing = false on("keydown", (event) => { if (!buzzing && event.keyCode === buzzer_key.code) { buzzing = true send("buzz", servertime_now_ns()) show("active") hide("ready") } }) on("keyup", (event) => { if (event.keyCode === buzzer_key.code) { buzzing = false hide("active") show("ready") } }) q("#username").addEventListener("change", (event) => { send("name", event.target.value) }) ul = q("#info ul") } function setup_ws() { const sid = session_id() socket = new WebSocket(`${config.wsurl}/quiz/${sid}`) socket.addEventListener("open", function (event) { send("name", q("#username").value) }) socket.addEventListener("message", function (event) { const msg = JSON.parse(event.data) if (msg.type === "time") { servertime = msg.value toffset_ms = performance.now() } else if (msg.type === "id") { me = msg.value redraw_clients(me, clients) } else if (msg.type === "buzz") { const buzztime_ns = msg.time const client_id = msg.client const duration_ns = 3 * s_ns const until_ns = buzztime_ns + duration_ns highlight(client_id, until_ns) } else if (msg.type === "clients") { clients = msg.value redraw_clients(me, clients) } else if (msg.type === "client") { const client = msg.value for (const c of clients) { if (c.id === client.id) { c.name = client.name redraw_clients(me, clients) return } } clients.push(client) redraw_clients(me, clients) } else { console.error(`Unknown message: ${event.data}`) } }) } setup_url() setup_ui() setup_ws()