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) {