mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
Merge pull request #46 from dergigi/relay-hints
refactor: improve relay hint selection and relay management
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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { nip19 } from 'nostr-tools'
|
|||||||
import { getNostrUrl, getSearchUrl } from '../config/nostrGateways'
|
import { getNostrUrl, getSearchUrl } from '../config/nostrGateways'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { getActiveRelayUrls } from '../services/relayManager'
|
import { getActiveRelayUrls } from '../services/relayManager'
|
||||||
|
import { isContentRelay } from '../config/relays'
|
||||||
|
import { isLocalRelay } from '../utils/helpers'
|
||||||
import { IAccount } from 'applesauce-accounts'
|
import { IAccount } from 'applesauce-accounts'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { Highlight } from '../types/highlights'
|
import { Highlight } from '../types/highlights'
|
||||||
@@ -432,9 +434,10 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
|||||||
|
|
||||||
const dTag = currentArticle.tags.find(t => t[0] === 'd')?.[1] || ''
|
const dTag = currentArticle.tags.find(t => t[0] === 'd')?.[1] || ''
|
||||||
const activeRelays = relayPool ? getActiveRelayUrls(relayPool) : []
|
const activeRelays = relayPool ? getActiveRelayUrls(relayPool) : []
|
||||||
const relayHints = activeRelays.filter(r =>
|
const relayHints = activeRelays
|
||||||
!r.includes('localhost') && !r.includes('127.0.0.1')
|
.filter(url => !isLocalRelay(url))
|
||||||
).slice(0, 3)
|
.filter(url => isContentRelay(url))
|
||||||
|
.slice(0, 3)
|
||||||
|
|
||||||
const naddr = nip19.naddrEncode({
|
const naddr = nip19.naddrEncode({
|
||||||
kind: 30023,
|
kind: 30023,
|
||||||
|
|||||||
@@ -10,6 +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, 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'
|
||||||
@@ -224,11 +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
|
||||||
// Get non-local relays for the hint
|
// Relay hint selection priority:
|
||||||
const activeRelays = relayPool ? getActiveRelayUrls(relayPool) : []
|
// 1. Published relays (where we successfully published the event)
|
||||||
const relayHints = activeRelays.filter(r =>
|
// 2. Seen relays (where we observed the event)
|
||||||
!r.includes('localhost') && !r.includes('127.0.0.1')
|
// 3. Configured content relays (deterministic fallback)
|
||||||
).slice(0, 3) // Include up to 3 relay hints
|
// All candidates are deduplicated, filtered to content-capable remote relays, and limited to 3
|
||||||
|
|
||||||
|
const publishedRelays = highlight.publishedRelays || []
|
||||||
|
const seenOnRelays = highlight.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]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
const nevent = nip19.neventEncode({
|
const nevent = nip19.neventEncode({
|
||||||
id: highlight.id,
|
id: highlight.id,
|
||||||
|
|||||||
@@ -1,21 +1,101 @@
|
|||||||
|
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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// All relays including local relays
|
export type RelayRole = 'local-cache' | 'default' | 'fallback' | 'non-content' | 'bunker'
|
||||||
export const RELAYS = [
|
|
||||||
'ws://localhost:10547',
|
export interface RelayConfig {
|
||||||
'ws://localhost:4869',
|
url: string
|
||||||
'wss://relay.nsec.app',
|
roles: RelayRole[]
|
||||||
'wss://relay.damus.io',
|
}
|
||||||
'wss://nos.lol',
|
|
||||||
'wss://relay.nostr.band',
|
/**
|
||||||
'wss://wot.dergigi.com',
|
* Central relay registry with role annotations
|
||||||
'wss://relay.snort.social',
|
*/
|
||||||
'wss://nostr-pub.wellorder.net',
|
const RELAY_CONFIGS: RelayConfig[] = [
|
||||||
'wss://purplepag.es',
|
{ url: 'ws://localhost:10547', roles: ['local-cache'] },
|
||||||
'wss://relay.primal.net',
|
{ url: 'ws://localhost:4869', roles: ['local-cache'] },
|
||||||
'wss://proxy.nostr-relay.app/5d0d38afc49c4b84ca0da951a336affa18438efed302aeedfa92eb8b0d3fcb87',
|
{ url: 'wss://relay.nsec.app', roles: ['default', 'non-content'] },
|
||||||
|
{ url: 'wss://relay.damus.io', roles: ['default', 'fallback'] },
|
||||||
|
{ url: 'wss://nos.lol', roles: ['default', 'fallback'] },
|
||||||
|
{ url: 'wss://relay.nostr.band', roles: ['default', 'fallback'] },
|
||||||
|
{ url: 'wss://wot.dergigi.com', roles: ['default'] },
|
||||||
|
{ url: 'wss://relay.snort.social', roles: ['default'] },
|
||||||
|
{ url: 'wss://nostr-pub.wellorder.net', roles: ['default'] },
|
||||||
|
{ url: 'wss://purplepag.es', roles: ['default'] },
|
||||||
|
{ url: 'wss://relay.primal.net', roles: ['default', 'fallback'] },
|
||||||
|
{ url: 'wss://proxy.nostr-relay.app/5d0d38afc49c4b84ca0da951a336affa18438efed302aeedfa92eb8b0d3fcb87', roles: ['default'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all local cache relays (localhost relays)
|
||||||
|
*/
|
||||||
|
export function getLocalRelays(): string[] {
|
||||||
|
return RELAY_CONFIGS
|
||||||
|
.filter(config => config.roles.includes('local-cache'))
|
||||||
|
.map(config => config.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all default relays (main public relays)
|
||||||
|
*/
|
||||||
|
export function getDefaultRelays(): string[] {
|
||||||
|
return RELAY_CONFIGS
|
||||||
|
.filter(config => config.roles.includes('default'))
|
||||||
|
.map(config => config.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fallback content relays (last resort public relays for content fetching)
|
||||||
|
* These are reliable public relays that should be tried when other methods fail
|
||||||
|
*/
|
||||||
|
export function getFallbackContentRelays(): string[] {
|
||||||
|
return RELAY_CONFIGS
|
||||||
|
.filter(config => config.roles.includes('fallback'))
|
||||||
|
.map(config => config.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get relays suitable for content fetching (excludes non-content relays like auth/signer relays)
|
||||||
|
*/
|
||||||
|
export function getContentRelays(): string[] {
|
||||||
|
return RELAY_CONFIGS
|
||||||
|
.filter(config => !config.roles.includes('non-content'))
|
||||||
|
.map(config => config.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get relays that should NOT be used as content hints
|
||||||
|
*/
|
||||||
|
export function getNonContentRelays(): string[] {
|
||||||
|
return RELAY_CONFIGS
|
||||||
|
.filter(config => config.roles.includes('non-content'))
|
||||||
|
.map(config => config.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All relays including local relays (backwards compatibility)
|
||||||
|
*/
|
||||||
|
export const RELAYS = [
|
||||||
|
...getLocalRelays(),
|
||||||
|
...getDefaultRelays(),
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relays that should NOT be used as content hints (backwards compatibility)
|
||||||
|
*/
|
||||||
|
export const NON_CONTENT_RELAYS = getNonContentRelays()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a relay URL is suitable for use as a content hint
|
||||||
|
* Returns true for relays that are reasonable for posts/highlights
|
||||||
|
*/
|
||||||
|
export function isContentRelay(url: string): boolean {
|
||||||
|
const normalized = normalizeRelayUrl(url)
|
||||||
|
const nonContentRelays = getNonContentRelays().map(normalizeRelayUrl)
|
||||||
|
return !nonContentRelays.includes(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { nip19 } from 'nostr-tools'
|
|||||||
import { AddressPointer } from 'nostr-tools/nip19'
|
import { AddressPointer } from 'nostr-tools/nip19'
|
||||||
import { NostrEvent } from 'nostr-tools'
|
import { NostrEvent } from 'nostr-tools'
|
||||||
import { Helpers } from 'applesauce-core'
|
import { Helpers } from 'applesauce-core'
|
||||||
import { RELAYS } from '../config/relays'
|
import { getContentRelays, getFallbackContentRelays, isContentRelay } from '../config/relays'
|
||||||
import { prioritizeLocalRelays, partitionRelays, createParallelReqStreams } from '../utils/helpers'
|
import { prioritizeLocalRelays, partitionRelays, createParallelReqStreams } from '../utils/helpers'
|
||||||
import { merge, toArray as rxToArray } from 'rxjs'
|
import { merge, toArray as rxToArray } from 'rxjs'
|
||||||
import { UserSettings } from './settingsService'
|
import { UserSettings } from './settingsService'
|
||||||
@@ -138,13 +138,6 @@ export async function fetchArticleByNaddr(
|
|||||||
|
|
||||||
const pointer = decoded.data as AddressPointer
|
const pointer = decoded.data as AddressPointer
|
||||||
|
|
||||||
// Define relays to query - use union of relay hints from naddr and configured relays
|
|
||||||
// This avoids failures when naddr contains stale/unreachable relay hints
|
|
||||||
const hintedRelays = (pointer.relays && pointer.relays.length > 0) ? pointer.relays : []
|
|
||||||
const baseRelays = Array.from(new Set<string>([...hintedRelays, ...RELAYS]))
|
|
||||||
const orderedRelays = prioritizeLocalRelays(baseRelays)
|
|
||||||
const { local: localRelays, remote: remoteRelays } = partitionRelays(orderedRelays)
|
|
||||||
|
|
||||||
// Fetch the article event
|
// Fetch the article event
|
||||||
const filter = {
|
const filter = {
|
||||||
kinds: [pointer.kind],
|
kinds: [pointer.kind],
|
||||||
@@ -152,24 +145,45 @@ export async function fetchArticleByNaddr(
|
|||||||
'#d': [pointer.identifier]
|
'#d': [pointer.identifier]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parallel local+remote, stream immediate, collect up to first from each
|
let events: NostrEvent[] = []
|
||||||
const { local$, remote$ } = createParallelReqStreams(relayPool, localRelays, remoteRelays, filter, 1200, 6000)
|
|
||||||
const collected = await lastValueFrom(merge(local$.pipe(take(1)), remote$.pipe(take(1))).pipe(rxToArray()))
|
|
||||||
let events = collected as NostrEvent[]
|
|
||||||
|
|
||||||
// Fallback: if nothing found, try a second round against a set of reliable public 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)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
localUnified,
|
||||||
|
remoteUnified,
|
||||||
|
filter,
|
||||||
|
1200,
|
||||||
|
6000
|
||||||
|
)
|
||||||
|
const collected = await lastValueFrom(
|
||||||
|
merge(local$.pipe(take(1)), remote$.pipe(take(1))).pipe(rxToArray())
|
||||||
|
)
|
||||||
|
events = collected as NostrEvent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: try fallback content relays (most reliable public relays)
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
const reliableRelays = Array.from(new Set<string>([
|
const fallbackRelays = getFallbackContentRelays()
|
||||||
'wss://relay.nostr.band',
|
|
||||||
'wss://relay.primal.net',
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
|
||||||
...remoteRelays // keep any configured remote relays
|
|
||||||
]))
|
|
||||||
const { remote$: fallback$ } = createParallelReqStreams(
|
const { remote$: fallback$ } = createParallelReqStreams(
|
||||||
relayPool,
|
relayPool,
|
||||||
[], // no local
|
[], // no local for fallback
|
||||||
reliableRelays,
|
fallbackRelays,
|
||||||
filter,
|
filter,
|
||||||
1500,
|
1500,
|
||||||
12000
|
12000
|
||||||
|
|||||||
@@ -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,20 +1,17 @@
|
|||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { prioritizeLocalRelays } from '../utils/helpers'
|
import { prioritizeLocalRelays, normalizeRelayUrl } from '../utils/helpers'
|
||||||
|
import { getLocalRelays, getFallbackContentRelays } from '../config/relays'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local relays that are always included
|
* Local relays that are always included
|
||||||
*/
|
*/
|
||||||
export const ALWAYS_LOCAL_RELAYS = [
|
export const ALWAYS_LOCAL_RELAYS = getLocalRelays()
|
||||||
'ws://localhost:10547',
|
|
||||||
'ws://localhost:4869'
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hardcoded relays that are always included
|
* 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
|
||||||
@@ -24,76 +21,84 @@ export function getActiveRelayUrls(relayPool: RelayPool): string[] {
|
|||||||
return prioritizeLocalRelays(urls)
|
return prioritizeLocalRelays(urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface RelaySetChangeSummary {
|
||||||
* Normalizes a relay URL to match what applesauce-relay stores internally
|
added: string[]
|
||||||
* Adds trailing slash for URLs without a path
|
removed: string[]
|
||||||
*/
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a new relay set to the pool: adds missing relays, removes extras
|
* Applies a new relay set to the pool: adds missing relays, removes extras
|
||||||
|
* Always preserves local relays even if not in finalUrls
|
||||||
|
* @returns Summary of changes for debugging
|
||||||
*/
|
*/
|
||||||
export function applyRelaySetToPool(
|
export function applyRelaySetToPool(
|
||||||
relayPool: RelayPool,
|
relayPool: RelayPool,
|
||||||
finalUrls: string[]
|
finalUrls: string[],
|
||||||
): void {
|
options?: { preserveAlwaysLocal?: boolean }
|
||||||
// Normalize all URLs to match pool's internal format
|
): RelaySetChangeSummary {
|
||||||
const currentUrls = new Set(Array.from(relayPool.relays.keys()))
|
const preserveLocal = options?.preserveAlwaysLocal !== false // default true
|
||||||
const normalizedTargetUrls = new Set(finalUrls.map(normalizeRelayUrl))
|
|
||||||
|
|
||||||
|
// Ensure local relays are always included
|
||||||
|
const urlsWithLocal = preserveLocal
|
||||||
|
? Array.from(new Set([...finalUrls, ...ALWAYS_LOCAL_RELAYS]))
|
||||||
|
: finalUrls
|
||||||
|
|
||||||
|
// Normalize all URLs consistently for comparison
|
||||||
|
const normalizedCurrent = new Set(
|
||||||
|
Array.from(relayPool.relays.keys()).map(normalizeRelayUrl)
|
||||||
|
)
|
||||||
|
const normalizedTarget = new Set(urlsWithLocal.map(normalizeRelayUrl))
|
||||||
|
|
||||||
// Add new relays (use original URLs for adding, not normalized)
|
// Map normalized URLs back to original for adding
|
||||||
const toAdd = finalUrls.filter(url => !currentUrls.has(normalizeRelayUrl(url)))
|
const normalizedToOriginal = new Map<string, string>()
|
||||||
|
for (const url of urlsWithLocal) {
|
||||||
|
normalizedToOriginal.set(normalizeRelayUrl(url), url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find relays to add (not in current pool)
|
||||||
|
const toAdd: string[] = []
|
||||||
|
for (const normalizedUrl of normalizedTarget) {
|
||||||
|
if (!normalizedCurrent.has(normalizedUrl)) {
|
||||||
|
const originalUrl = normalizedToOriginal.get(normalizedUrl) || normalizedUrl
|
||||||
|
toAdd.push(originalUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find relays to remove (not in target, but preserve local relays)
|
||||||
|
const normalizedLocal = new Set(ALWAYS_LOCAL_RELAYS.map(normalizeRelayUrl))
|
||||||
|
const toRemove: string[] = []
|
||||||
|
for (const currentUrl of relayPool.relays.keys()) {
|
||||||
|
const normalizedCurrentUrl = normalizeRelayUrl(currentUrl)
|
||||||
|
if (!normalizedTarget.has(normalizedCurrentUrl)) {
|
||||||
|
// Always preserve local relays
|
||||||
|
if (!preserveLocal || !normalizedLocal.has(normalizedCurrentUrl)) {
|
||||||
|
toRemove.push(currentUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply changes
|
||||||
if (toAdd.length > 0) {
|
if (toAdd.length > 0) {
|
||||||
relayPool.group(toAdd)
|
relayPool.group(toAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove relays not in target (but always keep local relays)
|
|
||||||
const toRemove: string[] = []
|
|
||||||
for (const url of currentUrls) {
|
|
||||||
// Check if this normalized URL is in the target set
|
|
||||||
if (!normalizedTargetUrls.has(url)) {
|
|
||||||
// Also check if it's a local relay (check both normalized and original forms)
|
|
||||||
const isLocal = ALWAYS_LOCAL_RELAYS.some(localUrl =>
|
|
||||||
normalizeRelayUrl(localUrl) === url || localUrl === url
|
|
||||||
)
|
|
||||||
if (!isLocal) {
|
|
||||||
toRemove.push(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (const url of toRemove) {
|
for (const url of toRemove) {
|
||||||
const relay = relayPool.relays.get(url)
|
const relay = relayPool.relays.get(url)
|
||||||
if (relay) {
|
if (relay) {
|
||||||
try {
|
try {
|
||||||
// Only close if relay is actually connected or attempting to connect
|
|
||||||
// This helps avoid WebSocket warnings for connections that never started
|
|
||||||
relay.close()
|
relay.close()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Suppress errors when closing relays that haven't fully connected yet
|
// Suppress errors when closing relays that haven't fully connected yet
|
||||||
// This can happen when switching relay sets before connections establish
|
|
||||||
// Silently ignore
|
|
||||||
}
|
}
|
||||||
relayPool.relays.delete(url)
|
relayPool.relays.delete(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return summary for debugging (useful for understanding relay churn)
|
||||||
|
if (import.meta.env.DEV && (toAdd.length > 0 || toRemove.length > 0)) {
|
||||||
|
console.debug('[relay-pool] Changes:', { added: toAdd, removed: toRemove })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { added: toAdd, removed: toRemove }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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