Normalize relay URLs to prevent duplicate WebSocket connections

The deduplication was comparing raw URL strings, so
'wss://relay.ditto.pub' and 'wss://relay.ditto.pub/' were treated
as different relays, each getting their own WebSocket connection.

- Normalize URLs (lowercase, strip trailing slashes) before comparing
- Strip trailing slashes from NIP-65 relay URLs when syncing
- Deduplicate user-only relay lists too, not just merged lists

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy
2026-02-18 00:13:50 -06:00
parent 4fdd596cbc
commit 9ca82c3431
2 changed files with 34 additions and 8 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ export function NostrSync() {
const fetchedRelays = event.tags
.filter(([name]) => name === 'r')
.map(([_, url, marker]) => ({
url,
url: url.replace(/\/+$/, ''),
read: !marker || marker === 'read',
write: !marker || marker === 'write',
}));
+33 -7
View File
@@ -1,5 +1,10 @@
import type { RelayMetadata } from '@/contexts/AppContext';
/** Normalize a relay URL for deduplication (lowercase, strip trailing slash). */
function normalizeUrl(url: string): string {
return url.toLowerCase().replace(/\/+$/, '');
}
/**
* App default relays that are used as a fallback when the user has no NIP-65 relay list,
* and can be optionally combined with user relays.
@@ -24,18 +29,39 @@ export function getEffectiveRelays(
useAppRelays: boolean
): RelayMetadata {
if (!useAppRelays) {
return userRelays;
return deduplicateRelays(userRelays);
}
// Merge app relays with user relays, avoiding duplicates
const appRelayUrls = new Set(APP_RELAYS.relays.map(r => r.url));
const mergedRelays = [
...APP_RELAYS.relays,
...userRelays.relays.filter(r => !appRelayUrls.has(r.url)),
];
// Merge app relays with user relays, avoiding duplicates by normalized URL
const seen = new Set<string>();
const mergedRelays = [];
for (const relay of [...APP_RELAYS.relays, ...userRelays.relays]) {
const normalized = normalizeUrl(relay.url);
if (!seen.has(normalized)) {
seen.add(normalized);
mergedRelays.push(relay);
}
}
return {
relays: mergedRelays,
updatedAt: userRelays.updatedAt,
};
}
/** Deduplicate relays within a single list by normalized URL. */
function deduplicateRelays(metadata: RelayMetadata): RelayMetadata {
const seen = new Set<string>();
const relays = [];
for (const relay of metadata.relays) {
const normalized = normalizeUrl(relay.url);
if (!seen.has(normalized)) {
seen.add(normalized);
relays.push(relay);
}
}
return { relays, updatedAt: metadata.updatedAt };
}