From 6ad2d37a2326cecd7a1585b4720bc5c13e27e99e Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Fri, 26 Jun 2026 13:43:40 -0400 Subject: [PATCH] =?UTF-8?q?Redesign=20handleGetDiscoveredRelays:=20use=20D?= =?UTF-8?q?exie=20cache=20for=20follows'=20kind=2010002=20events=20instead?= =?UTF-8?q?=20of=20ephemeral=20outbox=20tracker.=20Stable=20relay=E2=86=92?= =?UTF-8?q?follows=20mapping=20that=20only=20changes=20when=20contact=20li?= =?UTF-8?q?st=20changes.=20Live=20connection=20status=20from=20pool.=20Cac?= =?UTF-8?q?he=20result=20in=20worker=20to=20avoid=20re-querying=20Dexie=20?= =?UTF-8?q?on=20every=20expand.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- www/js/version.json | 6 +- www/ndk-worker.js | 152 +++++++++++++++++++++++++++++--------------- 2 files changed, 105 insertions(+), 53 deletions(-) diff --git a/www/js/version.json b/www/js/version.json index 0748877..0a3b834 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.51", - "VERSION_NUMBER": "0.7.51", - "BUILD_DATE": "2026-06-26T17:29:49.389Z" + "VERSION": "v0.7.52", + "VERSION_NUMBER": "0.7.52", + "BUILD_DATE": "2026-06-26T17:43:40.739Z" } diff --git a/www/ndk-worker.js b/www/ndk-worker.js index f5785f1..ca43b53 100644 --- a/www/ndk-worker.js +++ b/www/ndk-worker.js @@ -6443,73 +6443,125 @@ async function handleSetRelayEventLogging(requestId, enabled, port) { // from followed authors who write to relays the user does not subscribe to. // For each discovered relay we also report which followed pubkeys it serves, // by inverting ndk.outboxTracker.data (pubkey -> OutboxItem{writeRelays}). +// Cache for discovered relays — rebuilt only when the contact list changes. +let discoveredRelaysCache = null; +let discoveredRelaysCacheKey = ''; + async function handleGetDiscoveredRelays(requestId, port) { try { - if (!ndk?.pool?.relays) { - port.postMessage({ - type: 'getDiscoveredRelaysResult', - requestId, - relays: [] - }); + if (!ndk) { + port.postMessage({ type: 'getDiscoveredRelaysResult', requestId, relays: [] }); return; } // Build the set of the user's own relay URLs (normalized) from - // relayTypes (kind 10002). These are the "configured" relays; anything - // else in the pool is a discovered/temporary outbox relay. + // relayTypes (kind 10002). These are the "configured" relays. const ownRelays = new Set(); for (const url of relayTypes.keys()) { const normalized = normalizeRelayUrl(url); if (normalized) ownRelays.add(normalized); } - // Build relayUrl -> [pubkeys] map by inverting the outbox tracker. - // The typescript-lru-cache iteration API is broken (it yields millions - // of phantom keys from only a handful of real entries), so instead of - // iterating the LRU cache we query the NDK cache for the user's kind 3 - // contact list and then look up each followed pubkey by key. - const relayServesPubkeys = new Map(); // normalizedUrl -> Set - try { - const kind3Events = await ndk.fetchEvents( - { kinds: [3], authors: [currentPubkey], limit: 1 }, - { cacheUsage: 'ONLY_CACHE' } - ); - const latestKind3 = kind3Events && kind3Events.size > 0 - ? Array.from(kind3Events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0] - : null; - const followedPubkeys = latestKind3 ? extractFollowedPubkeysFromKind3(latestKind3) : []; + // Get the user's kind 3 contact list from the Dexie cache to + // determine the followed pubkeys. Use the contact list's created_at + // as a cache key so we only rebuild when it changes. + const kind3Events = await ndk.fetchEvents( + { kinds: [3], authors: [currentPubkey], limit: 1 }, + { cacheUsage: 'ONLY_CACHE' } + ); + const latestKind3 = kind3Events && kind3Events.size > 0 + ? Array.from(kind3Events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0] + : null; + const followedPubkeys = latestKind3 ? extractFollowedPubkeysFromKind3(latestKind3) : []; + const cacheKey = `${latestKind3?.id || 'none'}_${latestKind3?.created_at || 0}`; - for (const pubkey of followedPubkeys) { - try { - const outboxItem = ndk?.outboxTracker?.data?.get(pubkey); - if (!outboxItem?.writeRelays) continue; - for (const relayUrl of outboxItem.writeRelays) { - const normalized = normalizeRelayUrl(relayUrl); - if (!normalized) continue; - if (!relayServesPubkeys.has(normalized)) { - relayServesPubkeys.set(normalized, new Set()); + // Rebuild the relay→[pubkeys] mapping from follows' kind 10002 events + // in the Dexie cache if the contact list has changed. + if (discoveredRelaysCacheKey !== cacheKey) { + discoveredRelaysCacheKey = cacheKey; + discoveredRelaysCache = null; // invalidate + + if (followedPubkeys.length > 0) { + // Query all follows' kind 10002 events from the Dexie cache. + // This is the persistent data source — no 2-minute TTL like + // the outbox tracker. + const relayServesPubkeys = new Map(); // normalizedUrl -> Set + + // Process in batches of 400 to avoid filter size limits. + for (let i = 0; i < followedPubkeys.length; i += 400) { + const batch = followedPubkeys.slice(i, i + 400); + try { + const k10002Events = await ndk.fetchEvents( + { kinds: [10002], authors: batch }, + { cacheUsage: 'ONLY_CACHE' } + ); + if (k10002Events && k10002Events.size > 0) { + // For each author, keep only the latest kind 10002. + const latestByAuthor = new Map(); + for (const evt of k10002Events) { + const existing = latestByAuthor.get(evt.pubkey); + if (!existing || (evt.created_at || 0) > (existing.created_at || 0)) { + latestByAuthor.set(evt.pubkey, evt); + } + } + // Parse 'r' tags from each author's latest kind 10002. + for (const [pubkey, evt] of latestByAuthor) { + if (!Array.isArray(evt.tags)) continue; + for (const tag of evt.tags) { + if (tag[0] === 'r' && tag[1]) { + const relayType = tag[2] || 'both'; + // Include 'write' and 'both' relays — these are + // where the follow posts content. + if (relayType === 'write' || relayType === 'both') { + const normalized = normalizeRelayUrl(tag[1]); + if (!normalized) continue; + if (!relayServesPubkeys.has(normalized)) { + relayServesPubkeys.set(normalized, new Set()); + } + relayServesPubkeys.get(normalized).add(pubkey); + } + } + } + } } - relayServesPubkeys.get(normalized).add(pubkey); - } - } catch (_) {} + } catch (_) {} + } + + // Build the discovered relays list: relays that are NOT in the + // user's own kind 10002 but ARE in follows' kind 10002. + const relays = []; + for (const [relayUrl, servingSet] of relayServesPubkeys) { + if (ownRelays.has(relayUrl)) continue; // skip user's own relays + // Check live connection status from the pool. + const poolRelay = ndk.pool?.relays?.get(relayUrl); + relays.push({ + url: relayUrl, + status: poolRelay?.status || 0, + connected: poolRelay ? poolRelay.status >= 5 : false, + servingPubkeys: Array.from(servingSet) + }); + } + // Sort by serving count (most follows first), then by URL. + relays.sort((a, b) => { + const countDiff = b.servingPubkeys.length - a.servingPubkeys.length; + if (countDiff !== 0) return countDiff; + return a.url.localeCompare(b.url); + }); + discoveredRelaysCache = relays; } - } catch (err) { - console.warn('[Worker] getDiscoveredRelays: outbox tracker inversion failed:', err?.message || err); } - const relays = []; - for (const relay of ndk.pool.relays.values()) { - const normalized = normalizeRelayUrl(relay?.url); - if (!normalized) continue; - // Skip relays that are part of the user's own kind 10002 list. - if (ownRelays.has(normalized)) continue; - - const servingSet = relayServesPubkeys.get(normalized); - relays.push({ - url: relay.url, - status: relay.status, - connected: relay.status >= 5, - servingPubkeys: servingSet ? Array.from(servingSet) : [] + // If we have cached data, update the live connection status from the + // pool without rebuilding the entire mapping. + let relays = discoveredRelaysCache || []; + if (relays.length > 0 && ndk.pool?.relays) { + relays = relays.map(r => { + const poolRelay = ndk.pool.relays.get(r.url); + return { + ...r, + status: poolRelay?.status || r.status || 0, + connected: poolRelay ? poolRelay.status >= 5 : r.connected + }; }); }