+56
-37
package-lock.json
+56
-37
package-lock.json
···
8
"name": "firehose-diff",
9
"version": "0.0.0",
10
"dependencies": {
11
"@emotion/react": "^11.14.0",
12
"@emotion/styled": "^11.14.0",
13
"@mui/material": "^7.1.0",
14
-
"@mui/x-charts": "^8.3.1",
15
"@skyware/firehose": "^0.5.1",
16
"react": "^19.1.0",
17
"react-dom": "^19.1.0"
···
84
"dependencies": {
85
"@atcute/uint8array": "^1.0.1"
86
}
87
},
88
"node_modules/@atcute/uint8array": {
89
"version": "1.0.1",
···
316
}
317
},
318
"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==",
322
"license": "MIT",
323
"engines": {
324
"node": ">=6.9.0"
···
1403
}
1404
},
1405
"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==",
1409
"license": "MIT",
1410
"dependencies": {
1411
-
"@babel/runtime": "^7.27.1"
1412
},
1413
"peerDependencies": {
1414
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
···
1420
}
1421
},
1422
"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==",
1426
"license": "MIT",
1427
"dependencies": {
1428
-
"@babel/runtime": "^7.27.1",
1429
-
"@mui/types": "^7.4.2",
1430
-
"@types/prop-types": "^15.7.14",
1431
"clsx": "^2.1.1",
1432
"prop-types": "^15.8.1",
1433
-
"react-is": "^19.1.0"
1434
},
1435
"engines": {
1436
"node": ">=14.0.0"
···
1450
}
1451
},
1452
"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==",
1456
"license": "MIT",
1457
"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",
1462
"bezier-easing": "^2.1.0",
1463
"clsx": "^2.1.1",
1464
"prop-types": "^15.8.1",
···
1486
}
1487
},
1488
"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==",
1492
"license": "MIT AND ISC",
1493
"dependencies": {
1494
-
"@babel/runtime": "^7.27.1",
1495
"@types/d3-color": "^3.1.3",
1496
"@types/d3-delaunay": "^6.0.4",
1497
"@types/d3-interpolate": "^3.0.4",
···
1510
"robust-predicates": "^3.0.2"
1511
}
1512
},
1513
"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==",
1517
"license": "MIT",
1518
"dependencies": {
1519
-
"@babel/runtime": "^7.27.1",
1520
-
"@mui/utils": "^7.0.2"
1521
},
1522
"engines": {
1523
"node": ">=14.0.0"
···
1992
"license": "MIT"
1993
},
1994
"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==",
1998
"license": "MIT"
1999
},
2000
"node_modules/@types/react": {
···
4460
}
4461
},
4462
"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==",
4466
"license": "MIT"
4467
},
4468
"node_modules/react-refresh": {
···
8
"name": "firehose-diff",
9
"version": "0.0.0",
10
"dependencies": {
11
+
"@atcute/tid": "^1.0.2",
12
"@emotion/react": "^11.14.0",
13
"@emotion/styled": "^11.14.0",
14
"@mui/material": "^7.1.0",
15
+
"@mui/x-charts": "^8.10.2",
16
"@skyware/firehose": "^0.5.1",
17
"react": "^19.1.0",
18
"react-dom": "^19.1.0"
···
85
"dependencies": {
86
"@atcute/uint8array": "^1.0.1"
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"
94
},
95
"node_modules/@atcute/uint8array": {
96
"version": "1.0.1",
···
323
}
324
},
325
"node_modules/@babel/runtime": {
326
+
"version": "7.28.3",
327
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
328
+
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
329
"license": "MIT",
330
"engines": {
331
"node": ">=6.9.0"
···
1410
}
1411
},
1412
"node_modules/@mui/types": {
1413
+
"version": "7.4.5",
1414
+
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz",
1415
+
"integrity": "sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==",
1416
"license": "MIT",
1417
"dependencies": {
1418
+
"@babel/runtime": "^7.28.2"
1419
},
1420
"peerDependencies": {
1421
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
···
1427
}
1428
},
1429
"node_modules/@mui/utils": {
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==",
1433
"license": "MIT",
1434
"dependencies": {
1435
+
"@babel/runtime": "^7.28.2",
1436
+
"@mui/types": "^7.4.5",
1437
+
"@types/prop-types": "^15.7.15",
1438
"clsx": "^2.1.1",
1439
"prop-types": "^15.8.1",
1440
+
"react-is": "^19.1.1"
1441
},
1442
"engines": {
1443
"node": ">=14.0.0"
···
1457
}
1458
},
1459
"node_modules/@mui/x-charts": {
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==",
1463
"license": "MIT",
1464
"dependencies": {
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",
1470
"bezier-easing": "^2.1.0",
1471
"clsx": "^2.1.1",
1472
"prop-types": "^15.8.1",
···
1494
}
1495
},
1496
"node_modules/@mui/x-charts-vendor": {
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==",
1500
"license": "MIT AND ISC",
1501
"dependencies": {
1502
+
"@babel/runtime": "^7.27.6",
1503
"@types/d3-color": "^3.1.3",
1504
"@types/d3-delaunay": "^6.0.4",
1505
"@types/d3-interpolate": "^3.0.4",
···
1518
"robust-predicates": "^3.0.2"
1519
}
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
+
},
1530
"node_modules/@mui/x-internals": {
1531
+
"version": "8.10.2",
1532
+
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.10.2.tgz",
1533
+
"integrity": "sha512-dlC0BQRRBdiWtqn1yDppaHYRUjU3OuPWTxy0UtqxDaJjJf4pfR8ALr243nbxgJAFqvQyWPWyO4A6p9x9eJMJEQ==",
1534
"license": "MIT",
1535
"dependencies": {
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"
1540
},
1541
"engines": {
1542
"node": ">=14.0.0"
···
2011
"license": "MIT"
2012
},
2013
"node_modules/@types/prop-types": {
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==",
2017
"license": "MIT"
2018
},
2019
"node_modules/@types/react": {
···
4479
}
4480
},
4481
"node_modules/react-is": {
4482
+
"version": "19.1.1",
4483
+
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
4484
+
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
4485
"license": "MIT"
4486
},
4487
"node_modules/react-refresh": {
+2
-1
package.json
+2
-1
package.json
···
10
"preview": "vite preview"
11
},
12
"dependencies": {
13
+
"@atcute/tid": "^1.0.2",
14
"@emotion/react": "^11.14.0",
15
"@emotion/styled": "^11.14.0",
16
"@mui/material": "^7.1.0",
17
+
"@mui/x-charts": "^8.10.2",
18
"@skyware/firehose": "^0.5.1",
19
"react": "^19.1.0",
20
"react-dom": "^19.1.0"
+26
-1
src/App.tsx
+26
-1
src/App.tsx
···
28
29
function App() {
30
const [relays, setRelays] = useState([] as string[]);
31
const [receiver, setReceiver] = useState(() => noopReceiver);
32
const [keepalive, setKeepalive] = useState(() => () => {});
33
const [rateBars, setRateBars] = useState({ series: [] } as any);
···
143
return (
144
<>
145
<h1>compare hoses</h1>
146
-
<p><em>warning: enabling many relay connections requires a lot of bandwidth</em></p>
147
148
<form style={{ display: 'block', textAlign: 'left' }}>
149
{knownRelays.map(({ url, desc }: Relay) => (
···
193
</p>
194
</form>
195
196
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2em', textAlign: 'left' }}>
197
{relays.map(url => {
198
const { desc } = knownRelays.find((r: Relay) => r.url === url) ?? { desc: "custom relay" };
···
201
<Relay
202
url={url}
203
desc={desc}
204
onRecieveEvent={(type: string, event: any) => receiver(url, type, event)}
205
/>
206
</div>
···
28
29
function 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);
···
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) => (
···
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" };
···
225
<Relay
226
url={url}
227
desc={desc}
228
+
includeEvents={eventsSelected}
229
onRecieveEvent={(type: string, event: any) => receiver(url, type, event)}
230
/>
231
</div>
+89
-3
src/Relay.tsx
+89
-3
src/Relay.tsx
···
1
-
import { useEffect, useState } from 'react';
2
import { Firehose } from '@skyware/firehose';
3
import './Relay.css';
4
5
type HoseState = 'connecting' | 'connected' | 'errored' | 'closed';
6
7
-
function Relay({ url, desc, onRecieveEvent }: {
8
url: string,
9
desc: string,
10
onRecieveEvent: (type: string, event: any) => void,
11
}) {
12
const [state, setState] = useState('connecting' as HoseState);
13
const [commits, setCommits] = useState(0);
14
const [reconnects, setReconnects] = useState(0);
15
16
useEffect(() => {
17
const sendIt = (type: string, event: any) => {
18
onRecieveEvent(type, event);
19
setCommits(n => n + 1);
20
};
21
const firehose = new Firehose({ relay: url });
22
firehose.on('open', () => setState('connected'));
···
38
return () => {
39
firehose.close();
40
};
41
-
}, [url]);
42
43
return (
44
<div className="relay">
···
48
{(reconnects > 0) && (
49
<p>reconnects: <code>{reconnects}</code></p>
50
)}
51
</div>
52
);
53
}
···
1
+
import { useCallback, useEffect, useState } from 'react';
2
import { Firehose } from '@skyware/firehose';
3
+
import * as TID from '@atcute/tid';
4
+
import { BarChart } from '@mui/x-charts/BarChart';
5
import './Relay.css';
6
7
type HoseState = 'connecting' | 'connected' | 'errored' | 'closed';
8
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 }: {
15
url: string,
16
desc: string,
17
+
includeEvents: Set,
18
onRecieveEvent: (type: string, event: any) => void,
19
}) {
20
const [state, setState] = useState('connecting' as HoseState);
21
const [commits, setCommits] = useState(0);
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
+
});
27
28
useEffect(() => {
29
const sendIt = (type: string, event: any) => {
30
+
if (!includeEvents.has(type)) return;
31
onRecieveEvent(type, event);
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
+
}
70
};
71
const firehose = new Firehose({ relay: url });
72
firehose.on('open', () => setState('connected'));
···
88
return () => {
89
firehose.close();
90
};
91
+
}, [url, includeEvents]);
92
93
return (
94
<div className="relay">
···
98
{(reconnects > 0) && (
99
<p>reconnects: <code>{reconnects}</code></p>
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
+
/>
137
</div>
138
);
139
}