@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
import { ndkInstance , activeInboxRelays } from "../ndk.ts" ;
import { ndkInstance , activeInboxRelays , activeOutboxRelays } from "../ndk.ts" ;
import { getUserMetadata , getNpubFromNip05 } from "./nostrUtils.ts" ;
import NDK , { NDKRelaySet , NDKEvent } from "@nostr-dev-kit/ndk" ;
import { searchCache } from "./searchCache.ts" ;
import { searchRelays , communityRelays , secondaryRelays } from "../consts.ts" ;
import { searchRelays , communityRelays , secondaryRelays , localRelays } from "../consts.ts" ;
import { get } from "svelte/store" ;
import type { NostrProfile , ProfileSearchResult } from "./search_types.ts" ;
import {
@ -11,138 +11,418 @@ import {
@@ -11,138 +11,418 @@ import {
normalizeSearchTerm ,
createProfileFromEvent ,
} from "./search_utils.ts" ;
import {
fetchCurrentUserLists ,
getPubkeysFromUserLists ,
isPubkeyInUserLists ,
getListKindsForPubkey ,
updateProfileCacheForPubkeys ,
PEOPLE_LIST_KINDS
} from "./user_lists.ts" ;
import { nip19 } from "nostr-tools" ;
import { TIMEOUTS , SEARCH_LIMITS , CACHE_DURATIONS } from "./search_constants.ts" ;
// AI-NOTE: 2025-01-24 - User list cache with stale-while-revalidate for performance
// This prevents redundant relay queries by caching user lists for 5 minutes
// Fresh cache: Return immediately
// Stale cache: Return stale data immediately, update in background
// No cache: Wait for fresh data
/ * *
* Search for profiles by various criteria ( display name , name , NIP - 05 , npub )
* User list cache interface
* /
export async function searchProfiles (
searchTerm : string ,
) : Promise < ProfileSearchResult > {
const normalizedSearchTerm = normalizeSearchTerm ( searchTerm ) ;
interface UserListCache {
lists : any [ ] ;
pubkeys : Set < string > ;
lastUpdated : number ;
isUpdating : boolean ;
}
console . log (
"searchProfiles called with:" ,
searchTerm ,
"normalized:" ,
normalizedSearchTerm ,
) ;
/ * *
* Search strategy types
* /
type SearchStrategy = 'npub' | 'nip05' | 'userLists' | 'nip05Domains' | 'relaySearch' ;
// Check cache first
const cachedResult = searchCache . get ( "profile" , normalizedSearchTerm ) ;
if ( cachedResult ) {
console . log ( "Found cached result for:" , normalizedSearchTerm ) ;
const profiles = cachedResult . events
. map ( ( event ) = > {
try {
const profileData = JSON . parse ( event . content ) ;
return createProfileFromEvent ( event , profileData ) ;
} catch {
return null ;
}
} )
. filter ( Boolean ) as NostrProfile [ ] ;
/ * *
* Global user list cache instance
* /
let userListCache : UserListCache | null = null ;
console . log ( "Cached profiles found:" , profiles . length ) ;
return { profiles , Status : { } } ;
/ * *
* Get user lists with stale - while - revalidate caching
* Returns cached data immediately if available , updates in background if stale
* /
async function getUserListsWithCache ( ) : Promise < { lists : any [ ] ; pubkeys : Set < string > } > {
const now = Date . now ( ) ;
// If we have fresh cache, return it immediately
if ( userListCache && ( now - userListCache . lastUpdated ) < CACHE_DURATIONS . SEARCH_CACHE ) {
console . log ( "profile_search: Using fresh user list cache" ) ;
return {
lists : userListCache.lists ,
pubkeys : userListCache.pubkeys
} ;
}
// If we have stale cache and no update in progress, return stale data and update in background
if ( userListCache && ! userListCache . isUpdating ) {
console . log ( "profile_search: Using stale user list cache, updating in background" ) ;
// Start background update
userListCache . isUpdating = true ;
updateUserListCacheInBackground ( ) . catch ( error = > {
console . warn ( "profile_search: Background user list cache update failed:" , error ) ;
if ( userListCache ) {
userListCache . isUpdating = false ;
}
} ) ;
return {
lists : userListCache.lists ,
pubkeys : userListCache.pubkeys
} ;
}
// If no cache or update in progress, wait for fresh data
console . log ( "profile_search: Fetching fresh user lists" ) ;
return await updateUserListCache ( ) ;
}
/ * *
* Update user list cache in background
* /
async function updateUserListCacheInBackground ( ) : Promise < void > {
try {
const { lists , pubkeys } = await updateUserListCache ( ) ;
console . log ( "profile_search: Background user list cache update completed" ) ;
} catch ( error ) {
console . warn ( "profile_search: Background user list cache update failed:" , error ) ;
} finally {
if ( userListCache ) {
userListCache . isUpdating = false ;
}
}
}
/ * *
* Update user list cache with fresh data
* /
async function updateUserListCache ( ) : Promise < { lists : any [ ] ; pubkeys : Set < string > } > {
const lists = await fetchCurrentUserLists ( [ . . . PEOPLE_LIST_KINDS ] ) ;
const pubkeys = getPubkeysFromUserLists ( lists ) ;
userListCache = {
lists ,
pubkeys ,
lastUpdated : Date.now ( ) ,
isUpdating : false
} ;
console . log ( ` profile_search: Updated user list cache with ${ lists . length } lists and ${ pubkeys . size } pubkeys ` ) ;
// Update profile cache for all user list pubkeys to ensure follows are cached
if ( pubkeys . size > 0 ) {
updateProfileCacheForPubkeys ( Array . from ( pubkeys ) ) . catch ( error = > {
console . warn ( "profile_search: Failed to update profile cache:" , error ) ;
} ) ;
}
return { lists , pubkeys } ;
}
/ * *
* Clear user list cache ( useful for logout or force refresh )
* /
export function clearUserListCache ( ) : void {
userListCache = null ;
console . log ( "profile_search: User list cache cleared" ) ;
}
/ * *
* Force refresh user list cache ( useful when user follows / unfollows someone )
* /
export async function refreshUserListCache ( ) : Promise < void > {
console . log ( "profile_search: Forcing user list cache refresh" ) ;
userListCache = null ;
await updateUserListCache ( ) ;
}
/ * *
* Get user list cache status for debugging
* /
export function getUserListCacheStatus ( ) : {
hasCache : boolean ;
isStale : boolean ;
isUpdating : boolean ;
ageMinutes : number | null ;
listCount : number | null ;
pubkeyCount : number | null ;
} {
if ( ! userListCache ) {
return {
hasCache : false ,
isStale : false ,
isUpdating : false ,
ageMinutes : null ,
listCount : null ,
pubkeyCount : null
} ;
}
const ndk = get ( ndkInstance ) ;
const now = Date . now ( ) ;
const ageMs = now - userListCache . lastUpdated ;
const ageMinutes = Math . round ( ageMs / ( 60 * 1000 ) ) ;
const isStale = ageMs > CACHE_DURATIONS . SEARCH_CACHE ;
return {
hasCache : true ,
isStale ,
isUpdating : userListCache.isUpdating ,
ageMinutes ,
listCount : userListCache.lists.length ,
pubkeyCount : userListCache.pubkeys.size
} ;
}
/ * *
* Wait for NDK to be properly initialized
* /
async function waitForNdk ( ) : Promise < NDK > {
let ndk = get ( ndkInstance ) ;
if ( ! ndk ) {
console . error ( "NDK not initialized" ) ;
throw new Error ( "NDK not initialized" ) ;
console . log ( "profile_search: Waiting for NDK initialization..." ) ;
let retryCount = 0 ;
const maxRetries = 10 ;
const retryDelay = 500 ; // milliseconds
while ( retryCount < maxRetries && ! ndk ) {
await new Promise ( resolve = > setTimeout ( resolve , retryDelay ) ) ;
ndk = get ( ndkInstance ) ;
retryCount ++ ;
}
if ( ! ndk ) {
console . error ( "profile_search: NDK not initialized after waiting" ) ;
throw new Error ( "NDK not initialized" ) ;
}
}
console . log ( "NDK initialized, starting search logic" ) ;
return ndk ;
}
let foundProfiles : NostrProfile [ ] = [ ] ;
/ * *
* Check if search term is a valid npub / nprofile identifier
* /
function isNostrIdentifier ( searchTerm : string ) : boolean {
return searchTerm . startsWith ( "npub" ) || searchTerm . startsWith ( "nprofile" ) ;
}
/ * *
* Check if search term is a NIP - 05 address
* /
function isNip05Address ( searchTerm : string ) : boolean {
return searchTerm . includes ( "@" ) ;
}
/ * *
* Determine search strategy based on search term
* /
function determineSearchStrategy ( searchTerm : string ) : SearchStrategy {
if ( isNostrIdentifier ( searchTerm ) ) {
return 'npub' ;
}
if ( isNip05Address ( searchTerm ) ) {
return 'nip05' ;
}
return 'userLists' ; // Default to user lists first, then other strategies
}
/ * *
* Search for profiles by npub / nprofile identifier
* /
async function searchByNostrIdentifier ( searchTerm : string , ndk : NDK ) : Promise < NostrProfile [ ] > {
try {
// Check if it's a valid npub/nprofile first
if (
normalizedSearchTerm . startsWith ( "npub" ) ||
normalizedSearchTerm . startsWith ( "nprofile" )
) {
try {
const metadata = await getUserMetadata ( normalizedSearchTerm ) ;
if ( metadata ) {
foundProfiles = [ metadata ] ;
}
} catch ( error ) {
console . error ( "Error fetching metadata for npub:" , error ) ;
}
} else if ( normalizedSearchTerm . includes ( "@" ) ) {
// Check if it's a NIP-05 address - normalize it properly
const normalizedNip05 = normalizedSearchTerm . toLowerCase ( ) ;
try {
const npub = await getNpubFromNip05 ( normalizedNip05 ) ;
if ( npub ) {
const metadata = await getUserMetadata ( npub ) ;
const profile : NostrProfile = {
. . . metadata ,
pubkey : npub ,
} ;
foundProfiles = [ profile ] ;
}
} catch ( e ) {
console . error ( "[Search] NIP-05 lookup failed:" , e ) ;
}
const cleanId = searchTerm . replace ( /^nostr:/ , "" ) ;
const decoded = nip19 . decode ( cleanId ) ;
if ( ! decoded ) {
return [ ] ;
}
let pubkey : string ;
if ( decoded . type === "npub" ) {
pubkey = decoded . data ;
} else if ( decoded . type === "nprofile" ) {
pubkey = decoded . data . pubkey ;
} else {
// Try NIP-05 search first (faster than relay search)
console . log ( "Starting NIP-05 search for:" , normalizedSearchTerm ) ;
foundProfiles = await searchNip05Domains ( normalizedSearchTerm ) ;
console . log (
"NIP-05 search completed, found:" ,
foundProfiles . length ,
"profiles" ,
) ;
console . warn ( "Unsupported identifier type:" , decoded . type ) ;
return [ ] ;
}
// If no NIP-05 results, try quick relay search
if ( foundProfiles . length === 0 ) {
console . log ( "No NIP-05 results, trying quick relay search" ) ;
foundProfiles = await quickRelaySearch ( normalizedSearchTerm , ndk ) ;
console . log (
"Quick relay search completed, found:" ,
foundProfiles . length ,
"profiles" ,
) ;
// AI-NOTE: 2025-01-24 - For npub/nprofile searches, fetch the actual event to preserve timestamp
const events = await ndk . fetchEvents ( {
kinds : [ 0 ] ,
authors : [ pubkey ] ,
} ) ;
if ( events . size > 0 ) {
// Get the most recent profile event
const event = Array . from ( events ) . sort ( ( a , b ) = >
( b . created_at || 0 ) - ( a . created_at || 0 )
) [ 0 ] ;
if ( event && event . content ) {
try {
const profileData = JSON . parse ( event . content ) ;
const profile = createProfileFromEvent ( event , profileData ) ;
return [ profile ] ;
} catch ( error ) {
console . error ( "Error parsing profile content for npub:" , error ) ;
}
}
}
// Cache the results
if ( foundProfiles . length > 0 ) {
const events = foundProfiles . map ( ( profile ) = > {
const event = new NDKEvent ( ndk ) ;
event . content = JSON . stringify ( profile ) ;
event . pubkey = profile . pubkey || "" ;
return event ;
} ) ;
// Fallback to metadata
const metadata = await getUserMetadata ( searchTerm ) ;
const profileWithPubkey : NostrProfile = {
. . . metadata ,
pubkey : pubkey ,
} ;
return [ profileWithPubkey ] ;
} catch ( error ) {
console . error ( "Error fetching metadata for npub:" , error ) ;
return [ ] ;
}
}
const result = {
events ,
secondOrder : [ ] ,
tTagEvents : [ ] ,
eventIds : new Set < string > ( ) ,
addresses : new Set < string > ( ) ,
searchType : "profile" ,
searchTerm : normalizedSearchTerm ,
/ * *
* Search for profiles by NIP - 05 address
* /
async function searchByNip05Address ( searchTerm : string ) : Promise < NostrProfile [ ] > {
try {
const normalizedNip05 = searchTerm . toLowerCase ( ) ;
const npub = await getNpubFromNip05 ( normalizedNip05 ) ;
if ( npub ) {
const metadata = await getUserMetadata ( npub ) ;
const profile : NostrProfile = {
. . . metadata ,
pubkey : npub ,
} ;
searchCache . set ( "profile" , normalizedSearchTerm , result ) ;
return [ profile ] ;
}
console . log ( "Search completed, found profiles:" , foundProfiles . length ) ;
return { profiles : foundProfiles , Status : { } } ;
} catch ( error ) {
console . error ( "Error searching profiles:" , error ) ;
return { profiles : [ ] , Status : { } } ;
console . error ( "[Search] NIP-05 lookup failed:" , error ) ;
}
return [ ] ;
}
/ * *
* Search for NIP - 05 addresses across common domains
* Fuzzy match function for user list searche s
* /
async function searchNip05Domains (
function fuzzyMatch ( text : string , searchTerm : string ) : boolean {
if ( ! text || ! searchTerm ) return false ;
const normalizedText = text . toLowerCase ( ) ;
const normalizedSearchTerm = searchTerm . toLowerCase ( ) ;
// Direct substring match
if ( normalizedText . includes ( normalizedSearchTerm ) ) {
return true ;
}
// AI-NOTE: 2025-01-24 - More strict word boundary matching for profile searches
// Only match if the search term is a significant part of a word
const words = normalizedText . split ( /[\s\-_\.]+/ ) ;
for ( const word of words ) {
// Only match if search term is at least 3 characters and represents a significant part of the word
if ( normalizedSearchTerm . length >= 3 ) {
if ( word . includes ( normalizedSearchTerm ) || normalizedSearchTerm . includes ( word ) ) {
return true ;
}
}
}
return false ;
}
/ * *
* Search for profiles within user ' s lists with fuzzy matching
* /
async function searchWithinUserLists (
searchTerm : string ,
userLists : any [ ] ,
ndk : NDK ,
) : Promise < NostrProfile [ ] > {
const normalizedSearchTerm = normalizeSearchTerm ( searchTerm ) ;
const foundProfiles : NostrProfile [ ] = [ ] ;
const processedPubkeys = new Set < string > ( ) ;
// Get all pubkeys from user lists
const allPubkeys : string [ ] = [ ] ;
userLists . forEach ( list = > {
list . pubkeys . forEach ( ( pubkey : string ) = > {
if ( ! processedPubkeys . has ( pubkey ) ) {
allPubkeys . push ( pubkey ) ;
processedPubkeys . add ( pubkey ) ;
}
} ) ;
} ) ;
if ( allPubkeys . length === 0 ) {
return foundProfiles ;
}
console . log ( ` searchWithinUserLists: Searching ${ allPubkeys . length } pubkeys from user lists with fuzzy matching ` ) ;
// Fetch profiles for all pubkeys in batches
for ( let i = 0 ; i < allPubkeys . length ; i += SEARCH_LIMITS . BATCH_SIZE ) {
const batch = allPubkeys . slice ( i , i + SEARCH_LIMITS . BATCH_SIZE ) ;
try {
const events = await ndk . fetchEvents ( {
kinds : [ 0 ] ,
authors : batch ,
} ) ;
for ( const event of events ) {
try {
if ( ! event . content ) continue ;
const profileData = JSON . parse ( event . content ) ;
const displayName = profileData . displayName || profileData . display_name || "" ;
const name = profileData . name || "" ;
const nip05 = profileData . nip05 || "" ;
const about = profileData . about || "" ;
// Check if any field matches the search term with exact field matching only
const matchesDisplayName = fieldMatches ( displayName , normalizedSearchTerm ) ;
const matchesName = fieldMatches ( name , normalizedSearchTerm ) ;
const matchesNip05 = nip05Matches ( nip05 , normalizedSearchTerm ) ;
const matchesAbout = fieldMatches ( about , normalizedSearchTerm ) ;
if ( matchesDisplayName || matchesName || matchesNip05 || matchesAbout ) {
const profile = createProfileFromEvent ( event , profileData ) ;
foundProfiles . push ( profile ) ;
}
} catch {
// Invalid JSON, skip
}
}
} catch ( error ) {
console . warn ( "searchWithinUserLists: Error fetching batch:" , error ) ;
}
}
console . log ( ` searchWithinUserLists: Found ${ foundProfiles . length } matching profiles in user lists with fuzzy matching ` ) ;
return foundProfiles ;
}
/ * *
* Search for NIP - 05 addresses across common domains
* /
async function searchNip05Domains ( searchTerm : string ) : Promise < NostrProfile [ ] > {
const foundProfiles : NostrProfile [ ] = [ ] ;
// Enhanced list of common domains for NIP-05 lookups
@ -180,33 +460,25 @@ async function searchNip05Domains(
@@ -180,33 +460,25 @@ async function searchNip05Domains(
try {
const npub = await getNpubFromNip05 ( gitcitadelAddress ) ;
if ( npub ) {
console . log (
"NIP-05 search: SUCCESS! found npub for gitcitadel.com:" ,
npub ,
) ;
console . log ( "NIP-05 search: SUCCESS! found npub for gitcitadel.com:" , npub ) ;
const metadata = await getUserMetadata ( npub ) ;
const profile : NostrProfile = {
. . . metadata ,
pubkey : npub ,
} ;
console . log (
"NIP-05 search: created profile for gitcitadel.com:" ,
profile ,
) ;
console . log ( "NIP-05 search: created profile for gitcitadel.com:" , profile ) ;
foundProfiles . push ( profile ) ;
return foundProfiles ; // Return immediately if we found it on gitcitadel.com
} else {
console . log ( "NIP-05 search: no npub found for gitcitadel.com" ) ;
}
} catch ( e ) {
console . log ( "NIP-05 search: error for gitcitadel.com:" , e ) ;
} catch ( error ) {
console . log ( "NIP-05 search: error for gitcitadel.com:" , error ) ;
}
// If gitcitadel.com didn't work, try other domains
console . log ( "NIP-05 search: gitcitadel.com failed, trying other domains..." ) ;
const otherDomains = commonDomains . filter (
( domain ) = > domain !== "gitcitadel.com" ,
) ;
const otherDomains = commonDomains . filter ( domain = > domain !== "gitcitadel.com" ) ;
// Search all other domains in parallel with timeout
const searchPromises = otherDomains . map ( async ( domain ) = > {
@ -221,18 +493,13 @@ async function searchNip05Domains(
@@ -221,18 +493,13 @@ async function searchNip05Domains(
. . . metadata ,
pubkey : npub ,
} ;
console . log (
"NIP-05 search: created profile for" ,
nip05Address ,
":" ,
profile ,
) ;
console . log ( "NIP-05 search: created profile for" , nip05Address , ":" , profile ) ;
return profile ;
} else {
console . log ( "NIP-05 search: no npub found for" , nip05Address ) ;
}
} catch ( e ) {
console . log ( "NIP-05 search: error for" , nip05Address , ":" , e ) ;
} catch ( error ) {
console . log ( "NIP-05 search: error for" , nip05Address , ":" , error ) ;
// Continue to next domain
}
return null ;
@ -251,39 +518,57 @@ async function searchNip05Domains(
@@ -251,39 +518,57 @@ async function searchNip05Domains(
return foundProfiles ;
}
/ * *
* Get all available relay URLs for comprehensive search
* /
function getAllRelayUrls ( ) : string [ ] {
const userInboxRelays = get ( activeInboxRelays ) ;
const userOutboxRelays = get ( activeOutboxRelays ) ;
// AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive profile search coverage
// This includes all relays from consts.ts, user's personal relays, and local relays
const allRelayUrls = [
. . . searchRelays , // Dedicated profile search relays
. . . communityRelays , // Community relays
. . . secondaryRelays , // Secondary relays
. . . localRelays , // Local relays
. . . userInboxRelays , // User's personal inbox relays
. . . userOutboxRelays // User's personal outbox relays
] ;
// Deduplicate relay URLs
return [ . . . new Set ( allRelayUrls ) ] ;
}
/ * *
* Quick relay search with short timeout
* /
async function quickRelaySearch (
searchTerm : string ,
ndk : NDK ,
) : Promise < NostrProfile [ ] > {
async function quickRelaySearch ( searchTerm : string , ndk : NDK ) : Promise < NostrProfile [ ] > {
console . log ( "quickRelaySearch called with:" , searchTerm ) ;
// Normalize the search term for relay search
const normalizedSearchTerm = normalizeSearchTerm ( searchTerm ) ;
console . log ( "Normalized search term for relay search:" , normalizedSearchTerm ) ;
// Use search relays (optimized for profiles) + user's inbox relays + community relays
const userInboxRelays = get ( activeInboxRelays ) ;
const quickRelayUrls = [
. . . searchRelays , // Dedicated profile search relays
. . . userInboxRelays , // User's personal inbox relays
. . . communityRelays , // Community relays
. . . secondaryRelays // Secondary relays as fallback
] ;
// Deduplicate relay URLs
const uniqueRelayUrls = [ . . . new Set ( quickRelayUrls ) ] ;
console . log ( "Using relays for profile search:" , uniqueRelayUrls ) ;
const uniqueRelayUrls = getAllRelayUrls ( ) ;
console . log ( "Using ALL available relays for profile search:" , uniqueRelayUrls ) ;
console . log ( "Relay breakdown:" , {
searchRelays : searchRelays.length ,
communityRelays : communityRelays.length ,
secondaryRelays : secondaryRelays.length ,
localRelays : localRelays.length ,
userInboxRelays : get ( activeInboxRelays ) . length ,
userOutboxRelays : get ( activeOutboxRelays ) . length ,
totalUnique : uniqueRelayUrls.length
} ) ;
// Create relay sets for parallel search
const relaySets = uniqueRelayUrls
. map ( ( url ) = > {
try {
return NDKRelaySet . fromRelayUrls ( [ url ] , ndk ) ;
} catch ( e ) {
console . warn ( ` Failed to create relay set for ${ url } : ` , e ) ;
} catch ( error ) {
console . warn ( ` Failed to create relay set for ${ url } : ` , error ) ;
return null ;
}
} )
@ -297,9 +582,7 @@ async function quickRelaySearch(
@@ -297,9 +582,7 @@ async function quickRelaySearch(
const foundInRelay : NostrProfile [ ] = [ ] ;
let eventCount = 0 ;
console . log (
` Starting search on relay ${ index + 1 } : ${ uniqueRelayUrls [ index ] } ` ,
) ;
console . log ( ` Starting search on relay ${ index + 1 } : ${ uniqueRelayUrls [ index ] } ` ) ;
const sub = ndk . subscribe (
{ kinds : [ 0 ] } ,
@ -312,22 +595,15 @@ async function quickRelaySearch(
@@ -312,22 +595,15 @@ async function quickRelaySearch(
try {
if ( ! event . content ) return ;
const profileData = JSON . parse ( event . content ) ;
const displayName =
profileData . displayName || profileData . display_name || "" ;
const displayName = profileData . displayName || profileData . display_name || "" ;
const display_name = profileData . display_name || "" ;
const name = profileData . name || "" ;
const nip05 = profileData . nip05 || "" ;
const about = profileData . about || "" ;
// Check if any field matches the search term using normalized comparison
const matchesDisplayName = fieldMatches (
displayName ,
normalizedSearchTerm ,
) ;
const matchesDisplay_name = fieldMatches (
display_name ,
normalizedSearchTerm ,
) ;
// Check if any field matches the search term using exact field matching only
const matchesDisplayName = fieldMatches ( displayName , normalizedSearchTerm ) ;
const matchesDisplay_name = fieldMatches ( display_name , normalizedSearchTerm ) ;
const matchesName = fieldMatches ( name , normalizedSearchTerm ) ;
const matchesNip05 = nip05Matches ( nip05 , normalizedSearchTerm ) ;
const matchesAbout = fieldMatches ( about , normalizedSearchTerm ) ;
@ -375,7 +651,7 @@ async function quickRelaySearch(
@@ -375,7 +651,7 @@ async function quickRelaySearch(
) ;
sub . stop ( ) ;
resolve ( foundInRelay ) ;
} , 1500 ) ; // 1.5 second timeout per relay
} , TIMEOUTS . RELAY_TIMEOUT ) ;
} ) ;
} ) ;
@ -395,8 +671,211 @@ async function quickRelaySearch(
@@ -395,8 +671,211 @@ async function quickRelaySearch(
}
}
console . log (
` Total unique profiles found: ${ Object . keys ( allProfiles ) . length } ` ,
) ;
console . log ( ` Total unique profiles found: ${ Object . keys ( allProfiles ) . length } ` ) ;
return Object . values ( allProfiles ) ;
}
/ * *
* Add user list information to profiles and prioritize them
* /
function prioritizeProfiles ( profiles : NostrProfile [ ] , userLists : any [ ] ) : NostrProfile [ ] {
return profiles . map ( profile = > {
if ( profile . pubkey ) {
const inLists = isPubkeyInUserLists ( profile . pubkey , userLists ) ;
const listKinds = getListKindsForPubkey ( profile . pubkey , userLists ) ;
return {
. . . profile ,
isInUserLists : inLists ,
listKinds : listKinds ,
} ;
}
return profile ;
} ) . sort ( ( a , b ) = > {
const aInLists = a . isInUserLists || false ;
const bInLists = b . isInUserLists || false ;
if ( aInLists && ! bInLists ) return - 1 ;
if ( ! aInLists && bInLists ) return 1 ;
// If both are in lists, prioritize by list kind (follows first)
if ( aInLists && bInLists && a . listKinds && b . listKinds ) {
const aHasFollows = a . listKinds . includes ( 3 ) ;
const bHasFollows = b . listKinds . includes ( 3 ) ;
if ( aHasFollows && ! bHasFollows ) return - 1 ;
if ( ! aHasFollows && bHasFollows ) return 1 ;
}
return 0 ;
} ) ;
}
/ * *
* Cache search results
* /
function cacheSearchResults ( profiles : NostrProfile [ ] , searchTerm : string , ndk : NDK ) : void {
if ( profiles . length > 0 ) {
const events = profiles . map ( ( profile ) = > {
const event = new NDKEvent ( ndk ) ;
event . content = JSON . stringify ( profile ) ;
event . pubkey = profile . pubkey || "" ;
// AI-NOTE: 2025-01-24 - Preserve timestamp for proper date display
if ( profile . created_at ) {
event . created_at = profile . created_at ;
}
return event ;
} ) ;
const result = {
events ,
secondOrder : [ ] ,
tTagEvents : [ ] ,
eventIds : new Set < string > ( ) ,
addresses : new Set < string > ( ) ,
searchType : "profile" ,
searchTerm : searchTerm ,
} ;
searchCache . set ( "profile" , searchTerm , result ) ;
}
}
/ * *
* Get cached search results
* /
function getCachedResults ( searchTerm : string ) : NostrProfile [ ] | null {
const cachedResult = searchCache . get ( "profile" , searchTerm ) ;
if ( cachedResult ) {
console . log ( "Found cached result for:" , searchTerm ) ;
const profiles = cachedResult . events
. map ( ( event ) = > {
try {
const profileData = JSON . parse ( event . content ) ;
return createProfileFromEvent ( event , profileData ) ;
} catch {
return null ;
}
} )
. filter ( Boolean ) as NostrProfile [ ] ;
console . log ( "Cached profiles found:" , profiles . length ) ;
return profiles ;
}
return null ;
}
/ * *
* Execute search strategy based on search term type
* /
async function executeSearchStrategy (
strategy : SearchStrategy ,
searchTerm : string ,
ndk : NDK ,
userLists : any [ ] ,
) : Promise < NostrProfile [ ] > {
switch ( strategy ) {
case 'npub' :
return await searchByNostrIdentifier ( searchTerm , ndk ) ;
case 'nip05' :
return await searchByNip05Address ( searchTerm ) ;
case 'userLists' :
const foundProfiles : NostrProfile [ ] = [ ] ;
// First, search within user's lists for exact matches
if ( userLists . length > 0 ) {
console . log ( "Searching within user's lists first for:" , searchTerm ) ;
const listMatches = await searchWithinUserLists ( searchTerm , userLists , ndk ) ;
foundProfiles . push ( . . . listMatches ) ;
console . log ( "User list search completed, found:" , listMatches . length , "profiles" ) ;
}
// If we found enough matches in user lists, return them
if ( foundProfiles . length >= 5 ) {
console . log ( "Found sufficient matches in user lists, skipping other searches" ) ;
return foundProfiles ;
}
// Try NIP-05 search (faster than relay search)
console . log ( "Starting NIP-05 search for:" , searchTerm ) ;
const nip05Profiles = await searchNip05Domains ( searchTerm ) ;
console . log ( "NIP-05 search completed, found:" , nip05Profiles . length , "profiles" ) ;
foundProfiles . push ( . . . nip05Profiles ) ;
// If still not enough results, try quick relay search
if ( foundProfiles . length < 10 ) {
console . log ( "Not enough results, trying quick relay search" ) ;
const relayProfiles = await quickRelaySearch ( searchTerm , ndk ) ;
console . log ( "Quick relay search completed, found:" , relayProfiles . length , "profiles" ) ;
foundProfiles . push ( . . . relayProfiles ) ;
}
// AI-NOTE: 2025-01-24 - Limit results to prevent overwhelming the UI
// For profile searches, we want quality over quantity
if ( foundProfiles . length > SEARCH_LIMITS . MAX_PROFILE_RESULTS ) {
console . log ( ` Limiting results from ${ foundProfiles . length } to ${ SEARCH_LIMITS . MAX_PROFILE_RESULTS } most relevant profiles ` ) ;
return foundProfiles . slice ( 0 , SEARCH_LIMITS . MAX_PROFILE_RESULTS ) ;
}
return foundProfiles ;
default :
return [ ] ;
}
}
/ * *
* Search for profiles by various criteria ( display name , name , NIP - 05 , npub )
* Prioritizes profiles from user ' s lists ( follows , etc . )
* /
export async function searchProfiles ( searchTerm : string ) : Promise < ProfileSearchResult > {
const normalizedSearchTerm = normalizeSearchTerm ( searchTerm ) ;
console . log ( "searchProfiles called with:" , searchTerm , "normalized:" , normalizedSearchTerm ) ;
// Check cache first
const cachedProfiles = getCachedResults ( normalizedSearchTerm ) ;
if ( cachedProfiles ) {
return { profiles : cachedProfiles , Status : { } } ;
}
// Get user lists with stale-while-revalidate caching
let userLists : any [ ] = [ ] ;
let userPubkeys : Set < string > = new Set ( ) ;
try {
const userListResult = await getUserListsWithCache ( ) ;
userLists = userListResult . lists ;
userPubkeys = userListResult . pubkeys ;
console . log ( ` searchProfiles: Using user lists - ${ userLists . length } lists with ${ userPubkeys . size } unique pubkeys ` ) ;
} catch ( error ) {
console . warn ( "searchProfiles: Failed to get user lists:" , error ) ;
}
// Wait for NDK to be properly initialized
const ndk = await waitForNdk ( ) ;
console . log ( "profile_search: NDK initialized, starting search logic" ) ;
try {
// Determine search strategy
const strategy = determineSearchStrategy ( normalizedSearchTerm ) ;
console . log ( "profile_search: Using search strategy:" , strategy ) ;
// Execute search strategy
const foundProfiles = await executeSearchStrategy ( strategy , normalizedSearchTerm , ndk , userLists ) ;
// Cache the results
cacheSearchResults ( foundProfiles , normalizedSearchTerm , ndk ) ;
// Add user list information to profiles and prioritize them
const prioritizedProfiles = prioritizeProfiles ( foundProfiles , userLists ) ;
console . log ( "Search completed, found profiles:" , foundProfiles . length ) ;
console . log ( "Prioritized profiles - follows first:" , prioritizedProfiles . length ) ;
return { profiles : prioritizedProfiles , Status : { } } ;
} catch ( error ) {
console . error ( "Error searching profiles:" , error ) ;
return { profiles : [ ] , Status : { } } ;
}
}