setup basic pubkey relay assignment service

This commit is contained in:
hzrd149
2023-03-07 19:25:24 -06:00
parent 697af2c451
commit 8ee7e56039
7 changed files with 183 additions and 19 deletions

View File

@@ -87,6 +87,11 @@ export class Subject<Value> implements Connectable<Value> {
}
return this;
}
disconnectAll() {
for (const [connectable, listener] of this.upstream) {
this.disconnect(connectable);
}
}
}
export class PersistentSubject<Value> extends Subject<Value> implements ConnectableApi<Value> {

View File

@@ -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) : [];
}

View File

@@ -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<pubkey, relay[]>();
pubkeyRelays = new SuperMap<string, Subject<UserRelays>>(() => new Subject());
assignments = new PersistentSubject<Record<pubkey, relay[]>>({});
constructor() {
let sub: Subject<UserContacts>;
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<relay>();
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<relay, number>();
for (const relay of allRelays) {
relayScores.set(relay, relayScoreboardService.getRelayScore(relay));
}
const readRelays = clientRelaysService.getReadUrls();
const assignments: Record<pubkey, relay[]> = {};
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;

View File

@@ -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<string, { read: boolean; write: boolean }>;
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) => {

View File

@@ -21,6 +21,9 @@ class UserMetadataService {
}
private parsedSubjects = new SuperMap<string, Subject<Kind0ParsedContent>>(() => new Subject<Kind0ParsedContent>());
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);

View File

@@ -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<string, Subject<UserRelays>>();
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;

View File

@@ -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<string, Subject<UserRelays>>(() => new Subject<UserRelays>());
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);