5 changed files with 142 additions and 28 deletions
@ -0,0 +1,27 @@ |
|||||||
|
import { describe, expect, it } from 'vitest' |
||||||
|
import { buildRandomPublishRelayCandidateList } from './random-publish-relay-pool' |
||||||
|
|
||||||
|
describe('buildRandomPublishRelayCandidateList', () => { |
||||||
|
it('fills from fallback write relays when NIP-66 list is empty', () => { |
||||||
|
const candidates = buildRandomPublishRelayCandidateList({ |
||||||
|
excludeSessionKeys: new Set(['wss://relay.user.example']), |
||||||
|
sessionBoost: [], |
||||||
|
nip66Lively: [], |
||||||
|
fallbackWriteRelays: ['wss://alpha.example', 'wss://beta.example', 'wss://gamma.example'] |
||||||
|
}) |
||||||
|
expect(candidates.length).toBeGreaterThanOrEqual(3) |
||||||
|
expect(candidates.some((u) => u.includes('alpha.example'))).toBe(true) |
||||||
|
expect(candidates.some((u) => u.includes('relay.user.example'))).toBe(false) |
||||||
|
}) |
||||||
|
|
||||||
|
it('dedupes session boost and NIP-66 entries', () => { |
||||||
|
const candidates = buildRandomPublishRelayCandidateList({ |
||||||
|
excludeSessionKeys: new Set(), |
||||||
|
sessionBoost: ['wss://same.example/'], |
||||||
|
nip66Lively: ['wss://same.example'], |
||||||
|
fallbackWriteRelays: [] |
||||||
|
}) |
||||||
|
expect(candidates).toHaveLength(1) |
||||||
|
expect(candidates[0]).toMatch(/same\.example/) |
||||||
|
}) |
||||||
|
}) |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
import { RANDOM_PUBLISH_RELAY_COUNT } from '@/constants' |
||||||
|
import { canonicalRelaySessionKey, normalizeAnyRelayUrl, normalizeRelayUrlByScheme } from '@/lib/url' |
||||||
|
|
||||||
|
export function normalizePublishRelayCandidate(url: string): string { |
||||||
|
return normalizeRelayUrlByScheme(normalizeAnyRelayUrl(url) || url) || url.trim() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Ordered candidate pool for optional random publish relays (NIP-66 lively, then write fallbacks). |
||||||
|
* Excludes relays already in the user's publish picker list ({@link excludeSessionKeys}). |
||||||
|
*/ |
||||||
|
export function buildRandomPublishRelayCandidateList(args: { |
||||||
|
excludeSessionKeys: ReadonlySet<string> |
||||||
|
sessionBoost: readonly string[] |
||||||
|
nip66Lively: readonly string[] |
||||||
|
fallbackWriteRelays: readonly string[] |
||||||
|
/** Upper bound on candidates before {@link pickRandomPublishRelays} narrows to {@link RANDOM_PUBLISH_RELAY_COUNT}. */ |
||||||
|
maxCandidates?: number |
||||||
|
}): string[] { |
||||||
|
const seen = new Set<string>() |
||||||
|
const out: string[] = [] |
||||||
|
const max = args.maxCandidates ?? RANDOM_PUBLISH_RELAY_COUNT * 8 |
||||||
|
|
||||||
|
const push = (raw: string) => { |
||||||
|
if (out.length >= max) return |
||||||
|
const normalized = normalizePublishRelayCandidate(raw) |
||||||
|
const key = canonicalRelaySessionKey(normalized) |
||||||
|
if (!key || args.excludeSessionKeys.has(key) || seen.has(key)) return |
||||||
|
seen.add(key) |
||||||
|
out.push(normalized) |
||||||
|
} |
||||||
|
|
||||||
|
for (const u of args.sessionBoost) push(u) |
||||||
|
for (const u of args.nip66Lively) push(u) |
||||||
|
if (out.length < RANDOM_PUBLISH_RELAY_COUNT) { |
||||||
|
for (const u of args.fallbackWriteRelays) push(u) |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
Loading…
Reference in new issue