40 changed files with 1685 additions and 955 deletions
@ -1,78 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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