improve dns identity caching

This commit is contained in:
hzrd149
2023-09-07 21:14:18 -05:00
parent 98ed8685b9
commit a5e7f59289
5 changed files with 89 additions and 96 deletions

View File

@@ -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 <Spinner size="xs" ml="1" title={metadata.nip05} />;
} else if (error) {
if (identity === undefined) {
return <VerificationFailed color="yellow.500" />;
} else if (!identity) {
} else if (identity === null) {
return <VerificationMissing color="red.500" />;
} else if (pubkey === identity.pubkey) {
return <VerifiedIcon color="purple.500" />;

View File

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

View File

@@ -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<IdentityJson>,
);
class DnsIdentityService {
identities = new SuperMap<string, Subject<DnsIdentity | null>>(() => 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<IdentityJson>)
.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<IdentityJson>)
.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<string, DnsIdentity>();
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<string>();
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<string, ReturnType<typeof getIdentity> | 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

View File

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

View File

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