mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-09 20:33:03 +02:00
improve dns identity caching
This commit is contained in:
@@ -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 { useDnsIdentity } from "../hooks/use-dns-identity";
|
||||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
import { VerificationFailed, VerificationMissing, VerifiedIcon } from "./icons";
|
import { VerificationFailed, VerificationMissing, VerifiedIcon } from "./icons";
|
||||||
|
|
||||||
export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) => {
|
export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) => {
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const { identity, loading, error } = useDnsIdentity(metadata?.nip05);
|
const identity = useDnsIdentity(metadata?.nip05);
|
||||||
|
|
||||||
if (!metadata?.nip05) {
|
if (!metadata?.nip05) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (loading) {
|
if (identity === undefined) {
|
||||||
return <Spinner size="xs" ml="1" title={metadata.nip05} />;
|
|
||||||
} else if (error) {
|
|
||||||
return <VerificationFailed color="yellow.500" />;
|
return <VerificationFailed color="yellow.500" />;
|
||||||
} else if (!identity) {
|
} else if (identity === null) {
|
||||||
return <VerificationMissing color="red.500" />;
|
return <VerificationMissing color="red.500" />;
|
||||||
} else if (pubkey === identity.pubkey) {
|
} else if (pubkey === identity.pubkey) {
|
||||||
return <VerifiedIcon color="purple.500" />;
|
return <VerifiedIcon color="purple.500" />;
|
||||||
|
@@ -1,15 +1,11 @@
|
|||||||
import { useAsync } from "react-use";
|
|
||||||
import dnsIdentityService from "../services/dns-identity";
|
import dnsIdentityService from "../services/dns-identity";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
export function useDnsIdentity(address: string | undefined) {
|
export function useDnsIdentity(address: string | undefined) {
|
||||||
const { value, loading, error } = useAsync(async () => {
|
const subject = useMemo(() => {
|
||||||
if (!address) return;
|
if (address) return dnsIdentityService.getIdentity(address);
|
||||||
return dnsIdentityService.getIdentity(address);
|
|
||||||
}, [address]);
|
}, [address]);
|
||||||
|
|
||||||
return {
|
return useSubject(subject);
|
||||||
identity: value,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
|
||||||
import { fetchWithCorsFallback } from "../helpers/cors";
|
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 } {
|
export function parseAddress(address: string): { name?: string; domain?: string } {
|
||||||
const parts = address.trim().toLowerCase().split("@");
|
const parts = address.trim().toLowerCase().split("@");
|
||||||
@@ -26,106 +30,101 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson):
|
|||||||
return { name, domain, pubkey, relays };
|
return { name, domain, pubkey, relays };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAllIdentities(domain: string) {
|
class DnsIdentityService {
|
||||||
const json = await fetchWithCorsFallback(`//${domain}/.well-known/nostr.json`).then(
|
identities = new SuperMap<string, Subject<DnsIdentity | null>>(() => new Subject());
|
||||||
(res) => res.json() as Promise<IdentityJson>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||||
const { name, domain } = parseAddress(address);
|
.then((res) => res.json() as Promise<IdentityJson>)
|
||||||
if (!name || !domain) throw new Error("invalid address");
|
.then((json) => {
|
||||||
|
// convert all keys in names, and relays to lower case
|
||||||
const json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
if (json.names) {
|
||||||
.then((res) => res.json() as Promise<IdentityJson>)
|
for (const [name, pubkey] of Object.entries(json.names)) {
|
||||||
.then((json) => {
|
delete json.names[name];
|
||||||
// convert all keys in names, and relays to lower case
|
json.names[name.toLowerCase()] = pubkey;
|
||||||
if (json.names) {
|
}
|
||||||
for (const [name, pubkey] of Object.entries(json.names)) {
|
|
||||||
delete json.names[name];
|
|
||||||
json.names[name.toLowerCase()] = pubkey;
|
|
||||||
}
|
}
|
||||||
}
|
if (json.relays) {
|
||||||
if (json.relays) {
|
for (const [name, pubkey] of Object.entries(json.relays)) {
|
||||||
for (const [name, pubkey] of Object.entries(json.relays)) {
|
delete json.relays[name];
|
||||||
delete json.relays[name];
|
json.relays[name.toLowerCase()] = pubkey;
|
||||||
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) {
|
for (const name of Object.keys(json.names)) {
|
||||||
const now = dayjs().unix();
|
const identity = getIdentityFromJson(name, domain, json);
|
||||||
const transaction = db.transaction("dnsIdentifiers", "readwrite");
|
if (identity) {
|
||||||
|
const address = `${name}@${domain}`;
|
||||||
|
|
||||||
for (const name of Object.keys(json.names)) {
|
// add to memory cache
|
||||||
const identity = getIdentityFromJson(name, domain, json);
|
this.identities.get(address).next(identity);
|
||||||
if (identity) {
|
|
||||||
const id = `${name}@${domain}`;
|
|
||||||
|
|
||||||
// add to memory cache
|
// ad to db cache
|
||||||
inMemoryCache.set(id, identity);
|
if (transaction.store.put) {
|
||||||
|
await transaction.store.put({ ...identity, updated: now }, address);
|
||||||
// ad to db cache
|
}
|
||||||
if (transaction.store.put) {
|
|
||||||
await transaction.store.put({ ...identity, updated: now }, id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
await transaction.done;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = inMemoryCache.get(address);
|
loading = new Set<string>();
|
||||||
if (cached && !alwaysFetch) return cached;
|
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() {
|
db.get("dnsIdentifiers", address).then((fromDb) => {
|
||||||
const keys = await db.getAllKeysFromIndex(
|
if (fromDb) sub.next(fromDb);
|
||||||
"dnsIdentifiers",
|
this.loading.delete(address);
|
||||||
"updated",
|
});
|
||||||
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const pubkey of keys) {
|
if (!sub.value || alwaysFetch) {
|
||||||
db.delete("dnsIdentifiers", pubkey);
|
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> = {};
|
export const dnsIdentityService = new DnsIdentityService();
|
||||||
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 = {
|
setInterval(() => {
|
||||||
fetchAllIdentities,
|
dnsIdentityService.pruneCache();
|
||||||
fetchIdentity,
|
}, 1000 * 60);
|
||||||
getIdentity: dedupedGetIdentity,
|
|
||||||
pruneCache,
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => pruneCache(), 1000 * 60 * 20);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@@ -43,7 +43,7 @@ export default function LoginNip05View() {
|
|||||||
async () => {
|
async () => {
|
||||||
if (nip05) {
|
if (nip05) {
|
||||||
try {
|
try {
|
||||||
const id = await dnsIdentityService.getIdentity(nip05, true);
|
const id = await dnsIdentityService.fetchIdentity(nip05);
|
||||||
setPubkey(id?.pubkey);
|
setPubkey(id?.pubkey);
|
||||||
setRelays(id?.relays);
|
setRelays(id?.relays);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
@@ -122,7 +122,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
|
|||||||
if (!address) return true;
|
if (!address) return true;
|
||||||
if (!address.includes("@")) return "Invalid address";
|
if (!address.includes("@")) return "Invalid address";
|
||||||
try {
|
try {
|
||||||
const id = await dnsIdentityService.getIdentity(address);
|
const id = await dnsIdentityService.fetchIdentity(address);
|
||||||
if (!id) return "Cant find NIP-05 ID";
|
if (!id) return "Cant find NIP-05 ID";
|
||||||
if (id.pubkey !== account.pubkey) return "Pubkey dose not match";
|
if (id.pubkey !== account.pubkey) return "Pubkey dose not match";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Reference in New Issue
Block a user