Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
cf629ee7a2
  1. 3
      src/PageManager.tsx
  2. 60
      src/components/NoteStats/ZapButton.tsx
  3. 7
      src/constants.ts
  4. 14
      src/lib/profile-batch-coordinator.ts
  5. 7
      src/providers/ThreadProfileBatchProvider.tsx
  6. 58
      src/services/client-replaceable-events.service.ts

3
src/PageManager.tsx

@ -14,6 +14,8 @@ import { NavigationService } from '@/services/navigation.service' @@ -14,6 +14,8 @@ import { NavigationService } from '@/services/navigation.service'
import { ImwaldBrandBar } from '@/assets/Logo'
import LiveActivitiesStrip from '@/components/LiveActivitiesStrip'
import NoteDrawer from '@/components/NoteDrawer'
import { PROFILE_SECONDARY_PANEL_DEFER_MS } from '@/constants'
import { extendProfileNetworkDeferral } from '@/lib/profile-batch-coordinator'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import { navigationEventStore } from '@/services/navigation-event-store'
@ -2172,6 +2174,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2172,6 +2174,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
useLayoutEffect(() => {
noteStatsService.setBackgroundStatsPaused(primaryFrozen)
if (primaryFrozen) {
extendProfileNetworkDeferral(PROFILE_SECONDARY_PANEL_DEFER_MS)
client.interruptBackgroundQueries()
}
}, [primaryFrozen])

60
src/components/NoteStats/ZapButton.tsx

