You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

205 lines
7.2 KiB

import {
FAST_READ_RELAY_URLS,
FAST_WRITE_RELAY_URLS,
SOCIAL_KIND_BLOCKED_RELAY_URLS,
MAX_PUBLISH_RELAYS,
MAX_REQ_RELAY_URLS
} from '@/constants'
import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
export { MAX_REQ_RELAY_URLS }
export function dedupeNormalizeRelayUrlsOrdered(urls: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const u of urls) {
const n = normalizeAnyRelayUrl(u) || u.trim()
if (!n || seen.has(n)) continue
seen.add(n)
out.push(n)
}
return out
}
/** LAN / local host relays first, then the rest; deduped. */
export function relayUrlsLocalsFirst(urls: string[]): string[] {
const local: string[] = []
const remote: string[] = []
for (const u of urls) {
const n = normalizeAnyRelayUrl(u) || u.trim()
if (!n) continue
if (isLocalNetworkUrl(u) || isLocalNetworkUrl(n)) local.push(n)
else remote.push(n)
}
return dedupeNormalizeRelayUrlsOrdered([...local, ...remote])
}
function blockedNormSet(blockedRelays: string[] | undefined): Set<string> {
return new Set(
(blockedRelays ?? []).map((b) => normalizeAnyRelayUrl(b) || b.trim()).filter(Boolean)
)
}
let socialKindBlockedNormCache: Set<string> | undefined
function socialKindBlockedNormSet(): Set<string> {
if (!socialKindBlockedNormCache) {
socialKindBlockedNormCache = new Set(
SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean)
)
}
return socialKindBlockedNormCache
}
export type MergeRelayPriorityLayersOptions = {
/** When true, drop {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} before applying the max cap. */
applySocialKindBlockedFilter?: boolean
/**
* Normalized relay URLs that stay in the stack even when {@link applySocialKindBlockedFilter} is on — e.g. the
* user’s NIP-65 read list — so an explicit inbox still appears under “Seen on”. ({@link READ_ONLY_RELAY_URLS} such as
* aggr are a separate concern: no publishes, but they are not in {@link SOCIAL_KIND_BLOCKED_RELAY_URLS}.)
*/
exemptNormUrlsFromSocialKindBlock?: Set<string>
}
/**
* Merge priority layers in order; first occurrence wins; skip blocked (and optional social-kind block list); stop at `max`.
*/
export function mergeRelayPriorityLayers(
layers: string[][],
blockedRelays: string[] | undefined,
max: number,
mergeOpts?: MergeRelayPriorityLayersOptions
): string[] {
const blocked = blockedNormSet(blockedRelays)
const socialBlocked = mergeOpts?.applySocialKindBlockedFilter
? socialKindBlockedNormSet()
: new Set<string>()
const socialExempt = mergeOpts?.exemptNormUrlsFromSocialKindBlock
const seen = new Set<string>()
const out: string[] = []
for (const layer of layers) {
for (const u of layer) {
// Must not use {@link normalizeUrl}: it turns http(s) index relays into ws(s), which then hit the WS pool.
const n = normalizeAnyRelayUrl(u) || u.trim()
if (!n || blocked.has(n) || seen.has(n)) continue
if (
socialBlocked.has(n) &&
!(socialExempt?.has(n) ?? false)
) {
continue
}
seen.add(n)
out.push(n)
if (out.length >= max) return out
}
}
return out
}
const normFastRead = (): string[] =>
dedupeNormalizeRelayUrlsOrdered(
FAST_READ_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
)
const normFastWrite = (): string[] =>
dedupeNormalizeRelayUrlsOrdered(
FAST_WRITE_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
)
/**
* Ordered layers for REQ / read (before merge, dedupe, blocked strip, social-kind strip, cap).
*/
export function buildReadRelayPriorityLayers(opts: {
userReadRelays: string[]
userWriteRelays?: string[]
authorWriteRelays?: string[]
favoriteRelays: string[]
}): string[][] {
const userWrite = opts.userWriteRelays ?? []
const writeLocals = userWrite.filter((u) => {
const n = normalizeAnyRelayUrl(u) || u.trim()
return n && isLocalNetworkUrl(n)
})
const userReadOrdered = relayUrlsLocalsFirst(opts.userReadRelays)
const tier1 = dedupeNormalizeRelayUrlsOrdered([...writeLocals, ...userReadOrdered])
const tier2 = dedupeNormalizeRelayUrlsOrdered(opts.authorWriteRelays ?? [])
const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? [])
const tier4 = normFastRead()
return [tier1, tier2, tier3, tier4]
}
/**
* REQ / read: user inboxes (locals first) + user local outboxes → author outboxes → favorites → FAST_READ.
* Blocked and (optionally) social-kind-blocked relays are removed before slicing to `maxRelays`.
*/
export function buildPrioritizedReadRelayUrls(opts: {
userReadRelays: string[]
userWriteRelays?: string[]
authorWriteRelays?: string[]
favoriteRelays: string[]
blockedRelays?: string[]
maxRelays?: number
/** Default true: strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} for social-kind-heavy timelines. Set false for other queries. */
applySocialKindBlockedFilter?: boolean
}): string[] {
const max = opts.maxRelays ?? MAX_REQ_RELAY_URLS
const applySocial = opts.applySocialKindBlockedFilter !== false
const exemptFromSocial = new Set<string>()
for (const u of opts.userReadRelays ?? []) {
const n = normalizeAnyRelayUrl(u) || u.trim()
if (n) exemptFromSocial.add(n)
}
const layers = buildReadRelayPriorityLayers({
userReadRelays: opts.userReadRelays,
userWriteRelays: opts.userWriteRelays,
authorWriteRelays: opts.authorWriteRelays,
favoriteRelays: opts.favoriteRelays
})
return mergeRelayPriorityLayers(layers, opts.blockedRelays, max, {
applySocialKindBlockedFilter: applySocial,
exemptNormUrlsFromSocialKindBlock: exemptFromSocial
})
}
/**
* Ordered layers for publish / write (before merge, blocked strip, kind-1 strip, cap).
*/
function buildWriteRelayPriorityLayers(opts: {
userWriteRelays: string[]
authorReadRelays?: string[]
favoriteRelays?: string[]
extraRelays?: string[]
}): string[][] {
const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays)
const tier2 = dedupeNormalizeRelayUrlsOrdered(opts.authorReadRelays ?? [])
const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? [])
const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? [])
const tier5 = normFastWrite()
const tier6 = normFastRead()
return [tier1, tier2, tier3, tier4, tier5, tier6]
}
/**
* Publish / write: user outboxes (locals first) → target author inboxes → favorites → extras → FAST_WRITE → FAST_READ.
*/
export function buildPrioritizedWriteRelayUrls(opts: {
userWriteRelays: string[]
authorReadRelays?: string[]
favoriteRelays?: string[]
extraRelays?: string[]
blockedRelays?: string[]
maxRelays?: number
/** When true, strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} before capping (social kinds). */
applySocialKindBlockedFilter?: boolean
}): string[] {
const max = opts.maxRelays ?? MAX_PUBLISH_RELAYS
const layers = buildWriteRelayPriorityLayers({
userWriteRelays: opts.userWriteRelays,
authorReadRelays: opts.authorReadRelays,
favoriteRelays: opts.favoriteRelays,
extraRelays: opts.extraRelays
})
return mergeRelayPriorityLayers(layers, opts.blockedRelays, max, {
applySocialKindBlockedFilter: opts.applySocialKindBlockedFilter === true
})
}