@ -27,12 +27,12 @@ const normalizeUrl = (url: string): string => {
// AI-NOTE: Define prioritized event kinds for subscription search
// AI-NOTE: Define prioritized event kinds for subscription search
const PRIORITIZED_EVENT_KINDS = new Set ( [
const PRIORITIZED_EVENT_KINDS = new Set ( [
1 , // Text notes
1 , // Text notes
1111 , // Comments
1111 , // Comments
9802 , // Highlights
9802 , // Highlights
20 , // Article
20 , // Article
21 , // Article
21 , // Article
22 , // Article
22 , // Article
1222 , // Long-form content
1222 , // Long-form content
1244 , // Long-form content
1244 , // Long-form content
30023 , // Long-form content
30023 , // Long-form content
@ -47,7 +47,7 @@ const PRIORITIZED_EVENT_KINDS = new Set([
* @param maxResults Maximum number of results to return
* @param maxResults Maximum number of results to return
* @param ndk NDK instance for user list and community checks
* @param ndk NDK instance for user list and community checks
* @returns Prioritized array of events
* @returns Prioritized array of events
*
*
* Priority tiers :
* Priority tiers :
* 1 . Prioritized event kinds ( 1 , 1111 , 9802 , 20 , 21 , 22 , 1222 , 1244 , 30023 , 30040 , 30041 ) + target pubkey events ( n : searches only )
* 1 . Prioritized event kinds ( 1 , 1111 , 9802 , 20 , 21 , 22 , 1222 , 1244 , 30023 , 30040 , 30041 ) + target pubkey events ( n : searches only )
* 2 . Events from user ' s follows ( if logged in )
* 2 . Events from user ' s follows ( if logged in )
@ -58,7 +58,7 @@ async function prioritizeSearchEvents(
events : NDKEvent [ ] ,
events : NDKEvent [ ] ,
targetPubkey? : string ,
targetPubkey? : string ,
maxResults : number = SEARCH_LIMITS . GENERAL_CONTENT ,
maxResults : number = SEARCH_LIMITS . GENERAL_CONTENT ,
ndk? : NDK
ndk? : NDK ,
) : Promise < NDKEvent [ ] > {
) : Promise < NDKEvent [ ] > {
if ( events . length === 0 ) {
if ( events . length === 0 ) {
return [ ] ;
return [ ] ;
@ -67,58 +67,75 @@ async function prioritizeSearchEvents(
// AI-NOTE: Get user lists and community status for prioritization
// AI-NOTE: Get user lists and community status for prioritization
let userFollowPubkeys = new Set < string > ( ) ;
let userFollowPubkeys = new Set < string > ( ) ;
let communityMemberPubkeys = new Set < string > ( ) ;
let communityMemberPubkeys = new Set < string > ( ) ;
// Only attempt user list and community checks if NDK is provided
// Only attempt user list and community checks if NDK is provided
if ( ndk ) {
if ( ndk ) {
try {
try {
// Import user list functions dynamically to avoid circular dependencies
// Import user list functions dynamically to avoid circular dependencies
const { fetchCurrentUserLists , getPubkeysFromListKind } = await import ( "./user_lists.ts" ) ;
const { fetchCurrentUserLists , getPubkeysFromListKind } = await import (
"./user_lists.ts"
) ;
const { checkCommunity } = await import ( "./community_checker.ts" ) ;
const { checkCommunity } = await import ( "./community_checker.ts" ) ;
// Get current user's follow lists (if logged in)
// Get current user's follow lists (if logged in)
const userLists = await fetchCurrentUserLists ( undefined , ndk ) ;
const userLists = await fetchCurrentUserLists ( undefined , ndk ) ;
userFollowPubkeys = getPubkeysFromListKind ( userLists , 3 ) ; // Kind 3 = follow list
userFollowPubkeys = getPubkeysFromListKind ( userLists , 3 ) ; // Kind 3 = follow list
// Check community status for unique pubkeys in events (limit to prevent hanging)
// Check community status for unique pubkeys in events (limit to prevent hanging)
const uniquePubkeys = new Set ( events . map ( e = > e . pubkey ) . filter ( Boolean ) ) ;
const uniquePubkeys = new Set (
events . map ( ( e ) = > e . pubkey ) . filter ( Boolean ) ,
) ;
const pubkeysToCheck = Array . from ( uniquePubkeys ) . slice ( 0 , 20 ) ; // Limit to first 20 pubkeys
const pubkeysToCheck = Array . from ( uniquePubkeys ) . slice ( 0 , 20 ) ; // Limit to first 20 pubkeys
console . log ( ` subscription_search: Checking community status for ${ pubkeysToCheck . length } pubkeys out of ${ uniquePubkeys . size } total ` ) ;
console . log (
` subscription_search: Checking community status for ${ pubkeysToCheck . length } pubkeys out of ${ uniquePubkeys . size } total ` ,
) ;
const communityChecks = await Promise . allSettled (
const communityChecks = await Promise . allSettled (
pubkeysToCheck . map ( async ( pubkey ) = > {
pubkeysToCheck . map ( async ( pubkey ) = > {
try {
try {
const isCommunityMember = await Promise . race ( [
const isCommunityMember = await Promise . race ( [
checkCommunity ( pubkey ) ,
checkCommunity ( pubkey ) ,
new Promise ( ( _ , reject ) = >
new Promise ( ( _ , reject ) = >
setTimeout ( ( ) = > reject ( new Error ( 'Community check timeout' ) ) , 2000 )
setTimeout (
)
( ) = > reject ( new Error ( "Community check timeout" ) ) ,
2000 ,
)
) ,
] ) ;
] ) ;
return { pubkey , isCommunityMember } ;
return { pubkey , isCommunityMember } ;
} catch ( error ) {
} catch ( error ) {
console . warn ( ` subscription_search: Community check failed for ${ pubkey } : ` , error ) ;
console . warn (
` subscription_search: Community check failed for ${ pubkey } : ` ,
error ,
) ;
return { pubkey , isCommunityMember : false } ;
return { pubkey , isCommunityMember : false } ;
}
}
} )
} ) ,
) ;
) ;
// Build set of community member pubkeys
// Build set of community member pubkeys
communityChecks . forEach ( result = > {
communityChecks . forEach ( ( result ) = > {
if ( result . status === "fulfilled" && result . value . isCommunityMember ) {
if ( result . status === "fulfilled" && result . value . isCommunityMember ) {
communityMemberPubkeys . add ( result . value . pubkey ) ;
communityMemberPubkeys . add ( result . value . pubkey ) ;
}
}
} ) ;
} ) ;
console . log ( "subscription_search: Prioritization data loaded:" , {
console . log ( "subscription_search: Prioritization data loaded:" , {
userFollows : userFollowPubkeys.size ,
userFollows : userFollowPubkeys.size ,
communityMembers : communityMemberPubkeys.size ,
communityMembers : communityMemberPubkeys.size ,
totalEvents : events.length
totalEvents : events.length ,
} ) ;
} ) ;
} catch ( error ) {
} catch ( error ) {
console . warn ( "subscription_search: Failed to load prioritization data:" , error ) ;
console . warn (
"subscription_search: Failed to load prioritization data:" ,
error ,
) ;
}
}
} else {
} else {
console . log ( "subscription_search: No NDK provided, skipping user list and community checks" ) ;
console . log (
"subscription_search: No NDK provided, skipping user list and community checks" ,
) ;
}
}
// Separate events into priority tiers
// Separate events into priority tiers
@ -131,8 +148,10 @@ async function prioritizeSearchEvents(
const isFromTarget = targetPubkey && event . pubkey === targetPubkey ;
const isFromTarget = targetPubkey && event . pubkey === targetPubkey ;
const isPrioritizedKind = PRIORITIZED_EVENT_KINDS . has ( event . kind || 0 ) ;
const isPrioritizedKind = PRIORITIZED_EVENT_KINDS . has ( event . kind || 0 ) ;
const isFromFollow = userFollowPubkeys . has ( event . pubkey || "" ) ;
const isFromFollow = userFollowPubkeys . has ( event . pubkey || "" ) ;
const isFromCommunityMember = communityMemberPubkeys . has ( event . pubkey || "" ) ;
const isFromCommunityMember = communityMemberPubkeys . has (
event . pubkey || "" ,
) ;
// AI-NOTE: Prioritized kinds are always in tier 1
// AI-NOTE: Prioritized kinds are always in tier 1
// Target pubkey priority only applies to n: searches (when targetPubkey is provided)
// Target pubkey priority only applies to n: searches (when targetPubkey is provided)
if ( isPrioritizedKind || isFromTarget ) {
if ( isPrioritizedKind || isFromTarget ) {
@ -154,22 +173,22 @@ async function prioritizeSearchEvents(
// Combine tiers in priority order, respecting the limit
// Combine tiers in priority order, respecting the limit
const result : NDKEvent [ ] = [ ] ;
const result : NDKEvent [ ] = [ ] ;
// Add tier 1 events (highest priority)
// Add tier 1 events (highest priority)
result . push ( . . . tier1 ) ;
result . push ( . . . tier1 ) ;
// Add tier 2 events (follows) if we haven't reached the limit
// Add tier 2 events (follows) if we haven't reached the limit
const remainingAfterTier1 = maxResults - result . length ;
const remainingAfterTier1 = maxResults - result . length ;
if ( remainingAfterTier1 > 0 ) {
if ( remainingAfterTier1 > 0 ) {
result . push ( . . . tier2 . slice ( 0 , remainingAfterTier1 ) ) ;
result . push ( . . . tier2 . slice ( 0 , remainingAfterTier1 ) ) ;
}
}
// Add tier 3 events (community members) if we haven't reached the limit
// Add tier 3 events (community members) if we haven't reached the limit
const remainingAfterTier2 = maxResults - result . length ;
const remainingAfterTier2 = maxResults - result . length ;
if ( remainingAfterTier2 > 0 ) {
if ( remainingAfterTier2 > 0 ) {
result . push ( . . . tier3 . slice ( 0 , remainingAfterTier2 ) ) ;
result . push ( . . . tier3 . slice ( 0 , remainingAfterTier2 ) ) ;
}
}
// Add tier 4 events (others) if we haven't reached the limit
// Add tier 4 events (others) if we haven't reached the limit
const remainingAfterTier3 = maxResults - result . length ;
const remainingAfterTier3 = maxResults - result . length ;
if ( remainingAfterTier3 > 0 ) {
if ( remainingAfterTier3 > 0 ) {
@ -181,7 +200,7 @@ async function prioritizeSearchEvents(
tier2 : tier2.length , // User follows
tier2 : tier2.length , // User follows
tier3 : tier3.length , // Community members
tier3 : tier3.length , // Community members
tier4 : tier4.length , // Others
tier4 : tier4.length , // Others
total : result.length
total : result.length ,
} ) ;
} ) ;
return result ;
return result ;
@ -221,61 +240,74 @@ export async function searchBySubscription(
const cachedResult = searchCache . get ( searchType , normalizedSearchTerm ) ;
const cachedResult = searchCache . get ( searchType , normalizedSearchTerm ) ;
if ( cachedResult ) {
if ( cachedResult ) {
console . log ( "subscription_search: Found cached result:" , cachedResult ) ;
console . log ( "subscription_search: Found cached result:" , cachedResult ) ;
// AI-NOTE: Ensure cached events have created_at property preserved
// AI-NOTE: Ensure cached events have created_at property preserved
// This fixes the "Unknown date" issue when events are retrieved from cache
// This fixes the "Unknown date" issue when events are retrieved from cache
const eventsWithCreatedAt = cachedResult . events . map ( event = > {
const eventsWithCreatedAt = cachedResult . events . map ( ( event ) = > {
if ( event && typeof event === 'object' && ! event . created_at ) {
if ( event && typeof event === "object" && ! event . created_at ) {
console . warn ( "subscription_search: Event missing created_at, setting to 0:" , event . id ) ;
console . warn (
"subscription_search: Event missing created_at, setting to 0:" ,
event . id ,
) ;
( event as any ) . created_at = 0 ;
( event as any ) . created_at = 0 ;
}
}
return event ;
return event ;
} ) ;
} ) ;
const secondOrderWithCreatedAt = cachedResult . secondOrder . map ( event = > {
const secondOrderWithCreatedAt = cachedResult . secondOrder . map ( ( event ) = > {
if ( event && typeof event === 'object' && ! event . created_at ) {
if ( event && typeof event === "object" && ! event . created_at ) {
console . warn ( "subscription_search: Second order event missing created_at, setting to 0:" , event . id ) ;
console . warn (
"subscription_search: Second order event missing created_at, setting to 0:" ,
event . id ,
) ;
( event as any ) . created_at = 0 ;
( event as any ) . created_at = 0 ;
}
}
return event ;
return event ;
} ) ;
} ) ;
const tTagEventsWithCreatedAt = cachedResult . tTagEvents . map ( event = > {
const tTagEventsWithCreatedAt = cachedResult . tTagEvents . map ( ( event ) = > {
if ( event && typeof event === 'object' && ! event . created_at ) {
if ( event && typeof event === "object" && ! event . created_at ) {
console . warn ( "subscription_search: T-tag event missing created_at, setting to 0:" , event . id ) ;
console . warn (
"subscription_search: T-tag event missing created_at, setting to 0:" ,
event . id ,
) ;
( event as any ) . created_at = 0 ;
( event as any ) . created_at = 0 ;
}
}
return event ;
return event ;
} ) ;
} ) ;
const resultWithCreatedAt = {
const resultWithCreatedAt = {
. . . cachedResult ,
. . . cachedResult ,
events : eventsWithCreatedAt ,
events : eventsWithCreatedAt ,
secondOrder : secondOrderWithCreatedAt ,
secondOrder : secondOrderWithCreatedAt ,
tTagEvents : tTagEventsWithCreatedAt
tTagEvents : tTagEventsWithCreatedAt ,
} ;
} ;
// AI-NOTE: Return cached results immediately but trigger second-order search in background
// AI-NOTE: Return cached results immediately but trigger second-order search in background
// This ensures we get fast results while still updating second-order data
// 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" ) ;
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
// Trigger second-order search in background for all search types
if ( searchType === "n" || searchType === "d" ) {
if ( ndk ) {
console . log ( "subscription_search: Triggering background second-order search for cached result" ) ;
// Start second-order search in background for n and d searches only
performSecondOrderSearchInBackground (
if ( searchType === "n" || searchType === "d" ) {
searchType as "n" | "d" ,
console . log (
eventsWithCreatedAt ,
"subscription_search: Triggering background second-order search for cached result" ,
cachedResult . eventIds || new Set ( ) ,
) ;
cachedResult . addresses || new Set ( ) ,
performSecondOrderSearchInBackground (
ndk ,
searchType as "n" | "d" ,
searchType === "n" ? eventsWithCreatedAt [ 0 ] ? . pubkey : undefined ,
eventsWithCreatedAt ,
callbacks
cachedResult . eventIds || new Set ( ) ,
) ;
cachedResult . addresses || new Set ( ) ,
}
ndk ,
searchType === "n" ? eventsWithCreatedAt [ 0 ] ? . pubkey : undefined ,
callbacks ,
) ;
}
}
}
return resultWithCreatedAt ;
return resultWithCreatedAt ;
}
}
@ -316,7 +348,10 @@ export async function searchBySubscription(
// AI-NOTE: Check for preloaded events first (for profile searches)
// AI-NOTE: Check for preloaded events first (for profile searches)
if ( searchFilter . preloadedEvents && searchFilter . preloadedEvents . length > 0 ) {
if ( searchFilter . preloadedEvents && searchFilter . preloadedEvents . length > 0 ) {
console . log ( "subscription_search: Using preloaded events:" , searchFilter . preloadedEvents . length ) ;
console . log (
"subscription_search: Using preloaded events:" ,
searchFilter . preloadedEvents . length ,
) ;
processPrimaryRelayResults (
processPrimaryRelayResults (
new Set ( searchFilter . preloadedEvents ) ,
new Set ( searchFilter . preloadedEvents ) ,
searchType ,
searchType ,
@ -326,9 +361,11 @@ export async function searchBySubscription(
abortSignal ,
abortSignal ,
cleanup ,
cleanup ,
) ;
) ;
if ( hasResults ( searchState , searchType ) ) {
if ( hasResults ( searchState , searchType ) ) {
console . log ( "subscription_search: Found results from preloaded events, returning immediately" ) ;
console . log (
"subscription_search: Found results from preloaded events, returning immediately" ,
) ;
const immediateResult = createSearchResult (
const immediateResult = createSearchResult (
searchState ,
searchState ,
searchType ,
searchType ,
@ -367,19 +404,25 @@ export async function searchBySubscription(
"subscription_search: Searching primary relay with filter:" ,
"subscription_search: Searching primary relay with filter:" ,
searchFilter . filter ,
searchFilter . filter ,
) ;
) ;
// Add timeout to primary relay search
// Add timeout to primary relay search
const primaryEventsPromise = ndk . fetchEvents (
const primaryEventsPromise = ndk . fetchEvents (
searchFilter . filter ,
searchFilter . filter ,
{ closeOnEose : true } ,
{ closeOnEose : true } ,
primaryRelaySet ,
primaryRelaySet ,
) ;
) ;
const timeoutPromise = new Promise ( ( _ , reject ) = > {
const timeoutPromise = new Promise ( ( _ , reject ) = > {
setTimeout ( ( ) = > reject ( new Error ( "Primary relay search timeout" ) ) , TIMEOUTS . SUBSCRIPTION_SEARCH ) ;
setTimeout (
( ) = > reject ( new Error ( "Primary relay search timeout" ) ) ,
TIMEOUTS . SUBSCRIPTION_SEARCH ,
) ;
} ) ;
} ) ;
const primaryEvents = await Promise . race ( [ primaryEventsPromise , timeoutPromise ] ) as any ;
const primaryEvents = await Promise . race ( [
primaryEventsPromise ,
timeoutPromise ,
] ) as any ;
console . log (
console . log (
"subscription_search: Primary relay returned" ,
"subscription_search: Primary relay returned" ,
@ -429,7 +472,7 @@ export async function searchBySubscription(
console . log (
console . log (
` subscription_search: Profile search completed in ${ elapsed } ms ` ,
` subscription_search: Profile search completed in ${ elapsed } ms ` ,
) ;
) ;
// Clear the main timeout since we're returning early
// Clear the main timeout since we're returning early
cleanup ( ) ;
cleanup ( ) ;
return immediateResult ;
return immediateResult ;
@ -471,12 +514,18 @@ export async function searchBySubscription(
{ closeOnEose : true } ,
{ closeOnEose : true } ,
allRelaySet ,
allRelaySet ,
) ;
) ;
const fallbackTimeoutPromise = new Promise ( ( _ , reject ) = > {
const fallbackTimeoutPromise = new Promise ( ( _ , reject ) = > {
setTimeout ( ( ) = > reject ( new Error ( "Fallback search timeout" ) ) , TIMEOUTS . SUBSCRIPTION_SEARCH ) ;
setTimeout (
( ) = > reject ( new Error ( "Fallback search timeout" ) ) ,
TIMEOUTS . SUBSCRIPTION_SEARCH ,
) ;
} ) ;
} ) ;
const fallbackEvents = await Promise . race ( [ fallbackEventsPromise , fallbackTimeoutPromise ] ) as any ;
const fallbackEvents = await Promise . race ( [
fallbackEventsPromise ,
fallbackTimeoutPromise ,
] ) as any ;
console . log (
console . log (
"subscription_search: Fallback search returned" ,
"subscription_search: Fallback search returned" ,
@ -508,7 +557,7 @@ export async function searchBySubscription(
console . log (
console . log (
` subscription_search: Profile search completed in ${ elapsed } ms (fallback) ` ,
` subscription_search: Profile search completed in ${ elapsed } ms (fallback) ` ,
) ;
) ;
// Clear the main timeout since we're returning early
// Clear the main timeout since we're returning early
cleanup ( ) ;
cleanup ( ) ;
return fallbackResult ;
return fallbackResult ;
@ -518,10 +567,15 @@ export async function searchBySubscription(
"subscription_search: Fallback search failed:" ,
"subscription_search: Fallback search failed:" ,
fallbackError ,
fallbackError ,
) ;
) ;
// If it's a timeout error, continue to return empty result
// If it's a timeout error, continue to return empty result
if ( fallbackError instanceof Error && fallbackError . message . includes ( "timeout" ) ) {
if (
console . log ( "subscription_search: Fallback search timed out, returning empty result" ) ;
fallbackError instanceof Error &&
fallbackError . message . includes ( "timeout" )
) {
console . log (
"subscription_search: Fallback search timed out, returning empty result" ,
) ;
}
}
}
}
@ -538,7 +592,7 @@ export async function searchBySubscription(
console . log (
console . log (
` subscription_search: Profile search completed in ${ elapsed } ms (not found) ` ,
` subscription_search: Profile search completed in ${ elapsed } ms (not found) ` ,
) ;
) ;
// Clear the main timeout since we're returning early
// Clear the main timeout since we're returning early
cleanup ( ) ;
cleanup ( ) ;
return emptyResult ;
return emptyResult ;
@ -553,10 +607,12 @@ export async function searchBySubscription(
` subscription_search: Error searching primary relay: ` ,
` subscription_search: Error searching primary relay: ` ,
error ,
error ,
) ;
) ;
// If it's a timeout error, continue to Phase 2 instead of failing
// If it's a timeout error, continue to Phase 2 instead of failing
if ( error instanceof Error && error . message . includes ( "timeout" ) ) {
if ( error instanceof Error && error . message . includes ( "timeout" ) ) {
console . log ( "subscription_search: Primary relay search timed out, continuing to Phase 2" ) ;
console . log (
"subscription_search: Primary relay search timed out, continuing to Phase 2" ,
) ;
} else {
} else {
// For other errors, we might want to fail the search
// For other errors, we might want to fail the search
throw error ;
throw error ;
@ -669,12 +725,12 @@ async function createSearchFilter(
// This properly handles NIP-05 lookups and name searches
// This properly handles NIP-05 lookups and name searches
const { searchProfiles } = await import ( "./profile_search.ts" ) ;
const { searchProfiles } = await import ( "./profile_search.ts" ) ;
const profileResult = await searchProfiles ( normalizedSearchTerm , ndk ) ;
const profileResult = await searchProfiles ( normalizedSearchTerm , ndk ) ;
// Convert profile results to events for compatibility
// Convert profile results to events for compatibility
const events = profileResult . profiles . map ( ( profile ) = > {
const events = profileResult . profiles . map ( ( profile ) = > {
const event = new NDKEvent ( ndk ) ;
const event = new NDKEvent ( ndk ) ;
event . content = JSON . stringify ( profile ) ;
event . content = JSON . stringify ( profile ) ;
// AI-NOTE: Convert npub to hex public key for compatibility with nprofileEncode
// AI-NOTE: Convert npub to hex public key for compatibility with nprofileEncode
// The profile.pubkey is an npub (bech32-encoded), but nprofileEncode expects hex-encoded public key
// The profile.pubkey is an npub (bech32-encoded), but nprofileEncode expects hex-encoded public key
let hexPubkey = profile . pubkey || "" ;
let hexPubkey = profile . pubkey || "" ;
@ -685,26 +741,36 @@ async function createSearchFilter(
hexPubkey = decoded . data as string ;
hexPubkey = decoded . data as string ;
}
}
} catch ( e ) {
} catch ( e ) {
console . warn ( "subscription_search: Failed to decode npub:" , profile . pubkey , e ) ;
console . warn (
"subscription_search: Failed to decode npub:" ,
profile . pubkey ,
e ,
) ;
}
}
}
}
event . pubkey = hexPubkey ;
event . pubkey = hexPubkey ;
event . kind = 0 ;
event . kind = 0 ;
// AI-NOTE: Use the preserved created_at timestamp from the profile
// AI-NOTE: Use the preserved created_at timestamp from the profile
// This ensures the profile cards show the actual creation date instead of "Unknown date"
// This ensures the profile cards show the actual creation date instead of "Unknown date"
if ( ( profile as any ) . created_at ) {
if ( ( profile as any ) . created_at ) {
event . created_at = ( profile as any ) . created_at ;
event . created_at = ( profile as any ) . created_at ;
console . log ( "subscription_search: Using preserved timestamp:" , event . created_at ) ;
console . log (
"subscription_search: Using preserved timestamp:" ,
event . created_at ,
) ;
} else {
} else {
// Fallback to current timestamp if no preserved timestamp
// Fallback to current timestamp if no preserved timestamp
event . created_at = Math . floor ( Date . now ( ) / 1000 ) ;
event . created_at = Math . floor ( Date . now ( ) / 1000 ) ;
console . log ( "subscription_search: Using fallback timestamp:" , event . created_at ) ;
console . log (
"subscription_search: Using fallback timestamp:" ,
event . created_at ,
) ;
}
}
return event ;
return event ;
} ) ;
} ) ;
// Return a mock filter since we're using the profile search directly
// Return a mock filter since we're using the profile search directly
const nFilter = {
const nFilter = {
filter : { kinds : [ 0 ] , limit : 1 } , // Dummy filter
filter : { kinds : [ 0 ] , limit : 1 } , // Dummy filter
@ -712,7 +778,10 @@ async function createSearchFilter(
searchTerm : normalizedSearchTerm ,
searchTerm : normalizedSearchTerm ,
preloadedEvents : events , // AI-NOTE: Pass preloaded events
preloadedEvents : events , // AI-NOTE: Pass preloaded events
} ;
} ;
console . log ( "subscription_search: Created profile filter with preloaded events:" , nFilter ) ;
console . log (
"subscription_search: Created profile filter with preloaded events:" ,
nFilter ,
) ;
return nFilter ;
return nFilter ;
}
}
default : {
default : {
@ -721,8 +790,6 @@ async function createSearchFilter(
}
}
}
}
/ * *
/ * *
* Create primary relay set for search operations
* Create primary relay set for search operations
* AI - NOTE : Updated to use all available relays to prevent search failures
* AI - NOTE : Updated to use all available relays to prevent search failures
@ -816,7 +883,9 @@ function processPrimaryRelayResults(
for ( const event of events ) {
for ( const event of events ) {
// Check if we've reached the event limit
// Check if we've reached the event limit
if ( processedCount >= maxEvents ) {
if ( processedCount >= maxEvents ) {
console . log ( ` subscription_search: Reached event limit of ${ maxEvents } in primary relay processing ` ) ;
console . log (
` subscription_search: Reached event limit of ${ maxEvents } in primary relay processing ` ,
) ;
break ;
break ;
}
}
@ -1029,13 +1098,15 @@ function searchOtherRelaysInBackground(
sub . on ( "event" , ( event : NDKEvent ) = > {
sub . on ( "event" , ( event : NDKEvent ) = > {
// Check if we've reached the event limit
// Check if we've reached the event limit
if ( eventCount >= maxEvents ) {
if ( eventCount >= maxEvents ) {
console . log ( ` subscription_search: Reached event limit of ${ maxEvents } , stopping event processing ` ) ;
console . log (
` subscription_search: Reached event limit of ${ maxEvents } , stopping event processing ` ,
) ;
sub . stop ( ) ;
sub . stop ( ) ;
return ;
return ;
}
}
eventCount ++ ;
eventCount ++ ;
try {
try {
if ( searchType === "n" ) {
if ( searchType === "n" ) {
processProfileEvent (
processProfileEvent (
@ -1054,11 +1125,13 @@ function searchOtherRelaysInBackground(
return new Promise < SearchResult > ( ( resolve ) = > {
return new Promise < SearchResult > ( ( resolve ) = > {
let resolved = false ;
let resolved = false ;
// Add timeout to prevent hanging
// Add timeout to prevent hanging
const timeoutId = setTimeout ( async ( ) = > {
const timeoutId = setTimeout ( async ( ) = > {
if ( ! resolved ) {
if ( ! resolved ) {
console . log ( "subscription_search: Background search timeout, resolving with current results" ) ;
console . log (
"subscription_search: Background search timeout, resolving with current results" ,
) ;
resolved = true ;
resolved = true ;
sub . stop ( ) ;
sub . stop ( ) ;
const result = await processEoseResults (
const result = await processEoseResults (
@ -1073,7 +1146,7 @@ function searchOtherRelaysInBackground(
resolve ( result ) ;
resolve ( result ) ;
}
}
} , TIMEOUTS . SUBSCRIPTION_SEARCH ) ;
} , TIMEOUTS . SUBSCRIPTION_SEARCH ) ;
sub . on ( "eose" , async ( ) = > {
sub . on ( "eose" , async ( ) = > {
if ( ! resolved ) {
if ( ! resolved ) {
resolved = true ;
resolved = true ;
@ -1106,7 +1179,12 @@ async function processEoseResults(
if ( searchType === "n" ) {
if ( searchType === "n" ) {
return processProfileEoseResults ( searchState , searchFilter , ndk , callbacks ) ;
return processProfileEoseResults ( searchState , searchFilter , ndk , callbacks ) ;
} else if ( searchType === "d" ) {
} else if ( searchType === "d" ) {
return await processContentEoseResults ( searchState , searchType , ndk , callbacks ) ;
return await processContentEoseResults (
searchState ,
searchType ,
ndk ,
callbacks ,
) ;
} else if ( searchType === "t" ) {
} else if ( searchType === "t" ) {
return await processTTagEoseResults ( searchState , ndk ) ;
return await processTTagEoseResults ( searchState , ndk ) ;
}
}
@ -1242,7 +1320,7 @@ async function processContentEoseResults(
dedupedEvents ,
dedupedEvents ,
undefined , // No specific target pubkey for d-tag searches
undefined , // No specific target pubkey for d-tag searches
SEARCH_LIMITS . GENERAL_CONTENT ,
SEARCH_LIMITS . GENERAL_CONTENT ,
ndk
ndk ,
) ;
) ;
// AI-NOTE: Attach profile data to first-order events for display
// AI-NOTE: Attach profile data to first-order events for display
@ -1276,7 +1354,10 @@ async function processContentEoseResults(
/ * *
/ * *
* Process t - tag EOSE results
* Process t - tag EOSE results
* /
* /
async function processTTagEoseResults ( searchState : any , ndk? : NDK ) : Promise < SearchResult > {
async function processTTagEoseResults (
searchState : any ,
ndk? : NDK ,
) : Promise < SearchResult > {
if ( searchState . tTagEvents . length === 0 ) {
if ( searchState . tTagEvents . length === 0 ) {
return createEmptySearchResult ( "t" , searchState . normalizedSearchTerm ) ;
return createEmptySearchResult ( "t" , searchState . normalizedSearchTerm ) ;
}
}
@ -1287,7 +1368,7 @@ async function processTTagEoseResults(searchState: any, ndk?: NDK): Promise<Sear
searchState . tTagEvents ,
searchState . tTagEvents ,
undefined , // No specific target pubkey for t-tag searches
undefined , // No specific target pubkey for t-tag searches
SEARCH_LIMITS . GENERAL_CONTENT ,
SEARCH_LIMITS . GENERAL_CONTENT ,
ndk
ndk ,
) ;
) ;
// AI-NOTE: Attach profile data to t-tag events for display
// AI-NOTE: Attach profile data to t-tag events for display
@ -1458,10 +1539,12 @@ async function performSecondOrderSearchInBackground(
// Race between fetch and timeout - only timeout the initial event fetching
// Race between fetch and timeout - only timeout the initial event fetching
await Promise . race ( [ fetchPromise , fetchTimeoutPromise ] ) ;
await Promise . race ( [ fetchPromise , fetchTimeoutPromise ] ) ;
// Now do the prioritization without timeout
// Now do the prioritization without timeout
console . log ( "subscription_search: Event fetching completed, starting prioritization..." ) ;
console . log (
"subscription_search: Event fetching completed, starting prioritization..." ,
) ;
// Deduplicate by event ID
// Deduplicate by event ID
const uniqueSecondOrder = new Map < string , NDKEvent > ( ) ;
const uniqueSecondOrder = new Map < string , NDKEvent > ( ) ;
allSecondOrderEvents . forEach ( ( event ) = > {
allSecondOrderEvents . forEach ( ( event ) = > {
@ -1484,18 +1567,18 @@ async function performSecondOrderSearchInBackground(
deduplicatedSecondOrder ,
deduplicatedSecondOrder ,
targetPubkey ,
targetPubkey ,
SEARCH_LIMITS . SECOND_ORDER_RESULTS ,
SEARCH_LIMITS . SECOND_ORDER_RESULTS ,
ndk
ndk ,
) ;
) ;
const prioritizationTimeoutPromise = new Promise ( ( _ , reject ) = > {
const prioritizationTimeoutPromise = new Promise ( ( _ , reject ) = > {
setTimeout ( ( ) = > reject ( new Error ( 'Prioritization timeout' ) ) , 15000 ) ; // 15 second timeout
setTimeout ( ( ) = > reject ( new Error ( "Prioritization timeout" ) ) , 15000 ) ; // 15 second timeout
} ) ;
} ) ;
let prioritizedSecondOrder : NDKEvent [ ] ;
let prioritizedSecondOrder : NDKEvent [ ] ;
try {
try {
prioritizedSecondOrder = await Promise . race ( [
prioritizedSecondOrder = await Promise . race ( [
prioritizationPromise ,
prioritizationPromise ,
prioritizationTimeoutPromise
prioritizationTimeoutPromise ,
] ) as NDKEvent [ ] ;
] ) as NDKEvent [ ] ;
console . log (
console . log (
@ -1504,7 +1587,10 @@ async function performSecondOrderSearchInBackground(
"prioritized results" ,
"prioritized results" ,
) ;
) ;
} catch ( error ) {
} catch ( error ) {
console . warn ( "subscription_search: Prioritization failed, using simple sorting:" , error ) ;
console . warn (
"subscription_search: Prioritization failed, using simple sorting:" ,
error ,
) ;
// Fallback to simple sorting if prioritization fails
// Fallback to simple sorting if prioritization fails
prioritizedSecondOrder = deduplicatedSecondOrder . sort ( ( a , b ) = > {
prioritizedSecondOrder = deduplicatedSecondOrder . sort ( ( a , b ) = > {
// Prioritize events from target pubkey first (for n: searches)
// Prioritize events from target pubkey first (for n: searches)
@ -1514,17 +1600,17 @@ async function performSecondOrderSearchInBackground(
if ( aIsTarget && ! bIsTarget ) return - 1 ;
if ( aIsTarget && ! bIsTarget ) return - 1 ;
if ( ! aIsTarget && bIsTarget ) return 1 ;
if ( ! aIsTarget && bIsTarget ) return 1 ;
}
}
// Prioritize by event kind (for t: searches and general prioritization)
// Prioritize by event kind (for t: searches and general prioritization)
const aIsPrioritized = PRIORITIZED_EVENT_KINDS . has ( a . kind || 0 ) ;
const aIsPrioritized = PRIORITIZED_EVENT_KINDS . has ( a . kind || 0 ) ;
const bIsPrioritized = PRIORITIZED_EVENT_KINDS . has ( b . kind || 0 ) ;
const bIsPrioritized = PRIORITIZED_EVENT_KINDS . has ( b . kind || 0 ) ;
if ( aIsPrioritized && ! bIsPrioritized ) return - 1 ;
if ( aIsPrioritized && ! bIsPrioritized ) return - 1 ;
if ( ! aIsPrioritized && bIsPrioritized ) return 1 ;
if ( ! aIsPrioritized && bIsPrioritized ) return 1 ;
// Then sort by creation time (newest first)
// Then sort by creation time (newest first)
return ( b . created_at || 0 ) - ( a . created_at || 0 ) ;
return ( b . created_at || 0 ) - ( a . created_at || 0 ) ;
} ) . slice ( 0 , SEARCH_LIMITS . SECOND_ORDER_RESULTS ) ;
} ) . slice ( 0 , SEARCH_LIMITS . SECOND_ORDER_RESULTS ) ;
console . log (
console . log (
"subscription_search: Using fallback sorting with" ,
"subscription_search: Using fallback sorting with" ,
prioritizedSecondOrder . length ,
prioritizedSecondOrder . length ,
@ -1577,20 +1663,27 @@ async function performSecondOrderSearchInBackground(
* @param ndk NDK instance for fetching profile data
* @param ndk NDK instance for fetching profile data
* @returns Promise that resolves when profile data is attached
* @returns Promise that resolves when profile data is attached
* /
* /
async function attachProfileDataToEvents ( events : NDKEvent [ ] , ndk : NDK ) : Promise < void > {
async function attachProfileDataToEvents (
events : NDKEvent [ ] ,
ndk : NDK ,
) : Promise < void > {
if ( events . length === 0 ) {
if ( events . length === 0 ) {
return ;
return ;
}
}
console . log ( ` subscription_search: Attaching profile data to ${ events . length } events ` ) ;
console . log (
` subscription_search: Attaching profile data to ${ events . length } events ` ,
) ;
try {
try {
// Import user list functions dynamically to avoid circular dependencies
// Import user list functions dynamically to avoid circular dependencies
const { fetchCurrentUserLists , isPubkeyInUserLists } = await import ( "./user_lists.ts" ) ;
const { fetchCurrentUserLists , isPubkeyInUserLists } = await import (
"./user_lists.ts"
) ;
// Get current user's lists for user list status
// Get current user's lists for user list status
const userLists = await fetchCurrentUserLists ( undefined , ndk ) ;
const userLists = await fetchCurrentUserLists ( undefined , ndk ) ;
// Get unique pubkeys from events
// Get unique pubkeys from events
const uniquePubkeys = new Set < string > ( ) ;
const uniquePubkeys = new Set < string > ( ) ;
events . forEach ( ( event ) = > {
events . forEach ( ( event ) = > {
@ -1599,39 +1692,46 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
}
}
} ) ;
} ) ;
console . log ( ` subscription_search: Found ${ uniquePubkeys . size } unique pubkeys to fetch profiles for ` ) ;
console . log (
` subscription_search: Found ${ uniquePubkeys . size } unique pubkeys to fetch profiles for ` ,
) ;
// Fetch profile data for each unique pubkey
// Fetch profile data for each unique pubkey
const profilePromises = Array . from ( uniquePubkeys ) . map ( async ( pubkey ) = > {
const profilePromises = Array . from ( uniquePubkeys ) . map ( async ( pubkey ) = > {
try {
try {
// Import getUserMetadata dynamically to avoid circular dependencies
// Import getUserMetadata dynamically to avoid circular dependencies
const { getUserMetadata } = await import ( "./nostrUtils.ts" ) ;
const { getUserMetadata } = await import ( "./nostrUtils.ts" ) ;
const npub = await import ( "./nostrUtils.ts" ) . then ( m = > m . toNpub ( pubkey ) ) ;
const npub = await import ( "./nostrUtils.ts" ) . then ( ( m ) = >
m . toNpub ( pubkey )
) ;
if ( npub ) {
if ( npub ) {
const profileData = await getUserMetadata ( npub , ndk , true ) ;
const profileData = await getUserMetadata ( npub , ndk , true ) ;
if ( profileData ) {
if ( profileData ) {
// Check if this pubkey is in user's lists
// Check if this pubkey is in user's lists
const isInLists = isPubkeyInUserLists ( pubkey , userLists ) ;
const isInLists = isPubkeyInUserLists ( pubkey , userLists ) ;
// Return profile data with user list status
// Return profile data with user list status
return {
return {
pubkey ,
pubkey ,
profileData : {
profileData : {
. . . profileData ,
. . . profileData ,
isInUserLists : isInLists
isInUserLists : isInLists ,
}
} ,
} ;
} ;
}
}
}
}
} catch ( error ) {
} catch ( error ) {
console . warn ( ` subscription_search: Failed to fetch profile for ${ pubkey } : ` , error ) ;
console . warn (
` subscription_search: Failed to fetch profile for ${ pubkey } : ` ,
error ,
) ;
}
}
return null ;
return null ;
} ) ;
} ) ;
const profileResults = await Promise . allSettled ( profilePromises ) ;
const profileResults = await Promise . allSettled ( profilePromises ) ;
// Create a map of pubkey to profile data
// Create a map of pubkey to profile data
const profileMap = new Map < string , any > ( ) ;
const profileMap = new Map < string , any > ( ) ;
profileResults . forEach ( ( result ) = > {
profileResults . forEach ( ( result ) = > {
@ -1640,7 +1740,9 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
}
}
} ) ;
} ) ;
console . log ( ` subscription_search: Successfully fetched ${ profileMap . size } profiles ` ) ;
console . log (
` subscription_search: Successfully fetched ${ profileMap . size } profiles ` ,
) ;
// Attach profile data to each event
// Attach profile data to each event
events . forEach ( ( event ) = > {
events . forEach ( ( event ) = > {