quiz/public/shared.js
ducklet 9e7000054b fix connection time syncing handling
Share the time syncing code, and while we're at it wrap the whole
connection thing in a class.  Makes it easier to pass the connection
around & later on add more advanced handler registration if we want to.
2021-02-02 21:02:30 +01:00

162 lines
3.9 KiB
JavaScript

"use strict"
/* global document, window */
const location = document.location
const performance = window.performance
import config from "./config.js"
export function isString(x) {
return typeof x === "string"
}
export function isEmpty(x) {
return (Array.isArray(x) ? x : Object.keys(x)).length === 0
}
export const q = (selector, root = document) => root.querySelector(selector)
const _evlisteners = {}
/**
* @param {String} event [description]
* @param {Function} cb [description]
* @param {?{once: bool, target: Element = document}} options
*/
export function on(event, cb, options = {}) {
const target = options.target || document
if ((_evlisteners[target] ?? {})[event]) {
console.warn(`Replacing previous event listener for '${event}' on target:`, target)
off(event, target)
}
target.addEventListener(event, cb, { once: !!options.once })
if (!_evlisteners[target]) {
_evlisteners[target] = {}
}
_evlisteners[target][event] = cb
}
export function off(event, target = document) {
if (!_evlisteners[target] || !_evlisteners[target][event]) {
return
}
target.removeEventListener(event, _evlisteners[target][event])
delete _evlisteners[target][event]
if (isEmpty(_evlisteners[target])) {
delete _evlisteners[target]
}
}
export function node(type, { appendTo, cls, text, data, style, ...attrs } = {}) {
let elem = isString(type) ? document.createElement(type) : type
if (cls) {
if (isString(cls)) {
elem.className = cls
} else {
for (const [name, on] of Object.entries(cls)) {
if (on) {
elem.classList.add(name)
} else {
elem.classList.remove(name)
}
}
}
}
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.
*/
export const ms_ns = 1_000_000 // nanoseconds in a millisecond
export const s_ms = 1_000 // milliseconds in a second
export const s_ns = 1_000_000_000 // nanoseconds in a second
export function session_url(sid) {
return `${config.wsurl}/${sid}`
}
export function session_id_from_url(url) {
const wsurl = new URL(config.wsurl)
const match = RegExp(`${wsurl.pathname}/([^/]+)$`).exec(url)
return !match ? null : match[1]
}
export function clear(container) {
while (container.children.length > 0) {
const child = container.children[0]
child.remove()
}
}
export function session_id() {
const match = /^#?(.+)/.exec(location.hash)
return match ? match[1] : null
}
export class Connection {
constructor() {
this.socket = null
this.toffset_ms = null
this.servertime = null
this.handlers = {
time: this._handler_time.bind(this),
error: this._handler_error.bind(this),
}
}
_handler_time({ value: { time } }) {
this.servertime = time
this.toffset_ms = performance.now()
}
_handler_error({ value: { reason } }) {
console.error(`Error: ${reason}`)
}
connect(url) {
this.socket = new WebSocket(url)
this.socket.addEventListener("open", (event) => {
if ("helo" in this.handlers) {
this.handlers["helo"](event)
}
})
this.socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data)
if (msg.type in this.handlers) {
this.handlers[msg.type](msg)
} else {
console.error(`Unhandled message: ${event.data}`)
}
})
}
on(type, callback) {
this.handlers[type] = callback
}
/**
* Guess the exact current server time.
*/
servertime_now_ns() {
const now_ms = performance.now()
const delta_ns = ms_ns * (now_ms - this.toffset_ms)
return this.servertime + delta_ns
}
send(type, value) {
// console.debug('sending', value)
this.socket.send(JSON.stringify({ type, value }))
}
}