"use strict" /* global document, window */ const crypto = window.crypto const location = document.location const performance = window.performance const storage = window.sessionStorage // 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, style, ...attrs } = {}) { let elem = typeof type === "string" ? document.createElement(type) : type if (cls) { elem.className = cls } if (text) { elem.textContent = text } Object.assign(elem.dataset, data ?? {}) Object.assign(elem.style, style ?? {}) 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, session_key function hide(e) { e = typeof e === "string" ? q(`#${e}`) : e e.style.display = "none" } function show(e) { e = typeof e === "string" ? q(`#${e}`) : e e.style.display = "block" } function session_id() { const match = /^#?(.+)/.exec(document.location.hash) return match ? match[1] : null } function new_session_id() { if (!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) const player_tpl = q("template#player").content.firstElementChild for (const c of clients) { node(player_tpl.cloneNode(), { text: c.name, data: { cid: c.id }, appendTo: ul, cls: c.id === me.id ? "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") } }) const username_el = q("#username") if (storage["my_name"]) { username_el.value = storage["my_name"] } username_el.addEventListener("change", (event) => { set_name(event.target.value) }) ul = q("#info ul") } function set_name(name) { storage["my_name"] = name send("name", name) } function session_url(sid) { return `${config.wsurl}/${sid}` } function session_id_from_url(url) { const wsurl = new URL(config.wsurl) const match = RegExp(`${wsurl.pathname}/([^/]+)$`).exec(url) return !match ? null : match[1] } function setup_ws() { const sid = session_id() const credentials = { id: storage["my_uid"], key: storage["my_key"] } socket = new WebSocket(`${session_url(sid)}`) socket.addEventListener("open", function (event) { if (sid === storage["my_sid"]) { send("login", credentials) } set_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 = { id: msg.id, key: msg.key, path: msg.path } storage["my_uid"] = me.id storage["my_key"] = me.key storage["my_sid"] = session_id_from_url(me.path) redraw_clients(me, clients) } else if (msg.type === "session_key") { session_key = { path: msg.path, key: msg.key } storage["session_path"] = session_key.path storage["session_key"] = session_key.key } 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 = { name: msg.name, id: msg.id, active: msg.active } 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 if (msg.type === "error") { console.error(`Error: ${msg.reason}`) const errorbox = q("#error") q("code", errorbox).textContent = JSON.stringify(msg, null, 2) show(errorbox) } else { console.error(`Unknown message: ${event.data}`) } }) } setup_url() setup_ui() setup_ws()