rework router

This commit is contained in:
Jon Staab
2024-03-01 10:05:07 -08:00
parent e995141201
commit e7b604711a
+121 -215
View File
@@ -15,47 +15,26 @@ export type RouterOptions = {
getUserPubkey: () => string | null getUserPubkey: () => string | null
getGroupRelays: (address: string) => string[] getGroupRelays: (address: string) => string[]
getCommunityRelays: (address: string) => string[] getCommunityRelays: (address: string) => string[]
getPubkeyInboxRelays: (pubkey: string) => string[] getPubkeyRelays: (pubkey: string, mode?: RelayMode) => string[]
getPubkeyOutboxRelays: (pubkey: string) => string[] getDefaultRelays: (mode?: RelayMode) => string[]
getFallbackInboxRelays: () => string[]
getFallbackOutboxRelays: () => string[]
getRelayQuality?: (url: string) => number getRelayQuality?: (url: string) => number
getDefaultLimit: () => number getDefaultLimit: () => number
} }
// - Fetch from and publish to non-shareable relays, but don't use them for hints export type RouteScores = Record<string, {score: number, count: number}>
// - Test that scoring/sorting makes sense, particularly asc/desc sort
export class Router { export class Router {
constructor(readonly options: RouterOptions) {} constructor(readonly options: RouterOptions) {}
// Utilities derived from options // Utilities derived from options
getAllPubkeyRelays = (pubkey: string) => getUserRelays = (mode?: RelayMode) => {
[
...this.options.getPubkeyInboxRelays(pubkey),
...this.options.getPubkeyOutboxRelays(pubkey),
]
getUserInboxRelays = () => {
const pubkey = this.options.getUserPubkey() const pubkey = this.options.getUserPubkey()
return pubkey ? this.options.getPubkeyInboxRelays(pubkey) : [] return pubkey ? this.options.getPubkeyRelays(pubkey, mode) : []
} }
getUserOutboxRelays = () => { getContextRelayGroups = (event: EventTemplate) => {
const pubkey = this.options.getUserPubkey()
return pubkey ? this.options.getPubkeyOutboxRelays(pubkey) : []
}
getAllUserRelays = () => {
const pubkey = this.options.getUserPubkey()
return pubkey ? this.getAllPubkeyRelays(pubkey) : []
}
getEventContextRelayGroups = (event: EventTemplate) => {
const addresses = Tags.fromEvent(event).context().values().valueOf() const addresses = Tags.fromEvent(event).context().values().valueOf()
return [ return [
@@ -67,7 +46,7 @@ export class Router {
// Utilities for processing hints // Utilities for processing hints
getGroupScores = (groups: string[][]) => { getGroupScores = (groups: string[][]) => {
const scores: RouteScenarioScores = {} const scores: RouteScores = {}
groups.forEach((urls, i) => { groups.forEach((urls, i) => {
for (const url of shuffle(uniq(urls))) { for (const url of shuffle(uniq(urls))) {
@@ -92,193 +71,122 @@ export class Router {
return scores return scores
} }
urlsFromScores = (limit: number, scores: RouteScenarioScores) => urlsFromScores = (scores: RouteScores) =>
Object.entries(scores).sort((a, b) => a[1].score > b[1].score ? -1 : 1).map(pair => pair[0] as string).slice(0, limit) Object.entries(scores).sort((a, b) => a[1].score > b[1].score ? -1 : 1).map(p => p[0] as string)
groupsToUrls = (limit: number, groups: string[][]) => this.urlsFromScores(limit, this.getGroupScores(groups)) groupsToUrls = (groups: string[][]) => this.urlsFromScores(this.getGroupScores(groups))
scenario = (options: RouterScenarioOptions) => new RouterScenario(this, options) scenario = (groups: string[][]) => new RouterScenario(this, groups)
merge = ({fallbackPolicy, scenarios}: {fallbackPolicy: FallbackPolicy, scenarios: RouterScenario[]}) => merge = (scenarios: RouterScenario[]) =>
this.scenario({fallbackPolicy, getGroups: () => scenarios.map(s => s.getRawUrls())}) this.scenario(scenarios.map(scenario => scenario.policy(addNoFallbacks).getUrls()))
// Routing scenarios // Routing scenarios
Broadcast = () => this.scenario({ User = () => this.scenario([])
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
getGroups: () => [this.getAllUserRelays()],
})
Aggregate = () => this.scenario({ Inbox = () => this.scenario([]).mode(RelayMode.Inbox)
fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox),
getGroups: () => [this.getAllUserRelays()],
})
NoteToSelf = () => this.scenario({ Outbox = () => this.scenario([]).mode(RelayMode.Outbox)
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
getGroups: () => [this.getUserInboxRelays()],
})
FetchAllMessages = () => this.scenario({ AllMessages = () => this.scenario([this.getUserRelays()])
fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox),
getGroups: () => [this.getAllUserRelays()],
})
FetchMessages = (pubkeys: string[]) => this.scenario({ Messages = (pubkeys: string[]) =>
fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox), this.scenario([
getGroups: () => [ this.getUserRelays(),
this.getAllUserRelays(), ...pubkeys.map(pubkey => this.options.getPubkeyRelays(pubkey))
...pubkeys.map(this.getAllPubkeyRelays) ])
],
})
PublishMessage = (pubkeys: string[]) => this.scenario({ PublishMessage = (pubkey: string) =>
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox), this.scenario([
getGroups: () => [ this.getUserRelays(RelayMode.Outbox),
this.getUserOutboxRelays(), this.options.getPubkeyRelays(pubkey, RelayMode.Inbox)
...pubkeys.map(this.options.getPubkeyInboxRelays) ]).policy(addMinimalFallbacks)
],
})
FetchEvent = (event: UnsignedEvent) => this.scenario({ Event = (event: UnsignedEvent) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), this.scenario([
getGroups: () => [ this.options.getPubkeyRelays(event.pubkey, RelayMode.Outbox),
this.options.getPubkeyOutboxRelays(event.pubkey), ...this.getContextRelayGroups(event),
...this.getEventContextRelayGroups(event), ])
],
})
FetchEventChildren = (event: UnsignedEvent) => this.scenario({ EventChildren = (event: UnsignedEvent) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), this.scenario([
getGroups: () => [ this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox),
this.options.getPubkeyInboxRelays(event.pubkey), ...this.getContextRelayGroups(event),
...this.getEventContextRelayGroups(event), ])
],
})
FetchEventParent = (event: UnsignedEvent) => this.scenario({ EventParent = (event: UnsignedEvent) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), this.scenario([
getGroups: () => [
Tags.fromEvent(event).replies().relays().valueOf(), Tags.fromEvent(event).replies().relays().valueOf(),
this.options.getPubkeyInboxRelays(event.pubkey), this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox),
...this.getEventContextRelayGroups(event), ...this.getContextRelayGroups(event),
], ])
})
FetchEventRoot = (event: UnsignedEvent) => this.scenario({ EventRoot = (event: UnsignedEvent) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), this.scenario([
getGroups: () => [
Tags.fromEvent(event).roots().relays().valueOf(), Tags.fromEvent(event).roots().relays().valueOf(),
this.options.getPubkeyInboxRelays(event.pubkey), this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox),
...this.getEventContextRelayGroups(event), ...this.getContextRelayGroups(event),
], ])
})
PublishEvent = (event: UnsignedEvent) => this.scenario({ PublishEvent = (event: UnsignedEvent) => {
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox), const tags = Tags.fromEvent(event)
getGroups: () => { const mentions = tags.values("p").valueOf()
const tags = Tags.fromEvent(event) const addresses = tags.context().values().valueOf()
const mentions = tags.values("p").valueOf() const groupAddresses = addresses.filter(isGroupAddress)
const addresses = tags.context().values().valueOf() const communityAddresses = addresses.filter(isCommunityAddress)
const groupAddresses = addresses.filter(isGroupAddress)
const communityAddresses = addresses.filter(isCommunityAddress)
// If we're publishing only to private groups, only publish to those groups' relays. // If we're publishing only to private groups, only publish to those groups' relays.
// Otherwise, publish to all relays, because it's essentially public. // Otherwise, publish to all relays, because it's essentially public.
if (groupAddresses.length > 0 && communityAddresses.length === 0) { if (groupAddresses.length > 0 && communityAddresses.length === 0) {
return groupAddresses.map(this.options.getGroupRelays) return this.scenario(groupAddresses.map(this.options.getGroupRelays))
} }
return [ return this.scenario([
this.options.getPubkeyOutboxRelays(event.pubkey), this.options.getPubkeyRelays(event.pubkey, RelayMode.Outbox),
...groupAddresses.map(this.options.getGroupRelays), ...groupAddresses.map(this.options.getGroupRelays),
...communityAddresses.map(this.options.getCommunityRelays), ...communityAddresses.map(this.options.getCommunityRelays),
...mentions.map((pk: string) => this.options.getPubkeyInboxRelays(pk)), ...mentions.map((pk: string) => this.options.getPubkeyRelays(pk, RelayMode.Inbox)),
] ])
}, }
})
FetchFromHints = (...groups: string[][]) => this.scenario({ FromPubkeys = (pubkeys: string[]) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Outbox)))
getGroups: () => [...groups, this.getAllUserRelays()],
})
FetchFromPubkey = (pubkey: string) => this.scenario({ ForPubkeys = (pubkeys: string[]) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox), this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Inbox)))
getGroups: () => [this.options.getPubkeyOutboxRelays(pubkey)],
})
FetchFromPubkeys = (pubkeys: string[]) => this.scenario({ WithinGroup = (address: string) =>
fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox), this.scenario([this.options.getGroupRelays(address)]).policy(addNoFallbacks)
getGroups: () => pubkeys.map(this.options.getPubkeyOutboxRelays),
})
FetchFromGroup = (address: string) => this.scenario({ WithinCommunity = (address: string) =>
fallbackPolicy: useNoFallbacks(), this.scenario([this.options.getCommunityRelays(address)])
getGroups: () => [this.options.getGroupRelays(address)],
})
PublishToGroup = (address: string) => this.scenario({ WithinContext = (address: string) => {
fallbackPolicy: useNoFallbacks(),
getGroups: () => [this.options.getGroupRelays(address)],
})
FetchFromCommunity = (address: string) => this.scenario({
fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox),
getGroups: () => [this.options.getCommunityRelays(address)],
})
PublishToCommunity = (address: string) => this.scenario({
fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox),
getGroups: () => [this.options.getCommunityRelays(address)],
})
FetchFromContext = (address: string) => {
if (isGroupAddress(address)) { if (isGroupAddress(address)) {
return this.FetchFromGroup(address) return this.WithinGroup(address)
} }
if (isCommunityAddress(address)) { if (isCommunityAddress(address)) {
return this.FetchFromCommunity(address) return this.WithinCommunity(address)
} }
throw new Error(`Unknown context ${address}`) throw new Error(`Unknown context ${address}`)
} }
FetchFromContexts = (addresses: string[]) => WithinMultipleContexts = (addresses: string[]) =>
this.merge({ this.merge(addresses.map(this.WithinContext))
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
scenarios: addresses.map(this.FetchFromContext),
})
PublishToContext = (address: string) => {
if (isGroupAddress(address)) {
return this.PublishToGroup(address)
}
if (isCommunityAddress(address)) {
return this.PublishToCommunity(address)
}
throw new Error(`Unknown context ${address}`)
}
PublishToContexts = (addresses: string[]) =>
this.merge({
fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox),
scenarios: addresses.map(this.PublishToContext),
})
// Higher level utils that use hints // Higher level utils that use hints
tagPubkey = (pubkey: string) => tagPubkey = (pubkey: string) =>
Tag.from(["p", pubkey, this.FetchFromPubkey(pubkey).getUrl()]) Tag.from(["p", pubkey, this.FromPubkeys([pubkey]).getUrl()])
tagEventId = (event: Rumor, ...extra: string[]) => tagEventId = (event: Rumor, ...extra: string[]) =>
Tag.from(["e", event.id, this.FetchEvent(event).getUrl(), ...extra]) Tag.from(["e", event.id, this.Event(event).getUrl(), ...extra])
tagEventAddress = (event: UnsignedEvent, ...extra: string[]) => tagEventAddress = (event: UnsignedEvent, ...extra: string[]) =>
Tag.from(["a", getAddress(event), this.FetchEvent(event).getUrl(), ...extra]) Tag.from(["a", getAddress(event), this.Event(event).getUrl(), ...extra])
tagEvent = (event: Rumor, ...extra: string[]) => { tagEvent = (event: Rumor, ...extra: string[]) => {
const tags = [this.tagEventId(event, ...extra)] const tags = [this.tagEventId(event, ...extra)]
@@ -295,70 +203,68 @@ export class Router {
kind: event.kind, kind: event.kind,
pubkey: event.pubkey, pubkey: event.pubkey,
identifier: Tags.fromEvent(event).get("d")?.value() || "", identifier: Tags.fromEvent(event).get("d")?.value() || "",
relays: this.FetchEvent(event).getUrls(3), relays: this.Event(event).limit(3).getUrls(),
}) })
} }
// Router Scenario // Router Scenario
export type RouterScenarioOptions = { export type RouterScenarioOptions = {
getGroups: () => string[][] mode?: RelayMode
fallbackPolicy: FallbackPolicy limit?: number
policy?: FallbackPolicy
} }
export type RouteScenarioScores = Record<string, {score: number, count: number}>
export class RouterScenario { export class RouterScenario {
constructor(readonly router: Router, readonly options: RouterScenarioOptions) {} constructor(readonly router: Router, readonly groups: string[][], readonly options: RouterScenarioOptions = {}) {}
clone = (options: RouterScenarioOptions) =>
new RouterScenario(this.router, this.groups, {...this.options, ...options})
limit = (limit: number) => this.clone({limit})
mode = (mode: RelayMode) => this.clone({mode})
policy = (policy: FallbackPolicy) => this.clone({policy})
getLimit = () => this.options.limit || this.router.options.getDefaultLimit()
getPolicy = () => this.options.policy || addMaximalFallbacks
getFallbackRelays = () => { getFallbackRelays = () => {
switch (this.options.fallbackPolicy.mode) { const userRelays = shuffle(this.router.getUserRelays(this.options.mode))
case RelayMode.Inbox: const defaultRelays = shuffle(this.router.options.getDefaultRelays(this.options.mode))
return this.router.options.getFallbackInboxRelays()
case RelayMode.Outbox: return uniq([...userRelays, ...defaultRelays])
return this.router.options.getFallbackOutboxRelays()
default:
throw new Error(`Invalid relay mode ${this.options.fallbackPolicy.mode}`)
}
} }
addFallbacks = (limit: number, urls: string[]) => { getUrls = () => {
if (urls.length < limit) { const fallbackPolicy = this.getPolicy()
const fallbackRelays = this.getFallbackRelays() const limit = fallbackPolicy(this.getLimit())
const fallbackLimit = this.options.fallbackPolicy.getLimit(limit, urls) const urls = this.router.groupsToUrls(this.groups)
return [...urls, ...fallbackRelays.slice(0, fallbackLimit)] for (const url of this.getFallbackRelays()) {
if (urls.length >= limit) {
break
}
if (!urls.includes(url)) {
urls.push(url)
}
} }
return urls return urls.slice(0, limit)
} }
getRawUrls = (limit?: number, extra: string[] = []) => { getUrl = () => first(this.limit(1).getUrls())
const maxRelays = limit || this.router.options.getDefaultLimit()
const urlGroups = this.options.getGroups().concat([extra])
return this.router.groupsToUrls(maxRelays, urlGroups)
}
getUrls = (limit?: number, extra: string[] = []) => {
const maxRelays = limit || this.router.options.getDefaultLimit()
const urlGroups = [extra].concat(this.options.getGroups())
const urls = this.router.groupsToUrls(maxRelays, urlGroups)
return this.addFallbacks(maxRelays, urls)
}
getUrl = () => first(this.getUrls(1))
} }
// Fallback Policy // Fallback Policy
export class FallbackPolicy { export type FallbackPolicy = (limit: number) => number
constructor(readonly mode: string, readonly getLimit: (limit: number, urls: string[]) => number) {}
}
export const useNoFallbacks = () => new FallbackPolicy(RelayMode.Inbox, (limit: number, urls: string[]) => 0) export const addNoFallbacks = (limit: number) => 0
export const useMinimalFallbacks = (mode: string) => new FallbackPolicy(mode, (limit: number, urls: string[]) => urls.length === 0 ? 1 : 0) export const addMinimalFallbacks = (limit: number) => 1
export const useMaximalFallbacks = (mode: string) => new FallbackPolicy(mode, (limit: number, urls: string[]) => Math.max(0, limit - urls.length)) export const addMaximalFallbacks = (limit: number) => limit