Skip to content

Commit 4879de2

Browse files
authored
feat(peers): updated ipfs-geoip and Peers screen optimizations (#2480)
* chore(deps): update ipfs-geoip to 9.3.0 * perf(peers): make geoip selectors synchronous, reduce re-renders - peer-locations.js: replace p-memoize getPublicIP with sync ref-cached version, make isPrivateAndNearby/selectPeerLocationsForSwarm/selectPeersCoordinates sync, add in-memory cache to PeerLocationResolver to skip IndexedDB reads, bump poll interval from 1s to 3s, add queue.onIdle() re-fetch trigger - PeersTable.js: remove useState/useEffect async wrapper, consume array directly - WorldMap.js: remove async wrappers from MapPins and PeerInfo, fix resize useEffect missing dependency array - peer-locations.test.js: remove unnecessary await from sync selector calls * feat(peers): enable geoip lookups for IPv6 peers - peer-locations.js: replace `isNonHomeIPv4` with `isPublicIP` to accept both protocol 4 (IPv4) and 41 (IPv6), using `ip.isPrivate()` to skip all private/loopback/link-local addresses for both families - peer-locations.js: rename `ipv4Tuple`/`ipv4Addr` to `ipTuple`/`ipAddr` since the filter now matches both IP versions - peer-locations.test.js: add tests for IPv6 peer location resolution, private IPv6/IPv4 `isPrivate` flag, and private IP filtering in `findLocations` - peer-locations.test.js: fix `optimizedPeerSet` IP generation to use only public addresses (old pattern produced 10.x and 127.x) * fix(peers): use HLRU for geoip memory cache to prevent unbounded growth - peer-locations.js: swap `new Map()` for `HLRU(500)`, matching `failedAddrs` capacity * perf(peers): progressive geoip rendering on initial load - guard getPromise against null peers to prevent crash during ramp-up - chain immediate re-fetches during optimizedPeerSet ramp-up (10→100→200→all) instead of waiting 3s staleAfter between each pass - replace onIdle handler with throttled completed event listener so uncached geoip lookups render progressively as they land * perf(peers): skip redundant re-renders when geoip data is unchanged - guard null peers with quick 100ms retry instead of waiting 3s staleAfter - shallow-compare new locations with previous data and return the same reference when unchanged, preventing the full selector/render cascade (selectPeerLocationsForSwarm → selectPeersCoordinates → MapPins D3 rebuild) * perf(peers): reduce unnecessary re-renders on peers page - WorldMap: use useRef for selectedTimeout instead of useState every mouse leave was calling setSelectedTimeout which re-rendered WorldMap, recreated handleMapPinMouseEnter (selectedTimeout in deps), passed new prop to MapPins, triggering full D3 SVG rebuild. timeout ID is bookkeeping, not display state -- useRef avoids renders. - WorldMap: memoize GeoPath d3 projection with useMemo([width, height]) every mouse hover changed selectedPeers, re-rendered WorldMap, re-rendered GeoPath which created a new d3.geoPath() reference, passed as new prop to MapPins triggering SVG rebuild. now stable unless window is resized. - peer-locations: remove unused selectBootstrapPeers from selectPeerLocationsForSwarm. reselect recomputed the selector (mapping all 298 peers) whenever bootstrap peers changed, even though bootstrapPeers was never referenced in the function body. * fix(peers): wrap peers fallback in useMemo to fix CI build react-hooks/exhaustive-deps flagged the `peerLocationsForSwarm || []` expression as unstable dependency for the filteredPeerList useMemo. CI treats warnings as errors, failing the build.
1 parent bc3856b commit 4879de2

File tree

6 files changed

+304
-98
lines changed

6 files changed

+304
-98
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"intl-messageformat": "^10.3.3",
6565
"ip": "^1.1.9",
6666
"ipfs-css": "^1.4.0",
67-
"ipfs-geoip": "^9.2.0",
67+
"ipfs-geoip": "^9.3.0",
6868
"ipfs-provider": "^2.1.0",
6969
"ipld-explorer-components": "^8.1.3",
7070
"is-ipfs": "^8.0.1",

src/bundles/peer-locations.js

Lines changed: 114 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import HLRU from 'hashlru'
66
import { multiaddr } from '@multiformats/multiaddr'
77
import ms from 'milliseconds'
88
import ip from 'ip'
9-
import memoize from 'p-memoize'
109
import { createContextSelector } from '../helpers/context-bridge'
1110
import pkgJson from '../../package.json'
1211

@@ -25,7 +24,7 @@ const selectIdentityData = () => {
2524

2625
// After this time interval, we re-check the locations for each peer
2726
// once again through PeerLocationResolver.
28-
const UPDATE_EVERY = ms.seconds(1)
27+
const UPDATE_EVERY = ms.seconds(3)
2928

3029
// We reuse cached geoip lookups as long geoipVersion is the same.
3130
const geoipVersion = dependencies['ipfs-geoip']
@@ -44,8 +43,68 @@ function createPeersLocations (opts) {
4443
const bundle = createAsyncResourceBundle({
4544
name: 'peerLocations',
4645
actionBaseType: 'PEER_LOCATIONS',
47-
getPromise: ({ store }) => peerLocResolver.findLocations(
48-
store.selectAvailableGatewayUrl(), store.selectPeers()),
46+
getPromise: ({ store }) => {
47+
const peers = store.selectPeers()
48+
if (!peers) {
49+
// Peers bundle hasn't loaded yet. Return empty result but schedule
50+
// a quick retry -- the reactor doesn't depend on selectPeers, so
51+
// without this we'd wait the full staleAfter (3s) before re-fetching.
52+
setTimeout(() => store.doMarkPeerLocationsAsOutdated(), 100)
53+
return Promise.resolve({})
54+
}
55+
56+
const promise = peerLocResolver.findLocations(
57+
store.selectAvailableGatewayUrl(), peers)
58+
59+
// While optimizedPeerSet is still ramping up (pass 0→10, 1→100, 2→200, 3→all),
60+
// chain an immediate re-fetch after the current one resolves instead of
61+
// waiting for staleAfter (3s) between each pass.
62+
if (peerLocResolver.pass <= 3) {
63+
promise.then(() => setTimeout(() => store.doMarkPeerLocationsAsOutdated(), 0))
64+
}
65+
66+
if (!peerLocResolver._completedHandler && peerLocResolver.queue.size > 0) {
67+
let throttleTimer = null
68+
let pendingUpdate = false
69+
70+
const throttledUpdate = () => {
71+
if (throttleTimer) {
72+
pendingUpdate = true
73+
return
74+
}
75+
store.doMarkPeerLocationsAsOutdated()
76+
throttleTimer = setTimeout(() => {
77+
throttleTimer = null
78+
if (pendingUpdate) {
79+
pendingUpdate = false
80+
throttledUpdate()
81+
}
82+
}, 500)
83+
}
84+
85+
peerLocResolver._completedHandler = throttledUpdate
86+
peerLocResolver.queue.on('completed', throttledUpdate)
87+
88+
peerLocResolver.queue.onIdle().then(() => {
89+
peerLocResolver.queue.off('completed', throttledUpdate)
90+
peerLocResolver._completedHandler = null
91+
clearTimeout(throttleTimer)
92+
store.doMarkPeerLocationsAsOutdated()
93+
})
94+
}
95+
96+
// Avoid unnecessary selector recomputation and re-renders: return
97+
// the previous data reference when nothing actually changed. This is
98+
// cheap because memoryCache (HLRU) returns the same value references
99+
// for the same IPs, so a shallow comparison suffices.
100+
return promise.then(newLocations => {
101+
const prev = store.selectPeerLocations()
102+
if (prev && shallowEqualObjects(prev, newLocations)) {
103+
return prev
104+
}
105+
return newLocations
106+
})
107+
},
49108
staleAfter: UPDATE_EVERY,
50109
retryAfter: UPDATE_EVERY,
51110
persist: false,
@@ -66,9 +125,8 @@ function createPeersLocations (opts) {
66125
bundle.selectPeerLocationsForSwarm = createSelector(
67126
'selectPeers',
68127
'selectPeerLocations',
69-
'selectBootstrapPeers',
70128
selectIdentityData, // ipfs.id info from identity context, used for detecting local peers
71-
(peers, locations = {}, bootstrapPeers, identity) => peers && Promise.all(peers.map(async (peer) => {
129+
(peers, locations = {}, identity) => peers && peers.map((peer) => {
72130
const peerId = peer.peer
73131
const locationObj = locations ? locations[peerId] : null
74132
const location = toLocationString(locationObj)
@@ -81,7 +139,7 @@ function createPeersLocations (opts) {
81139
const address = peer.addr.toString()
82140
const latency = parseLatency(peer.latency)
83141
const direction = peer.direction
84-
const { isPrivate, isNearby } = await isPrivateAndNearby(peer.addr, identity)
142+
const { isPrivate, isNearby } = isPrivateAndNearby(peer.addr, identity)
85143

86144
const protocols = (Array.isArray(peer.streams)
87145
? Array.from(new Set(peer.streams
@@ -107,18 +165,17 @@ function createPeersLocations (opts) {
107165
isNearby,
108166
agentVersion
109167
}
110-
}))
168+
})
111169
)
112170

113171
const COORDINATES_RADIUS = 4
114172

115173
bundle.selectPeersCoordinates = createSelector(
116174
'selectPeerLocationsForSwarm',
117-
async (peers) => {
175+
(peers) => {
118176
if (!peers) return []
119177

120-
const fetchedPeers = await peers
121-
return fetchedPeers.reduce((previous, { peerId, coordinates }) => {
178+
return peers.reduce((previous, { peerId, coordinates }) => {
122179
if (!coordinates) return previous
123180

124181
let hasFoundACloseCoordinate = false
@@ -152,7 +209,15 @@ function createPeersLocations (opts) {
152209
return bundle
153210
}
154211

155-
const isNonHomeIPv4 = t => t[0] === 4 && t[1] !== '127.0.0.1'
212+
const shallowEqualObjects = (a, b) => {
213+
const keysA = Object.keys(a)
214+
const keysB = Object.keys(b)
215+
if (keysA.length !== keysB.length) return false
216+
return keysA.every(key => a[key] === b[key])
217+
}
218+
219+
const isPublicIP = t =>
220+
(t[0] === 4 || t[0] === 41) && !ip.isPrivate(t[1])
156221

157222
const toLocationString = loc => {
158223
if (!loc) return null
@@ -178,27 +243,32 @@ const parseLatency = (latency) => {
178243
return value
179244
}
180245

181-
const getPublicIP = memoize((identity) => {
246+
let _cachedPublicIP
247+
let _lastIdentityRef
248+
249+
const getPublicIP = (identity) => {
182250
if (!identity) return
251+
if (identity === _lastIdentityRef) return _cachedPublicIP
252+
253+
_lastIdentityRef = identity
254+
_cachedPublicIP = undefined
183255

184256
for (const maddr of identity.addresses) {
185257
try {
186258
const addr = multiaddr(maddr).nodeAddress()
187259

188260
if ((ip.isV4Format(addr.address) || ip.isV6Format(addr.address)) && !ip.isPrivate(addr.address)) {
189-
return addr.address
261+
_cachedPublicIP = addr.address
262+
return _cachedPublicIP
190263
}
191264
} catch (e) {
192-
// TODO: We should provide a way to log these errors when debugging
193-
// if (['development', 'test'].includes(process.env.REACT_APP_ENV)) {
194-
// console.error(e)
195-
// }
265+
// Might fail for non-IP multiaddrs, safe to ignore.
196266
}
197267
}
198-
})
268+
}
199269

200-
const isPrivateAndNearby = async (maddr, identity) => {
201-
const publicIP = await getPublicIP(identity)
270+
const isPrivateAndNearby = (maddr, identity) => {
271+
const publicIP = getPublicIP(identity)
202272
let isPrivate = false
203273
let isNearby = false
204274
let addr
@@ -246,8 +316,10 @@ class PeerLocationResolver {
246316
})
247317

248318
this.geoipLookupPromises = new Map()
319+
this.memoryCache = HLRU(500)
249320

250321
this.pass = 0
322+
this._completedHandler = null
251323
}
252324

253325
async findLocations (gatewayUrls, peers) {
@@ -260,37 +332,46 @@ class PeerLocationResolver {
260332
for (const p of this.optimizedPeerSet(peers)) {
261333
const peerId = p.peer
262334

263-
const ipv4Tuple = p.addr.stringTuples().find(isNonHomeIPv4)
264-
if (!ipv4Tuple) {
335+
const ipTuple = p.addr.stringTuples().find(isPublicIP)
336+
if (!ipTuple) {
337+
continue
338+
}
339+
340+
const ipAddr = ipTuple[1]
341+
if (this.failedAddrs.has(ipAddr)) {
265342
continue
266343
}
267344

268-
const ipv4Addr = ipv4Tuple[1]
269-
if (this.failedAddrs.has(ipv4Addr)) {
345+
// check in-memory cache first (avoids IndexedDB reads for known IPs)
346+
const memoryCached = this.memoryCache.get(ipAddr)
347+
if (memoryCached) {
348+
res[peerId] = memoryCached
270349
continue
271350
}
272351

273-
// maybe we have it cached by ipv4 address already, check that.
274-
const location = await this.geoipCache.get(ipv4Addr)
352+
// maybe we have it cached by IP address in IndexedDB
353+
const location = await this.geoipCache.get(ipAddr)
275354
if (location) {
355+
this.memoryCache.set(ipAddr, location)
276356
res[peerId] = location
277357
continue
278358
}
279359

280360
// no ip address cached. are we looking it up already?
281-
if (this.geoipLookupPromises.has(ipv4Addr)) {
361+
if (this.geoipLookupPromises.has(ipAddr)) {
282362
continue
283363
}
284364

285-
this.geoipLookupPromises.set(ipv4Addr, this.queue.add(async () => {
365+
this.geoipLookupPromises.set(ipAddr, this.queue.add(async () => {
286366
try {
287-
const data = await lookup(gatewayUrls, ipv4Addr)
288-
await this.geoipCache.set(ipv4Addr, data)
367+
const data = await lookup(gatewayUrls, ipAddr)
368+
this.memoryCache.set(ipAddr, data)
369+
await this.geoipCache.set(ipAddr, data)
289370
} catch (e) {
290371
// mark this one as failed so we don't retry again
291-
this.failedAddrs.set(ipv4Addr, true)
372+
this.failedAddrs.set(ipAddr, true)
292373
} finally {
293-
this.geoipLookupPromises.delete(ipv4Addr)
374+
this.geoipLookupPromises.delete(ipAddr)
294375
}
295376
}))
296377
}

0 commit comments

Comments
 (0)