mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +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
|
// Handle user relay list and blocked relays when account changes
|
||||||
const userRelaysSub = accounts.active$.subscribe((account) => {
|
const userRelaysSub = accounts.active$.subscribe((account) => {
|
||||||
if (account) {
|
if (account) {
|
||||||
@@ -604,20 +629,6 @@ function App() {
|
|||||||
// Apply initial set immediately
|
// Apply initial set immediately
|
||||||
applyRelaySetToPool(pool, initialRelays)
|
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
|
// Begin loading blocked relays in background
|
||||||
const blockedPromise = loadBlockedRelays(pool, pubkey)
|
const blockedPromise = loadBlockedRelays(pool, pubkey)
|
||||||
|
|
||||||
@@ -649,43 +660,16 @@ function App() {
|
|||||||
applyRelaySetToPool(pool, finalRelays)
|
applyRelaySetToPool(pool, finalRelays)
|
||||||
|
|
||||||
updateKeepAlive()
|
updateKeepAlive()
|
||||||
|
updateAddressLoader()
|
||||||
// Update address loader with new relays
|
|
||||||
const activeRelays = getActiveRelayUrls(pool)
|
|
||||||
const addressLoader = createAddressLoader(pool, {
|
|
||||||
eventStore: store,
|
|
||||||
lookupRelays: activeRelays
|
|
||||||
})
|
|
||||||
store.addressableLoader = addressLoader
|
|
||||||
store.replaceableLoader = addressLoader
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('[relay-init] Failed to load user relay list (continuing with initial set):', 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
|
// Continue with initial relay set on error - no need to change anything
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// User logged out - reset to hardcoded relays
|
// User logged out - reset to hardcoded relays
|
||||||
|
|
||||||
applyRelaySetToPool(pool, RELAYS)
|
applyRelaySetToPool(pool, RELAYS)
|
||||||
|
updateKeepAlive(RELAYS)
|
||||||
|
updateAddressLoader(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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Hooks } from 'applesauce-react'
|
|||||||
import { onSyncStateChange, isEventSyncing, isEventOfflineCreated } from '../services/offlineSyncService'
|
import { onSyncStateChange, isEventSyncing, isEventOfflineCreated } from '../services/offlineSyncService'
|
||||||
import { areAllRelaysLocal, isLocalRelay } from '../utils/helpers'
|
import { areAllRelaysLocal, isLocalRelay } from '../utils/helpers'
|
||||||
import { getActiveRelayUrls } from '../services/relayManager'
|
import { getActiveRelayUrls } from '../services/relayManager'
|
||||||
import { isContentRelay } from '../config/relays'
|
import { isContentRelay, getContentRelays, getFallbackContentRelays } from '../config/relays'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import { formatDateCompact } from '../utils/bookmarkUtils'
|
import { formatDateCompact } from '../utils/bookmarkUtils'
|
||||||
import { createDeletionRequest } from '../services/deletionService'
|
import { createDeletionRequest } from '../services/deletionService'
|
||||||
@@ -225,23 +225,36 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
|
|||||||
|
|
||||||
const getHighlightLinks = () => {
|
const getHighlightLinks = () => {
|
||||||
// Encode the highlight event itself (kind 9802) as a nevent
|
// 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 publishedRelays = highlight.publishedRelays || []
|
||||||
const seenOnRelays = highlight.seenOnRelays || []
|
const seenOnRelays = highlight.seenOnRelays || []
|
||||||
|
|
||||||
// Use published relays if available, else seen relays, else fall back to active relays
|
// Determine base candidates: prefer published, then seen, then configured relays
|
||||||
const baseRelays = publishedRelays.length > 0
|
let candidates: string[]
|
||||||
? publishedRelays
|
if (publishedRelays.length > 0) {
|
||||||
: (seenOnRelays.length > 0 ? seenOnRelays : [])
|
// 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) : []
|
// Filter to content-capable remote relays (exclude local and non-content relays)
|
||||||
const candidates = baseRelays.length > 0 ? baseRelays : activeRelays
|
// Then take up to 3 for relay hints
|
||||||
|
|
||||||
// Filter to content-capable remote relays
|
|
||||||
const relayHints = candidates
|
const relayHints = candidates
|
||||||
.filter(url => !isLocalRelay(url))
|
.filter(url => !isLocalRelay(url))
|
||||||
.filter(url => isContentRelay(url))
|
.filter(url => isContentRelay(url))
|
||||||
.slice(0, 3) // Include up to 3 relay hints
|
.slice(0, 3)
|
||||||
|
|
||||||
const nevent = nip19.neventEncode({
|
const nevent = nip19.neventEncode({
|
||||||
id: highlight.id,
|
id: highlight.id,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { normalizeRelayUrl } from '../utils/helpers'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized relay configuration
|
* Centralized relay configuration
|
||||||
* Single set of relays used throughout the application
|
* 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
|
* Returns true for relays that are reasonable for posts/highlights
|
||||||
*/
|
*/
|
||||||
export function isContentRelay(url: string): boolean {
|
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[] = []
|
let events: NostrEvent[] = []
|
||||||
|
|
||||||
// First, try relay hints from naddr (primary source)
|
// Build unified relay set: hints + configured content relays
|
||||||
// Filter to only content relays to avoid using auth/signer relays
|
// Filter hinted relays to only content-capable relays
|
||||||
const hintedRelays = (pointer.relays && pointer.relays.length > 0)
|
const hintedRelays = (pointer.relays && pointer.relays.length > 0)
|
||||||
? pointer.relays.filter(isContentRelay)
|
? pointer.relays.filter(isContentRelay)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
if (hintedRelays.length > 0) {
|
// Get configured content relays
|
||||||
const orderedHintedRelays = prioritizeLocalRelays(hintedRelays)
|
const contentRelays = getContentRelays()
|
||||||
const { local: localHinted, remote: remoteHinted } = partitionRelays(orderedHintedRelays)
|
|
||||||
|
// 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(
|
const { local$, remote$ } = createParallelReqStreams(
|
||||||
relayPool,
|
relayPool,
|
||||||
localHinted,
|
localUnified,
|
||||||
remoteHinted,
|
remoteUnified,
|
||||||
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,
|
|
||||||
filter,
|
filter,
|
||||||
1200,
|
1200,
|
||||||
6000
|
6000
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { queryEvents } from './dataFetch'
|
import { queryEvents } from './dataFetch'
|
||||||
|
import { normalizeRelayUrl } from '../utils/helpers'
|
||||||
|
|
||||||
export interface UserRelayInfo {
|
export interface UserRelayInfo {
|
||||||
url: string
|
url: string
|
||||||
@@ -144,35 +145,55 @@ export function computeRelaySet(params: {
|
|||||||
alwaysIncludeLocal
|
alwaysIncludeLocal
|
||||||
} = params
|
} = 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 relaySet = new Set<string>()
|
||||||
const blockedSet = new Set(blocked)
|
const normalizedRelaySet = new Set<string>()
|
||||||
|
|
||||||
// Helper to check if relay should be included
|
// Helper to check if relay should be included (using normalized URLs)
|
||||||
const shouldInclude = (url: string): boolean => {
|
const shouldInclude = (normalizedUrl: string): boolean => {
|
||||||
// Always include local relays
|
// Always include local relays
|
||||||
if (alwaysIncludeLocal.includes(url)) return true
|
if (normalizedLocal.has(normalizedUrl)) return true
|
||||||
// Otherwise check if blocked
|
// Otherwise check if blocked
|
||||||
return !blockedSet.has(url)
|
return !normalizedBlocked.has(normalizedUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add hardcoded relays
|
// Add hardcoded relays (normalized)
|
||||||
for (const url of hardcoded) {
|
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) {
|
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) {
|
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) {
|
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)
|
return Array.from(relaySet)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { prioritizeLocalRelays } from '../utils/helpers'
|
import { prioritizeLocalRelays, normalizeRelayUrl } from '../utils/helpers'
|
||||||
import { getLocalRelays } from '../config/relays'
|
import { getLocalRelays, getFallbackContentRelays } from '../config/relays'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local relays that are always included
|
* 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)
|
* Hardcoded relays that are always included (minimal reliable set)
|
||||||
|
* Derived from RELAY_CONFIGS fallback relays
|
||||||
*/
|
*/
|
||||||
export const HARDCODED_RELAYS = [
|
export const HARDCODED_RELAYS = getFallbackContentRelays()
|
||||||
'wss://relay.nostr.band'
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets active relay URLs from the relay pool
|
* Gets active relay URLs from the relay pool
|
||||||
@@ -22,24 +21,6 @@ export function getActiveRelayUrls(relayPool: RelayPool): string[] {
|
|||||||
return prioritizeLocalRelays(urls)
|
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 {
|
export interface RelaySetChangeSummary {
|
||||||
added: string[]
|
added: string[]
|
||||||
removed: string[]
|
removed: string[]
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ export const classifyUrl = (url: string | undefined): UrlClassification => {
|
|||||||
return { type: 'article' }
|
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)
|
* Checks if a relay URL is a local relay (localhost or 127.0.0.1)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user