import { explorerElement } from "../utils/elements.js"; import { brk } from "../utils/client.js"; import { createMapCache } from "../utils/cache.js"; import { initChain, goToCube, poll, selectCube, deselectCube, } from "./chain.js"; import { initBlockDetails, update as updateBlock, show as showBlock, hide as hideBlock, } from "./block.js"; import { initTxDetails, update as updateTx, clear as clearTx, show as showTx, hide as hideTx, } from "./tx.js"; import { initAddrDetails, update as updateAddr, show as showAddr, hide as hideAddr, } from "./address.js"; /** @returns {string[]} */ function pathSegments() { return window.location.pathname.split("/").filter((v) => v); } /** @type {number | undefined} */ let pollInterval; let navController = new AbortController(); const txCache = createMapCache(50); let lastLoadedUrl = ""; function navigate() { navController.abort(); navController = new AbortController(); lastLoadedUrl = window.location.pathname; return navController.signal; } function showPanel(/** @type {"block" | "tx" | "addr"} */ which) { which === "block" ? showBlock() : hideBlock(); which === "tx" ? showTx() : hideTx(); which === "addr" ? showAddr() : hideAddr(); } /** @param {MouseEvent} e */ function handleLinkClick(e) { const a = /** @type {HTMLAnchorElement | null} */ ( /** @type {HTMLElement} */ (e.target).closest("a[href]") ); if (!a) return; const m = a.pathname.match(/^\/(block|tx|address)\/(.+)/); if (!m) return; e.preventDefault(); history.pushState(null, "", a.href); if (m[1] === "block") { navigateToBlock(m[2]); } else if (m[1] === "tx") { navigateToTx(m[2]); } else { navigateToAddr(m[2]); } } /** @param {{ onChange: (cb: (option: Option) => void) => void }} selected */ export function init(selected) { initChain(explorerElement, { onSelect: (block) => { updateBlock(block); showPanel("block"); }, onCubeClick: (cube) => { const hash = cube.dataset.hash; if (hash) history.pushState(null, "", `/block/${hash}`); navigate(); selectCube(cube); }, }); initBlockDetails(explorerElement, handleLinkClick); initTxDetails(explorerElement, handleLinkClick); initAddrDetails(explorerElement, handleLinkClick); new MutationObserver(() => { if (explorerElement.hidden) stopPolling(); else startPolling(); }).observe(explorerElement, { attributes: true, attributeFilter: ["hidden"], }); document.addEventListener("visibilitychange", () => { if (!document.hidden && !explorerElement.hidden) poll(); }); selected.onChange((option) => { if (option.kind === "explorer") { const url = window.location.pathname; if (url !== lastLoadedUrl) load(); } }); } function startPolling() { stopPolling(); poll(); pollInterval = setInterval(poll, 15_000); } function stopPolling() { if (pollInterval !== undefined) { clearInterval(pollInterval); pollInterval = undefined; } } async function load() { const signal = navigate(); try { const [kind, value] = pathSegments(); if (kind === "tx" && value) { const txid = await resolveTxid(value, { signal }); if (signal.aborted) return; const tx = txCache.get(txid) ?? (await brk.getTx(txid, { signal })); if (signal.aborted) return; txCache.set(txid, tx); await goToCube(tx.status?.blockHash ?? tx.status?.blockHeight ?? null, { silent: true }); updateTx(tx); showPanel("tx"); return; } if (kind === "address" && value) { await goToCube(null, { silent: true }); navigateToAddr(value); return; } await goToCube(kind === "block" ? value : null); } catch (e) { if (signal.aborted) return; console.error("explorer load:", e); await goToCube(); showPanel("block"); } } /** @param {string} hashOrHeight */ async function navigateToBlock(hashOrHeight) { const signal = navigate(); await goToCube(hashOrHeight); if (!signal.aborted) showPanel("block"); } /** @param {Txid | TxIndex} value @param {{ signal?: AbortSignal }} [options] */ async function resolveTxid(value, { signal } = {}) { return typeof value === "number" || /^\d+$/.test(value) ? await brk.getTxByIndex(Number(value), { signal }) : value; } /** @param {Txid | TxIndex} txidOrIndex */ async function navigateToTx(txidOrIndex) { const signal = navigate(); clearTx(); showPanel("tx"); try { const txid = await resolveTxid(txidOrIndex, { signal }); if (signal.aborted) return; const tx = txCache.get(txid) ?? (await brk.getTx(txid, { signal })); if (signal.aborted) return; txCache.set(txid, tx); await goToCube(tx.status?.blockHash ?? tx.status?.blockHeight ?? null, { silent: true }); updateTx(tx); } catch (e) { if (!signal.aborted) { console.error("explorer tx:", e); await goToCube(); showPanel("block"); } } } /** @param {string} address */ function navigateToAddr(address) { const signal = navigate(); deselectCube(); updateAddr(address, signal); showPanel("addr"); }