Browse Source

corrected relay management

master
silberengel 7 months ago
parent
commit
f3a589b595
  1. 3
      import_map.json
  2. 592
      package-lock.json
  3. 1
      package.json
  4. 571
      src/lib/components/EventSearch.svelte
  5. 42
      src/lib/components/publications/PublicationFeed.svelte
  6. 17
      src/lib/ndk.ts
  7. 84
      src/lib/services/event_search_service.ts
  8. 62
      src/lib/services/search_state_manager.ts
  9. 28
      src/lib/utils/event_search.ts
  10. 16
      src/lib/utils/nostrUtils.ts
  11. 26
      src/lib/utils/search_result_formatter.ts
  12. 57
      src/lib/utils/subscription_search.ts
  13. 5
      vite.config.ts

3
import_map.json

@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
"node-emoji": "npm:node-emoji@^2.2.0",
"plantuml-encoder": "npm:plantuml-encoder@^1.4.0",
"qrcode": "npm:qrcode@^1.5.4",
"child_process": "node:child_process"
"child_process": "node:child_process",
"process": "node:process"
}
}

592
package-lock.json generated

File diff suppressed because it is too large Load Diff

1
package.json

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite dev",
"dev:debug": "DEBUG_RELAYS=true vite dev",
"dev:node": "node --version && vite dev",
"build": "vite build",
"preview": "vite preview",

571
src/lib/components/EventSearch.svelte

