diff --git a/src/components/user-dns-identity-icon.tsx b/src/components/user-dns-identity-icon.tsx index 7e9f61bf3..6741cda1a 100644 --- a/src/components/user-dns-identity-icon.tsx +++ b/src/components/user-dns-identity-icon.tsx @@ -1,22 +1,20 @@ -import { Spinner, Text, Tooltip } from "@chakra-ui/react"; +import { Text, Tooltip } from "@chakra-ui/react"; import { useDnsIdentity } from "../hooks/use-dns-identity"; import { useUserMetadata } from "../hooks/use-user-metadata"; import { VerificationFailed, VerificationMissing, VerifiedIcon } from "./icons"; export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) => { const metadata = useUserMetadata(pubkey); - const { identity, loading, error } = useDnsIdentity(metadata?.nip05); + const identity = useDnsIdentity(metadata?.nip05); if (!metadata?.nip05) { return null; } const renderIcon = () => { - if (loading) { - return ; - } else if (error) { + if (identity === undefined) { return ; - } else if (!identity) { + } else if (identity === null) { return ; } else if (pubkey === identity.pubkey) { return ; diff --git a/src/hooks/use-dns-identity.ts b/src/hooks/use-dns-identity.ts index f19cc1ec5..114eab65e 100644 --- a/src/hooks/use-dns-identity.ts +++ b/src/hooks/use-dns-identity.ts @@ -1,15 +1,11 @@ -import { useAsync } from "react-use"; import dnsIdentityService from "../services/dns-identity"; +import { useMemo } from "react"; +import useSubject from "./use-subject"; export function useDnsIdentity(address: string | undefined) { - const { value, loading, error } = useAsync(async () => { - if (!address) return; - return dnsIdentityService.getIdentity(address); + const subject = useMemo(() => { + if (address) return dnsIdentityService.getIdentity(address); }, [address]); - return { - identity: value, - error, - loading, - }; + return useSubject(subject); } diff --git a/src/services/dns-identity.ts b/src/services/dns-identity.ts index b91e6b463..d20ae67e8 100644 --- a/src/services/dns-identity.ts +++ b/src/services/dns-identity.ts @@ -1,6 +1,10 @@ import dayjs from "dayjs"; import db from "./db"; +import _throttle from "lodash.throttle"; + import { fetchWithCorsFallback } from "../helpers/cors"; +import { SuperMap } from "../classes/super-map"; +import Subject from "../classes/subject"; export function parseAddress(address: string): { name?: string; domain?: string } { const parts = address.trim().toLowerCase().split("@"); @@ -26,106 +30,101 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson): return { name, domain, pubkey, relays }; } -async function fetchAllIdentities(domain: string) { - const json = await fetchWithCorsFallback(`//${domain}/.well-known/nostr.json`).then( - (res) => res.json() as Promise, - ); +class DnsIdentityService { + identities = new SuperMap>(() => new Subject()); - await addToCache(domain, json); -} + async fetchIdentity(address: string) { + const { name, domain } = parseAddress(address); + if (!name || !domain) throw new Error("invalid address"); -async function fetchIdentity(address: string) { - const { name, domain } = parseAddress(address); - if (!name || !domain) throw new Error("invalid address"); - - const json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`) - .then((res) => res.json() as Promise) - .then((json) => { - // convert all keys in names, and relays to lower case - if (json.names) { - for (const [name, pubkey] of Object.entries(json.names)) { - delete json.names[name]; - json.names[name.toLowerCase()] = pubkey; + const json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`) + .then((res) => res.json() as Promise) + .then((json) => { + // convert all keys in names, and relays to lower case + if (json.names) { + for (const [name, pubkey] of Object.entries(json.names)) { + delete json.names[name]; + json.names[name.toLowerCase()] = pubkey; + } } - } - if (json.relays) { - for (const [name, pubkey] of Object.entries(json.relays)) { - delete json.relays[name]; - json.relays[name.toLowerCase()] = pubkey; + if (json.relays) { + for (const [name, pubkey] of Object.entries(json.relays)) { + delete json.relays[name]; + json.relays[name.toLowerCase()] = pubkey; + } } - } - return json; - }); + return json; + }); - await addToCache(domain, json); + await this.addToCache(domain, json); - return getIdentityFromJson(name, domain, json); -} + return getIdentityFromJson(name, domain, json); + } -const inMemoryCache = new Map(); + async addToCache(domain: string, json: IdentityJson) { + const now = dayjs().unix(); + const transaction = db.transaction("dnsIdentifiers", "readwrite"); -async function addToCache(domain: string, json: IdentityJson) { - const now = dayjs().unix(); - const transaction = db.transaction("dnsIdentifiers", "readwrite"); + for (const name of Object.keys(json.names)) { + const identity = getIdentityFromJson(name, domain, json); + if (identity) { + const address = `${name}@${domain}`; - for (const name of Object.keys(json.names)) { - const identity = getIdentityFromJson(name, domain, json); - if (identity) { - const id = `${name}@${domain}`; + // add to memory cache + this.identities.get(address).next(identity); - // add to memory cache - inMemoryCache.set(id, identity); - - // ad to db cache - if (transaction.store.put) { - await transaction.store.put({ ...identity, updated: now }, id); + // ad to db cache + if (transaction.store.put) { + await transaction.store.put({ ...identity, updated: now }, address); + } } } - } - await transaction.done; -} - -async function getIdentity(address: string, alwaysFetch = false) { - if (!inMemoryCache.has(address)) { - const fromDb = await db.get("dnsIdentifiers", address); - if (fromDb) inMemoryCache.set(address, fromDb); + await transaction.done; } - const cached = inMemoryCache.get(address); - if (cached && !alwaysFetch) return cached; + loading = new Set(); + getIdentity(address: string, alwaysFetch = false) { + const sub = this.identities.get(address); - return fetchIdentity(address); -} + if (this.loading.has(address)) return sub; + this.loading.add(address); -async function pruneCache() { - const keys = await db.getAllKeysFromIndex( - "dnsIdentifiers", - "updated", - IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()), - ); + db.get("dnsIdentifiers", address).then((fromDb) => { + if (fromDb) sub.next(fromDb); + this.loading.delete(address); + }); - for (const pubkey of keys) { - db.delete("dnsIdentifiers", pubkey); + if (!sub.value || alwaysFetch) { + this.fetchIdentity(address) + .then((identity) => { + sub.next(identity ?? null); + }) + .finally(() => { + this.loading.delete(address); + }); + } + + return sub; + } + + async pruneCache() { + const keys = await db.getAllKeysFromIndex( + "dnsIdentifiers", + "updated", + IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()), + ); + + for (const pubkey of keys) { + db.delete("dnsIdentifiers", pubkey); + } } } -const pending: Record | undefined> = {}; -function dedupedGetIdentity(address: string, alwaysFetch = false) { - if (pending[address]) return pending[address]; - return (pending[address] = getIdentity(address, alwaysFetch).then((v) => { - delete pending[address]; - return v; - })); -} +export const dnsIdentityService = new DnsIdentityService(); -export const dnsIdentityService = { - fetchAllIdentities, - fetchIdentity, - getIdentity: dedupedGetIdentity, - pruneCache, -}; - -setTimeout(() => pruneCache(), 1000 * 60 * 20); +setInterval(() => { + dnsIdentityService.pruneCache(); +}, 1000 * 60); if (import.meta.env.DEV) { // @ts-ignore diff --git a/src/views/login/nip05.tsx b/src/views/login/nip05.tsx index e204d3795..477ca649a 100644 --- a/src/views/login/nip05.tsx +++ b/src/views/login/nip05.tsx @@ -43,7 +43,7 @@ export default function LoginNip05View() { async () => { if (nip05) { try { - const id = await dnsIdentityService.getIdentity(nip05, true); + const id = await dnsIdentityService.fetchIdentity(nip05); setPubkey(id?.pubkey); setRelays(id?.relays); } catch (e) {} diff --git a/src/views/profile/edit.tsx b/src/views/profile/edit.tsx index 80495f940..54ac17283 100644 --- a/src/views/profile/edit.tsx +++ b/src/views/profile/edit.tsx @@ -122,7 +122,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => { if (!address) return true; if (!address.includes("@")) return "Invalid address"; try { - const id = await dnsIdentityService.getIdentity(address); + const id = await dnsIdentityService.fetchIdentity(address); if (!id) return "Cant find NIP-05 ID"; if (id.pubkey !== account.pubkey) return "Pubkey dose not match"; } catch (e) {