From 5e72e31fcac68e30b9c0d3a53826c49d447dfc25 Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 16:11:50 +0200 Subject: [PATCH] interim bugfix state --- package-lock.json | 170 +++---- src/lib/components/CommentBox.svelte | 59 ++- src/lib/components/EventDetails.svelte | 40 +- src/lib/components/EventInput.svelte | 5 +- src/lib/components/EventSearch.svelte | 118 +++-- src/lib/components/Login.svelte | 78 ---- src/lib/components/LoginMenu.svelte | 28 +- src/lib/components/NetworkStatus.svelte | 59 +++ src/lib/components/RelayActions.svelte | 8 +- src/lib/components/RelayDisplay.svelte | 11 +- src/lib/components/RelayStatus.svelte | 25 +- .../publications/PublicationFeed.svelte | 199 +++++--- .../publications/PublicationHeader.svelte | 11 +- .../publications/PublicationSection.svelte | 10 +- src/lib/components/util/CardActions.svelte | 16 +- .../components/util/ContainingIndexes.svelte | 8 +- src/lib/components/util/Profile.svelte | 165 +++---- .../util/ViewPublicationLink.svelte | 5 +- src/lib/consts.ts | 55 +-- .../EventNetwork/utils/networkBuilder.ts | 7 +- src/lib/ndk.ts | 415 ++++++++--------- src/lib/stores.ts | 10 +- src/lib/stores/networkStore.ts | 55 +++ src/lib/stores/relayStore.ts | 4 - src/lib/stores/userStore.ts | 6 +- src/lib/utils/ZettelParser.ts | 2 +- src/lib/utils/community_checker.ts | 82 ++-- src/lib/utils/network_detection.ts | 189 ++++++++ src/lib/utils/nostrEventService.ts | 108 ++--- src/lib/utils/nostrUtils.ts | 111 ++--- src/lib/utils/profile_search.ts | 4 +- src/lib/utils/relayDiagnostics.ts | 8 +- src/lib/utils/relay_management.ts | 424 ++++++++++++++++++ src/lib/utils/subscription_search.ts | 58 ++- src/routes/+layout.svelte | 4 + src/routes/+layout.ts | 9 +- src/routes/+page.svelte | 22 +- src/routes/contact/+page.svelte | 17 +- src/routes/events/+page.svelte | 30 +- src/routes/publication/+page.ts | 5 +- 40 files changed, 1685 insertions(+), 955 deletions(-) delete mode 100644 src/lib/components/Login.svelte create mode 100644 src/lib/components/NetworkStatus.svelte create mode 100644 src/lib/stores/networkStore.ts delete mode 100644 src/lib/stores/relayStore.ts create mode 100644 src/lib/utils/network_detection.ts create mode 100644 src/lib/utils/relay_management.ts diff --git a/package-lock.json b/package-lock.json index e25f782..0d82dc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2559,18 +2559,37 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { - "readdirp": "^4.0.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/cliui": { @@ -3431,6 +3450,15 @@ "node": ">=4" } }, + "node_modules/eslint-plugin-svelte/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5488,16 +5516,25 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">= 14.18.0" + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/require-directory": { @@ -5966,6 +6003,34 @@ "typescript": ">=5.0.0" } }, + "node_modules/svelte-check/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/svelte-check/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -6184,51 +6249,6 @@ "node": ">=14.0.0" } }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tailwindcss/node_modules/postcss-load-config": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", @@ -6275,28 +6295,6 @@ "node": ">=4" } }, - "node_modules/tailwindcss/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6837,12 +6835,14 @@ } }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/src/lib/components/CommentBox.svelte b/src/lib/components/CommentBox.svelte index 5407f72..fbff0f3 100644 --- a/src/lib/components/CommentBox.svelte +++ b/src/lib/components/CommentBox.svelte @@ -21,6 +21,7 @@ } from "$lib/utils/nostrEventService"; import { tick } from "svelte"; import { goto } from "$app/navigation"; + import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; const props = $props<{ event: NDKEvent; @@ -33,7 +34,7 @@ let success = $state<{ relay: string; eventId: string } | null>(null); let error = $state(null); let showOtherRelays = $state(false); - let showFallbackRelays = $state(false); + let showSecondaryRelays = $state(false); let userProfile = $state(null); // Add state for modals and search @@ -150,7 +151,7 @@ preview = ""; error = null; showOtherRelays = false; - showFallbackRelays = false; + showSecondaryRelays = false; } function removeFormatting() { @@ -169,7 +170,7 @@ async function handleSubmit( useOtherRelays = false, - useFallbackRelays = false, + useSecondaryRelays = false, ) { isSubmitting = true; error = null; @@ -208,34 +209,30 @@ tags, ); - // Publish the event - const result = await publishEvent( - signedEvent, - useOtherRelays, - useFallbackRelays, - props.userRelayPreference, - ); - - if (result.success) { - success = { relay: result.relay!, eventId: result.eventId! }; - // Navigate to the published event - navigateToEvent(result.eventId!); - } else { - if (!useOtherRelays && !useFallbackRelays) { - showOtherRelays = true; - error = - "Failed to publish to primary relays. Would you like to try the other relays?"; - } else if (useOtherRelays && !useFallbackRelays) { - showFallbackRelays = true; - error = - "Failed to publish to other relays. Would you like to try the fallback relays?"; - } else { - error = "Failed to publish comment. Please try again later."; - } + // Publish the event using the new relay system + let relays = $activeOutboxRelays; + + if (useOtherRelays && !useSecondaryRelays) { + relays = [...$activeOutboxRelays, ...$activeInboxRelays]; + } else if (useSecondaryRelays) { + // For secondary relays, use a subset of outbox relays + relays = $activeOutboxRelays.slice(0, 3); // Use first 3 outbox relays } - } catch (e: unknown) { - console.error("Error publishing comment:", e); - error = e instanceof Error ? e.message : "An unexpected error occurred"; + + const successfulRelays = await publishEvent(signedEvent, relays); + + success = { + relay: successfulRelays[0] || "Unknown relay", + eventId: signedEvent.id, + }; + + // Clear form after successful submission + content = ""; + preview = ""; + showOtherRelays = false; + showSecondaryRelays = false; + } catch (e) { + error = e instanceof Error ? e.message : "Unknown error occurred"; } finally { isSubmitting = false; } @@ -563,7 +560,7 @@ >Try Other Relays {/if} - {#if showFallbackRelays} + {#if showSecondaryRelays} - {#if signInFailed} -
- {errorMessage} -
- {/if} - - - - {/if} - diff --git a/src/lib/components/LoginMenu.svelte b/src/lib/components/LoginMenu.svelte index 8814333..4ac4222 100644 --- a/src/lib/components/LoginMenu.svelte +++ b/src/lib/components/LoginMenu.svelte @@ -11,10 +11,11 @@ loginWithNpub, logoutUser, } from "$lib/stores/userStore"; - import { get } from "svelte/store"; + import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { onMount } from "svelte"; import { goto } from "$app/navigation"; + import NetworkStatus from "./NetworkStatus.svelte"; // UI state let isLoadingExtension: boolean = $state(false); @@ -36,13 +37,16 @@ } }); - // Subscribe to userStore - let user = $state(get(userStore)); - userStore.subscribe((val) => { - user = val; + // Use reactive user state from store + let user = $derived($userStore); + + // Handle user state changes with effects + $effect(() => { + const currentUser = user; + // Check for fallback flag when user state changes to signed in if ( - val.signedIn && + currentUser.signedIn && localStorage.getItem("alexandria/amber/fallback") === "1" && !showAmberFallback ) { @@ -53,7 +57,7 @@ } // Set up periodic check when user is signed in - if (val.signedIn && !fallbackCheckInterval) { + if (currentUser.signedIn && !fallbackCheckInterval) { fallbackCheckInterval = setInterval(() => { if ( localStorage.getItem("alexandria/amber/fallback") === "1" && @@ -65,7 +69,7 @@ showAmberFallback = true; } }, 500); // Check every 500ms - } else if (!val.signedIn && fallbackCheckInterval) { + } else if (!currentUser.signedIn && fallbackCheckInterval) { clearInterval(fallbackCheckInterval); fallbackCheckInterval = null; } @@ -249,6 +253,10 @@ > 📖 npub (read only) +
+
Network Status:
+ +
{#if result} @@ -313,6 +321,10 @@ Unknown login method {/if} +
  • +
    Network Status:
    + +
  • -
  • - {#if isNav} -
  • - -
  • - {:else} - - {/if} - - - - - {/key} - - {/if} + + +
    +
    + {#if username} +

    {username}

    + {#if isNav}

    @{tag}

    {/if} + {:else} +

    Loading...

    + {/if} +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • + {#if isNav} +
    • + +
    • + {:else} + + {/if} +
    +
    +
    +
    + diff --git a/src/lib/components/util/ViewPublicationLink.svelte b/src/lib/components/util/ViewPublicationLink.svelte index fbbc550..fd7538d 100644 --- a/src/lib/components/util/ViewPublicationLink.svelte +++ b/src/lib/components/util/ViewPublicationLink.svelte @@ -3,7 +3,8 @@ import { getMatchingTags } from "$lib/utils/nostrUtils"; import { naddrEncode } from "$lib/utils"; import { getEventType } from "$lib/utils/mime"; - import { standardRelays } from "$lib/consts"; + import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + import { communityRelays } from "$lib/consts"; import { goto } from "$app/navigation"; let { event, className = "" } = $props<{ @@ -25,7 +26,7 @@ return null; } try { - return naddrEncode(event, standardRelays); + return naddrEncode(event, $activeInboxRelays); } catch { return null; } diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 19955c5..4241d1d 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,45 +1,48 @@ export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [30041, 30818]; -export const communityRelay = "wss://theforest.nostr1.com"; -export const profileRelays = [ + +export const communityRelays = [ + "wss://theforest.nostr1.com", + //"wss://theforest.gitcitadel.eu" +]; + +export const searchRelays = [ "wss://profiles.nostr1.com", "wss://aggr.nostr.land", "wss://relay.noswhere.com", -]; -export const standardRelays = [ - "wss://thecitadel.nostr1.com", - "wss://theforest.nostr1.com", - "wss://profiles.nostr1.com", - // Removed gitcitadel.nostr1.com as it's causing connection issues - //'wss://thecitadel.gitcitadel.eu', - //'wss://theforest.gitcitadel.eu', + "wss://nostr.wine", ]; -// Non-auth relays for anonymous users -export const anonymousRelays = [ - "wss://thecitadel.nostr1.com", +export const secondaryRelays = [ "wss://theforest.nostr1.com", - "wss://profiles.nostr1.com", - "wss://freelay.sovbit.host", -]; -export const fallbackRelays = [ - "wss://purplepag.es", - "wss://indexer.coracle.social", - "wss://relay.noswhere.com", - "wss://aggr.nostr.land", + //"wss://theforest.gitcitadel.eu" + "wss://thecitadel.nostr1.com", + //"wss://thecitadel.gitcitadel.eu", "wss://nostr.land", "wss://nostr.wine", "wss://nostr.sovbit.host", - "wss://freelay.sovbit.host", "wss://nostr21.com", - "wss://greensoul.space", - "wss://relay.damus.io", - "wss://relay.nostr.band", +]; + +export const anonymousRelays = [ + "wss://freelay.sovbit.host", + "wss://thecitadel.nostr1.com" +]; + +export const lowbandwidthRelays = [ + "wss://theforest.nostr1.com", + "wss://thecitadel.nostr1.com", + "wss://aggr.nostr.land" +]; + +export const localRelays = [ + "wss://localhost:8080", + "wss://localhost:4869" ]; export enum FeedType { - StandardRelays = "standard", + CommunityRelays = "standard", UserRelays = "user", } diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index 83537c3..d62f189 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -8,8 +8,9 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; import { nip19 } from "nostr-tools"; -import { standardRelays } from "$lib/consts"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { getMatchingTags } from "$lib/utils/nostrUtils"; +import { get } from "svelte/store"; // Configuration const DEBUG = false; // Set to true to enable debug logging @@ -71,13 +72,13 @@ export function createNetworkNode( pubkey: event.pubkey, identifier: dTag, kind: event.kind, - relays: standardRelays, + relays: [...get(activeInboxRelays), ...get(activeOutboxRelays)], }); // Create nevent (NIP-19 event reference) for the event node.nevent = nip19.neventEncode({ id: event.id, - relays: standardRelays, + relays: [...get(activeInboxRelays), ...get(activeOutboxRelays)], kind: event.kind, }); } catch (error) { diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index 4e319b8..cd13df2 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -8,15 +8,29 @@ import NDK, { } from "@nostr-dev-kit/ndk"; import { get, writable, type Writable } from "svelte/store"; import { - fallbackRelays, + secondaryRelays, FeedType, loginStorageKey, - standardRelays, + communityRelays, anonymousRelays, + searchRelays, } from "./consts"; -import { feedType } from "./stores"; +import { + buildCompleteRelaySet, + testRelayConnection, + discoverLocalRelays, + getUserLocalRelays, + getUserBlockedRelays, + getUserOutboxRelays, + deduplicateRelayUrls, +} from "./utils/relay_management"; + +// Re-export testRelayConnection for components that need it +export { testRelayConnection }; +import { startNetworkMonitoring, NetworkCondition } from "./utils/network_detection"; import { userStore } from "./stores/userStore"; import { userPubkey } from "$lib/stores/authStore.Svelte"; +import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore"; export const ndkInstance: Writable = writable(); export const ndkSignedIn = writable(false); @@ -24,6 +38,10 @@ export const activePubkey = writable(null); export const inboxRelays = writable([]); export const outboxRelays = writable([]); +// New relay management stores +export const activeInboxRelays = writable([]); +export const activeOutboxRelays = writable([]); + /** * Custom authentication policy that handles NIP-42 authentication manually * when the default NDK authentication fails @@ -207,83 +225,7 @@ export function checkWebSocketSupport(): void { } } -/** - * Tests connection to a relay and returns connection status - * @param relayUrl The relay URL to test - * @param ndk The NDK instance - * @returns Promise that resolves to connection status - */ -export async function testRelayConnection( - relayUrl: string, - ndk: NDK, -): Promise<{ - connected: boolean; - requiresAuth: boolean; - error?: string; - actualUrl?: string; -}> { - return new Promise((resolve) => { - console.debug(`[NDK.ts] Testing connection to: ${relayUrl}`); - - // Ensure the URL is using wss:// protocol - const secureUrl = ensureSecureWebSocket(relayUrl); - - const relay = new NDKRelay(secureUrl, undefined, new NDK()); - let authRequired = false; - let connected = false; - let error: string | undefined; - let actualUrl: string | undefined; - - const timeout = setTimeout(() => { - relay.disconnect(); - resolve({ - connected: false, - requiresAuth: authRequired, - error: "Connection timeout", - actualUrl, - }); - }, 5000); - relay.on("connect", () => { - console.debug(`[NDK.ts] Connected to ${secureUrl}`); - connected = true; - actualUrl = secureUrl; - clearTimeout(timeout); - relay.disconnect(); - resolve({ - connected: true, - requiresAuth: authRequired, - error, - actualUrl, - }); - }); - - relay.on("notice", (message: string) => { - if (message.includes("auth-required")) { - authRequired = true; - console.debug(`[NDK.ts] ${secureUrl} requires authentication`); - } - }); - - relay.on("disconnect", () => { - if (!connected) { - error = "Connection failed"; - console.error(`[NDK.ts] Failed to connect to ${secureUrl}`); - clearTimeout(timeout); - resolve({ - connected: false, - requiresAuth: authRequired, - error, - actualUrl, - }); - } - }); - - // Log the actual WebSocket URL being used - console.debug(`[NDK.ts] Attempting connection to: ${secureUrl}`); - relay.connect(); - }); -} /** * Gets the user's pubkey from local storage, if it exists. @@ -433,98 +375,180 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { return relay; } -export function getActiveRelays(ndk: NDK): NDKRelaySet { + + + + +/** + * Gets the active relay set for the current user + * @param ndk NDK instance + * @returns Promise that resolves to object with inbox and outbox relay arrays + */ +export async function getActiveRelaySet(ndk: NDK): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { const user = get(userStore); + + if (user.signedIn && user.ndkUser) { + return await buildCompleteRelaySet(ndk, user.ndkUser); + } else { + return await buildCompleteRelaySet(ndk, null); + } +} - // Filter out problematic relays that are known to cause connection issues - const filterProblematicRelays = (relays: string[]) => { - return relays.filter((relay) => { - // Filter out gitcitadel.nostr1.com which is causing connection issues - if (relay.includes("gitcitadel.nostr1.com")) { - console.warn(`[NDK.ts] Filtering out problematic relay: ${relay}`); - return false; +/** + * Updates the active relay stores and NDK pool with new relay URLs + * @param ndk NDK instance + */ +export async function updateActiveRelayStores(ndk: NDK): Promise { + try { + // Get the active relay set from the relay management system + const relaySet = await getActiveRelaySet(ndk); + + // Update the stores with the new relay configuration + activeInboxRelays.set(relaySet.inboxRelays); + activeOutboxRelays.set(relaySet.outboxRelays); + + // Add relays to NDK pool (deduplicated) + const allRelayUrls = deduplicateRelayUrls([...relaySet.inboxRelays, ...relaySet.outboxRelays]); + for (const url of allRelayUrls) { + try { + const relay = createRelayWithAuth(url, ndk); + ndk.pool?.addRelay(relay); + } catch (error) { + // Silently ignore relay addition failures } - return true; - }); - }; + } + } catch (error) { + // Silently ignore relay store update errors + } +} - return get(feedType) === FeedType.UserRelays && user.signedIn - ? new NDKRelaySet( - new Set( - filterProblematicRelays(user.relays.inbox).map( - (relay) => - new NDKRelay(relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk), - ), - ), - ndk, - ) - : new NDKRelaySet( - new Set( - filterProblematicRelays(standardRelays).map( - (relay) => - new NDKRelay(relay, NDKRelayAuthPolicies.signIn({ ndk }), ndk), - ), - ), - ndk, - ); +/** + * Logs the current relay configuration to console + */ +export function logCurrentRelayConfiguration(): void { + const inboxRelays = get(activeInboxRelays); + const outboxRelays = get(activeOutboxRelays); + + console.log('🔌 Current Relay Configuration:'); + console.log('📥 Inbox Relays:', inboxRelays); + console.log('📤 Outbox Relays:', outboxRelays); + console.log(`📊 Total: ${inboxRelays.length} inbox, ${outboxRelays.length} outbox`); } /** - * Initializes an instance of NDK, and connects it to the logged-in user's preferred relay set - * (if available), or to Alexandria's standard relay set. - * @returns The initialized NDK instance. + * Updates relay stores when user state changes + * @param ndk NDK instance */ -export function initNdk(): NDK { - const startingPubkey = getPersistedLogin(); - const [startingInboxes, _] = - startingPubkey != null - ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey })) - : [null, null]; +export async function refreshRelayStores(ndk: NDK): Promise { + console.debug('[NDK.ts] Refreshing relay stores due to user state change'); + await updateActiveRelayStores(ndk); +} + +/** + * Updates relay stores when network condition changes + * @param ndk NDK instance + */ +export async function refreshRelayStoresOnNetworkChange(ndk: NDK): Promise { + console.debug('[NDK.ts] Refreshing relay stores due to network condition change'); + await updateActiveRelayStores(ndk); +} + +/** + * Starts network monitoring for relay optimization + * @param ndk NDK instance + */ +export function startNetworkMonitoringForRelays(ndk: NDK): void { + // Use centralized network monitoring instead of separate monitoring + startNetworkStatusMonitoring(); +} - // Ensure all relay URLs use secure WebSocket protocol - const secureRelayUrls = ( - startingInboxes != null - ? Array.from(startingInboxes.values()) - : anonymousRelays - ).map(ensureSecureWebSocket); +/** + * Creates NDKRelaySet from relay URLs with proper authentication + * @param relayUrls Array of relay URLs + * @param ndk NDK instance + * @returns NDKRelaySet + */ +function createRelaySetFromUrls(relayUrls: string[], ndk: NDK): NDKRelaySet { + const relays = relayUrls.map(url => + new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk) + ); + + return new NDKRelaySet(new Set(relays), ndk); +} - console.debug("[NDK.ts] Initializing NDK with relay URLs:", secureRelayUrls); +/** + * Gets the active relay set as NDKRelaySet for use in queries + * @param ndk NDK instance + * @param useInbox Whether to use inbox relays (true) or outbox relays (false) + * @returns Promise that resolves to NDKRelaySet + */ +export async function getActiveRelaySetAsNDKRelaySet( + ndk: NDK, + useInbox: boolean = true +): Promise { + const relaySet = await getActiveRelaySet(ndk); + const urls = useInbox ? relaySet.inboxRelays : relaySet.outboxRelays; + + return createRelaySetFromUrls(urls, ndk); +} + +/** + * Initializes an instance of NDK with the new relay management system + * @returns The initialized NDK instance + */ +export function initNdk(): NDK { + console.debug("[NDK.ts] Initializing NDK with new relay management system"); const ndk = new NDK({ - autoConnectUserRelays: true, + autoConnectUserRelays: false, // We'll manage relays manually enableOutboxModel: true, - explicitRelayUrls: secureRelayUrls, }); // Set up custom authentication policy ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk }); - // Connect with better error handling - ndk - .connect() - .then(() => { + // Connect with better error handling and reduced retry attempts + let retryCount = 0; + const maxRetries = 2; + + const attemptConnection = async () => { + try { + await ndk.connect(); console.debug("[NDK.ts] NDK connected successfully"); - }) - .catch((error) => { - console.error("[NDK.ts] Failed to connect NDK:", error); - // Try to reconnect after a delay - setTimeout(() => { - console.debug("[NDK.ts] Attempting to reconnect..."); - ndk.connect().catch((retryError) => { - console.error("[NDK.ts] Reconnection failed:", retryError); - }); - }, 5000); - }); + // Update relay stores after connection + await updateActiveRelayStores(ndk); + // Start network monitoring for relay optimization + startNetworkMonitoringForRelays(ndk); + } catch (error) { + console.warn("[NDK.ts] Failed to connect NDK:", error); + + // Only retry a limited number of times + if (retryCount < maxRetries) { + retryCount++; + console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); + setTimeout(attemptConnection, 3000); + } else { + console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); + // Still try to update relay stores even if connection failed + try { + await updateActiveRelayStores(ndk); + startNetworkMonitoringForRelays(ndk); + } catch (storeError) { + console.warn("[NDK.ts] Failed to update relay stores:", storeError); + } + } + } + }; + + attemptConnection(); return ndk; } /** - * Signs in with a NIP-07 browser extension, and determines the user's preferred inbox and outbox - * relays. - * @returns The user's profile, if it is available. - * @throws If sign-in fails. This may because there is no accessible NIP-07 extension, or because - * NDK is unable to fetch the user's profile or relay lists. + * Signs in with a NIP-07 browser extension using the new relay management system + * @returns The user's profile, if it is available + * @throws If sign-in fails */ export async function loginWithExtension( pubkey?: string, @@ -542,23 +566,10 @@ export async function loginWithExtension( activePubkey.set(signerUser.pubkey); userPubkey.set(signerUser.pubkey); - const [persistedInboxes, persistedOutboxes] = - getPersistedRelays(signerUser); - for (const relay of persistedInboxes) { - ndk.addExplicitRelay(relay); - } - const user = ndk.getUser({ pubkey: signerUser.pubkey }); - const [inboxes, outboxes] = await getUserPreferredRelays(ndk, user); - - inboxRelays.set( - Array.from(inboxes ?? persistedInboxes).map((relay) => relay.url), - ); - outboxRelays.set( - Array.from(outboxes ?? persistedOutboxes).map((relay) => relay.url), - ); - - persistRelays(signerUser, inboxes, outboxes); + + // Update relay stores with the new system + await updateActiveRelayStores(ndk); ndk.signer = signer; ndk.activeUser = user; @@ -582,73 +593,17 @@ export function logout(user: NDKUser): void { activePubkey.set(null); userPubkey.set(null); ndkSignedIn.set(false); - ndkInstance.set(initNdk()); // Re-initialize with anonymous instance + + // Clear relay stores + activeInboxRelays.set([]); + activeOutboxRelays.set([]); + + // Stop network monitoring + stopNetworkStatusMonitoring(); + + // Re-initialize with anonymous instance + const newNdk = initNdk(); + ndkInstance.set(newNdk); } -/** - * Fetches the user's NIP-65 relay list, if one can be found, and returns the inbox and outbox - * relay sets. - * @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`. - */ -export async function getUserPreferredRelays( - ndk: NDK, - user: NDKUser, - fallbacks: readonly string[] = fallbackRelays, -): Promise<[Set, Set]> { - const relayList = await ndk.fetchEvent( - { - kinds: [10002], - authors: [user.pubkey], - }, - { - groupable: false, - skipVerification: false, - skipValidation: false, - }, - NDKRelaySet.fromRelayUrls(fallbacks, ndk), - ); - - const inboxRelays = new Set(); - const outboxRelays = new Set(); - // Filter out problematic relays - const filterProblematicRelay = (url: string): boolean => { - if (url.includes("gitcitadel.nostr1.com")) { - console.warn( - `[NDK.ts] Filtering out problematic relay from user preferences: ${url}`, - ); - return false; - } - return true; - }; - - if (relayList == null) { - const relayMap = await window.nostr?.getRelays?.(); - Object.entries(relayMap ?? {}).forEach(([url, relayType]) => { - if (filterProblematicRelay(url)) { - const relay = createRelayWithAuth(url, ndk); - if (relayType.read) inboxRelays.add(relay); - if (relayType.write) outboxRelays.add(relay); - } - }); - } else { - relayList.tags.forEach((tag) => { - if (filterProblematicRelay(tag[1])) { - switch (tag[0]) { - case "r": - inboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - case "w": - outboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - default: - inboxRelays.add(createRelayWithAuth(tag[1], ndk)); - outboxRelays.add(createRelayWithAuth(tag[1], ndk)); - break; - } - } - }); - } - - return [inboxRelays, outboxRelays]; -} diff --git a/src/lib/stores.ts b/src/lib/stores.ts index ae38008..3315022 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,11 +1,11 @@ -import { readable, writable } from "svelte/store"; -import { FeedType } from "./consts.ts"; +import { writable } from "svelte/store"; -export let idList = writable([]); +// The old feedType store is no longer needed since we use the new relay management system +// All relay selection is now handled by the activeInboxRelays and activeOutboxRelays stores in ndk.ts -export let alexandriaKinds = readable([30040, 30041, 30818]); +export let idList = writable([]); -export let feedType = writable(FeedType.StandardRelays); +export let alexandriaKinds = writable([30040, 30041, 30818]); export interface PublicationLayoutVisibility { toc: boolean; diff --git a/src/lib/stores/networkStore.ts b/src/lib/stores/networkStore.ts new file mode 100644 index 0000000..981b0c9 --- /dev/null +++ b/src/lib/stores/networkStore.ts @@ -0,0 +1,55 @@ +import { writable, type Writable } from 'svelte/store'; +import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '$lib/utils/network_detection'; + +// Network status store +export const networkCondition = writable(NetworkCondition.ONLINE); +export const isNetworkChecking = writable(false); + +// Network monitoring state +let stopNetworkMonitoring: (() => void) | null = null; + +/** + * Starts network monitoring if not already running + */ +export function startNetworkStatusMonitoring(): void { + if (stopNetworkMonitoring) { + return; // Already monitoring + } + + console.debug('[networkStore.ts] Starting network status monitoring'); + + stopNetworkMonitoring = startNetworkMonitoring( + (condition: NetworkCondition) => { + console.debug(`[networkStore.ts] Network condition changed to: ${condition}`); + networkCondition.set(condition); + }, + 60000 // Check every 60 seconds to reduce spam + ); +} + +/** + * Stops network monitoring + */ +export function stopNetworkStatusMonitoring(): void { + if (stopNetworkMonitoring) { + console.debug('[networkStore.ts] Stopping network status monitoring'); + stopNetworkMonitoring(); + stopNetworkMonitoring = null; + } +} + +/** + * Manually check network status (for immediate updates) + */ +export async function checkNetworkStatus(): Promise { + try { + isNetworkChecking.set(true); + const condition = await detectNetworkCondition(); + networkCondition.set(condition); + } catch (error) { + console.warn('[networkStore.ts] Failed to check network status:', error); + networkCondition.set(NetworkCondition.OFFLINE); + } finally { + isNetworkChecking.set(false); + } +} \ No newline at end of file diff --git a/src/lib/stores/relayStore.ts b/src/lib/stores/relayStore.ts deleted file mode 100644 index 2c038c7..0000000 --- a/src/lib/stores/relayStore.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { writable } from "svelte/store"; - -// Initialize with empty array, will be populated from user preferences -export const userRelays = writable([]); diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts index e5dbe8b..3275e23 100644 --- a/src/lib/stores/userStore.ts +++ b/src/lib/stores/userStore.ts @@ -8,8 +8,8 @@ import { NDKRelay, } from "@nostr-dev-kit/ndk"; import { getUserMetadata } from "$lib/utils/nostrUtils"; -import { ndkInstance } from "$lib/ndk"; -import { loginStorageKey, fallbackRelays } from "$lib/consts"; +import { ndkInstance, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; +import { loginStorageKey } from "$lib/consts"; import { nip19 } from "nostr-tools"; export interface UserState { @@ -70,7 +70,7 @@ function getPersistedRelays(user: NDKUser): [Set, Set] { async function getUserPreferredRelays( ndk: any, user: NDKUser, - fallbacks: readonly string[] = fallbackRelays, + fallbacks: readonly string[] = [...get(activeInboxRelays), ...get(activeOutboxRelays)], ): Promise<[Set, Set]> { const relayList = await ndk.fetchEvent( { diff --git a/src/lib/utils/ZettelParser.ts b/src/lib/utils/ZettelParser.ts index 24b0891..42d1b77 100644 --- a/src/lib/utils/ZettelParser.ts +++ b/src/lib/utils/ZettelParser.ts @@ -1,7 +1,7 @@ import { ndkInstance } from "$lib/ndk"; import { signEvent, getEventHash } from "$lib/utils/nostrUtils"; import { getMimeTags } from "$lib/utils/mime"; -import { standardRelays } from "$lib/consts"; +import { communityRelays } from "$lib/consts"; import { nip19 } from "nostr-tools"; export interface ZettelSection { diff --git a/src/lib/utils/community_checker.ts b/src/lib/utils/community_checker.ts index 56c9030..7e6ea11 100644 --- a/src/lib/utils/community_checker.ts +++ b/src/lib/utils/community_checker.ts @@ -1,4 +1,4 @@ -import { communityRelay } from "$lib/consts"; +import { communityRelays } from "$lib/consts"; import { RELAY_CONSTANTS, SEARCH_LIMITS } from "./search_constants"; // Cache for pubkeys with kind 1 events on communityRelay @@ -13,40 +13,54 @@ export async function checkCommunity(pubkey: string): Promise { } try { - const relayUrl = communityRelay; - const ws = new WebSocket(relayUrl); - return await new Promise((resolve) => { - ws.onopen = () => { - ws.send( - JSON.stringify([ - "REQ", - RELAY_CONSTANTS.COMMUNITY_REQUEST_ID, - { - kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS, - authors: [pubkey], - limit: SEARCH_LIMITS.COMMUNITY_CHECK, - }, - ]), - ); - }; - ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data[0] === "EVENT" && data[2]?.kind === 1) { - communityCache.set(pubkey, true); - ws.close(); - resolve(true); - } else if (data[0] === "EOSE") { - communityCache.set(pubkey, false); - ws.close(); - resolve(false); + // Try each community relay until we find one that works + for (const relayUrl of communityRelays) { + try { + const ws = new WebSocket(relayUrl); + const result = await new Promise((resolve) => { + ws.onopen = () => { + ws.send( + JSON.stringify([ + "REQ", + RELAY_CONSTANTS.COMMUNITY_REQUEST_ID, + { + kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS, + authors: [pubkey], + limit: SEARCH_LIMITS.COMMUNITY_CHECK, + }, + ]), + ); + }; + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data[0] === "EVENT" && data[2]?.kind === 1) { + communityCache.set(pubkey, true); + ws.close(); + resolve(true); + } else if (data[0] === "EOSE") { + communityCache.set(pubkey, false); + ws.close(); + resolve(false); + } + }; + ws.onerror = () => { + ws.close(); + resolve(false); + }; + }); + + if (result) { + return true; } - }; - ws.onerror = () => { - communityCache.set(pubkey, false); - ws.close(); - resolve(false); - }; - }); + } catch { + // Continue to next relay if this one fails + continue; + } + } + + // If we get here, no relay found the user + communityCache.set(pubkey, false); + return false; } catch { communityCache.set(pubkey, false); return false; diff --git a/src/lib/utils/network_detection.ts b/src/lib/utils/network_detection.ts new file mode 100644 index 0000000..1e812db --- /dev/null +++ b/src/lib/utils/network_detection.ts @@ -0,0 +1,189 @@ +import { deduplicateRelayUrls } from './relay_management'; + +/** + * Network conditions for relay selection + */ +export enum NetworkCondition { + ONLINE = 'online', + SLOW = 'slow', + OFFLINE = 'offline' +} + +/** + * Network connectivity test endpoints + */ +const NETWORK_ENDPOINTS = [ + 'https://www.google.com/favicon.ico', + 'https://httpbin.org/status/200', + 'https://api.github.com/zen' +]; + +/** + * Detects if the network is online using more reliable endpoints + * @returns Promise that resolves to true if online, false otherwise + */ +export async function isNetworkOnline(): Promise { + for (const endpoint of NETWORK_ENDPOINTS) { + try { + // Use a simple fetch without HEAD method to avoid CORS issues + const response = await fetch(endpoint, { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(3000), + mode: 'no-cors' // Use no-cors mode to avoid CORS issues + }); + // With no-cors mode, we can't check response.ok, so we assume success if no error + return true; + } catch (error) { + console.debug(`[network_detection.ts] Failed to reach ${endpoint}:`, error); + continue; + } + } + + console.debug('[network_detection.ts] All network endpoints failed'); + return false; +} + +/** + * Tests network speed by measuring response time + * @returns Promise that resolves to network speed in milliseconds + */ +export async function testNetworkSpeed(): Promise { + const startTime = performance.now(); + + for (const endpoint of NETWORK_ENDPOINTS) { + try { + await fetch(endpoint, { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(5000), + mode: 'no-cors' // Use no-cors mode to avoid CORS issues + }); + + const endTime = performance.now(); + return endTime - startTime; + } catch (error) { + console.debug(`[network_detection.ts] Speed test failed for ${endpoint}:`, error); + continue; + } + } + + console.debug('[network_detection.ts] Network speed test failed for all endpoints'); + return Infinity; // Very slow if it fails +} + +/** + * Determines network condition based on connectivity and speed + * @returns Promise that resolves to NetworkCondition + */ +export async function detectNetworkCondition(): Promise { + const isOnline = await isNetworkOnline(); + + if (!isOnline) { + console.debug('[network_detection.ts] Network condition: OFFLINE'); + return NetworkCondition.OFFLINE; + } + + const speed = await testNetworkSpeed(); + + // Consider network slow if response time > 2000ms + if (speed > 2000) { + console.debug(`[network_detection.ts] Network condition: SLOW (${speed.toFixed(0)}ms)`); + return NetworkCondition.SLOW; + } + + console.debug(`[network_detection.ts] Network condition: ONLINE (${speed.toFixed(0)}ms)`); + return NetworkCondition.ONLINE; +} + +/** + * Gets the appropriate relay sets based on network condition + * @param networkCondition The detected network condition + * @param discoveredLocalRelays Array of discovered local relay URLs + * @param lowbandwidthRelays Array of low bandwidth relay URLs + * @param fullRelaySet The complete relay set for normal conditions + * @returns Object with inbox and outbox relay arrays + */ +export function getRelaySetForNetworkCondition( + networkCondition: NetworkCondition, + discoveredLocalRelays: string[], + lowbandwidthRelays: string[], + fullRelaySet: { inboxRelays: string[]; outboxRelays: string[] } +): { inboxRelays: string[]; outboxRelays: string[] } { + switch (networkCondition) { + case NetworkCondition.OFFLINE: + // When offline, use local relays if available, otherwise rely on cache + // This will be improved when IndexedDB local relay is implemented + if (discoveredLocalRelays.length > 0) { + console.debug('[network_detection.ts] Using local relays (offline)'); + return { + inboxRelays: discoveredLocalRelays, + outboxRelays: discoveredLocalRelays + }; + } else { + console.debug('[network_detection.ts] No local relays available, will rely on cache (offline)'); + return { + inboxRelays: [], + outboxRelays: [] + }; + } + + case NetworkCondition.SLOW: + // Local relays + low bandwidth relays when slow (deduplicated) + console.debug('[network_detection.ts] Using local + low bandwidth relays (slow network)'); + const slowInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]); + const slowOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]); + return { + inboxRelays: slowInboxRelays, + outboxRelays: slowOutboxRelays + }; + + case NetworkCondition.ONLINE: + default: + // Full relay set when online + console.debug('[network_detection.ts] Using full relay set (online)'); + return fullRelaySet; + } +} + +/** + * Starts periodic network monitoring with reduced frequency to avoid spam + * @param onNetworkChange Callback function called when network condition changes + * @param checkInterval Interval in milliseconds between network checks (default: 60 seconds) + * @returns Function to stop the monitoring + */ +export function startNetworkMonitoring( + onNetworkChange: (condition: NetworkCondition) => void, + checkInterval: number = 60000 // Increased to 60 seconds to reduce spam +): () => void { + let lastCondition: NetworkCondition | null = null; + let intervalId: number | null = null; + + const checkNetwork = async () => { + try { + const currentCondition = await detectNetworkCondition(); + + if (currentCondition !== lastCondition) { + console.debug(`[network_detection.ts] Network condition changed: ${lastCondition} -> ${currentCondition}`); + lastCondition = currentCondition; + onNetworkChange(currentCondition); + } + } catch (error) { + console.warn('[network_detection.ts] Network monitoring error:', error); + } + }; + + // Initial check + checkNetwork(); + + // Set up periodic monitoring + intervalId = window.setInterval(checkNetwork, checkInterval); + + // Return function to stop monitoring + return () => { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + }; +} \ No newline at end of file diff --git a/src/lib/utils/nostrEventService.ts b/src/lib/utils/nostrEventService.ts index 5efb37b..e7d437f 100644 --- a/src/lib/utils/nostrEventService.ts +++ b/src/lib/utils/nostrEventService.ts @@ -1,11 +1,13 @@ import { nip19 } from "nostr-tools"; import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils"; -import { standardRelays, fallbackRelays } from "$lib/consts"; -import { userRelays } from "$lib/stores/relayStore"; +import { communityRelays, secondaryRelays } from "$lib/consts"; import { get } from "svelte/store"; import { goto } from "$app/navigation"; import type { NDKEvent } from "./nostrUtils"; import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from "./search_constants"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; +import { ndkInstance } from "$lib/ndk"; +import { NDKRelaySet } from "@nostr-dev-kit/ndk"; export interface RootEventInfo { rootId: string; @@ -358,82 +360,44 @@ export async function createSignedEvent( } /** - * Publish event to a single relay - */ -async function publishToRelay( - relayUrl: string, - signedEvent: any, -): Promise { - const ws = new WebSocket(relayUrl); - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - ws.close(); - reject(new Error("Timeout")); - }, TIMEOUTS.GENERAL); - - ws.onopen = () => { - ws.send(JSON.stringify(["EVENT", signedEvent])); - }; - - ws.onmessage = (e) => { - const [type, id, ok, message] = JSON.parse(e.data); - if (type === "OK" && id === signedEvent.id) { - clearTimeout(timeout); - if (ok) { - ws.close(); - resolve(); - } else { - ws.close(); - reject(new Error(message)); - } - } - }; - - ws.onerror = () => { - clearTimeout(timeout); - ws.close(); - reject(new Error("WebSocket error")); - }; - }); -} - -/** - * Publish event to relays + * Publishes an event to relays using the new relay management system + * @param event The event to publish + * @param relayUrls Array of relay URLs to publish to + * @returns Promise that resolves to array of successful relay URLs */ export async function publishEvent( - signedEvent: any, - useOtherRelays = false, - useFallbackRelays = false, - userRelayPreference = false, -): Promise { - // Determine which relays to use - let relays = userRelayPreference ? get(userRelays) : standardRelays; - if (useOtherRelays) { - relays = userRelayPreference ? standardRelays : get(userRelays); - } - if (useFallbackRelays) { - relays = fallbackRelays; + event: NDKEvent, + relayUrls: string[], +): Promise { + const successfulRelays: string[] = []; + const ndk = get(ndkInstance); + + if (!ndk) { + throw new Error("NDK instance not available"); } - // Try to publish to relays - for (const relayUrl of relays) { - try { - await publishToRelay(relayUrl, signedEvent); - return { - success: true, - relay: relayUrl, - eventId: signedEvent.id, - }; - } catch (e) { - console.error(`Failed to publish to ${relayUrl}:`, e); - } + // Create relay set from URLs + const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk); + + try { + // Publish with timeout + await event.publish(relaySet).withTimeout(10000); + + // For now, assume all relays were successful + // In a more sophisticated implementation, you'd track individual relay responses + successfulRelays.push(...relayUrls); + + console.debug("[nostrEventService] Published event successfully:", { + eventId: event.id, + relayCount: relayUrls.length, + successfulRelays + }); + } catch (error) { + console.error("[nostrEventService] Failed to publish event:", error); + throw new Error(`Failed to publish event: ${error}`); } - return { - success: false, - error: "Failed to publish to any relays", - }; + return successfulRelays; } /** diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 23ed7b9..a7aef5e 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -4,7 +4,8 @@ import { ndkInstance } from "$lib/ndk"; import { npubCache } from "./npubCache"; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; -import { standardRelays, fallbackRelays, anonymousRelays } from "$lib/consts"; +import { communityRelays, secondaryRelays, anonymousRelays } from "$lib/consts"; +import { activeInboxRelays } from "$lib/ndk"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { sha256 } from "@noble/hashes/sha256"; import { schnorr } from "@noble/curves/secp256k1"; @@ -174,9 +175,9 @@ export async function createProfileLinkWithVerification( }; const allRelays = [ - ...standardRelays, + ...communityRelays, ...userRelays, - ...fallbackRelays, + ...secondaryRelays, ].filter((url, idx, arr) => arr.indexOf(url) === idx); const filteredRelays = filterProblematicRelays(allRelays); @@ -422,91 +423,43 @@ export async function fetchEventWithFallback( filterOrId: string | NDKFilter, timeoutMs: number = 3000, ): Promise { - // Get user relays if logged in - const userRelays = ndk.activeUser - ? Array.from(ndk.pool?.relays.values() || []) - .filter((r) => r.status === 1) // Only use connected relays - .map((r) => r.url) - .filter((url) => !url.includes("gitcitadel.nostr1.com")) // Filter out problematic relay - : []; - - // Determine which relays to use based on user authentication status - const isSignedIn = ndk.signer && ndk.activeUser; - const primaryRelays = isSignedIn ? standardRelays : anonymousRelays; - - // Create three relay sets in priority order - const relaySets = [ - NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous) - NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in) - NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk), // 3. fallback relays (last resort) - ]; + // Use the active inbox relays from the relay management system + const inboxRelays = get(activeInboxRelays); + + // Create relay set from active inbox relays + const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk); try { - let found: NDKEvent | null = null; - const triedRelaySets: string[] = []; - - // Helper function to try fetching from a relay set - async function tryFetchFromRelaySet( - relaySet: NDKRelaySetFromNDK, - setName: string, - ): Promise { - if (relaySet.relays.size === 0) return null; - triedRelaySets.push(setName); - - if ( - typeof filterOrId === "string" && - new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, "i").test(filterOrId) - ) { - return await ndk - .fetchEvent({ ids: [filterOrId] }, undefined, relaySet) - .withTimeout(timeoutMs); - } else { - const filter = - typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId; - const results = await ndk - .fetchEvents(filter, undefined, relaySet) - .withTimeout(timeoutMs); - return results instanceof Set - ? (Array.from(results)[0] as NDKEvent) - : null; - } + if (relaySet.relays.size === 0) { + console.warn("No inbox relays available for event fetch"); + return null; } - // Try each relay set in order - for (const [index, relaySet] of relaySets.entries()) { - const setName = - index === 0 - ? isSignedIn - ? "standard relays" - : "anonymous relays" - : index === 1 - ? "user relays" - : "fallback relays"; - - found = await tryFetchFromRelaySet(relaySet, setName); - if (found) break; + let found: NDKEvent | null = null; + + if ( + typeof filterOrId === "string" && + new RegExp(`^[0-9a-f]{${VALIDATION.HEX_LENGTH}}$`, "i").test(filterOrId) + ) { + found = await ndk + .fetchEvent({ ids: [filterOrId] }, undefined, relaySet) + .withTimeout(timeoutMs); + } else { + const filter = + typeof filterOrId === "string" ? { ids: [filterOrId] } : filterOrId; + const results = await ndk + .fetchEvents(filter, undefined, relaySet) + .withTimeout(timeoutMs); + found = results instanceof Set + ? (Array.from(results)[0] as NDKEvent) + : null; } if (!found) { const timeoutSeconds = timeoutMs / 1000; - const relayUrls = relaySets - .map((set, i) => { - const setName = - i === 0 - ? isSignedIn - ? "standard relays" - : "anonymous relays" - : i === 1 - ? "user relays" - : "fallback relays"; - const urls = Array.from(set.relays).map((r) => r.url); - return urls.length > 0 ? `${setName} (${urls.join(", ")})` : null; - }) - .filter(Boolean) - .join(", then "); - + const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", "); console.warn( - `Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`, + `Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`, ); return null; } diff --git a/src/lib/utils/profile_search.ts b/src/lib/utils/profile_search.ts index 20b3db0..1fde323 100644 --- a/src/lib/utils/profile_search.ts +++ b/src/lib/utils/profile_search.ts @@ -2,7 +2,7 @@ import { ndkInstance } from "$lib/ndk"; import { getUserMetadata, getNpubFromNip05 } from "$lib/utils/nostrUtils"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "$lib/utils/searchCache"; -import { standardRelays, fallbackRelays } from "$lib/consts"; +import { communityRelays, secondaryRelays } from "$lib/consts"; import { get } from "svelte/store"; import type { NostrProfile, ProfileSearchResult } from "./search_types"; import { @@ -270,7 +270,7 @@ async function quickRelaySearch( console.log("Normalized search term for relay search:", normalizedSearchTerm); // Use all profile relays for better coverage - const quickRelayUrls = [...standardRelays, ...fallbackRelays]; // Use all available relays + const quickRelayUrls = [...communityRelays, ...secondaryRelays]; // Use all available relays console.log("Using all relays for search:", quickRelayUrls); // Create relay sets for parallel search diff --git a/src/lib/utils/relayDiagnostics.ts b/src/lib/utils/relayDiagnostics.ts index dee9bc4..9a0db7c 100644 --- a/src/lib/utils/relayDiagnostics.ts +++ b/src/lib/utils/relayDiagnostics.ts @@ -1,6 +1,7 @@ -import { standardRelays, anonymousRelays, fallbackRelays } from "$lib/consts"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import NDK from "@nostr-dev-kit/ndk"; import { TIMEOUTS } from "./search_constants"; +import { get } from "svelte/store"; export interface RelayDiagnostic { url: string; @@ -85,9 +86,8 @@ export async function testRelay(url: string): Promise { * Tests all relays and returns diagnostic information */ export async function testAllRelays(): Promise { - const allRelays = [ - ...new Set([...standardRelays, ...anonymousRelays, ...fallbackRelays]), - ]; + // Use the new relay management system + const allRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; console.log("[RelayDiagnostics] Testing", allRelays.length, "relays..."); diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts new file mode 100644 index 0000000..dbaa08a --- /dev/null +++ b/src/lib/utils/relay_management.ts @@ -0,0 +1,424 @@ +import NDK, { NDKRelay, NDKUser } from "@nostr-dev-kit/ndk"; +import { communityRelays, searchRelays, secondaryRelays, anonymousRelays, lowbandwidthRelays, localRelays } from "../consts"; +import { getRelaySetForNetworkCondition, NetworkCondition } from "./network_detection"; +import { networkCondition } from "../stores/networkStore"; +import { get } from "svelte/store"; + +/** + * Normalizes a relay URL to a standard format + * @param url The relay URL to normalize + * @returns The normalized relay URL + */ +export function normalizeRelayUrl(url: string): string { + let normalized = url.toLowerCase().trim(); + + // Ensure protocol is present + if (!normalized.startsWith('ws://') && !normalized.startsWith('wss://')) { + normalized = 'wss://' + normalized; + } + + // Remove trailing slash + normalized = normalized.replace(/\/$/, ''); + + return normalized; +} + +/** + * Normalizes an array of relay URLs + * @param urls Array of relay URLs to normalize + * @returns Array of normalized relay URLs + */ +export function normalizeRelayUrls(urls: string[]): string[] { + return urls.map(normalizeRelayUrl); +} + +/** + * Removes duplicates from an array of relay URLs + * @param urls Array of relay URLs + * @returns Array of unique relay URLs + */ +export function deduplicateRelayUrls(urls: string[]): string[] { + const normalized = normalizeRelayUrls(urls); + return [...new Set(normalized)]; +} + +/** + * Tests connection to a relay and returns connection status + * @param relayUrl The relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status + */ +export async function testRelayConnection( + relayUrl: string, + ndk: NDK, +): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + return new Promise((resolve) => { + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(relayUrl); + + // Use the existing NDK instance instead of creating a new one + const relay = new NDKRelay(secureUrl, undefined, ndk); + let authRequired = false; + let connected = false; + let error: string | undefined; + let actualUrl: string | undefined; + + const timeout = setTimeout(() => { + relay.disconnect(); + resolve({ + connected: false, + requiresAuth: authRequired, + error: "Connection timeout", + actualUrl, + }); + }, 3000); // Increased timeout to 3 seconds to give relays more time + + relay.on("connect", () => { + connected = true; + actualUrl = secureUrl; + clearTimeout(timeout); + relay.disconnect(); + resolve({ + connected: true, + requiresAuth: authRequired, + error, + actualUrl, + }); + }); + + relay.on("notice", (message: string) => { + if (message.includes("auth-required")) { + authRequired = true; + } + }); + + relay.on("disconnect", () => { + if (!connected) { + error = "Connection failed"; + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: authRequired, + error, + actualUrl, + }); + } + }); + + relay.connect(); + }); +} + +/** + * Ensures a relay URL uses secure WebSocket protocol for remote relays + * @param url The relay URL to secure + * @returns The URL with wss:// protocol (except for localhost) + */ +function ensureSecureWebSocket(url: string): string { + // For localhost, always use ws:// (never wss://) + if (url.includes('localhost') || url.includes('127.0.0.1')) { + // Convert any wss://localhost to ws://localhost + return url.replace(/^wss:\/\//, "ws://"); + } + + // Replace ws:// with wss:// for remote relays + const secureUrl = url.replace(/^ws:\/\//, "wss://"); + + if (secureUrl !== url) { + console.warn( + `[relay_management.ts] Protocol upgrade for rem ote relay: ${url} -> ${secureUrl}`, + ); + } + + return secureUrl; +} + +/** + * Tests connection to local relays + * @param localRelayUrls Array of local relay URLs to test + * @param ndk NDK instance + * @returns Promise that resolves to array of working local relay URLs + */ +async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise { + const workingRelays: string[] = []; + + await Promise.all( + localRelayUrls.map(async (url) => { + try { + const result = await testRelayConnection(url, ndk); + if (result.connected) { + workingRelays.push(url); + } + } catch (error) { + // Silently ignore local relay failures + } + }) + ); + + return workingRelays; +} + +/** + * Discovers local relays by testing common localhost URLs + * @param ndk NDK instance + * @returns Promise that resolves to array of working local relay URLs + */ +export async function discoverLocalRelays(ndk: NDK): Promise { + try { + // Convert wss:// URLs from consts to ws:// for local testing + const localRelayUrls = localRelays.map(url => + url.replace(/^wss:\/\//, 'ws://') + ); + + const workingRelays = await testLocalRelays(localRelayUrls, ndk); + + // If no local relays are working, return empty array + // The network detection logic will provide fallback relays + return workingRelays; + } catch (error) { + // Silently fail and return empty array + return []; + } +} + +/** + * Fetches user's local relays from kind 10432 event + * @param ndk NDK instance + * @param user User to fetch local relays for + * @returns Promise that resolves to array of local relay URLs + */ +export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise { + try { + const localRelayEvent = await ndk.fetchEvent( + { + kinds: [10432 as any], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + } + ); + + if (!localRelayEvent) { + return []; + } + + const localRelays: string[] = []; + localRelayEvent.tags.forEach((tag) => { + if (tag[0] === 'r' && tag[1]) { + localRelays.push(tag[1]); + } + }); + + return localRelays; + } catch (error) { + console.info('[relay_management.ts] Error fetching user local relays:', error); + return []; + } +} + +/** + * Fetches user's blocked relays from kind 10006 event + * @param ndk NDK instance + * @param user User to fetch blocked relays for + * @returns Promise that resolves to array of blocked relay URLs + */ +export async function getUserBlockedRelays(ndk: NDK, user: NDKUser): Promise { + try { + const blockedRelayEvent = await ndk.fetchEvent( + { + kinds: [10006], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + } + ); + + if (!blockedRelayEvent) { + return []; + } + + const blockedRelays: string[] = []; + blockedRelayEvent.tags.forEach((tag) => { + if (tag[0] === 'r' && tag[1]) { + blockedRelays.push(tag[1]); + } + }); + + return blockedRelays; + } catch (error) { + console.info('[relay_management.ts] Error fetching user blocked relays:', error); + return []; + } +} + +/** + * Fetches user's outbox relays from NIP-65 relay list + * @param ndk NDK instance + * @param user User to fetch outbox relays for + * @returns Promise that resolves to array of outbox relay URLs + */ +export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { + try { + const relayList = await ndk.fetchEvent( + { + kinds: [10002], + authors: [user.pubkey], + }, + { + groupable: false, + skipVerification: false, + skipValidation: false, + } + ); + + if (!relayList) { + return []; + } + + const outboxRelays: string[] = []; + relayList.tags.forEach((tag) => { + if (tag[0] === 'w' && tag[1]) { + outboxRelays.push(tag[1]); + } + }); + + return outboxRelays; + } catch (error) { + console.info('[relay_management.ts] Error fetching user outbox relays:', error); + return []; + } +} + +/** + * Tests a set of relays in batches to avoid overwhelming them + * @param relayUrls Array of relay URLs to test + * @param ndk NDK instance + * @returns Promise that resolves to array of working relay URLs + */ +async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { + const workingRelays: string[] = []; + const maxConcurrent = 3; // Test 3 relays at a time to avoid overwhelming them + + for (let i = 0; i < relayUrls.length; i += maxConcurrent) { + const batch = relayUrls.slice(i, i + maxConcurrent); + + const batchPromises = batch.map(async (url) => { + try { + const result = await testRelayConnection(url, ndk); + return result.connected ? url : null; + } catch (error) { + return null; + } + }); + + const batchResults = await Promise.all(batchPromises); + const batchWorkingRelays = batchResults.filter((url): url is string => url !== null); + workingRelays.push(...batchWorkingRelays); + } + + return workingRelays; +} + +/** + * Builds a complete relay set for a user, including local, user-specific, and fallback relays + * @param ndk NDK instance + * @param user NDKUser or null for anonymous access + * @returns Promise that resolves to inbox and outbox relay arrays + */ +export async function buildCompleteRelaySet( + ndk: NDK, + user: NDKUser | null +): Promise<{ inboxRelays: string[]; outboxRelays: string[] }> { + // Discover local relays first + const discoveredLocalRelays = await discoverLocalRelays(ndk); + + // Get user-specific relays if available + let userOutboxRelays: string[] = []; + let userLocalRelays: string[] = []; + let blockedRelays: string[] = []; + + if (user) { + try { + userOutboxRelays = await getUserOutboxRelays(ndk, user); + } catch (error) { + // Silently ignore user relay fetch errors + } + + try { + userLocalRelays = await getUserLocalRelays(ndk, user); + } catch (error) { + // Silently ignore user local relay fetch errors + } + + try { + blockedRelays = await getUserBlockedRelays(ndk, user); + } catch (error) { + // Silently ignore blocked relay fetch errors + } + } + + // Build initial relay sets and deduplicate + const finalInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userLocalRelays]); + const finalOutboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...userOutboxRelays]); + + // Test relays and filter out non-working ones + let testedInboxRelays: string[] = []; + let testedOutboxRelays: string[] = []; + + if (finalInboxRelays.length > 0) { + testedInboxRelays = await testRelaySet(finalInboxRelays, ndk); + } + + if (finalOutboxRelays.length > 0) { + testedOutboxRelays = await testRelaySet(finalOutboxRelays, ndk); + } + + // If no relays passed testing, use remote relays without testing + if (testedInboxRelays.length === 0 && testedOutboxRelays.length === 0) { + const remoteRelays = deduplicateRelayUrls([...secondaryRelays, ...searchRelays]); + return { + inboxRelays: remoteRelays, + outboxRelays: remoteRelays + }; + } + + // Use tested relays and deduplicate + const inboxRelays = testedInboxRelays.length > 0 ? deduplicateRelayUrls(testedInboxRelays) : deduplicateRelayUrls(secondaryRelays); + const outboxRelays = testedOutboxRelays.length > 0 ? deduplicateRelayUrls(testedOutboxRelays) : deduplicateRelayUrls(secondaryRelays); + + // Apply network condition optimization + const currentNetworkCondition = get(networkCondition); + const networkOptimizedRelaySet = getRelaySetForNetworkCondition( + currentNetworkCondition, + discoveredLocalRelays, + lowbandwidthRelays, + { inboxRelays, outboxRelays } + ); + + // Filter out blocked relays and deduplicate final sets + const finalRelaySet = { + inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter(r => !blockedRelays.includes(r))), + outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter(r => !blockedRelays.includes(r))) + }; + + // If no relays are working, use anonymous relays as fallback + if (finalRelaySet.inboxRelays.length === 0 && finalRelaySet.outboxRelays.length === 0) { + return { + inboxRelays: deduplicateRelayUrls(anonymousRelays), + outboxRelays: deduplicateRelayUrls(anonymousRelays) + }; + } + + return finalRelaySet; +} \ No newline at end of file diff --git a/src/lib/utils/subscription_search.ts b/src/lib/utils/subscription_search.ts index 97ef19d..22cefb4 100644 --- a/src/lib/utils/subscription_search.ts +++ b/src/lib/utils/subscription_search.ts @@ -3,7 +3,7 @@ import { getMatchingTags, getNpubFromNip05 } from "$lib/utils/nostrUtils"; import { nip19 } from "$lib/utils/nostrUtils"; import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk"; import { searchCache } from "$lib/utils/searchCache"; -import { communityRelay, profileRelays } from "$lib/consts"; +import { communityRelays, searchRelays } from "$lib/consts"; import { get } from "svelte/store"; import type { SearchResult, @@ -20,6 +20,12 @@ import { isEmojiReaction, } from "./search_utils"; import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants"; +import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; + +// Helper function to normalize URLs for comparison +const normalizeUrl = (url: string): string => { + return url.replace(/\/$/, ''); // Remove trailing slash +}; /** * Search for events by subscription type (d, t, n) @@ -292,23 +298,40 @@ function createPrimaryRelaySet( searchType: SearchSubscriptionType, ndk: any, ): NDKRelaySet { + // Use the new relay management system + const searchRelays = [...get(activeInboxRelays), ...get(activeOutboxRelays)]; + console.debug('subscription_search: Active relay stores:', { + inboxRelays: get(activeInboxRelays), + outboxRelays: get(activeOutboxRelays), + searchRelays + }); + + // Debug: Log all relays in NDK pool + const poolRelays = Array.from(ndk.pool.relays.values()); + console.debug('subscription_search: NDK pool relays:', poolRelays.map((r: any) => r.url)); + if (searchType === "n") { - // For profile searches, use profile relays first - const profileRelaySet = Array.from(ndk.pool.relays.values()).filter( + // For profile searches, use search relays first + const profileRelaySet = poolRelays.filter( (relay: any) => - profileRelays.some( - (profileRelay) => - relay.url === profileRelay || relay.url === profileRelay + "/", + searchRelays.some( + (searchRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(searchRelay), ), ); + console.debug('subscription_search: Profile relay set:', profileRelaySet.map((r: any) => r.url)); return new NDKRelaySet(new Set(profileRelaySet) as any, ndk); } else { - // For other searches, use community relay first - const communityRelaySet = Array.from(ndk.pool.relays.values()).filter( + // For other searches, use active relays first + const activeRelaySet = poolRelays.filter( (relay: any) => - relay.url === communityRelay || relay.url === communityRelay + "/", + searchRelays.some( + (searchRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(searchRelay), + ), ); - return new NDKRelaySet(new Set(communityRelaySet) as any, ndk); + console.debug('subscription_search: Active relay set:', activeRelaySet.map((r: any) => r.url)); + return new NDKRelaySet(new Set(activeRelaySet) as any, ndk); } } @@ -511,15 +534,16 @@ async function searchOtherRelaysInBackground( new Set( Array.from(ndk.pool.relays.values()).filter((relay: any) => { if (searchType === "n") { - // For profile searches, exclude profile relays from fallback search - return !profileRelays.some( - (profileRelay) => - relay.url === profileRelay || relay.url === profileRelay + "/", + // For profile searches, exclude search relays from fallback search + return !searchRelays.some( + (searchRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(searchRelay), ); } else { - // For other searches, exclude community relay from fallback search - return ( - relay.url !== communityRelay && relay.url !== communityRelay + "/" + // For other searches, exclude community relays from fallback search + return !communityRelays.some( + (communityRelay: string) => + normalizeUrl(relay.url) === normalizeUrl(communityRelay), ); } }), diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9cb3197..47be24c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import { page } from "$app/stores"; import { Alert } from "flowbite-svelte"; import { HammerSolid } from "flowbite-svelte-icons"; + import { logCurrentRelayConfiguration } from "$lib/ndk"; // Get standard metadata for OpenGraph tags let title = "Library of Alexandria"; @@ -18,6 +19,9 @@ onMount(() => { const rect = document.body.getBoundingClientRect(); // document.body.style.height = `${rect.height}px`; + + // Log relay configuration when layout mounts + logCurrentRelayConfiguration(); }); diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index eb40d1c..dd255ca 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,5 +1,3 @@ -import { feedTypeStorageKey } from "$lib/consts"; -import { FeedType } from "$lib/consts"; import { getPersistedLogin, initNdk, ndkInstance } from "$lib/ndk"; import { loginWithExtension, @@ -8,18 +6,13 @@ import { } from "$lib/stores/userStore"; import { loginMethodStorageKey } from "$lib/stores/userStore"; import Pharos, { pharosInstance } from "$lib/parser"; -import { feedType } from "$lib/stores"; import type { LayoutLoad } from "./$types"; import { get } from "svelte/store"; export const ssr = false; export const load: LayoutLoad = () => { - const initialFeedType = - (localStorage.getItem(feedTypeStorageKey) as FeedType) ?? - FeedType.StandardRelays; - feedType.set(initialFeedType); - + // Initialize NDK with new relay management system const ndk = initNdk(); ndkInstance.set(ndk); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 594d3cc..8461bd4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,14 +1,17 @@ + + {#if eventCount.total > 0} +
    + Showing {eventCount.displayed} of {eventCount.total} events. +
    + {/if} + diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index fbe29c0..4137220 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -9,9 +9,9 @@ Input, Modal, } from "flowbite-svelte"; - import { ndkInstance, ndkSignedIn } from "$lib/ndk"; + import { ndkInstance, ndkSignedIn, activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { userStore } from "$lib/stores/userStore"; - import { standardRelays } from "$lib/consts"; + import { communityRelays } from "$lib/consts"; import type NDK from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; // @ts-ignore - Workaround for Svelte component import issue @@ -62,12 +62,13 @@ const repoAddress = "naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqy88wumn8ghj7mn0wvhxcmmv9uqq5stvv4uxzmnywf5kz2elajr"; - // Hard-coded relays to ensure we have working relays + // Use the new relay management system instead of hardcoded relays const allRelays = [ "wss://relay.damus.io", "wss://relay.nostr.band", "wss://nos.lol", - ...standardRelays, + ...$activeInboxRelays, + ...$activeOutboxRelays, ]; // Hard-coded repository owner pubkey and ID from the task @@ -451,7 +452,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi size="xs" class="absolute bottom-2 right-2 z-10 opacity-60 hover:opacity-100" color="light" - on:click={toggleSize} + onclick={toggleSize} > {isExpanded ? "⌃" : "⌄"} @@ -459,7 +460,7 @@ Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. Wi
    - - + +
    diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index c0932b5..0b8c20d 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -13,13 +13,9 @@ import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import EventInput from "$lib/components/EventInput.svelte"; import { userPubkey, isLoggedIn } from "$lib/stores/authStore.Svelte"; - import { - testAllRelays, - logRelayDiagnostics, - } from "$lib/utils/relayDiagnostics"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import { neventEncode, naddrEncode } from "$lib/utils"; - import { standardRelays } from "$lib/consts"; + import { activeInboxRelays, activeOutboxRelays, logCurrentRelayConfiguration } from "$lib/ndk"; import { getEventType } from "$lib/utils/mime"; import ViewPublicationLink from "$lib/components/util/ViewPublicationLink.svelte"; import { checkCommunity } from "$lib/utils/search_utility"; @@ -246,8 +242,15 @@ return "Reference"; } - function getNeventAddress(event: NDKEvent): string { - return neventEncode(event, standardRelays); + function getNeventUrl(event: NDKEvent): string { + if (event.kind === 0) { + return neventEncode(event, $activeInboxRelays); + } + return neventEncode(event, $activeInboxRelays); + } + + function getNaddrUrl(event: NDKEvent): string { + return naddrEncode(event, $activeInboxRelays); } function isAddressableEvent(event: NDKEvent): boolean { @@ -259,7 +262,7 @@ return null; } try { - return naddrEncode(event, standardRelays); + return naddrEncode(event, $activeInboxRelays); } catch { return null; } @@ -498,12 +501,11 @@ handleUrlChange(); }); + // Log relay configuration when page mounts onMount(() => { - userRelayPreference = localStorage.getItem("useUserRelays") === "true"; - - // Run relay diagnostics to help identify connection issues - testAllRelays().then(logRelayDiagnostics).catch(console.error); + logCurrentRelayConfiguration(); }); +
    @@ -942,8 +944,8 @@ {#if event.kind !== 0}
    {#if isAddressableEvent(event)} {@const naddrAddress = getViewPublicationNaddr(event)} diff --git a/src/routes/publication/+page.ts b/src/routes/publication/+page.ts index b870262..6a06156 100644 --- a/src/routes/publication/+page.ts +++ b/src/routes/publication/+page.ts @@ -2,7 +2,7 @@ import { error } from "@sveltejs/kit"; import type { Load } from "@sveltejs/kit"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { nip19 } from "nostr-tools"; -import { getActiveRelays } from "$lib/ndk"; +import { getActiveRelaySetAsNDKRelaySet } from "$lib/ndk"; import { getMatchingTags } from "$lib/utils/nostrUtils"; /** @@ -68,10 +68,11 @@ async function fetchEventById(ndk: any, id: string): Promise { */ async function fetchEventByDTag(ndk: any, dTag: string): Promise { try { + const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays const event = await ndk.fetchEvent( { "#d": [dTag] }, { closeOnEose: false }, - getActiveRelays(ndk), + relaySet, ); if (!event) {