Files
brk/website/scripts/explorer/index.js
2026-04-12 18:00:02 +02:00

200 lines
5.0 KiB
JavaScript

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");
}