diff --git a/web/src/App.tsx b/web/src/App.tsx index 82efeb5..1452463 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -195,6 +195,7 @@ export default function App() { {route.index === 'wormhole' ? ( diff --git a/web/src/components/WormholeStage.tsx b/web/src/components/WormholeStage.tsx index e0b973f..b863359 100644 --- a/web/src/components/WormholeStage.tsx +++ b/web/src/components/WormholeStage.tsx @@ -30,6 +30,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { fetchWormhole } from '../api/rest' +import type { RecentBlock } from '../api/generated/RecentBlock' import type { WormholePulse } from '../api/socket' import { shortAddress, tokens } from '../lib/format' import { href } from '../lib/routes' @@ -187,10 +188,13 @@ function BlockLink({ chain, height }: { chain: string | null; height: number }) export function WormholeStage({ chain, + lastBlock, decimals, symbol, }: { chain: string | null + /** The newest block in the ticker, for the waiting counter. */ + lastBlock: RecentBlock | null decimals: number symbol: string }) { @@ -213,6 +217,69 @@ export function WormholeStage({ /** Monotonic line id. See `ConsoleLine.id` for why this is not derived. */ const nextLineId = useRef(0) + /** + * When *this page* saw the newest block, from the monotonic clock. + * + * Not `observed_at`. That is the server's wall clock, and subtracting it from + * the browser's would fold in whatever the two disagree by — a viewer whose + * machine is a minute fast would watch the counter run backwards. + * `performance.now()` cannot be skewed or stepped, and "since this page saw + * it" is the honest claim anyway. + * + * `null` until a block actually arrives while the page is open: the first + * block the ticker hands over came from the snapshot and may be a minute old, + * so counting from the moment it was *rendered* would start at zero and lie. + */ + const [blockSeenAt, setBlockSeenAt] = useState(null) + const lastHeight = useRef(null) + useEffect(() => { + if (lastBlock === null) return + if (lastHeight.current === null) { + // The snapshot's tail, not an arrival. Note it and wait for a real one. + lastHeight.current = lastBlock.height + return + } + if (lastBlock.height !== lastHeight.current) { + lastHeight.current = lastBlock.height + setBlockSeenAt(performance.now()) + } + }, [lastBlock]) + + /** + * The waiting counter, written straight to the DOM. + * + * A number changing sixty times a second through React state would re-render + * this component — the mouth, the population, the whole console — for one + * span of text. Same reasoning as the flight marks. + */ + const elapsed = useRef(null) + useEffect(() => { + if (blockSeenAt === null || log.length > 0) return + let frame = 0 + let timer = 0 + const paint = () => { + if (elapsed.current) { + const ms = Math.max(0, Math.round(performance.now() - blockSeenAt)) + elapsed.current.textContent = ms.toLocaleString('en-US') + } + } + if (reduced) { + // Still a counter, at a quarter of the flicker. + paint() + timer = window.setInterval(paint, 250) + } else { + const tick = () => { + paint() + frame = requestAnimationFrame(tick) + } + frame = requestAnimationFrame(tick) + } + return () => { + if (frame) cancelAnimationFrame(frame) + if (timer) window.clearInterval(timer) + } + }, [blockSeenAt, log.length, reduced]) + useEffect(() => { if (chain === null) return const controller = new AbortController() @@ -540,7 +607,11 @@ export function WormholeStage({
{last === null ? ( - Watching for the next block. + // The caption is about wormhole activity, the console's counter about + // blocks. Saying "watching for the next block" here while the counter + // beside it times one the page has plainly seen reads as a + // contradiction; this says which of the two is still missing. + Nothing has moved through yet. ) : ( <> #{last.height.toLocaleString('en-US')} @@ -576,7 +647,16 @@ export function WormholeStage({
    {log.length === 0 ? (
  1. - Each note a block commits or releases will be listed here as it happens. + {blockSeenAt === null || lastBlock === null ? ( + 'Each note a block commits or releases will be listed here as it happens.' + ) : ( + <> + + 0 + {' '} + ms since + + )}
  2. ) : ( log.map((line, i) => { diff --git a/web/src/index.css b/web/src/index.css index 2837c41..47f05fd 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1209,6 +1209,13 @@ tr.mine .share-bar > i { color: var(--text-muted); } +/* The waiting counter. Tabular figures so a number climbing through four + digits does not shuffle the words after it sideways sixty times a second. */ +.stage-elapsed { + color: var(--data-bright); + font-variant-numeric: tabular-nums; +} + .stage-log-height { color: var(--text-muted); min-width: 62px;