Relay firehose browser tools: https://compare.hose.cam
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