From 8ee7e56039645ec0e7d10117f1eb4f01b043b8ef Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Tue, 7 Mar 2023 19:25:24 -0600 Subject: [PATCH] setup basic pubkey relay assignment service --- src/classes/subject.ts | 5 ++ src/hooks/use-fallback-user-relays.tsx | 27 +++--- src/services/pubkey-relay-assignment.ts | 115 ++++++++++++++++++++++++ src/services/user-contacts.ts | 5 +- src/services/user-metadata.ts | 3 + src/services/user-relays-fallback.ts | 41 +++++++++ src/services/user-relays.ts | 6 +- 7 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 src/services/pubkey-relay-assignment.ts create mode 100644 src/services/user-relays-fallback.ts diff --git a/src/classes/subject.ts b/src/classes/subject.ts index f65686bde..2898d200e 100644 --- a/src/classes/subject.ts +++ b/src/classes/subject.ts @@ -87,6 +87,11 @@ export class Subject implements Connectable { } return this; } + disconnectAll() { + for (const [connectable, listener] of this.upstream) { + this.disconnect(connectable); + } + } } export class PersistentSubject extends Subject implements ConnectableApi { diff --git a/src/hooks/use-fallback-user-relays.tsx b/src/hooks/use-fallback-user-relays.tsx index d1e368605..d28504d9e 100644 --- a/src/hooks/use-fallback-user-relays.tsx +++ b/src/hooks/use-fallback-user-relays.tsx @@ -1,22 +1,17 @@ import { useMemo } from "react"; -import { RelayConfig } from "../classes/relay"; import { normalizeRelayConfigs } from "../helpers/relay"; -import { useUserContacts } from "./use-user-contacts"; -import { useUserRelays } from "./use-user-relays"; +import userRelaysFallbackService from "../services/user-relays-fallback"; +import { useReadRelayUrls } from "./use-client-relays"; +import useSubject from "./use-subject"; -export default function useFallbackUserRelays(pubkey: string, alwaysFetch = false) { - const contacts = useUserContacts(pubkey, [], alwaysFetch); - const userRelays = useUserRelays(pubkey, [], alwaysFetch); +export default function useFallbackUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysFetch = false) { + const readRelays = useReadRelayUrls(additionalRelays); - return useMemo(() => { - let relays: RelayConfig[] = userRelays?.relays ?? []; + const observable = useMemo( + () => userRelaysFallbackService.requestRelays(pubkey, readRelays, alwaysFetch), + [pubkey, readRelays.join("|"), alwaysFetch] + ); + const userRelays = useSubject(observable); - // use the relays stored in contacts if there are no relay config - if (relays.length === 0 && contacts) { - relays = contacts.relays; - } - - // normalize relay urls and remove bad ones - return normalizeRelayConfigs(relays); - }, [userRelays, contacts]); + return userRelays ? normalizeRelayConfigs(userRelays.relays) : []; } diff --git a/src/services/pubkey-relay-assignment.ts b/src/services/pubkey-relay-assignment.ts new file mode 100644 index 000000000..4ace63e9b --- /dev/null +++ b/src/services/pubkey-relay-assignment.ts @@ -0,0 +1,115 @@ +import { RelayMode } from "../classes/relay"; +import Subject, { PersistentSubject } from "../classes/subject"; +import { SuperMap } from "../classes/super-map"; +import { unique } from "../helpers/array"; +import accountService from "./account"; +import clientRelaysService from "./client-relays"; +import relayScoreboardService from "./relay-scoreboard"; +import userContactsService, { UserContacts } from "./user-contacts"; +import { UserRelays } from "./user-relays"; +import userRelaysFallbackService from "./user-relays-fallback"; + +type pubkey = string; +type relay = string; + +class PubkeyRelayAssignmentService { + pubkeys = new Map(); + pubkeyRelays = new SuperMap>(() => new Subject()); + assignments = new PersistentSubject>({}); + + constructor() { + let sub: Subject; + + accountService.current.subscribe((account) => { + if (sub) { + sub.unsubscribe(this.handleUserContacts, this); + } + if (account) { + this.pubkeys.clear(); + this.pubkeyRelays.clear(); + const contactsSub = userContactsService.requestContacts(account.pubkey, account.relays ?? []); + contactsSub.subscribe(this.handleUserContacts, this); + sub = contactsSub; + } + }); + } + + private handleUserContacts(contacts: UserContacts) { + for (const pubkey of contacts.contacts) { + const relay = contacts.contactRelay[pubkey]; + pubkeyRelayAssignmentService.addPubkey(pubkey, relay ? [relay] : []); + } + } + + addPubkey(pubkey: string, relays: string[] = []) { + if (this.pubkeys.has(pubkey)) return; + this.pubkeys.set(pubkey, relays); + + const readRelays = clientRelaysService.getReadUrls(); + const subject = userRelaysFallbackService.requestRelays(pubkey, unique([...readRelays, ...relays])); + this.pubkeyRelays.set(pubkey, subject); + // subject.subscribe(this.updateAssignments, this); + } + removePubkey(pubkey: string) { + if (!this.pubkeys.has(pubkey)) return; + + this.pubkeys.delete(pubkey); + this.pubkeyRelays.delete(pubkey); + } + + updateAssignments() { + const allRelays = new Set(); + + for (const [pubkey, userRelays] of this.pubkeyRelays) { + if (!userRelays.value) continue; + for (const relayConfig of userRelays.value.relays) { + // only use relays the users are writing to + if (relayConfig.mode & RelayMode.WRITE) { + allRelays.add(relayConfig.url); + } + } + } + + const relayScores = new Map(); + for (const relay of allRelays) { + relayScores.set(relay, relayScoreboardService.getRelayScore(relay)); + } + + const readRelays = clientRelaysService.getReadUrls(); + const assignments: Record = {}; + for (const [pubkey] of this.pubkeys) { + let userRelays = + this.pubkeyRelays + .get(pubkey) + .value?.relays.filter((r) => r.mode & RelayMode.WRITE) + .map((r) => r.url) ?? []; + + if (userRelays.length === 0) userRelays = Array.from(readRelays); + + const rankedOptions = Array.from(userRelays).sort( + (a, b) => (relayScores.get(b) ?? 0) - (relayScores.get(a) ?? 0) + ); + + assignments[pubkey] = rankedOptions.slice(0, 3); + + for (const relay of assignments[pubkey]) { + relayScores.set(relay, (relayScores.get(relay) ?? 0) + 1); + } + } + + this.assignments.next(assignments); + } +} + +const pubkeyRelayAssignmentService = new PubkeyRelayAssignmentService(); + +setInterval(() => { + pubkeyRelayAssignmentService.updateAssignments(); +}, 1000 * 5); + +if (import.meta.env.DEV) { + //@ts-ignore + window.pubkeyRelayAssignmentService = pubkeyRelayAssignmentService; +} + +export default pubkeyRelayAssignmentService; diff --git a/src/services/user-contacts.ts b/src/services/user-contacts.ts index 0090abbea..48ac69771 100644 --- a/src/services/user-contacts.ts +++ b/src/services/user-contacts.ts @@ -5,6 +5,7 @@ import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-reque import { SuperMap } from "../classes/super-map"; import Subject from "../classes/subject"; import { RelayConfig, RelayMode } from "../classes/relay"; +import { normalizeRelayConfigs } from "../helpers/relay"; export type UserContacts = { pubkey: string; @@ -15,7 +16,7 @@ export type UserContacts = { }; type RelayJson = Record; -function relayJsonToRelayConfig(relayJson: RelayJson) { +function relayJsonToRelayConfig(relayJson: RelayJson): RelayConfig[] { try { return Array.from(Object.entries(relayJson)).map(([url, opts]) => ({ url, @@ -27,7 +28,7 @@ function relayJsonToRelayConfig(relayJson: RelayJson) { function parseContacts(event: NostrEvent): UserContacts { const relayJson = safeJson(event.content, {}) as RelayJson; - const relays = relayJsonToRelayConfig(relayJson); + const relays = normalizeRelayConfigs(relayJsonToRelayConfig(relayJson)); const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]); const contactRelay = event.tags.filter(isPTag).reduce((dir, tag) => { diff --git a/src/services/user-metadata.ts b/src/services/user-metadata.ts index 7d28b103d..d66925c40 100644 --- a/src/services/user-metadata.ts +++ b/src/services/user-metadata.ts @@ -21,6 +21,9 @@ class UserMetadataService { } private parsedSubjects = new SuperMap>(() => new Subject()); + getSubject(pubkey: string) { + return this.parsedSubjects.get(pubkey); + } requestMetadata(pubkey: string, relays: string[], alwaysRequest = false) { const sub = this.parsedSubjects.get(pubkey); const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest); diff --git a/src/services/user-relays-fallback.ts b/src/services/user-relays-fallback.ts new file mode 100644 index 000000000..74a746226 --- /dev/null +++ b/src/services/user-relays-fallback.ts @@ -0,0 +1,41 @@ +import Subject from "../classes/subject"; +import { normalizeRelayConfigs } from "../helpers/relay"; +import userContactsService from "./user-contacts"; +import userRelaysService, { UserRelays } from "./user-relays"; + +class UserRelaysFallbackService { + subjects = new Map>(); + + requestRelays(pubkey: string, relays: string[], alwaysFetch = false) { + let subject = this.subjects.get(pubkey); + if (!subject) { + subject = new Subject(); + this.subjects.set(pubkey, subject); + + subject.connectWithHandler(userRelaysService.getSubject(pubkey), (userRelays, next, value) => { + if (!value || userRelays.created_at > value.created_at) { + next(userRelays); + } + }); + subject.connectWithHandler(userContactsService.getSubject(pubkey), (contacts, next, value) => { + if (!value || contacts.created_at > value.created_at) { + next({ pubkey: contacts.pubkey, relays: contacts.relays, created_at: contacts.created_at }); + } + }); + } + + userRelaysService.requestRelays(pubkey, relays, alwaysFetch); + userContactsService.requestContacts(pubkey, relays, alwaysFetch); + + return subject; + } +} + +const userRelaysFallbackService = new UserRelaysFallbackService(); + +if (import.meta.env.DEV) { + // @ts-ignore + window.userRelaysFallbackService = userRelaysFallbackService; +} + +export default userRelaysFallbackService; diff --git a/src/services/user-relays.ts b/src/services/user-relays.ts index 66950cd94..dfe46c9a0 100644 --- a/src/services/user-relays.ts +++ b/src/services/user-relays.ts @@ -5,6 +5,7 @@ import { parseRTag } from "../helpers/nostr-event"; import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester"; import { SuperMap } from "../classes/super-map"; import Subject from "../classes/subject"; +import { normalizeRelayConfigs } from "../helpers/relay"; export type UserRelays = { pubkey: string; @@ -15,7 +16,7 @@ export type UserRelays = { function parseRelaysEvent(event: NostrEvent): UserRelays { return { pubkey: event.pubkey, - relays: event.tags.filter(isRTag).map(parseRTag), + relays: normalizeRelayConfigs(event.tags.filter(isRTag).map(parseRTag)), created_at: event.created_at, }; } @@ -36,6 +37,9 @@ class UserRelaysService { } private subjects = new SuperMap>(() => new Subject()); + getSubject(pubkey: string) { + return this.subjects.get(pubkey); + } requestRelays(pubkey: string, relays: string[], alwaysRequest = false) { const sub = this.subjects.get(pubkey); const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest);