Improve the NIP-05 cache
This commit is contained in:
parent
aa64029041
commit
8eccdafa64
@ -5,7 +5,7 @@ import { booleanParamSchema } from '@/schema.ts';
|
|||||||
import { nostrIdSchema } from '@/schemas/nostr.ts';
|
import { nostrIdSchema } from '@/schemas/nostr.ts';
|
||||||
import { searchStore } from '@/storages.ts';
|
import { searchStore } from '@/storages.ts';
|
||||||
import { dedupeEvents } from '@/utils.ts';
|
import { dedupeEvents } from '@/utils.ts';
|
||||||
import { lookupNip05Cached } from '@/utils/nip05.ts';
|
import { nip05Cache } from '@/utils/nip05.ts';
|
||||||
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
|
|
||||||
@ -95,13 +95,13 @@ function typeToKinds(type: SearchQuery['type']): number[] {
|
|||||||
|
|
||||||
/** Resolve a searched value into an event, if applicable. */
|
/** Resolve a searched value into an event, if applicable. */
|
||||||
async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<Event | undefined> {
|
async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<Event | undefined> {
|
||||||
const filters = await getLookupFilters(query);
|
const filters = await getLookupFilters(query, signal);
|
||||||
const [event] = await searchStore.filter(filters, { limit: 1, signal });
|
const [event] = await searchStore.filter(filters, { limit: 1, signal });
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get filters to lookup the input value. */
|
/** Get filters to lookup the input value. */
|
||||||
async function getLookupFilters({ q, type, resolve }: SearchQuery): Promise<DittoFilter[]> {
|
async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: AbortSignal): Promise<DittoFilter[]> {
|
||||||
const filters: DittoFilter[] = [];
|
const filters: DittoFilter[] = [];
|
||||||
|
|
||||||
const accounts = !type || type === 'accounts';
|
const accounts = !type || type === 'accounts';
|
||||||
@ -139,9 +139,13 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery): Promise<Ditt
|
|||||||
if (accounts) filters.push({ kinds: [0], authors: [q] });
|
if (accounts) filters.push({ kinds: [0], authors: [q] });
|
||||||
if (statuses) filters.push({ kinds: [1], ids: [q] });
|
if (statuses) filters.push({ kinds: [1], ids: [q] });
|
||||||
} else if (accounts && ACCT_REGEX.test(q)) {
|
} else if (accounts && ACCT_REGEX.test(q)) {
|
||||||
const pubkey = await lookupNip05Cached(q);
|
try {
|
||||||
if (pubkey) {
|
const { pubkey } = await nip05Cache.fetch(q, { signal });
|
||||||
filters.push({ kinds: [0], authors: [pubkey], relations: ['author_stats'] });
|
if (pubkey) {
|
||||||
|
filters.push({ kinds: [0], authors: [pubkey], relations: ['author_stats'] });
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,5 +86,9 @@ export { EventEmitter } from 'npm:tseep@^1.1.3';
|
|||||||
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
|
export { default as stringifyStable } from 'npm:fast-stable-stringify@^1.0.0';
|
||||||
// @deno-types="npm:@types/debug@^4.1.12"
|
// @deno-types="npm:@types/debug@^4.1.12"
|
||||||
export { default as Debug } from 'npm:debug@^4.3.4';
|
export { default as Debug } from 'npm:debug@^4.3.4';
|
||||||
|
export {
|
||||||
|
type MapCache,
|
||||||
|
NIP05,
|
||||||
|
} from 'https://gitlab.com/soapbox-pub/nlib/-/raw/46be9e985950547574b1735d0ae52a6a7217d056/mod.ts';
|
||||||
|
|
||||||
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { type Event, type EventTemplate, getEventHash, nip19, z } from '@/deps.ts';
|
import { type Event, type EventTemplate, getEventHash, nip19, z } from '@/deps.ts';
|
||||||
import { getAuthor } from '@/queries.ts';
|
import { getAuthor } from '@/queries.ts';
|
||||||
import { lookupNip05Cached } from '@/utils/nip05.ts';
|
import { nip05Cache } from '@/utils/nip05.ts';
|
||||||
import { nostrIdSchema } from '@/schemas/nostr.ts';
|
import { nostrIdSchema } from '@/schemas/nostr.ts';
|
||||||
|
|
||||||
/** Get the current time in Nostr format. */
|
/** Get the current time in Nostr format. */
|
||||||
@ -56,10 +56,11 @@ function parseNip05(value: string): Nip05 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve a bech32 or NIP-05 identifier to an account. */
|
/** Resolve a bech32 or NIP-05 identifier to an account. */
|
||||||
async function lookupAccount(value: string): Promise<Event<0> | undefined> {
|
async function lookupAccount(value: string, signal = AbortSignal.timeout(3000)): Promise<Event<0> | undefined> {
|
||||||
console.log(`Looking up ${value}`);
|
console.log(`Looking up ${value}`);
|
||||||
|
|
||||||
const pubkey = bech32ToPubkey(value) || await lookupNip05Cached(value);
|
const pubkey = bech32ToPubkey(value) ||
|
||||||
|
await nip05Cache.fetch(value, { signal }).then(({ pubkey }) => pubkey).catch(() => undefined);
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
return getAuthor(pubkey);
|
return getAuthor(pubkey);
|
||||||
|
37
src/utils/SimpleLRU.ts
Normal file
37
src/utils/SimpleLRU.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// deno-lint-ignore-file ban-types
|
||||||
|
|
||||||
|
import { LRUCache, type MapCache } from '@/deps.ts';
|
||||||
|
|
||||||
|
type FetchFn<K extends {}, V extends {}, O extends {}> = (key: K, opts: O) => Promise<V>;
|
||||||
|
|
||||||
|
interface FetchFnOpts {
|
||||||
|
signal?: AbortSignal | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SimpleLRU<
|
||||||
|
K extends {},
|
||||||
|
V extends {},
|
||||||
|
O extends {} = FetchFnOpts,
|
||||||
|
> implements MapCache<K, V, O> {
|
||||||
|
protected cache: LRUCache<K, V, void>;
|
||||||
|
|
||||||
|
constructor(fetchFn: FetchFn<K, V, { signal: AbortSignal }>, opts: LRUCache.Options<K, V, void>) {
|
||||||
|
this.cache = new LRUCache({
|
||||||
|
fetchMethod: (key, _staleValue, { signal }) => fetchFn(key, { signal: signal as AbortSignal }),
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(key: K, opts?: O): Promise<V> {
|
||||||
|
const result = await this.cache.fetch(key, opts);
|
||||||
|
if (result === undefined) {
|
||||||
|
throw new Error('SimpleLRU: fetch failed');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
put(key: K, value: V): Promise<void> {
|
||||||
|
this.cache.set(key, value);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
@ -1,73 +1,22 @@
|
|||||||
import { Debug, TTLCache, z } from '@/deps.ts';
|
import { Debug, NIP05, nip19 } from '@/deps.ts';
|
||||||
|
import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
import { fetchWorker } from '@/workers/fetch.ts';
|
|
||||||
|
|
||||||
const debug = Debug('ditto:nip05');
|
const debug = Debug('ditto:nip05');
|
||||||
|
|
||||||
const nip05Cache = new TTLCache<string, Promise<string | null>>({ ttl: Time.hours(1), max: 5000 });
|
const nip05Cache = new SimpleLRU<string, nip19.ProfilePointer>(
|
||||||
|
async (key, { signal }) => {
|
||||||
const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w.-]+)$/;
|
debug(`Lookup ${key}`);
|
||||||
|
try {
|
||||||
interface LookupOpts {
|
const result = await NIP05.lookup(key, { fetch, signal });
|
||||||
signal?: AbortSignal;
|
debug(`Found: ${key} is ${result.pubkey}`);
|
||||||
}
|
return result;
|
||||||
|
} catch (e) {
|
||||||
/** Get pubkey from NIP-05. */
|
debug(`Not found: ${key}`);
|
||||||
async function lookup(value: string, opts: LookupOpts = {}): Promise<string | null> {
|
throw e;
|
||||||
const { signal = AbortSignal.timeout(2000) } = opts;
|
|
||||||
|
|
||||||
const match = value.match(NIP05_REGEX);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
const [_, name = '_', domain] = match;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetchWorker(`https://${domain}/.well-known/nostr.json?name=${name}`, {
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { names } = nostrJsonSchema.parse(await res.json());
|
|
||||||
|
|
||||||
return names[name] || null;
|
|
||||||
} catch (e) {
|
|
||||||
debug(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** nostr.json schema. */
|
|
||||||
const nostrJsonSchema = z.object({
|
|
||||||
names: z.record(z.string(), z.string()),
|
|
||||||
relays: z.record(z.string(), z.array(z.string())).optional().catch(undefined),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lookup the NIP-05 and serve from cache first.
|
|
||||||
* To prevent race conditions we put the promise in the cache instead of the result.
|
|
||||||
*/
|
|
||||||
function lookupNip05Cached(value: string): Promise<string | null> {
|
|
||||||
const cached = nip05Cache.get(value);
|
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
|
|
||||||
debug(`Lookup ${value}`);
|
|
||||||
const result = lookup(value);
|
|
||||||
nip05Cache.set(value, result);
|
|
||||||
|
|
||||||
result.then((result) => {
|
|
||||||
if (result) {
|
|
||||||
debug(`Found: ${value} is ${result}`);
|
|
||||||
} else {
|
|
||||||
debug(`Not found: ${value} is ${result}`);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
{ max: 5000, ttl: Time.hours(1) },
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
export { nip05Cache };
|
||||||
}
|
|
||||||
|
|
||||||
/** Verify the NIP-05 matches the pubkey, with cache. */
|
|
||||||
async function verifyNip05Cached(value: string, pubkey: string): Promise<boolean> {
|
|
||||||
const result = await lookupNip05Cached(value);
|
|
||||||
return result === pubkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { lookupNip05Cached, verifyNip05Cached };
|
|
||||||
|
@ -3,7 +3,7 @@ import { findUser } from '@/db/users.ts';
|
|||||||
import { lodash, nip19, type UnsignedEvent } from '@/deps.ts';
|
import { lodash, nip19, type UnsignedEvent } from '@/deps.ts';
|
||||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||||
import { type DittoEvent } from '@/storages/types.ts';
|
import { type DittoEvent } from '@/storages/types.ts';
|
||||||
import { verifyNip05Cached } from '@/utils/nip05.ts';
|
import { nip05Cache } from '@/utils/nip05.ts';
|
||||||
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
||||||
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
||||||
|
|
||||||
@ -86,9 +86,19 @@ function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) {
|
|||||||
return renderAccount(event, opts);
|
return renderAccount(event, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseAndVerifyNip05(nip05: string | undefined, pubkey: string): Promise<Nip05 | undefined> {
|
async function parseAndVerifyNip05(
|
||||||
if (nip05 && await verifyNip05Cached(nip05, pubkey)) {
|
nip05: string | undefined,
|
||||||
return parseNip05(nip05);
|
pubkey: string,
|
||||||
|
signal = AbortSignal.timeout(3000),
|
||||||
|
): Promise<Nip05 | undefined> {
|
||||||
|
if (!nip05) return;
|
||||||
|
try {
|
||||||
|
const result = await nip05Cache.fetch(nip05, { signal });
|
||||||
|
if (result.pubkey === pubkey) {
|
||||||
|
return parseNip05(nip05);
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user