From e7b604711a6d9815213422b5542ab583a3c1dc01 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 1 Mar 2024 10:05:07 -0800 Subject: [PATCH] rework router --- src/util/Router.ts | 336 ++++++++++++++++----------------------------- 1 file changed, 121 insertions(+), 215 deletions(-) diff --git a/src/util/Router.ts b/src/util/Router.ts index de2978f..d17cc1a 100644 --- a/src/util/Router.ts +++ b/src/util/Router.ts @@ -15,47 +15,26 @@ export type RouterOptions = { getUserPubkey: () => string | null getGroupRelays: (address: string) => string[] getCommunityRelays: (address: string) => string[] - getPubkeyInboxRelays: (pubkey: string) => string[] - getPubkeyOutboxRelays: (pubkey: string) => string[] - getFallbackInboxRelays: () => string[] - getFallbackOutboxRelays: () => string[] + getPubkeyRelays: (pubkey: string, mode?: RelayMode) => string[] + getDefaultRelays: (mode?: RelayMode) => string[] getRelayQuality?: (url: string) => number getDefaultLimit: () => number } -// - Fetch from and publish to non-shareable relays, but don't use them for hints -// - Test that scoring/sorting makes sense, particularly asc/desc sort +export type RouteScores = Record export class Router { constructor(readonly options: RouterOptions) {} // Utilities derived from options - getAllPubkeyRelays = (pubkey: string) => - [ - ...this.options.getPubkeyInboxRelays(pubkey), - ...this.options.getPubkeyOutboxRelays(pubkey), - ] - - getUserInboxRelays = () => { + getUserRelays = (mode?: RelayMode) => { const pubkey = this.options.getUserPubkey() - return pubkey ? this.options.getPubkeyInboxRelays(pubkey) : [] + return pubkey ? this.options.getPubkeyRelays(pubkey, mode) : [] } - getUserOutboxRelays = () => { - 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) => { + getContextRelayGroups = (event: EventTemplate) => { const addresses = Tags.fromEvent(event).context().values().valueOf() return [ @@ -67,7 +46,7 @@ export class Router { // Utilities for processing hints getGroupScores = (groups: string[][]) => { - const scores: RouteScenarioScores = {} + const scores: RouteScores = {} groups.forEach((urls, i) => { for (const url of shuffle(uniq(urls))) { @@ -92,193 +71,122 @@ export class Router { return scores } - urlsFromScores = (limit: number, scores: RouteScenarioScores) => - Object.entries(scores).sort((a, b) => a[1].score > b[1].score ? -1 : 1).map(pair => pair[0] as string).slice(0, limit) + urlsFromScores = (scores: RouteScores) => + 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[]}) => - this.scenario({fallbackPolicy, getGroups: () => scenarios.map(s => s.getRawUrls())}) + merge = (scenarios: RouterScenario[]) => + this.scenario(scenarios.map(scenario => scenario.policy(addNoFallbacks).getUrls())) // Routing scenarios - Broadcast = () => this.scenario({ - fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox), - getGroups: () => [this.getAllUserRelays()], - }) + User = () => this.scenario([]) - Aggregate = () => this.scenario({ - fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox), - getGroups: () => [this.getAllUserRelays()], - }) + Inbox = () => this.scenario([]).mode(RelayMode.Inbox) - NoteToSelf = () => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), - getGroups: () => [this.getUserInboxRelays()], - }) + Outbox = () => this.scenario([]).mode(RelayMode.Outbox) - FetchAllMessages = () => this.scenario({ - fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox), - getGroups: () => [this.getAllUserRelays()], - }) + AllMessages = () => this.scenario([this.getUserRelays()]) - FetchMessages = (pubkeys: string[]) => this.scenario({ - fallbackPolicy: useMinimalFallbacks(RelayMode.Inbox), - getGroups: () => [ - this.getAllUserRelays(), - ...pubkeys.map(this.getAllPubkeyRelays) - ], - }) + Messages = (pubkeys: string[]) => + this.scenario([ + this.getUserRelays(), + ...pubkeys.map(pubkey => this.options.getPubkeyRelays(pubkey)) + ]) - PublishMessage = (pubkeys: string[]) => this.scenario({ - fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox), - getGroups: () => [ - this.getUserOutboxRelays(), - ...pubkeys.map(this.options.getPubkeyInboxRelays) - ], - }) + PublishMessage = (pubkey: string) => + this.scenario([ + this.getUserRelays(RelayMode.Outbox), + this.options.getPubkeyRelays(pubkey, RelayMode.Inbox) + ]).policy(addMinimalFallbacks) - FetchEvent = (event: UnsignedEvent) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), - getGroups: () => [ - this.options.getPubkeyOutboxRelays(event.pubkey), - ...this.getEventContextRelayGroups(event), - ], - }) + Event = (event: UnsignedEvent) => + this.scenario([ + this.options.getPubkeyRelays(event.pubkey, RelayMode.Outbox), + ...this.getContextRelayGroups(event), + ]) - FetchEventChildren = (event: UnsignedEvent) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), - getGroups: () => [ - this.options.getPubkeyInboxRelays(event.pubkey), - ...this.getEventContextRelayGroups(event), - ], - }) + EventChildren = (event: UnsignedEvent) => + this.scenario([ + this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox), + ...this.getContextRelayGroups(event), + ]) - FetchEventParent = (event: UnsignedEvent) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), - getGroups: () => [ + EventParent = (event: UnsignedEvent) => + this.scenario([ Tags.fromEvent(event).replies().relays().valueOf(), - this.options.getPubkeyInboxRelays(event.pubkey), - ...this.getEventContextRelayGroups(event), - ], - }) + this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox), + ...this.getContextRelayGroups(event), + ]) - FetchEventRoot = (event: UnsignedEvent) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), - getGroups: () => [ + EventRoot = (event: UnsignedEvent) => + this.scenario([ Tags.fromEvent(event).roots().relays().valueOf(), - this.options.getPubkeyInboxRelays(event.pubkey), - ...this.getEventContextRelayGroups(event), - ], - }) + this.options.getPubkeyRelays(event.pubkey, RelayMode.Inbox), + ...this.getContextRelayGroups(event), + ]) - PublishEvent = (event: UnsignedEvent) => this.scenario({ - fallbackPolicy: useMinimalFallbacks(RelayMode.Outbox), - getGroups: () => { - const tags = Tags.fromEvent(event) - const mentions = tags.values("p").valueOf() - const addresses = tags.context().values().valueOf() - const groupAddresses = addresses.filter(isGroupAddress) - const communityAddresses = addresses.filter(isCommunityAddress) + PublishEvent = (event: UnsignedEvent) => { + const tags = Tags.fromEvent(event) + const mentions = tags.values("p").valueOf() + const addresses = tags.context().values().valueOf() + const groupAddresses = addresses.filter(isGroupAddress) + const communityAddresses = addresses.filter(isCommunityAddress) - // If we're publishing only to private groups, only publish to those groups' relays. - // Otherwise, publish to all relays, because it's essentially public. - if (groupAddresses.length > 0 && communityAddresses.length === 0) { - return groupAddresses.map(this.options.getGroupRelays) - } + // If we're publishing only to private groups, only publish to those groups' relays. + // Otherwise, publish to all relays, because it's essentially public. + if (groupAddresses.length > 0 && communityAddresses.length === 0) { + return this.scenario(groupAddresses.map(this.options.getGroupRelays)) + } - return [ - this.options.getPubkeyOutboxRelays(event.pubkey), - ...groupAddresses.map(this.options.getGroupRelays), - ...communityAddresses.map(this.options.getCommunityRelays), - ...mentions.map((pk: string) => this.options.getPubkeyInboxRelays(pk)), - ] - }, - }) + return this.scenario([ + this.options.getPubkeyRelays(event.pubkey, RelayMode.Outbox), + ...groupAddresses.map(this.options.getGroupRelays), + ...communityAddresses.map(this.options.getCommunityRelays), + ...mentions.map((pk: string) => this.options.getPubkeyRelays(pk, RelayMode.Inbox)), + ]) + } - FetchFromHints = (...groups: string[][]) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Inbox), - getGroups: () => [...groups, this.getAllUserRelays()], - }) + FromPubkeys = (pubkeys: string[]) => + this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Outbox))) - FetchFromPubkey = (pubkey: string) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox), - getGroups: () => [this.options.getPubkeyOutboxRelays(pubkey)], - }) + ForPubkeys = (pubkeys: string[]) => + this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Inbox))) - FetchFromPubkeys = (pubkeys: string[]) => this.scenario({ - fallbackPolicy: useMaximalFallbacks(RelayMode.Outbox), - getGroups: () => pubkeys.map(this.options.getPubkeyOutboxRelays), - }) + WithinGroup = (address: string) => + this.scenario([this.options.getGroupRelays(address)]).policy(addNoFallbacks) - FetchFromGroup = (address: string) => this.scenario({ - fallbackPolicy: useNoFallbacks(), - getGroups: () => [this.options.getGroupRelays(address)], - }) + WithinCommunity = (address: string) => + this.scenario([this.options.getCommunityRelays(address)]) - PublishToGroup = (address: string) => this.scenario({ - 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) => { + WithinContext = (address: string) => { if (isGroupAddress(address)) { - return this.FetchFromGroup(address) + return this.WithinGroup(address) } if (isCommunityAddress(address)) { - return this.FetchFromCommunity(address) + return this.WithinCommunity(address) } throw new Error(`Unknown context ${address}`) } - FetchFromContexts = (addresses: string[]) => - this.merge({ - 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), - }) + WithinMultipleContexts = (addresses: string[]) => + this.merge(addresses.map(this.WithinContext)) // Higher level utils that use hints tagPubkey = (pubkey: string) => - Tag.from(["p", pubkey, this.FetchFromPubkey(pubkey).getUrl()]) + Tag.from(["p", pubkey, this.FromPubkeys([pubkey]).getUrl()]) 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[]) => - 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[]) => { const tags = [this.tagEventId(event, ...extra)] @@ -295,70 +203,68 @@ export class Router { kind: event.kind, pubkey: event.pubkey, identifier: Tags.fromEvent(event).get("d")?.value() || "", - relays: this.FetchEvent(event).getUrls(3), + relays: this.Event(event).limit(3).getUrls(), }) } // Router Scenario export type RouterScenarioOptions = { - getGroups: () => string[][] - fallbackPolicy: FallbackPolicy + mode?: RelayMode + limit?: number + policy?: FallbackPolicy } -export type RouteScenarioScores = Record - 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 = () => { - switch (this.options.fallbackPolicy.mode) { - case RelayMode.Inbox: - return this.router.options.getFallbackInboxRelays() - case RelayMode.Outbox: - return this.router.options.getFallbackOutboxRelays() - default: - throw new Error(`Invalid relay mode ${this.options.fallbackPolicy.mode}`) - } + const userRelays = shuffle(this.router.getUserRelays(this.options.mode)) + const defaultRelays = shuffle(this.router.options.getDefaultRelays(this.options.mode)) + + return uniq([...userRelays, ...defaultRelays]) } - addFallbacks = (limit: number, urls: string[]) => { - if (urls.length < limit) { - const fallbackRelays = this.getFallbackRelays() - const fallbackLimit = this.options.fallbackPolicy.getLimit(limit, urls) + getUrls = () => { + const fallbackPolicy = this.getPolicy() + const limit = fallbackPolicy(this.getLimit()) + 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[] = []) => { - 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)) + getUrl = () => first(this.limit(1).getUrls()) } // Fallback Policy -export class FallbackPolicy { - constructor(readonly mode: string, readonly getLimit: (limit: number, urls: string[]) => number) {} -} +export type FallbackPolicy = (limit: number) => 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