Compare commits

...

6 Commits

3 changed files with 197 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.50",
"VERSION_NUMBER": "0.7.50",
"BUILD_DATE": "2026-06-26T14:52:01.942Z"
"VERSION": "v0.7.56",
"VERSION_NUMBER": "0.7.56",
"BUILD_DATE": "2026-06-27T12:17:42.524Z"
}

View File

@@ -6443,73 +6443,200 @@ 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}).
function shortHexPubkey(hex) {
if (!hex || hex.length < 16) return hex || '-';
return `${hex.slice(0, 8)}${hex.slice(-4)}`;
}
// Cache for discovered relays — rebuilt only when the contact list changes.
// The version suffix forces a rebuild when the handler code changes (e.g.
// adding servingNames).
let discoveredRelaysCache = null;
let discoveredRelaysCacheKey = '';
const DISCOVERED_RELAYS_CACHE_VERSION = 'v3-names-k0';
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<pubkey>
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 = `${DISCOVERED_RELAYS_CACHE_VERSION}_${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<pubkey>
// 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);
}
}
}
}
}
} catch (_) {}
}
// Look up profile names for all serving pubkeys from the Dexie
// profiles table so we can display usernames instead of pubkeys.
// Look up profile names for all serving pubkeys.
const pubkeyToName = new Map();
const allServingPubkeys = new Set();
for (const servingSet of relayServesPubkeys.values()) {
for (const pk of servingSet) allServingPubkeys.add(pk);
}
// Strategy 1: Try Dexie profiles table.
try {
const adapter = ndk?.cacheAdapter?.adapter;
if (adapter?.db?.profiles) {
for (const pk of allServingPubkeys) {
try {
const row = await adapter.db.profiles.get(pk);
if (row) {
const name = row.displayName || row.name || '';
if (name) pubkeyToName.set(pk, name);
}
} catch (_) {}
}
relayServesPubkeys.get(normalized).add(pubkey);
}
} catch (_) {}
// Strategy 2: If Dexie profiles didn't have names for everyone,
// query kind 0 events from the NDK cache for the missing pubkeys.
const missingPubkeys = Array.from(allServingPubkeys).filter(pk => !pubkeyToName.has(pk));
if (missingPubkeys.length > 0) {
try {
// Query in batches of 400.
for (let i = 0; i < missingPubkeys.length; i += 400) {
const batch = missingPubkeys.slice(i, i + 400);
const k0Events = await ndk.fetchEvents(
{ kinds: [0], authors: batch },
{ cacheUsage: 'ONLY_CACHE' }
);
if (k0Events && k0Events.size > 0) {
// For each author, keep only the latest kind 0.
const latestByAuthor = new Map();
for (const evt of k0Events) {
const existing = latestByAuthor.get(evt.pubkey);
if (!existing || (evt.created_at || 0) > (existing.created_at || 0)) {
latestByAuthor.set(evt.pubkey, evt);
}
}
for (const [pubkey, evt] of latestByAuthor) {
try {
const profile = typeof evt.content === 'string'
? JSON.parse(evt.content) : evt.content;
const name = profile?.display_name || profile?.name || '';
if (name) pubkeyToName.set(pubkey, name);
} catch (_) {}
}
}
}
} catch (_) {}
}
console.log('[Worker] getDiscoveredRelays: profile names resolved for', pubkeyToName.size, 'of', allServingPubkeys.size, 'pubkeys');
// 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);
const pubkeys = Array.from(servingSet);
// Map pubkeys to names, falling back to truncated pubkey.
const names = pubkeys.map(pk =>
pubkeyToName.get(pk) || shortHexPubkey(pk)
);
relays.push({
url: relayUrl,
status: poolRelay?.status || 0,
connected: poolRelay ? poolRelay.status >= 5 : false,
servingPubkeys: pubkeys,
servingNames: names
});
}
// 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
};
});
}

View File

@@ -257,9 +257,9 @@
================================================================ -->
<div id="divBody" class="clsBodyFull">
<div id="divRelays">Loading relays...</div>
<div id="divDiscoveredRelaysWrap" style="width:95%;margin:10px auto 0 auto;">
<div id="divDiscoveredRelaysWrap" style="width:100%;margin:10px 0 0 0;overflow-x:auto;">
<div id="divDiscoveredRelaysTitle" style="font-size:120%;margin-bottom:6px;cursor:pointer;user-select:none;">▸ Discovered Relays (Outbox)</div>
<div id="divDiscoveredRelays" style="display:none;"></div>
<div id="divDiscoveredRelays" style="display:none;max-height:300px;overflow-y:auto;"></div>
</div>
<div id="divRelayEventsWrap">
<div id="divRelayEvents" class="relay-events-stream"></div>
@@ -1361,27 +1361,38 @@ const versionInfo = await getVersion();
return;
}
// Filter out relays with no serving pubkeys — if no follows use this
// relay, there's no reason to show it in the discovered relays list.
const relaysWithFollows = relays.filter(r =>
Array.isArray(r.servingPubkeys) && r.servingPubkeys.length > 0
);
if (relaysWithFollows.length === 0) {
container.innerHTML = '<div style="padding:8px;color:var(--muted-color);font-size:80%;">No discovered relays with active follow data. Load the feed page to populate outbox relays from your follows.</div>';
return;
}
let html = '<table style="width:100%;font-size:70%;border-collapse:collapse;">';
html += '<thead><tr>';
html += '<th class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;">Relay</th>';
html += '<th class="tblCol tblColCenter" style="border:0.1px solid var(--muted-color);padding:4px;">Connected</th>';
html += '<th class="tblCol tblColRight" style="border:0.1px solid var(--muted-color);padding:4px;">Serving</th>';
html += '<th class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;">Sample Follows</th>';
html += '<th class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;">Follows</th>';
html += '</tr></thead><tbody>';
for (const relay of relays) {
for (const relay of relaysWithFollows) {
const connected = relay.connected ? SVG_CHECKED : SVG_UNCHECKED;
const servingCount = Array.isArray(relay.servingPubkeys) ? relay.servingPubkeys.length : 0;
const sampleFollows = Array.isArray(relay.servingPubkeys)
? relay.servingPubkeys.slice(0, 3).map(shortNpub).join(', ')
: '-';
const followsList = Array.isArray(relay.servingNames) && relay.servingNames.length > 0
? relay.servingNames.join(', ')
: (Array.isArray(relay.servingPubkeys) ? relay.servingPubkeys.map(shortNpub).join(', ') : '-');
const rowClass = relay.connected ? 'tblRowConnected' : '';
html += `<tr class="${rowClass}">`;
html += `<td class="tblCol tblColLeft tblColRelay" style="border:0.1px solid var(--muted-color);padding:4px;" title="${relay.url}">${relay.url}</td>`;
html += `<td class="tblCol tblColCenter" style="border:0.1px solid var(--muted-color);padding:4px;">${connected}</td>`;
html += `<td class="tblCol tblColRight" style="border:0.1px solid var(--muted-color);padding:4px;">${servingCount}</td>`;
html += `<td class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;font-family:monospace;">${sampleFollows}</td>`;
html += `<td class="tblCol tblColLeft" style="border:0.1px solid var(--muted-color);padding:4px;font-family:monospace;word-break:break-all;">${followsList}</td>`;
html += '</tr>';
}