Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
e2bece4645
  1. 150
      src/components/NoteList/index.tsx
  2. 53
      src/pages/primary/SpellsPage/fauxSpellFeeds.ts
  3. 21
      src/pages/primary/SpellsPage/useSpellsPageFeed.ts
  4. 55
      src/services/indexed-db.service.ts

150
src/components/NoteList/index.tsx

@ -551,6 +551,39 @@ function eventMatchesSubRequestFilter(event: Event, filter: Filter): boolean { @@ -551,6 +551,39 @@ function eventMatchesSubRequestFilter(event: Event, filter: Filter): boolean {
return true
}
/** Same as {@link eventMatchesSubRequestFilter} plus `since` / `until` / substring `search` (local warm-up only). */
function eventMatchesSubRequestFilterWithWindow(event: Event, filter: Filter): boolean {
if (typeof filter.since === 'number' && event.created_at < filter.since) return false
if (typeof filter.until === 'number' && event.created_at > filter.until) return false
const searchRaw = typeof filter.search === 'string' ? filter.search.trim() : ''
if (searchRaw.length > 0) {
const needle = searchRaw.toLowerCase()
const hay = `${event.content ?? ''} ${(event.tags ?? []).flat().join(' ')}`.toLowerCase()
if (!hay.includes(needle)) return false
}
return eventMatchesSubRequestFilter(event, filter)
}
function unionKindsForSpellLocalWarmup(
shardFilters: Filter[],
fallbackKinds: readonly number[]
): number[] {
const kindUnion = new Set<number>()
for (const f of shardFilters) {
const kk = Array.isArray(f.kinds) ? f.kinds : []
for (const k of kk) kindUnion.add(k as number)
}
if (kindUnion.size > 0) return Array.from(kindUnion).sort((a, b) => a - b)
return fallbackKinds.length > 0 ? [...fallbackKinds] : [kinds.ShortTextNote]
}
function tightestSinceFromSpellFilters(shardFilters: Filter[]): number | undefined {
const sinceCandidates = shardFilters
.map((f) => (typeof f.since === 'number' ? f.since : undefined))
.filter((n): n is number => n !== undefined)
return sinceCandidates.length > 0 ? Math.max(...sinceCandidates) : undefined
}
const NoteList = forwardRef(
(
{
@ -1935,7 +1968,114 @@ const NoteList = forwardRef( @@ -1935,7 +1968,114 @@ const NoteList = forwardRef(
setLoading(!!oneShotFetch)
} else {
let primedFromDisk = false
if (!oneShotFetch && mappedSubRequests.length > 0) {
let spellLocalMergeBase: Event[] = []
const isSpellPageLocalWarmup =
hostPrimaryPageName === 'spells' && !oneShotFetch && mappedSubRequests.length > 0
if (isSpellPageLocalWarmup) {
const shardFilters = mappedSubRequests.map(({ filter }) => filter as Filter)
const matchesSpellLocal = (ev: Event) =>
shardFilters.some((f) => eventMatchesSubRequestFilterWithWindow(ev, f))
const kindsForScan = unionKindsForSpellLocalWarmup(
shardFilters,
effectiveShowKindsRef.current
)
const sinceTightest = tightestSinceFromSpellFilters(shardFilters)
const localLayerCap = Math.min(
FEED_FULL_SEARCH_MERGE_CAP,
Math.max(eventCapEarly, 200)
)
const sessionScanCap = Math.min(800, localLayerCap * 4)
const sessionHits = client
.getSessionEventsMatchingSearch('', sessionScanCap, kindsForScan)
.filter(matchesSpellLocal)
.sort((a, b) => b.created_at - a.created_at)
if (!timelineEffectStale() && sessionHits.length > 0) {
const narrowedS = narrowLiveBatch(sessionHits)
if (narrowedS.length > 0) {
const mergedS = collapseDuplicateNip18RepostTimelineRows(
mergeEventBatchesById([], narrowedS, eventCapEarly, areAlgoRelays)
)
if (mergedS.length > 0) {
spellLocalMergeBase = mergedS
setEvents(mergedS)
lastEventsForTimelinePrefetchRef.current = mergedS
setNewEvents([])
setShowCount(revealBatchSize ?? SHOW_COUNT)
setLoading(false)
feedPaintRelayPendingRef.current = true
feedPaintRelayMetaRef.current = {
variant: 'spell_local_session',
mergedCount: mergedS.length
}
primedFromDisk = true
}
}
}
try {
const [diskRaw, fromPub, fromArch] = await Promise.all([
client.getTimelineDiskSnapshotEvents(
mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }>
),
indexedDb.getCachedPublicationEventsByKinds(localLayerCap * 2, kindsForScan),
indexedDb.scanEventArchiveByKinds({
kinds: kindsForScan,
since: sinceTightest,
maxRowsScanned: 10_000,
maxMatches: localLayerCap * 2
})
])
if (!timelineEffectStale()) {
const seen = new Set<string>()
const combinedRaw: Event[] = []
for (const ev of diskRaw) {
if (seen.has(ev.id)) continue
seen.add(ev.id)
combinedRaw.push(ev)
}
for (const ev of fromPub) {
if (seen.has(ev.id)) continue
if (!matchesSpellLocal(ev)) continue
seen.add(ev.id)
combinedRaw.push(ev)
}
for (const ev of fromArch) {
if (seen.has(ev.id)) continue
if (!matchesSpellLocal(ev)) continue
seen.add(ev.id)
combinedRaw.push(ev)
}
combinedRaw.sort((a, b) => b.created_at - a.created_at)
if (combinedRaw.length > 0) {
const diskNarrowed = narrowLiveBatch(combinedRaw)
if (diskNarrowed.length > 0) {
const merged = collapseDuplicateNip18RepostTimelineRows(
mergeEventBatchesById(spellLocalMergeBase, diskNarrowed, eventCapEarly, areAlgoRelays)
)
if (merged.length > 0) {
setEvents(merged)
lastEventsForTimelinePrefetchRef.current = merged
setNewEvents([])
setShowCount(revealBatchSize ?? SHOW_COUNT)
setLoading(false)
feedPaintRelayPendingRef.current = true
feedPaintRelayMetaRef.current = {
variant:
spellLocalMergeBase.length > 0 ? 'spell_local_merged' : 'disk_snapshot',
mergedCount: merged.length
}
primedFromDisk = true
}
}
}
}
} catch {
/* spell local + disk snapshot is best-effort */
}
} else if (!oneShotFetch && mappedSubRequests.length > 0) {
try {
const diskRaw = await client.getTimelineDiskSnapshotEvents(
mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }>
@ -2569,7 +2709,8 @@ const NoteList = forwardRef( @@ -2569,7 +2709,8 @@ const NoteList = forwardRef(
withKindFilter,
onSingleRelayKindlessEmpty,
mapLiveSubRequestsForTimeline,
progressiveWarmupQuery
progressiveWarmupQuery,
hostPrimaryPageName
])
useEffect(() => {
@ -2882,6 +3023,8 @@ const NoteList = forwardRef( @@ -2882,6 +3023,8 @@ const NoteList = forwardRef(
const hasMoreRef = useRef(hasMore)
const timelineKeyRef = useRef(timelineKey)
const blankFeedHiddenAtRef = useRef<number | null>(null)
/** Avoid subscribe storms when the tab stays empty (dead relays): visibility resume used to call `refresh()` every few seconds. */
const blankFeedVisibilityResumeRetryAtRef = useRef(0)
useEffect(() => {
showCountRef.current = showCount
@ -2981,6 +3124,9 @@ const NoteList = forwardRef( @@ -2981,6 +3124,9 @@ const NoteList = forwardRef(
if (loadingRef.current) return
if (eventsRef.current.length > 0) return
if (!subRequestsRef.current.length) return
const now = Date.now()
if (now - blankFeedVisibilityResumeRetryAtRef.current < 45_000) return
blankFeedVisibilityResumeRetryAtRef.current = now
logger.info('[NoteList] Blank feed — auto-retry after tab resume', { hiddenMs })
refresh()
}

53
src/pages/primary/SpellsPage/fauxSpellFeeds.ts

@ -12,14 +12,16 @@ @@ -12,14 +12,16 @@
import {
DEFAULT_FEED_SHOW_KINDS,
ExtendedKind,
FAST_READ_RELAY_URLS,
PROFILE_MEDIA_TAB_KINDS,
READ_ONLY_RELAY_URLS
} from '@/constants'
import { RENDERABLE_NOTE_KINDS_SORTED } from '@/lib/note-renderable-kinds'
import { buildProfileAugmentedReadRelayUrls } from '@/lib/favorites-feed-relays'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeTopic } from '@/lib/discussion-topics'
import { userIdToPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import type { TFeedSubRequest } from '@/types'
import { type Event, type Filter } from 'nostr-tools'
@ -77,6 +79,55 @@ const INTERESTS_MAX_TOPIC_TAG_VALUES = INTERESTS_MAX_TOPICS * 4 @@ -77,6 +79,55 @@ const INTERESTS_MAX_TOPIC_TAG_VALUES = INTERESTS_MAX_TOPICS * 4
* ({@link FAUX_SPELL_MAX_RELAYS}); appending read-only at the end dropped mirrors whenever inbox+favorites
* filled the cap.
*/
/**
* {@link buildPrioritizedReadRelayUrls} merges inbox favorites FAST_READ under {@link FAUX_SPELL_MAX_RELAYS}.
* Long NIP-65 read lists can fill the cap before FAST_READ is reached, so every REQ shard was only dead/private
* relays live faux feeds (media, etc.) stayed empty while the console showed only connection refused.
*/
export function ensureFauxSpellRelayStackTouchesFastRead(urls: string[]): string[] {
const fast = dedupeNormalizeRelayUrlsOrdered(
FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
)
const fastNormSet = new Set<string>()
for (const u of fast) {
const n = normalizeAnyRelayUrl(u) || u.trim()
if (n) fastNormSet.add(n)
}
let out = dedupeNormalizeRelayUrlsOrdered(urls)
if (!out.length) return fast.slice(0, FAUX_SPELL_MAX_RELAYS)
const fastCount = () =>
out.reduce((n, u) => {
const k = normalizeAnyRelayUrl(u) || u.trim()
return n + (k && fastNormSet.has(k) ? 1 : 0)
}, 0)
while (fastCount() < 2) {
let addedOne = false
for (const fr of fast) {
const fn = normalizeAnyRelayUrl(fr) || fr.trim()
if (!fn || out.some((u) => (normalizeAnyRelayUrl(u) || u.trim()) === fn)) continue
while (out.length >= FAUX_SPELL_MAX_RELAYS) {
let dropped = false
for (let i = out.length - 1; i >= 0; i--) {
const kn = normalizeAnyRelayUrl(out[i]!) || out[i]!.trim()
if (kn && !fastNormSet.has(kn)) {
out.splice(i, 1)
dropped = true
break
}
}
if (!dropped) break
}
out.push(fr)
addedOne = true
break
}
if (!addedOne) break
}
return dedupeNormalizeRelayUrlsOrdered(out).slice(0, FAUX_SPELL_MAX_RELAYS)
}
export function appendCuratedReadOnlyRelays(curated: string[], blockedRelays: string[]): string[] {
const blocked = new Set(blockedRelays.map((b) => normalizeAnyRelayUrl(b) || b))
const seen = new Set<string>()

21
src/pages/primary/SpellsPage/useSpellsPageFeed.ts

@ -33,7 +33,8 @@ import { @@ -33,7 +33,8 @@ import {
FAUX_SPELL_EVENT_LIMIT,
MEDIA_SPELL_KINDS,
NOTIFICATION_SPELL_KINDS,
applyFauxSpellCapsToSubRequests
applyFauxSpellCapsToSubRequests,
ensureFauxSpellRelayStackTouchesFastRead
} from './fauxSpellFeeds'
import { getRelaysForSpell, spellEventToFilter } from '@/services/spell.service'
import type { TFeedSubRequest } from '@/types'
@ -348,14 +349,16 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -348,14 +349,16 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
selectedFauxSpell === 'media' ||
selectedFauxSpell === 'bookmarks' ||
selectedFauxSpell === 'interests'
const feedUrls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{
userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: fauxSpellSkipSocialKindBlocked ? false : undefined
}
const feedUrls = ensureFauxSpellRelayStackTouchesFastRead(
getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
userReadRelaysWithHttp(relayList),
{
userWriteRelays: relayList?.write ?? [],
applySocialKindBlockedFilter: fauxSpellSkipSocialKindBlocked ? false : undefined
}
)
)
if (selectedFauxSpell === 'notifications') {

55
src/services/indexed-db.service.ts

@ -1456,6 +1456,61 @@ class IndexedDbService { @@ -1456,6 +1456,61 @@ class IndexedDbService {
})
}
/**
* Iterate {@link StoreNames.PUBLICATION_EVENTS} and return up to `limit` events whose kind is in `allowedKinds`,
* newest {@link Event.created_at} first. Used for spell feeds and similar: show cached rows before relay REQ.
*/
async getCachedPublicationEventsByKinds(
limit: number,
allowedKinds: number[],
options?: { scanBudget?: number }
): Promise<Event[]> {
await this.initPromise
if (
!this.db ||
!this.db.objectStoreNames.contains(StoreNames.PUBLICATION_EVENTS) ||
allowedKinds.length === 0 ||
limit <= 0
) {
return []
}
const kindSet = new Set(allowedKinds)
const scanBudget = Math.min(Math.max(options?.scanBudget ?? 12_000, 200), 50_000)
const collectCap = Math.min(4000, Math.max(limit * 4, limit + 80))
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(StoreNames.PUBLICATION_EVENTS, 'readonly')
const store = transaction.objectStore(StoreNames.PUBLICATION_EVENTS)
const request = store.openCursor()
const results: Event[] = []
let scanned = 0
request.onsuccess = () => {
const cursor = (request as IDBRequest<IDBCursorWithValue>).result
if (!cursor || scanned >= scanBudget || results.length >= collectCap) {
transaction.commit()
results.sort((a, b) => b.created_at - a.created_at)
resolve(results.slice(0, limit))
return
}
scanned += 1
const item = cursor.value as TValue<Event> | undefined
if (item?.value) {
const event = item.value as Event
if (kindSet.has(event.kind)) {
results.push(event)
}
}
cursor.continue()
}
request.onerror = (event) => {
transaction.commit()
reject(event)
}
})
}
/**
* Publication store + hot {@link StoreNames.EVENT_ARCHIVE}: events whose kind is allowed and content or any tag
* value matches the query (case-insensitive). Used to show local hits before NIP-50 relay results.

Loading…
Cancel
Save