|
|
|
@ -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.
|
|
|
|
|