|
|
|
|
@ -1,20 +1,38 @@
@@ -1,20 +1,38 @@
|
|
|
|
|
import { ExtendedKind, LIBRARY_RELAY_URLS } from '@/constants' |
|
|
|
|
import logger from '@/lib/logger' |
|
|
|
|
import { queryIndexRelay } from '@/lib/index-relay-http' |
|
|
|
|
import { |
|
|
|
|
buildIndexByAddress, |
|
|
|
|
collectReachableAddresses, |
|
|
|
|
collectPublicationIndexEventIds, |
|
|
|
|
collectReachableAddressesCached, |
|
|
|
|
eventTagAddress, |
|
|
|
|
fetchMissingIndexByAddress, |
|
|
|
|
filterValidIndexEvents, |
|
|
|
|
getTopLevelIndexEvents |
|
|
|
|
getTopLevelIndexEvents, |
|
|
|
|
hydrateNestedIndexEvents |
|
|
|
|
} from '@/lib/publication-index' |
|
|
|
|
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' |
|
|
|
|
import { normalizeUrl } from '@/lib/url' |
|
|
|
|
import { |
|
|
|
|
canonicalRelaySessionKey, |
|
|
|
|
httpIndexBasesForRelayQuery, |
|
|
|
|
normalizeHttpRelayUrl, |
|
|
|
|
normalizeUrl |
|
|
|
|
} from '@/lib/url' |
|
|
|
|
import { queryService } from '@/services/client.service' |
|
|
|
|
import type { Event, Filter } from 'nostr-tools' |
|
|
|
|
import { kinds, nip19 } from 'nostr-tools' |
|
|
|
|
|
|
|
|
|
const INDEX_FETCH_LIMIT = 500 |
|
|
|
|
const ENGAGEMENT_FETCH_LIMIT = 500 |
|
|
|
|
const INDEX_HTTP_PAGE_LIMIT = 100 |
|
|
|
|
const INDEX_HTTP_MAX_PAGES = 5 |
|
|
|
|
const ENGAGEMENT_ADDRESS_CHUNK = 36 |
|
|
|
|
const ENGAGEMENT_EVENT_ID_CHUNK = 44 |
|
|
|
|
const MAX_TARGET_ADDRESSES = 480 |
|
|
|
|
const HYDRATE_MISSING_CAP = 64 |
|
|
|
|
const QUERY_OPTS = { |
|
|
|
|
globalTimeout: 18_000, |
|
|
|
|
eoseTimeout: 3_000, |
|
|
|
|
firstRelayResultGraceMs: false as const |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export type PublicationEngagementMaps = { |
|
|
|
|
labelAddresses: Set<string> |
|
|
|
|
@ -44,17 +62,18 @@ function relaySetKey(urls: string[]): string {
@@ -44,17 +62,18 @@ function relaySetKey(urls: string[]): string {
|
|
|
|
|
return [...new Set(urls.map((u) => normalizeUrl(u) || u))].sort().join('|') |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function buildLibraryRelayUrls(userPubkey?: string): Promise<string[]> { |
|
|
|
|
const base = LIBRARY_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) |
|
|
|
|
const urls = await buildComprehensiveRelayList({ |
|
|
|
|
userPubkey, |
|
|
|
|
includeUserOwnRelays: true, |
|
|
|
|
includeFastReadRelays: true, |
|
|
|
|
includeSearchableRelays: true, |
|
|
|
|
includeFavoriteRelays: true, |
|
|
|
|
relayHints: base |
|
|
|
|
}) |
|
|
|
|
return [...new Set([...base, ...urls])] |
|
|
|
|
function splitWsAndHttpRelays(relayUrls: string[]): { wsRelays: string[]; httpRelays: string[] } { |
|
|
|
|
const httpKeys = new Set( |
|
|
|
|
httpIndexBasesForRelayQuery(relayUrls, []).map((u) => canonicalRelaySessionKey(u)) |
|
|
|
|
) |
|
|
|
|
const wsRelays: string[] = [] |
|
|
|
|
const httpRelays: string[] = [] |
|
|
|
|
for (const url of relayUrls) { |
|
|
|
|
const key = canonicalRelaySessionKey(normalizeUrl(url) || url) |
|
|
|
|
if (httpKeys.has(key)) httpRelays.push(url) |
|
|
|
|
else if (!/^https?:\/\//i.test(url.trim())) wsRelays.push(url) |
|
|
|
|
} |
|
|
|
|
return { wsRelays, httpRelays } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function dedupeEventsById(events: Event[]): Event[] { |
|
|
|
|
@ -66,53 +85,167 @@ function dedupeEventsById(events: Event[]): Event[] {
@@ -66,53 +85,167 @@ function dedupeEventsById(events: Event[]): Event[] {
|
|
|
|
|
return [...byId.values()] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function chunkArray<T>(items: T[], size: number): T[][] { |
|
|
|
|
const out: T[][] = [] |
|
|
|
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size)) |
|
|
|
|
return out |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function fetchPaginatedFromHttpIndexRelay(baseUrl: string, filter: Filter): Promise<Event[]> { |
|
|
|
|
const out: Event[] = [] |
|
|
|
|
const seen = new Set<string>() |
|
|
|
|
let until: number | undefined |
|
|
|
|
|
|
|
|
|
for (let page = 0; page < INDEX_HTTP_MAX_PAGES; page++) { |
|
|
|
|
const pageFilter: Filter = { |
|
|
|
|
...filter, |
|
|
|
|
limit: INDEX_HTTP_PAGE_LIMIT, |
|
|
|
|
...(until != null ? { until } : {}) |
|
|
|
|
} |
|
|
|
|
const batch = await queryIndexRelay(baseUrl, pageFilter) |
|
|
|
|
if (batch.length === 0) break |
|
|
|
|
|
|
|
|
|
let oldest = batch[0].created_at |
|
|
|
|
for (const ev of batch) { |
|
|
|
|
if (ev.created_at < oldest) oldest = ev.created_at |
|
|
|
|
if (seen.has(ev.id)) continue |
|
|
|
|
seen.add(ev.id) |
|
|
|
|
out.push(ev) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (batch.length < INDEX_HTTP_PAGE_LIMIT) break |
|
|
|
|
until = oldest - 1 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return out |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function normalizeLibraryRelayUrl(url: string): string { |
|
|
|
|
const trimmed = url.trim() |
|
|
|
|
if (!trimmed) return '' |
|
|
|
|
const http = normalizeHttpRelayUrl(trimmed) |
|
|
|
|
if (http) return http |
|
|
|
|
return normalizeUrl(trimmed) || trimmed |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function libraryIndexRelayUrls(extraRelayUrls: string[] = []): string[] { |
|
|
|
|
const base = LIBRARY_RELAY_URLS.map(normalizeLibraryRelayUrl).filter(Boolean) |
|
|
|
|
const extra = extraRelayUrls.map(normalizeLibraryRelayUrl).filter(Boolean) |
|
|
|
|
return [...new Set([...base, ...extra])] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function buildLibraryRelayUrls(userPubkey?: string): Promise<string[]> { |
|
|
|
|
const base = libraryIndexRelayUrls() |
|
|
|
|
const urls = await buildComprehensiveRelayList({ |
|
|
|
|
userPubkey, |
|
|
|
|
includeUserOwnRelays: true, |
|
|
|
|
includeFastReadRelays: false, |
|
|
|
|
includeSearchableRelays: false, |
|
|
|
|
includeFavoriteRelays: false, |
|
|
|
|
relayHints: base |
|
|
|
|
}) |
|
|
|
|
return libraryIndexRelayUrls([...urls]) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function fetchLibraryIndexEvents(relayUrls: string[]): Promise<Event[]> { |
|
|
|
|
if (relayUrls.length === 0) return [] |
|
|
|
|
const indexRelays = libraryIndexRelayUrls(relayUrls) |
|
|
|
|
if (indexRelays.length === 0) return [] |
|
|
|
|
const filter: Filter = { kinds: [ExtendedKind.PUBLICATION], limit: INDEX_FETCH_LIMIT } |
|
|
|
|
const events = await queryService.fetchEvents(relayUrls, [filter], { |
|
|
|
|
globalTimeout: 25_000, |
|
|
|
|
eoseTimeout: 4_000, |
|
|
|
|
firstRelayResultGraceMs: false |
|
|
|
|
}) |
|
|
|
|
return filterValidIndexEvents(dedupeEventsById(events)) |
|
|
|
|
const { wsRelays, httpRelays } = splitWsAndHttpRelays(indexRelays) |
|
|
|
|
|
|
|
|
|
const batches: Promise<Event[]>[] = [] |
|
|
|
|
if (wsRelays.length > 0) { |
|
|
|
|
batches.push(queryService.fetchEvents(wsRelays, [filter], QUERY_OPTS)) |
|
|
|
|
} |
|
|
|
|
for (const httpRelay of httpRelays) { |
|
|
|
|
batches.push(fetchPaginatedFromHttpIndexRelay(httpRelay, filter)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const merged = dedupeEventsById((await Promise.all(batches)).flat()) |
|
|
|
|
const valid = filterValidIndexEvents(merged) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] index fetch', { |
|
|
|
|
indexRelays: indexRelays.length, |
|
|
|
|
wsRelays: wsRelays.length, |
|
|
|
|
httpRelays: httpRelays.length, |
|
|
|
|
mergedCount: merged.length, |
|
|
|
|
validCount: valid.length |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
return valid |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function buildEngagementMapsFromEvents( |
|
|
|
|
labels: Event[], |
|
|
|
|
comments: Event[], |
|
|
|
|
highlights: Event[] |
|
|
|
|
highlights: Event[], |
|
|
|
|
targetAddresses?: Set<string>, |
|
|
|
|
targetEventIds?: Set<string> |
|
|
|
|
): PublicationEngagementMaps { |
|
|
|
|
const labelAddresses = new Set<string>() |
|
|
|
|
const labelEventIds = new Set<string>() |
|
|
|
|
const commentAddresses = new Set<string>() |
|
|
|
|
const highlightAddresses = new Set<string>() |
|
|
|
|
|
|
|
|
|
const addressMatches = (addr: string) => !targetAddresses || targetAddresses.has(addr) |
|
|
|
|
const eventIdMatches = (id: string) => !targetEventIds || targetEventIds.has(id.toLowerCase()) |
|
|
|
|
|
|
|
|
|
for (const ev of labels) { |
|
|
|
|
for (const tag of ev.tags) { |
|
|
|
|
if (tag[0] === 'a' && tag[1]) labelAddresses.add(tag[1]) |
|
|
|
|
if (tag[0] === 'e' && tag[1]) labelEventIds.add(tag[1].toLowerCase()) |
|
|
|
|
if (tag[0] === 'a' && tag[1] && addressMatches(tag[1])) labelAddresses.add(tag[1]) |
|
|
|
|
if (tag[0] === 'e' && tag[1] && eventIdMatches(tag[1])) labelEventIds.add(tag[1].toLowerCase()) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for (const ev of comments) { |
|
|
|
|
for (const tag of ev.tags) { |
|
|
|
|
if (tag[0] === 'A' && tag[1]) commentAddresses.add(tag[1]) |
|
|
|
|
if (tag[0] === 'A' && tag[1] && addressMatches(tag[1])) commentAddresses.add(tag[1]) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for (const ev of highlights) { |
|
|
|
|
for (const tag of ev.tags) { |
|
|
|
|
if (tag[0] === 'a' && tag[1]) highlightAddresses.add(tag[1]) |
|
|
|
|
if (tag[0] === 'a' && tag[1] && addressMatches(tag[1])) highlightAddresses.add(tag[1]) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { labelAddresses, labelEventIds, commentAddresses, highlightAddresses } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function fetchHttpEngagementByAddresses( |
|
|
|
|
httpRelays: string[], |
|
|
|
|
kind: number, |
|
|
|
|
tagKey: '#a' | '#A', |
|
|
|
|
addressChunks: string[][] |
|
|
|
|
): Promise<Event[]> { |
|
|
|
|
if (httpRelays.length === 0 || addressChunks.length === 0) return [] |
|
|
|
|
const out: Event[] = [] |
|
|
|
|
const seen = new Set<string>() |
|
|
|
|
for (const relay of httpRelays) { |
|
|
|
|
for (const chunk of addressChunks) { |
|
|
|
|
if (chunk.length === 0) continue |
|
|
|
|
const filter = { |
|
|
|
|
kinds: [kind], |
|
|
|
|
[tagKey]: chunk, |
|
|
|
|
limit: Math.min(chunk.length * 10, INDEX_HTTP_PAGE_LIMIT) |
|
|
|
|
} as Filter |
|
|
|
|
const batch = await queryIndexRelay(relay, filter) |
|
|
|
|
for (const ev of batch) { |
|
|
|
|
if (seen.has(ev.id)) continue |
|
|
|
|
seen.add(ev.id) |
|
|
|
|
out.push(ev) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return out |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function fetchPublicationEngagementMaps( |
|
|
|
|
relayUrls: string[] |
|
|
|
|
relayUrls: string[], |
|
|
|
|
targetAddresses: Set<string>, |
|
|
|
|
targetEventIds: Set<string> |
|
|
|
|
): Promise<PublicationEngagementMaps> { |
|
|
|
|
if (relayUrls.length === 0) { |
|
|
|
|
if (relayUrls.length === 0 || targetAddresses.size === 0) { |
|
|
|
|
return { |
|
|
|
|
labelAddresses: new Set(), |
|
|
|
|
labelEventIds: new Set(), |
|
|
|
|
@ -121,34 +254,59 @@ export async function fetchPublicationEngagementMaps(
@@ -121,34 +254,59 @@ export async function fetchPublicationEngagementMaps(
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const opts = { |
|
|
|
|
globalTimeout: 25_000, |
|
|
|
|
eoseTimeout: 4_000, |
|
|
|
|
firstRelayResultGraceMs: false as const |
|
|
|
|
} |
|
|
|
|
const addressChunks = chunkArray([...targetAddresses], ENGAGEMENT_ADDRESS_CHUNK) |
|
|
|
|
const eventIdChunks = chunkArray([...targetEventIds], ENGAGEMENT_EVENT_ID_CHUNK) |
|
|
|
|
const { wsRelays, httpRelays } = splitWsAndHttpRelays(relayUrls) |
|
|
|
|
|
|
|
|
|
const [labels, comments, highlights] = await Promise.all([ |
|
|
|
|
queryService.fetchEvents( |
|
|
|
|
relayUrls, |
|
|
|
|
[{ kinds: [ExtendedKind.LABEL], limit: ENGAGEMENT_FETCH_LIMIT }], |
|
|
|
|
opts |
|
|
|
|
), |
|
|
|
|
queryService.fetchEvents( |
|
|
|
|
relayUrls, |
|
|
|
|
[{ kinds: [ExtendedKind.COMMENT], limit: ENGAGEMENT_FETCH_LIMIT }], |
|
|
|
|
opts |
|
|
|
|
), |
|
|
|
|
queryService.fetchEvents( |
|
|
|
|
relayUrls, |
|
|
|
|
[{ kinds: [kinds.Highlights], limit: ENGAGEMENT_FETCH_LIMIT }], |
|
|
|
|
opts |
|
|
|
|
) |
|
|
|
|
const highlightFilters = addressChunks.map( |
|
|
|
|
(chunk): Filter => ({ kinds: [kinds.Highlights], '#a': chunk, limit: chunk.length * 12 }) |
|
|
|
|
) |
|
|
|
|
const labelAddressFilters = addressChunks.map( |
|
|
|
|
(chunk): Filter => ({ kinds: [ExtendedKind.LABEL], '#a': chunk, limit: chunk.length * 8 }) |
|
|
|
|
) |
|
|
|
|
const labelEventFilters = eventIdChunks.map( |
|
|
|
|
(chunk): Filter => ({ kinds: [ExtendedKind.LABEL], '#e': chunk, limit: chunk.length * 6 }) |
|
|
|
|
) |
|
|
|
|
const commentWsFilters = addressChunks.map( |
|
|
|
|
(chunk): Filter => ({ kinds: [ExtendedKind.COMMENT], '#A': chunk, limit: chunk.length * 12 }) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const highlightPromise = Promise.all([ |
|
|
|
|
wsRelays.length > 0 && highlightFilters.length > 0 |
|
|
|
|
? queryService.fetchEvents(wsRelays, highlightFilters, QUERY_OPTS) |
|
|
|
|
: Promise.resolve([] as Event[]), |
|
|
|
|
fetchHttpEngagementByAddresses(httpRelays, kinds.Highlights, '#a', addressChunks) |
|
|
|
|
]).then(([scoped, bulk]) => dedupeEventsById([...scoped, ...bulk])) |
|
|
|
|
|
|
|
|
|
const labelPromise = Promise.all([ |
|
|
|
|
wsRelays.length > 0 && labelAddressFilters.length > 0 |
|
|
|
|
? queryService.fetchEvents(wsRelays, labelAddressFilters, QUERY_OPTS) |
|
|
|
|
: Promise.resolve([] as Event[]), |
|
|
|
|
wsRelays.length > 0 && labelEventFilters.length > 0 |
|
|
|
|
? queryService.fetchEvents(wsRelays, labelEventFilters, QUERY_OPTS) |
|
|
|
|
: Promise.resolve([] as Event[]), |
|
|
|
|
fetchHttpEngagementByAddresses(httpRelays, ExtendedKind.LABEL, '#a', addressChunks) |
|
|
|
|
]).then(([byAddress, byEvent, bulk]) => dedupeEventsById([...byAddress, ...byEvent, ...bulk])) |
|
|
|
|
|
|
|
|
|
const commentPromise = Promise.all([ |
|
|
|
|
wsRelays.length > 0 && commentWsFilters.length > 0 |
|
|
|
|
? queryService.fetchEvents(wsRelays, commentWsFilters, QUERY_OPTS) |
|
|
|
|
: Promise.resolve([] as Event[]), |
|
|
|
|
fetchHttpEngagementByAddresses(httpRelays, ExtendedKind.COMMENT, '#A', addressChunks) |
|
|
|
|
]).then(([scoped, bulk]) => dedupeEventsById([...scoped, ...bulk])) |
|
|
|
|
|
|
|
|
|
const [highlights, labels, comments] = await Promise.all([ |
|
|
|
|
highlightPromise, |
|
|
|
|
labelPromise, |
|
|
|
|
commentPromise |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
return buildEngagementMapsFromEvents( |
|
|
|
|
dedupeEventsById(labels), |
|
|
|
|
dedupeEventsById(comments), |
|
|
|
|
dedupeEventsById(highlights) |
|
|
|
|
dedupeEventsById(highlights), |
|
|
|
|
targetAddresses, |
|
|
|
|
targetEventIds |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -165,17 +323,15 @@ function addressHasEngagement(
@@ -165,17 +323,15 @@ function addressHasEngagement(
|
|
|
|
|
return { hasLabel, hasComment, hasHighlight } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function filterEngagedPublications( |
|
|
|
|
export function filterEngagedPublications( |
|
|
|
|
roots: Event[], |
|
|
|
|
indexByAddress: Map<string, Event>, |
|
|
|
|
engagement: PublicationEngagementMaps, |
|
|
|
|
relayUrls: string[] |
|
|
|
|
): Promise<LibraryPublicationEntry[]> { |
|
|
|
|
const fetchMissing = (address: string) => fetchMissingIndexByAddress(address, relayUrls) |
|
|
|
|
engagement: PublicationEngagementMaps |
|
|
|
|
): LibraryPublicationEntry[] { |
|
|
|
|
const out: LibraryPublicationEntry[] = [] |
|
|
|
|
|
|
|
|
|
for (const root of roots) { |
|
|
|
|
const reachable = await collectReachableAddresses(root, indexByAddress, fetchMissing) |
|
|
|
|
const reachable = collectReachableAddressesCached(root, indexByAddress) |
|
|
|
|
const rootAddr = eventTagAddress(root) |
|
|
|
|
if (rootAddr) reachable.add(rootAddr) |
|
|
|
|
|
|
|
|
|
@ -278,6 +434,41 @@ export function filterLibraryPublicationsByUser(
@@ -278,6 +434,41 @@ export function filterLibraryPublicationsByUser(
|
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function collectTargetAddressesFromIndexes( |
|
|
|
|
indexEvents: Event[], |
|
|
|
|
indexByAddress: Map<string, Event> |
|
|
|
|
): Set<string> { |
|
|
|
|
const addresses = new Set<string>() |
|
|
|
|
outer: for (const root of getTopLevelIndexEvents(indexEvents)) { |
|
|
|
|
for (const addr of collectReachableAddressesCached(root, indexByAddress)) { |
|
|
|
|
addresses.add(addr) |
|
|
|
|
if (addresses.size >= MAX_TARGET_ADDRESSES) break outer |
|
|
|
|
} |
|
|
|
|
const rootAddr = eventTagAddress(root) |
|
|
|
|
if (rootAddr) { |
|
|
|
|
addresses.add(rootAddr) |
|
|
|
|
if (addresses.size >= MAX_TARGET_ADDRESSES) break outer |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return addresses |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function buildEngagedFromCache( |
|
|
|
|
relayUrls: string[], |
|
|
|
|
indexEvents: Event[], |
|
|
|
|
indexByAddress: Map<string, Event>, |
|
|
|
|
engagement?: PublicationEngagementMaps |
|
|
|
|
): Promise<LibraryPublicationEntry[]> { |
|
|
|
|
const topLevel = getTopLevelIndexEvents(indexEvents) |
|
|
|
|
let maps = engagement |
|
|
|
|
if (!maps) { |
|
|
|
|
const targetAddresses = collectTargetAddressesFromIndexes(indexEvents, indexByAddress) |
|
|
|
|
const targetEventIds = collectPublicationIndexEventIds(indexEvents) |
|
|
|
|
maps = await fetchPublicationEngagementMaps(relayUrls, targetAddresses, targetEventIds) |
|
|
|
|
} |
|
|
|
|
return sortLibraryPublications(filterEngagedPublications(topLevel, indexByAddress, maps)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function loadLibraryPublicationIndex( |
|
|
|
|
relayUrls: string[], |
|
|
|
|
options?: { forceRefresh?: boolean } |
|
|
|
|
@ -287,32 +478,76 @@ export async function loadLibraryPublicationIndex(
@@ -287,32 +478,76 @@ export async function loadLibraryPublicationIndex(
|
|
|
|
|
topLevelCount: number |
|
|
|
|
}> { |
|
|
|
|
const key = relaySetKey(relayUrls) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] load start', { relayCount: relayUrls.length, cached: sessionCache?.relayKey === key }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!options?.forceRefresh && sessionCache?.relayKey === key) { |
|
|
|
|
const engaged = await buildEngagedFromCache( |
|
|
|
|
relayUrls, |
|
|
|
|
sessionCache.indexEvents, |
|
|
|
|
sessionCache.indexByAddress, |
|
|
|
|
sessionCache.engagement |
|
|
|
|
) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] load from cache', { engaged: engaged.length }) |
|
|
|
|
} |
|
|
|
|
return { |
|
|
|
|
engaged: sortLibraryPublications( |
|
|
|
|
await filterEngagedPublications( |
|
|
|
|
getTopLevelIndexEvents(sessionCache.indexEvents), |
|
|
|
|
sessionCache.indexByAddress, |
|
|
|
|
sessionCache.engagement, |
|
|
|
|
relayUrls |
|
|
|
|
) |
|
|
|
|
), |
|
|
|
|
engaged, |
|
|
|
|
allIndexCount: sessionCache.indexEvents.length, |
|
|
|
|
topLevelCount: getTopLevelIndexEvents(sessionCache.indexEvents).length |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const [indexEvents, engagement] = await Promise.all([ |
|
|
|
|
fetchLibraryIndexEvents(relayUrls), |
|
|
|
|
fetchPublicationEngagementMaps(relayUrls) |
|
|
|
|
]) |
|
|
|
|
const indexEvents = await fetchLibraryIndexEvents(relayUrls) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] indexes fetched', { validCount: indexEvents.length }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const indexByAddress = buildIndexByAddress(indexEvents) |
|
|
|
|
const topLevelForHydrate = getTopLevelIndexEvents(indexEvents) |
|
|
|
|
await hydrateNestedIndexEvents(indexEvents, indexByAddress, relayUrls, { |
|
|
|
|
maxPasses: 1, |
|
|
|
|
maxMissingPerPass: HYDRATE_MISSING_CAP, |
|
|
|
|
scanRoots: topLevelForHydrate |
|
|
|
|
}) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] nested hydrate done', { indexCount: indexEvents.length }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const targetAddresses = collectTargetAddressesFromIndexes(indexEvents, indexByAddress) |
|
|
|
|
const targetEventIds = collectPublicationIndexEventIds(indexEvents) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] fetching engagement', { |
|
|
|
|
targetAddresses: targetAddresses.size, |
|
|
|
|
targetEventIds: targetEventIds.size |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
const engagement = await fetchPublicationEngagementMaps( |
|
|
|
|
relayUrls, |
|
|
|
|
targetAddresses, |
|
|
|
|
targetEventIds |
|
|
|
|
) |
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] engagement maps built', { |
|
|
|
|
labels: engagement.labelAddresses.size + engagement.labelEventIds.size, |
|
|
|
|
comments: engagement.commentAddresses.size, |
|
|
|
|
highlights: engagement.highlightAddresses.size |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
sessionCache = { relayKey: key, indexEvents, indexByAddress, engagement } |
|
|
|
|
|
|
|
|
|
const topLevel = getTopLevelIndexEvents(indexEvents) |
|
|
|
|
const engaged = sortLibraryPublications( |
|
|
|
|
await filterEngagedPublications(topLevel, indexByAddress, engagement, relayUrls) |
|
|
|
|
) |
|
|
|
|
const engaged = sortLibraryPublications(filterEngagedPublications(topLevel, indexByAddress, engagement)) |
|
|
|
|
|
|
|
|
|
if (import.meta.env.DEV) { |
|
|
|
|
logger.info('[Library] load done', { |
|
|
|
|
engaged: engaged.length, |
|
|
|
|
topLevel: topLevel.length, |
|
|
|
|
allIndexCount: indexEvents.length |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
engaged, |
|
|
|
|
|