From 7f99112e7a9c68a46e7fecaaed6c4bd3ad0228fe Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 21 May 2026 21:41:57 +0200 Subject: [PATCH] bug-fixes --- src/PageManager.tsx | 4 ++ src/components/NoteStats/ZapButton.tsx | 14 ++--- src/components/ProfileOptions/index.tsx | 4 +- src/constants.ts | 2 +- .../secondary/ProfileEditorPage/index.tsx | 2 +- .../client-replaceable-events.service.ts | 55 ++++++++++++++----- src/services/client.service.ts | 8 ++- 7 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 641465cc..c2f8e831 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -69,6 +69,8 @@ import { SecondaryPageContext, useSecondaryPage, useSecondaryPageOptional } from /** Survives React StrictMode remount so initial URL → secondary stack is not built twice. */ let historyLocationSeedApplied = false +/** Dedupes note URL seed when React runs the history effect twice before state commits. */ +let historyNoteStackSeedUrl: string | null = null /** Lazy-loaded so PageManager does not synchronously import SpellsPage (avoids HMR cycle: SpellsPage → PrimaryPageLayout → PageManager → SpellsPage). */ const SpellsPageLazy = lazy(() => import('./pages/primary/SpellsPage')) @@ -1342,6 +1344,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { let primaryForNoteUrl: TPrimaryPageName = currentPrimaryPage const pushNoteUrlOnStack = (noteUrl: string) => { + if (historyNoteStackSeedUrl === noteUrl) return + historyNoteStackSeedUrl = noteUrl setSecondaryStack((prevStack) => { if (isCurrentPage(prevStack, noteUrl)) return prevStack const { newStack, newItem } = pushNewPageToStack(prevStack, noteUrl, maxStackSize) diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx index 863df51f..652861f2 100644 --- a/src/components/NoteStats/ZapButton.tsx +++ b/src/components/NoteStats/ZapButton.tsx @@ -54,12 +54,10 @@ async function resolveZapRecipientData( feedProfile && !feedProfile.batchPlaceholder ? feedProfile : null const deferNetwork = shouldDeferPerPubkeyProfileNetwork(authorPubkey) - const paymentPromise = deferNetwork - ? replaceableEventService.getPaymentInfoFromIndexedDB(authorPubkey) - : client.fetchPaymentInfoEvent(authorPubkey) - if (cachedFeed) { - const paymentEvent = await paymentPromise.catch(() => undefined) + const paymentEvent = deferNetwork + ? await replaceableEventService.getPaymentInfoFromIndexedDB(authorPubkey).catch(() => undefined) + : await client.fetchPaymentInfoEvent(authorPubkey).catch(() => undefined) return { profile: cachedFeed, profileEvent: undefined, @@ -70,7 +68,9 @@ async function resolveZapRecipientData( const idbProfile = await replaceableEventService.getProfileFromIndexedDB(authorPubkey) if (deferNetwork) { - const paymentEvent = await paymentPromise.catch(() => undefined) + const paymentEvent = await replaceableEventService + .getPaymentInfoFromIndexedDB(authorPubkey) + .catch(() => undefined) return { profile: idbProfile ?? null, profileEvent: undefined, @@ -80,7 +80,7 @@ async function resolveZapRecipientData( const [profileRes, paymentRes] = await Promise.allSettled([ replaceableEventService.fetchReplaceableEvent(authorPubkey, kinds.Metadata), - paymentPromise + client.fetchPaymentInfoEvent(authorPubkey) ]) const profileEvent = profileRes.status === 'fulfilled' ? profileRes.value : undefined diff --git a/src/components/ProfileOptions/index.tsx b/src/components/ProfileOptions/index.tsx index dafb4490..11e4d751 100644 --- a/src/components/ProfileOptions/index.tsx +++ b/src/components/ProfileOptions/index.tsx @@ -84,7 +84,9 @@ export default function ProfileOptions({ const fetchEvent = async () => { try { // Use fetchProfileEvent which includes comprehensive relay search - const event = await replaceableEventService.fetchProfileEvent(pubkey, false) + const event = await replaceableEventService.fetchProfileEvent(pubkey, false, { + allowWideRelayFallback: true + }) if (event) { setLocalProfileEvent(event) } diff --git a/src/constants.ts b/src/constants.ts index 42ceb00d..da632457 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -267,7 +267,7 @@ export const PROFILE_BATCH_NETWORK_LOAD_TIMEOUT_MS = 12_000 * After a feed/thread profile batch finishes, block per-row metadata/payment relay REQs so * {@link ZapButton} and {@link useFetchProfile} do not fan out hundreds of parallel queries. */ -export const PROFILE_BATCH_POST_COOLDOWN_MS = 45_000 +export const PROFILE_BATCH_POST_COOLDOWN_MS = 90_000 /** * Hex-id / replaceable-coordinate note lookup ({@link EventService.tryHarderToFetchEvent}, big-relays dataloader). diff --git a/src/pages/secondary/ProfileEditorPage/index.tsx b/src/pages/secondary/ProfileEditorPage/index.tsx index e8db6eff..366ac4cb 100644 --- a/src/pages/secondary/ProfileEditorPage/index.tsx +++ b/src/pages/secondary/ProfileEditorPage/index.tsx @@ -297,7 +297,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { await syncUserDeletionTombstones(account.pubkey, relayList) await client.forceRefreshProfileAndPaymentInfoCache(account.pubkey) const [profileEvt, paymentEvt] = await Promise.all([ - client.fetchProfileEvent(account.pubkey), + client.fetchProfileEvent(account.pubkey, false, { allowWideRelayFallback: true }), client.fetchPaymentInfoEvent(account.pubkey) ]) if (profileEvt) { diff --git a/src/services/client-replaceable-events.service.ts b/src/services/client-replaceable-events.service.ts index 0a379545..929684ef 100644 --- a/src/services/client-replaceable-events.service.ts +++ b/src/services/client-replaceable-events.service.ts @@ -41,6 +41,14 @@ import { import { isPromiseTimeoutError, racePromiseWithTimeout } from '@/lib/async-timeout' import { networkKindsForReplaceableFetch } from '@/lib/replaceable-fetch-kinds' +export type FetchProfileEventOptions = { + /** + * When false (default), stop after batched profile relays / DataLoader — no per-author + * NIP-65 + expanded-relay REQ (avoids hundreds of parallel 7-relay queries on feed paint). + */ + allowWideRelayFallback?: boolean +} + export class ReplaceableEventService { /** Limits parallel {@link fetchKind0FromProfileRelays} (7-relay REQ per author). */ private static kind0ProfileRelaySlotsInUse = 0 @@ -249,7 +257,9 @@ export class ReplaceableEventService { if (pick) { this.replaceableEventFromBigRelaysDataloader.prime({ pubkey, kind }, Promise.resolve(pick)) void indexedDb.putReplaceableEvent(pick).catch(() => {}) - void this.refreshInBackground(pubkey, kind, d).catch(() => {}) + if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) { + void this.refreshInBackground(pubkey, kind, d).catch(() => {}) + } return pick } } @@ -333,6 +343,7 @@ export class ReplaceableEventService { * Refresh event in background (non-blocking) */ private async refreshInBackground(pubkey: string, kind: number, d?: string): Promise { + if (shouldDeferPerPubkeyProfileNetwork(pubkey)) return try { if (d) { await this.replaceableEventDataLoader.load({ pubkey, kind, d }) @@ -943,7 +954,12 @@ export class ReplaceableEventService { /** * Fetch profile event by id (hex, npub, nprofile) */ - async fetchProfileEvent(id: string, _skipCache: boolean = false): Promise { + async fetchProfileEvent( + id: string, + _skipCache: boolean = false, + options: FetchProfileEventOptions = {} + ): Promise { + const allowWideRelayFallback = options.allowWideRelayFallback === true let pubkey: string | undefined let relays: string[] = [] if (/^[0-9a-f]{64}$/.test(id)) { @@ -983,7 +999,9 @@ export class ReplaceableEventService { ) await this.indexProfile(sessionEv) void indexedDb.putReplaceableEvent(sessionEv).catch(() => {}) - void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) + if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) { + void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) + } return sessionEv } try { @@ -994,7 +1012,9 @@ export class ReplaceableEventService { Promise.resolve(idbEv) ) await this.indexProfile(idbEv) - void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) + if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) { + void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) + } return idbEv } } catch { @@ -1015,19 +1035,20 @@ export class ReplaceableEventService { return sessionFallback } - // Relay hints from bech32 (nprofile, etc.) — highest priority in later steps + // Relay hints from bech32 (nprofile, etc.) — highest priority in wide fallback only const relayHints = relays.length > 0 ? [...relays] : [] - // Step 0: {@link PROFILE_RELAY_URLS} by `authors` — after local caches miss. - const fromProfileRelays = await this.fetchKind0FromProfileRelays(pubkey) - if (fromProfileRelays) { - this.replaceableEventFromBigRelaysDataloader.prime( - { pubkey, kind: kinds.Metadata }, - Promise.resolve(fromProfileRelays) - ) - await this.indexProfile(fromProfileRelays) - void indexedDb.putReplaceableEvent(fromProfileRelays).catch(() => {}) - return fromProfileRelays + if (allowWideRelayFallback) { + const fromProfileRelays = await this.fetchKind0FromProfileRelays(pubkey) + if (fromProfileRelays) { + this.replaceableEventFromBigRelaysDataloader.prime( + { pubkey, kind: kinds.Metadata }, + Promise.resolve(fromProfileRelays) + ) + await this.indexProfile(fromProfileRelays) + void indexedDb.putReplaceableEvent(fromProfileRelays).catch(() => {}) + return fromProfileRelays + } } // Step 1: DataLoader (IndexedDB + batched profile relay stack) @@ -1043,6 +1064,10 @@ export class ReplaceableEventService { return profileEvent } + if (!allowWideRelayFallback) { + return sessionFallback + } + await ReplaceableEventService.acquireProfileFallbackNetworkSlot() try { // Step 2: Only after cache + default relays miss — NIP-65 relay list (timeout-capped), then hints + outbox/inbox + defaults. diff --git a/src/services/client.service.ts b/src/services/client.service.ts index b86d8110..6e035b66 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -4105,8 +4105,12 @@ class ClientService extends EventTarget { } // Delegate to ReplaceableEventService - async fetchProfileEvent(id: string, skipCache: boolean = false): Promise { - return this.replaceableEventService.fetchProfileEvent(id, skipCache) + async fetchProfileEvent( + id: string, + skipCache: boolean = false, + options?: import('./client-replaceable-events.service').FetchProfileEventOptions + ): Promise { + return this.replaceableEventService.fetchProfileEvent(id, skipCache, options) } async fetchProfile(id: string, skipCache: boolean = false): Promise {