mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Remove SearchStore
This commit is contained in:
parent
3e7f33a63d
commit
aabe6350a7
6 changed files with 3 additions and 221 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 }))
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
Loading…
Add table
Reference in a new issue