global: snap

This commit is contained in:
nym21
2026-04-12 18:00:02 +02:00
parent 18d9c166d8
commit c3cef71aa3
36 changed files with 2366 additions and 371 deletions
+3
View File
@@ -55,9 +55,12 @@
* @typedef {Brk._0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern} Ratio1ySdPattern
* @typedef {Brk.Dollars} Dollars
* @typedef {Brk.BlockInfo} BlockInfo
* @typedef {Brk.Height} Height
* @typedef {Brk.BlockHash} BlockHash
* @typedef {Brk.BlockInfoV1} BlockInfoV1
* @typedef {Brk.Transaction} Transaction
* @typedef {Brk.Txid} Txid
* @typedef {Brk.TxIndex} TxIndex
* @typedef {Brk.AddrStats} AddrStats
* @typedef {Brk.TxIn} TxIn
* @typedef {Brk.TxOut} TxOut
+46 -5
View File
@@ -56,10 +56,16 @@ export function initChain(parent, callbacks) {
);
}
/** @param {string} hash */
export function findCube(hash) {
/** @param {BlockHash | Height | null} [hashOrHeight] */
function findCube(hashOrHeight) {
if (hashOrHeight == null) {
return reachedTip && newestHeight >= 0
? /** @type {HTMLDivElement | null} */ (blocksEl.lastElementChild)
: null;
}
const attr = typeof hashOrHeight === "number" ? "height" : "hash";
return /** @type {HTMLDivElement | null} */ (
blocksEl.querySelector(`[data-hash="${hash}"]`)
blocksEl.querySelector(`[data-${attr}="${hashOrHeight}"]`)
);
}
@@ -86,7 +92,7 @@ export function selectCube(cube, { scroll, silent } = {}) {
}
}
export function clear() {
function clear() {
newestHeight = -1;
oldestHeight = Infinity;
loadingOlder = false;
@@ -126,12 +132,13 @@ function appendNewerBlocks(blocks) {
}
/** @param {number | null} [height] @returns {Promise<BlockHash>} */
export async function loadInitial(height) {
async function loadInitial(height) {
const blocks =
height != null
? await brk.getBlocksV1FromHeight(height)
: await brk.getBlocksV1();
clear();
for (const b of blocks) blocksEl.prepend(createBlockCube(b));
newestHeight = blocks[0].height;
oldestHeight = blocks[blocks.length - 1].height;
@@ -141,6 +148,39 @@ export async function loadInitial(height) {
return blocks[0].id;
}
/** @param {BlockHash | Height | null} [hashOrHeight] @returns {Promise<Height | null>} */
async function resolveHeight(hashOrHeight) {
if (typeof hashOrHeight === "number") return hashOrHeight;
if (typeof hashOrHeight === "string") {
const cached = blocksByHash.get(hashOrHeight);
if (cached) return cached.height;
const block = await brk.getBlockV1(hashOrHeight);
blocksByHash.set(hashOrHeight, block);
return block.height;
}
return null;
}
/** @param {BlockHash | Height | string | null} [hashOrHeight] @param {{ silent?: boolean }} [options] */
export async function goToCube(hashOrHeight, { silent } = {}) {
if (typeof hashOrHeight === "string" && /^\d+$/.test(hashOrHeight)) {
hashOrHeight = Number(hashOrHeight);
}
let cube = findCube(hashOrHeight);
if (cube) {
selectCube(cube, { scroll: "smooth", silent });
return;
}
let startHash;
try {
const height = await resolveHeight(hashOrHeight);
startHash = await loadInitial(height);
} catch (e) {
try { startHash = await loadInitial(null); } catch (_) { return; }
}
selectCube(/** @type {HTMLDivElement} */ (findCube(startHash)), { scroll: "instant", silent });
}
export async function poll() {
if (newestHeight === -1 || !reachedTip) return;
try {
@@ -185,6 +225,7 @@ function createBlockCube(block) {
createCube();
cubeElement.dataset.hash = block.id;
cubeElement.dataset.height = String(block.height);
blocksByHash.set(block.id, block);
cubeElement.addEventListener("click", () => onCubeClick(cubeElement));
+47 -71
View File
@@ -3,12 +3,10 @@ import { brk } from "../utils/client.js";
import { createMapCache } from "../utils/cache.js";
import {
initChain,
loadInitial,
goToCube,
poll,
selectCube,
deselectCube,
findCube,
clear as clearChain,
} from "./chain.js";
import {
initBlockDetails,
@@ -38,10 +36,12 @@ function pathSegments() {
/** @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;
}
@@ -60,27 +60,27 @@ function handleLinkClick(e) {
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") {
history.pushState(null, "", a.href);
navigateToTx(m[2]);
} else {
history.pushState(null, "", a.href);
navigateToAddr(m[2]);
}
}
export function init() {
/** @param {{ onChange: (cb: (option: Option) => void) => void }} selected */
export function init(selected) {
initChain(explorerElement, {
onSelect: (block) => {
updateBlock(block);
showPanel("block");
},
onCubeClick: (cube) => {
navigate();
const hash = cube.dataset.hash;
if (hash) history.pushState(null, "", `/block/${hash}`);
navigate();
selectCube(cube);
},
});
@@ -101,15 +101,12 @@ export function init() {
if (!document.hidden && !explorerElement.hidden) poll();
});
window.addEventListener("popstate", () => {
const [kind, value] = pathSegments();
if (kind === "block" && value) navigateToBlock(value, false);
else if (kind === "tx" && value) navigateToTx(value);
else if (kind === "address" && value) navigateToAddr(value);
else showPanel("block");
selected.onChange((option) => {
if (option.kind === "explorer") {
const url = window.location.pathname;
if (url !== lastLoadedUrl) load();
}
});
load();
}
function startPolling() {
@@ -126,98 +123,77 @@ function stopPolling() {
}
async function load() {
const signal = navigate();
try {
const [kind, value] = pathSegments();
if (kind === "tx" && value) {
const tx = txCache.get(value) ?? (await brk.getTx(value));
txCache.set(value, tx);
const startHash = await loadInitial(tx.status?.blockHeight ?? null);
const cube = tx.status?.blockHash ? findCube(tx.status.blockHash) : findCube(startHash);
if (cube) selectCube(cube, { silent: true });
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) {
const startHash = await loadInitial(null);
const cube = findCube(startHash);
if (cube) selectCube(cube, { silent: true });
await goToCube(null, { silent: true });
navigateToAddr(value);
return;
}
const height =
kind === "block" && value
? /^\d+$/.test(value)
? Number(value)
: (await brk.getBlockV1(value)).height
: null;
const startHash = await loadInitial(height);
const cube = findCube(startHash);
if (cube) selectCube(cube, { scroll: "instant" });
await goToCube(kind === "block" ? value : null);
} catch (e) {
if (signal.aborted) return;
console.error("explorer load:", e);
await goToCube();
showPanel("block");
}
}
/** @param {string} hash @param {boolean} [pushUrl] */
async function navigateToBlock(hash, pushUrl = true) {
if (pushUrl) history.pushState(null, "", `/block/${hash}`);
const existing = findCube(hash);
if (existing) {
navigate();
selectCube(existing, { scroll: "smooth" });
return;
}
/** @param {string} hashOrHeight */
async function navigateToBlock(hashOrHeight) {
const signal = navigate();
try {
clearChain();
const height = /^\d+$/.test(hash)
? Number(hash)
: (await brk.getBlockV1(hash, { signal })).height;
if (signal.aborted) return;
const startHash = await loadInitial(height);
if (signal.aborted) return;
const cube = findCube(hash) ?? findCube(startHash);
if (cube) selectCube(cube);
} catch (e) {
if (!signal.aborted) console.error("explorer block:", e);
}
await goToCube(hashOrHeight);
if (!signal.aborted) showPanel("block");
}
/** @param {string} txid */
async function navigateToTx(txid) {
/** @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);
if (tx.status?.blockHash) {
let cube = findCube(tx.status.blockHash);
if (!cube) {
clearChain();
const startHash = await loadInitial(tx.status.blockHeight ?? null);
if (signal.aborted) return;
cube = findCube(tx.status.blockHash) ?? findCube(startHash);
}
if (cube) selectCube(cube, { scroll: "smooth", silent: true });
}
await goToCube(tx.status?.blockHash ?? tx.status?.blockHeight ?? null, { silent: true });
updateTx(tx);
} catch (e) {
if (!signal.aborted) console.error("explorer tx:", e);
if (!signal.aborted) {
console.error("explorer tx:", e);
await goToCube();
showPanel("block");
}
}
}
/** @param {string} address */
function navigateToAddr(address) {
navigate();
const signal = navigate();
deselectCube();
updateAddr(address, navController.signal);
updateAddr(address, signal);
showPanel("addr");
}
+1 -1
View File
@@ -136,7 +136,7 @@ function initSelected() {
element = explorerElement;
if (firstTimeLoadingExplorer) {
initExplorer();
initExplorer(options.selected);
}
firstTimeLoadingExplorer = false;
+14 -7
View File
@@ -49,6 +49,13 @@ export function initOptions() {
highlightedLis.push(li);
}
}
if (!highlightedLis.length) {
const li = liByPath.get(stringToId(sel.name));
if (li) {
li.dataset.highlight = "";
highlightedLis.push(li);
}
}
}
const selected = {
@@ -360,8 +367,9 @@ export function initOptions() {
/**
* @param {ProcessedNode[]} nodes
* @param {HTMLElement} parentEl
* @param {boolean} autoOpen
*/
function buildTreeDOM(nodes, parentEl) {
function buildTreeDOM(nodes, parentEl, autoOpen) {
const ul = window.document.createElement("ul");
for (const node of nodes) {
@@ -370,8 +378,6 @@ export function initOptions() {
liByPath.set(node.pathKey, li);
const onSelectedPath = isOnSelectedPath(node.path);
if (node.type === "group") {
const details = window.document.createElement("details");
details.dataset.name = node.serName;
@@ -386,16 +392,17 @@ export function initOptions() {
summary.append(count);
let built = false;
if (onSelectedPath) {
if (autoOpen && isOnSelectedPath(node.path)) {
built = true;
details.open = true;
buildTreeDOM(node.children, details);
buildTreeDOM(node.children, details, true);
}
details.addEventListener("toggle", () => {
if (details.open && !built) {
built = true;
buildTreeDOM(node.children, details);
buildTreeDOM(node.children, details, false);
}
updateHighlight(selected.value);
});
} else {
const element = createOptionElement({
@@ -417,7 +424,7 @@ export function initOptions() {
function setParent(el) {
if (parentEl) return;
parentEl = el;
buildTreeDOM(processedTree, el);
buildTreeDOM(processedTree, el, true);
updateHighlight(selected.value);
}
+82 -22
View File
@@ -18,44 +18,104 @@ export function init(options) {
const matcher = new QuickMatch(haystack);
/** @type {HTMLLIElement | undefined} */
let highlighted;
/** @param {HTMLLIElement} [li] */
function setHighlight(li) {
if (highlighted) delete highlighted.dataset.highlight;
highlighted = li;
if (li) li.dataset.highlight = "";
}
function inputEvent() {
const needle = /** @type {string} */ (searchInput.value).trim();
searchResultsElement.scrollTo({ top: 0 });
searchResultsElement.innerHTML = "";
setHighlight();
if (!needle.length) {
return;
}
if (!needle.length) return;
const matches = matcher.matches(needle);
if (!matches.length) {
const indexMatch = needle.match(
/^(?:(block|b)|(transaction|tx))?\s*#?\s*(\d+)$/i,
);
if (indexMatch) {
const num = indexMatch[3];
const entries = indexMatch[1]
? [["Block", `/block/${num}`]]
: indexMatch[2]
? [["Transaction", `/tx/${num}`]]
: [
["Block", `/block/${num}`],
["Transaction", `/tx/${num}`],
];
for (const [label, href] of entries) {
const li = window.document.createElement("li");
const a = window.document.createElement("a");
a.href = href;
a.textContent = `${label} #${num}`;
a.title = `${label} #${num}`;
if (href === window.location.pathname) setHighlight(li);
a.addEventListener("click", (e) => {
e.preventDefault();
setHighlight(li);
history.pushState(null, "", href);
options.resolveUrl();
});
li.append(a);
searchResultsElement.appendChild(li);
}
}
if (matches.length) {
matches.forEach((title) => {
const option = titleToOption.get(title);
if (!option) return;
const li = window.document.createElement("li");
searchResultsElement.appendChild(li);
if (option === options.selected.value) setHighlight(li);
const element = options.createOptionElement({
option,
name: option.title,
});
if (element) li.append(element);
});
}
if (!searchResultsElement.children.length) {
const li = window.document.createElement("li");
li.textContent = "No results";
li.style.color = "var(--off-color)";
searchResultsElement.appendChild(li);
return;
}
matches.forEach((title) => {
const option = titleToOption.get(title);
if (!option) return;
const li = window.document.createElement("li");
searchResultsElement.appendChild(li);
const element = options.createOptionElement({
option,
name: option.title,
});
if (element) {
li.append(element);
}
});
}
options.selected.onChange(() => {
const selected = options.selected.value;
const href =
selected?.kind === "explorer"
? window.location.pathname
: selected?.path.length
? `/${selected.path.join("/")}`
: null;
if (!href) return setHighlight();
for (const li of searchResultsElement.children) {
const a = li.querySelector("a");
if (a && a.getAttribute("href") === href) {
return setHighlight(/** @type {HTMLLIElement} */ (li));
}
}
setHighlight();
});
inputEvent();
searchInput.addEventListener("input", inputEvent);