Browse Source

speed up feeds

imwald
Silberengel 1 month ago
parent
commit
c758ece6c6
  1. 6
      src/components/NoteInteractions/index.tsx
  2. 41
      src/components/NoteList/index.tsx
  3. 6
      src/components/Relay/index.tsx
  4. 10
      src/components/ReplyNoteList/index.tsx
  5. 2
      src/constants.ts
  6. 24
      src/lib/relay-list-builder.test.ts
  7. 26
      src/lib/relay-list-builder.ts
  8. 31
      src/services/client-query.service.ts
  9. 33
      src/services/client.service.ts

6
src/components/NoteInteractions/index.tsx

@ -13,7 +13,8 @@ export default function NoteInteractions({ @@ -13,7 +13,8 @@ export default function NoteInteractions({
event,
showQuotes: showQuotesProp,
statsForeground = false,
refreshToken = 0
refreshToken = 0,
singleRelayAuthoritativeRead = false
}: {
pageIndex?: number
event: Event
@ -23,6 +24,8 @@ export default function NoteInteractions({ @@ -23,6 +24,8 @@ export default function NoteInteractions({
statsForeground?: boolean
/** Bump to force the reply list to refetch. */
refreshToken?: number
/** Explore single-relay context: scope reply REQ to the browsing relay only. */
singleRelayAuthoritativeRead?: boolean
}) {
const { t } = useTranslation()
const [replySort, setReplySort] = useState<ReplySortOption>('oldest')
@ -61,6 +64,7 @@ export default function NoteInteractions({ @@ -61,6 +64,7 @@ export default function NoteInteractions({
showQuotes={showQuotes}
statsForeground={statsForeground}
refreshToken={refreshToken}
singleRelayAuthoritativeRead={singleRelayAuthoritativeRead}
/>
</>
)

41
src/components/NoteList/index.tsx

@ -1947,7 +1947,12 @@ const NoteList = forwardRef( @@ -1947,7 +1947,12 @@ const NoteList = forwardRef(
setLoading(true)
let diskPrimeCancelled = false
const primeDiskWhileAwaitingRelayProbe = async () => {
if (relayAuthoritativeFeedOnlyRef.current) return
const strictSingleRelayAuthoritative =
subRequestsRef.current.length === 1 &&
subRequestsRef.current[0]!.urls.length === 1 &&
(hostPrimaryPageNameRef.current === 'relay' ||
(allowKindlessRelayExploreRef.current && useFilterAsIsRef.current))
if (relayAuthoritativeFeedOnlyRef.current && !strictSingleRelayAuthoritative) return
try {
const mapped = stripNostrLandAggrFromTimelineSubRequests(
feedSubscriptionKey,
@ -2028,12 +2033,6 @@ const NoteList = forwardRef( @@ -2028,12 +2033,6 @@ const NoteList = forwardRef(
keepExistingTimelineEvents &&
eventsRef.current.length > 0
const sessionSnap =
!userPulledRefresh && !relayAuthoritativeFeedOnlyRef.current
? getSessionFeedSnapshot(sessionSnapshotIdentityKey)
: undefined
const restoredFromSession = !keepExistingTimelineEvents && !!(sessionSnap?.length)
const seeAllNoSpell = seeAllFeedEventsRef.current && !useFilterAsIsRef.current
const mappedSubRequests = stripNostrLandAggrFromTimelineSubRequests(
@ -2049,6 +2048,18 @@ const NoteList = forwardRef( @@ -2049,6 +2048,18 @@ const NoteList = forwardRef(
// key collisions where all offline relay-specific views share the same key.
.filter((req) => req.urls.length > 0)
const strictSingleRelayAuthoritativeEarly =
mappedSubRequests.length === 1 &&
mappedSubRequests[0]!.urls.length === 1 &&
(hostPrimaryPageNameRef.current === 'relay' ||
(allowKindlessRelayExploreRef.current && useFilterAsIsRef.current))
const sessionSnap =
!userPulledRefresh &&
(!relayAuthoritativeFeedOnlyRef.current || strictSingleRelayAuthoritativeEarly)
? getSessionFeedSnapshot(sessionSnapshotIdentityKey)
: undefined
const restoredFromSession = !keepExistingTimelineEvents && !!(sessionSnap?.length)
const filterMissingKinds = (f: Filter) => !f.kinds || f.kinds.length === 0
const invalidFilters = mappedSubRequests.filter(({ urls, filter: f }) => {
if (seeAllNoSpell) return false
@ -2131,7 +2142,12 @@ const NoteList = forwardRef( @@ -2131,7 +2142,12 @@ const NoteList = forwardRef(
* {@link onEvents} so rows appear as soon as local sources resolve.
*/
const startNonBlockingTimelineDiskPrime = () => {
if (relayAuthoritativeFeedOnlyRef.current) return
const strictSingleRelayAuthoritative =
mappedSubRequests.length === 1 &&
mappedSubRequests[0]!.urls.length === 1 &&
(hostPrimaryPageNameRef.current === 'relay' ||
(allowKindlessRelayExploreRef.current && useFilterAsIsRef.current))
if (relayAuthoritativeFeedOnlyRef.current && !strictSingleRelayAuthoritative) return
if (oneShotFetch || mappedSubRequests.length === 0) return
if (isSpellPageLocalWarmup) return
const diskReq = mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }>
@ -2252,7 +2268,7 @@ const NoteList = forwardRef( @@ -2252,7 +2268,7 @@ const NoteList = forwardRef(
}
if (!keepExistingTimelineEvents) {
if (restoredFromSession && sessionSnap) {
if (restoredFromSession && sessionSnap && sessionSnap.length > 0) {
feedPaintSessionPendingRef.current = true
const restored = collapseDuplicateNip18RepostTimelineRows(sessionSnap)
timelineMergeBootstrapRef.current = restored.slice()
@ -3087,7 +3103,12 @@ const NoteList = forwardRef( @@ -3087,7 +3103,12 @@ const NoteList = forwardRef(
setProgressiveLayersSearching(false)
followingFeedDeltaCloserRef.current?.()
followingFeedDeltaCloserRef.current = null
if (!relayAuthoritativeFeedOnlyRef.current) {
const strictSingleRelayAuthoritativeCleanup =
subRequestsRef.current.length === 1 &&
subRequestsRef.current[0]!.urls.length === 1 &&
(hostPrimaryPageNameRef.current === 'relay' ||
(allowKindlessRelayExploreRef.current && useFilterAsIsRef.current))
if (!relayAuthoritativeFeedOnlyRef.current || strictSingleRelayAuthoritativeCleanup) {
setSessionFeedSnapshot(snapshotKeyForCleanup, eventsRef.current)
}
if (kindlessEoseTimeoutRef.current) {

6
src/components/Relay/index.tsx

@ -113,6 +113,9 @@ const Relay = forwardRef< @@ -113,6 +113,9 @@ const Relay = forwardRef<
)
const shouldHideEventNotFromThisRelay = useCallback(
(ev: Event) => {
if (hostPrimaryPageName === 'relay' || allowKindlessRelayExplore) {
return false
}
if (!relaySeenMatchKey) return false
// LAN/loopback: REQ already targets this relay; "seen on" often lists another URL first
// (favorites merge, localhost vs 127.0.0.1, etc.) — hiding would empty the relay-only feed.
@ -121,7 +124,7 @@ const Relay = forwardRef< @@ -121,7 +124,7 @@ const Relay = forwardRef<
if (seen.length === 0) return false
return !seen.some((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase() === relaySeenMatchKey)
},
[relaySeenMatchKey, normalizedUrl]
[relaySeenMatchKey, normalizedUrl, hostPrimaryPageName, allowKindlessRelayExplore]
)
const alexandriaFeedEmptyUrl = useMemo(() => {
@ -158,6 +161,7 @@ const Relay = forwardRef< @@ -158,6 +161,7 @@ const Relay = forwardRef<
showAllKinds
showFeedClientFilter
hostPrimaryPageName={hostPrimaryPageName}
feedTimelineScopeKey={`relay:${normalizedUrl}`}
extraShouldHideEvent={shouldHideEventNotFromThisRelay}
extraShouldHideRepliesEvent={shouldHideEventNotFromThisRelay}
relayAuthoritativeFeedOnly

10
src/components/ReplyNoteList/index.tsx

@ -342,7 +342,8 @@ function ReplyNoteList({ @@ -342,7 +342,8 @@ function ReplyNoteList({
showQuotes = true,
duplicateWebPreviewCleanedUrlHints,
statsForeground = false,
refreshToken = 0
refreshToken = 0,
singleRelayAuthoritativeRead
}: {
index?: number
event: NEvent
@ -355,6 +356,8 @@ function ReplyNoteList({ @@ -355,6 +356,8 @@ function ReplyNoteList({
statsForeground?: boolean
/** Bump to force the relay reply scan to run again. */
refreshToken?: number
/** Explore single-relay: only query the active browsing relay (see `useCurrentRelays`). */
singleRelayAuthoritativeRead?: boolean
}) {
const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigation()
@ -367,6 +370,8 @@ function ReplyNoteList({ @@ -367,6 +370,8 @@ function ReplyNoteList({
const { zapReplyThreshold } = useZap()
const { blockedRelays, favoriteRelays } = useFavoriteRelays()
const { relayUrls: browsingRelayUrls } = useCurrentRelays()
const relayAuthoritativeRead =
singleRelayAuthoritativeRead ?? browsingRelayUrls.length === 1
const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(undefined)
const { repliesMap, addReplies } = useReply()
const isDiscussionRoot = event.kind === ExtendedKind.DISCUSSION
@ -1064,7 +1069,8 @@ function ReplyNoteList({ @@ -1064,7 +1069,8 @@ function ReplyNoteList({
opAuthorPubkey,
userPubkey || undefined,
replyBlockedRelays,
threadRelayHints
threadRelayHints,
relayAuthoritativeRead ? { relayAuthoritative: true } : undefined
)
// URL/article threads (NIP-22 `#i`): synthetic root has no e-tags or seen-relay hints — merge the same

2
src/constants.ts

@ -80,7 +80,7 @@ export const DEFAULT_FAVORITE_RELAYS = [ @@ -80,7 +80,7 @@ export const DEFAULT_FAVORITE_RELAYS = [
* Max concurrent relay connection + REQ setups (ensureRelay + subscribe) app-wide.
* Limits parallel WebSocket handshakes when many relays or timeline shards open at once.
*/
export const MAX_CONCURRENT_RELAY_CONNECTIONS = 10
export const MAX_CONCURRENT_RELAY_CONNECTIONS = 12
/**
* Max concurrent live REQ subscriptions on a single relay. Some relays enforce 10 SUBs; stay under

24
src/lib/relay-list-builder.test.ts

@ -1,17 +1,15 @@ @@ -1,17 +1,15 @@
import { describe, expect, it } from 'vitest'
import { pickAuthorNip65RelaysPreferringViewerOverlap } from './relay-list-builder'
import { buildReplyReadRelayList } from '@/lib/relay-list-builder'
describe('pickAuthorNip65RelaysPreferringViewerOverlap', () => {
it('prefers relays shared with the viewer, capped at max', () => {
const author = [
'wss://author-only.example/',
'wss://shared.example/',
'wss://author-two.example/'
]
const viewer = ['wss://shared.example/', 'wss://viewer-only.example/']
expect(pickAuthorNip65RelaysPreferringViewerOverlap(author, viewer, 2)).toEqual([
'wss://shared.example/',
'wss://author-only.example/'
])
describe('buildReplyReadRelayList relayAuthoritative', () => {
it('returns only thread hints and author/user layers without favorite bootstrap', async () => {
const out = await buildReplyReadRelayList(
undefined,
undefined,
[],
['wss://nostr.land/'],
{ relayAuthoritative: true }
)
expect(out).toEqual(['wss://nostr.land/'])
})
})

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

@ -572,12 +572,36 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -572,12 +572,36 @@ export async function buildPollResultsReadRelayUrls(options: {
* Build relay list for reading replies/comments: thread hints, author/user NIP-65, favorites, cache
* then default favorite relays only when global bootstrap applies (signed-out or no configured stack).
*/
export type BuildReplyReadRelayListOptions = {
/** When true (e.g. Explore single-relay page), query only thread hints + author/user NIP-65 — no favorite/fast-read bootstrap layer. */
relayAuthoritative?: boolean
}
export async function buildReplyReadRelayList(
opAuthorPubkey: string | undefined,
userPubkey: string | undefined,
blockedRelays: string[] = [],
threadRelayHints: string[] = []
threadRelayHints: string[] = [],
options?: BuildReplyReadRelayListOptions
): Promise<string[]> {
if (options?.relayAuthoritative) {
const scoped = await buildComprehensiveRelayList({
authorPubkey: opAuthorPubkey,
userPubkey,
relayHints: threadRelayHints,
includeUserOwnRelays: Boolean(userPubkey),
includeFastReadRelays: false,
useGlobalRelayDefaults: false,
includeSearchableRelays: false,
includeLocalRelays: true,
includeFavoriteRelays: false,
preferPublicReadRelaysEarly: false,
includeProfileFetchRelays: false,
blockedRelays
})
return scoped
}
let useGlobal = true
if (userPubkey) {
try {

31
src/services/client-query.service.ts

@ -264,7 +264,7 @@ export class QueryService { @@ -264,7 +264,7 @@ export class QueryService {
/** App-wide cap on parallel ensureRelay + initial subscribe setup (any relay). */
private globalRelayConnectionSlotsInUse = 0
private globalRelayConnectionWaitQueue: Array<() => void> = []
private globalRelayConnectionWaitQueue: Array<{ resolve: () => void; priority: boolean }> = []
/**
* Aborted whenever {@link interruptBackgroundQueries} runs. Default {@link query} runs listen until close so
@ -281,23 +281,38 @@ export class QueryService { @@ -281,23 +281,38 @@ export class QueryService {
this.backgroundInterruptController = new AbortController()
}
async acquireGlobalRelayConnectionSlot(): Promise<void> {
async acquireGlobalRelayConnectionSlot(opts?: { priority?: boolean }): Promise<void> {
const priority = opts?.priority === true
if (this.globalRelayConnectionSlotsInUse < MAX_CONCURRENT_RELAY_CONNECTIONS) {
this.globalRelayConnectionSlotsInUse++
return
}
await new Promise<void>((resolve) => {
this.globalRelayConnectionWaitQueue.push(() => {
this.globalRelayConnectionSlotsInUse++
resolve()
})
const entry = { resolve, priority }
if (priority) {
this.globalRelayConnectionWaitQueue.unshift(entry)
} else {
this.globalRelayConnectionWaitQueue.push(entry)
}
})
}
releaseGlobalRelayConnectionSlot(): void {
this.globalRelayConnectionSlotsInUse = Math.max(0, this.globalRelayConnectionSlotsInUse - 1)
const next = this.globalRelayConnectionWaitQueue.shift()
if (next) next()
const pickNext = (): (() => void) | undefined => {
const priIdx = this.globalRelayConnectionWaitQueue.findIndex((e) => e.priority)
if (priIdx >= 0) {
const [entry] = this.globalRelayConnectionWaitQueue.splice(priIdx, 1)
return entry!.resolve
}
const entry = this.globalRelayConnectionWaitQueue.shift()
return entry?.resolve
}
const next = pickNext()
if (next) {
this.globalRelayConnectionSlotsInUse++
next()
}
}
constructor(pool: SimplePool, relaySession?: QueryServiceRelaySessionOptions) {

33
src/services/client.service.ts

@ -137,6 +137,7 @@ import { @@ -137,6 +137,7 @@ import {
stripLocalNetworkRelaysFromRelayList,
stripMailboxLocalUrlsForRemoteViewers,
syntheticOriginalRelaysFromReadWrite,
stripLocalNetworkRelaysForWssReq,
urlIsNonLocalForRemoteViewer
} from '@/lib/relay-list-sanitize'
import {
@ -2422,20 +2423,25 @@ class ClientService extends EventTarget { @@ -2422,20 +2423,25 @@ class ClientService extends EventTarget {
oneose,
onclose,
startLogin,
onAllClose
onAllClose,
connectionSlotPriority
}: {
onevent?: (evt: NEvent) => void
oneose?: (eosed: boolean) => void
onclose?: (url: string, reason: string) => void
startLogin?: () => void
onAllClose?: (reasons: string[]) => void
/** Jump the global connection queue (single-relay authoritative timelines). */
connectionSlotPriority?: boolean
},
relayReqLog?: { groupId?: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void }
) {
const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
// While offline, silently drop every non-local relay so nothing is added to groupedRequests.
if (!navigator.onLine) {
if (navigator.onLine) {
relays = stripLocalNetworkRelaysForWssReq(relays)
} else {
// While offline, silently drop every non-local relay so nothing is added to groupedRequests.
relays = relays.filter((url) => isLocalNetworkUrl(url))
}
const filters = sanitizeSubscribeFiltersBeforeReq(filter)
@ -2605,9 +2611,12 @@ class ClientService extends EventTarget { @@ -2605,9 +2611,12 @@ class ClientService extends EventTarget {
/** Ignore a follow-up `closed by caller` while NIP-42 auth + resubscribe is in flight (parent `close()` must not finalize the batch early). */
const nip42ResubscribePending = new Set<number>()
const nip42HasAuthedOnce = new Set<number>()
const slotPriority = connectionSlotPriority === true
const allOpened = Promise.all(
groupedRequests.map(async ({ url, filters: relayFilters }, i) => {
await that.queryService.acquireGlobalRelayConnectionSlot()
await that.queryService.acquireGlobalRelayConnectionSlot(
slotPriority ? { priority: true } : undefined
)
try {
const relayKey = normalizeUrl(url) || url
await that.queryService.acquireSubSlot(relayKey)
@ -2657,7 +2666,9 @@ class ClientService extends EventTarget { @@ -2657,7 +2666,9 @@ class ClientService extends EventTarget {
})
.then(async () => {
nip42HasAuthedOnce.add(i)
await that.queryService.acquireGlobalRelayConnectionSlot()
await that.queryService.acquireGlobalRelayConnectionSlot(
slotPriority ? { priority: true } : undefined
)
try {
await that.queryService.acquireSubSlot(relayKey)
// After AUTH the socket may be closed or the relay dropped from the pool;
@ -2816,9 +2827,11 @@ class ClientService extends EventTarget { @@ -2816,9 +2827,11 @@ class ClientService extends EventTarget {
} = {}
) {
let relays = Array.from(new Set(urls))
// While offline, strip non-local relays before any further processing so the
// capital-letter-tag fallback below cannot re-introduce internet relays.
if (!navigator.onLine) {
if (navigator.onLine) {
relays = stripLocalNetworkRelaysForWssReq(relays)
} else {
// While offline, strip non-local relays before any further processing so the
// capital-letter-tag fallback below cannot re-introduce internet relays.
relays = relays.filter((url) => isLocalNetworkUrl(url))
}
if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) {
@ -3124,7 +3137,9 @@ class ClientService extends EventTarget { @@ -3124,7 +3137,9 @@ class ClientService extends EventTarget {
applySubscribedTimelineEvent(evt)
},
oneose: httpOnlyShard ? undefined : handleTimelineEose,
onclose: onClose
onclose: onClose,
connectionSlotPriority:
relayAuthoritativeTimeline && wsRelays.length === 1 && navigator.onLine
},
httpOnlyShard ? undefined : relayReqLog)

Loading…
Cancel
Save