Merge branch 'rm-search-store' into 'main'

Remove SearchStore

See merge request soapbox-pub/ditto!677
This commit is contained in:
Alex Gleason 2025-02-18 21:09:56 +00:00
commit 32813485a5
6 changed files with 3 additions and 221 deletions

View file

@ -115,6 +115,7 @@ const accountSearchQuerySchema = z.object({
}); });
const accountSearchController: AppController = async (c) => { const accountSearchController: AppController = async (c) => {
const { store } = c.var;
const { signal } = c.req.raw; const { signal } = c.req.raw;
const { limit } = c.get('pagination'); const { limit } = c.get('pagination');
@ -128,7 +129,6 @@ const accountSearchController: AppController = async (c) => {
} }
const query = decodeURIComponent(result.data.q); const query = decodeURIComponent(result.data.q);
const store = await Storages.search();
const lookup = extractIdentifier(query); const lookup = extractIdentifier(query);
const event = await lookupAccount(lookup ?? query); const event = await lookupAccount(lookup ?? query);

View file

@ -94,7 +94,7 @@ async function searchEvents(
return Promise.resolve([]); return Promise.resolve([]);
} }
const store = await Storages.search(); const store = await Storages.db();
const filter: NostrFilter = { const filter: NostrFilter = {
kinds: typeToKinds(type), kinds: typeToKinds(type),
@ -150,7 +150,7 @@ 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<NostrEvent | undefined> { async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<NostrEvent | undefined> {
const filters = await getLookupFilters(query, signal); const filters = await getLookupFilters(query, signal);
const store = await Storages.search(); const store = await Storages.db();
return store.query(filters, { limit: 1, signal }) return store.query(filters, { limit: 1, signal })
.then((events) => hydrateEvents({ events, store, signal })) .then((events) => hydrateEvents({ events, store, signal }))

View file

@ -1,46 +0,0 @@
import { assertEquals } from '@std/assert';
import event0 from '~/fixtures/events/event-0.json' with { type: 'json' };
import event1 from '~/fixtures/events/event-1.json' with { type: 'json' };
import { eventToMicroFilter, getFilterId, getFilterLimit, getMicroFilters, isMicrofilter } from './filter.ts';
Deno.test('getMicroFilters', () => {
const event = event0;
const microfilters = getMicroFilters(event);
assertEquals(microfilters.length, 2);
assertEquals(microfilters[0], { authors: [event.pubkey], kinds: [0] });
assertEquals(microfilters[1], { ids: [event.id] });
});
Deno.test('eventToMicroFilter', () => {
assertEquals(eventToMicroFilter(event0), { authors: [event0.pubkey], kinds: [0] });
assertEquals(eventToMicroFilter(event1), { ids: [event1.id] });
});
Deno.test('isMicrofilter', () => {
assertEquals(isMicrofilter({ ids: [event0.id] }), true);
assertEquals(isMicrofilter({ authors: [event0.pubkey], kinds: [0] }), true);
assertEquals(isMicrofilter({ ids: [event0.id], authors: [event0.pubkey], kinds: [0] }), false);
});
Deno.test('getFilterId', () => {
assertEquals(
getFilterId({ ids: [event0.id] }),
'{"ids":["63d38c9b483d2d98a46382eadefd272e0e4bdb106a5b6eddb400c4e76f693d35"]}',
);
assertEquals(
getFilterId({ authors: [event0.pubkey], kinds: [0] }),
'{"authors":["79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6"],"kinds":[0]}',
);
});
Deno.test('getFilterLimit', () => {
assertEquals(getFilterLimit({ ids: [event0.id] }), 1);
assertEquals(getFilterLimit({ ids: [event0.id], limit: 2 }), 1);
assertEquals(getFilterLimit({ ids: [event0.id], limit: 0 }), 0);
assertEquals(getFilterLimit({ ids: [event0.id], limit: -1 }), 0);
assertEquals(getFilterLimit({ kinds: [0], authors: [event0.pubkey] }), 1);
assertEquals(getFilterLimit({ kinds: [1], authors: [event0.pubkey] }), Infinity);
assertEquals(getFilterLimit({}), Infinity);
});

View file

@ -1,97 +0,0 @@
import { NKinds, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
import stringifyStable from 'fast-stable-stringify';
import { z } from 'zod';
/** Microfilter to get one specific event by ID. */
type IdMicrofilter = { ids: [NostrEvent['id']] };
/** Microfilter to get an author. */
type AuthorMicrofilter = { kinds: [0]; authors: [NostrEvent['pubkey']] };
/** Filter to get one specific event. */
type MicroFilter = IdMicrofilter | AuthorMicrofilter;
/** Get deterministic ID for a microfilter. */
function getFilterId(filter: MicroFilter): string {
if ('ids' in filter) {
return stringifyStable({ ids: [filter.ids[0]] });
} else {
return stringifyStable({
kinds: [filter.kinds[0]],
authors: [filter.authors[0]],
});
}
}
/** Get a microfilter from a Nostr event. */
function eventToMicroFilter(event: NostrEvent): MicroFilter {
const [microfilter] = getMicroFilters(event);
return microfilter;
}
/** Get all the microfilters for an event, in order of priority. */
function getMicroFilters(event: NostrEvent): MicroFilter[] {
const microfilters: MicroFilter[] = [];
if (event.kind === 0) {
microfilters.push({ kinds: [0], authors: [event.pubkey] });
}
microfilters.push({ ids: [event.id] });
return microfilters;
}
/** Microfilter schema. */
const microFilterSchema = z.union([
z.object({ ids: z.tuple([n.id()]) }).strict(),
z.object({ kinds: z.tuple([z.literal(0)]), authors: z.tuple([n.id()]) }).strict(),
]);
/** Checks whether the filter is a microfilter. */
function isMicrofilter(filter: NostrFilter): filter is MicroFilter {
return microFilterSchema.safeParse(filter).success;
}
/** Returns true if the filter could potentially return any stored events at all. */
function canFilter(filter: NostrFilter): boolean {
return getFilterLimit(filter) > 0;
}
/** Normalize the `limit` of each filter, and remove filters that can't produce any events. */
function normalizeFilters<F extends NostrFilter>(filters: F[]): F[] {
return filters.reduce<F[]>((acc, filter) => {
const limit = getFilterLimit(filter);
if (limit > 0) {
acc.push(limit === Infinity ? filter : { ...filter, limit });
}
return acc;
}, []);
}
/** Calculate the intrinsic limit of a filter. This function may return `Infinity`. */
function getFilterLimit(filter: NostrFilter): number {
if (filter.ids && !filter.ids.length) return 0;
if (filter.kinds && !filter.kinds.length) return 0;
if (filter.authors && !filter.authors.length) return 0;
for (const [key, value] of Object.entries(filter)) {
if (key[0] === '#' && Array.isArray(value) && !value.length) return 0;
}
return Math.min(
Math.max(0, filter.limit ?? Infinity),
filter.ids?.length ?? Infinity,
filter.authors?.length && filter.kinds?.every((kind) => NKinds.replaceable(kind))
? filter.authors.length * filter.kinds.length
: Infinity,
);
}
export {
type AuthorMicrofilter,
canFilter,
eventToMicroFilter,
getFilterId,
getFilterLimit,
getMicroFilters,
type IdMicrofilter,
isMicrofilter,
type MicroFilter,
normalizeFilters,
};

View file

@ -7,7 +7,6 @@ import { Conf } from '@/config.ts';
import { wsUrlSchema } from '@/schema.ts'; import { wsUrlSchema } from '@/schema.ts';
import { AdminStore } from '@/storages/AdminStore.ts'; import { AdminStore } from '@/storages/AdminStore.ts';
import { EventsDB } from '@/storages/EventsDB.ts'; import { EventsDB } from '@/storages/EventsDB.ts';
import { SearchStore } from '@/storages/search-store.ts';
import { InternalRelay } from '@/storages/InternalRelay.ts'; import { InternalRelay } from '@/storages/InternalRelay.ts';
import { NPool, NRelay1 } from '@nostrify/nostrify'; import { NPool, NRelay1 } from '@nostrify/nostrify';
import { getRelays } from '@/utils/outbox.ts'; import { getRelays } from '@/utils/outbox.ts';
@ -19,7 +18,6 @@ export class Storages {
private static _admin: Promise<AdminStore> | undefined; private static _admin: Promise<AdminStore> | undefined;
private static _client: Promise<NPool<NRelay1>> | undefined; private static _client: Promise<NPool<NRelay1>> | undefined;
private static _pubsub: Promise<InternalRelay> | undefined; private static _pubsub: Promise<InternalRelay> | undefined;
private static _search: Promise<SearchStore> | undefined;
public static async database(): Promise<DittoDatabase> { public static async database(): Promise<DittoDatabase> {
if (!this._database) { if (!this._database) {
@ -124,17 +122,4 @@ export class Storages {
} }
return this._client; return this._client;
} }
/** Storage to use for remote search. */
public static async search(): Promise<SearchStore> {
if (!this._search) {
this._search = Promise.resolve(
new SearchStore({
relay: Conf.searchRelay,
fallback: await this.db(),
}),
);
}
return this._search;
}
} }

View file

@ -1,60 +0,0 @@
import { NostrEvent, NostrFilter, NRelay1, NStore } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi';
import { JsonValue } from '@std/json';
import { normalizeFilters } from '@/filter.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { hydrateEvents } from '@/storages/hydrate.ts';
import { abortError } from '@/utils/abort.ts';
interface SearchStoreOpts {
relay: string | undefined;
fallback: NStore;
hydrator?: NStore;
}
class SearchStore implements NStore {
#fallback: NStore;
#hydrator: NStore;
#relay: NRelay1 | undefined;
constructor(opts: SearchStoreOpts) {
this.#fallback = opts.fallback;
this.#hydrator = opts.hydrator ?? this;
if (opts.relay) {
this.#relay = new NRelay1(opts.relay);
}
}
event(_event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise<void> {
return Promise.reject(new Error('EVENT not implemented.'));
}
async query(filters: NostrFilter[], opts?: { signal?: AbortSignal; limit?: number }): Promise<DittoEvent[]> {
filters = normalizeFilters(filters);
if (opts?.signal?.aborted) return Promise.reject(abortError());
if (!filters.length) return Promise.resolve([]);
logi({ level: 'debug', ns: 'ditto.req', source: 'search', filters: filters as JsonValue });
const query = filters[0]?.search;
if (this.#relay && this.#relay.socket.readyState === WebSocket.OPEN) {
logi({ level: 'debug', ns: 'ditto.search', query, source: 'relay', relay: this.#relay.socket.url });
const events = await this.#relay.query(filters, opts);
return hydrateEvents({
events,
store: this.#hydrator,
signal: opts?.signal,
});
} else {
logi({ level: 'debug', ns: 'ditto.search', query, source: 'db' });
return this.#fallback.query(filters, opts);
}
}
}
export { SearchStore };