+56
-37
package-lock.json
+56
-37
package-lock.json
···
8
8
"name": "firehose-diff",
9
9
"version": "0.0.0",
10
10
"dependencies": {
11
+
"@atcute/tid": "^1.0.2",
11
12
"@emotion/react": "^11.14.0",
12
13
"@emotion/styled": "^11.14.0",
13
14
"@mui/material": "^7.1.0",
14
-
"@mui/x-charts": "^8.3.1",
15
+
"@mui/x-charts": "^8.10.2",
15
16
"@skyware/firehose": "^0.5.1",
16
17
"react": "^19.1.0",
17
18
"react-dom": "^19.1.0"
···
84
85
"dependencies": {
85
86
"@atcute/uint8array": "^1.0.1"
86
87
}
88
+
},
89
+
"node_modules/@atcute/tid": {
90
+
"version": "1.0.2",
91
+
"resolved": "https://registry.npmjs.org/@atcute/tid/-/tid-1.0.2.tgz",
92
+
"integrity": "sha512-ahmjroNyeDPJhtuf3+HTJropaH04HmJ8fhntDu73Gpz/RkAF7+nkz6kcP2QTgfvMCgMPAJUdskAAP82GPDTY9w==",
93
+
"license": "MIT"
87
94
},
88
95
"node_modules/@atcute/uint8array": {
89
96
"version": "1.0.1",
···
316
323
}
317
324
},
318
325
"node_modules/@babel/runtime": {
319
-
"version": "7.27.1",
320
-
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
321
-
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
326
+
"version": "7.28.3",
327
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
328
+
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
322
329
"license": "MIT",
323
330
"engines": {
324
331
"node": ">=6.9.0"
···
1403
1410
}
1404
1411
},
1405
1412
"node_modules/@mui/types": {
1406
-
"version": "7.4.2",
1407
-
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
1408
-
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
1413
+
"version": "7.4.5",
1414
+
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz",
1415
+
"integrity": "sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==",
1409
1416
"license": "MIT",
1410
1417
"dependencies": {
1411
-
"@babel/runtime": "^7.27.1"
1418
+
"@babel/runtime": "^7.28.2"
1412
1419
},
1413
1420
"peerDependencies": {
1414
1421
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
···
1420
1427
}
1421
1428
},
1422
1429
"node_modules/@mui/utils": {
1423
-
"version": "7.1.0",
1424
-
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
1425
-
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
1430
+
"version": "7.3.1",
1431
+
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.1.tgz",
1432
+
"integrity": "sha512-/31y4wZqVWa0jzMnzo6JPjxwP6xXy4P3+iLbosFg/mJQowL1KIou0LC+lquWW60FKVbKz5ZUWBg2H3jausa0pw==",
1426
1433
"license": "MIT",
1427
1434
"dependencies": {
1428
-
"@babel/runtime": "^7.27.1",
1429
-
"@mui/types": "^7.4.2",
1430
-
"@types/prop-types": "^15.7.14",
1435
+
"@babel/runtime": "^7.28.2",
1436
+
"@mui/types": "^7.4.5",
1437
+
"@types/prop-types": "^15.7.15",
1431
1438
"clsx": "^2.1.1",
1432
1439
"prop-types": "^15.8.1",
1433
-
"react-is": "^19.1.0"
1440
+
"react-is": "^19.1.1"
1434
1441
},
1435
1442
"engines": {
1436
1443
"node": ">=14.0.0"
···
1450
1457
}
1451
1458
},
1452
1459
"node_modules/@mui/x-charts": {
1453
-
"version": "8.3.1",
1454
-
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.3.1.tgz",
1455
-
"integrity": "sha512-jZClK40++ftcMwCeHKudGKmazd0MsgnrIP6RhYi2lH1kg0jK2upueokyxVIIxqquwWsQYE3WsflJBP61DvYXOQ==",
1460
+
"version": "8.10.2",
1461
+
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.10.2.tgz",
1462
+
"integrity": "sha512-F4SdpixbaAeNaZHylYLpaj/WD1jKn6gMD1Twbd+Y8FzZneE2mrtXwaQc6W2Hbri/VdJl2OV55nL5pFMpDSlwAA==",
1456
1463
"license": "MIT",
1457
1464
"dependencies": {
1458
-
"@babel/runtime": "^7.27.1",
1459
-
"@mui/utils": "^7.0.2",
1460
-
"@mui/x-charts-vendor": "8.3.1",
1461
-
"@mui/x-internals": "8.3.1",
1465
+
"@babel/runtime": "^7.28.2",
1466
+
"@mui/utils": "^7.3.1",
1467
+
"@mui/x-charts-vendor": "8.6.0",
1468
+
"@mui/x-internal-gestures": "0.2.4",
1469
+
"@mui/x-internals": "8.10.2",
1462
1470
"bezier-easing": "^2.1.0",
1463
1471
"clsx": "^2.1.1",
1464
1472
"prop-types": "^15.8.1",
···
1486
1494
}
1487
1495
},
1488
1496
"node_modules/@mui/x-charts-vendor": {
1489
-
"version": "8.3.1",
1490
-
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.3.1.tgz",
1491
-
"integrity": "sha512-UcUa7HDIpSfeVBYgeHewWoVALcB4Gg9we53l78j2cyadYBZOWdtLj8fezo9zAhxfZ5s9T+1yIyuD+CCnYJnUpQ==",
1497
+
"version": "8.6.0",
1498
+
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.6.0.tgz",
1499
+
"integrity": "sha512-TTtfhxXuwtoZfyno7+4y3ZhZeFqavFJecWbteLEby0lFqALWB9GGJpkc1TIHWr3GkWE5UHEbdADZ0pfrPenezA==",
1492
1500
"license": "MIT AND ISC",
1493
1501
"dependencies": {
1494
-
"@babel/runtime": "^7.27.1",
1502
+
"@babel/runtime": "^7.27.6",
1495
1503
"@types/d3-color": "^3.1.3",
1496
1504
"@types/d3-delaunay": "^6.0.4",
1497
1505
"@types/d3-interpolate": "^3.0.4",
···
1510
1518
"robust-predicates": "^3.0.2"
1511
1519
}
1512
1520
},
1521
+
"node_modules/@mui/x-internal-gestures": {
1522
+
"version": "0.2.4",
1523
+
"resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.2.4.tgz",
1524
+
"integrity": "sha512-Hpc5+LQfT0TrI7O0ngBlZ1LD6Gp1h5DUNs4ABbrotY/2OsOfJOzltV/QtBjINTmiBzDPnrE8l8nEiG97qOWX3Q==",
1525
+
"license": "MIT",
1526
+
"dependencies": {
1527
+
"@babel/runtime": "^7.28.2"
1528
+
}
1529
+
},
1513
1530
"node_modules/@mui/x-internals": {
1514
-
"version": "8.3.1",
1515
-
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.3.1.tgz",
1516
-
"integrity": "sha512-8kIxT66cea63iEseEIHSWzKju2Wzl7MsWFoAUQEyRvYqOFa2j9Un2Vn/EH2vy9nm/MtMAYpwOE/nt68/KTIA2w==",
1531
+
"version": "8.10.2",
1532
+
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.10.2.tgz",
1533
+
"integrity": "sha512-dlC0BQRRBdiWtqn1yDppaHYRUjU3OuPWTxy0UtqxDaJjJf4pfR8ALr243nbxgJAFqvQyWPWyO4A6p9x9eJMJEQ==",
1517
1534
"license": "MIT",
1518
1535
"dependencies": {
1519
-
"@babel/runtime": "^7.27.1",
1520
-
"@mui/utils": "^7.0.2"
1536
+
"@babel/runtime": "^7.28.2",
1537
+
"@mui/utils": "^7.3.1",
1538
+
"reselect": "^5.1.1",
1539
+
"use-sync-external-store": "^1.5.0"
1521
1540
},
1522
1541
"engines": {
1523
1542
"node": ">=14.0.0"
···
1992
2011
"license": "MIT"
1993
2012
},
1994
2013
"node_modules/@types/prop-types": {
1995
-
"version": "15.7.14",
1996
-
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
1997
-
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
2014
+
"version": "15.7.15",
2015
+
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
2016
+
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1998
2017
"license": "MIT"
1999
2018
},
2000
2019
"node_modules/@types/react": {
···
4460
4479
}
4461
4480
},
4462
4481
"node_modules/react-is": {
4463
-
"version": "19.1.0",
4464
-
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
4465
-
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
4482
+
"version": "19.1.1",
4483
+
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
4484
+
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
4466
4485
"license": "MIT"
4467
4486
},
4468
4487
"node_modules/react-refresh": {
+2
-1
package.json
+2
-1
package.json
···
10
10
"preview": "vite preview"
11
11
},
12
12
"dependencies": {
13
+
"@atcute/tid": "^1.0.2",
13
14
"@emotion/react": "^11.14.0",
14
15
"@emotion/styled": "^11.14.0",
15
16
"@mui/material": "^7.1.0",
16
-
"@mui/x-charts": "^8.3.1",
17
+
"@mui/x-charts": "^8.10.2",
17
18
"@skyware/firehose": "^0.5.1",
18
19
"react": "^19.1.0",
19
20
"react-dom": "^19.1.0"
+26
-1
src/App.tsx
+26
-1
src/App.tsx
···
28
28
29
29
function App() {
30
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));
31
33
const [receiver, setReceiver] = useState(() => noopReceiver);
32
34
const [keepalive, setKeepalive] = useState(() => () => {});
33
35
const [rateBars, setRateBars] = useState({ series: [] } as any);
···
143
145
return (
144
146
<>
145
147
<h1>compare hoses</h1>
146
-
<p><em>warning: enabling many relay connections requires a lot of bandwidth</em></p>
148
+
<p><em>warning: enabling many relay connections requires a <strong>lot</strong> of bandwidth</em></p>
147
149
148
150
<form style={{ display: 'block', textAlign: 'left' }}>
149
151
{knownRelays.map(({ url, desc }: Relay) => (
···
193
195
</p>
194
196
</form>
195
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
+
196
220
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2em', textAlign: 'left' }}>
197
221
{relays.map(url => {
198
222
const { desc } = knownRelays.find((r: Relay) => r.url === url) ?? { desc: "custom relay" };
···
201
225
<Relay
202
226
url={url}
203
227
desc={desc}
228
+
includeEvents={eventsSelected}
204
229
onRecieveEvent={(type: string, event: any) => receiver(url, type, event)}
205
230
/>
206
231
</div>
+89
-3
src/Relay.tsx
+89
-3
src/Relay.tsx
···
1
-
import { useEffect, useState } from 'react';
1
+
import { useCallback, useEffect, useState } from 'react';
2
2
import { Firehose } from '@skyware/firehose';
3
+
import * as TID from '@atcute/tid';
4
+
import { BarChart } from '@mui/x-charts/BarChart';
3
5
import './Relay.css';
4
6
5
7
type HoseState = 'connecting' | 'connected' | 'errored' | 'closed';
6
8
7
-
function Relay({ url, desc, onRecieveEvent }: {
9
+
const TIME_SIGNAL_NSID = 'app.bsky.feed.like';
10
+
const BUCKET_WIDTH = 32; // ms
11
+
const BUCKETS = 32;
12
+
const MAX_BUCKET = BUCKET_WIDTH * BUCKETS;
13
+
14
+
function Relay({ url, desc, includeEvents, onRecieveEvent }: {
8
15
url: string,
9
16
desc: string,
17
+
includeEvents: Set,
10
18
onRecieveEvent: (type: string, event: any) => void,
11
19
}) {
12
20
const [state, setState] = useState('connecting' as HoseState);
13
21
const [commits, setCommits] = useState(0);
14
22
const [reconnects, setReconnects] = useState(0);
23
+
const [buckets, setBuckets] = useState({
24
+
idx: Array.from({ length: BUCKETS + 2 }).map(() => 0),
25
+
recv: Array.from({ length: BUCKETS + 2 }).map(() => 0),
26
+
});
15
27
16
28
useEffect(() => {
17
29
const sendIt = (type: string, event: any) => {
30
+
if (!includeEvents.has(type)) return;
18
31
onRecieveEvent(type, event);
19
32
setCommits(n => n + 1);
33
+
if (type === 'commit' && event.ops.length === 1) {
34
+
const op = event.ops[0];
35
+
try {
36
+
const [nsid, rkey] = op.path.split('/');
37
+
if (nsid === TIME_SIGNAL_NSID) {
38
+
const posted = TID.parse(rkey).timestamp / 1000;
39
+
const indexed = Date.parse(event.time)
40
+
const indexed_dt = indexed - posted;
41
+
const received_dt = +new Date() - indexed;
42
+
43
+
let idx_bucket, recv_bucket;
44
+
45
+
if (indexed_dt < 0) {
46
+
idx_bucket = -1;
47
+
} else if (indexed_dt >= MAX_BUCKET) {
48
+
idx_bucket = BUCKETS;
49
+
} else {
50
+
idx_bucket = Math.min(Math.floor(indexed_dt / BUCKET_WIDTH), MAX_BUCKET)
51
+
}
52
+
if (received_dt < 0) {
53
+
recv_bucket = -1;
54
+
} else if (received_dt >= MAX_BUCKET) {
55
+
recv_bucket = BUCKETS
56
+
} else {
57
+
recv_bucket = Math.min(Math.floor(received_dt / BUCKET_WIDTH), MAX_BUCKET)
58
+
}
59
+
60
+
setBuckets(({ idx, recv }) => {
61
+
idx = idx.slice();
62
+
recv = recv.slice();
63
+
idx[idx_bucket + 1] += 1;
64
+
recv[recv_bucket + 1] += 1;
65
+
return { idx, recv };
66
+
});
67
+
}
68
+
} catch (e) {}
69
+
}
20
70
};
21
71
const firehose = new Firehose({ relay: url });
22
72
firehose.on('open', () => setState('connected'));
···
38
88
return () => {
39
89
firehose.close();
40
90
};
41
-
}, [url]);
91
+
}, [url, includeEvents]);
42
92
43
93
return (
44
94
<div className="relay">
···
48
98
{(reconnects > 0) && (
49
99
<p>reconnects: <code>{reconnects}</code></p>
50
100
)}
101
+
<BarChart
102
+
height={180}
103
+
width={420}
104
+
yAxis={[{
105
+
label: 'events',
106
+
scaleType: 'symlog',
107
+
}]}
108
+
skipAnimation={true}
109
+
xAxis={[{
110
+
data: [-1]
111
+
.concat(Array.from({ length: BUCKETS }).map((_, i) => i * BUCKET_WIDTH))
112
+
.concat(['+']),
113
+
label: 'index latency (ms)',
114
+
}]}
115
+
series={[{
116
+
data: buckets.idx,
117
+
}]}
118
+
/>
119
+
<BarChart
120
+
height={180}
121
+
width={420}
122
+
yAxis={[{
123
+
label: 'events',
124
+
scaleType: 'symlog',
125
+
}]}
126
+
skipAnimation={true}
127
+
xAxis={[{
128
+
data: [-1]
129
+
.concat(Array.from({ length: BUCKETS }).map((_, i) => i * BUCKET_WIDTH))
130
+
.concat(['+']),
131
+
label: 'receive latency (ms)',
132
+
}]}
133
+
series={[{
134
+
data: buckets.recv,
135
+
}]}
136
+
/>
51
137
</div>
52
138
);
53
139
}