Fix relay connectivity: remove aggressive liveness probe, add fetch timeouts

The liveness probe in ensureConnected was causing a death spiral —
it force-disconnected working relays when a 3s probe timed out,
then resetNDK killed new connections before they could establish.

Now ensureConnected trusts relay.connected and only reconnects when
zero relays are connected. All fetchEvents calls have timeouts
(5-10s) so nothing hangs. resetNDK kept as background-only recovery
in the connection monitor after 30s of continuous failure.

Also adds feed diagnostics (feedDiagnostics.ts) for tracking fetch
timing, event freshness, and relay states via console helpers.
This commit is contained in:
Jure
2026-03-22 10:42:27 +01:00
parent 9fbc71ac8c
commit 2e03c6ce11
7 changed files with 410 additions and 100 deletions

View File

@@ -3,7 +3,8 @@ import { useFeedStore } from "../../stores/feed";
import { useUserStore } from "../../stores/user";
import { useMuteStore } from "../../stores/mute";
import { useUIStore } from "../../stores/ui";
import { fetchFollowFeed, getNDK } from "../../lib/nostr";
import { fetchFollowFeed, getNDK, ensureConnected } from "../../lib/nostr";
import { diagWrapFetch, logDiag } from "../../lib/feedDiagnostics";
import { detectScript, getEventLanguageTag, FILTER_SCRIPTS } from "../../lib/language";
import { NoteCard } from "./NoteCard";
import { ArticleCard } from "../article/ArticleCard";
@@ -38,7 +39,8 @@ export function Feed() {
const loadFollowFeed = async () => {
setFollowLoading(true);
try {
const events = await fetchFollowFeed(follows);
await ensureConnected();
const events = await diagWrapFetch("follow_fetch", () => fetchFollowFeed(follows));
setFollowNotes(events);
} finally {
setFollowLoading(false);
@@ -140,7 +142,10 @@ export function Feed() {
</span>
)}
<button
onClick={isTrending ? () => loadTrendingFeed(true) : isFollowing ? loadFollowFeed : loadFeed}
onClick={() => {
logDiag({ ts: new Date().toISOString(), action: "refresh_click", details: `tab=${tab}` });
(isTrending ? () => loadTrendingFeed(true) : isFollowing ? loadFollowFeed : loadFeed)();
}}
disabled={isLoading}
className="text-text-muted hover:text-text text-[11px] px-2 py-1 border border-border hover:border-text-dim transition-colors disabled:opacity-40"
>

View File

@@ -3,7 +3,7 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useUIStore } from "../../stores/ui";
import { useUserStore } from "../../stores/user";
import { useMuteStore } from "../../stores/mute";
import { fetchNoteById, fetchThreadEvents, fetchAncestors, publishReply, getNDK } from "../../lib/nostr";
import { fetchNoteById, fetchThreadEvents, fetchAncestors, publishReply, getNDK, ensureConnected } from "../../lib/nostr";
import { buildThreadTree, getRootEventId } from "../../lib/threadTree";
import type { ThreadNode } from "../../lib/threadTree";
import { AncestorChain } from "./AncestorChain";
@@ -22,6 +22,7 @@ export function ThreadView() {
const [ancestors, setAncestors] = useState<NDKEvent[]>([]);
const [tree, setTree] = useState<ThreadNode | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [showRootReply, setShowRootReply] = useState(false);
const [replyText, setReplyText] = useState("");
const [replying, setReplying] = useState(false);
@@ -30,17 +31,26 @@ export function ThreadView() {
const replyRef = useRef<HTMLTextAreaElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
let cancelled = false;
async function loadThread() {
setLoading(true);
setLoadError(null);
setTree(null);
setAncestors([]);
setRootEvent(null);
setShowRootReply(false);
try {
// Ensure we have relay connectivity before fetching
const connected = await ensureConnected();
if (!connected && !cancelled) {
setLoadError("No relay connections available. Check your network.");
}
const rootId = getRootEventId(focusedEvent);
let root: NDKEvent;
@@ -54,6 +64,7 @@ export function ThreadView() {
if (!cancelled) setAncestors(anc.filter((a) => a.id !== root.id));
} else {
root = focusedEvent;
if (!cancelled) setLoadError("Could not fetch the root note — relay may be slow.");
}
}
@@ -66,8 +77,14 @@ export function ThreadView() {
const allEvents = [root, ...events.filter((e) => e.id !== root.id)];
const built = buildThreadTree(root.id, allEvents);
setTree(built);
// Warn if we got zero replies (possible timeout)
if (events.length === 0 && !loadError) {
console.warn("[Wrystr] Thread fetch returned 0 replies — possible timeout or empty thread");
}
} catch (err) {
console.error("Failed to load thread:", err);
if (!cancelled) setLoadError(`Failed to load: ${err}`);
} finally {
if (!cancelled) setLoading(false);
}
@@ -75,7 +92,7 @@ export function ThreadView() {
loadThread();
return () => { cancelled = true; };
}, [focusedEvent.id]);
}, [focusedEvent.id, retryCount]);
// Scroll to focused note after tree renders (if not root)
useEffect(() => {
@@ -133,6 +150,19 @@ export function ThreadView() {
</header>
<div ref={scrollRef} className="flex-1 overflow-y-auto">
{/* Error banner */}
{loadError && !loading && (
<div className="px-4 py-2 bg-danger/10 border-b border-danger/20 flex items-center justify-between">
<span className="text-danger text-[11px]">{loadError}</span>
<button
onClick={() => setRetryCount((c) => c + 1)}
className="text-[11px] text-accent hover:text-accent-hover transition-colors px-2 py-0.5 border border-accent/30 hover:border-accent"
>
retry
</button>
</div>
)}
{/* Loading shimmer */}
{loading && (
<div className="px-4 py-6 space-y-4">
@@ -234,8 +264,14 @@ export function ThreadView() {
)}
{!loading && !tree && (
<div className="px-4 py-6 text-text-dim text-[12px] text-center">
Could not load thread.
<div className="px-4 py-6 text-center space-y-2">
<p className="text-text-dim text-[12px]">Could not load thread.</p>
<button
onClick={() => setRetryCount((c) => c + 1)}
className="text-[11px] text-accent hover:text-accent-hover transition-colors px-3 py-1 border border-accent/30 hover:border-accent"
>
retry
</button>
</div>
)}
</div>