Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
5fb032a120
  1. 68
      src/hooks/useFetchThreadContextEvent.tsx
  2. 3
      src/lib/favorites-feed-relays.ts
  3. 8
      src/lib/nostr-land-relay-eligibility.test.ts
  4. 8
      src/lib/nostr-land-relay-eligibility.ts
  5. 11
      src/lib/relay-list-builder.ts
  6. 9
      src/lib/thread-context-relays.ts
  7. 3
      src/services/client-events.service.ts

68
src/hooks/useFetchThreadContextEvent.tsx

@ -10,10 +10,37 @@ import { buildThreadContextFetchRelayUrls } from '@/lib/thread-context-relays'
import client, { eventService } from '@/services/client.service' import client, { eventService } from '@/services/client.service'
import { navigationEventStore } from '@/services/navigation-event-store' import { navigationEventStore } from '@/services/navigation-event-store'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
export type ThreadContextRole = 'parent' | 'root' export type ThreadContextRole = 'parent' | 'root'
/** First usable event from parallel fetches, or undefined after all settle or `timeoutMs`. */
function raceThreadContextFetches(
tasks: Array<() => Promise<Event | undefined>>,
timeoutMs: number
): Promise<Event | undefined> {
if (tasks.length === 0) return Promise.resolve(undefined)
return new Promise((resolve) => {
const timer = window.setTimeout(() => resolve(undefined), timeoutMs)
let settled = 0
const finish = (ev: Event | undefined) => {
settled++
if (ev) {
window.clearTimeout(timer)
resolve(ev)
return
}
if (settled === tasks.length) {
window.clearTimeout(timer)
resolve(undefined)
}
}
for (const run of tasks) {
void run().then(finish).catch(() => finish(undefined))
}
})
}
export function useFetchThreadContextEvent( export function useFetchThreadContextEvent(
eventId: string | undefined, eventId: string | undefined,
contextEvent: Event | undefined, contextEvent: Event | undefined,
@ -28,10 +55,8 @@ export function useFetchThreadContextEvent(
const [event, setEvent] = useState<Event | undefined>(initialEvent) const [event, setEvent] = useState<Event | undefined>(initialEvent)
const [isFetching, setIsFetching] = useState(!initialEvent) const [isFetching, setIsFetching] = useState(!initialEvent)
const [refetchToken, setRefetchToken] = useState(0) const [refetchToken, setRefetchToken] = useState(0)
const searchableAttemptedRef = useRef(false)
const refetch = useCallback(() => { const refetch = useCallback(() => {
searchableAttemptedRef.current = false
setRefetchToken((n) => n + 1) setRefetchToken((n) => n + 1)
}, []) }, [])
@ -45,10 +70,6 @@ export function useFetchThreadContextEvent(
[blockedRelays] [blockedRelays]
) )
useEffect(() => {
searchableAttemptedRef.current = false
}, [eventId])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@ -111,35 +132,28 @@ export function useFetchThreadContextEvent(
? { relayHints: relayUrls, threadContext: true as const } ? { relayHints: relayUrls, threadContext: true as const }
: { threadContext: true as const } : { threadContext: true as const }
const fetchParentOrRoot = async () => { const fetchParentOrRoot = () => {
if (skipShortcuts) { if (skipShortcuts) {
return eventService.fetchEventForceRetry(eventId, threadOpts) return eventService.fetchEventForceRetry(eventId, threadOpts)
} }
return eventService.fetchEvent(eventId, threadOpts) return eventService.fetchEvent(eventId, threadOpts)
} }
let fetchedEvent = await Promise.race([ const aggrAwareSearch = sanitizeRelayUrlsForFetch(getAggrAwareSearchRelayUrls())
fetchParentOrRoot(), const tasks: Array<() => Promise<Event | undefined>> = [fetchParentOrRoot]
new Promise<undefined>((resolve) => { if (aggrAwareSearch.length > 0) {
window.setTimeout(() => resolve(undefined), THREAD_CONTEXT_EVENT_FETCH_GLOBAL_TIMEOUT_MS) tasks.push(async () => {
const ev = await client.fetchEventWithExternalRelays(eventId, aggrAwareSearch)
if (ev) client.addEventToCache(ev)
return ev
}) })
])
const aggrAwareSearch = getAggrAwareSearchRelayUrls()
if (!fetchedEvent && !searchableAttemptedRef.current && aggrAwareSearch.length > 0) {
searchableAttemptedRef.current = true
const searchable = sanitizeRelayUrlsForFetch(aggrAwareSearch)
fetchedEvent = await Promise.race([
client.fetchEventWithExternalRelays(eventId, searchable),
new Promise<undefined>((resolve) => {
window.setTimeout(() => resolve(undefined), THREAD_CONTEXT_EVENT_FETCH_GLOBAL_TIMEOUT_MS)
})
])
if (fetchedEvent) {
client.addEventToCache(fetchedEvent)
}
} }
const fetchedEvent = await raceThreadContextFetches(
tasks,
THREAD_CONTEXT_EVENT_FETCH_GLOBAL_TIMEOUT_MS
)
if (cancelled) return if (cancelled) return
if (fetchedEvent && !isEventDeleted(fetchedEvent)) { if (fetchedEvent && !isEventDeleted(fetchedEvent)) {
setEvent(fetchedEvent) setEvent(fetchedEvent)

3
src/lib/favorites-feed-relays.ts

@ -19,7 +19,6 @@ import {
import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy' import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy'
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize' import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
import { relaySessionStrikes } from '@/lib/relay-strikes' import { relaySessionStrikes } from '@/lib/relay-strikes'
import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eligibility'
import { profileFetchRelayUrlsWithoutFastReadLayer } from '@/lib/viewer-relay-defaults' import { profileFetchRelayUrlsWithoutFastReadLayer } from '@/lib/viewer-relay-defaults'
function isBlockedRelay(url: string, blockedRelays: string[]): boolean { function isBlockedRelay(url: string, blockedRelays: string[]): boolean {
@ -40,7 +39,7 @@ export function userReadRelaysWithHttp(
): string[] { ): string[] {
const http = relayList?.httpRead ?? [] const http = relayList?.httpRead ?? []
const read = relayList?.read ?? [] const read = relayList?.read ?? []
return prependAggrNostrLandIfViewerEligible(dedupeNormalizeRelayUrlsOrdered([...http, ...read])) return dedupeNormalizeRelayUrlsOrdered([...http, ...read])
} }
export function getFavoritesFeedRelayUrls( export function getFavoritesFeedRelayUrls(

8
src/lib/nostr-land-relay-eligibility.test.ts

@ -4,6 +4,7 @@ import {
filterAggrNostrLandUnlessViewerEligible, filterAggrNostrLandUnlessViewerEligible,
getAggrAwareSearchRelayUrls, getAggrAwareSearchRelayUrls,
getViewerNostrLandAggrSearchRelayUrls, getViewerNostrLandAggrSearchRelayUrls,
prependAggrForEventLookupRelayUrls,
prependAggrNostrLandIfViewerEligible, prependAggrNostrLandIfViewerEligible,
relayUrlsIncludeCanonicalNostrLandRelay, relayUrlsIncludeCanonicalNostrLandRelay,
syncViewerRelayStackNostrLandAggrEligible syncViewerRelayStackNostrLandAggrEligible
@ -32,6 +33,13 @@ describe('nostr.land aggr eligibility', () => {
]) ])
}) })
it('prependAggrForEventLookupRelayUrls matches prependAggrNostrLandIfViewerEligible', () => {
syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
expect(prependAggrForEventLookupRelayUrls(['wss://inbox.example/'])[0]).toMatch(
/^wss:\/\/aggr\.nostr\.land\/?$/
)
})
it('getAggrAwareSearchRelayUrls prepends aggr when eligible', () => { it('getAggrAwareSearchRelayUrls prepends aggr when eligible', () => {
syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/']) syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
const urls = getAggrAwareSearchRelayUrls() const urls = getAggrAwareSearchRelayUrls()

8
src/lib/nostr-land-relay-eligibility.ts

@ -108,6 +108,14 @@ export function urlsForViewerNostrLandAggrEligibilitySync(options: {
] ]
} }
/**
* Prepend aggr for event-by-id lookups (threads, embeds, parent previews, comprehensive fetch).
* Home OP timelines must not use this use {@link buildAllFavoritesFeedRelayUrls} / `nostrLandAggr: 'never'`.
*/
export function prependAggrForEventLookupRelayUrls(relayUrls: readonly string[]): string[] {
return prependAggrNostrLandIfViewerEligible(relayUrls)
}
/** Deduped prepend of aggr when the viewer opted into nostr.land relays (see sync…). */ /** Deduped prepend of aggr when the viewer opted into nostr.land relays (see sync…). */
export function prependAggrNostrLandIfViewerEligible(relayUrls: readonly string[]): string[] { export function prependAggrNostrLandIfViewerEligible(relayUrls: readonly string[]): string[] {
if (!viewerStackMentionsNostrLand) return [...relayUrls] if (!viewerStackMentionsNostrLand) return [...relayUrls]

11
src/lib/relay-list-builder.ts

@ -13,6 +13,7 @@ import { FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS, SEARCHABLE_RELAY_URLS } from
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { mergeRelayUrlLayers, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' import { mergeRelayUrlLayers, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { isRelayBlockedByUser } from '@/lib/relay-blocked' import { isRelayBlockedByUser } from '@/lib/relay-blocked'
import { prependAggrForEventLookupRelayUrls } from '@/lib/nostr-land-relay-eligibility'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { import {
canonicalRelaySessionKey, canonicalRelaySessionKey,
@ -385,7 +386,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
}), }),
personalKeys personalKeys
) )
if (httpRelayUrls.length === 0) return ws if (httpRelayUrls.length === 0) return prependAggrForEventLookupRelayUrls(ws)
const seen = new Set(ws.map(relayKey)) const seen = new Set(ws.map(relayKey))
const out = [...ws] const out = [...ws]
for (const u of httpRelayUrls) { for (const u of httpRelayUrls) {
@ -394,7 +395,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
seen.add(k) seen.add(k)
out.push(u) out.push(u)
} }
return out return prependAggrForEventLookupRelayUrls(out)
} }
/** /**
@ -649,7 +650,7 @@ export async function buildReplyReadRelayList(
includeProfileFetchRelays: false, includeProfileFetchRelays: false,
blockedRelays blockedRelays
}) })
return scoped return prependAggrForEventLookupRelayUrls(scoped)
} }
let useGlobal = true let useGlobal = true
@ -682,5 +683,7 @@ export async function buildReplyReadRelayList(
includeProfileFetchRelays: useGlobal, includeProfileFetchRelays: useGlobal,
blockedRelays blockedRelays
}) })
return mergeRelayUrlLayers([scoped, defaultFavoriteRelaysForViewer(useGlobal)], blockedRelays) return prependAggrForEventLookupRelayUrls(
mergeRelayUrlLayers([scoped, defaultFavoriteRelaysForViewer(useGlobal)], blockedRelays)
)
} }

9
src/lib/thread-context-relays.ts

@ -1,3 +1,4 @@
import { prependAggrForEventLookupRelayUrls } from '@/lib/nostr-land-relay-eligibility'
import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal' import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { buildReplyReadRelayList, relayHintsFromEventTags } from '@/lib/relay-list-builder' import { buildReplyReadRelayList, relayHintsFromEventTags } from '@/lib/relay-list-builder'
@ -41,5 +42,11 @@ export async function buildThreadContextFetchRelayUrls(
...new Set([...tagHints, ...relayHintsFromEventTags(contextEvent)]) ...new Set([...tagHints, ...relayHintsFromEventTags(contextEvent)])
]) ])
const opAuthorPubkey = pubkeyFromThreadETag(targetTag) const opAuthorPubkey = pubkeyFromThreadETag(targetTag)
return buildReplyReadRelayList(opAuthorPubkey, viewerPubkey, blockedRelays, threadRelayHints) const relays = await buildReplyReadRelayList(
opAuthorPubkey,
viewerPubkey,
blockedRelays,
threadRelayHints
)
return prependAggrForEventLookupRelayUrls(relays)
} }

3
src/services/client-events.service.ts

@ -11,6 +11,7 @@ import {
SINGLE_EVENT_BY_ID_QUERY_EOSE_TIMEOUT_MS, SINGLE_EVENT_BY_ID_QUERY_EOSE_TIMEOUT_MS,
SINGLE_EVENT_BY_ID_QUERY_GLOBAL_TIMEOUT_MS SINGLE_EVENT_BY_ID_QUERY_GLOBAL_TIMEOUT_MS
} from '@/constants' } from '@/constants'
import { prependAggrForEventLookupRelayUrls } from '@/lib/nostr-land-relay-eligibility'
import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal' import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -1195,7 +1196,7 @@ export class EventService {
[...new Set(urls.map((u) => normalizeUrl(u)).filter((u): u is string => Boolean(u)))] [...new Set(urls.map((u) => normalizeUrl(u)).filter((u): u is string => Boolean(u)))]
) )
if (extraRelayHints?.length) { if (extraRelayHints?.length) {
relays = normalizeRelayList(extraRelayHints) relays = normalizeRelayList(prependAggrForEventLookupRelayUrls(extraRelayHints))
} }
if (/^[0-9a-f]{64}$/i.test(id)) { if (/^[0-9a-f]{64}$/i.test(id)) {

Loading…
Cancel
Save