Relay firehose browser tools: https://compare.hose.cam
at main 7.2 kB view raw
1import { useEffect, useState } from 'react' 2import { BarChart } from '@mui/x-charts/BarChart'; 3import './App.css' 4import '@mui/x-charts-vendor/d3-scale' 5import Relay from './Relay' 6import knownRelays from './knownRelays.json' 7 8const INTERVAL = 1600; 9const SERIES_LEN = 6; 10const KEEPALIVE = 10 * 60 * 1000; 11 12interface Relay { 13 url: string, 14 desc: string, 15}; 16 17interface Counts { 18 [k: string]: number, 19}; 20 21interface CountBatch { 22 t: number, 23 dt: number, 24 counts: Counts, 25}; 26 27const noopReceiver = (_url: string, _type: string, _event: any) => {}; 28 29function App() { 30 const [relays, setRelays] = useState([] as string[]); 31 const [events, setEvents] = useState(['commit', 'sync', 'account', 'identity', 'unknown']); 32 const [eventsSelected, setEventsSelected] = useState(new Set(events)); 33 const [receiver, setReceiver] = useState(() => noopReceiver); 34 const [keepalive, setKeepalive] = useState(() => () => {}); 35 const [rateBars, setRateBars] = useState({ series: [] } as any); 36 const [died, setDied] = useState(false); 37 const [customRelayHost, setCustomRelayHost] = useState(""); 38 39 useEffect(() => { 40 let lastChangeover = performance.now(); 41 let currentCounts: Counts = {}; 42 let series: CountBatch[] = []; 43 let raf = requestAnimationFrame(update); 44 let ttl = setTimeout(die, KEEPALIVE); 45 46 setReceiver(() => (url: string, _type: string, _event: any) => { 47 if (!currentCounts[url]) currentCounts[url] = 0; 48 currentCounts[url] += 1; 49 }); 50 51 setKeepalive(() => () => { 52 clearTimeout(ttl); 53 ttl = setTimeout(die, KEEPALIVE); 54 setDied(false); 55 console.info('keepalive: disconnection timer reset'); 56 }); 57 58 function die() { 59 console.info('disconnecting due to inactivity'); 60 setRelays([]); 61 setDied(true); 62 } 63 64 const nextBlock = setInterval(() => { 65 let now = performance.now(); 66 let dt = now - lastChangeover; 67 if (series.length >= SERIES_LEN - 1) series.shift(); 68 series.push({ 69 t: now, 70 dt, 71 counts: currentCounts, 72 }); 73 lastChangeover = now; 74 currentCounts = {}; 75 }, INTERVAL); 76 77 function update() { 78 let now = performance.now(); 79 const relays = Object.keys(series.at(-1)?.counts || {}).toSorted(); 80 81 setRateBars({ 82 xAxis: [{ 83 data: series 84 .map(({ t }) => (-(now - t) / 1000).toFixed(1)) 85 .concat(['now']), 86 label: 'bucket (seconds ago)', 87 }], 88 series: relays.map((r: string) => ({ 89 label: r, 90 data: series 91 .map(({ dt, counts }) => { 92 if (!counts[r]) return null; 93 return (counts[r] / (dt / 1000)).toFixed(1); 94 }) 95 .concat([!currentCounts[r] 96 ? null 97 : (currentCounts[r] / (INTERVAL / 1000)).toFixed(1) 98 ]), 99 })), 100 }); 101 102 raf = requestAnimationFrame(update); 103 }; 104 105 return () => { 106 setReceiver(() => noopReceiver); 107 setKeepalive(() => () => null); 108 clearInterval(nextBlock); 109 cancelAnimationFrame(raf); 110 }; 111 }, []); 112 113 114 function showRelay(url: string, show: boolean) { 115 setDied(false); 116 if (show) { 117 setRelays((rs: string[]) => rs.includes(url) ? rs : [...rs, url]); 118 } else { 119 setRelays((rs: string[]) => rs.includes(url) ? rs.filter(u => u !== url) : rs); 120 } 121 keepalive(); 122 } 123 124 function getCustomRelayURL(): string { 125 if (!customRelayHost) return ""; 126 127 try { 128 let url: URL; 129 if (customRelayHost.includes("://")) { 130 url = new URL(customRelayHost); 131 } else { 132 url = new URL("https://" + customRelayHost); 133 } 134 if (url.protocol === 'https:') { 135 url.protocol = 'wss:'; 136 } else if (url.protocol === 'http:') { 137 url.protocol = 'ws:'; 138 } 139 return url.origin; 140 } catch (err) { 141 return ""; 142 } 143 } 144 145 return ( 146 <> 147 <h1>compare hoses</h1> 148 <p><em>warning: enabling many relay connections requires a <strong>lot</strong> of bandwidth</em></p> 149 150 <form style={{ display: 'block', textAlign: 'left' }}> 151 {knownRelays.map(({ url, desc }: Relay) => ( 152 <p key={url} style={{margin: 0}}> 153 <label> 154 <input 155 type="checkbox" 156 onChange={e => showRelay(url, e.target.checked)} 157 checked={relays.includes(url)} 158 /> 159 { ` ${desc} ` } 160 (<code>{ url.slice('wss://'.length) }</code>) 161 </label> 162 </p> 163 ))} 164 165 <p style={{margin: 0}}> 166 <label> 167 <input 168 type='checkbox' 169 onChange={e => { 170 const url = getCustomRelayURL(); 171 if (url) showRelay(url, e.target.checked); 172 }} 173 checked={relays.includes(getCustomRelayURL())} 174 /> 175 {` `} 176 <input 177 type='text' 178 placeholder='wss://…' 179 value={customRelayHost} 180 onChange={(e) => { 181 const oldURL = getCustomRelayURL(); 182 setRelays(relays => relays.includes(oldURL) ? relays.filter(u => u !== oldURL) : relays); 183 setCustomRelayHost(e.target.value); 184 }} 185 onKeyDown={e => { 186 if (e.key !== 'Enter') return; 187 e.preventDefault(); 188 const url = getCustomRelayURL(); 189 if (url) showRelay(url, true); 190 }} 191 /> 192 {` `} 193 {customRelayHost && (<code>{getCustomRelayURL()}</code>)} 194 </label> 195 </p> 196 </form> 197 198 <form style={{ display: 'block', margin: '1rem 0' }}> 199 <span>events: </span> 200 {events.map(eventName => ( 201 <label key={eventName}> 202 <input 203 type='checkbox' 204 checked={eventsSelected.has(eventName)} 205 onChange={e => { 206 setEventsSelected(evs => { 207 const s = new Set(evs); 208 if (e.target.checked) s.add(eventName); 209 else s.delete(eventName); 210 return s; 211 }) 212 }} 213 /> 214 {' '} 215 {eventName} 216 </label> 217 ))} 218 </form> 219 220 <div style={{ display: 'flex', flexWrap: 'wrap', gap: '2em', textAlign: 'left' }}> 221 {relays.map(url => { 222 const { desc } = knownRelays.find((r: Relay) => r.url === url) ?? { desc: "custom relay" }; 223 return ( 224 <div key={url}> 225 <Relay 226 url={url} 227 desc={desc} 228 includeEvents={eventsSelected} 229 onRecieveEvent={(type: string, event: any) => receiver(url, type, event)} 230 /> 231 </div> 232 ); 233 })} 234 </div> 235 236 {died && ( 237 <p><em>disconnected to save bandwidth due to inactivity</em></p> 238 )} 239 240 <div className="throughputs"> 241 <BarChart 242 height={300} 243 yAxis={[{ label: 'events / sec' }]} 244 skipAnimation={true} 245 {...rateBars} 246 /> 247 </div> 248 </> 249 ) 250} 251 252export default App