Files
brk/website_next/explore/block/preview/inspector.js
T
2026-07-09 23:08:47 +02:00

193 lines
5.2 KiB
JavaScript

import { formatFeeRate } from "../../../utils/fee-rate.js";
import { formatNumber, formatWeight } from "../format.js";
import { FILTER_GROUP_FILTERS } from "./filters/model.js";
import { placeReadout } from "./inspector/position.js";
import { createTxidLoader } from "./inspector/txid.js";
const TRAITS_DWELL_MS = 180;
/**
* @param {HTMLElement} parent
* @param {string} label
*/
function appendField(parent, label) {
const row = document.createElement("div");
const term = document.createElement("dt");
const value = document.createElement("dd");
term.textContent = label;
row.append(term, value);
parent.append(row);
return value;
}
/**
* @param {HTMLElement} element
* @param {string} value
* @param {boolean} loading
*/
function setField(element, value, loading = false) {
element.textContent = value;
if (loading) element.setAttribute("aria-busy", "true");
else element.removeAttribute("aria-busy");
}
/**
* @param {AbortSignal} parentSignal
* @param {() => Promise<BlockPreviewFilterState>} loadFilters
*/
export function createBlockPreviewInspector(parentSignal, loadFilters) {
const element = document.createElement("dl");
const txid = appendField(element, "txid");
const tx = appendField(element, "tx");
const fee = appendField(element, "fee");
const weight = appendField(element, "weight");
const traitFields = FILTER_GROUP_FILTERS.map(({ filters, label }) => {
return /** @type {const} */ ({ filters, value: appendField(element, label) });
});
const txidLoader = createTxidLoader(parentSignal);
let inspected = /** @type {BlockPreviewTransaction | null} */ (null);
let point = /** @type {BlockPreviewPointer | null} */ (null);
let filterState = /** @type {BlockPreviewFilterState | null} */ (null);
let filterPromise = /** @type {Promise<void> | null} */ (null);
let traitsTimer = 0;
element.dataset.blockPreviewTransaction = "";
element.hidden = true;
function abortPending() {
clearTimeout(traitsTimer);
txidLoader.abort();
}
function setTraitsLoading() {
for (const { value } of traitFields) {
value.removeAttribute("title");
setField(value, "loading", true);
}
}
/** @param {number} mask */
function setTraits(mask) {
for (const { filters, value } of traitFields) {
const labels = filters
.filter(({ bit }) => mask & bit)
.map(({ label }) => label);
const text = labels.join(" · ") || "none";
setField(value, text);
value.title = text;
}
}
/** @param {BlockPreviewTransaction} transaction */
function setTransactionTraits(transaction) {
const state = /** @type {BlockPreviewFilterState} */ (filterState);
const mask = state.masks[transaction.txIndex - state.start];
setTraits(mask);
if (point !== null) placeReadout(element, point);
}
function loadFilterState() {
if (filterState !== null || filterPromise !== null) return;
filterPromise = loadFilters()
.then((state) => {
filterState = state;
filterPromise = null;
if (inspected !== null) setTransactionTraits(inspected);
})
.catch((error) => {
filterPromise = null;
if (parentSignal.aborted || inspected === null) return;
for (const { value } of traitFields) setField(value, "unavailable");
console.error(error);
});
}
/**
* @param {BlockPreviewTransaction} transaction
* @param {boolean} eager
*/
function loadTraits(transaction, eager) {
setTraitsLoading();
if (filterState !== null) {
setTransactionTraits(transaction);
return;
}
traitsTimer = setTimeout(() => {
loadFilterState();
}, eager ? 0 : TRAITS_DWELL_MS);
}
/**
* @param {BlockPreviewTransaction | null} transaction
* @param {BlockPreviewPointer | null} nextPoint
* @param {boolean} eager
*/
function inspect(transaction, nextPoint, eager) {
if (transaction === null || nextPoint === null) {
abortPending();
inspected = null;
point = null;
element.hidden = true;
return;
}
element.hidden = false;
point = nextPoint;
placeReadout(element, nextPoint);
if (transaction === inspected) return;
abortPending();
inspected = transaction;
setField(tx, `#${formatNumber(transaction.txIndex)}`);
setField(fee, `${formatFeeRate(transaction.feeRate)} sat/vB`);
setField(weight, formatWeight(transaction.weight));
loadTraits(transaction, eager);
const cached = txidLoader.load(
transaction,
eager,
(loadedTxid) => {
if (inspected !== transaction) return;
setField(txid, loadedTxid);
txid.title = loadedTxid;
},
(error, signal) => {
if (signal.aborted) return;
console.error(error);
},
);
if (!cached) {
txid.removeAttribute("title");
setField(txid, "loading", true);
}
}
return /** @type {const} */ ({
destroy: abortPending,
element,
inspect,
});
}
/** @typedef {import("./data.js").BlockPreviewTransaction} BlockPreviewTransaction */
/** @typedef {import("./data.js").BlockPreviewFilterState} BlockPreviewFilterState */
/**
* @typedef {Object} BlockPreviewPointer
* @property {number} clientX
* @property {number} clientY
*/