fork chart
This commit is contained in:
+200
-74
@@ -1,42 +1,52 @@
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { BlockInfo, blocksByHeight$, chainHead$ } from "./block.state"
|
||||
import {
|
||||
animationFrames,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
merge,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { useEffect, useState } from "react"
|
||||
import { BlockInfo, blocksByHeight$, chainHead$ } from "./block.state"
|
||||
import { FC } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
const finalized$ = state(
|
||||
chainHead$.pipe(switchMap((chainHead) => chainHead.finalized$)),
|
||||
)
|
||||
const best$ = state(chainHead$.pipe(switchMap((chainHead) => chainHead.best$)))
|
||||
|
||||
export const Explorer = withSubscribe(() => {
|
||||
const finalized = useStateObservable(finalized$)
|
||||
const best = useStateObservable(best$)
|
||||
const explorer$ = merge(finalized$, best$, blocksByHeight$)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<span>Finalized:</span>
|
||||
<span className="tabular-nums">{finalized.number}</span>
|
||||
<span className="text-slate-300">{finalized.hash}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span>Best:</span>
|
||||
<span className="tabular-nums">{best.number}</span>
|
||||
<span className="text-slate-300">{best.hash}</span>
|
||||
</div>
|
||||
export const Explorer = withSubscribe(
|
||||
() => {
|
||||
const finalized = useStateObservable(finalized$)
|
||||
const best = useStateObservable(best$)
|
||||
|
||||
return (
|
||||
<div>
|
||||
Block time: <span className="tabular-nums">{bestBlockTime$}s</span>
|
||||
<div className="flex gap-2">
|
||||
<span>Finalized:</span>
|
||||
<span className="tabular-nums">{finalized.number}</span>
|
||||
<span className="text-slate-300">{finalized.hash}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span>Best:</span>
|
||||
<span className="tabular-nums">{best.number}</span>
|
||||
<span className="text-slate-300">{best.hash}</span>
|
||||
</div>
|
||||
<div>
|
||||
Block time: <span className="tabular-nums">{bestBlockTime$}s</span>
|
||||
</div>
|
||||
<BlockTable />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
{
|
||||
source$: explorer$,
|
||||
},
|
||||
)
|
||||
|
||||
const bestBlockTime$ = best$.pipeState(
|
||||
// Only count when increasing height
|
||||
@@ -52,65 +62,181 @@ const bestBlockTime$ = best$.pipeState(
|
||||
}),
|
||||
)
|
||||
|
||||
const blockTable$ = blocksByHeight$
|
||||
.pipe(
|
||||
withLatestFrom(best$),
|
||||
map(([blocks, best]) => {
|
||||
console.log(blocks)
|
||||
const result: Array<{
|
||||
block: BlockInfo
|
||||
position: number
|
||||
branched: number | null
|
||||
branches: number[]
|
||||
}> = []
|
||||
export interface PositionedBlock {
|
||||
block: BlockInfo
|
||||
position: number
|
||||
branched: number | null
|
||||
branches: number[]
|
||||
}
|
||||
const blockTable$ = blocksByHeight$.pipeState(
|
||||
withLatestFrom(best$),
|
||||
map(([blocks, best]) => {
|
||||
const result: Array<PositionedBlock> = []
|
||||
|
||||
const blockPositions: Record<string, number> = {}
|
||||
const positionsTaken = new Set<number>()
|
||||
const lockFreePosition = () => {
|
||||
for (let i = 0; ; i++) {
|
||||
if (!positionsTaken.has(i)) {
|
||||
positionsTaken.add(i)
|
||||
return i
|
||||
}
|
||||
const blockPositions: Record<string, number> = {}
|
||||
const positionsTaken = new Set<number>()
|
||||
const lockFreePosition = () => {
|
||||
for (let i = 0; ; i++) {
|
||||
if (!positionsTaken.has(i)) {
|
||||
positionsTaken.add(i)
|
||||
return i
|
||||
}
|
||||
}
|
||||
for (let height = best.number; blocks[height]; height--) {
|
||||
const competingBlocks = [...blocks[height].values()]
|
||||
}
|
||||
for (let height = best.number; blocks[height]; height--) {
|
||||
const competingBlocks = [...blocks[height].values()]
|
||||
if (competingBlocks.length > 1) {
|
||||
if (height === best.number) {
|
||||
competingBlocks.sort((a) => (a.hash === best.hash ? -1 : 1))
|
||||
}
|
||||
const positionsMerged: number[] = []
|
||||
competingBlocks.forEach((block) => {
|
||||
const branches = [...positionsTaken].filter(
|
||||
(v) => !positionsMerged.includes(v),
|
||||
} else {
|
||||
competingBlocks.sort((a) =>
|
||||
(blockPositions[a.hash] ?? Number.POSITIVE_INFINITY) <
|
||||
(blockPositions[best.hash] ?? Number.POSITIVE_INFINITY)
|
||||
? -1
|
||||
: 1,
|
||||
)
|
||||
|
||||
const position = blockPositions[block.hash] ?? lockFreePosition()
|
||||
if (blockPositions[block.parent]) {
|
||||
// then it means the parent was already discovered by a previous
|
||||
// so this is the start of a branch
|
||||
result.push({
|
||||
block,
|
||||
branched: blockPositions[block.parent],
|
||||
branches,
|
||||
position,
|
||||
})
|
||||
positionsMerged.push(position)
|
||||
} else {
|
||||
// We put our parent underneath us
|
||||
blockPositions[block.parent] = position
|
||||
result.push({
|
||||
block,
|
||||
branched: null,
|
||||
branches,
|
||||
position,
|
||||
})
|
||||
}
|
||||
})
|
||||
positionsMerged.forEach((v) => positionsTaken.delete(v))
|
||||
}
|
||||
}
|
||||
const positionsMerged: number[] = []
|
||||
competingBlocks.forEach((block) => {
|
||||
const branches = [...positionsTaken].filter(
|
||||
(v) => !positionsMerged.includes(v),
|
||||
)
|
||||
|
||||
return result
|
||||
}),
|
||||
const position = blockPositions[block.hash] ?? lockFreePosition()
|
||||
if (blockPositions[block.parent] != null) {
|
||||
// then it means the parent was already discovered by a previous
|
||||
// so this is the start of a branch
|
||||
result.push({
|
||||
block,
|
||||
branched: blockPositions[block.parent],
|
||||
branches,
|
||||
position,
|
||||
})
|
||||
positionsMerged.push(position)
|
||||
} else {
|
||||
// We put our parent underneath us
|
||||
blockPositions[block.parent] = position
|
||||
result.push({
|
||||
block,
|
||||
branched: null,
|
||||
branches,
|
||||
position,
|
||||
})
|
||||
}
|
||||
})
|
||||
positionsMerged.forEach((v) => positionsTaken.delete(v))
|
||||
}
|
||||
|
||||
return result
|
||||
}),
|
||||
)
|
||||
|
||||
const BlockTable = () => {
|
||||
const rows = useStateObservable(blockTable$)
|
||||
const finalized = useStateObservable(finalized$)
|
||||
|
||||
const numberSpan = (idx: number) => {
|
||||
const initialIdx = idx
|
||||
const number = rows[idx].block.number
|
||||
do {
|
||||
idx++
|
||||
} while (number === rows[idx]?.block.number)
|
||||
return idx - initialIdx
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Block Number</th>
|
||||
<th>Forks</th>
|
||||
<th>Hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => (
|
||||
<tr
|
||||
key={row.block.hash}
|
||||
className={twMerge(
|
||||
row.block.number <= finalized.number ? "bg-polkadot-900" : "",
|
||||
row.block.number === finalized.number &&
|
||||
row.position === 0 &&
|
||||
"border-t",
|
||||
)}
|
||||
>
|
||||
{rows[i - 1]?.block.number !== row.block.number ? (
|
||||
<td rowSpan={numberSpan(i)} className="px-2">
|
||||
{row.block.number}
|
||||
</td>
|
||||
) : null}
|
||||
<td className="p-0">
|
||||
<ForkRenderer row={row} />
|
||||
</td>
|
||||
<td
|
||||
className={
|
||||
row.position === 0
|
||||
? ""
|
||||
: row.block.number > finalized.number
|
||||
? "opacity-80"
|
||||
: "opacity-50"
|
||||
}
|
||||
>
|
||||
{row.block.hash}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
.subscribe((v) => console.log(v.map((v) => ({ ...v, hash: v.block.hash }))))
|
||||
}
|
||||
|
||||
const CELL_WIDTH = 26
|
||||
const CELL_HEIGHT = 26
|
||||
const CIRCLE_R = 5
|
||||
const ForkRenderer: FC<{ row: PositionedBlock }> = ({ row }) => {
|
||||
const totalCells = Math.max(row.position, ...row.branches) + 1
|
||||
|
||||
const getPositionCenter = (p: number) => CELL_WIDTH * p + CELL_WIDTH / 2
|
||||
|
||||
return (
|
||||
<svg
|
||||
height={CELL_HEIGHT}
|
||||
width={CELL_WIDTH * totalCells}
|
||||
className="stroke-polkadot-200"
|
||||
>
|
||||
{row.branches.map((branch, i) => (
|
||||
<line
|
||||
key={i}
|
||||
x1={getPositionCenter(branch)}
|
||||
y1={0}
|
||||
x2={getPositionCenter(branch)}
|
||||
y2={CELL_HEIGHT}
|
||||
/>
|
||||
))}
|
||||
{row.branched != null ? (
|
||||
<line
|
||||
x1={getPositionCenter(row.branched)}
|
||||
y1={CELL_HEIGHT / 2}
|
||||
x2={getPositionCenter(row.position)}
|
||||
y2={CELL_HEIGHT / 2}
|
||||
/>
|
||||
) : row.branches.includes(row.position) ? null : (
|
||||
<line
|
||||
x1={getPositionCenter(row.position)}
|
||||
y1={CELL_HEIGHT / 2}
|
||||
x2={getPositionCenter(row.position)}
|
||||
y2={CELL_HEIGHT}
|
||||
/>
|
||||
)}
|
||||
<circle
|
||||
cx={getPositionCenter(row.position)}
|
||||
cy={CELL_HEIGHT / 2}
|
||||
r={CIRCLE_R}
|
||||
className={
|
||||
row.position === 0 ? "fill-polkadot-500" : "fill-polkadot-600"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,9 @@ const [blockInfo$, recordedBlocks$] = partitionByKey(
|
||||
export const blocksByHeight$ = state(
|
||||
recordedBlocks$.pipe(
|
||||
mergeMap((change) => {
|
||||
const targets$ = forkJoin([...change.keys].map(blockInfo$))
|
||||
const targets$ = forkJoin(
|
||||
[...change.keys].map((hash) => blockInfo$(hash).pipe(take(1))),
|
||||
)
|
||||
|
||||
return targets$.pipe(
|
||||
map((targets) => ({
|
||||
|
||||
Reference in New Issue
Block a user