diff --git a/src/filter.test.ts b/src/filter.test.ts index efc00d7..b1ea872 100644 --- a/src/filter.test.ts +++ b/src/filter.test.ts @@ -4,7 +4,7 @@ import { assertEquals } from '@/deps-test.ts'; import event0 from '~/fixtures/events/event-0.json' assert { type: 'json' }; import event1 from '~/fixtures/events/event-1.json' assert { type: 'json' }; -import { eventToMicroFilter, getFilterId, getMicroFilters, isMicrofilter } from './filter.ts'; +import { eventToMicroFilter, getFilterId, getFilterLimit, getMicroFilters, isMicrofilter } from './filter.ts'; Deno.test('getMicroFilters', () => { const event = event0 as Event<0>; @@ -35,3 +35,13 @@ Deno.test('getFilterId', () => { '{"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); +}); diff --git a/src/filter.ts b/src/filter.ts index 430a8d3..b98d44e 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -2,6 +2,7 @@ import { Conf } from '@/config.ts'; import { type Event, type Filter, matchFilters, stringifyStable, z } from '@/deps.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; import { type EventData } from '@/types.ts'; +import { isReplaceableKind } from '@/kinds.ts'; /** Additional properties that may be added by Ditto to events. */ type Relation = 'author' | 'author_stats' | 'event_stats'; @@ -82,11 +83,34 @@ function isMicrofilter(filter: Filter): filter is MicroFilter { return microFilterSchema.safeParse(filter).success; } +/** Calculate the intrinsic limit of a filter. */ +function getFilterLimit(filter: Filter): 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; + + return Math.min( + Math.max(0, filter.limit ?? Infinity), + filter.ids?.length ?? Infinity, + filter.authors?.length && + filter.kinds?.every((kind) => isReplaceableKind(kind)) + ? filter.authors.length * filter.kinds.length + : Infinity, + ); +} + +/** Returns true if the filter could potentially return any stored events at all. */ +function canFilter(filter: Filter): boolean { + return getFilterLimit(filter) > 0; +} + export { type AuthorMicrofilter, + canFilter, type DittoFilter, eventToMicroFilter, getFilterId, + getFilterLimit, getMicroFilters, type IdMicrofilter, isMicrofilter, diff --git a/src/storages/memorelay.ts b/src/storages/memorelay.ts index 71c68c6..66c2c17 100644 --- a/src/storages/memorelay.ts +++ b/src/storages/memorelay.ts @@ -1,4 +1,5 @@ import { Debug, type Event, type Filter, LRUCache, matchFilter, matchFilters } from '@/deps.ts'; +import { canFilter, getFilterLimit } from '@/filter.ts'; import { type EventStore, type GetEventsOpts } from '@/store.ts'; /** In-memory data store for events. */ @@ -27,6 +28,7 @@ class Memorelay implements EventStore { /** Get events from memory. */ getEvents(filters: Filter[], opts: GetEventsOpts = {}): Promise[]> { if (opts.signal?.aborted) return Promise.resolve([]); + filters = filters.filter(canFilter); if (!filters.length) return Promise.resolve([]); this.#debug('REQ', JSON.stringify(filters)); @@ -37,7 +39,7 @@ class Memorelay implements EventStore { let index = 0; for (const filter of filters) { - const limit = filter.limit ?? Infinity; + const limit = getFilterLimit(filter); const usage = usages[index] ?? 0; if (usage >= limit) { @@ -50,7 +52,7 @@ class Memorelay implements EventStore { index++; } - if (filters.every((filter, index) => usages[index] >= (filter.limit ?? Infinity))) { + if (filters.every((filter, index) => usages[index] >= getFilterLimit(filter))) { break; } }