diff --git a/nip66-cron/index.mjs b/nip66-cron/index.mjs index f019aa91..1add35d0 100644 --- a/nip66-cron/index.mjs +++ b/nip66-cron/index.mjs @@ -76,7 +76,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [ /** Relays to publish 30166/10166 and to REQ kind 10002 from; broad enough for Imwald + NIP-66 discovery. */ const DEFAULT_PUBLISH_RELAYS = [ 'wss://nos.lol', - 'wss://orly-relay.imwald.eu', 'wss://relay.damus.io', 'wss://relay.nostr.watch', 'wss://relay.primal.net', diff --git a/src/constants.ts b/src/constants.ts index a7647fe9..44399553 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -493,7 +493,6 @@ export const GIF_RELAY_URLS = [ export const SEARCHABLE_RELAY_URLS = [ 'wss://search.nos.today', 'wss://nostr.wine', - 'wss://orly-relay.imwald.eu', 'wss://relay.noswhere.com', 'wss://nostr-pub.wellorder.net', ] diff --git a/src/hooks/useProfileReportsEvents.tsx b/src/hooks/useProfileReportsEvents.tsx index 03cb99c9..b5bfaace 100644 --- a/src/hooks/useProfileReportsEvents.tsx +++ b/src/hooks/useProfileReportsEvents.tsx @@ -89,10 +89,20 @@ export function useProfileReportsEvents({ const receivedCached = memoryByKey.get(receivedCacheKey) const madeCached = memoryByKey.get(madeCacheKey) - - const [received, setReceived] = useState(receivedCached?.events ?? []) - const [made, setMade] = useState(madeCached?.events ?? []) - const [isLoading, setIsLoading] = useState(!receivedCached || !madeCached) + const reportsCacheHasRows = + (receivedCached?.events.length ?? 0) + (madeCached?.events.length ?? 0) > 0 + const reportsCacheFresh = + !!receivedCached && + !!madeCached && + Date.now() - receivedCached.lastUpdated < CACHE_DURATION && + Date.now() - madeCached.lastUpdated < CACHE_DURATION + const hasUsefulReportsCache = reportsCacheHasRows && reportsCacheFresh + + const [received, setReceived] = useState( + hasUsefulReportsCache ? receivedCached!.events : [] + ) + const [made, setMade] = useState(hasUsefulReportsCache ? madeCached!.events : []) + const [isLoading, setIsLoading] = useState(!hasUsefulReportsCache) const [refreshToken, setRefreshToken] = useState(0) const includeAuthorLocalRelays = useMemo(() => { @@ -118,6 +128,7 @@ export function useProfileReportsEvents({ blockedRelaysRef.current = blockedRelays const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap) useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap + const runGenRef = useRef(0) const resolveFeedUrls = useCallback( ( @@ -174,6 +185,7 @@ export function useProfileReportsEvents({ useEffect(() => { let cancelled = false + const runGen = ++runGenRef.current const loadMode = async ( mode: FetchMode, @@ -196,7 +208,11 @@ export function useProfileReportsEvents({ isEventDeletedRef.current, postFilter(pubkey, mode) ) - memoryByKey.set(cacheKey, { events: processed, lastUpdated: Date.now() }) + if (processed.length > 0) { + memoryByKey.set(cacheKey, { events: processed, lastUpdated: Date.now() }) + } else { + memoryByKey.delete(cacheKey) + } setEvents((prev) => (eventsEqualById(prev, processed) ? prev : processed)) } @@ -284,15 +300,21 @@ export function useProfileReportsEvents({ const madeMem = memoryByKey.get(madeCacheKey) const recvFresh = recvMem && Date.now() - recvMem.lastUpdated < CACHE_DURATION const madeFresh = madeMem && Date.now() - madeMem.lastUpdated < CACHE_DURATION + const cachedAny = + (recvMem?.events.length ?? 0) + (madeMem?.events.length ?? 0) > 0 if (recvFresh && recvMem) { setReceived(recvMem.events) + } else if (recvMem?.events.length === 0) { + memoryByKey.delete(receivedCacheKey) } if (madeFresh && madeMem) { setMade(madeMem.events) + } else if (madeMem?.events.length === 0) { + memoryByKey.delete(madeCacheKey) } - if (recvFresh && madeFresh && refreshToken === 0) { - setIsLoading(false) + if (recvFresh && madeFresh && refreshToken === 0 && cachedAny) { + if (runGen === runGenRef.current) setIsLoading(false) return } @@ -303,7 +325,7 @@ export function useProfileReportsEvents({ loadMode('made', madeCacheKey, setMade) ]) } finally { - if (!cancelled) setIsLoading(false) + if (!cancelled && runGen === runGenRef.current) setIsLoading(false) } } diff --git a/src/hooks/useProfileWall.tsx b/src/hooks/useProfileWall.tsx index 5f38830f..51dbbc52 100644 --- a/src/hooks/useProfileWall.tsx +++ b/src/hooks/useProfileWall.tsx @@ -92,10 +92,16 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine const cacheKey = useMemo(() => `${pubkey}-profile-wall-v1`, [pubkey]) const cached = wallCacheByKey.get(cacheKey) + const hasUsefulWallCache = + !!cached && + cached.badges.length > 0 && + Date.now() - cached.lastUpdated < CACHE_DURATION - const [badges, setBadges] = useState(cached?.badges ?? []) - const [comments, setComments] = useState(cached?.comments ?? []) - const [isLoading, setIsLoading] = useState(!cached) + const [badges, setBadges] = useState( + hasUsefulWallCache ? cached!.badges : [] + ) + const [comments, setComments] = useState(hasUsefulWallCache ? cached!.comments : []) + const [isLoading, setIsLoading] = useState(!hasUsefulWallCache) const [refreshToken, setRefreshToken] = useState(0) const relayListsKey = useMemo( @@ -108,18 +114,29 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine blockedRelaysRef.current = blockedRelays const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap) useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap + const runGenRef = useRef(0) useEffect(() => { let cancelled = false + const runGen = ++runGenRef.current const run = async () => { const mem = wallCacheByKey.get(cacheKey) - if (mem && Date.now() - mem.lastUpdated < CACHE_DURATION && refreshToken === 0) { + // Do not reuse empty cache (transient abort when secondary panel opens used to cache [] for 5m). + if ( + mem && + mem.badges.length > 0 && + Date.now() - mem.lastUpdated < CACHE_DURATION && + refreshToken === 0 + ) { setBadges(mem.badges) setComments(mem.comments) - setIsLoading(false) + if (runGen === runGenRef.current) setIsLoading(false) return } + if (mem?.badges.length === 0) { + wallCacheByKey.delete(cacheKey) + } setIsLoading(true) try { @@ -148,7 +165,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine ) // --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only --- - let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls) + let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls, { foreground: true }) if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) { const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls) if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy @@ -208,13 +225,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine if (cancelled) return setBadges(resolvedBadges) setComments(wallComments) - wallCacheByKey.set(cacheKey, { - badges: resolvedBadges, - comments: wallComments, - lastUpdated: Date.now() - }) + if (resolvedBadges.length > 0 || wallComments.length > 0) { + wallCacheByKey.set(cacheKey, { + badges: resolvedBadges, + comments: wallComments, + lastUpdated: Date.now() + }) + } else { + wallCacheByKey.delete(cacheKey) + } } finally { - if (!cancelled) setIsLoading(false) + if (!cancelled && runGen === runGenRef.current) setIsLoading(false) } } diff --git a/src/lib/nip58-profile-badges-list.ts b/src/lib/nip58-profile-badges-list.ts index a9db514f..d92d8de1 100644 --- a/src/lib/nip58-profile-badges-list.ts +++ b/src/lib/nip58-profile-badges-list.ts @@ -13,6 +13,7 @@ import { normalizeHexPubkey } from '@/lib/pubkey' import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' import { normalizeAnyRelayUrl } from '@/lib/url' import client, { replaceableEventService } from '@/services/client.service' +import indexedDb from '@/services/indexed-db.service' import type { Event } from 'nostr-tools' export function profileBadgeEntriesToTags(entries: ProfileBadgeEntry[]): string[][] { @@ -42,19 +43,31 @@ export function profileBadgeListTagsAfterRemovingEntry( export async function fetchProfileBadgesListEvent( pubkeyHex: string, - relayUrls: string[] + relayUrls: string[], + options?: { foreground?: boolean } ): Promise { const pk = normalizeHexPubkey(pubkeyHex) + const foreground = options?.foreground === true let cached: Event | undefined try { - cached = + const disk = await indexedDb.getReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST) + if (disk) cached = disk + } catch { + cached = undefined + } + try { + const fromService = (await replaceableEventService.fetchReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST)) ?? undefined + if (!cached) cached = fromService + else if (fromService && fromService.created_at >= cached.created_at) cached = fromService } catch { - cached = undefined + /* best-effort */ } const fromRelays = relayUrls.length - ? await fetchLatestReplaceableListEvent(pk, ExtendedKind.PROFILE_BADGES_LIST, relayUrls) + ? await fetchLatestReplaceableListEvent(pk, ExtendedKind.PROFILE_BADGES_LIST, relayUrls, { + foreground + }) : undefined if (!cached) return fromRelays if (!fromRelays) return cached @@ -93,7 +106,8 @@ export async function fetchLegacyProfileBadgesListEvent( { replaceableRace: true, eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, - globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS + globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS, + foreground: true } ) diff --git a/src/lib/replaceable-list-latest.ts b/src/lib/replaceable-list-latest.ts index ac638193..d3f1526e 100644 --- a/src/lib/replaceable-list-latest.ts +++ b/src/lib/replaceable-list-latest.ts @@ -41,7 +41,8 @@ function newestReplaceableEvent(candidates: Event[]): Event | undefined { export async function fetchLatestReplaceableListEvent( pubkeyHex: string, kind: number, - relayUrls: string[] + relayUrls: string[], + options?: { foreground?: boolean } ): Promise { const pk = normalizeHexPubkey(pubkeyHex) const allUrls = [...new Set(relayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))] @@ -50,7 +51,10 @@ export async function fetchLatestReplaceableListEvent( const rows = await client.fetchEvents( allUrls, { authors: [pk], kinds: networkKindsForReplaceableFetch(kind), limit: 80 }, - replaceableListFetchQueryOpts(kind) + { + ...replaceableListFetchQueryOpts(kind), + ...(options?.foreground ? { foreground: true } : {}) + } ) return newestReplaceableEvent(rows.filter((e) => e.kind === kind))