40 changed files with 1685 additions and 955 deletions
@ -1,78 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { type NDKUserProfile } from "@nostr-dev-kit/ndk"; |
|
||||||
import { |
|
||||||
activePubkey, |
|
||||||
loginWithExtension, |
|
||||||
ndkInstance, |
|
||||||
ndkSignedIn, |
|
||||||
persistLogin, |
|
||||||
} from "$lib/ndk"; |
|
||||||
import { Avatar, Button, Popover } from "flowbite-svelte"; |
|
||||||
import Profile from "$components/util/Profile.svelte"; |
|
||||||
|
|
||||||
let profile = $state<NDKUserProfile | null>(null); |
|
||||||
let npub = $state<string | undefined>(undefined); |
|
||||||
|
|
||||||
let signInFailed = $state<boolean>(false); |
|
||||||
let errorMessage = $state<string>(""); |
|
||||||
|
|
||||||
$effect(() => { |
|
||||||
if ($ndkSignedIn) { |
|
||||||
$ndkInstance |
|
||||||
.getUser({ pubkey: $activePubkey ?? undefined }) |
|
||||||
?.fetchProfile() |
|
||||||
.then((userProfile) => { |
|
||||||
profile = userProfile; |
|
||||||
}); |
|
||||||
npub = $ndkInstance.activeUser?.npub; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
async function handleSignInClick() { |
|
||||||
try { |
|
||||||
signInFailed = false; |
|
||||||
errorMessage = ""; |
|
||||||
|
|
||||||
const user = await loginWithExtension(); |
|
||||||
if (!user) { |
|
||||||
throw new Error("The NIP-07 extension did not return a user."); |
|
||||||
} |
|
||||||
|
|
||||||
profile = await user.fetchProfile(); |
|
||||||
persistLogin(user); |
|
||||||
} catch (e) { |
|
||||||
console.error(e); |
|
||||||
signInFailed = true; |
|
||||||
errorMessage = |
|
||||||
e instanceof Error ? e.message : "Failed to sign in. Please try again."; |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="m-4"> |
|
||||||
{#if $ndkSignedIn} |
|
||||||
<Profile pubkey={$activePubkey} isNav={true} /> |
|
||||||
{:else} |
|
||||||
<Avatar rounded class="h-6 w-6 cursor-pointer bg-transparent" id="avatar" /> |
|
||||||
<Popover |
|
||||||
class="popover-leather w-fit" |
|
||||||
placement="bottom" |
|
||||||
triggeredBy="#avatar" |
|
||||||
> |
|
||||||
<div class="w-full flex flex-col space-y-2"> |
|
||||||
<Button onclick={handleSignInClick}>Extension Sign-In</Button> |
|
||||||
{#if signInFailed} |
|
||||||
<div class="p-2 text-sm text-red-600 bg-red-100 rounded"> |
|
||||||
{errorMessage} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
<!-- <Button |
|
||||||
color='alternative' |
|
||||||
on:click={signInWithBunker} |
|
||||||
> |
|
||||||
Bunker Sign-In |
|
||||||
</Button> --> |
|
||||||
</div> |
|
||||||
</Popover> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
@ -0,0 +1,59 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import { networkCondition, isNetworkChecking, startNetworkStatusMonitoring } from '$lib/stores/networkStore'; |
||||||
|
import { NetworkCondition } from '$lib/utils/network_detection'; |
||||||
|
import { onMount } from 'svelte'; |
||||||
|
|
||||||
|
function getStatusColor(): string { |
||||||
|
switch ($networkCondition) { |
||||||
|
case NetworkCondition.ONLINE: |
||||||
|
return 'text-green-600 dark:text-green-400'; |
||||||
|
case NetworkCondition.SLOW: |
||||||
|
return 'text-yellow-600 dark:text-yellow-400'; |
||||||
|
case NetworkCondition.OFFLINE: |
||||||
|
return 'text-red-600 dark:text-red-400'; |
||||||
|
default: |
||||||
|
return 'text-gray-600 dark:text-gray-400'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getStatusIcon(): string { |
||||||
|
switch ($networkCondition) { |
||||||
|
case NetworkCondition.ONLINE: |
||||||
|
return '🟢'; |
||||||
|
case NetworkCondition.SLOW: |
||||||
|
return '🟡'; |
||||||
|
case NetworkCondition.OFFLINE: |
||||||
|
return '🔴'; |
||||||
|
default: |
||||||
|
return '⚪'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getStatusText(): string { |
||||||
|
switch ($networkCondition) { |
||||||
|
case NetworkCondition.ONLINE: |
||||||
|
return 'Online'; |
||||||
|
case NetworkCondition.SLOW: |
||||||
|
return 'Slow Connection'; |
||||||
|
case NetworkCondition.OFFLINE: |
||||||
|
return 'Offline'; |
||||||
|
default: |
||||||
|
return 'Unknown'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMount(() => { |
||||||
|
// Start centralized network monitoring |
||||||
|
startNetworkStatusMonitoring(); |
||||||
|
}); |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="flex items-center space-x-2 text-xs {getStatusColor()} font-medium"> |
||||||
|
{#if $isNetworkChecking} |
||||||
|
<span class="animate-spin">⏳</span> |
||||||
|
<span>Checking...</span> |
||||||
|
{:else} |
||||||
|
<span class="text-lg">{getStatusIcon()}</span> |
||||||
|
<span>{getStatusText()}</span> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
@ -0,0 +1,55 @@ |
|||||||
|
import { writable, type Writable } from 'svelte/store'; |
||||||
|
import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '$lib/utils/network_detection'; |
||||||
|
|
||||||
|
// Network status store
|
||||||
|
export const networkCondition = writable<NetworkCondition>(NetworkCondition.ONLINE); |
||||||
|
export const isNetworkChecking = writable<boolean>(false); |
||||||
|
|
||||||
|
// Network monitoring state
|
||||||
|
let stopNetworkMonitoring: (() => void) | null = null; |
||||||
|
|
||||||
|
/** |
||||||
|
* Starts network monitoring if not already running |
||||||
|
*/ |
||||||
|
export function startNetworkStatusMonitoring(): void { |
||||||
|
if (stopNetworkMonitoring) { |
||||||
|
return; // Already monitoring
|
||||||
|
} |
||||||
|
|
||||||
|
console.debug('[networkStore.ts] Starting network status monitoring'); |
||||||
|
|
||||||
|
stopNetworkMonitoring = startNetworkMonitoring( |
||||||
|
(condition: NetworkCondition) => { |
||||||
|
console.debug(`[networkStore.ts] Network condition changed to: ${condition}`); |
||||||
|
networkCondition.set(condition); |
||||||
|
}, |
||||||
|
60000 // Check every 60 seconds to reduce spam
|
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Stops network monitoring |
||||||
|
*/ |
||||||
|
export function stopNetworkStatusMonitoring(): void { |
||||||
|
if (stopNetworkMonitoring) { |
||||||
|
console.debug('[networkStore.ts] Stopping network status monitoring'); |
||||||
|
stopNetworkMonitoring(); |
||||||
|
stopNetworkMonitoring = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Manually check network status (for immediate updates) |
||||||
|
*/ |
||||||
|
export async function checkNetworkStatus(): Promise<void> { |
||||||
|
try { |
||||||
|
isNetworkChecking.set(true); |
||||||
|
const condition = await detectNetworkCondition(); |
||||||
|
networkCondition.set(condition); |
||||||
|
} catch (error) { |
||||||
|
console.warn('[networkStore.ts] Failed to check network status:', error); |
||||||
|
networkCondition.set(NetworkCondition.OFFLINE); |
||||||
|
} finally { |
||||||
|
isNetworkChecking.set(false); |
||||||
|
} |
||||||
|
}
|
||||||
@ -1,4 +0,0 @@ |
|||||||
import { writable } from "svelte/store"; |
|
||||||
|
|
||||||
// Initialize with empty array, will be populated from user preferences
|
|
||||||
export const userRelays = writable<string[]>([]); |
|
||||||
@ -0,0 +1,189 @@ |
|||||||
|
import { deduplicateRelayUrls } from './relay_management'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Network conditions for relay selection |
||||||
|
*/ |
||||||
|
export enum NetworkCondition { |
||||||
|
ONLINE = 'online', |
||||||
|
SLOW = 'slow', |
||||||
|
OFFLINE = 'offline' |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Network connectivity test endpoints |
||||||
|
*/ |
||||||
|
const NETWORK_ENDPOINTS = [ |
||||||
|
'https://www.google.com/favicon.ico', |
||||||
|
'https://httpbin.org/status/200', |
||||||
|
'https://api.github.com/zen' |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Detects if the network is online using more reliable endpoints |
||||||
|
* @returns Promise that resolves to true if online, false otherwise |
||||||
|
*/ |
||||||
|
export async function isNetworkOnline(): Promise<boolean> { |
||||||
|
for (const endpoint of NETWORK_ENDPOINTS) { |
||||||
|
try { |
||||||
|
// Use a simple fetch without HEAD method to avoid CORS issues
|
||||||
|
const response = await fetch(endpoint, { |
||||||
|
method: 'GET', |
||||||
|
cache: 'no-cache', |
||||||
|
signal: AbortSignal.timeout(3000), |
||||||
|
mode: 'no-cors' // Use no-cors mode to avoid CORS issues
|
||||||
|
}); |
||||||
|
// With no-cors mode, we can't check response.ok, so we assume success if no error
|
||||||
|
return true; |
||||||
|
} catch (error) { |
||||||
|
console.debug(`[network_detection.ts] Failed to reach ${endpoint}:`, error); |
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.debug('[network_detection.ts] All network endpoints failed'); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests network speed by measuring response time |
||||||
|
* @returns Promise that resolves to network speed in milliseconds |
||||||
|
*/ |
||||||
|
export async function testNetworkSpeed(): Promise<number> { |
||||||
|
const startTime = performance.now(); |
||||||
|
|
||||||
|
for (const endpoint of NETWORK_ENDPOINTS) { |
||||||
|
try { |
||||||
|
await fetch(endpoint, { |
||||||
|
method: 'GET', |
||||||
|
cache: 'no-cache', |
||||||
|
signal: AbortSignal.timeout(5000), |
||||||
|
mode: 'no-cors' // Use no-cors mode to avoid CORS issues
|
||||||
|
}); |
||||||
|
|
||||||
|
const endTime = performance.now(); |
||||||
|
return endTime - startTime; |
||||||
|
} catch (error) { |
||||||
|
console.debug(`[network_detection.ts] Speed test failed for ${endpoint}:`, error); |
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.debug('[network_detection.ts] Network speed test failed for all endpoints'); |
||||||
|
return Infinity; // Very slow if it fails
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determines network condition based on connectivity and speed |
||||||
|
* @returns Promise that resolves to NetworkCondition |
||||||
|
*/ |
||||||
|
export async function detectNetworkCondition(): Promise<NetworkCondition> { |
||||||
|
const isOnline = await isNetworkOnline(); |
||||||
|
|
||||||
|
if (!isOnline) { |
||||||
|
console.debug('[network_detection.ts] Network condition: OFFLINE'); |
||||||
|
return NetworkCondition.OFFLINE; |
||||||
|
} |
||||||
|
|
||||||
|
const speed = await testNetworkSpeed(); |
||||||
|
|
||||||
|
// Consider network slow if response time > 2000ms
|
||||||
|
if (speed > 2000) { |
||||||
|
console.debug(`[network_detection.ts] Network condition: SLOW (${speed.toFixed(0)}ms)`); |
||||||
|
return NetworkCondition.SLOW; |
||||||
|
} |
||||||
|
|
||||||
|
console.debug(`[network_detection.ts] Network condition: ONLINE (${speed.toFixed(0)}ms)`); |
||||||
|
return NetworkCondition.ONLINE; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the appropriate relay sets based on network condition |
||||||
|
* @param networkCondition The detected network condition |
||||||
|
* @param discoveredLocalRelays Array of discovered local relay URLs |
||||||
|
* @param lowbandwidthRelays Array of low bandwidth relay URLs |
||||||
|
* @param fullRelaySet The complete relay set for normal conditions |
||||||
|
* @returns Object with inbox and outbox relay arrays |
||||||
|
*/ |
||||||
|
export function getRelaySetForNetworkCondition( |
||||||
|
networkCondition: NetworkCondition, |
||||||
|
discoveredLocalRelays: string[], |
||||||
|
lowbandwidthRelays: string[], |
||||||
|
fullRelaySet: { inboxRelays: string[]; outboxRelays: string[] } |
||||||
|
): { inboxRelays: string[]; outboxRelays: string[] } { |
||||||
|
switch (networkCondition) { |
||||||
|
case NetworkCondition.OFFLINE: |
||||||
|
// When offline, use local relays if available, otherwise rely on cache
|
||||||
|
// This will be improved when IndexedDB local relay is implemented
|
||||||
|
if (discoveredLocalRelays.length > 0) { |
||||||
|
console.debug('[network_detection.ts] Using local relays (offline)'); |
||||||
|
return { |
||||||
|
inboxRelays: discoveredLocalRelays, |
||||||
|
outboxRelays: discoveredLocalRelays |
||||||
|
}; |
||||||
|
} else { |
||||||
|
console.debug('[network_detection.ts] No local relays available, will rely on cache (offline)'); |
||||||
|
return { |
||||||
|
inboxRelays: [], |
||||||
|
outboxRelays: [] |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
case NetworkCondition.SLOW: |
||||||
|
// Local relays + low bandwidth relays when slow (deduplicated)
|
||||||
|
console.debug('[network_detection.ts] Using local + low bandwidth relays (slow network)'); |
||||||
|
const slowInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]); |
||||||
|
const slowOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]); |
||||||
|
return { |
||||||
|
inboxRelays: slowInboxRelays, |
||||||
|
outboxRelays: slowOutboxRelays |
||||||
|
}; |
||||||
|
|
||||||
|
case NetworkCondition.ONLINE: |
||||||
|
default: |
||||||
|
// Full relay set when online
|
||||||
|
console.debug('[network_detection.ts] Using full relay set (online)'); |
||||||
|
return fullRelaySet; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Starts periodic network monitoring with reduced frequency to avoid spam |
||||||
|
* @param onNetworkChange Callback function called when network condition changes |
||||||
|
* @param checkInterval Interval in milliseconds between network checks (default: 60 seconds) |
||||||
|
* @returns Function to stop the monitoring |
||||||
|
*/ |
||||||
|
export function startNetworkMonitoring( |
||||||
|
onNetworkChange: (condition: NetworkCondition) => void, |
||||||
|
checkInterval: number = 60000 // Increased to 60 seconds to reduce spam
|
||||||
|
): () => void { |
||||||
|
let lastCondition: NetworkCondition | null = null; |
||||||
|
let intervalId: number | null = null; |
||||||
|
|
||||||
|
const checkNetwork = async () => { |
||||||
|
try { |
||||||
|
const currentCondition = await detectNetworkCondition(); |
||||||
|
|
||||||
|
if (currentCondition !== lastCondition) { |
||||||
|
console.debug(`[network_detection.ts] Network condition changed: ${lastCondition} -> ${currentCondition}`); |
||||||
|
lastCondition = currentCondition; |
||||||
|
onNetworkChange(currentCondition); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.warn('[network_detection.ts] Network monitoring error:', error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Initial check
|
||||||
|
checkNetwork(); |
||||||
|
|
||||||
|
// Set up periodic monitoring
|
||||||
|
intervalId = window.setInterval(checkNetwork, checkInterval); |
||||||
|
|
||||||
|
// Return function to stop monitoring
|
||||||
|
return () => { |
||||||
|
if (intervalId !== null) { |
||||||
|
clearInterval(intervalId); |
||||||
|
intervalId = null; |
||||||
|
} |
||||||
|
}; |
||||||
|
}
|
||||||
@ -0,0 +1,424 @@ |
|||||||
|
import NDK, { NDKRelay, NDKUser } from "@nostr-dev-kit/ndk"; |
||||||
|
import { communityRelays, searchRelays, secondaryRelays, anonymousRelays, lowbandwidthRelays, localRelays } from "../consts"; |
||||||
|
import { getRelaySetForNetworkCondition, NetworkCondition } from "./network_detection"; |
||||||
|
import { networkCondition } from "../stores/networkStore"; |
||||||
|
import { get } from "svelte/store"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Normalizes a relay URL to a standard format |
||||||
|
* @param url The relay URL to normalize |
||||||
|
* @returns The normalized relay URL |
||||||
|
*/ |
||||||
|
export function normalizeRelayUrl(url: string): string { |
||||||
|
let normalized = url.toLowerCase().trim(); |
||||||
|
|
||||||
|
// Ensure protocol is present
|
||||||
|
if (!normalized.startsWith('ws://') && !normalized.startsWith('wss://')) { |
||||||
|
normalized = 'wss://' + normalized; |
||||||
|
} |
||||||
|
|
||||||
|
// Remove trailing slash
|
||||||
|
normalized = normalized.replace(/\/$/, ''); |
||||||
|
|
||||||
|
return normalized; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Normalizes an array of relay URLs |
||||||
|
* @param urls Array of relay URLs to normalize |
||||||
|
* @returns Array of normalized relay URLs |
||||||
|
*/ |
||||||
|
export function normalizeRelayUrls(urls: string[]): string[] { |
||||||
|
return urls.map(normalizeRelayUrl); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes duplicates from an array of relay URLs |
||||||
|
* @param urls Array of relay URLs |
||||||
|
* @returns Array of unique relay URLs |
||||||
|
*/ |
||||||
|
export function deduplicateRelayUrls(urls: string[]): string[] { |
||||||
|
const normalized = normalizeRelayUrls(urls); |
||||||
|
return [...new Set(normalized)]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests connection to a relay and returns connection status |
||||||
|
* @param relayUrl The relay URL to test |
||||||
|
* @param ndk The NDK instance |
||||||
|
* @returns Promise that resolves to connection status |
||||||
|
*/ |
||||||
|
export async function testRelayConnection( |
||||||
|
relayUrl: string, |
||||||
|
ndk: NDK, |
||||||
|
): Promise<{ |
||||||
|
connected: boolean; |
||||||
|
requiresAuth: boolean; |
||||||
|
error?: string; |
||||||
|
actualUrl?: string; |
||||||
|
}> { |
||||||
|
return new Promise((resolve) => { |
||||||
|
// Ensure the URL is using wss:// protocol
|
||||||
|
const secureUrl = ensureSecureWebSocket(relayUrl); |
||||||
|
|
||||||
|
// Use the existing NDK instance instead of creating a new one
|
||||||
|
const relay = new NDKRelay(secureUrl, undefined, ndk); |
||||||
|
let authRequired = false; |
||||||
|
let connected = false; |
||||||
|
let error: string | undefined; |
||||||
|
let actualUrl: string | undefined; |
||||||
|
|
||||||
|
const timeout = setTimeout(() => { |
||||||
|
relay.disconnect(); |
||||||
|
resolve({ |
||||||
|
connected: false, |
||||||
|
requiresAuth: authRequired, |
||||||
|
error: "Connection timeout", |
||||||
|
actualUrl, |
||||||
|
}); |
||||||
|
}, 3000); // Increased timeout to 3 seconds to give relays more time
|
||||||
|
|
||||||
|
relay.on("connect", () => { |
||||||
|
connected = true; |
||||||
|
actualUrl = secureUrl; |
||||||
|
clearTimeout(timeout); |
||||||
|
relay.disconnect(); |
||||||
|
resolve({ |
||||||
|
connected: true, |
||||||
|
requiresAuth: authRequired, |
||||||
|
error, |
||||||
|
actualUrl, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
relay.on("notice", (message: string) => { |
||||||
|
if (message.includes("auth-required")) { |
||||||
|
authRequired = true; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
relay.on("disconnect", () => { |
||||||
|
if (!connected) { |
||||||
|
error = "Connection failed"; |
||||||
|
clearTimeout(timeout); |
||||||
|
resolve({ |
||||||
|
connected: false, |
||||||
|
requiresAuth: authRequired, |
||||||
|
error, |
||||||
|
actualUrl, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
relay.connect(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Ensures a relay URL uses secure WebSocket protocol for remote relays |
||||||
|
* @param url The relay URL to secure |
||||||
|
* @returns The URL with wss:// protocol (except for localhost)
|
||||||
|
*/ |
||||||
|
function ensureSecureWebSocket(url: string): string { |
||||||
|
// For localhost, always use ws:// (never wss://)
|
||||||
|
if (url.includes('localhost') || url.includes('127.0.0.1')) { |
||||||
|
// Convert any wss://localhost to ws://localhost
|
||||||
|
return url.replace(/^wss:\/\//, "ws://"); |
||||||
|
} |
||||||
|
|
||||||
|
// Replace ws:// with wss:// for remote relays
|
||||||
|
const secureUrl = url.replace(/^ws:\/\//, "wss://"); |
||||||
|
|
||||||
|
if (secureUrl !== url) { |
||||||
|
console.warn( |
||||||
|
`[relay_management.ts] Protocol upgrade for rem ote relay: ${url} -> ${secureUrl}`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return secureUrl; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests connection to local relays |
||||||
|
* @param localRelayUrls Array of local relay URLs to test |
||||||
|
* @param ndk NDK instance |
||||||
|
* @returns Promise that resolves to array of working local relay URLs |
||||||
|
*/ |
||||||
|
async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<string[]> { |
||||||
|
const workingRelays: string[] = []; |
||||||
|
|
||||||
|
await Promise.all( |
||||||
|
localRelayUrls.map(async (url) => { |
||||||
|
try { |
||||||
|
const result = await testRelayConnection(url, ndk); |
||||||
|
if (result.connected) { |
||||||
|
workingRelays.push(url); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
// Silently ignore local relay failures
|
||||||
|
} |
||||||
|
}) |
||||||
|
); |
||||||
|
|
||||||
|
return workingRelays; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Discovers local relays by testing common localhost URLs |
||||||
|
* @param ndk NDK instance |
||||||
|
* @returns Promise that resolves to array of working local relay URLs |
||||||
|
*/ |
||||||
|
export async function discoverLocalRelays(ndk: NDK): Promise<string[]> { |
||||||
|
try { |
||||||
|
// Convert wss:// URLs from consts to ws:// for local testing
|
||||||
|
const localRelayUrls = localRelays.map(url =>
|
||||||
|
url.replace(/^wss:\/\//, 'ws://') |
||||||
|
); |
||||||
|
|
||||||
|
const workingRelays = await testLocalRelays(localRelayUrls, ndk); |
||||||
|
|
||||||
|
// If no local relays are working, return empty array
|
||||||
|
// The network detection logic will provide fallback relays
|
||||||
|
return workingRelays; |
||||||
|
} catch (error) { |
||||||
|
// Silently fail and return empty array
|
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches user's local relays from kind 10432 event |
||||||
|
* @param ndk NDK instance |
||||||
|
* @param user User to fetch local relays for |
||||||
|
* @returns Promise that resolves to array of local relay URLs |
||||||
|
*/ |
||||||
|
export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise<string[]> { |
||||||
|
try { |
||||||
|
const localRelayEvent = await ndk.fetchEvent( |
||||||
|
{ |
||||||
|
kinds: [10432 as any], |
||||||
|
authors: [user.pubkey], |
||||||
|
}, |
||||||
|
{ |
||||||
|
groupable: false, |
||||||
|
skipVerification: false, |
||||||
|
skipValidation: false, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (!localRelayEvent) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
const localRelays: string[] = []; |
||||||
|
localRelayEvent.tags.forEach((tag) => { |
||||||
|
if (tag[0] === 'r' && tag[1]) { |
||||||
|
localRelays.push(tag[1]); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return localRelays; |
||||||
|
} catch (error) { |
||||||
|
console.info('[relay_management.ts] Error fetching user local relays:', error); |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches user's blocked relays from kind 10006 event |
||||||
|
* @param ndk NDK instance |
||||||
|
* @param user User to fetch blocked relays for |
||||||
|
* @returns Promise that resolves to array of blocked relay URLs |
||||||
|
*/ |
||||||
|
export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise<string[]> { |
||||||
|
try { |
||||||
|
const blockedRelayEvent = await ndk.fetchEvent( |
||||||
|
{ |
||||||
|
kinds: [10006], |
||||||
|
authors: [user.pubkey], |
||||||
|
}, |
||||||
|
{ |
||||||
|
groupable: false, |
||||||
|
skipVerification: false, |
||||||
|
skipValidation: false, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (!blockedRelayEvent) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
const blockedRelays: string[] = []; |
||||||
|
blockedRelayEvent.tags.forEach((tag) => { |
||||||
|
if (tag[0] === 'r' && tag[1]) { |
||||||
|
blockedRelays.push(tag[1]); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return blockedRelays; |
||||||
|
} catch (error) { |
||||||
|
console.info('[relay_management.ts] Error fetching user blocked relays:', error); |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches user's outbox relays from NIP-65 relay list |
||||||
|
* @param ndk NDK instance |
||||||
|
* @param user User to fetch outbox relays for |
||||||
|
* @returns Promise that resolves to array of outbox relay URLs |
||||||
|
*/ |
||||||
|
export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise<string[]> { |
||||||
|
try { |
||||||
|
const relayList = await ndk.fetchEvent( |
||||||
|
{ |
||||||
|
kinds: [10002], |
||||||
|
authors: [user.pubkey], |
||||||
|
}, |
||||||
|
{ |
||||||
|
groupable: false, |
||||||
|
skipVerification: false, |
||||||
|
skipValidation: false, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (!relayList) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
const outboxRelays: string[] = []; |
||||||
|
relayList.tags.forEach((tag) => { |
||||||
|
if (tag[0] === 'w' && tag[1]) { |
||||||
|
outboxRelays.push(tag[1]); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return outboxRelays; |
||||||
|
} catch (error) { |
||||||
|
console.info('[relay_management.ts] Error fetching user outbox relays:', error); |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests a set of relays in batches to avoid overwhelming them |
||||||
|
* @param relayUrls Array of relay URLs to test |
||||||
|
* @param ndk NDK instance |
||||||
|
* @returns Promise that resolves to array of working relay URLs |
||||||
|
*/ |
||||||
|
async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> { |
||||||
|
const workingRelays: string[] = []; |
||||||
|
const maxConcurrent = 3; // Test 3 relays at a time to avoid overwhelming them
|
||||||
|
|
||||||
|
for (let i = 0; i < relayUrls.length; i += maxConcurrent) { |
||||||
|
const batch = relayUrls.slice(i, i + maxConcurrent); |
||||||
|
|
||||||
|
const batchPromises = batch.map(async (url) => { |
||||||
|
try { |
||||||
|
const result = await testRelayConnection(url, ndk); |
||||||
|
return result.connected ? url : null; |
||||||
|
} catch (error) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const batchResults = await Promise.all(batchPromises); |
||||||
|
const batchWorkingRelays = batchResults.filter((url): url is string => url !== null); |
||||||
|
workingRelays.push(...batchWorkingRelays); |
||||||
|
} |
||||||
|
|
||||||
|
return workingRelays; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Builds a complete relay set for a user, including local, user-specific, and fallback relays |
||||||
|
* @param ndk NDK instance |
||||||
|
* @param user NDKUser or null for anonymous access |
||||||
|
* @returns Promise that resolves to inbox and outbox relay arrays |
||||||
|
*/ |
||||||
|
export async function buildCompleteRelaySet( |
||||||
|
ndk: NDK, |
||||||
|
user: NDKUser | null |
||||||
|
): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { |
||||||
|
// Discover local relays first
|
||||||
|
const discoveredLocalRelays = await discoverLocalRelays(ndk); |
||||||
|
|
||||||
|
// Get user-specific relays if available
|
||||||
|
let userOutboxRelays: string[] = []; |
||||||
|
let userLocalRelays: string[] = []; |
||||||
|
let blockedRelays: string[] = []; |
||||||
|
|
||||||
|
if (user) { |
||||||
|
try { |
||||||
|
userOutboxRelays = await getUserOutboxRelays(ndk, user); |
||||||
|
} catch (error) { |
||||||
|
// Silently ignore user relay fetch errors
|
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
userLocalRelays = await getUserLocalRelays(ndk, user); |
||||||
|
} catch (error) { |
||||||
|
// Silently ignore user local relay fetch errors
|
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
blockedRelays = await getUserBlockedRelays(ndk, user); |
||||||
|
} catch (error) { |
||||||
|
// Silently ignore blocked relay fetch errors
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Build initial relay sets and deduplicate
|
||||||
|
const finalInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userLocalRelays]); |
||||||
|
const finalOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userOutboxRelays]); |
||||||
|
|
||||||
|
// Test relays and filter out non-working ones
|
||||||
|
let testedInboxRelays: string[] = []; |
||||||
|
let testedOutboxRelays: string[] = []; |
||||||
|
|
||||||
|
if (finalInboxRelays.length > 0) { |
||||||
|
testedInboxRelays = await testRelaySet(finalInboxRelays, ndk); |
||||||
|
} |
||||||
|
|
||||||
|
if (finalOutboxRelays.length > 0) { |
||||||
|
testedOutboxRelays = await testRelaySet(finalOutboxRelays, ndk); |
||||||
|
} |
||||||
|
|
||||||
|
// If no relays passed testing, use remote relays without testing
|
||||||
|
if (testedInboxRelays.length === 0 && testedOutboxRelays.length === 0) { |
||||||
|
const remoteRelays = deduplicateRelayUrls([...secondaryRelays, ...searchRelays]); |
||||||
|
return { |
||||||
|
inboxRelays: remoteRelays, |
||||||
|
outboxRelays: remoteRelays |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
// Use tested relays and deduplicate
|
||||||
|
const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays); |
||||||
|
const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(secondaryRelays); |
||||||
|
|
||||||
|
// Apply network condition optimization
|
||||||
|
const currentNetworkCondition = get(networkCondition); |
||||||
|
const networkOptimizedRelaySet = getRelaySetForNetworkCondition( |
||||||
|
currentNetworkCondition, |
||||||
|
discoveredLocalRelays, |
||||||
|
lowbandwidthRelays, |
||||||
|
{ inboxRelays, outboxRelays } |
||||||
|
); |
||||||
|
|
||||||
|
// Filter out blocked relays and deduplicate final sets
|
||||||
|
const finalRelaySet = { |
||||||
|
inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter(r => !blockedRelays.includes(r))), |
||||||
|
outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter(r => !blockedRelays.includes(r))) |
||||||
|
}; |
||||||
|
|
||||||
|
// If no relays are working, use anonymous relays as fallback
|
||||||
|
if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) { |
||||||
|
return { |
||||||
|
inboxRelays: deduplicateRelayUrls(anonymousRelays), |
||||||
|
outboxRelays: deduplicateRelayUrls(anonymousRelays) |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
return finalRelaySet; |
||||||
|
}
|
||||||
Loading…
Reference in new issue