cleanup fetching user relays

This commit is contained in:
hzrd149 2023-06-05 08:43:32 -04:00
parent 4506c82bd0
commit d4aef8f8d6
17 changed files with 82 additions and 167 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
cleanup fetching user relays

View File

@ -19,22 +19,22 @@ export class CachedPubkeyEventRequester extends PubkeyEventRequester {
requestEvent(pubkey: string, relays: string[], alwaysRequest = false) {
const sub = this.getSubject(pubkey);
if (!sub.value || alwaysRequest) {
if (!sub.value) {
// only call this.readCache once per pubkey
const promise = this.readCacheDedupe.get(pubkey) || this.readCache(pubkey);
this.readCacheDedupe.set(pubkey, promise);
if (!this.readCacheDedupe.has(pubkey)) {
const promise = this.readCacheDedupe.get(pubkey) || this.readCache(pubkey);
this.readCacheDedupe.set(pubkey, promise);
promise.then((cached) => {
this.readCacheDedupe.delete(pubkey);
promise.then((cached) => {
this.readCacheDedupe.delete(pubkey);
if (cached && (!sub.value || cached.created_at > sub.value.created_at)) {
sub.next(cached);
}
if (cached) this.handleEvent(cached);
if (!sub.value || alwaysRequest) {
super.requestEvent(pubkey, relays, alwaysRequest);
}
});
if (!sub.value || alwaysRequest) super.requestEvent(pubkey, relays);
});
}
} else if (alwaysRequest) {
super.requestEvent(pubkey, relays);
}
return sub;

View File

@ -53,10 +53,10 @@ class PubkeyEventRequestSubscription {
return this.subjects.get(pubkey);
}
requestEvent(pubkey: string, alwaysRequest = false) {
requestEvent(pubkey: string) {
const sub = this.subjects.get(pubkey);
if (!sub.value || alwaysRequest) {
if (!sub.value) {
this.requestNext.add(pubkey);
}
@ -129,11 +129,11 @@ export class PubkeyEventRequester {
}
private connected = new WeakSet<any>();
requestEvent(pubkey: string, relays: string[], alwaysRequest = false) {
requestEvent(pubkey: string, relays: string[]) {
const sub = this.subjects.get(pubkey);
for (const relay of relays) {
const relaySub = this.subscriptions.get(relay).requestEvent(pubkey, alwaysRequest);
const relaySub = this.subscriptions.get(relay).requestEvent(pubkey);
if (!this.connected.has(relaySub)) {
relaySub.subscribe((event) => event && this.handleEvent(event));

View File

@ -5,10 +5,14 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import RawValue from "./raw-value";
import RawJson from "./raw-json";
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
import userRelaysService from "../../services/user-relays";
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]);
const metadata = useUserMetadata(pubkey);
const nprofile = useSharableProfileId(pubkey);
const relays = userRelaysService.requester.getSubject(pubkey).value;
return (
<Modal {...props}>
@ -18,8 +22,10 @@ export default function UserDebugModal({ pubkey, ...props }: { pubkey: string }
<ModalBody overflow="auto" p="4">
<Flex gap="2" direction="column">
<RawValue heading="Hex pubkey" value={pubkey} />
{npub && <RawValue heading="Encoded pubkey (NIP-19)" value={npub} />}
<RawJson heading="Metadata (kind 0)" json={metadata} />
{npub && <RawValue heading="npub" value={npub} />}
<RawValue heading="nprofile" value={nprofile} />
<RawJson heading="Parsed Metadata (kind 0)" json={metadata} />
{relays && <RawJson heading="Relay List (kind 10002)" json={relays} />}
</Flex>
</ModalBody>
</ModalContent>

View File

@ -1,17 +0,0 @@
import { useMemo } from "react";
import { normalizeRelayConfigs } from "../helpers/relay";
import userRelaysFallbackService from "../services/user-relays-fallback";
import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
export default function useFallbackUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysFetch = false) {
const readRelays = useReadRelayUrls(additionalRelays);
const observable = useMemo(
() => userRelaysFallbackService.requestRelays(pubkey, readRelays, alwaysFetch),
[pubkey, readRelays.join("|"), alwaysFetch]
);
const userRelays = useSubject(observable);
return userRelays ? normalizeRelayConfigs(userRelays.relays) : [];
}

View File

@ -1,11 +1,11 @@
import { useMemo } from "react";
import useFallbackUserRelays from "./use-fallback-user-relays";
import relayScoreboardService from "../services/relay-scoreboard";
import { RelayMode } from "../classes/relay";
import { nip19 } from "nostr-tools";
import { useUserRelays } from "./use-user-relays";
export function useSharableProfileId(pubkey: string) {
const userRelays = useFallbackUserRelays(pubkey);
const userRelays = useUserRelays(pubkey);
return useMemo(() => {
const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);

View File

@ -1,16 +1,15 @@
import { useMemo } from "react";
import userRelaysService from "../services/user-relays";
import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
import { useReadRelayUrls } from "./use-client-relays";
export function useUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
const readRelays = useReadRelayUrls(additionalRelays);
const observable = useMemo(
() => userRelaysService.requestRelays(pubkey, readRelays, alwaysRequest),
[pubkey, readRelays.join("|"), alwaysRequest]
const relays = useReadRelayUrls(additionalRelays);
const subject = useMemo(
() => userRelaysService.requestRelays(pubkey, relays, alwaysRequest),
[pubkey, relays.join("|"), alwaysRequest]
);
const userRelays = useSubject(observable);
const userRelays = useSubject(subject);
return userRelays;
return userRelays?.relays ?? [];
}

View File

@ -4,7 +4,7 @@ import { unique } from "../helpers/array";
import { DraftNostrEvent, RTag } from "../types/nostr-event";
import accountService from "./account";
import { RelayConfig, RelayMode } from "../classes/relay";
import userRelaysService, { UserRelays } from "./user-relays";
import userRelaysService, { ParsedUserRelays } from "./user-relays";
import { PersistentSubject, Subject } from "../classes/subject";
import signingService from "./signing";
@ -17,7 +17,7 @@ class ClientRelayService {
readRelays = new PersistentSubject<RelayConfig[]>([]);
constructor() {
let lastSubject: Subject<UserRelays> | undefined;
let lastSubject: Subject<ParsedUserRelays> | undefined;
accountService.current.subscribe((account) => {
this.relays.next([]);
@ -49,7 +49,7 @@ class ClientRelayService {
this.relays.subscribe((relays) => this.readRelays.next(relays.filter((r) => r.mode & RelayMode.READ)));
}
private handleRelayChanged(relays: UserRelays) {
private handleRelayChanged(relays: ParsedUserRelays) {
this.relays.next(relays.relays);
}

View File

@ -6,15 +6,14 @@ 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";
import userRelaysService, { ParsedUserRelays } from "./user-relays";
type pubkey = string;
type relay = string;
class PubkeyRelayAssignmentService {
pubkeys = new Map<pubkey, relay[]>();
pubkeyRelays = new SuperMap<string, Subject<UserRelays>>(() => new Subject());
pubkeyRelays = new SuperMap<string, Subject<ParsedUserRelays>>(() => new Subject());
assignments = new PersistentSubject<Record<pubkey, relay[]>>({});
constructor() {
@ -46,7 +45,7 @@ class PubkeyRelayAssignmentService {
this.pubkeys.set(pubkey, relays);
const readRelays = clientRelaysService.getReadUrls();
const subject = userRelaysFallbackService.requestRelays(pubkey, unique([...readRelays, ...relays]));
const subject = userRelaysService.requestRelays(pubkey, unique([...readRelays, ...relays]));
this.pubkeyRelays.set(pubkey, subject);
// subject.subscribe(this.updateAssignments, this);
}

View File

@ -1,40 +0,0 @@
import Subject from "../classes/subject";
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 (contacts.relays.length > 0 && (!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

@ -6,14 +6,15 @@ import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-reque
import { SuperMap } from "../classes/super-map";
import Subject from "../classes/subject";
import { normalizeRelayConfigs } from "../helpers/relay";
import userContactsService from "./user-contacts";
export type UserRelays = {
export type ParsedUserRelays = {
pubkey: string;
relays: RelayConfig[];
created_at: number;
};
function parseRelaysEvent(event: NostrEvent): UserRelays {
function parseRelaysEvent(event: NostrEvent): ParsedUserRelays {
return {
pubkey: event.pubkey,
relays: normalizeRelayConfigs(event.tags.filter(isRTag).map(parseRTag)),
@ -25,25 +26,27 @@ class UserRelaysService {
requester: CachedPubkeyEventRequester;
constructor() {
this.requester = new CachedPubkeyEventRequester(10002, "user-relays");
this.requester.readCache = this.readCache;
this.requester.writeCache = this.writeCache;
this.requester.readCache = (pubkey) => db.get("userRelays", pubkey);
this.requester.writeCache = (pubkey, event) => db.put("userRelays", event);
}
readCache(pubkey: string) {
return db.get("userRelays", pubkey);
}
writeCache(pubkey: string, event: NostrEvent) {
return db.put("userRelays", event);
}
private subjects = new SuperMap<string, Subject<UserRelays>>(() => new Subject<UserRelays>());
getSubject(pubkey: string) {
private subjects = new SuperMap<string, Subject<ParsedUserRelays>>(() => new Subject<ParsedUserRelays>());
getRelays(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);
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
// also fetch the relays from the users contacts
const contactsSub = userContactsService.requestContacts(pubkey, relays, alwaysRequest);
sub.connectWithHandler(contactsSub, (contacts, next, value) => {
if (contacts.relays.length > 0 && (!value || contacts.created_at > value.created_at)) {
next({ pubkey: contacts.pubkey, relays: contacts.relays, created_at: contacts.created_at });
}
});
return sub;
}

View File

@ -1,8 +1,5 @@
import { Flex, Heading, SkeletonText, Text, Link, IconButton, Image } from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { useMemo } from "react";
import { Flex, Heading, SkeletonText, Text, Link, IconButton } from "@chakra-ui/react";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import { RelayMode } from "../../../classes/relay";
import { CopyIconButton } from "../../../components/copy-icon-button";
import { ChatIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons";
import { QrIconButton } from "./share-qr-button";
@ -15,23 +12,9 @@ import { truncatedId } from "../../../helpers/nostr-event";
import { fixWebsiteUrl, getUserDisplayName } from "../../../helpers/user-metadata";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import { useIsMobile } from "../../../hooks/use-is-mobile";
import useFallbackUserRelays from "../../../hooks/use-fallback-user-relays";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import relayScoreboardService from "../../../services/relay-scoreboard";
import { UserProfileMenu } from "./user-profile-menu";
function useUserShareLink(pubkey: string) {
const userRelays = useFallbackUserRelays(pubkey);
return useMemo(() => {
const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
const ranked = relayScoreboardService.getRankedRelays(writeUrls);
const onlyTwo = ranked.slice(0, 2);
return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey);
}, [userRelays]);
}
export default function Header({
pubkey,
showRelaySelectionModal,

View File

@ -15,33 +15,19 @@ import {
Input,
Flex,
} from "@chakra-ui/react";
import { useMemo } from "react";
import { RelayMode } from "../../../classes/relay";
import { QrCodeIcon } from "../../../components/icons";
import QrCodeSvg from "../../../components/qr-code-svg";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import useFallbackUserRelays from "../../../hooks/use-fallback-user-relays";
import relayScoreboardService from "../../../services/relay-scoreboard";
import { nip19 } from "nostr-tools";
import { CopyIconButton } from "../../../components/copy-icon-button";
function useUserShareLink(pubkey: string) {
const userRelays = useFallbackUserRelays(pubkey);
return useMemo(() => {
const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
const ranked = relayScoreboardService.getRankedRelays(writeUrls);
const onlyTwo = ranked.slice(0, 2);
return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey);
}, [userRelays]);
}
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "icon">) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey) || pubkey;
const nprofile = useUserShareLink(pubkey);
const npubLink = "nostr:" + npub;
const nprofile = useSharableProfileId(pubkey);
const nprofileLink = "nostr:" + nprofile;
return (
<>
@ -59,17 +45,17 @@ export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit<Ico
<TabPanels>
<TabPanel p="0" pt="2">
<QrCodeSvg content={"nostr:" + nprofile} border={2} />
<QrCodeSvg content={nprofileLink} border={2} />
<Flex gap="2" mt="2">
<Input readOnly value={"nostr:" + nprofile} />
<CopyIconButton text={"nostr:" + nprofile} aria-label="copy nprofile" />
<Input readOnly value={nprofileLink} />
<CopyIconButton text={nprofileLink} aria-label="copy nprofile" />
</Flex>
</TabPanel>
<TabPanel p="0" pt="2">
<QrCodeSvg content={"nostr:" + npub} border={2} />
<QrCodeSvg content={npubLink} border={2} />
<Flex gap="2" mt="2">
<Input readOnly value={"nostr:" + npub} />
<CopyIconButton text={"nostr:" + npub} aria-label="copy npub" />
<Input readOnly value={npubLink} />
<CopyIconButton text={npubLink} aria-label="copy npub" />
</Flex>
</TabPanel>
</TabPanels>

View File

@ -24,7 +24,7 @@ export const UserProfileMenu = ({
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const loginAsUser = () => {
const readRelays = userRelays?.relays.filter((r) => r.mode === RelayMode.READ).map((r) => r.url) ?? [];
const readRelays = userRelays.filter((r) => r.mode === RelayMode.READ).map((r) => r.url) ?? [];
if (!accountService.hasAccount(pubkey)) {
accountService.addAccount({
pubkey,

View File

@ -32,7 +32,6 @@ import { Bech32Prefix, isHex, normalizeToBech32 } from "../../helpers/nip19";
import { useAppTitle } from "../../hooks/use-app-title";
import Header from "./components/header";
import { Suspense, useState } from "react";
import useFallbackUserRelays from "../../hooks/use-fallback-user-relays";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import { RelayMode } from "../../classes/relay";
@ -40,6 +39,7 @@ import { AdditionalRelayProvider } from "../../providers/additional-relay-contex
import { nip19 } from "nostr-tools";
import { unique } from "../../helpers/array";
import { RelayFavicon } from "../../components/relay-favicon";
import { useUserRelays } from "../../hooks/use-user-relays";
const tabs = [
{ label: "Notes", path: "notes" },
@ -68,12 +68,12 @@ function useUserPointer() {
}
function useUserTopRelays(pubkey: string, count: number = 4) {
const readRelays = useReadRelayUrls();
// get user relays
const userRelays = useFallbackUserRelays(pubkey)
const userRelays = useUserRelays(pubkey, readRelays)
.filter((r) => r.mode & RelayMode.WRITE)
.map((r) => r.url);
// merge the users relays with client relays
const readRelays = useReadRelayUrls();
if (userRelays.length === 0) return readRelays;
const sorted = relayScoreboardService.getRankedRelays(userRelays);

View File

@ -2,13 +2,14 @@ import { Text, Box, IconButton, Flex, Badge } from "@chakra-ui/react";
import { useNavigate, useOutletContext } from "react-router-dom";
import { GlobalIcon } from "../../components/icons";
import { RelayMode } from "../../classes/relay";
import useFallbackUserRelays from "../../hooks/use-fallback-user-relays";
import { RelayScoreBreakdown } from "../../components/relay-score-breakdown";
import useRankedRelayConfigs from "../../hooks/use-ranked-relay-configs";
import { useUserRelays } from "../../hooks/use-user-relays";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
const UserRelaysTab = () => {
const { pubkey } = useOutletContext() as { pubkey: string };
const userRelays = useFallbackUserRelays(pubkey);
const userRelays = useUserRelays(pubkey);
const navigate = useNavigate();
const ranked = useRankedRelayConfigs(userRelays);

View File

@ -1,15 +1,12 @@
import { Box, Button, Flex, Spinner, Text } from "@chakra-ui/react";
import { Button, Flex, Spinner, Text } from "@chakra-ui/react";
import moment from "moment";
import { useOutletContext } from "react-router-dom";
import { RelayMode } from "../../classes/relay";
import { NoteLink } from "../../components/note-link";
import { UserLink } from "../../components/user-link";
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr-event";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useFallbackUserRelays from "../../hooks/use-fallback-user-relays";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import relayScoreboardService from "../../services/relay-scoreboard";
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
function ReportEvent({ report }: { report: NostrEvent }) {
const reportedEvent = report.tags.filter(isETag)[0]?.[1];
@ -39,14 +36,7 @@ function ReportEvent({ report }: { report: NostrEvent }) {
export default function UserReportsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
// get user relays
const userRelays = useFallbackUserRelays(pubkey)
.filter((r) => r.mode & RelayMode.WRITE)
.map((r) => r.url);
// merge the users relays with client relays
const readRelays = useReadRelayUrls();
// find the top 4
const relays = relayScoreboardService.getRankedRelays(userRelays.length === 0 ? readRelays : userRelays).slice(0, 4);
const contextRelays = useAdditionalRelayContext();
const {
events: reports,
@ -54,7 +44,7 @@ export default function UserReportsTab() {
loadMore,
} = useTimelineLoader(
`${truncatedId(pubkey)}-reports`,
relays,
contextRelays,
{ authors: [pubkey], kinds: [1984] },
{ pageSize: moment.duration(1, "week").asSeconds() }
);