Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
b4463d9cb0
  1. 30
      src/components/NoteList/index.tsx
  2. 3
      src/constants.ts
  3. 3
      src/lib/index-relay-http.ts
  4. 12
      src/lib/profile-author-warmup-spec.test.ts
  5. 5
      src/lib/profile-author-warmup-spec.ts
  6. 3
      src/lib/profile-relay-search-filters.ts
  7. 20
      src/lib/replaceable-fetch-kinds.test.ts
  8. 8
      src/lib/replaceable-fetch-kinds.ts
  9. 5
      src/lib/replaceable-list-latest.ts
  10. 93
      src/services/client-replaceable-events.service.ts
  11. 9
      src/services/client.service.ts

30
src/components/NoteList/index.tsx

@ -80,7 +80,8 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -80,7 +80,8 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { buildFeedFullSearchRelayUrls } from '@/lib/feed-full-search-relays'
import {
getProfileAuthorWarmupRelayUrls,
getProfileAuthorWarmupSpec
getProfileAuthorWarmupSpec,
isProfileTimelineSubscriptionKey
} from '@/lib/profile-author-warmup-spec'
import type { TProfile } from '@/types'
import { Button } from '@/components/ui/button'
@ -1982,6 +1983,7 @@ const NoteList = forwardRef( @@ -1982,6 +1983,7 @@ const NoteList = forwardRef(
preserveTimelineOnSubRequestsChange &&
!userPulledRefresh &&
!feedScopeChanged &&
eventsRef.current.length > 0 &&
(prevSubKey === subRequestsKey ||
isRelayUrlStrictSupersetIdentityKey(prevSubKey, subRequestsKey) ||
(mergeTimelineWhenSubRequestFiltersMatch &&
@ -2058,6 +2060,10 @@ const NoteList = forwardRef( @@ -2058,6 +2060,10 @@ const NoteList = forwardRef(
return undefined
}
const isProfileTimelineFeed =
hostPrimaryPageNameRef.current === 'profile' ||
isProfileTimelineSubscriptionKey(timelineSubscriptionKey)
/**
* Relay kindless firehose: keep the full batch. Else when the kind picker applies, narrow like
* {@link applyKindPickerInUi}. Remaining spell paths use kinds-only narrowing when client-side kind filter runs.
@ -2074,11 +2080,7 @@ const NoteList = forwardRef( @@ -2074,11 +2080,7 @@ const NoteList = forwardRef(
showKind1111Ref.current
)
)
if (
out.length > 0 ||
hostPrimaryPageNameRef.current !== 'profile' ||
mappedSubRequests.length === 0
) {
if (out.length > 0 || !isProfileTimelineFeed || mappedSubRequests.length === 0) {
return out
}
return filterEvsToMappedTimelineReqKinds(evs, mappedSubRequests)
@ -2086,18 +2088,14 @@ const NoteList = forwardRef( @@ -2086,18 +2088,14 @@ const NoteList = forwardRef(
if (!useFilterAsIsRef.current || !clientSideKindFilterRef.current) return evs
if (!withKindFilterRef.current) return evs
const byPicker = evs.filter((e) => effectiveShowKindsRef.current.includes(e.kind))
if (
byPicker.length > 0 ||
hostPrimaryPageNameRef.current !== 'profile' ||
mappedSubRequests.length === 0
) {
if (byPicker.length > 0 || !isProfileTimelineFeed || mappedSubRequests.length === 0) {
return byPicker
}
return filterEvsToMappedTimelineReqKinds(evs, mappedSubRequests)
}
const eventMatchesProfileTimelineRequest = (event: Event) =>
hostPrimaryPageNameRef.current === 'profile' &&
isProfileTimelineFeed &&
mappedSubRequests.some(({ filter }) =>
eventMatchesSubRequestFilterWithWindow(event, filter as Filter)
)
@ -2377,7 +2375,7 @@ const NoteList = forwardRef( @@ -2377,7 +2375,7 @@ const NoteList = forwardRef(
}>
const profileAuthorWarmSpec = getProfileAuthorWarmupSpec(profileMapped)
if (
hostPrimaryPageName === 'profile' &&
isProfileTimelineFeed &&
profileAuthorWarmSpec &&
!timelineEffectStale()
) {
@ -3235,7 +3233,8 @@ const NoteList = forwardRef( @@ -3235,7 +3233,8 @@ const NoteList = forwardRef(
}
const eventMatchesProfileDeltaRequest = (event: Event) =>
hostPrimaryPageNameRef.current === 'profile' &&
(hostPrimaryPageNameRef.current === 'profile' ||
isProfileTimelineSubscriptionKey(timelineSubscriptionKey)) &&
mappedDelta.some(({ filter }) =>
eventMatchesSubRequestFilterWithWindow(event, filter as Filter)
)
@ -3629,7 +3628,8 @@ const NoteList = forwardRef( @@ -3629,7 +3628,8 @@ const NoteList = forwardRef(
publicReadFallbackAttemptedRef.current = true
const profileWarm =
hostPrimaryPageNameRef.current === 'profile'
hostPrimaryPageNameRef.current === 'profile' ||
isProfileTimelineSubscriptionKey(timelineSubscriptionKey)
? getProfileAuthorWarmupSpec(
mapped as Array<{ urls: string[]; filter: TSubRequestFilter }>
)

3
src/constants.ts

@ -613,6 +613,9 @@ export function isAuthorProfileMetadataPublishKind(kind: number): boolean { @@ -613,6 +613,9 @@ export function isAuthorProfileMetadataPublishKind(kind: number): boolean {
* Author-published replaceables refetched on profile-view refresh, profile editor Refresh cache,
* settings Refresh cache, and {@link ReplaceableEventService.refreshAuthorPublishedReplaceablesFromRelays}.
*/
/** Kinds requested in the same REQ whenever the app fetches author metadata (kind 0). */
export const METADATA_CO_FETCH_KINDS: readonly number[] = [kinds.Metadata, ExtendedKind.PAYMENT_INFO]
export const AUTHOR_PROFILE_VIEW_REPLACEABLE_KINDS: readonly number[] = [
kinds.Metadata,
kinds.Contacts,

3
src/lib/index-relay-http.ts

@ -227,6 +227,9 @@ export async function queryIndexRelay( @@ -227,6 +227,9 @@ export async function queryIndexRelay(
status: res.status
})
}
if (res.status >= 500) {
throw new IndexRelayTransportError(new Error(`HTTP ${res.status}`))
}
continue
}
const json = (await res.json()) as { data?: unknown }

12
src/lib/profile-author-warmup-spec.test.ts

@ -1,6 +1,9 @@ @@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest'
import { ExtendedKind } from '@/constants'
import { getProfileAuthorWarmupSpec } from './profile-author-warmup-spec'
import {
getProfileAuthorWarmupSpec,
isProfileTimelineSubscriptionKey
} from './profile-author-warmup-spec'
describe('getProfileAuthorWarmupSpec', () => {
const authorHex = 'a'.repeat(64)
@ -23,6 +26,13 @@ describe('getProfileAuthorWarmupSpec', () => { @@ -23,6 +26,13 @@ describe('getProfileAuthorWarmupSpec', () => {
expect(spec).toEqual({ author: authorHex, kinds: [1] })
})
it('detects profile feed subscription keys', () => {
expect(isProfileTimelineSubscriptionKey('profile-posts-abc-1-200')).toBe(true)
expect(isProfileTimelineSubscriptionKey('profile-media-abc')).toBe(true)
expect(isProfileTimelineSubscriptionKey('home-all-favorites')).toBe(false)
expect(isProfileTimelineSubscriptionKey(null)).toBe(false)
})
it('returns null when no author shards', () => {
expect(
getProfileAuthorWarmupSpec([

5
src/lib/profile-author-warmup-spec.ts

@ -2,6 +2,11 @@ import type { TSubRequestFilter } from '@/types' @@ -2,6 +2,11 @@ import type { TSubRequestFilter } from '@/types'
import { normalizeHexPubkey } from '@/lib/pubkey'
import type { Filter } from 'nostr-tools'
/** Profile Posts/Media tabs pass stable keys like `profile-posts-…` / `profile-media-…`. */
export function isProfileTimelineSubscriptionKey(key: string | undefined | null): boolean {
return typeof key === 'string' && key.startsWith('profile-')
}
/**
* Profile feeds may include calendar invite shards (`#p`) without `authors`. Local session/IDB
* warmup and relay fallback only need the single-author + kinds REQ shards.

3
src/lib/profile-relay-search-filters.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { METADATA_CO_FETCH_KINDS } from '@/constants'
import type { Filter } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import { splitNip05Identifier } from '@/lib/nip05'
@ -22,7 +23,7 @@ export function buildProfileKind0SearchFilters(opts: { @@ -22,7 +23,7 @@ export function buildProfileKind0SearchFilters(opts: {
const limit = Math.max(1, Math.min(opts.limit ?? 50, 500))
const time =
typeof opts.until === 'number' && opts.until > 0 ? ({ until: opts.until } as Pick<Filter, 'until'>) : {}
const k = [kinds.Metadata] as number[]
const k = [...METADATA_CO_FETCH_KINDS] as number[]
const pubkeyHex = decodeProfileSearchQueryToPubkeyHex(searchRaw)
if (pubkeyHex) {

20
src/lib/replaceable-fetch-kinds.test.ts

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
import { ExtendedKind } from '@/constants'
import { networkKindsForReplaceableFetch } from '@/lib/replaceable-fetch-kinds'
import { kinds } from 'nostr-tools'
import { describe, expect, it } from 'vitest'
describe('networkKindsForReplaceableFetch', () => {
it('includes kind 10133 when fetching kind 0', () => {
expect(networkKindsForReplaceableFetch(kinds.Metadata)).toEqual([
kinds.Metadata,
ExtendedKind.PAYMENT_INFO
])
})
it('leaves other replaceable kinds unchanged', () => {
expect(networkKindsForReplaceableFetch(kinds.Contacts)).toEqual([kinds.Contacts])
expect(networkKindsForReplaceableFetch(ExtendedKind.PAYMENT_INFO)).toEqual([
ExtendedKind.PAYMENT_INFO
])
})
})

8
src/lib/replaceable-fetch-kinds.ts

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
import { METADATA_CO_FETCH_KINDS } from '@/constants'
import { kinds } from 'nostr-tools'
/** Network `kinds` for a replaceable fetch: kind 0 always includes NIP-A3 payment info (10133). */
export function networkKindsForReplaceableFetch(kind: number): number[] {
if (kind === kinds.Metadata) return [...METADATA_CO_FETCH_KINDS]
return [kind]
}

5
src/lib/replaceable-list-latest.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { ExtendedKind, METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS } from '@/constants'
import { networkKindsForReplaceableFetch } from '@/lib/replaceable-fetch-kinds'
import { normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl } from '@/lib/url'
import client, { eventService } from '@/services/client.service'
@ -48,11 +49,11 @@ export async function fetchLatestReplaceableListEvent( @@ -48,11 +49,11 @@ export async function fetchLatestReplaceableListEvent(
const rows = await client.fetchEvents(
allUrls,
{ authors: [pk], kinds: [kind], limit: 80 },
{ authors: [pk], kinds: networkKindsForReplaceableFetch(kind), limit: 80 },
replaceableListFetchQueryOpts(kind)
)
return newestReplaceableEvent(rows)
return newestReplaceableEvent(rows.filter((e) => e.kind === kind))
}
/**

93
src/services/client-replaceable-events.service.ts

@ -34,6 +34,7 @@ import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eli @@ -34,6 +34,7 @@ import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eli
import { stripLocalNetworkRelaysForWssReq } from '@/lib/relay-list-sanitize'
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter'
import { isPromiseTimeoutError, racePromiseWithTimeout } from '@/lib/async-timeout'
import { networkKindsForReplaceableFetch } from '@/lib/replaceable-fetch-kinds'
export class ReplaceableEventService {
/** Limits parallel Step 2/3 profile network work (relay list + wide metadata REQ). */
@ -244,7 +245,7 @@ export class ReplaceableEventService { @@ -244,7 +245,7 @@ export class ReplaceableEventService {
relayUrls,
{
authors: [pubkey],
kinds: [kind]
kinds: networkKindsForReplaceableFetch(kind)
},
undefined,
{
@ -253,7 +254,12 @@ export class ReplaceableEventService { @@ -253,7 +254,12 @@ export class ReplaceableEventService {
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
}
)
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
if (kind === kinds.Metadata) {
this.ingestMetadataCoFetchSidecars(events)
}
const sortedEvents = events
.filter((e) => e.kind === kind)
.sort((a, b) => b.created_at - a.created_at)
event = sortedEvents.length > 0 ? sortedEvents[0] : undefined
} else {
// Use DataLoader for batching (IndexedDB checks and network fetches are batched)
@ -650,10 +656,13 @@ export class ReplaceableEventService { @@ -650,10 +656,13 @@ export class ReplaceableEventService {
kind === kinds.Metadata ? false : !isSlowReplaceableBatch || !chunkMulti
const evts = await this.queryService.query(
relayUrls,
{ authors: chunkPubkeys, kinds: [kind] },
{ authors: chunkPubkeys, kinds: networkKindsForReplaceableFetch(kind) },
undefined,
{ ...queryOpts, replaceableRace: chunkRace }
)
if (kind === kinds.Metadata) {
this.ingestMetadataCoFetchSidecars(evts)
}
merged.push(...evts)
}
events = merged
@ -662,11 +671,14 @@ export class ReplaceableEventService { @@ -662,11 +671,14 @@ export class ReplaceableEventService {
relayUrls,
{
authors: pubkeys,
kinds: [kind]
kinds: networkKindsForReplaceableFetch(kind)
},
undefined,
queryOpts
)
if (kind === kinds.Metadata) {
this.ingestMetadataCoFetchSidecars(events)
}
}
// CRITICAL: Limit the number of events processed to prevent memory issues during rapid scrolling
@ -690,32 +702,12 @@ export class ReplaceableEventService { @@ -690,32 +702,12 @@ export class ReplaceableEventService {
const limitedEvents = Array.from(eventsByPubkey.values()).slice(0, 500)
// Use limited events for processing
for (const event of limitedEvents) {
const key = `${event.pubkey}:${event.kind}`
const existing = eventsMap.get(key)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(key, event)
// Update results array for this event
const itemIndex = missingItems.findIndex(item => item.pubkey === event.pubkey)
if (itemIndex >= 0) {
const paramIndex = missingItems[itemIndex]!.index
results[paramIndex] = event
}
}
this.applyNetworkReplaceableEventToBatch(event, kind, missingItems, results, eventsMap)
}
} else {
// Normal processing for smaller batches
for (const event of events) {
const key = `${event.pubkey}:${event.kind}`
const existing = eventsMap.get(key)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(key, event)
// Update results array for this event
const itemIndex = missingItems.findIndex(item => item.pubkey === event.pubkey)
if (itemIndex >= 0) {
const paramIndex = missingItems[itemIndex]!.index
results[paramIndex] = event
}
}
this.applyNetworkReplaceableEventToBatch(event, kind, missingItems, results, eventsMap)
}
}
@ -743,12 +735,8 @@ export class ReplaceableEventService { @@ -743,12 +735,8 @@ export class ReplaceableEventService {
// Step 3: Persist hits only. Do not write negative cache rows (`value: null`) — optional kinds
// (e.g. 10432 cache relays, 10001 pins) are missing for most pubkeys and would flood IndexedDB.
await Promise.allSettled(
missingParams.map(async ({ pubkey, kind }) => {
const key = `${pubkey}:${kind}`
const event = eventsMap.get(key)
if (event) {
Array.from(eventsMap.values()).map(async (event) => {
await indexedDb.putReplaceableEvent(event)
}
})
)
@ -812,6 +800,33 @@ export class ReplaceableEventService { @@ -812,6 +800,33 @@ export class ReplaceableEventService {
})
}
/** Persist kind 10133 rows returned alongside a kind-0 REQ (same filter, separate cache slots). */
private ingestMetadataCoFetchSidecars(events: readonly NEvent[]): void {
for (const event of events) {
if (event.kind !== ExtendedKind.PAYMENT_INFO || shouldDropEventOnIngest(event)) continue
void this.updateReplaceableEventFromBigRelaysCache(event)
}
}
private applyNetworkReplaceableEventToBatch(
event: NEvent,
requestedKind: number,
missingItems: { pubkey: string; index: number }[],
results: (NEvent | null)[],
eventsMap: Map<string, NEvent>
): void {
const kindKey = `${event.pubkey}:${event.kind}`
const existing = eventsMap.get(kindKey)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(kindKey, event)
}
if (event.kind !== requestedKind) return
const itemIndex = missingItems.findIndex((item) => item.pubkey === event.pubkey)
if (itemIndex < 0) return
const paramIndex = missingItems[itemIndex]!.index
results[paramIndex] = event
}
/**
* Private: Update cache for replaceable event from big relays
*/
@ -858,7 +873,7 @@ export class ReplaceableEventService { @@ -858,7 +873,7 @@ export class ReplaceableEventService {
try {
const events = await this.queryService.query(
relays,
{ authors: [pk], kinds: [kinds.Metadata], limit: 1 },
{ authors: [pk], kinds: networkKindsForReplaceableFetch(kinds.Metadata), limit: 4 },
undefined,
{
replaceableRace: false,
@ -868,8 +883,10 @@ export class ReplaceableEventService { @@ -868,8 +883,10 @@ export class ReplaceableEventService {
relayOpSource: 'ReplaceableEventService.fetchKind0FromProfileRelays'
}
)
if (events.length === 0) return undefined
const sorted = events.sort((a, b) => b.created_at - a.created_at)
this.ingestMetadataCoFetchSidecars(events)
const metadataRows = events.filter((e) => e.kind === kinds.Metadata)
if (metadataRows.length === 0) return undefined
const sorted = metadataRows.sort((a, b) => b.created_at - a.created_at)
return sorted[0]
} catch (error) {
logger.warn('[ReplaceableEventService] fetchKind0FromProfileRelays failed', {
@ -1038,7 +1055,7 @@ export class ReplaceableEventService { @@ -1038,7 +1055,7 @@ export class ReplaceableEventService {
relaysForQuery,
{
authors: [pubkey],
kinds: [kinds.Metadata]
kinds: networkKindsForReplaceableFetch(kinds.Metadata)
},
undefined,
{
@ -1049,8 +1066,10 @@ export class ReplaceableEventService { @@ -1049,8 +1066,10 @@ export class ReplaceableEventService {
}
)
if (events.length > 0) {
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
this.ingestMetadataCoFetchSidecars(events)
const metadataRows = events.filter((e) => e.kind === kinds.Metadata)
if (metadataRows.length > 0) {
const sortedEvents = metadataRows.sort((a, b) => b.created_at - a.created_at)
const found = sortedEvents[0]!
await this.indexProfile(found)
return found

9
src/services/client.service.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import {
FAST_READ_RELAY_URLS,
METADATA_CO_FETCH_KINDS,
ExtendedKind,
FAST_WRITE_RELAY_URLS,
DOCUMENT_RELAY_URLS,
@ -3797,9 +3798,9 @@ class ClientService extends EventTarget { @@ -3797,9 +3798,9 @@ class ClientService extends EventTarget {
limit: limitCap,
until: filter.until
})
return built.length > 0 ? built : [{ ...filter, kinds: [kinds.Metadata] }]
return built.length > 0 ? built : [{ ...filter, kinds: [...METADATA_CO_FETCH_KINDS] }]
})()
: { ...filter, kinds: [kinds.Metadata] }
: { ...filter, kinds: [...METADATA_CO_FETCH_KINDS] }
/** NIP-50 text on many index relays: per-relay EOSE can be ~38s; global cap was 9s so subs were torn down early. */
const filtersArr = Array.isArray(queryFilter) ? queryFilter : [queryFilter]
@ -3819,6 +3820,10 @@ class ClientService extends EventTarget { @@ -3819,6 +3820,10 @@ class ClientService extends EventTarget {
const byPk = new Map<string, NEvent>()
for (const e of events) {
if (e.kind === ExtendedKind.PAYMENT_INFO && !shouldDropEventOnIngest(e)) {
void this.replaceableEventService.updateReplaceableEventCache(e)
continue
}
if (e.kind !== kinds.Metadata) continue
const prev = byPk.get(e.pubkey)
if (!prev || e.created_at > prev.created_at) {

Loading…
Cancel
Save