mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 07:09:59 -07:00
200 lines
5.0 KiB
JavaScript
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");
|
|
}
|