diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index bb6d692..07cfa81 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -93,16 +93,28 @@ export async function searchBySubscription( tTagEvents: tTagEventsWithCreatedAt }; - // AI-NOTE: 2025-01-24 - For profile searches, return cached results immediately - // The EventSearch component now handles cache checking before calling this function - if (searchType === "n") { - console.log( - "subscription_search: Returning cached profile result immediately", - ); - return resultWithCreatedAt; - } else { - return resultWithCreatedAt; - } + // AI-NOTE: 2025-01-24 - Return cached results immediately but trigger second-order search in background + // This ensures we get fast results while still updating second-order data + console.log("subscription_search: Returning cached result immediately, triggering background second-order search"); + + // Trigger second-order search in background for all search types + if (ndk) { + // Start second-order search in background for n and d searches only + if (searchType === "n" || searchType === "d") { + console.log("subscription_search: Triggering background second-order search for cached result"); + performSecondOrderSearchInBackground( + searchType as "n" | "d", + eventsWithCreatedAt, + cachedResult.eventIds || new Set(), + cachedResult.addresses || new Set(), + ndk, + searchType === "n" ? eventsWithCreatedAt[0]?.pubkey : undefined, + callbacks + ); + } + } + + return resultWithCreatedAt; } if (!ndk) { @@ -161,6 +173,27 @@ export async function searchBySubscription( normalizedSearchTerm, ); searchCache.set(searchType, normalizedSearchTerm, immediateResult); + + // AI-NOTE: 2025-01-24 - For profile searches, start background second-order search even for preloaded events + if (searchType === "n") { + console.log( + "subscription_search: Profile found from preloaded events, starting background second-order search", + ); + + // Start Phase 2 in background for second-order results + searchOtherRelaysInBackground( + searchType, + searchFilter, + searchState, + ndk, + callbacks, + cleanup, + ); + + // Clear the main timeout since we're returning early + cleanup(); + } + return immediateResult; } } @@ -172,11 +205,19 @@ export async function searchBySubscription( "subscription_search: Searching primary relay with filter:", searchFilter.filter, ); - const primaryEvents = await ndk.fetchEvents( + + // Add timeout to primary relay search + const primaryEventsPromise = ndk.fetchEvents( searchFilter.filter, { closeOnEose: true }, primaryRelaySet, ); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Primary relay search timeout")), TIMEOUTS.SUBSCRIPTION_SEARCH); + }); + + const primaryEvents = await Promise.race([primaryEventsPromise, timeoutPromise]) as any; console.log( "subscription_search: Primary relay returned", @@ -226,6 +267,9 @@ export async function searchBySubscription( console.log( `subscription_search: Profile search completed in ${elapsed}ms`, ); + + // Clear the main timeout since we're returning early + cleanup(); return immediateResult; } @@ -239,6 +283,8 @@ export async function searchBySubscription( cleanup, ); + // Clear the main timeout since we're returning early + cleanup(); return immediateResult; } else { console.log( @@ -257,11 +303,18 @@ export async function searchBySubscription( ndk, ); try { - const fallbackEvents = await ndk.fetchEvents( + // Add timeout to fallback search + const fallbackEventsPromise = ndk.fetchEvents( searchFilter.filter, { closeOnEose: true }, allRelaySet, ); + + const fallbackTimeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Fallback search timeout")), TIMEOUTS.SUBSCRIPTION_SEARCH); + }); + + const fallbackEvents = await Promise.race([fallbackEventsPromise, fallbackTimeoutPromise]) as any; console.log( "subscription_search: Fallback search returned", @@ -293,6 +346,9 @@ export async function searchBySubscription( console.log( `subscription_search: Profile search completed in ${elapsed}ms (fallback)`, ); + + // Clear the main timeout since we're returning early + cleanup(); return fallbackResult; } } catch (fallbackError) { @@ -300,6 +356,11 @@ export async function searchBySubscription( "subscription_search: Fallback search failed:", fallbackError, ); + + // If it's a timeout error, continue to return empty result + if (fallbackError instanceof Error && fallbackError.message.includes("timeout")) { + console.log("subscription_search: Fallback search timed out, returning empty result"); + } } console.log( @@ -315,6 +376,9 @@ export async function searchBySubscription( console.log( `subscription_search: Profile search completed in ${elapsed}ms (not found)`, ); + + // Clear the main timeout since we're returning early + cleanup(); return emptyResult; } else { console.log( @@ -327,6 +391,14 @@ export async function searchBySubscription( `subscription_search: Error searching primary relay:`, error, ); + + // If it's a timeout error, continue to Phase 2 instead of failing + if (error instanceof Error && error.message.includes("timeout")) { + console.log("subscription_search: Primary relay search timed out, continuing to Phase 2"); + } else { + // For other errors, we might want to fail the search + throw error; + } } } else { console.log( @@ -352,6 +424,8 @@ export async function searchBySubscription( ); } + // Clear the main timeout since we're completing normally + cleanup(); return result; } @@ -793,17 +867,42 @@ function searchOtherRelaysInBackground( }); return new Promise((resolve) => { + let resolved = false; + + // Add timeout to prevent hanging + const timeoutId = setTimeout(() => { + if (!resolved) { + console.log("subscription_search: Background search timeout, resolving with current results"); + resolved = true; + sub.stop(); + const result = processEoseResults( + searchType, + searchState, + searchFilter, + ndk, + callbacks, + ); + searchCache.set(searchType, searchState.normalizedSearchTerm, result); + cleanup?.(); + resolve(result); + } + }, TIMEOUTS.SUBSCRIPTION_SEARCH); + sub.on("eose", () => { - const result = processEoseResults( - searchType, - searchState, - searchFilter, - ndk, - callbacks, - ); - searchCache.set(searchType, searchState.normalizedSearchTerm, result); - cleanup?.(); - resolve(result); + if (!resolved) { + resolved = true; + clearTimeout(timeoutId); + const result = processEoseResults( + searchType, + searchState, + searchFilter, + ndk, + callbacks, + ); + searchCache.set(searchType, searchState.normalizedSearchTerm, result); + cleanup?.(); + resolve(result); + } }); }); } @@ -821,7 +920,7 @@ function processEoseResults( if (searchType === "n") { return processProfileEoseResults(searchState, searchFilter, ndk, callbacks); } else if (searchType === "d") { - return processContentEoseResults(searchState, searchType, ndk); + return processContentEoseResults(searchState, searchType, ndk, callbacks); } else if (searchType === "t") { return processTTagEoseResults(searchState); } @@ -926,6 +1025,7 @@ function processContentEoseResults( searchState: any, searchType: SearchSubscriptionType, ndk: NDK, + callbacks?: SearchCallbacks, ): SearchResult { if (searchState.firstOrderEvents.length === 0) { return createEmptySearchResult( @@ -954,6 +1054,8 @@ function processContentEoseResults( searchState.eventIds, searchState.eventAddresses, ndk, + undefined, // targetPubkey not needed for d-tag searches + callbacks, ); } @@ -1041,23 +1143,16 @@ async function performSecondOrderSearchInBackground( targetPubkey, ); - // AI-NOTE: 2025-01-24 - Use only active relays for second-order profile search to prevent hanging - const activeRelays = [ - ...get(activeInboxRelays), - ...get(activeOutboxRelays), - ]; - const availableRelays = activeRelays - .map((url) => ndk.pool.relays.get(url)) - .filter((relay): relay is any => relay !== undefined); + // AI-NOTE: 2025-01-24 - Use all available relays for second-order search to maximize results const relaySet = new NDKRelaySet( - new Set(availableRelays), + new Set(Array.from(ndk.pool.relays.values())), ndk, ); console.log( "subscription_search: Using", - activeRelays.length, - "active relays for second-order search", + ndk.pool.relays.size, + "relays for second-order search", ); // Search for events that mention this pubkey via p-tags diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index e65e02c..b17c08c 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -33,6 +33,28 @@ import { UserOutline } from "flowbite-svelte-icons"; import type { UserProfile } from "$lib/models/user_profile"; import type { SearchType } from "$lib/models/search_type"; + import { clearAllCaches } from "$lib/utils/cache_manager"; + + // AI-NOTE: 2025-01-24 - Add cache clearing function for testing second-order search + // This can be called from browser console: window.clearCache() + if (typeof window !== 'undefined') { + (window as any).clearCache = () => { + console.log('Clearing all caches for testing...'); + clearAllCaches(); + console.log('Caches cleared. Try searching again to test second-order search.'); + }; + + // AI-NOTE: 2025-01-24 - Add function to clear specific search cache + // Usage: window.clearSearchCache('n', 'silberengel') + (window as any).clearSearchCache = (searchType: string, searchTerm: string) => { + console.log(`Clearing search cache for ${searchType}:${searchTerm}...`); + // Import searchCache dynamically + import('$lib/utils/searchCache').then(({ searchCache }) => { + searchCache.clear(); + console.log('Search cache cleared. Try searching again to test second-order search.'); + }); + }; + } let loading = $state(false); let error = $state(null); @@ -137,7 +159,7 @@ const npub = toNpub(pubkey); if (npub) { // Force fetch to ensure profile is cached - await getUserMetadata(npub, undefined, true); + await getUserMetadata(npub, ndk, true); console.log(`[Events Page] Cached profile for pubkey: ${pubkey}`); } } catch (error) {