|
|
|
@ -1,14 +1,11 @@ |
|
|
|
import logger from '@/lib/logger' |
|
|
|
import logger from '@/lib/logger' |
|
|
|
import { publicationCoordinateLookupKeys } from '@/lib/publication-coordinate' |
|
|
|
import { publicationCoordinateLookupKeys, splitPublicationCoordinate } from '@/lib/publication-coordinate' |
|
|
|
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' |
|
|
|
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' |
|
|
|
import { normalizeUrl } from '@/lib/url' |
|
|
|
import { normalizeUrl } from '@/lib/url' |
|
|
|
import client, { queryService } from '@/services/client.service' |
|
|
|
import client, { queryService } from '@/services/client.service' |
|
|
|
import { ExtendedKind } from '@/constants' |
|
|
|
|
|
|
|
import type { Event, Filter } from 'nostr-tools' |
|
|
|
import type { Event, Filter } from 'nostr-tools' |
|
|
|
import { nip19 } from 'nostr-tools' |
|
|
|
import { nip19 } from 'nostr-tools' |
|
|
|
import { kinds } from 'nostr-tools' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Parsed a/e reference from publication index tags (same shape as PublicationIndex uses). */ |
|
|
|
|
|
|
|
export type PublicationSectionRef = { |
|
|
|
export type PublicationSectionRef = { |
|
|
|
type: 'a' | 'e' |
|
|
|
type: 'a' | 'e' |
|
|
|
coordinate?: string |
|
|
|
coordinate?: string |
|
|
|
@ -23,64 +20,48 @@ export function publicationRefKey(ref: PublicationSectionRef): string { |
|
|
|
return (ref.coordinate || ref.eventId || '').trim() |
|
|
|
return (ref.coordinate || ref.eventId || '').trim() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Parse NIP-33 `a` coordinate `kind:64-hex-pubkey:d-identifier` where `d` may contain `:`. |
|
|
|
|
|
|
|
* Returns a canonical coordinate with lowercase pubkey for cache / REQ / matching. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
export function parsePublicationATagCoordinate(raw: string): { |
|
|
|
export function parsePublicationATagCoordinate(raw: string): { |
|
|
|
kind: number |
|
|
|
kind: number |
|
|
|
pubkey: string |
|
|
|
pubkey: string |
|
|
|
identifier: string |
|
|
|
identifier: string |
|
|
|
coordinate: string |
|
|
|
coordinate: string |
|
|
|
} | null { |
|
|
|
} | null { |
|
|
|
const trimmed = raw.trim() |
|
|
|
const parsed = splitPublicationCoordinate(raw) |
|
|
|
const i0 = trimmed.indexOf(':') |
|
|
|
if (!parsed) return null |
|
|
|
const i1 = trimmed.indexOf(':', i0 + 1) |
|
|
|
|
|
|
|
if (i0 < 1 || i1 <= i0 + 1) return null |
|
|
|
|
|
|
|
const kindStr = trimmed.slice(0, i0) |
|
|
|
|
|
|
|
const pubkeyRaw = trimmed.slice(i0 + 1, i1) |
|
|
|
|
|
|
|
const identifier = trimmed.slice(i1 + 1) |
|
|
|
|
|
|
|
const kind = parseInt(kindStr, 10) |
|
|
|
|
|
|
|
if (Number.isNaN(kind) || !/^[0-9a-fA-F]{64}$/.test(pubkeyRaw)) return null |
|
|
|
|
|
|
|
const pubkey = pubkeyRaw.toLowerCase() |
|
|
|
|
|
|
|
return { |
|
|
|
return { |
|
|
|
kind, |
|
|
|
kind: parsed.kind, |
|
|
|
pubkey, |
|
|
|
pubkey: parsed.pubkey, |
|
|
|
identifier, |
|
|
|
identifier: parsed.d, |
|
|
|
coordinate: `${kind}:${pubkey}:${identifier}` |
|
|
|
coordinate: `${parsed.kind}:${parsed.pubkey}:${parsed.d}` |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function resolvePublicationEventIdToHex(eventId: string): string | undefined { |
|
|
|
export function resolvePublicationEventIdToHex(eventId: string): string | undefined { |
|
|
|
if (!eventId) return undefined |
|
|
|
|
|
|
|
const trimmed = eventId.trim() |
|
|
|
const trimmed = eventId.trim() |
|
|
|
|
|
|
|
if (!trimmed) return undefined |
|
|
|
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) return trimmed.toLowerCase() |
|
|
|
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) return trimmed.toLowerCase() |
|
|
|
try { |
|
|
|
try { |
|
|
|
const decoded = nip19.decode(trimmed) |
|
|
|
const decoded = nip19.decode(trimmed) |
|
|
|
if (decoded.type === 'note') return decoded.data |
|
|
|
if (decoded.type === 'note') return decoded.data |
|
|
|
if (decoded.type === 'nevent') return decoded.data.id |
|
|
|
if (decoded.type === 'nevent') return decoded.data.id |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
/* ignore */ |
|
|
|
// ignore malformed bech32 ids
|
|
|
|
} |
|
|
|
} |
|
|
|
return undefined |
|
|
|
return undefined |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function collectRelayHints(refs: PublicationSectionRef[]): string[] { |
|
|
|
function collectRelayHints(refs: PublicationSectionRef[]): string[] { |
|
|
|
const out: string[] = [] |
|
|
|
const out: string[] = [] |
|
|
|
for (const r of refs) { |
|
|
|
for (const ref of refs) { |
|
|
|
const h = r.relay?.trim() |
|
|
|
const relay = ref.relay?.trim() |
|
|
|
if (h && (h.startsWith('wss://') || h.startsWith('ws://'))) { |
|
|
|
if (!relay) continue |
|
|
|
const n = normalizeUrl(h) || h |
|
|
|
if (!relay.startsWith('wss://') && !relay.startsWith('ws://')) continue |
|
|
|
out.push(n) |
|
|
|
const normalized = normalizeUrl(relay) || relay |
|
|
|
} |
|
|
|
out.push(normalized) |
|
|
|
} |
|
|
|
} |
|
|
|
return out |
|
|
|
return [...new Set(out)] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Focused relay set for publication sections: hints + author + user + profile/fast read, capped. |
|
|
|
|
|
|
|
* Omits full SEARCHABLE list to avoid opening dozens of relays per publication. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
export async function buildPublicationSectionRelayUrls( |
|
|
|
export async function buildPublicationSectionRelayUrls( |
|
|
|
indexEvent: Event, |
|
|
|
indexEvent: Event, |
|
|
|
refs: PublicationSectionRef[], |
|
|
|
refs: PublicationSectionRef[], |
|
|
|
@ -99,28 +80,25 @@ export async function buildPublicationSectionRelayUrls( |
|
|
|
includeFavoriteRelays: true, |
|
|
|
includeFavoriteRelays: true, |
|
|
|
includeLocalRelays: true |
|
|
|
includeLocalRelays: true |
|
|
|
}) |
|
|
|
}) |
|
|
|
return urls.slice(0, maxRelays) |
|
|
|
const prioritized = [...new Set([...hints, ...urls])] |
|
|
|
|
|
|
|
return prioritized.slice(0, maxRelays) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const IDS_CHUNK = 44 |
|
|
|
const IDS_CHUNK = 44 |
|
|
|
const D_TAGS_CHUNK = 28 |
|
|
|
const D_CHUNK = 28 |
|
|
|
const SECTION_KIND_FALLBACK_CANDIDATES = [ |
|
|
|
const ANY_KIND_LIMIT_PER_D = 12 |
|
|
|
ExtendedKind.PUBLICATION_CONTENT, // 30041
|
|
|
|
|
|
|
|
ExtendedKind.WIKI_ARTICLE, // 30818
|
|
|
|
function dTagOf(ev: Event): string | undefined { |
|
|
|
ExtendedKind.WIKI_ARTICLE_MARKDOWN, // 30817
|
|
|
|
const d = ev.tags.find((t) => t[0] === 'd')?.[1] |
|
|
|
kinds.LongFormArticle, // 30023
|
|
|
|
return d && d.length > 0 ? d : undefined |
|
|
|
kinds.ShortTextNote // 1
|
|
|
|
} |
|
|
|
] as number[] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function coordinateFromEvent(ev: Event): string { |
|
|
|
function coordinateOfEvent(ev: Event): string | null { |
|
|
|
const d = ev.tags.find((t) => t[0] === 'd')?.[1] ?? '' |
|
|
|
const d = dTagOf(ev) |
|
|
|
|
|
|
|
if (!d) return null |
|
|
|
return `${ev.kind}:${ev.pubkey.toLowerCase()}:${d}` |
|
|
|
return `${ev.kind}:${ev.pubkey.toLowerCase()}:${d}` |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* One batched query: chunk `ids` filters and grouped `authors + kinds + #d` filters. |
|
|
|
|
|
|
|
* Caller should hydrate from IndexedDB first. Keys are {@link publicationRefKey}. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
export async function batchFetchPublicationSectionEvents( |
|
|
|
export async function batchFetchPublicationSectionEvents( |
|
|
|
refs: PublicationSectionRef[], |
|
|
|
refs: PublicationSectionRef[], |
|
|
|
relayUrls: string[] |
|
|
|
relayUrls: string[] |
|
|
|
@ -128,45 +106,44 @@ export async function batchFetchPublicationSectionEvents( |
|
|
|
const out = new Map<string, Event>() |
|
|
|
const out = new Map<string, Event>() |
|
|
|
if (refs.length === 0 || relayUrls.length === 0) return out |
|
|
|
if (refs.length === 0 || relayUrls.length === 0) return out |
|
|
|
|
|
|
|
|
|
|
|
const idRefs: PublicationSectionRef[] = [] |
|
|
|
const eRefs: PublicationSectionRef[] = [] |
|
|
|
const hexByKey = new Map<string, string>() |
|
|
|
const eHexByKey = new Map<string, string>() |
|
|
|
for (const r of refs) { |
|
|
|
const aRefs = refs.filter((r) => r.type === 'a' && r.coordinate && r.pubkey && typeof r.kind === 'number') |
|
|
|
if (r.type !== 'e' || !r.eventId) continue |
|
|
|
|
|
|
|
const key = publicationRefKey(r) |
|
|
|
|
|
|
|
if (!key) continue |
|
|
|
|
|
|
|
const hex = resolvePublicationEventIdToHex(r.eventId) |
|
|
|
|
|
|
|
if (hex) { |
|
|
|
|
|
|
|
idRefs.push(r) |
|
|
|
|
|
|
|
hexByKey.set(key, hex) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const aRefs = refs.filter((r) => r.type === 'a' && r.coordinate && r.pubkey && r.kind != null) |
|
|
|
for (const ref of refs) { |
|
|
|
const aGroups = new Map<string, { pubkey: string; kind: number; dTags: string[] }>() |
|
|
|
if (ref.type !== 'e' || !ref.eventId) continue |
|
|
|
for (const r of aRefs) { |
|
|
|
const key = publicationRefKey(ref) |
|
|
|
const idf = r.identifier ?? r.coordinate!.split(':').slice(2).join(':') |
|
|
|
const hex = resolvePublicationEventIdToHex(ref.eventId) |
|
|
|
if (!idf) continue |
|
|
|
if (!key || !hex) continue |
|
|
|
const gk = `${r.pubkey}:${r.kind}` |
|
|
|
eRefs.push(ref) |
|
|
|
let g = aGroups.get(gk) |
|
|
|
eHexByKey.set(key, hex) |
|
|
|
if (!g) { |
|
|
|
|
|
|
|
g = { pubkey: r.pubkey!, kind: r.kind!, dTags: [] } |
|
|
|
|
|
|
|
aGroups.set(gk, g) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
g.dTags.push(idf) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const filters: Filter[] = [] |
|
|
|
const filters: Filter[] = [] |
|
|
|
|
|
|
|
|
|
|
|
const hexList = [...new Set([...hexByKey.values()])].filter((id) => /^[0-9a-f]{64}$/.test(id)) |
|
|
|
const ids = [...new Set([...eHexByKey.values()])] |
|
|
|
for (let i = 0; i < hexList.length; i += IDS_CHUNK) { |
|
|
|
for (let i = 0; i < ids.length; i += IDS_CHUNK) { |
|
|
|
const chunk = hexList.slice(i, i + IDS_CHUNK) |
|
|
|
const chunk = ids.slice(i, i + IDS_CHUNK) |
|
|
|
filters.push({ ids: chunk, limit: chunk.length }) |
|
|
|
filters.push({ ids: chunk, limit: chunk.length }) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (const g of aGroups.values()) { |
|
|
|
const groupedA = new Map<string, { pubkey: string; kind: number; dTags: string[] }>() |
|
|
|
|
|
|
|
for (const ref of aRefs) { |
|
|
|
|
|
|
|
const d = ref.identifier ?? ref.coordinate!.split(':').slice(2).join(':') |
|
|
|
|
|
|
|
if (!d) continue |
|
|
|
|
|
|
|
const gk = `${ref.pubkey}:${ref.kind}` |
|
|
|
|
|
|
|
let g = groupedA.get(gk) |
|
|
|
|
|
|
|
if (!g) { |
|
|
|
|
|
|
|
g = { pubkey: ref.pubkey!, kind: ref.kind!, dTags: [] } |
|
|
|
|
|
|
|
groupedA.set(gk, g) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
g.dTags.push(d) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const g of groupedA.values()) { |
|
|
|
const uniqueD = [...new Set(g.dTags)] |
|
|
|
const uniqueD = [...new Set(g.dTags)] |
|
|
|
for (let i = 0; i < uniqueD.length; i += D_TAGS_CHUNK) { |
|
|
|
for (let i = 0; i < uniqueD.length; i += D_CHUNK) { |
|
|
|
const dChunk = uniqueD.slice(i, i + D_TAGS_CHUNK) |
|
|
|
const dChunk = uniqueD.slice(i, i + D_CHUNK) |
|
|
|
filters.push({ |
|
|
|
filters.push({ |
|
|
|
authors: [g.pubkey.toLowerCase()], |
|
|
|
authors: [g.pubkey.toLowerCase()], |
|
|
|
kinds: [g.kind], |
|
|
|
kinds: [g.kind], |
|
|
|
@ -176,87 +153,153 @@ export async function batchFetchPublicationSectionEvents( |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (filters.length === 0) { |
|
|
|
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
|
|
|
logger.info('[PublicationSection] batch_fetch_skip — no filters', { |
|
|
|
|
|
|
|
aRefCount: aRefs.length, |
|
|
|
|
|
|
|
idRefCount: idRefs.length |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return out |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let events: Event[] = [] |
|
|
|
let events: Event[] = [] |
|
|
|
try { |
|
|
|
if (filters.length > 0) { |
|
|
|
events = await queryService.fetchEvents(relayUrls, filters, { |
|
|
|
try { |
|
|
|
globalTimeout: 14_000, |
|
|
|
events = await queryService.fetchEvents(relayUrls, filters, { |
|
|
|
eoseTimeout: 2_500, |
|
|
|
globalTimeout: 12_000, |
|
|
|
/** Do not early-resolve after the first event; this query must wait for the full batch. */ |
|
|
|
eoseTimeout: 2_000, |
|
|
|
firstRelayResultGraceMs: false |
|
|
|
firstRelayResultGraceMs: false |
|
|
|
}) |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
|
|
|
logger.warn('[PublicationSection] batch_fetch_error', { |
|
|
|
|
|
|
|
message: err instanceof Error ? err.message : String(err), |
|
|
|
|
|
|
|
filterCount: filters.length, |
|
|
|
|
|
|
|
relayCount: relayUrls.length |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
|
|
|
logger.warn('[PublicationSection] batch_fetch_error', { |
|
|
|
|
|
|
|
message: err instanceof Error ? err.message : String(err), |
|
|
|
|
|
|
|
filterCount: filters.length, |
|
|
|
|
|
|
|
relayCount: relayUrls.length |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return out |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const byId = new Map<string, Event>() |
|
|
|
const byId = new Map<string, Event>() |
|
|
|
const byCoord = new Map<string, Event>() |
|
|
|
const byCoord = new Map<string, Event>() |
|
|
|
for (const ev of events) { |
|
|
|
for (const ev of events) { |
|
|
|
byId.set(ev.id.toLowerCase(), ev) |
|
|
|
byId.set(ev.id.toLowerCase(), ev) |
|
|
|
const d = ev.tags.find((t) => t[0] === 'd')?.[1] |
|
|
|
const coord = coordinateOfEvent(ev) |
|
|
|
if (d !== undefined && d !== '') { |
|
|
|
if (!coord) continue |
|
|
|
const base = coordinateFromEvent(ev) |
|
|
|
for (const key of publicationCoordinateLookupKeys(coord)) { |
|
|
|
for (const k of publicationCoordinateLookupKeys(base)) { |
|
|
|
const prev = byCoord.get(key) |
|
|
|
if (!byCoord.has(k)) byCoord.set(k, ev) |
|
|
|
if (!prev || ev.created_at > prev.created_at) byCoord.set(key, ev) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (const r of idRefs) { |
|
|
|
for (const ref of eRefs) { |
|
|
|
const key = publicationRefKey(r) |
|
|
|
const key = publicationRefKey(ref) |
|
|
|
const hex = hexByKey.get(key) |
|
|
|
const hex = eHexByKey.get(key) |
|
|
|
if (!hex) continue |
|
|
|
if (!hex) continue |
|
|
|
const ev = byId.get(hex.toLowerCase()) |
|
|
|
const ev = byId.get(hex) |
|
|
|
|
|
|
|
if (ev) out.set(key, ev) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const ref of aRefs) { |
|
|
|
|
|
|
|
const key = publicationRefKey(ref) |
|
|
|
|
|
|
|
if (out.has(key)) continue |
|
|
|
|
|
|
|
const coord = ref.coordinate! |
|
|
|
|
|
|
|
let ev: Event | undefined |
|
|
|
|
|
|
|
for (const k of publicationCoordinateLookupKeys(coord)) { |
|
|
|
|
|
|
|
ev = byCoord.get(k) |
|
|
|
|
|
|
|
if (ev) break |
|
|
|
|
|
|
|
} |
|
|
|
if (ev) out.set(key, ev) |
|
|
|
if (ev) out.set(key, ev) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Fallback for mismatched/legacy kind in `a` tags:
|
|
|
|
// Relay-hint targeted pass for unresolved `a` refs.
|
|
|
|
// retry unresolved refs by author + #d across common section kinds.
|
|
|
|
const unresolvedAfterBatch = aRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
const unresolvedARefs = aRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
const byHintRelay = new Map<string, PublicationSectionRef[]>() |
|
|
|
if (unresolvedARefs.length > 0) { |
|
|
|
for (const ref of unresolvedAfterBatch) { |
|
|
|
const fallbackGroups = new Map<string, { pubkey: string; dTags: string[] }>() |
|
|
|
const relay = normalizeUrl(ref.relay || '') || ref.relay?.trim() |
|
|
|
for (const r of unresolvedARefs) { |
|
|
|
if (!relay) continue |
|
|
|
const pubkey = r.pubkey?.toLowerCase() |
|
|
|
const list = byHintRelay.get(relay) |
|
|
|
const idf = r.identifier ?? r.coordinate?.split(':').slice(2).join(':') |
|
|
|
if (list) list.push(ref) |
|
|
|
if (!pubkey || !idf) continue |
|
|
|
else byHintRelay.set(relay, [ref]) |
|
|
|
let g = fallbackGroups.get(pubkey) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const [relay, relayRefs] of byHintRelay) { |
|
|
|
|
|
|
|
const hintFilters: Filter[] = [] |
|
|
|
|
|
|
|
const groups = new Map<string, { pubkey: string; kind: number; dTags: string[] }>() |
|
|
|
|
|
|
|
for (const ref of relayRefs) { |
|
|
|
|
|
|
|
const d = ref.identifier ?? ref.coordinate!.split(':').slice(2).join(':') |
|
|
|
|
|
|
|
if (!d) continue |
|
|
|
|
|
|
|
const gk = `${ref.pubkey}:${ref.kind}` |
|
|
|
|
|
|
|
let g = groups.get(gk) |
|
|
|
if (!g) { |
|
|
|
if (!g) { |
|
|
|
g = { pubkey, dTags: [] } |
|
|
|
g = { pubkey: ref.pubkey!.toLowerCase(), kind: ref.kind!, dTags: [] } |
|
|
|
fallbackGroups.set(pubkey, g) |
|
|
|
groups.set(gk, g) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
g.dTags.push(d) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for (const g of groups.values()) { |
|
|
|
|
|
|
|
const uniqueD = [...new Set(g.dTags)] |
|
|
|
|
|
|
|
for (let i = 0; i < uniqueD.length; i += D_CHUNK) { |
|
|
|
|
|
|
|
const dChunk = uniqueD.slice(i, i + D_CHUNK) |
|
|
|
|
|
|
|
hintFilters.push({ |
|
|
|
|
|
|
|
authors: [g.pubkey], |
|
|
|
|
|
|
|
kinds: [g.kind], |
|
|
|
|
|
|
|
'#d': dChunk, |
|
|
|
|
|
|
|
limit: dChunk.length |
|
|
|
|
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
g.dTags.push(idf) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (hintFilters.length === 0) continue |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const hintEvents = await queryService.fetchEvents([relay], hintFilters, { |
|
|
|
|
|
|
|
globalTimeout: 8_000, |
|
|
|
|
|
|
|
eoseTimeout: 1_500, |
|
|
|
|
|
|
|
firstRelayResultGraceMs: false |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
const hintByCoord = new Map<string, Event>() |
|
|
|
|
|
|
|
for (const ev of hintEvents) { |
|
|
|
|
|
|
|
const coord = coordinateOfEvent(ev) |
|
|
|
|
|
|
|
if (!coord) continue |
|
|
|
|
|
|
|
for (const key of publicationCoordinateLookupKeys(coord)) { |
|
|
|
|
|
|
|
const prev = hintByCoord.get(key) |
|
|
|
|
|
|
|
if (!prev || ev.created_at > prev.created_at) hintByCoord.set(key, ev) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for (const ref of relayRefs) { |
|
|
|
|
|
|
|
const key = publicationRefKey(ref) |
|
|
|
|
|
|
|
if (out.has(key)) continue |
|
|
|
|
|
|
|
const coord = ref.coordinate! |
|
|
|
|
|
|
|
let ev: Event | undefined |
|
|
|
|
|
|
|
for (const k of publicationCoordinateLookupKeys(coord)) { |
|
|
|
|
|
|
|
ev = hintByCoord.get(k) |
|
|
|
|
|
|
|
if (ev) break |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (ev) out.set(key, ev) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
// ignore per-relay hint failures
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Last fallback: author + #d across any kind.
|
|
|
|
|
|
|
|
const unresolvedAfterHint = aRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
|
|
|
|
if (unresolvedAfterHint.length > 0) { |
|
|
|
const fallbackFilters: Filter[] = [] |
|
|
|
const fallbackFilters: Filter[] = [] |
|
|
|
for (const g of fallbackGroups.values()) { |
|
|
|
const groups = new Map<string, { pubkey: string; dTags: string[] }>() |
|
|
|
|
|
|
|
for (const ref of unresolvedAfterHint) { |
|
|
|
|
|
|
|
const d = ref.identifier ?? ref.coordinate!.split(':').slice(2).join(':') |
|
|
|
|
|
|
|
if (!d) continue |
|
|
|
|
|
|
|
const pk = ref.pubkey!.toLowerCase() |
|
|
|
|
|
|
|
let g = groups.get(pk) |
|
|
|
|
|
|
|
if (!g) { |
|
|
|
|
|
|
|
g = { pubkey: pk, dTags: [] } |
|
|
|
|
|
|
|
groups.set(pk, g) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
g.dTags.push(d) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for (const g of groups.values()) { |
|
|
|
const uniqueD = [...new Set(g.dTags)] |
|
|
|
const uniqueD = [...new Set(g.dTags)] |
|
|
|
for (let i = 0; i < uniqueD.length; i += D_TAGS_CHUNK) { |
|
|
|
for (let i = 0; i < uniqueD.length; i += D_CHUNK) { |
|
|
|
const dChunk = uniqueD.slice(i, i + D_TAGS_CHUNK) |
|
|
|
const dChunk = uniqueD.slice(i, i + D_CHUNK) |
|
|
|
fallbackFilters.push({ |
|
|
|
fallbackFilters.push({ |
|
|
|
authors: [g.pubkey], |
|
|
|
authors: [g.pubkey], |
|
|
|
kinds: [...SECTION_KIND_FALLBACK_CANDIDATES], |
|
|
|
|
|
|
|
'#d': dChunk, |
|
|
|
'#d': dChunk, |
|
|
|
limit: dChunk.length * SECTION_KIND_FALLBACK_CANDIDATES.length |
|
|
|
limit: dChunk.length * ANY_KIND_LIMIT_PER_D |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (fallbackFilters.length > 0) { |
|
|
|
if (fallbackFilters.length > 0) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
const fallbackEvents = await queryService.fetchEvents(relayUrls, fallbackFilters, { |
|
|
|
const fallbackEvents = await queryService.fetchEvents(relayUrls, fallbackFilters, { |
|
|
|
@ -264,49 +307,38 @@ export async function batchFetchPublicationSectionEvents( |
|
|
|
eoseTimeout: 2_000, |
|
|
|
eoseTimeout: 2_000, |
|
|
|
firstRelayResultGraceMs: false |
|
|
|
firstRelayResultGraceMs: false |
|
|
|
}) |
|
|
|
}) |
|
|
|
const byAuthorAndD = new Map<string, Event>() |
|
|
|
const byAuthorD = new Map<string, Event[]>() |
|
|
|
for (const ev of fallbackEvents) { |
|
|
|
for (const ev of fallbackEvents) { |
|
|
|
const d = ev.tags.find((t) => t[0] === 'd')?.[1] |
|
|
|
const d = dTagOf(ev) |
|
|
|
if (!d) continue |
|
|
|
if (!d) continue |
|
|
|
const k = `${ev.pubkey.toLowerCase()}:${d}` |
|
|
|
const k = `${ev.pubkey.toLowerCase()}:${d}` |
|
|
|
const prev = byAuthorAndD.get(k) |
|
|
|
const arr = byAuthorD.get(k) |
|
|
|
if (!prev || ev.created_at > prev.created_at) byAuthorAndD.set(k, ev) |
|
|
|
if (arr) arr.push(ev) |
|
|
|
|
|
|
|
else byAuthorD.set(k, [ev]) |
|
|
|
} |
|
|
|
} |
|
|
|
for (const r of unresolvedARefs) { |
|
|
|
for (const ref of unresolvedAfterHint) { |
|
|
|
const key = publicationRefKey(r) |
|
|
|
const key = publicationRefKey(ref) |
|
|
|
if (out.has(key)) continue |
|
|
|
if (out.has(key)) continue |
|
|
|
const pubkey = r.pubkey?.toLowerCase() |
|
|
|
const d = ref.identifier ?? ref.coordinate!.split(':').slice(2).join(':') |
|
|
|
const idf = r.identifier ?? r.coordinate?.split(':').slice(2).join(':') |
|
|
|
const candidates = byAuthorD.get(`${ref.pubkey!.toLowerCase()}:${d}`) |
|
|
|
if (!pubkey || !idf) continue |
|
|
|
if (!candidates || candidates.length === 0) continue |
|
|
|
const ev = byAuthorAndD.get(`${pubkey}:${idf}`) |
|
|
|
const preferred = candidates.filter((ev) => ev.kind === ref.kind) |
|
|
|
if (ev) out.set(key, ev) |
|
|
|
const src = preferred.length > 0 ? preferred : candidates |
|
|
|
} |
|
|
|
let newest = src[0] |
|
|
|
} catch (err) { |
|
|
|
for (let i = 1; i < src.length; i++) { |
|
|
|
if (import.meta.env.DEV) { |
|
|
|
if (src[i].created_at > newest.created_at) newest = src[i] |
|
|
|
logger.warn('[PublicationSection] batch_fetch_fallback_error', { |
|
|
|
} |
|
|
|
message: err instanceof Error ? err.message : String(err), |
|
|
|
out.set(key, newest) |
|
|
|
filterCount: fallbackFilters.length, |
|
|
|
|
|
|
|
relayCount: relayUrls.length |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
// ignore fallback errors
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (const r of aRefs) { |
|
|
|
|
|
|
|
const key = publicationRefKey(r) |
|
|
|
|
|
|
|
const coord = r.coordinate! |
|
|
|
|
|
|
|
let ev: Event | undefined |
|
|
|
|
|
|
|
for (const k of publicationCoordinateLookupKeys(coord)) { |
|
|
|
|
|
|
|
ev = byCoord.get(k) |
|
|
|
|
|
|
|
if (ev) break |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (ev) out.set(key, ev) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
if (import.meta.env.DEV) { |
|
|
|
const unmatchedA = aRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
const unmatchedA = aRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
const unmatchedE = idRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
const unmatchedE = eRefs.filter((r) => !out.has(publicationRefKey(r))) |
|
|
|
logger.info('[PublicationSection] batch_fetch_result', { |
|
|
|
logger.info('[PublicationSection] batch_fetch_result', { |
|
|
|
relayCount: relayUrls.length, |
|
|
|
relayCount: relayUrls.length, |
|
|
|
filterCount: filters.length, |
|
|
|
filterCount: filters.length, |
|
|
|
@ -315,11 +347,7 @@ export async function batchFetchPublicationSectionEvents( |
|
|
|
resolved: out.size, |
|
|
|
resolved: out.size, |
|
|
|
unmatchedACount: unmatchedA.length, |
|
|
|
unmatchedACount: unmatchedA.length, |
|
|
|
unmatchedECount: unmatchedE.length, |
|
|
|
unmatchedECount: unmatchedE.length, |
|
|
|
unmatchedAKeys: unmatchedA.map((r) => publicationRefKey(r)).slice(0, 12), |
|
|
|
unmatchedAKeys: unmatchedA.map((r) => publicationRefKey(r)).slice(0, 12) |
|
|
|
sampleEventCoords: events.slice(0, 3).map((ev) => { |
|
|
|
|
|
|
|
const d = ev.tags.find((t) => t[0] === 'd')?.[1] |
|
|
|
|
|
|
|
return d !== undefined && d !== '' ? coordinateFromEvent(ev) : `${ev.kind}:${ev.pubkey.slice(0, 8)}…` |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|