refactor: improve relay hint selection and relay management

- Extract updateKeepAlive and updateAddressLoader helpers in App.tsx for better code reuse
- Improve relay hint selection in HighlightItem with priority: published > seen > configured relays
- Add URL normalization for consistent relay comparison across services
- Unify relay set approach in articleService (hints + configured relays together)
- Improve relay deduplication in relayListService using normalized URLs
- Move normalizeRelayUrl to helpers.ts for shared use
- Update isContentRelay to use normalized URLs for comparison
- Use getFallbackContentRelays for HARDCODED_RELAYS in relayManager
This commit is contained in:
Gigi
2025-11-22 01:16:10 +01:00
parent cc22524466
commit efb6b56c3b
7 changed files with 126 additions and 119 deletions

View File

@@ -576,6 +576,31 @@ function App() {
}
})
// Helper to update keep-alive subscription based on current active relays
const updateKeepAlive = (relayUrls?: string[]) => {
const poolWithSub = pool as unknown as { _keepAliveSubscription?: { unsubscribe: () => void } }
if (poolWithSub._keepAliveSubscription) {
poolWithSub._keepAliveSubscription.unsubscribe()
}
const targetRelays = relayUrls || getActiveRelayUrls(pool)
const newKeepAliveSub = pool.subscription(targetRelays, { kinds: [0], limit: 0 }).subscribe({
next: () => {},
error: () => {}
})
poolWithSub._keepAliveSubscription = newKeepAliveSub
}
// Helper to update address loader based on current active relays
const updateAddressLoader = (relayUrls?: string[]) => {
const targetRelays = relayUrls || getActiveRelayUrls(pool)
const addressLoader = createAddressLoader(pool, {
eventStore: store,
lookupRelays: targetRelays
})
store.addressableLoader = addressLoader
store.replaceableLoader = addressLoader
}
// Handle user relay list and blocked relays when account changes
const userRelaysSub = accounts.active$.subscribe((account) => {
if (account) {
@@ -604,20 +629,6 @@ function App() {
// Apply initial set immediately
applyRelaySetToPool(pool, initialRelays)
// Prepare keep-alive helper
const updateKeepAlive = () => {
const poolWithSub = pool as unknown as { _keepAliveSubscription?: { unsubscribe: () => void } }
if (poolWithSub._keepAliveSubscription) {
poolWithSub._keepAliveSubscription.unsubscribe()
}
const activeRelays = getActiveRelayUrls(pool)
const newKeepAliveSub = pool.subscription(activeRelays, { kinds: [0], limit: 0 }).subscribe({
next: () => {},
error: () => {}
})
poolWithSub._keepAliveSubscription = newKeepAliveSub
}
// Begin loading blocked relays in background
const blockedPromise = loadBlockedRelays(pool, pubkey)
@@ -649,43 +660,16 @@ function App() {
applyRelaySetToPool(pool, finalRelays)
updateKeepAlive()
// Update address loader with new relays
const activeRelays = getActiveRelayUrls(pool)
const addressLoader = createAddressLoader(pool, {
eventStore: store,
lookupRelays: activeRelays
})
store.addressableLoader = addressLoader
store.replaceableLoader = addressLoader
updateAddressLoader()
}).catch((error) => {
console.error('[relay-init] Failed to load user relay list (continuing with initial set):', error)
// Continue with initial relay set on error - no need to change anything
})
} else {
// User logged out - reset to hardcoded relays
applyRelaySetToPool(pool, RELAYS)
// Update keep-alive subscription
const poolWithSub = pool as unknown as { _keepAliveSubscription?: { unsubscribe: () => void } }
if (poolWithSub._keepAliveSubscription) {
poolWithSub._keepAliveSubscription.unsubscribe()
}
const newKeepAliveSub = pool.subscription(RELAYS, { kinds: [0], limit: 0 }).subscribe({
next: () => {},
error: () => {}
})
poolWithSub._keepAliveSubscription = newKeepAliveSub
// Reset address loader
const addressLoader = createAddressLoader(pool, {
eventStore: store,
lookupRelays: RELAYS
})
store.addressableLoader = addressLoader
store.replaceableLoader = addressLoader
updateKeepAlive(RELAYS)
updateAddressLoader(RELAYS)
}
})

View File

