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
|
* 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'] },
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relays that should NOT be used as content hints (auth/signer, etc.)
|
* Get all local cache relays (localhost relays)
|
||||||
* These relays are fine for connection and other purposes, but shouldn't
|
|
||||||
* be suggested as places where posts/highlights are likely to be found.
|
|
||||||
*/
|
*/
|
||||||
export const NON_CONTENT_RELAYS = [
|
export function getLocalRelays(): string[] {
|
||||||
'wss://relay.nsec.app',
|
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
|
* 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 {
|
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 { 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,59 @@ 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
|
// 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) {
|
if (events.length === 0) {
|
||||||
const reliableRelays = Array.from(new Set<string>([
|
const defaultContentRelays = getContentRelays()
|
||||||
'wss://relay.nostr.band',
|
const orderedDefault = prioritizeLocalRelays(defaultContentRelays)
|
||||||
'wss://relay.primal.net',
|
const { local: localDefault, remote: remoteDefault } = partitionRelays(orderedDefault)
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
const { local$, remote$ } = createParallelReqStreams(
|
||||||
...remoteRelays // keep any configured remote relays
|
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(
|
const { remote$: fallback$ } = createParallelReqStreams(
|
||||||
relayPool,
|
relayPool,
|
||||||
[], // no local
|
[], // no local for fallback
|
||||||
reliableRelays,
|
fallbackRelays,
|
||||||
filter,
|
filter,
|
||||||
1500,
|
1500,
|
||||||
12000
|
12000
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { prioritizeLocalRelays } from '../utils/helpers'
|
import { prioritizeLocalRelays } from '../utils/helpers'
|
||||||
|
import { getLocalRelays, getDefaultRelays } 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)
|
||||||
*/
|
*/
|
||||||
export const HARDCODED_RELAYS = [
|
export const HARDCODED_RELAYS = [
|
||||||
'wss://relay.nostr.band'
|
'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
|
* Normalizes a relay URL to match what applesauce-relay stores internally
|
||||||
* Adds trailing slash for URLs without a path
|
* Adds trailing slash for URLs without a path
|
||||||
*/
|
*/
|
||||||
function normalizeRelayUrl(url: string): string {
|
export function normalizeRelayUrl(url: string): string {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url)
|
const parsed = new URL(url)
|
||||||
// If the pathname is empty or just "/", ensure it ends with "/"
|
// 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
|
* 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
|
||||||
// Add new relays (use original URLs for adding, not normalized)
|
const urlsWithLocal = preserveLocal
|
||||||
const toAdd = finalUrls.filter(url => !currentUrls.has(normalizeRelayUrl(url)))
|
? Array.from(new Set([...finalUrls, ...ALWAYS_LOCAL_RELAYS]))
|
||||||
|
: finalUrls
|
||||||
|
|
||||||
if (toAdd.length > 0) {
|
// Normalize all URLs consistently for comparison
|
||||||
relayPool.group(toAdd)
|
const normalizedCurrent = new Set(
|
||||||
|
Array.from(relayPool.relays.keys()).map(normalizeRelayUrl)
|
||||||
|
)
|
||||||
|
const normalizedTarget = new Set(urlsWithLocal.map(normalizeRelayUrl))
|
||||||
|
|
||||||
|
// Map normalized URLs back to original for adding
|
||||||
|
const normalizedToOriginal = new Map<string, string>()
|
||||||
|
for (const url of urlsWithLocal) {
|
||||||
|
normalizedToOriginal.set(normalizeRelayUrl(url), url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove relays not in target (but always keep local relays)
|
// 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[] = []
|
const toRemove: string[] = []
|
||||||
for (const url of currentUrls) {
|
for (const currentUrl of relayPool.relays.keys()) {
|
||||||
// Check if this normalized URL is in the target set
|
const normalizedCurrentUrl = normalizeRelayUrl(currentUrl)
|
||||||
if (!normalizedTargetUrls.has(url)) {
|
if (!normalizedTarget.has(normalizedCurrentUrl)) {
|
||||||
// Also check if it's a local relay (check both normalized and original forms)
|
// Always preserve local relays
|
||||||
const isLocal = ALWAYS_LOCAL_RELAYS.some(localUrl =>
|
if (!preserveLocal || !normalizedLocal.has(normalizedCurrentUrl)) {
|
||||||
normalizeRelayUrl(localUrl) === url || localUrl === url
|
toRemove.push(currentUrl)
|
||||||
)
|
|
||||||
if (!isLocal) {
|
|
||||||
toRemove.push(url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply changes
|
||||||
|
if (toAdd.length > 0) {
|
||||||
|
relayPool.group(toAdd)
|
||||||
|
}
|
||||||
|
|
||||||
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user