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
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 |
|
}) |
|
}
|
|
|