@@ -10,7 +10,7 @@ import { Hooks } from 'applesauce-react'
import { onSyncStateChange, isEventSyncing, isEventOfflineCreated } from '../services/offlineSyncService'
import { areAllRelaysLocal, isLocalRelay } from '../utils/helpers'
import { getActiveRelayUrls } from '../services/relayManager'
import { isContentRelay } from '../config/relays'
import { isContentRelay, getContentRelays, getFallbackContentRelays } from '../config/relays'
import { nip19 } from 'nostr-tools'
import { formatDateCompact } from '../utils/bookmarkUtils'
import { createDeletionRequest } from '../services/deletionService'
@@ -225,23 +225,36 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
const getHighlightLinks = () => {
// Encode the highlight event itself (kind 9802) as a nevent
// Prefer relays we actually published to or saw the event on
// Relay hint selection priority:
// 1. Published relays (where we successfully published the event)
// 2. Seen relays (where we observed the event)
// 3. Configured content relays (deterministic fallback)
// All candidates are deduplicated, filtered to content-capable remote relays, and limited to 3
const publishedRelays = highlight.publishedRelays || []
const seenOnRelays = highlight.seenOnRelays || []
// Use published relays if available, else seen relays, else fall back to active relays
const baseRelays = publishedRelays.length > 0
? publishedRelays
: (seenOnRelays.length > 0 ? seenOnRelays : [])
// Determine base candidates: prefer published, then seen, then configured relays
let candidates: string[]
if (publishedRelays.length > 0) {
// Prefer published relays, but include seen relays as backup
candidates = Array.from(new Set([...publishedRelays, ...seenOnRelays]))
.sort((a, b) => a.localeCompare(b))
} else if (seenOnRelays.length > 0) {
candidates = seenOnRelays
} else {
// Fallback to deterministic configured content relays
const contentRelays = getContentRelays()
const fallbackRelays = getFallbackContentRelays()
candidates = Array.from(new Set([...contentRelays, ...fallbackRelays]))
}
const activeRelays = relayPool ? getActiveRelayUrls(relayPool) : []
const candidates = baseRelays.length > 0 ? baseRelays : activeRelays
// Filter to content-capable remote relays
// Filter to content-capable remote relays (exclude local and non-content relays)
// Then take up to 3 for relay hints
const relayHints = candidates
.filter(url => !isLocalRelay(url))
.filter(url => isContentRelay(url))
.slice(0, 3) // Include up to 3 relay hints
.slice(0, 3)
const nevent = nip19.neventEncode({
id: highlight.id,

View File

@@ -1,3 +1,5 @@
import { normalizeRelayUrl } from '../utils/helpers'
/**
* Centralized relay configuration
* Single set of relays used throughout the application
@@ -92,6 +94,8 @@ export const NON_CONTENT_RELAYS = getNonContentRelays()
* Returns true for relays that are reasonable for posts/highlights
*/
export function isContentRelay(url: string): boolean {
return !getNonContentRelays().includes(url)
const normalized = normalizeRelayUrl(url)
const nonContentRelays = getNonContentRelays().map(normalizeRelayUrl)
return !nonContentRelays.includes(normalized)
}

View File

@@ -147,40 +147,26 @@ export async function fetchArticleByNaddr(
let events: NostrEvent[] = []
// First, try relay hints from naddr (primary source)
// Filter to only content relays to avoid using auth/signer relays
// Build unified relay set: hints + configured content relays
// Filter hinted relays to only content-capable relays
const hintedRelays = (pointer.relays && pointer.relays.length > 0)
? pointer.relays.filter(isContentRelay)
: []
if (hintedRelays.length > 0) {
const orderedHintedRelays = prioritizeLocalRelays(hintedRelays)
const { local: localHinted, remote: remoteHinted } = partitionRelays(orderedHintedRelays)
// Get configured content relays
const contentRelays = getContentRelays()
// Union of hinted and configured relays (deduplicated)
const unifiedRelays = Array.from(new Set([...hintedRelays, ...contentRelays]))
if (unifiedRelays.length > 0) {
const orderedUnified = prioritizeLocalRelays(unifiedRelays)
const { local: localUnified, remote: remoteUnified } = partitionRelays(orderedUnified)
const { local$, remote$ } = createParallelReqStreams(
relayPool,
localHinted,
remoteHinted,
filter,
1200,
6000
)
const collected = await lastValueFrom(
merge(local$.pipe(take(1)), remote$.pipe(take(1))).pipe(rxToArray())
)
events = collected as NostrEvent[]
}
// Fallback: if no hints or nothing found from hints, try default content relays
if (events.length === 0) {
const defaultContentRelays = getContentRelays()
const orderedDefault = prioritizeLocalRelays(defaultContentRelays)
const { local: localDefault, remote: remoteDefault } = partitionRelays(orderedDefault)
const { local$, remote$ } = createParallelReqStreams(
relayPool,
localDefault,
remoteDefault,
localUnified,
remoteUnified,
filter,
1200,
6000

View File

@@ -1,6 +1,7 @@
import { RelayPool } from 'applesauce-relay'
import { NostrEvent } from 'nostr-tools'
import { queryEvents } from './dataFetch'
import { normalizeRelayUrl } from '../utils/helpers'
export interface UserRelayInfo {
url: string
@@ -144,35 +145,55 @@ export function computeRelaySet(params: {
alwaysIncludeLocal
} = params
// Normalize all URLs for consistent comparison and deduplication
const normalizedBlocked = new Set(blocked.map(normalizeRelayUrl))
const normalizedLocal = new Set(alwaysIncludeLocal.map(normalizeRelayUrl))
const relaySet = new Set<string>()
const blockedSet = new Set(blocked)
const normalizedRelaySet = new Set<string>()
// Helper to check if relay should be included
const shouldInclude = (url: string): boolean => {
// Helper to check if relay should be included (using normalized URLs)
const shouldInclude = (normalizedUrl: string): boolean => {
// Always include local relays
if (alwaysIncludeLocal.includes(url)) return true
if (normalizedLocal.has(normalizedUrl)) return true
// Otherwise check if blocked
return !blockedSet.has(url)
return !normalizedBlocked.has(normalizedUrl)
}
// Add hardcoded relays
// Add hardcoded relays (normalized)
for (const url of hardcoded) {
if (shouldInclude(url)) relaySet.add(url)
const normalized = normalizeRelayUrl(url)
if (shouldInclude(normalized) && !normalizedRelaySet.has(normalized)) {
normalizedRelaySet.add(normalized)
relaySet.add(url) // Keep original URL for output
}
}
// Add bunker relays
// Add bunker relays (normalized)
for (const url of bunker) {
if (shouldInclude(url)) relaySet.add(url)
const normalized = normalizeRelayUrl(url)
if (shouldInclude(normalized) && !normalizedRelaySet.has(normalized)) {
normalizedRelaySet.add(normalized)
relaySet.add(url) // Keep original URL for output
}
}
// Add user relays (treating 'both' and 'read' as applicable for queries)
// Add user relays (normalized)
for (const relay of userList) {
if (shouldInclude(relay.url)) relaySet.add(relay.url)
const normalized = normalizeRelayUrl(relay.url)
if (shouldInclude(normalized) && !normalizedRelaySet.has(normalized)) {
normalizedRelaySet.add(normalized)
relaySet.add(relay.url) // Keep original URL for output
}
}
// Always ensure local relays are present
// Always ensure local relays are present (normalized check)
for (const url of alwaysIncludeLocal) {
relaySet.add(url)
const normalized = normalizeRelayUrl(url)
if (!normalizedRelaySet.has(normalized)) {
normalizedRelaySet.add(normalized)
relaySet.add(url) // Keep original URL for output
}
}
return Array.from(relaySet)

View File

@@ -1,6 +1,6 @@
import { RelayPool } from 'applesauce-relay'
import { prioritizeLocalRelays } from '../utils/helpers'
import { getLocalRelays } from '../config/relays'
import { prioritizeLocalRelays, normalizeRelayUrl } from '../utils/helpers'
import { getLocalRelays, getFallbackContentRelays } from '../config/relays'
/**
* Local relays that are always included
@@ -9,10 +9,9 @@ export const ALWAYS_LOCAL_RELAYS = getLocalRelays()
/**
* Hardcoded relays that are always included (minimal reliable set)
* Derived from RELAY_CONFIGS fallback relays
*/
export const HARDCODED_RELAYS = [
'wss://relay.nostr.band'
]
export const HARDCODED_RELAYS = getFallbackContentRelays()
/**
* Gets active relay URLs from the relay pool
@@ -22,24 +21,6 @@ export function getActiveRelayUrls(relayPool: RelayPool): string[] {
return prioritizeLocalRelays(urls)
}
/**
* Normalizes a relay URL to match what applesauce-relay stores internally
* Adds trailing slash for URLs without a path
*/
export function normalizeRelayUrl(url: string): string {
try {
const parsed = new URL(url)
// If the pathname is empty or just "/", ensure it ends with "/"
if (parsed.pathname === '' || parsed.pathname === '/') {
return url.endsWith('/') ? url : url + '/'
}
return url
} catch {
// If URL parsing fails, return as-is
return url
}
}
export interface RelaySetChangeSummary {
added: string[]
removed: string[]

View File

@@ -39,6 +39,24 @@ export const classifyUrl = (url: string | undefined): UrlClassification => {
return { type: 'article' }
}
/**
* Normalizes a relay URL to match what applesauce-relay stores internally
* Adds trailing slash for URLs without a path
*/
export function normalizeRelayUrl(url: string): string {
try {
const parsed = new URL(url)
// If the pathname is empty or just "/", ensure it ends with "/"
if (parsed.pathname === '' || parsed.pathname === '/') {
return url.endsWith('/') ? url : url + '/'
}
return url
} catch {
// If URL parsing fails, return as-is
return url
}
}
/**
* Checks if a relay URL is a local relay (localhost or 127.0.0.1)
*/