Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
efedaf9296
  1. 23
      src/components/RssFeedList/index.tsx
  2. 22
      src/hooks/useFetchCalendarRsvps.tsx
  3. 52
      src/hooks/useFetchProfile.tsx
  4. 113
      src/hooks/useProfileTimeline.tsx
  5. 2
      src/i18n/locales/de.ts
  6. 2
      src/i18n/locales/en.ts
  7. 77
      src/lib/rss-web-feed.ts
  8. 50
      src/pages/primary/CalendarPrimaryPage.tsx
  9. 18
      src/pages/primary/SpellsPage/index.tsx
  10. 10
      src/pages/secondary/ProfileInteractionDiagramPage/index.tsx
  11. 39
      src/services/mention-event-search.service.ts

23
src/components/RssFeedList/index.tsx

@ -11,6 +11,7 @@ import { RssUnifiedScopeSection } from './RssUnifiedScopeSection' @@ -11,6 +11,7 @@ import { RssUnifiedScopeSection } from './RssUnifiedScopeSection'
import { canonicalizeRssArticleUrl, isClawstrDotComHttpUrl } from '@/lib/rss-article'
import {
addManualRssWebUrl,
discoverRssWebArticleUrlsFromLocalCaches,
fetchDiscoveredWebUrlsFromRelays,
loadManualRssWebUrls,
loadPromotedRssThreadUrls,
@ -21,6 +22,7 @@ import { @@ -21,6 +22,7 @@ import {
isHttpArticleUrl,
isRssWebUnifiedClutterUrl,
mergeDiscoveredRssWebUrls,
mergeManualRssWebUrlEntries,
rssWebRowHasRealFeedItems,
saveRssWebFeedScopePreference,
saveRssWebHideUnifiedClutterPreference,
@ -669,14 +671,22 @@ export default function RssFeedList() { @@ -669,14 +671,22 @@ export default function RssFeedList() {
let cancelled = false
void (async () => {
try {
const discovered = await fetchDiscoveredWebUrlsFromRelays({
const local = await discoverRssWebArticleUrlsFromLocalCaches({
excludeClutterUrls: hideUnifiedClutter
})
let discovered: ManualRssWebUrlEntry[] = []
try {
discovered = await fetchDiscoveredWebUrlsFromRelays({
accountPubkey: pubkey,
favoriteRelays: favoriteRelays ?? [],
blockedRelays: blockedRelays ?? [],
excludeClutterUrls: hideUnifiedClutter
})
} catch {
/* relay discovery is best-effort */
}
if (cancelled) return
setRelayDiscoveredUrls(discovered)
setRelayDiscoveredUrls(mergeManualRssWebUrlEntries(local, discovered))
const didMerge = await mergeDiscoveredRssWebUrls(discovered)
if (didMerge && !cancelled) refreshManualWebUrls()
} catch {
@ -1114,6 +1124,15 @@ export default function RssFeedList() { @@ -1114,6 +1124,15 @@ export default function RssFeedList() {
? t('No URL-only items yet')
: t('No RSS feed items available')}
</p>
{feedScope === 'urls' &&
!searchQuery.trim() &&
selectedFeeds.includes('all') &&
timeFilter === 'all' &&
rssScopeRows.length > 0 ? (
<p className="mt-3 max-w-md text-center text-xs text-muted-foreground">
{t('RSS+Web url tab empty hint')}
</p>
) : null}
</div>
) : feedScope === 'urls' ? (
<>

22
src/hooks/useFetchCalendarRsvps.tsx

@ -73,18 +73,17 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -73,18 +73,17 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const userWrite = userWriteRelaysForQuery(relayList)
void (async () => {
// Read order: IndexedDB first (offline + last session), then in-memory session, then relays.
let fromIdb: Event[] = []
try {
fromIdb = await indexedDb.getCalendarRsvpEventsByParentCoordinate(coordinate)
} catch {
fromIdb = []
}
if (cancelled) return
const fromSession = client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent)
const mergedLocal = mergeRsvpList([...fromIdb, ...fromSession])
setRsvps(mergedLocal)
setRsvps(mergeRsvpList(fromSession))
const idbP = indexedDb
.getCalendarRsvpEventsByParentCoordinate(coordinate)
.catch((): Event[] => [])
void idbP.then((rows) => {
if (cancelled) return
setRsvps(mergeRsvpList([...rows, ...fromSession]))
})
const baseUrls = new Set<string>([
...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url),
@ -139,6 +138,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -139,6 +138,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
)
if (cancelled) return
const fromRelay = events ?? []
const fromIdb = await idbP
await Promise.allSettled(
fromRelay.map((ev) => indexedDb.putCalendarRsvpEventRow(ev).catch(() => undefined))
)

52
src/hooks/useFetchProfile.tsx

@ -12,31 +12,42 @@ import { kinds } from 'nostr-tools' @@ -12,31 +12,42 @@ import { kinds } from 'nostr-tools'
import { useEffect, useState, useRef, useCallback } from 'react'
import logger from '@/lib/logger'
/**
* Session LRU + IndexedDB kind 0 without ReplaceableEventService / batched DataLoader.
* Used when the hook's fetch race times out or the batch path is slow while disk/session already has metadata.
*/
async function tryHydrateProfileFromLocalCaches(
pubkey: string,
skipCache: boolean
): Promise<TProfile | null> {
function tryHydrateProfileFromSessionOnly(pubkey: string, skipCache: boolean): TProfile | null {
if (skipCache) return null
const pk = pubkey.toLowerCase()
const sessionEv = eventService.getSessionMetadataForPubkey(pk)
if (sessionEv) {
return getProfileFromEvent(sessionEv)
}
return null
}
try {
const idbEv = await indexedDb.getReplaceableEvent(pk, kinds.Metadata)
/** Single-flight IndexedDB kind-0 read (never blocks callers until they await the promise). */
function profileFromIdbPromise(pubkey: string, skipCache: boolean): Promise<TProfile | null> {
if (skipCache) return Promise.resolve(null)
const pk = pubkey.toLowerCase()
return indexedDb
.getReplaceableEvent(pk, kinds.Metadata)
.then((idbEv) => {
if (idbEv && !shouldDropEventOnIngest(idbEv)) {
return getProfileFromEvent(idbEv)
}
} catch {
/* IDB not ready */
}
return null
})
.catch(() => null)
}
/**
* Session LRU + IndexedDB kind 0 without ReplaceableEventService / batched DataLoader.
* Used when the hook's fetch race times out or the batch path is slow while disk/session already has metadata.
*/
async function tryHydrateProfileFromLocalCaches(
pubkey: string,
skipCache: boolean
): Promise<TProfile | null> {
const fromSession = tryHydrateProfileFromSessionOnly(pubkey, skipCache)
if (fromSession) return fromSession
return profileFromIdbPromise(pubkey, skipCache)
}
// CRITICAL: Global deduplication - shared across ALL hook instances
@ -199,11 +210,12 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -199,11 +210,12 @@ export function useFetchProfile(id?: string, skipCache = false) {
// Create a new fetch promise with timeout protection
const fetchPromise = (async (): Promise<TProfile | null> => {
let idbEarlyP: Promise<TProfile | null> | null = null
try {
globalFetchingPubkeys.add(pubkey)
const startTime = Date.now()
const quick = await tryHydrateProfileFromLocalCaches(pubkey, skipCache)
const quick = tryHydrateProfileFromSessionOnly(pubkey, skipCache)
if (quick) {
logger.debug('[useFetchProfile] Profile from session/IndexedDB (fast path)', {
pubkey: pubkey.substring(0, 8),
@ -212,6 +224,9 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -212,6 +224,9 @@ export function useFetchProfile(id?: string, skipCache = false) {
return quick
}
/** Disk read runs in parallel with `fetchProfileEvent` — never block network on IDB. */
idbEarlyP = profileFromIdbPromise(pubkey, skipCache)
// CRITICAL: Add timeout to prevent infinite hangs (must exceed batched metadata query globalTimeout)
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
@ -263,7 +278,8 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -263,7 +278,8 @@ export function useFetchProfile(id?: string, skipCache = false) {
fetchTime: `${fetchTime}ms`
})
}
const afterMiss = await tryHydrateProfileFromLocalCaches(pubkey, skipCache)
const afterMiss =
(idbEarlyP != null ? await idbEarlyP : null) ?? tryHydrateProfileFromSessionOnly(pubkey, skipCache)
if (afterMiss) {
logger.debug('[useFetchProfile] Profile from session/IndexedDB after network miss', {
pubkey: pubkey.substring(0, 8),
@ -281,7 +297,9 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -281,7 +297,9 @@ export function useFetchProfile(id?: string, skipCache = false) {
})
// Set cooldown period after timeout to prevent cascade of duplicate fetches
globalFetchCooldowns.set(pubkey, Date.now() + 10000) // 10 second cooldown
const fallback = await tryHydrateProfileFromLocalCaches(pubkey, skipCache)
const fallback =
tryHydrateProfileFromSessionOnly(pubkey, skipCache) ??
(idbEarlyP != null ? await idbEarlyP : null)
if (fallback) {
logger.debug('[useFetchProfile] Profile from session/IndexedDB after fetch timeout', {
pubkey: pubkey.substring(0, 8),

113
src/hooks/useProfileTimeline.tsx

@ -243,103 +243,72 @@ export function useProfileTimeline({ @@ -243,103 +243,72 @@ export function useProfileTimeline({
const socialKinds = kinds.some(isSocialKindBlockedKind)
const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] }
const idbDocKinds = kinds.filter((k) => isDocumentRelayKind(k))
/**
* Author NIP-65 read/write relays must feed the **first** REQ for every profile tab. Favorites-only
* misses most peoples kind-1 notes; we previously only prefetched relays for document tabs.
*/
let prefetchedAuthorRelays: typeof emptyAuthor = emptyAuthor
if (idbDocKinds.length > 0) {
let pkNorm: string | null = null
try {
pkNorm = normalizeHexPubkey(pubkey)
} catch {
pkNorm = null
}
let hadSessionHits = false
if (pkNorm) {
const pkForDisk = pkNorm
try {
const pkNorm = normalizeHexPubkey(pubkey)
const fromSession = eventService.listSessionEventsAuthoredBy(pkNorm, {
kinds: idbDocKinds,
const sessionKindList = idbDocKinds.length > 0 ? idbDocKinds : kinds
const fromSession = eventService.listSessionEventsAuthoredBy(pkForDisk, {
kinds: sessionKindList,
limit
})
hadSessionHits = fromSession.length > 0
if (!cancelled) {
for (const e of fromSession) {
pool.set(e.id, e as Event)
}
if (fromSession.length) flushPool()
}
const [authorRl, fromPubStore, fromArchive] = await Promise.all([
client.fetchRelayList(pubkey).catch(() => ({
read: [] as string[],
write: [] as string[],
httpRead: [] as string[],
httpWrite: [] as string[]
})),
indexedDb.getCachedPublicationStoreEventsForProfileAuthor(pkNorm, idbDocKinds, limit),
indexedDb.scanEventArchiveByAuthorPubkey(pkNorm, {
kinds: idbDocKinds,
maxRowsScanned: 18_000,
maxMatches: limit
})
])
if (!cancelled) {
prefetchedAuthorRelays = authorRl
for (const e of fromPubStore) {
pool.set(e.id, e)
}
for (const e of fromArchive) {
pool.set(e.id, e)
}
const hadDisk = fromPubStore.length > 0 || fromArchive.length > 0
if (hadDisk) flushPool()
else if (!isCacheFresh && !mem?.events?.length && fromSession.length === 0) {
setIsLoading(true)
}
}
} catch {
if (!cancelled) {
prefetchedAuthorRelays = await client.fetchRelayList(pubkey).catch(() => emptyAuthor)
}
if (!cancelled && !isCacheFresh && !mem?.events?.length) {
setIsLoading(true)
}
/* ignore malformed pubkeys */
}
} else {
void (async () => {
try {
const pkNorm = normalizeHexPubkey(pubkey)
const fromSession = eventService.listSessionEventsAuthoredBy(pkNorm, { kinds, limit })
if (!cancelled) {
for (const e of fromSession) {
pool.set(e.id, e as Event)
}
if (fromSession.length) flushPool()
}
const [authorRl, fromArchiveSocial] = await Promise.all([
client.fetchRelayList(pubkey).catch(() => emptyAuthor),
indexedDb.scanEventArchiveByAuthorPubkey(pkNorm, {
kinds,
maxRowsScanned: 16_000,
const idbKindsForScan = idbDocKinds.length > 0 ? idbDocKinds : kinds
const maxScan = idbDocKinds.length > 0 ? 18_000 : 16_000
const pubStorePromise =
idbDocKinds.length > 0
? indexedDb.getCachedPublicationStoreEventsForProfileAuthor(pkForDisk, idbDocKinds, limit)
: Promise.resolve([] as Event[])
const [fromPubStore, fromArchive] = await Promise.all([
pubStorePromise,
indexedDb.scanEventArchiveByAuthorPubkey(pkForDisk, {
kinds: idbKindsForScan,
maxRowsScanned: maxScan,
maxMatches: limit
})
])
if (!cancelled) {
prefetchedAuthorRelays = authorRl
for (const e of fromArchiveSocial) {
pool.set(e.id, e)
}
if (fromArchiveSocial.length) flushPool()
else if (!isCacheFresh && !mem?.events?.length && fromSession.length === 0) {
if (cancelled) return
for (const e of fromPubStore) pool.set(e.id, e)
for (const e of fromArchive) pool.set(e.id, e)
const hadDisk = fromPubStore.length + fromArchive.length > 0
if (hadDisk) flushPool()
else if (!isCacheFresh && !mem?.events?.length && !hadSessionHits) {
setIsLoading(true)
}
}
} catch {
if (!cancelled) {
prefetchedAuthorRelays = await client.fetchRelayList(pubkey).catch(() => emptyAuthor)
/* best-effort */
}
if (!cancelled && !isCacheFresh && !mem?.events?.length) {
})()
} else if (!isCacheFresh && !mem?.events?.length) {
setIsLoading(true)
}
}
}
const authorRelayPromise = client.fetchRelayList(pubkey).catch(() => emptyAuthor)
const provisionalFeedUrls = buildProfilePageReadRelayUrls(
favoriteRelays,
blockedRelays,
prefetchedAuthorRelays,
emptyAuthor,
socialKinds,
includeAuthorLocalRelays,
kinds
@ -429,7 +398,7 @@ export function useProfileTimeline({ @@ -429,7 +398,7 @@ export function useProfileTimeline({
})()
void (async () => {
const authorRl = prefetchedAuthorRelays
const authorRl = await authorRelayPromise
if (cancelled) return
const fullFeedUrls = buildProfilePageReadRelayUrls(
favoriteRelays,

2
src/i18n/locales/de.ts

@ -1675,6 +1675,8 @@ export default { @@ -1675,6 +1675,8 @@ export default {
URLs: "URLs",
RSS: "RSS",
"No URL-only items yet": "Noch keine reinen Artikel-URLs",
"RSS+Web url tab empty hint":
"Artikel aus deinen RSS-Feeds findest du unter dem Tab RSS. Hier erscheinen Artikel-URLs aus Nostr (Reaktionen, Kommentare, Lesezeichen auf Webseiten), wenn sie nicht schon vollständig als Feed-Einträge abgedeckt sind, sowie manuell hinzugefügte Links.",
"Respond to this RSS entry": "Auf diesen RSS-Eintrag reagieren",
"RSS read-only thread hint": "Nostr-Antworten, Zaps und Markierungen sind hier ausgeblendet. Damit fügst du den Artikel der URL-Liste hinzu und reagierst dort.",
"RSS feed item label": "RSS",

2
src/i18n/locales/en.ts

@ -1695,6 +1695,8 @@ export default { @@ -1695,6 +1695,8 @@ export default {
URLs: "URLs",
RSS: "RSS",
"No URL-only items yet": "No URL-only items yet",
"RSS+Web url tab empty hint":
"Links from your RSS subscriptions appear under the RSS tab. This tab lists article URLs from Nostr (reactions, comments, bookmarks on web pages) when they are not only covered by feed items, and links you add manually.",
"Respond to this RSS entry": "Respond to this RSS entry",
"RSS read-only thread hint": "Nostr replies, zaps, and highlights are hidden here. Use this to add the article to your URL feed and respond there.",
"RSS feed item label": "RSS",

77
src/lib/rss-web-feed.ts

@ -14,7 +14,7 @@ import { @@ -14,7 +14,7 @@ import {
} from '@/lib/rss-article'
import logger from '@/lib/logger'
import { isImage, isLocalNetworkUrl, isMedia, isVideo, normalizeUrl } from '@/lib/url'
import { queryService } from '@/services/client.service'
import { eventService, queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import type { RssFeedItem } from '@/services/rss-feed.service'
import { isWebOnlyFauxRssItem } from '@/services/rss-feed.service'
@ -479,6 +479,72 @@ function extractArticleUrlFromWebActivityEvent(evt: Event): string | undefined { @@ -479,6 +479,72 @@ function extractArticleUrlFromWebActivityEvent(evt: Event): string | undefined {
return undefined
}
function touchRssWebDiscoveryUrlFromEvent(
evt: Event,
excludeClutter: boolean,
latestByUrl: Map<string, number>
): void {
const url = extractArticleUrlFromWebActivityEvent(evt)
if (!url) return
if (excludeClutter && isRssWebUnifiedClutterUrl(url)) return
const key = canonicalizeRssArticleUrl(url)
const prev = latestByUrl.get(key) ?? 0
if (evt.created_at > prev) latestByUrl.set(key, evt.created_at)
}
/** Merge manual / discovered URL lists; per URL keep the newest `addedAt`. */
export function mergeManualRssWebUrlEntries(...parts: ManualRssWebUrlEntry[]): ManualRssWebUrlEntry[] {
const byUrl = new Map<string, number>()
for (const list of parts) {
for (const e of list) {
const prev = byUrl.get(e.url) ?? 0
if (e.addedAt > prev) byUrl.set(e.url, e.addedAt)
}
}
return [...byUrl.entries()].map(([url, addedAt]) => ({ url, addedAt }))
}
/**
* Article URLs from session LRU + event archive (same kinds as relay discovery), so the URL tab is not
* empty when relays return nothing but the client already saw reactions / bookmarks / etc.
*/
export async function discoverRssWebArticleUrlsFromLocalCaches(options?: {
excludeClutterUrls?: boolean
}): Promise<ManualRssWebUrlEntry[]> {
const excludeClutter = options?.excludeClutterUrls !== false
const sinceSec = Math.floor(Date.now() / 1000) - RSS_WEB_RELAY_DISCOVERY_SINCE_SEC
const latestByUrl = new Map<string, number>()
const sessionEv = eventService.listSessionEventsByKinds(RSS_WEB_RELAY_DISCOVERY_KINDS, {
since: sinceSec,
limit: 5000
})
for (const evt of sessionEv) {
touchRssWebDiscoveryUrlFromEvent(evt, excludeClutter, latestByUrl)
}
try {
const archived = await indexedDb.scanEventArchiveByKinds({
kinds: RSS_WEB_RELAY_DISCOVERY_KINDS,
since: sinceSec,
maxRowsScanned: 24_000,
maxMatches: 4000
})
for (const evt of archived) {
touchRssWebDiscoveryUrlFromEvent(evt, excludeClutter, latestByUrl)
}
} catch {
/* IDB unavailable */
}
const entries = [...latestByUrl.entries()].map(([url, addedAt]) => ({ url, addedAt }))
logger.info('[RssWebFeed] Local URL discovery finished', {
uniqueUrls: entries.length,
sessionHits: sessionEv.length
})
return entries
}
/**
* One REQ per kind, no `authors` filter: latest events from aggregated relays, grouped by canonical URL.
*/
@ -503,14 +569,7 @@ export async function fetchDiscoveredWebUrlsFromRelays(options: { @@ -503,14 +569,7 @@ export async function fetchDiscoveredWebUrlsFromRelays(options: {
})
const latestByUrl = new Map<string, number>()
const onEvent = (evt: Event) => {
const url = extractArticleUrlFromWebActivityEvent(evt)
if (!url) return
if (excludeClutter && isRssWebUnifiedClutterUrl(url)) return
const key = canonicalizeRssArticleUrl(url)
const prev = latestByUrl.get(key) ?? 0
if (evt.created_at > prev) latestByUrl.set(key, evt.created_at)
}
const onEvent = (evt: Event) => touchRssWebDiscoveryUrlFromEvent(evt, excludeClutter, latestByUrl)
await Promise.all(
RSS_WEB_RELAY_DISCOVERY_KINDS.map(async (kind) => {

50
src/pages/primary/CalendarPrimaryPage.tsx

@ -176,7 +176,8 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -176,7 +176,8 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
try {
const { rangeStartMs, rangeEndExclusiveMs } = paddedMonthRange
const [fromIdb, fromArchive] = await Promise.all([
const idbP = Promise.all([
indexedDb.getCalendarEventsForOccurrenceWindow(
rangeStartMs,
rangeEndExclusiveMs,
@ -189,30 +190,50 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -189,30 +190,50 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
2500
)
])
if (cancelled) return
const localBaseline = dedupeCalendarEventsPreferringOccurrenceRange(
.then(([fromIdb, fromArchive]) =>
dedupeCalendarEventsPreferringOccurrenceRange(
[...fromIdb, ...fromArchive],
rangeStartMs,
rangeEndExclusiveMs
)
)
.catch((): NostrEvent[] => [])
const fromSessionNow = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
const sessionOnly = dedupeCalendarEventsPreferringOccurrenceRange(
fromSessionNow,
rangeStartMs,
rangeEndExclusiveMs
)
if (!cancelled) {
setRawEvents(sessionOnly)
setLoading(false)
}
void idbP.then((localBaseline) => {
if (cancelled) return
const s2 = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents(
dedupeCalendarEventsPreferringOccurrenceRange(
[...localBaseline, ...fromSessionNow],
[...localBaseline, ...s2],
rangeStartMs,
rangeEndExclusiveMs
)
)
setLoading(false)
})
if (!relayUrls.length) {
scheduleLateSessionMerge(localBaseline)
void idbP.then((lb) => {
if (!cancelled) scheduleLateSessionMerge(lb)
})
return
}
@ -257,17 +278,18 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -257,17 +278,18 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
)
)
let batch: NostrEvent[] = []
const relayMergedP = Promise.all([mainReq, ...chunkReqs])
.then((merged) => {
const batch = merged[0] ?? []
const fromFollowing: NostrEvent[] = []
try {
const merged = await Promise.all([mainReq, ...chunkReqs])
batch = merged[0] ?? []
for (let i = 1; i < merged.length; i++) {
fromFollowing.push(...(merged[i] ?? []))
}
} catch {
/* keep IndexedDB + session view; relays may be unreachable */
}
return { batch, fromFollowing }
})
.catch(() => ({ batch: [] as NostrEvent[], fromFollowing: [] as NostrEvent[] }))
const [{ batch, fromFollowing }, localBaseline] = await Promise.all([relayMergedP, idbP])
if (cancelled) return
const fromSession = client.getSessionEventsMatchingSearch(

18
src/pages/primary/SpellsPage/index.tsx

@ -259,12 +259,14 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -259,12 +259,14 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
if (!cancelled) setFollowSetListEvents([])
return
}
const events = await queryService.fetchEvents(
const [events, tombstones] = await Promise.all([
queryService.fetchEvents(
feedUrls,
{ authors: [pubkey], kinds: [ExtendedKind.FOLLOW_SET], limit: 500 },
{ eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false }
)
const tombstones = await indexedDb.getAllTombstones()
),
indexedDb.getAllTombstones()
])
if (!cancelled) {
setFollowSetListEvents(dedupeFollowSetEventsByD(filterEventsExcludingTombstones(events, tombstones)))
}
@ -349,13 +351,15 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -349,13 +351,15 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
if (manualBump) {
spellCatalogLastManualKeyRef.current = spellCatalogManualRefreshKey
}
const cachedSpells = await indexedDb.getSpellEvents()
if (cancelled) return
const shouldSyncFromRelays = manualBump || cachedSpells.length === 0
if (!shouldSyncFromRelays) {
const idbSpellsP = indexedDb.getSpellEvents()
if (!manualBump) {
const cachedSpells = await idbSpellsP
if (cancelled) return
if (cachedSpells.length > 0) {
return
}
}
const urls = getRelaysForSpellCatalogSync(favoriteRelays, blockedRelays, userReadRelaysWithHttp(relayList), {
userWriteRelays: relayList?.write ?? []

10
src/pages/secondary/ProfileInteractionDiagramPage/index.tsx

@ -55,16 +55,24 @@ function useProfileInteractionPartners(authorPubkey: string | undefined, refresh @@ -55,16 +55,24 @@ function useProfileInteractionPartners(authorPubkey: string | undefined, refresh
const kindsArr = [...INTERACTION_KINDS]
const sessionEv = eventService.listSessionEventsAuthoredBy(pk, { kinds: kindsArr, limit: 900 })
setSessionEventCount(sessionEv.length)
setArchiveAuthorEvents(0)
const mergedSession = mergeEventsById([...sessionEv])
setPartners(buildInteractionPartnerStats(mergedSession, pk))
void (async () => {
try {
const idbEv = await indexedDb.scanEventArchiveByAuthorPubkey(pk, {
kinds: kindsArr,
maxRowsScanned: 14_000,
maxMatches: 450
})
setArchiveAuthorEvents(idbEv.length)
const merged = mergeEventsById([...sessionEv, ...idbEv])
setPartners(buildInteractionPartnerStats(merged, pk))
} catch {
/* best-effort disk */
}
})()
} finally {
setLoading(false)
}

39
src/services/mention-event-search.service.ts

@ -82,29 +82,31 @@ async function searchCitationEventsForPickerInternal( @@ -82,29 +82,31 @@ async function searchCitationEventsForPickerInternal(
}
const idHex = tryParseCitationEventIdFromQuery(q)
if (idHex) {
const ev = await client.fetchEvent(idHex)
if (ev && kindsList.includes(ev.kind)) push(ev, false)
if (out.length >= limit) return out.slice(0, limit)
}
for (const ev of eventService.getSessionCitationFieldSearch(q, limit)) {
push(ev, false)
if (out.length >= limit) return out.slice(0, limit)
}
const fromArch = await indexedDb.getCachedAndArchivedCitationFieldSearch(
q,
limit - out.length,
kindsList,
{ archiveScanMaxMs: 14_000 }
)
const needAfterSession = limit - out.length
const [idEv, fromArch, relayUrls] = await Promise.all([
idHex ? client.fetchEvent(idHex) : Promise.resolve(null),
needAfterSession > 0
? indexedDb.getCachedAndArchivedCitationFieldSearch(q, needAfterSession, kindsList, {
archiveScanMaxMs: 14_000
})
: Promise.resolve([] as NEvent[]),
buildCitationPickerSearchRelayUrls()
])
if (idEv && kindsList.includes(idEv.kind)) push(idEv, false)
if (out.length >= limit) return out.slice(0, limit)
for (const ev of fromArch) {
push(ev, false)
if (out.length >= limit) return out.slice(0, limit)
}
const relayUrls = await buildCitationPickerSearchRelayUrls()
const need = limit - out.length
if (need <= 0) return out.slice(0, limit)
@ -170,15 +172,16 @@ export async function searchEventsForPicker( @@ -170,15 +172,16 @@ export async function searchEventsForPicker(
fromSession.forEach(addUnique)
if (out.length >= limit) return out.slice(0, limit)
const fromIdb = await indexedDb.getCachedEventsForSearch(q, limit - out.length, kindsList)
fromIdb.forEach(addUnique)
if (out.length >= limit) return out.slice(0, limit)
const fromRelays = await queryService.fetchEvents(
const need = limit - out.length
const [fromIdb, fromRelays] = await Promise.all([
indexedDb.getCachedEventsForSearch(q, need, kindsList),
queryService.fetchEvents(
SEARCHABLE_RELAY_URLS,
{ kinds: kindsList, search: q, limit: limit - out.length },
{ kinds: kindsList, search: q, limit: need },
{ eoseTimeout: 5000, globalTimeout: 8000 }
)
])
fromIdb.forEach(addUnique)
fromRelays.forEach(addUnique)
return out.slice(0, limit)
}

Loading…
Cancel
Save