Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
7f99112e7a
  1. 4
      src/PageManager.tsx
  2. 14
      src/components/NoteStats/ZapButton.tsx
  3. 4
      src/components/ProfileOptions/index.tsx
  4. 2
      src/constants.ts
  5. 2
      src/pages/secondary/ProfileEditorPage/index.tsx
  6. 55
      src/services/client-replaceable-events.service.ts
  7. 8
      src/services/client.service.ts

4
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. */ /** Survives React StrictMode remount so initial URL → secondary stack is not built twice. */
let historyLocationSeedApplied = false 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). */ /** Lazy-loaded so PageManager does not synchronously import SpellsPage (avoids HMR cycle: SpellsPage → PrimaryPageLayout → PageManager → SpellsPage). */
const SpellsPageLazy = lazy(() => import('./pages/primary/SpellsPage')) const SpellsPageLazy = lazy(() => import('./pages/primary/SpellsPage'))
@ -1342,6 +1344,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
let primaryForNoteUrl: TPrimaryPageName = currentPrimaryPage let primaryForNoteUrl: TPrimaryPageName = currentPrimaryPage
const pushNoteUrlOnStack = (noteUrl: string) => { const pushNoteUrlOnStack = (noteUrl: string) => {
if (historyNoteStackSeedUrl === noteUrl) return
historyNoteStackSeedUrl = noteUrl
setSecondaryStack((prevStack) => { setSecondaryStack((prevStack) => {
if (isCurrentPage(prevStack, noteUrl)) return prevStack if (isCurrentPage(prevStack, noteUrl)) return prevStack
const { newStack, newItem } = pushNewPageToStack(prevStack, noteUrl, maxStackSize) const { newStack, newItem } = pushNewPageToStack(prevStack, noteUrl, maxStackSize)

14
src/components/NoteStats/ZapButton.tsx

@ -54,12 +54,10 @@ async function resolveZapRecipientData(
feedProfile && !feedProfile.batchPlaceholder ? feedProfile : null feedProfile && !feedProfile.batchPlaceholder ? feedProfile : null
const deferNetwork = shouldDeferPerPubkeyProfileNetwork(authorPubkey) const deferNetwork = shouldDeferPerPubkeyProfileNetwork(authorPubkey)
const paymentPromise = deferNetwork
? replaceableEventService.getPaymentInfoFromIndexedDB(authorPubkey)
: client.fetchPaymentInfoEvent(authorPubkey)
if (cachedFeed) { if (cachedFeed) {
const paymentEvent = await paymentPromise.catch(() => undefined) const paymentEvent = deferNetwork
? await replaceableEventService.getPaymentInfoFromIndexedDB(authorPubkey).catch(() => undefined)
: await client.fetchPaymentInfoEvent(authorPubkey).catch(() => undefined)
return { return {
profile: cachedFeed, profile: cachedFeed,
profileEvent: undefined, profileEvent: undefined,
@ -70,7 +68,9 @@ async function resolveZapRecipientData(
const idbProfile = await replaceableEventService.getProfileFromIndexedDB(authorPubkey) const idbProfile = await replaceableEventService.getProfileFromIndexedDB(authorPubkey)
if (deferNetwork) { if (deferNetwork) {
const paymentEvent = await paymentPromise.catch(() => undefined) const paymentEvent = await replaceableEventService
.getPaymentInfoFromIndexedDB(authorPubkey)
.catch(() => undefined)
return { return {
profile: idbProfile ?? null, profile: idbProfile ?? null,
profileEvent: undefined, profileEvent: undefined,
@ -80,7 +80,7 @@ async function resolveZapRecipientData(
const [profileRes, paymentRes] = await Promise.allSettled([ const [profileRes, paymentRes] = await Promise.allSettled([
replaceableEventService.fetchReplaceableEvent(authorPubkey, kinds.Metadata), replaceableEventService.fetchReplaceableEvent(authorPubkey, kinds.Metadata),
paymentPromise client.fetchPaymentInfoEvent(authorPubkey)
]) ])
const profileEvent = const profileEvent =
profileRes.status === 'fulfilled' ? profileRes.value : undefined profileRes.status === 'fulfilled' ? profileRes.value : undefined

4
src/components/ProfileOptions/index.tsx

@ -84,7 +84,9 @@ export default function ProfileOptions({
const fetchEvent = async () => { const fetchEvent = async () => {
try { try {
// Use fetchProfileEvent which includes comprehensive relay search // 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) { if (event) {
setLocalProfileEvent(event) setLocalProfileEvent(event)
} }

2
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 * 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. * {@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). * Hex-id / replaceable-coordinate note lookup ({@link EventService.tryHarderToFetchEvent}, big-relays dataloader).

2
src/pages/secondary/ProfileEditorPage/index.tsx

@ -297,7 +297,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
await syncUserDeletionTombstones(account.pubkey, relayList) await syncUserDeletionTombstones(account.pubkey, relayList)
await client.forceRefreshProfileAndPaymentInfoCache(account.pubkey) await client.forceRefreshProfileAndPaymentInfoCache(account.pubkey)
const [profileEvt, paymentEvt] = await Promise.all([ const [profileEvt, paymentEvt] = await Promise.all([
client.fetchProfileEvent(account.pubkey), client.fetchProfileEvent(account.pubkey, false, { allowWideRelayFallback: true }),
client.fetchPaymentInfoEvent(account.pubkey) client.fetchPaymentInfoEvent(account.pubkey)
]) ])
if (profileEvt) { if (profileEvt) {

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

@ -41,6 +41,14 @@ import {
import { isPromiseTimeoutError, racePromiseWithTimeout } from '@/lib/async-timeout' import { isPromiseTimeoutError, racePromiseWithTimeout } from '@/lib/async-timeout'
import { networkKindsForReplaceableFetch } from '@/lib/replaceable-fetch-kinds' 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 { export class ReplaceableEventService {
/** Limits parallel {@link fetchKind0FromProfileRelays} (7-relay REQ per author). */ /** Limits parallel {@link fetchKind0FromProfileRelays} (7-relay REQ per author). */
private static kind0ProfileRelaySlotsInUse = 0 private static kind0ProfileRelaySlotsInUse = 0
@ -249,7 +257,9 @@ export class ReplaceableEventService {
if (pick) { if (pick) {
this.replaceableEventFromBigRelaysDataloader.prime({ pubkey, kind }, Promise.resolve(pick)) this.replaceableEventFromBigRelaysDataloader.prime({ pubkey, kind }, Promise.resolve(pick))
void indexedDb.putReplaceableEvent(pick).catch(() => {}) void indexedDb.putReplaceableEvent(pick).catch(() => {})
void this.refreshInBackground(pubkey, kind, d).catch(() => {}) if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) {
void this.refreshInBackground(pubkey, kind, d).catch(() => {})
}
return pick return pick
} }
} }
@ -333,6 +343,7 @@ export class ReplaceableEventService {
* Refresh event in background (non-blocking) * Refresh event in background (non-blocking)
*/ */
private async refreshInBackground(pubkey: string, kind: number, d?: string): Promise<void> { private async refreshInBackground(pubkey: string, kind: number, d?: string): Promise<void> {
if (shouldDeferPerPubkeyProfileNetwork(pubkey)) return
try { try {
if (d) { if (d) {
await this.replaceableEventDataLoader.load({ pubkey, kind, d }) await this.replaceableEventDataLoader.load({ pubkey, kind, d })
@ -943,7 +954,12 @@ export class ReplaceableEventService {
/** /**
* Fetch profile event by id (hex, npub, nprofile) * Fetch profile event by id (hex, npub, nprofile)
*/ */
async fetchProfileEvent(id: string, _skipCache: boolean = false): Promise<NEvent | undefined> { async fetchProfileEvent(
id: string,
_skipCache: boolean = false,
options: FetchProfileEventOptions = {}
): Promise<NEvent | undefined> {
const allowWideRelayFallback = options.allowWideRelayFallback === true
let pubkey: string | undefined let pubkey: string | undefined
let relays: string[] = [] let relays: string[] = []
if (/^[0-9a-f]{64}$/.test(id)) { if (/^[0-9a-f]{64}$/.test(id)) {
@ -983,7 +999,9 @@ export class ReplaceableEventService {
) )
await this.indexProfile(sessionEv) await this.indexProfile(sessionEv)
void indexedDb.putReplaceableEvent(sessionEv).catch(() => {}) void indexedDb.putReplaceableEvent(sessionEv).catch(() => {})
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) {
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {})
}
return sessionEv return sessionEv
} }
try { try {
@ -994,7 +1012,9 @@ export class ReplaceableEventService {
Promise.resolve(idbEv) Promise.resolve(idbEv)
) )
await this.indexProfile(idbEv) await this.indexProfile(idbEv)
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) {
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {})
}
return idbEv return idbEv
} }
} catch { } catch {
@ -1015,19 +1035,20 @@ export class ReplaceableEventService {
return sessionFallback 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] : [] const relayHints = relays.length > 0 ? [...relays] : []
// Step 0: {@link PROFILE_RELAY_URLS} by `authors` — after local caches miss. if (allowWideRelayFallback) {
const fromProfileRelays = await this.fetchKind0FromProfileRelays(pubkey) const fromProfileRelays = await this.fetchKind0FromProfileRelays(pubkey)
if (fromProfileRelays) { if (fromProfileRelays) {
this.replaceableEventFromBigRelaysDataloader.prime( this.replaceableEventFromBigRelaysDataloader.prime(
{ pubkey, kind: kinds.Metadata }, { pubkey, kind: kinds.Metadata },
Promise.resolve(fromProfileRelays) Promise.resolve(fromProfileRelays)
) )
await this.indexProfile(fromProfileRelays) await this.indexProfile(fromProfileRelays)
void indexedDb.putReplaceableEvent(fromProfileRelays).catch(() => {}) void indexedDb.putReplaceableEvent(fromProfileRelays).catch(() => {})
return fromProfileRelays return fromProfileRelays
}
} }
// Step 1: DataLoader (IndexedDB + batched profile relay stack) // Step 1: DataLoader (IndexedDB + batched profile relay stack)
@ -1043,6 +1064,10 @@ export class ReplaceableEventService {
return profileEvent return profileEvent
} }
if (!allowWideRelayFallback) {
return sessionFallback
}
await ReplaceableEventService.acquireProfileFallbackNetworkSlot() await ReplaceableEventService.acquireProfileFallbackNetworkSlot()
try { try {
// Step 2: Only after cache + default relays miss — NIP-65 relay list (timeout-capped), then hints + outbox/inbox + defaults. // Step 2: Only after cache + default relays miss — NIP-65 relay list (timeout-capped), then hints + outbox/inbox + defaults.

8
src/services/client.service.ts

@ -4105,8 +4105,12 @@ class ClientService extends EventTarget {
} }
// Delegate to ReplaceableEventService // Delegate to ReplaceableEventService
async fetchProfileEvent(id: string, skipCache: boolean = false): Promise<NEvent | undefined> { async fetchProfileEvent(
return this.replaceableEventService.fetchProfileEvent(id, skipCache) id: string,
skipCache: boolean = false,
options?: import('./client-replaceable-events.service').FetchProfileEventOptions
): Promise<NEvent | undefined> {
return this.replaceableEventService.fetchProfileEvent(id, skipCache, options)
} }
async fetchProfile(id: string, skipCache: boolean = false): Promise<TProfile | undefined> { async fetchProfile(id: string, skipCache: boolean = false): Promise<TProfile | undefined> {

Loading…
Cancel
Save