@ -41,15 +41,49 @@ function formatAmount(amount: number) { @@ -41,15 +41,49 @@ function formatAmount(amount: number) {
return `${Math.round(amount / 100000) / 10}M`
}
type ZapRecipientResolveResult = {
profile: TProfile | null
profileEvent: Event | undefined
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null
}
const zapRecipientResolveByPubkey = new Map<string, Promise<ZapRecipientResolveResult>>()
function feedProfileRowSyncKey(
profile: TProfile | undefined | null,
pendingInFeed: boolean
): string {
if (!profile) return pendingInFeed ? 'p:wait' : 'p:none'
return [
profile.batchPlaceholder ? 'ph' : 'ok',
profile.username ?? '',
profile.avatar ?? '',
profile.npub ?? ''
].join('\x1e')
}
/** Avoid one metadata + payment REQ per visible note while feed profile batch runs. */
async function resolveZapRecipientData(
authorPubkey: string,
feedProfile: TProfile | undefined | null
): Promise<{
profile: TProfile | null
profileEvent: Event | undefined
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null
}> {
): Promise<ZapRecipientResolveResult> {
const pk = authorPubkey.toLowerCase()
const inFlight = zapRecipientResolveByPubkey.get(pk)
if (inFlight) return inFlight
const run = resolveZapRecipientDataBody(pk, feedProfile).finally(() => {
if (zapRecipientResolveByPubkey.get(pk) === run) {
zapRecipientResolveByPubkey.delete(pk)
}
})
zapRecipientResolveByPubkey.set(pk, run)
return run
}
async function resolveZapRecipientDataBody(
authorPubkey: string,
feedProfile: TProfile | undefined | null
): Promise<ZapRecipientResolveResult> {
const cachedFeed =
feedProfile && !feedProfile.batchPlaceholder ? feedProfile : null
const deferNetwork = shouldDeferPerPubkeyProfileNetwork(authorPubkey)
@ -114,6 +148,10 @@ function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapBut @@ -114,6 +148,10 @@ function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapBut
const feedProfile = feedProfiles?.profiles.get(authorPubkey)
const feedProfileRef = useRef(feedProfile)
feedProfileRef.current = feedProfile
const feedProfileSyncKey = feedProfileRowSyncKey(
feedProfile,
Boolean(feedProfiles?.pendingPubkeys.has(authorPubkey))
)
const [disable, setDisable] = useState(true)
const [tipPaymentData, setTipPaymentData] = useState<RecipientZapPaymentData | null>(null)
@ -144,7 +182,7 @@ function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapBut @@ -144,7 +182,7 @@ function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapBut
if (isSelf) return
if (!feedProfile || feedProfile.batchPlaceholder) return
applyTipAvailability(feedProfile, null, null)
}, [isSelf, feedProfile, feedProfiles?.version, applyTipAvailability])
}, [isSelf, feedProfile, feedProfileSyncKey, applyTipAvailability])
useEffect(() => {
if (isSelf) {
@ -167,7 +205,7 @@ function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapBut @@ -167,7 +205,7 @@ function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapBut
return () => {
cancelled = true
}
}, [authorPubkey, isSelf, feedProfiles?.version, applyTipAvailability])
}, [authorPubkey, isSelf, feedProfileSyncKey, applyTipAvailability])
const handleOpenPaymentMethods = (e: React.MouseEvent) => {
e.stopPropagation()
@ -248,6 +286,10 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -248,6 +286,10 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
const feedProfile = feedProfiles?.profiles.get(authorPubkey)
const feedProfileRef = useRef(feedProfile)
feedProfileRef.current = feedProfile
const feedProfileSyncKey = feedProfileRowSyncKey(
feedProfile,
Boolean(feedProfiles?.pendingPubkeys.has(authorPubkey))
)
const [disable, setDisable] = useState(true)
const [canLightningZap, setCanLightningZap] = useState(false)
@ -289,7 +331,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -289,7 +331,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
if (isSelf) return
if (!feedProfile || feedProfile.batchPlaceholder) return
applyTipAvailability(feedProfile, null, null, true)
}, [isSelf, feedProfile, feedProfiles?.version, applyTipAvailability])
}, [isSelf, feedProfile, feedProfileSyncKey, applyTipAvailability])
useEffect(() => {
if (isSelf) {
@ -314,7 +356,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB @@ -314,7 +356,7 @@ export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapB
return () => {
cancelled = true
}
}, [authorPubkey, isSelf, feedProfiles?.version, applyTipAvailability])
}, [authorPubkey, isSelf, feedProfileSyncKey, applyTipAvailability])
const handleZap = async () => {
try {

7
src/constants.ts

@ -269,6 +269,13 @@ export const PROFILE_BATCH_NETWORK_LOAD_TIMEOUT_MS = 12_000 @@ -269,6 +269,13 @@ export const PROFILE_BATCH_NETWORK_LOAD_TIMEOUT_MS = 12_000
*/
export const PROFILE_BATCH_POST_COOLDOWN_MS = 90_000
/**
* While a note (or other heavy secondary panel) is open, block per-row metadata/payment relay REQs
* on the feed behind it {@link ZapButton} must not re-fetch every author when thread batch bumps
* {@link NoteFeedProfileContext.version}.
*/
export const PROFILE_SECONDARY_PANEL_DEFER_MS = 120_000
/**
* Hex-id / replaceable-coordinate note lookup ({@link EventService.tryHarderToFetchEvent}, big-relays dataloader).
*/

14
src/lib/profile-batch-coordinator.ts

@ -8,6 +8,8 @@ import { PROFILE_BATCH_POST_COOLDOWN_MS } from '@/constants' @@ -8,6 +8,8 @@ import { PROFILE_BATCH_POST_COOLDOWN_MS } from '@/constants'
const awaitingBatch = new Set<string>()
const batchCooldownUntil = new Map<string, number>()
/** Blocks all per-pubkey metadata/payment relay traffic (e.g. while a note panel is open). */
let globalProfileNetworkDeferUntil = 0
function norm(pk: string): string {
return pk.trim().toLowerCase()
@ -47,8 +49,18 @@ export function isPubkeyInProfileBatchCooldown(pubkey: string): boolean { @@ -47,8 +49,18 @@ export function isPubkeyInProfileBatchCooldown(pubkey: string): boolean {
return true
}
/** True while batch is in flight or during the post-batch cooldown window. */
/** Extend the global defer window (e.g. opening a note panel — avoids re-storming the whole feed). */
export function extendProfileNetworkDeferral(durationMs: number): void {
if (durationMs <= 0) return
const until = Date.now() + durationMs
if (until > globalProfileNetworkDeferUntil) {
globalProfileNetworkDeferUntil = until
}
}
/** True while batch is in flight, post-batch cooldown, or a global defer window is active. */
export function shouldDeferPerPubkeyProfileNetwork(pubkey: string): boolean {
if (Date.now() < globalProfileNetworkDeferUntil) return true
return isPubkeyAwaitingProfileBatch(pubkey) || isPubkeyInProfileBatchCooldown(pubkey)
}

7
src/providers/ThreadProfileBatchProvider.tsx

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
import { PROFILE_SECONDARY_PANEL_DEFER_MS } from '@/constants'
import client from '@/services/client.service'
import { collectProfilePubkeysFromEvents } from '@/lib/profile-batch-coordinator'
import {
collectProfilePubkeysFromEvents,
extendProfileNetworkDeferral
} from '@/lib/profile-batch-coordinator'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import {
NoteFeedProfileContext,
@ -105,6 +109,7 @@ export function ThreadProfileBatchProvider({ @@ -105,6 +109,7 @@ export function ThreadProfileBatchProvider({
)
useLayoutEffect(() => {
extendProfileNetworkDeferral(PROFILE_SECONDARY_PANEL_DEFER_MS)
genRef.current += 1
const gen = genRef.current
loadedRef.current.clear()

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

@ -132,6 +132,9 @@ export class ReplaceableEventService { @@ -132,6 +132,9 @@ export class ReplaceableEventService {
private authorReplaceablesRefreshByPubkey = new Map<string, Promise<void>>()
/** Per-author cooldown after a successful profile-view replaceable sweep (avoids reopen loops). */
private authorProfileViewRefreshNotBeforeMs = new Map<string, number>()
/** Coalesce IDB-hit background refreshes so feed paint does not open one REQ per row per 100ms window. */
private backgroundRefreshByKey = new Map<string, { pubkey: string; kind: number; d?: string }>()
private backgroundRefreshFlushTimer: ReturnType<typeof setTimeout> | null = null
private replaceableEventFromBigRelaysDataloader: DataLoader<
{ pubkey: string; kind: number },
NEvent | null,
@ -258,7 +261,7 @@ export class ReplaceableEventService { @@ -258,7 +261,7 @@ export class ReplaceableEventService {
this.replaceableEventFromBigRelaysDataloader.prime({ pubkey, kind }, Promise.resolve(pick))
void indexedDb.putReplaceableEvent(pick).catch(() => {})
if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) {
void this.refreshInBackground(pubkey, kind, d).catch(() => {})
this.refreshInBackground(pubkey, kind, d)
}
return pick
}
@ -273,7 +276,7 @@ export class ReplaceableEventService { @@ -273,7 +276,7 @@ export class ReplaceableEventService {
const indexedDbCached = await indexedDb.getReplaceableEvent(pubkey, kind, d)
if (indexedDbCached) {
// Refresh in background
this.refreshInBackground(pubkey, kind, d).catch(() => {})
this.refreshInBackground(pubkey, kind, d)
return indexedDbCached
}
} catch (error) {
@ -340,18 +343,49 @@ export class ReplaceableEventService { @@ -340,18 +343,49 @@ export class ReplaceableEventService {
}
/**
* Refresh event in background (non-blocking)
* Refresh event in background (non-blocking). Batched via {@link flushScheduledBackgroundRefresh}.
*/
private async refreshInBackground(pubkey: string, kind: number, d?: string): Promise<void> {
private refreshInBackground(pubkey: string, kind: number, d?: string): void {
if (shouldDeferPerPubkeyProfileNetwork(pubkey)) return
const key = d ? `${pubkey}:${kind}:${d}` : `${pubkey}:${kind}`
if (!this.backgroundRefreshByKey.has(key)) {
this.backgroundRefreshByKey.set(key, { pubkey, kind, d })
}
if (this.backgroundRefreshFlushTimer != null) return
this.backgroundRefreshFlushTimer = setTimeout(() => {
this.backgroundRefreshFlushTimer = null
void this.flushScheduledBackgroundRefresh()
}, 250)
}
private async flushScheduledBackgroundRefresh(): Promise<void> {
const entries = [...this.backgroundRefreshByKey.values()]
this.backgroundRefreshByKey.clear()
if (entries.length === 0) return
const withoutD = entries.filter((e) => !e.d)
const withD = entries.filter((e) => e.d)
if (withoutD.length > 0) {
const eligible = withoutD.filter((e) => !shouldDeferPerPubkeyProfileNetwork(e.pubkey))
if (eligible.length > 0) {
try {
if (d) {
await this.replaceableEventDataLoader.load({ pubkey, kind, d })
} else {
await this.replaceableEventFromBigRelaysDataloader.load({ pubkey, kind })
await this.replaceableEventFromBigRelaysDataloader.loadMany(
eligible.map((e) => ({ pubkey: e.pubkey, kind: e.kind }))
)
} catch {
/* ignore */
}
}
}
for (const { pubkey, kind, d } of withD) {
if (shouldDeferPerPubkeyProfileNetwork(pubkey)) continue
try {
await this.replaceableEventDataLoader.load({ pubkey, kind, d })
} catch {
// Ignore errors in background refresh
/* ignore */
}
}
}
@ -523,7 +557,7 @@ export class ReplaceableEventService { @@ -523,7 +557,7 @@ export class ReplaceableEventService {
if (event && event !== null) {
results[index] = event
eventsMap.set(`${pubkey}:${kind}`, event)
this.refreshInBackground(pubkey, kind).catch(() => {})
this.refreshInBackground(pubkey, kind)
} else {
missingParams.push({ pubkey, kind, index })
}
@ -1000,7 +1034,7 @@ export class ReplaceableEventService { @@ -1000,7 +1034,7 @@ export class ReplaceableEventService {
await this.indexProfile(sessionEv)
void indexedDb.putReplaceableEvent(sessionEv).catch(() => {})
if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) {
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {})
this.refreshInBackground(pubkey, kinds.Metadata)
}
return sessionEv
}
@ -1013,7 +1047,7 @@ export class ReplaceableEventService { @@ -1013,7 +1047,7 @@ export class ReplaceableEventService {
)
await this.indexProfile(idbEv)
if (!shouldDeferPerPubkeyProfileNetwork(pubkey)) {
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {})
this.refreshInBackground(pubkey, kinds.Metadata)
}
return idbEv
}

Loading…
Cancel
Save