|
|
|
@ -34,18 +34,48 @@ import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eli |
|
|
|
import { stripLocalNetworkRelaysForWssReq } from '@/lib/relay-list-sanitize' |
|
|
|
import { stripLocalNetworkRelaysForWssReq } from '@/lib/relay-list-sanitize' |
|
|
|
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' |
|
|
|
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter' |
|
|
|
import { |
|
|
|
import { |
|
|
|
isPubkeyAwaitingProfileBatch, |
|
|
|
|
|
|
|
registerProfileBatchPubkeys, |
|
|
|
registerProfileBatchPubkeys, |
|
|
|
|
|
|
|
shouldDeferPerPubkeyProfileNetwork, |
|
|
|
unregisterProfileBatchPubkeys |
|
|
|
unregisterProfileBatchPubkeys |
|
|
|
} from '@/lib/profile-batch-coordinator' |
|
|
|
} from '@/lib/profile-batch-coordinator' |
|
|
|
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 class ReplaceableEventService { |
|
|
|
export class ReplaceableEventService { |
|
|
|
|
|
|
|
/** Limits parallel {@link fetchKind0FromProfileRelays} (7-relay REQ per author). */ |
|
|
|
|
|
|
|
private static kind0ProfileRelaySlotsInUse = 0 |
|
|
|
|
|
|
|
private static kind0ProfileRelayWaitQueue: Array<() => void> = [] |
|
|
|
|
|
|
|
private static readonly MAX_CONCURRENT_KIND0_PROFILE_RELAY_REQS = 3 |
|
|
|
|
|
|
|
|
|
|
|
/** Limits parallel Step 2/3 profile network work (relay list + wide metadata REQ). */ |
|
|
|
/** Limits parallel Step 2/3 profile network work (relay list + wide metadata REQ). */ |
|
|
|
private static profileFallbackSlotsInUse = 0 |
|
|
|
private static profileFallbackSlotsInUse = 0 |
|
|
|
private static profileFallbackWaitQueue: Array<() => void> = [] |
|
|
|
private static profileFallbackWaitQueue: Array<() => void> = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static async acquireKind0ProfileRelaySlot(): Promise<void> { |
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
|
|
ReplaceableEventService.kind0ProfileRelaySlotsInUse < |
|
|
|
|
|
|
|
ReplaceableEventService.MAX_CONCURRENT_KIND0_PROFILE_RELAY_REQS |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
ReplaceableEventService.kind0ProfileRelaySlotsInUse++ |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
await new Promise<void>((resolve) => { |
|
|
|
|
|
|
|
ReplaceableEventService.kind0ProfileRelayWaitQueue.push(() => { |
|
|
|
|
|
|
|
ReplaceableEventService.kind0ProfileRelaySlotsInUse++ |
|
|
|
|
|
|
|
resolve() |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static releaseKind0ProfileRelaySlot(): void { |
|
|
|
|
|
|
|
ReplaceableEventService.kind0ProfileRelaySlotsInUse = Math.max( |
|
|
|
|
|
|
|
0, |
|
|
|
|
|
|
|
ReplaceableEventService.kind0ProfileRelaySlotsInUse - 1 |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
const next = ReplaceableEventService.kind0ProfileRelayWaitQueue.shift() |
|
|
|
|
|
|
|
if (next) next() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static async acquireProfileFallbackNetworkSlot(): Promise<void> { |
|
|
|
private static async acquireProfileFallbackNetworkSlot(): Promise<void> { |
|
|
|
if (ReplaceableEventService.profileFallbackSlotsInUse < MAX_CONCURRENT_RELAY_CONNECTIONS) { |
|
|
|
if (ReplaceableEventService.profileFallbackSlotsInUse < MAX_CONCURRENT_RELAY_CONNECTIONS) { |
|
|
|
ReplaceableEventService.profileFallbackSlotsInUse++ |
|
|
|
ReplaceableEventService.profileFallbackSlotsInUse++ |
|
|
|
@ -867,7 +897,10 @@ export class ReplaceableEventService { |
|
|
|
private async fetchKind0FromProfileRelays(pubkey: string): Promise<NEvent | undefined> { |
|
|
|
private async fetchKind0FromProfileRelays(pubkey: string): Promise<NEvent | undefined> { |
|
|
|
const pk = pubkey.trim().toLowerCase() |
|
|
|
const pk = pubkey.trim().toLowerCase() |
|
|
|
if (!/^[0-9a-f]{64}$/.test(pk)) return undefined |
|
|
|
if (!/^[0-9a-f]{64}$/.test(pk)) return undefined |
|
|
|
|
|
|
|
if (shouldDeferPerPubkeyProfileNetwork(pk)) return undefined |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await ReplaceableEventService.acquireKind0ProfileRelaySlot() |
|
|
|
|
|
|
|
try { |
|
|
|
const relays = prependAggrNostrLandIfViewerEligible( |
|
|
|
const relays = prependAggrNostrLandIfViewerEligible( |
|
|
|
stripLocalNetworkRelaysForWssReq( |
|
|
|
stripLocalNetworkRelaysForWssReq( |
|
|
|
Array.from( |
|
|
|
Array.from( |
|
|
|
@ -902,6 +935,9 @@ export class ReplaceableEventService { |
|
|
|
}) |
|
|
|
}) |
|
|
|
return undefined |
|
|
|
return undefined |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} finally { |
|
|
|
|
|
|
|
ReplaceableEventService.releaseKind0ProfileRelaySlot() |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
@ -937,8 +973,7 @@ export class ReplaceableEventService { |
|
|
|
throw new Error('Invalid id') |
|
|
|
throw new Error('Invalid id') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Used only when relay steps miss — UI should already show this from {@link useFetchProfile} IDB/session first. */ |
|
|
|
// Local-first: session LRU, then IndexedDB — before any PROFILE_RELAY / DataLoader / wide relay pass.
|
|
|
|
let sessionFallback: NEvent | undefined |
|
|
|
|
|
|
|
if (!_skipCache) { |
|
|
|
if (!_skipCache) { |
|
|
|
const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey) |
|
|
|
const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey) |
|
|
|
if (sessionEv && !shouldDropEventOnIngest(sessionEv)) { |
|
|
|
if (sessionEv && !shouldDropEventOnIngest(sessionEv)) { |
|
|
|
@ -948,14 +983,42 @@ 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(() => {}) |
|
|
|
|
|
|
|
return sessionEv |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const idbEv = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata) |
|
|
|
|
|
|
|
if (idbEv && !shouldDropEventOnIngest(idbEv)) { |
|
|
|
|
|
|
|
this.replaceableEventFromBigRelaysDataloader.prime( |
|
|
|
|
|
|
|
{ pubkey, kind: kinds.Metadata }, |
|
|
|
|
|
|
|
Promise.resolve(idbEv) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
await this.indexProfile(idbEv) |
|
|
|
|
|
|
|
void this.refreshInBackground(pubkey, kinds.Metadata).catch(() => {}) |
|
|
|
|
|
|
|
return idbEv |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
/* ignore IDB read errors — fall through to network */ |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** When batch or wide relay steps miss, return session row if we had one earlier in the call. */ |
|
|
|
|
|
|
|
let sessionFallback: NEvent | undefined |
|
|
|
|
|
|
|
if (!_skipCache) { |
|
|
|
|
|
|
|
const sessionEv = client.eventService.getSessionMetadataForPubkey(pubkey) |
|
|
|
|
|
|
|
if (sessionEv && !shouldDropEventOnIngest(sessionEv)) { |
|
|
|
sessionFallback = sessionEv |
|
|
|
sessionFallback = sessionEv |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldDeferPerPubkeyProfileNetwork(pubkey)) { |
|
|
|
|
|
|
|
return sessionFallback |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Relay hints from bech32 (nprofile, etc.) — highest priority in later steps
|
|
|
|
// Relay hints from bech32 (nprofile, etc.) — highest priority in later steps
|
|
|
|
const relayHints = relays.length > 0 ? [...relays] : [] |
|
|
|
const relayHints = relays.length > 0 ? [...relays] : [] |
|
|
|
|
|
|
|
|
|
|
|
// Step 0: {@link PROFILE_RELAY_URLS} by `authors` — reliable for npub/hex; avoids batched DataLoader + abort races.
|
|
|
|
// Step 0: {@link PROFILE_RELAY_URLS} by `authors` — after local caches miss.
|
|
|
|
const fromProfileRelays = await this.fetchKind0FromProfileRelays(pubkey) |
|
|
|
const fromProfileRelays = await this.fetchKind0FromProfileRelays(pubkey) |
|
|
|
if (fromProfileRelays) { |
|
|
|
if (fromProfileRelays) { |
|
|
|
this.replaceableEventFromBigRelaysDataloader.prime( |
|
|
|
this.replaceableEventFromBigRelaysDataloader.prime( |
|
|
|
@ -980,10 +1043,6 @@ export class ReplaceableEventService { |
|
|
|
return profileEvent |
|
|
|
return profileEvent |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (isPubkeyAwaitingProfileBatch(pubkey)) { |
|
|
|
|
|
|
|
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.
|
|
|
|
@ -1383,7 +1442,20 @@ export class ReplaceableEventService { |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Fetch payment info event |
|
|
|
* Fetch payment info event |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
async getPaymentInfoFromIndexedDB(pubkey: string): Promise<NEvent | undefined> { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const row = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO) |
|
|
|
|
|
|
|
if (!row || row === null) return undefined |
|
|
|
|
|
|
|
return row as NEvent |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return undefined |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async fetchPaymentInfoEvent(pubkey: string): Promise<NEvent | undefined> { |
|
|
|
async fetchPaymentInfoEvent(pubkey: string): Promise<NEvent | undefined> { |
|
|
|
|
|
|
|
if (shouldDeferPerPubkeyProfileNetwork(pubkey)) { |
|
|
|
|
|
|
|
return this.getPaymentInfoFromIndexedDB(pubkey) |
|
|
|
|
|
|
|
} |
|
|
|
return await this.fetchReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO) |
|
|
|
return await this.fetchReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|