@ -10,12 +10,11 @@ @@ -10,12 +10,11 @@
searchNip05,
} from "$lib/utils/search_utility";
import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { activeInboxRelays, activeOutboxRelays, ndkInstance } from "$lib/ndk";
import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils";
import type { SearchResult } from '$lib/utils/search_types';
import { userStore } from "$lib/stores/userStore";
import { get } from "svelte/store";
// Props definition
let {
loading,
@ -52,7 +51,7 @@ @@ -52,7 +51,7 @@
let localError = $state<string | null>(null);
let foundEvent = $state<NDKEvent | null>(null);
let searching = $state(false);
let searchCompleted = $state(false);
let searchCompleted = $state<boolean>(false);
let searchResultCount = $state<number | null>(null);
let searchResultType = $state<string | null>(null);
let isResetting = $state(false);
@ -75,7 +74,10 @@ @@ -75,7 +74,10 @@
let isWaitingForSearchResult = $state(false);
let isUserEditing = $state(false);
// Move search handler functions above all $effect runes
// Debounced search timeout
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
// AI-NOTE: 2025-01-24 - Core search handlers extracted for better organization
async function handleNip05Search(query: string) {
try {
const foundEvent = await searchNip05(query);
@ -83,42 +85,11 @@ @@ -83,42 +85,11 @@
handleFoundEvent(foundEvent);
updateSearchState(false, true, 1, "nip05");
} else {
// relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
cleanupSearch();
updateSearchState(false, true, 0, "nip05");
}
} catch (error) {
localError =
error instanceof Error ? error.message : "NIP-05 lookup failed";
// relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
updateSearchState(false, false, null, null);
isProcessingSearch = false;
currentProcessingSearchValue = null;
lastSearchValue = null;
lastSearchValue = null;
handleSearchError(error, "NIP-05 lookup failed");
}
}
@ -128,19 +99,7 @@ @@ -128,19 +99,7 @@
if (!foundEvent) {
console.warn("[Events] Event not found for query:", query);
localError = "Event not found";
// relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
cleanupSearch();
updateSearchState(false, false, null, null);
} else {
console.log("[Events] Event found:", foundEvent);
@ -148,23 +107,7 @@ @@ -148,23 +107,7 @@
updateSearchState(false, true, 1, "event");
}
} catch (err) {
console.error("[Events] Error fetching event:", err, "Query:", query);
localError = "Error fetching event. Please check the ID and try again.";
// relayStatuses = {}; // This line was removed as per the edit hint
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
updateSearchState(false, false, null, null);
isProcessingSearch = false;
handleSearchError(err, "Error fetching event. Please check the ID and try again.");
}
}
@ -176,72 +119,123 @@ @@ -176,72 +119,123 @@
console.log("EventSearch: Already searching, skipping");
return;
}
resetSearchState();
localError = null;
updateSearchState(true);
updateSearchState(true, false);
isResetting = false;
isUserEditing = false; // Reset user editing flag when search starts
const query = (
queryOverride !== undefined ? queryOverride || "" : searchQuery || ""
).trim();
isUserEditing = false;
const query = (queryOverride !== undefined ? queryOverride || "" : searchQuery || "").trim();
if (!query) {
updateSearchState(false, false, null, null);
return;
}
if (query.toLowerCase().startsWith("d:")) {
const dTag = query.slice(2).trim().toLowerCase();
if (dTag) {
console.log("EventSearch: Processing d-tag search:", dTag);
navigateToSearch(dTag, "d");
updateSearchState(false, false, null, null);
// Handle different search types
const searchType = getSearchType(query);
if (searchType) {
await handleSearchByType(searchType, query, clearInput);
return;
}
if (clearInput) {
navigateToSearch(query, "id");
}
if (query.toLowerCase().startsWith("t:")) {
const searchTerm = query.slice(2).trim();
if (searchTerm) {
await handleSearchBySubscription("t", searchTerm);
return;
await handleEventSearch(query);
}
// AI-NOTE: 2025-01-24 - Helper functions for better code organization
function getSearchType(query: string): { type: string; term: string } | null {
const lowerQuery = query.toLowerCase();
if (lowerQuery.startsWith("d:")) {
const dTag = query.slice(2).trim().toLowerCase();
return dTag ? { type: "d", term: dTag } : null;
}
if (query.toLowerCase().startsWith("n:")) {
if (lowerQuery.startsWith("t:")) {
const searchTerm = query.slice(2).trim();
if (searchTerm) {
await handleSearchBySubscription("n", searchTerm);
return;
return searchTerm ? { type: "t", term: searchTerm } : null;
}
if (lowerQuery.startsWith("n:")) {
const searchTerm = query.slice(2).trim();
return searchTerm ? { type: "n", term: searchTerm } : null;
}
if (query.includes("@")) {
await handleNip05Search(query);
return { type: "nip05", term: query };
}
return null;
}
async function handleSearchByType(
searchType: { type: string; term: string },
query: string,
clearInput: boolean
) {
const { type, term } = searchType;
if (type === "d") {
console.log("EventSearch: Processing d-tag search:", term);
navigateToSearch(term, "d");
updateSearchState(false, false, null, null);
return;
}
if (clearInput) {
navigateToSearch(query, "id");
// Don't clear searchQuery here - let the effect handle it
if (type === "nip05") {
await handleNip05Search(term);
return;
}
await handleEventSearch(query);
if (type === "t" || type === "n") {
await handleSearchBySubscription(type as "t" | "n", term);
return;
}
}
function handleSearchError(error: unknown, defaultMessage: string) {
localError = error instanceof Error ? error.message : defaultMessage;
cleanupSearch();
updateSearchState(false, false, null, null);
isProcessingSearch = false;
currentProcessingSearchValue = null;
lastSearchValue = null;
}
// Keep searchQuery in sync with searchValue and dTagValue props
function cleanupSearch() {
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
}
// AI-NOTE: 2025-01-24 - Effects organized for better readability
$effect(() => {
// Only sync if we're not currently searching, resetting, or if the user is editing
if (searching || isResetting || isUserEditing) {
return;
}
if (dTagValue) {
// If dTagValue is set, show it as "d:tag" in the search bar
searchQuery = `d:${dTagValue}`;
} else if (searchValue) {
// searchValue should already be in the correct format (t:, n:, d:, etc.)
searchQuery = searchValue;
} else if (!searchQuery) {
// Only clear if searchQuery is empty to avoid clearing user input
searchQuery = "";
}
});
// Debounced effect to handle searchValue changes
$effect(() => {
if (
!searchValue ||
@ -253,76 +247,19 @@ @@ -253,76 +247,19 @@
return;
}
// Check if we've already processed this searchValue
if (searchValue === lastProcessedSearchValue) {
return;
}
// If we already have the event for this searchValue, do nothing
if (foundEvent) {
const currentEventId = foundEvent.id;
let currentNaddr = null;
let currentNevent = null;
let currentNpub = null;
try {
currentNevent = neventEncode(foundEvent, $activeInboxRelays);
} catch {}
try {
currentNaddr = getMatchingTags(foundEvent, "d")[0]?.[1]
? naddrEncode(foundEvent, $activeInboxRelays)
: null;
} catch {}
try {
currentNpub = foundEvent.kind === 0 ? toNpub(foundEvent.pubkey) : null;
} catch {}
// Debug log for comparison
console.log(
"[EventSearch effect] searchValue:",
searchValue,
"foundEvent.id:",
currentEventId,
"foundEvent.pubkey:",
foundEvent.pubkey,
"toNpub(pubkey):",
currentNpub,
"foundEvent.kind:",
foundEvent.kind,
"currentNaddr:",
currentNaddr,
"currentNevent:",
currentNevent,
);
// Also check if searchValue is an nprofile and matches the current event's pubkey
let currentNprofile = null;
if (
searchValue &&
searchValue.startsWith("nprofile1") &&
foundEvent.kind === 0
) {
try {
currentNprofile = nprofileEncode(foundEvent.pubkey, $activeInboxRelays);
} catch {}
}
if (
searchValue === currentEventId ||
(currentNaddr && searchValue === currentNaddr) ||
(currentNevent && searchValue === currentNevent) ||
(currentNpub && searchValue === currentNpub) ||
(currentNprofile && searchValue === currentNprofile)
) {
// Already displaying the event for this searchValue
if (foundEvent && isCurrentEventMatch(searchValue, foundEvent)) {
lastProcessedSearchValue = searchValue;
return;
}
}
// Otherwise, trigger a search for the new value
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(() => {
isProcessingSearch = true;
isWaitingForSearchResult = true;
@ -333,10 +270,6 @@ @@ -333,10 +270,6 @@
}, 300);
});
// Add debouncing to prevent rapid successive searches
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
// Cleanup function to clear timeout when component is destroyed
$effect(() => {
return () => {
if (searchTimeout) {
@ -345,7 +278,6 @@ @@ -345,7 +278,6 @@
};
});
// Simple effect to handle dTagValue changes
$effect(() => {
if (
dTagValue &&
@ -356,7 +288,6 @@ @@ -356,7 +288,6 @@
console.log("EventSearch: Processing dTagValue:", dTagValue);
lastProcessedDTagValue = dTagValue;
// Add a small delay to prevent rapid successive calls
setTimeout(() => {
if (!searching && !isResetting) {
handleSearchBySubscription("d", dTagValue);
@ -365,14 +296,53 @@ @@ -365,14 +296,53 @@
}
});
// Simple effect to handle event prop changes
$effect(() => {
if (event && !searching && !isResetting) {
foundEvent = event;
}
});
// Search utility functions
// AI-NOTE: 2025-01-24 - Utility functions for event matching and state management
function isCurrentEventMatch(searchValue: string, event: NDKEvent): boolean {
const currentEventId = event.id;
let currentNaddr: string | null = null;
let currentNevent: string | null = null;
let currentNpub: string | null = null;
let currentNprofile: string | null = null;
try {
currentNevent = neventEncode(event, $activeInboxRelays);
} catch {}
try {
currentNaddr = getMatchingTags(event, "d")[0]?.[1]
? naddrEncode(event, $activeInboxRelays)
: null;
} catch {}
try {
currentNpub = event.kind === 0 ? toNpub(event.pubkey) : null;
} catch {}
if (
searchValue &&
searchValue.startsWith("nprofile1") &&
event.kind === 0
) {
try {
currentNprofile = nprofileEncode(event.pubkey, $activeInboxRelays);
} catch {}
}
return !!(
searchValue === currentEventId ||
(currentNaddr && searchValue === currentNaddr) ||
(currentNevent && searchValue === currentNevent) ||
(currentNpub && searchValue === currentNpub) ||
(currentNprofile && searchValue === currentNprofile)
);
}
function updateSearchState(
isSearching: boolean,
completed: boolean = false,
@ -399,32 +369,14 @@ @@ -399,32 +369,14 @@
lastSearchValue = null;
updateSearchState(false, false, null, null);
// Cancel ongoing search
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
// Clean up subscription
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
// Clear search results
cleanupSearch();
onSearchResults([], [], [], new Set(), new Set());
// Clear any pending timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
searchTimeout = null;
}
// Reset the flag after a short delay to allow effects to settle
setTimeout(() => {
isResetting = false;
}, 100);
@ -432,37 +384,20 @@ @@ -432,37 +384,20 @@
function handleFoundEvent(event: NDKEvent) {
foundEvent = event;
localError = null; // Clear local error when event is found
// Stop any ongoing subscription
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
localError = null;
// Abort any ongoing fetch
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
cleanupSearch();
// Clear search state
searching = false;
searchCompleted = true;
searchResultCount = 1;
searchResultType = "event";
// Update last processed search value to prevent re-processing
if (searchValue) {
lastProcessedSearchValue = searchValue;
lastSearchValue = searchValue;
}
// Reset processing flag
isProcessingSearch = false;
currentProcessingSearchValue = null;
isWaitingForSearchResult = false;
@ -479,7 +414,7 @@ @@ -479,7 +414,7 @@
});
}
// Search handlers
// AI-NOTE: 2025-01-24 - Main subscription search handler with improved error handling
async function handleSearchBySubscription(
searchType: "d" | "t" | "n",
searchTerm: string,
@ -489,19 +424,61 @@ @@ -489,19 +424,61 @@
searchTerm,
});
// AI-NOTE: 2025-01-24 - Check cache first for profile searches to provide immediate response
if (searchType === "n") {
const cachedResult = await handleCachedProfileSearch(searchTerm);
if (cachedResult) {
return;
}
}
isResetting = false;
localError = null;
updateSearchState(true, false);
await waitForRelays();
try {
await performSubscriptionSearch(searchType, searchTerm);
} catch (error) {
handleSubscriptionSearchError(error);
}
}
async function handleCachedProfileSearch(searchTerm: string): Promise<boolean> {
if (!searchTerm.startsWith("npub") && !searchTerm.startsWith("nprofile")) {
return false;
}
try {
const { getUserMetadata } = await import("$lib/utils/nostrUtils");
const cachedProfile = await getUserMetadata(searchTerm, false);
if (cachedProfile && cachedProfile.name) {
console.log("EventSearch: Found cached profile, displaying immediately:", cachedProfile);
const mockEvent = await createMockProfileEvent(searchTerm, cachedProfile);
handleFoundEvent(mockEvent);
updateSearchState(false, true, 1, "profile-cached");
setTimeout(async () => {
try {
await performBackgroundProfileSearch("n", searchTerm);
} catch (error) {
console.warn("EventSearch: Background profile search failed:", error);
}
}, 100);
return true;
}
} catch (error) {
console.warn("EventSearch: Cache check failed, proceeding with subscription search:", error);
}
return false;
}
// Create a mock NDKEvent for the cached profile
async function createMockProfileEvent(searchTerm: string, profile: any): Promise<NDKEvent> {
const { NDKEvent } = await import("@nostr-dev-kit/ndk");
const { nip19 } = await import("$lib/utils/nostrUtils");
// Decode the npub to get the actual pubkey
let pubkey = searchTerm;
try {
const decoded = nip19.decode(searchTerm);
@ -512,88 +489,68 @@ @@ -512,88 +489,68 @@
console.warn("EventSearch: Failed to decode npub for mock event:", error);
}
const mockEvent = new NDKEvent(undefined, {
return new NDKEvent(undefined, {
kind: 0,
pubkey: pubkey,
content: JSON.stringify(cachedProfile),
content: JSON.stringify(profile),
tags: [],
created_at: Math.floor(Date.now() / 1000),
id: "", // Will be computed by NDK
sig: "", // Will be computed by NDK
id: "",
sig: "",
});
}
// Display the cached profile immediately
handleFoundEvent(mockEvent);
updateSearchState(false, true, 1, "profile-cached");
async function waitForRelays(): Promise<void> {
let retryCount = 0;
const maxRetries = 10; // Reduced retry count since we'll use all available relays
// AI-NOTE: 2025-01-24 - Still perform background search for second-order events
// but with better timeout handling to prevent hanging
setTimeout(async () => {
try {
await performBackgroundProfileSearch(searchType, searchTerm);
} catch (error) {
console.warn("EventSearch: Background profile search failed:", error);
// AI-NOTE: 2025-01-24 - Wait for any relays to be available, not just specific types
// This ensures searches can proceed even if some relay types are not available
while (retryCount < maxRetries) {
// Check if we have any relays in the NDK pool
const ndk = get(ndkInstance);
if (ndk && ndk.pool && ndk.pool.relays && ndk.pool.relays.size > 0) {
console.debug(`EventSearch: Found ${ndk.pool.relays.size} relays in NDK pool`);
break;
}
}, 100);
return;
// Also check active relay stores as fallback
if ($activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0) {
console.debug(`EventSearch: Found active relays - inbox: ${$activeInboxRelays.length}, outbox: ${$activeOutboxRelays.length}`);
break;
}
} catch (error) {
console.warn("EventSearch: Cache check failed, proceeding with subscription search:", error);
}
}
isResetting = false; // Allow effects to run for new searches
localError = null;
updateSearchState(true);
// Wait for relays to be available (with timeout)
let retryCount = 0;
const maxRetries = 20; // Wait up to 10 seconds (20 * 500ms) for user login to complete
while ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0 && retryCount < maxRetries) {
console.debug(`EventSearch: Waiting for relays... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, 500)); // Wait 500ms
retryCount++;
}
// Additional wait for user-specific relays if user is logged in
const currentUser = get(userStore);
if (currentUser.signedIn && currentUser.pubkey) {
console.debug(`EventSearch: User is logged in (${currentUser.pubkey}), waiting for user-specific relays...`);
retryCount = 0;
while ($activeOutboxRelays.length <= 9 && retryCount < maxRetries) {
// If we still have the default relay count (9), wait for user-specific relays
console.debug(`EventSearch: Waiting for user-specific relays... (attempt ${retryCount + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, 500));
retryCount++;
}
}
// Check if we have any relays available
if ($activeInboxRelays.length === 0 && $activeOutboxRelays.length === 0) {
console.warn("EventSearch: No relays available after waiting, failing search");
localError = "No relays available. Please check your connection and try again.";
updateSearchState(false, false, null, null);
isProcessingSearch = false;
currentProcessingSearchValue = null;
isWaitingForSearchResult = false;
searching = false;
return;
}
// AI-NOTE: 2025-01-24 - Don't fail if no relays are available, let the search functions handle fallbacks
// The search functions will use all available relays including fallback relays
const ndk = get(ndkInstance);
const poolRelayCount = ndk?.pool?.relays?.size || 0;
console.log("EventSearch: Relays available, proceeding with search:", {
console.log("EventSearch: Relay status for search:", {
poolRelayCount,
inboxCount: $activeInboxRelays.length,
outboxCount: $activeOutboxRelays.length
outboxCount: $activeOutboxRelays.length,
willUseAllRelays: poolRelayCount > 0 || $activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0
});
try {
// Cancel existing search
// If we have any relays available, proceed with search
if (poolRelayCount > 0 || $activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0) {
console.log("EventSearch: Relays available, proceeding with search");
} else {
console.warn("EventSearch: No relays detected, but proceeding with search - fallback relays will be used");
}
}
async function performSubscriptionSearch(searchType: "d" | "t" | "n", searchTerm: string): Promise<void> {
if (currentAbortController) {
currentAbortController.abort();
}
currentAbortController = new AbortController();
// Add a timeout to prevent hanging searches
const searchPromise = searchBySubscription(
searchType,
searchTerm,
@ -621,7 +578,6 @@ @@ -621,7 +578,6 @@
currentAbortController.signal,
);
// Add a 30-second timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Search timeout: No results received within 30 seconds"));
@ -630,6 +586,7 @@ @@ -630,6 +586,7 @@
const result = await Promise.race([searchPromise, timeoutPromise]) as any;
console.log("EventSearch: Search completed:", result);
onSearchResults(
result.events,
result.secondOrder,
@ -639,86 +596,54 @@ @@ -639,86 +596,54 @@
result.searchType,
result.searchTerm,
);
const totalCount =
result.events.length +
result.secondOrder.length +
result.tTagEvents.length;
localError = null; // Clear local error when search completes
// Stop any ongoing subscription
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
// Abort any ongoing fetch
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
const totalCount = result.events.length + result.secondOrder.length + result.tTagEvents.length;
localError = null;
cleanupSearch();
updateSearchState(false, true, totalCount, searchType);
isProcessingSearch = false;
currentProcessingSearchValue = null;
isWaitingForSearchResult = false;
// Update last processed search value to prevent re-processing
if (searchValue) {
lastProcessedSearchValue = searchValue;
}
} catch (error) {
}
function handleSubscriptionSearchError(error: unknown): void {
if (error instanceof Error && error.message === "Search cancelled") {
isProcessingSearch = false;
currentProcessingSearchValue = null;
isWaitingForSearchResult = false;
return;
}
console.error("EventSearch: Search failed:", error);
localError = error instanceof Error ? error.message : "Search failed";
// Provide more specific error messages for different failure types
if (error instanceof Error) {
if (
error.message.includes("timeout") ||
error.message.includes("connection")
) {
localError =
"Search timed out. The relays may be temporarily unavailable. Please try again.";
if (error.message.includes("timeout") || error.message.includes("connection")) {
localError = "Search timed out. The relays may be temporarily unavailable. Please try again.";
} else if (error.message.includes("NDK not initialized")) {
localError =
"Nostr client not initialized. Please refresh the page and try again.";
localError = "Nostr client not initialized. Please refresh the page and try again.";
} else {
localError = `Search failed: ${error.message}`;
}
} else {
localError = "Search failed";
}
localError = null; // Clear local error when search fails
// Stop any ongoing subscription
if (activeSub) {
try {
activeSub.stop();
} catch (e) {
console.warn("Error stopping subscription:", e);
}
activeSub = null;
}
// Abort any ongoing fetch
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
cleanupSearch();
updateSearchState(false, false, null, null);
isProcessingSearch = false;
currentProcessingSearchValue = null;
isWaitingForSearchResult = false;
// Update last processed search value to prevent re-processing even on error
if (searchValue) {
lastProcessedSearchValue = searchValue;
}
}
}
// AI-NOTE: 2025-01-24 - Function to perform background profile search without blocking UI
async function performBackgroundProfileSearch(
searchType: "d" | "t" | "n",
searchTerm: string,
@ -729,17 +654,15 @@ @@ -729,17 +654,15 @@
});
try {
// Cancel existing search
if (currentAbortController) {
currentAbortController.abort();
}
currentAbortController = new AbortController();
// AI-NOTE: 2025-01-24 - Add timeout to prevent hanging background searches
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Background search timeout"));
}, 10000); // 10 second timeout for background searches
}, 10000);
});
const searchPromise = searchBySubscription(
@ -748,7 +671,6 @@ @@ -748,7 +671,6 @@
{
onSecondOrderUpdate: (updatedResult) => {
console.log("EventSearch: Background second order update:", updatedResult);
// Only update if we have new results
if (updatedResult.events.length > 0) {
onSearchResults(
updatedResult.events,
@ -772,12 +694,10 @@ @@ -772,12 +694,10 @@
currentAbortController.signal,
);
// Race between search and timeout
const result = await Promise.race([searchPromise, timeoutPromise]) as any;
console.log("EventSearch: Background search completed:", result);
// Only update results if we have new data
if (result.events.length > 0) {
onSearchResults(
result.events,
@ -794,21 +714,18 @@ @@ -794,21 +714,18 @@
}
}
// Search utility functions
function handleClear() {
isResetting = true;
searchQuery = "";
isUserEditing = false; // Reset user editing flag
isUserEditing = false;
resetSearchState();
// Clear URL parameters to reset the page
goto("", {
replaceState: true,
keepFocus: true,
noScroll: true,
});
// Ensure all search state is cleared
searching = false;
searchCompleted = false;
searchResultCount = null;
@ -820,7 +737,6 @@ @@ -820,7 +737,6 @@
lastSearchValue = null;
isWaitingForSearchResult = false;
// Clear any pending timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
searchTimeout = null;
@ -830,7 +746,6 @@ @@ -830,7 +746,6 @@
onClear();
}
// Reset the flag after a short delay to allow effects to settle
setTimeout(() => {
isResetting = false;
}, 100);

42
src/lib/components/publications/PublicationFeed.svelte

@ -77,6 +77,8 @@ @@ -77,6 +77,8 @@
});
// Initialize relays and fetch events
// AI-NOTE: This function is called when the component mounts and when relay configuration changes
// It ensures that events are fetched from the current set of active relays
async function initializeAndFetch() {
if (!ndk) {
console.debug('[PublicationFeed] No NDK instance available');
@ -122,11 +124,12 @@ @@ -122,11 +124,12 @@
}
}
// Watch for relay store changes
// Watch for relay store changes and user authentication state
$effect(() => {
const inboxRelays = $activeInboxRelays;
const outboxRelays = $activeOutboxRelays;
const newRelays = [...inboxRelays, ...outboxRelays];
const userState = $userStore;
if (newRelays.length > 0 && !hasInitialized) {
console.debug('[PublicationFeed] Relays available, initializing');
@ -145,6 +148,18 @@ @@ -145,6 +148,18 @@
initializeAndFetch();
}, 3000);
}
} else if (hasInitialized && newRelays.length > 0) {
// AI-NOTE: Re-fetch events when user authentication state changes or relays are updated
// This ensures that when a user logs in and their relays are loaded, we fetch events from those relays
const currentRelaysString = allRelays.sort().join(',');
const newRelaysString = newRelays.sort().join(',');
if (currentRelaysString !== newRelaysString) {
console.debug('[PublicationFeed] Relay configuration changed, re-fetching events');
// Clear cache to force fresh fetch from new relays
indexEventCache.clear();
setTimeout(() => initializeAndFetch(), 0);
}
}
});
@ -513,6 +528,31 @@ @@ -513,6 +528,31 @@
debouncedSearch(props.searchQuery);
});
// AI-NOTE: Watch for user authentication state changes to re-fetch events when user logs in/out
$effect(() => {
const userState = $userStore;
if (hasInitialized && userState.signedIn) {
console.debug('[PublicationFeed] User signed in, checking if we need to re-fetch events');
// Check if we have user-specific relays that we haven't fetched from yet
const inboxRelays = $activeInboxRelays;
const outboxRelays = $activeOutboxRelays;
const newRelays = [...inboxRelays, ...outboxRelays];
if (newRelays.length > 0) {
const currentRelaysString = allRelays.sort().join(',');
const newRelaysString = newRelays.sort().join(',');
if (currentRelaysString !== newRelaysString) {
console.debug('[PublicationFeed] User logged in with new relays, re-fetching events');
// Clear cache to force fresh fetch from user's relays
indexEventCache.clear();
setTimeout(() => initializeAndFetch(), 0);
}
}
}
});
// AI-NOTE: Watch for changes in the user filter checkbox
$effect(() => {
// Trigger filtering when the user filter checkbox changes

17
src/lib/ndk.ts

@ -368,7 +368,10 @@ function ensureSecureWebSocket(url: string): string { @@ -368,7 +368,10 @@ function ensureSecureWebSocket(url: string): string {
*/
function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
try {
// Reduce verbosity in development - only log relay creation if debug mode is enabled
if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) {
console.debug(`[NDK.ts] Creating relay with URL: ${url}`);
}
// Ensure the URL is using appropriate protocol
const secureUrl = ensureSecureWebSocket(url);
@ -383,7 +386,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { @@ -383,7 +386,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
// Set up connection timeout
const connectionTimeout = setTimeout(() => {
try {
console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`);
// Only log connection timeouts if debug mode is enabled
if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) {
console.debug(`[NDK.ts] Connection timeout for ${secureUrl}`);
}
relay.disconnect();
} catch {
// Silently ignore disconnect errors
@ -395,7 +401,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { @@ -395,7 +401,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
const authPolicy = new CustomRelayAuthPolicy(ndk);
relay.on("connect", () => {
try {
// Only log successful connections if debug mode is enabled
if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) {
console.debug(`[NDK.ts] Relay connected: ${secureUrl}`);
}
clearTimeout(connectionTimeout);
authPolicy.authenticate(relay);
} catch {
@ -405,7 +414,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { @@ -405,7 +414,10 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
} else {
relay.on("connect", () => {
try {
// Only log successful connections if debug mode is enabled
if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) {
console.debug(`[NDK.ts] Relay connected: ${secureUrl}`);
}
clearTimeout(connectionTimeout);
} catch {
// Silently handle connect handler errors
@ -513,7 +525,10 @@ export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = f @@ -513,7 +525,10 @@ export async function updateActiveRelayStores(ndk: NDK, forceUpdate: boolean = f
// Add relays to NDK pool (deduplicated)
const allRelayUrls = deduplicateRelayUrls([...relaySet.inboxRelays, ...relaySet.outboxRelays]);
// Reduce verbosity in development - only log relay addition if debug mode is enabled
if (process.env.NODE_ENV === 'development' && process.env.DEBUG_RELAYS) {
console.debug('[NDK.ts] updateActiveRelayStores: Adding', allRelayUrls.length, 'relays to NDK pool');
}
for (const url of allRelayUrls) {
try {

84
src/lib/services/event_search_service.ts

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
/**
* Service class for handling event search operations
* AI-NOTE: 2025-01-24 - Extracted from EventSearch component for better separation of concerns
*/
export class EventSearchService {
/**
* Determines the search type from a query string
*/
getSearchType(query: string): { type: string; term: string } | null {
const lowerQuery = query.toLowerCase();
if (lowerQuery.startsWith("d:")) {
const dTag = query.slice(2).trim().toLowerCase();
return dTag ? { type: "d", term: dTag } : null;
}
if (lowerQuery.startsWith("t:")) {
const searchTerm = query.slice(2).trim();
return searchTerm ? { type: "t", term: searchTerm } : null;
}
if (lowerQuery.startsWith("n:")) {
const searchTerm = query.slice(2).trim();
return searchTerm ? { type: "n", term: searchTerm } : null;
}
if (query.includes("@")) {
return { type: "nip05", term: query };
}
return null;
}
/**
* Checks if a search value matches the current event
*/
isCurrentEventMatch(searchValue: string, event: any, relays: string[]): boolean {
const currentEventId = event.id;
let currentNaddr = null;
let currentNevent = null;
let currentNpub = null;
let currentNprofile = null;
try {
const { neventEncode, naddrEncode, nprofileEncode } = require("$lib/utils");
const { getMatchingTags, toNpub } = require("$lib/utils/nostrUtils");
currentNevent = neventEncode(event, relays);
} catch {}
try {
const { naddrEncode } = require("$lib/utils");
const { getMatchingTags } = require("$lib/utils/nostrUtils");
currentNaddr = getMatchingTags(event, "d")[0]?.[1]
? naddrEncode(event, relays)
: null;
} catch {}
try {
const { toNpub } = require("$lib/utils/nostrUtils");
currentNpub = event.kind === 0 ? toNpub(event.pubkey) : null;
} catch {}
if (
searchValue &&
searchValue.startsWith("nprofile1") &&
event.kind === 0
) {
try {
const { nprofileEncode } = require("$lib/utils");
currentNprofile = nprofileEncode(event.pubkey, relays);
} catch {}
}
return (
searchValue === currentEventId ||
(currentNaddr && searchValue === currentNaddr) ||
(currentNevent && searchValue === currentNevent) ||
(currentNpub && searchValue === currentNpub) ||
(currentNprofile && searchValue === currentNprofile)
);
}
}

62
src/lib/services/search_state_manager.ts

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
/**
* Service class for managing search state operations
* AI-NOTE: 2025-01-24 - Extracted from EventSearch component for better separation of concerns
*/
export class SearchStateManager {
/**
* Updates the search state with new values
*/
updateSearchState(
state: {
searching: boolean;
searchCompleted: boolean;
searchResultCount: number | null;
searchResultType: string | null;
},
onLoadingChange?: (loading: boolean) => void
): void {
if (onLoadingChange) {
onLoadingChange(state.searching);
}
}
/**
* Resets all search state to initial values
*/
resetSearchState(
callbacks: {
onSearchResults: (events: any[], secondOrder: any[], tTagEvents: any[], eventIds: Set<string>, addresses: Set<string>) => void;
cleanupSearch: () => void;
clearTimeout: () => void;
}
): void {
callbacks.cleanupSearch();
callbacks.onSearchResults([], [], [], new Set(), new Set());
callbacks.clearTimeout();
}
/**
* Handles search errors with consistent error handling
*/
handleSearchError(
error: unknown,
defaultMessage: string,
callbacks: {
setLocalError: (error: string | null) => void;
cleanupSearch: () => void;
updateSearchState: (state: any) => void;
resetProcessingFlags: () => void;
}
): void {
const errorMessage = error instanceof Error ? error.message : defaultMessage;
callbacks.setLocalError(errorMessage);
callbacks.cleanupSearch();
callbacks.updateSearchState({
searching: false,
searchCompleted: false,
searchResultCount: null,
searchResultType: null
});
callbacks.resetProcessingFlags();
}
}

28
src/lib/utils/event_search.ts

@ -6,6 +6,7 @@ import type { Filter } from "./search_types.ts"; @@ -6,6 +6,7 @@ import type { Filter } from "./search_types.ts";
import { get } from "svelte/store";
import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts";
import { TIMEOUTS, VALIDATION } from "./search_constants.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
/**
* Search for a single event by ID or filter
@ -17,18 +18,35 @@ export async function searchEvent(query: string): Promise<NDKEvent | null> { @@ -17,18 +18,35 @@ export async function searchEvent(query: string): Promise<NDKEvent | null> {
return null;
}
// Wait for relays to be available
// AI-NOTE: 2025-01-24 - Wait for any relays to be available, not just pool relays
// This ensures searches can proceed even if some relay types are not available
let attempts = 0;
const maxAttempts = 10;
while (ndk.pool.relays.size === 0 && attempts < maxAttempts) {
const maxAttempts = 5; // Reduced since we'll use fallback relays
while (attempts < maxAttempts) {
// Check if we have any relays in the pool
if (ndk.pool.relays.size > 0) {
console.log(`[Search] Found ${ndk.pool.relays.size} relays in NDK pool`);
break;
}
// Also check if we have any active relays
const inboxRelays = get(activeInboxRelays);
const outboxRelays = get(activeOutboxRelays);
if (inboxRelays.length > 0 || outboxRelays.length > 0) {
console.log(`[Search] Found active relays - inbox: ${inboxRelays.length}, outbox: ${outboxRelays.length}`);
break;
}
console.log(`[Search] Waiting for relays to be available (attempt ${attempts + 1}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, 500));
attempts++;
}
// AI-NOTE: 2025-01-24 - Don't fail if no relays are available, let fetchEventWithFallback handle fallbacks
// The fetchEventWithFallback function will use all available relays including fallback relays
if (ndk.pool.relays.size === 0) {
console.warn("[Search] No relays available after waiting");
return null;
console.warn("[Search] No relays in pool, but proceeding with search - fallback relays will be used");
}
// Clean the query and normalize to lowercase

16
src/lib/utils/nostrUtils.ts

@ -5,7 +5,7 @@ import { npubCache } from "./npubCache.ts"; @@ -5,7 +5,7 @@ import { npubCache } from "./npubCache.ts";
import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk";
import type { NDKKind, NostrEvent } from "@nostr-dev-kit/ndk";
import type { Filter } from "./search_types.ts";
import { communityRelays, secondaryRelays, searchRelays } from "../consts.ts";
import { communityRelays, secondaryRelays, searchRelays, anonymousRelays } from "../consts.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk";
import { sha256 } from "@noble/hashes/sha2.js";
@ -443,19 +443,27 @@ export async function fetchEventWithFallback( @@ -443,19 +443,27 @@ export async function fetchEventWithFallback(
filterOrId: string | Filter,
timeoutMs: number = 3000,
): Promise<NDKEvent | null> {
// Use both inbox and outbox relays for better event discovery
// AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive event discovery
// This ensures we don't miss events that might be on any available relay
// Get all relays from NDK pool first (most comprehensive)
const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => r.url);
const inboxRelays = get(activeInboxRelays);
const outboxRelays = get(activeOutboxRelays);
let allRelays = [...inboxRelays, ...outboxRelays];
// Combine all available relays, prioritizing pool relays
let allRelays = [...new Set([...poolRelays, ...inboxRelays, ...outboxRelays])];
console.log("fetchEventWithFallback: Using pool relays:", poolRelays);
console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays);
console.log("fetchEventWithFallback: Using outbox relays:", outboxRelays);
console.log("fetchEventWithFallback: Total unique relays:", allRelays.length);
// Check if we have any relays available
if (allRelays.length === 0) {
console.warn("fetchEventWithFallback: No relays available for event fetch, using fallback relays");
// Use fallback relays when no relays are available
allRelays = [...secondaryRelays, ...searchRelays];
allRelays = [...secondaryRelays, ...searchRelays, ...anonymousRelays];
console.log("fetchEventWithFallback: Using fallback relays:", allRelays);
}

26
src/lib/utils/search_result_formatter.ts

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/**
* Utility class for formatting search result messages
* AI-NOTE: 2025-01-24 - Extracted from EventSearch component for better separation of concerns
*/
export class SearchResultFormatter {
/**
* Formats a result message based on search count and type
*/
formatResultMessage(searchResultCount: number | null, searchResultType: string | null): string {
if (searchResultCount === 0) {
return "Search completed. No results found.";
}
const typeLabel =
searchResultType === "n"
? "profile"
: searchResultType === "nip05"
? "NIP-05 address"
: "event";
const countLabel = searchResultType === "n" ? "profiles" : "events";
return searchResultCount === 1
? `Search completed. Found 1 ${typeLabel}.`
: `Search completed. Found ${searchResultCount} ${countLabel}.`;
}
}

57
src/lib/utils/subscription_search.ts

@ -403,7 +403,8 @@ async function createProfileSearchFilter( @@ -403,7 +403,8 @@ async function createProfileSearchFilter(
}
/**
* Create primary relay set based on search type
* Create primary relay set for search operations
* AI-NOTE: 2025-01-24 - Updated to use all available relays to prevent search failures
*/
function createPrimaryRelaySet(
searchType: SearchSubscriptionType,
@ -413,9 +414,11 @@ function createPrimaryRelaySet( @@ -413,9 +414,11 @@ function createPrimaryRelaySet(
const poolRelays = Array.from(ndk.pool.relays.values());
console.debug('subscription_search: NDK pool relays:', poolRelays.map((r: any) => r.url));
// AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive search coverage
// This ensures searches don't fail due to missing relays and provides maximum event discovery
if (searchType === "n") {
// AI-NOTE: 2025-01-08 - For profile searches, prioritize search relays for speed
// Use search relays first, then fall back to all relays if needed
// For profile searches, prioritize search relays for speed but include all relays
const searchRelaySet = poolRelays.filter(
(relay: any) =>
searchRelays.some(
@ -426,30 +429,27 @@ function createPrimaryRelaySet( @@ -426,30 +429,27 @@ function createPrimaryRelaySet(
if (searchRelaySet.length > 0) {
console.debug('subscription_search: Profile search - using search relays for speed:', searchRelaySet.map((r: any) => r.url));
return new NDKRelaySet(new Set(searchRelaySet) as any, ndk);
// Still include all relays for comprehensive coverage
console.debug('subscription_search: Profile search - also including all relays for comprehensive coverage');
return new NDKRelaySet(new Set(poolRelays) as any, ndk);
} else {
// Fallback to all relays if search relays not available
console.debug('subscription_search: Profile search - fallback to all relays:', poolRelays.map((r: any) => r.url));
// Use all relays if search relays not available
console.debug('subscription_search: Profile search - using all relays:', poolRelays.map((r: any) => r.url));
return new NDKRelaySet(new Set(poolRelays) as any, ndk);
}
} else {
// For other searches, use active relays first
const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)];
// For all other searches, use ALL available relays for maximum coverage
const activeRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)];
console.debug('subscription_search: Active relay stores:', {
inboxRelays: get(activeInboxRelays),
outboxRelays: get(activeOutboxRelays),
searchRelays
activeRelays
});
const activeRelaySet = poolRelays.filter(
(relay: any) =>
searchRelays.some(
(searchRelay: string) =>
normalizeUrl(relay.url) === normalizeUrl(searchRelay),
),
);
console.debug('subscription_search: Active relay set:', activeRelaySet.map((r: any) => r.url));
return new NDKRelaySet(new Set(activeRelaySet) as any, ndk);
// AI-NOTE: 2025-01-24 - Use all pool relays instead of filtering to active relays only
// This ensures we don't miss events that might be on other relays
console.debug('subscription_search: Using ALL pool relays for comprehensive search coverage:', poolRelays.map((r: any) => r.url));
return new NDKRelaySet(new Set(poolRelays) as any, ndk);
}
}
@ -647,25 +647,16 @@ function searchOtherRelaysInBackground( @@ -647,25 +647,16 @@ function searchOtherRelaysInBackground(
): Promise<SearchResult> {
const ndk = get(ndkInstance);
// AI-NOTE: 2025-01-24 - Use ALL available relays for comprehensive search coverage
// This ensures we don't miss events that might be on any available relay
const otherRelays = new NDKRelaySet(
new Set(
Array.from(ndk.pool.relays.values()).filter((relay: any) => {
if (searchType === "n") {
// AI-NOTE: 2025-01-08 - For profile searches, use ALL available relays
// Don't exclude any relays since we want maximum coverage
return true;
} else {
// For other searches, exclude community relays from fallback search
return !communityRelays.some(
(communityRelay: string) =>
normalizeUrl(relay.url) === normalizeUrl(communityRelay),
);
}
}),
),
new Set(Array.from(ndk.pool.relays.values())),
ndk,
);
console.debug('subscription_search: Background search using ALL relays:',
Array.from(ndk.pool.relays.values()).map((r: any) => r.url));
// Subscribe to events from other relays
const sub = ndk.subscribe(
searchFilter.filter,

5
vite.config.ts

@ -42,6 +42,8 @@ export default defineConfig({ @@ -42,6 +42,8 @@ export default defineConfig({
define: {
// Expose the app version as a global variable
"import.meta.env.APP_VERSION": JSON.stringify(getAppVersionString()),
// Enable debug logging for relays when needed
"process.env.DEBUG_RELAYS": JSON.stringify(process.env.DEBUG_RELAYS || "false"),
},
optimizeDeps: {
esbuildOptions: {
@ -54,5 +56,8 @@ export default defineConfig({ @@ -54,5 +56,8 @@ export default defineConfig({
fs: {
allow: ['..'],
},
hmr: {
overlay: false, // Disable HMR overlay to prevent ESM URL scheme errors
},
},
});

Loading…
Cancel
Save