"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) export const qs = (selector, root = document) => root.querySelectorAll(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 })) } }