mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
refactor(relays): unify relay config with typed registry and improve hint usage
- Create typed RelayRole and RelayConfig interface in relays.ts - Add centralized RELAY_CONFIGS registry with role annotations - Add helper getters: getLocalRelays(), getDefaultRelays(), getContentRelays(), getFallbackContentRelays() - Maintain backwards compatibility with RELAYS and NON_CONTENT_RELAYS constants - Refactor relayManager to use new registry helpers - Harden applyRelaySetToPool with consistent normalization and local relay preservation - Add RelaySetChangeSummary return type for debugging - Improve articleService to prioritize and filter relay hints from naddr - Use centralized fallback content relay helpers instead of hard-coded arrays
This commit is contained in:
@@ -3,36 +3,95 @@
|
||||
* Single set of relays used throughout the application
|
||||
*/
|
||||
|
||||
// All relays including local relays
|
||||
export const RELAYS = [
|
||||
'ws://localhost:10547',
|
||||
'ws://localhost:4869',
|
||||
'wss://relay.nsec.app',
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://wot.dergigi.com',
|
||||
'wss://relay.snort.social',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
'wss://purplepag.es',
|
||||
'wss://relay.primal.net',
|
||||
'wss://proxy.nostr-relay.app/5d0d38afc49c4b84ca0da951a336affa18438efed302aeedfa92eb8b0d3fcb87',
|
||||
export type RelayRole = 'local-cache' | 'default' | 'fallback' | 'non-content' | 'bunker'
|
||||
|
||||
export interface RelayConfig {
|
||||
url: string
|
||||
roles: RelayRole[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Central relay registry with role annotations
|
||||
*/
|
||||
const RELAY_CONFIGS: RelayConfig[] = [
|
||||
{ url: 'ws://localhost:10547', roles: ['local-cache'] },
|
||||
{ url: 'ws://localhost:4869', roles: ['local-cache'] },
|
||||
{ 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'] },
|
||||
]
|
||||
|
||||
/**
|
||||
* Relays that should NOT be used as content hints (auth/signer, etc.)
|
||||
* These relays are fine for connection and other purposes, but shouldn't
|
||||
* be suggested as places where posts/highlights are likely to be found.
|
||||
* Get all local cache relays (localhost relays)
|
||||
*/
|
||||
export const NON_CONTENT_RELAYS = [
|
||||
'wss://relay.nsec.app',
|
||||
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 remote relays that are reasonable for posts/highlights
|
||||
* Returns true for relays that are reasonable for posts/highlights
|
||||
*/
|
||||
export function isContentRelay(url: string): boolean {
|
||||
return !NON_CONTENT_RELAYS.includes(url)
|
||||
return !getNonContentRelays().includes(url)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { nip19 } from 'nostr-tools'
|
||||
import { AddressPointer } from 'nostr-tools/nip19'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
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 { merge, toArray as rxToArray } from 'rxjs'
|
||||
import { UserSettings } from './settingsService'
|
||||
@@ -138,13 +138,6 @@ export async function fetchArticleByNaddr(
|
||||
|
||||
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
|
||||
const filter = {
|
||||
kinds: [pointer.kind],
|
||||
@@ -152,24 +145,59 @@ export async function fetchArticleByNaddr(
|
||||
'#d': [pointer.identifier]
|
||||
}
|
||||
|
||||
// Parallel local+remote, stream immediate, collect up to first from each
|
||||
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[]
|
||||
let events: NostrEvent[] = []
|
||||
|
||||
// Fallback: if nothing found, try a second round against a set of reliable public relays
|
||||
// First, try relay hints from naddr (primary source)
|
||||
// Filter to only content relays to avoid using auth/signer 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)
|
||||
|
||||
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 reliableRelays = Array.from(new Set<string>([
|
||||
'wss://relay.nostr.band',
|
||||
'wss://relay.primal.net',
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
...remoteRelays // keep any configured remote relays
|
||||
]))
|
||||
const defaultContentRelays = getContentRelays()
|
||||
const orderedDefault = prioritizeLocalRelays(defaultContentRelays)
|
||||
const { local: localDefault, remote: remoteDefault } = partitionRelays(orderedDefault)
|
||||
|
||||
const { local$, remote$ } = createParallelReqStreams(
|
||||
relayPool,
|
||||
localDefault,
|
||||
remoteDefault,
|
||||
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) {
|
||||
const fallbackRelays = getFallbackContentRelays()
|
||||
const { remote$: fallback$ } = createParallelReqStreams(
|
||||
relayPool,
|
||||
[], // no local
|
||||
reliableRelays,
|
||||
[], // no local for fallback
|
||||
fallbackRelays,
|
||||
filter,
|
||||
1500,
|
||||
12000
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { prioritizeLocalRelays } from '../utils/helpers'
|
||||
import { getLocalRelays, getDefaultRelays } from '../config/relays'
|
||||
|
||||
/**
|
||||
* Local relays that are always included
|
||||
*/
|
||||
export const ALWAYS_LOCAL_RELAYS = [
|
||||
'ws://localhost:10547',
|
||||
'ws://localhost:4869'
|
||||
]
|
||||
export const ALWAYS_LOCAL_RELAYS = getLocalRelays()
|
||||
|
||||
/**
|
||||
* Hardcoded relays that are always included
|
||||
* Hardcoded relays that are always included (minimal reliable set)
|
||||
*/
|
||||
export const HARDCODED_RELAYS = [
|
||||
'wss://relay.nostr.band'
|
||||
@@ -28,7 +26,7 @@ export function getActiveRelayUrls(relayPool: RelayPool): string[] {
|
||||
* Normalizes a relay URL to match what applesauce-relay stores internally
|
||||
* Adds trailing slash for URLs without a path
|
||||
*/
|
||||
function normalizeRelayUrl(url: string): string {
|
||||
export function normalizeRelayUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
// If the pathname is empty or just "/", ensure it ends with "/"
|
||||
@@ -42,58 +40,84 @@ function normalizeRelayUrl(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RelaySetChangeSummary {
|
||||
added: string[]
|
||||
removed: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
relayPool: RelayPool,
|
||||
finalUrls: string[]
|
||||
): void {
|
||||
// Normalize all URLs to match pool's internal format
|
||||
const currentUrls = new Set(Array.from(relayPool.relays.keys()))
|
||||
const normalizedTargetUrls = new Set(finalUrls.map(normalizeRelayUrl))
|
||||
finalUrls: string[],
|
||||
options?: { preserveAlwaysLocal?: boolean }
|
||||
): RelaySetChangeSummary {
|
||||
const preserveLocal = options?.preserveAlwaysLocal !== false // default true
|
||||
|
||||
// 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)
|
||||
const toAdd = finalUrls.filter(url => !currentUrls.has(normalizeRelayUrl(url)))
|
||||
// Map normalized URLs back to original for adding
|
||||
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) {
|
||||
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) {
|
||||
const relay = relayPool.relays.get(url)
|
||||
if (relay) {
|
||||
try {
|
||||
// Only close if relay is actually connected or attempting to connect
|
||||
// This helps avoid WebSocket warnings for connections that never started
|
||||
relay.close()
|
||||
} catch (error) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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 }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user