mirror of
https://github.com/dergigi/boris.git
synced 2025-12-16 22:24:25 +01:00
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:
72
src/App.tsx
72
src/App.tsx
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user