Add Optimizer storage with EventSet

This commit is contained in:
Alex Gleason 2024-01-03 20:22:02 -06:00
parent 48ce1ba6c9
commit 939eeae25a
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
3 changed files with 279 additions and 0 deletions

93
src/storages/optimizer.ts Normal file
View File

@ -0,0 +1,93 @@
import { type DittoEvent, type EventStore, type GetEventsOpts, type StoreEventOpts } from '@/store.ts';
import { type DittoFilter, normalizeFilters } from '@/filter.ts';
import { EventSet } from '@/utils/event-set.ts';
interface OptimizerOpts {
db: EventStore;
cache: EventStore;
client: EventStore;
}
class Optimizer implements EventStore {
#db: EventStore;
#cache: EventStore;
#client: EventStore;
constructor(opts: OptimizerOpts) {
this.#db = opts.db;
this.#cache = opts.cache;
this.#client = opts.client;
}
async storeEvent(event: DittoEvent<number>, opts?: StoreEventOpts | undefined): Promise<void> {
await Promise.all([
this.#db.storeEvent(event, opts),
this.#cache.storeEvent(event, opts),
]);
}
async getEvents<K extends number>(
filters: DittoFilter<K>[],
opts: GetEventsOpts | undefined = {},
): Promise<DittoEvent<K>[]> {
const { limit = Infinity } = opts;
filters = normalizeFilters(filters);
if (opts?.signal?.aborted) return Promise.resolve([]);
if (!filters.length) return Promise.resolve([]);
const results = new EventSet<DittoEvent<K>>();
// Filters with IDs are immutable, so we can take them straight from the cache if we have them.
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (filter.ids) {
const ids = new Set<string>(filter.ids);
for (const event of await this.#cache.getEvents([filter], opts)) {
ids.delete(event.id);
results.add(event);
if (results.size >= limit) return getResults();
}
filters[i] = { ...filter, ids: [...ids] };
}
}
filters = normalizeFilters(filters);
if (!filters.length) return getResults();
// Query the database for events.
for (const dbEvent of await this.#db.getEvents(filters, opts)) {
results.add(dbEvent);
if (results.size >= limit) return getResults();
}
// Query the cache again.
for (const cacheEvent of await this.#cache.getEvents(filters, opts)) {
results.add(cacheEvent);
if (results.size >= limit) return getResults();
}
// Finally, query the client.
for (const clientEvent of await this.#client.getEvents(filters, opts)) {
results.add(clientEvent);
if (results.size >= limit) return getResults();
}
/** Get return type from map. */
function getResults() {
return [...results.values()];
}
return getResults();
}
countEvents<K extends number>(_filters: DittoFilter<K>[]): Promise<number> {
throw new Error('COUNT not implemented.');
}
deleteEvents<K extends number>(_filters: DittoFilter<K>[]): Promise<void> {
throw new Error('DELETE not implemented.');
}
}
export { Optimizer };

109
src/utils/event-set.test.ts Normal file
View File

@ -0,0 +1,109 @@
import { assertEquals } from '@/deps-test.ts';
import { EventSet } from './event-set.ts';
Deno.test('EventSet', () => {
const set = new EventSet();
assertEquals(set.size, 0);
const event = { id: '1', kind: 0, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [] };
set.add(event);
assertEquals(set.size, 1);
assertEquals(set.has(event), true);
set.add(event);
assertEquals(set.size, 1);
assertEquals(set.has(event), true);
set.delete(event);
assertEquals(set.size, 0);
assertEquals(set.has(event), false);
set.delete(event);
assertEquals(set.size, 0);
assertEquals(set.has(event), false);
set.add(event);
assertEquals(set.size, 1);
assertEquals(set.has(event), true);
set.clear();
assertEquals(set.size, 0);
assertEquals(set.has(event), false);
});
Deno.test('EventSet.add (replaceable)', () => {
const event0 = { id: '1', kind: 0, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [] };
const event1 = { id: '2', kind: 0, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [] };
const event2 = { id: '3', kind: 0, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [] };
const set = new EventSet();
set.add(event0);
assertEquals(set.size, 1);
assertEquals(set.has(event0), true);
set.add(event1);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), true);
set.add(event2);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), false);
assertEquals(set.has(event2), true);
});
Deno.test('EventSet.add (parameterized)', () => {
const event0 = { id: '1', kind: 30000, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [['d', 'a']] };
const event1 = { id: '2', kind: 30000, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [['d', 'a']] };
const event2 = { id: '3', kind: 30000, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [['d', 'a']] };
const set = new EventSet();
set.add(event0);
assertEquals(set.size, 1);
assertEquals(set.has(event0), true);
set.add(event1);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), true);
set.add(event2);
assertEquals(set.size, 1);
assertEquals(set.has(event0), false);
assertEquals(set.has(event1), false);
assertEquals(set.has(event2), true);
});
Deno.test('EventSet.eventReplaces', () => {
const event0 = { id: '1', kind: 0, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [] };
const event1 = { id: '2', kind: 0, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [] };
const event2 = { id: '3', kind: 0, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [] };
const event3 = { id: '4', kind: 0, pubkey: 'def', content: '', created_at: 0, sig: '', tags: [] };
assertEquals(EventSet.eventReplaces(event1, event0), true);
assertEquals(EventSet.eventReplaces(event2, event0), true);
assertEquals(EventSet.eventReplaces(event2, event1), true);
assertEquals(EventSet.eventReplaces(event0, event1), false);
assertEquals(EventSet.eventReplaces(event0, event2), false);
assertEquals(EventSet.eventReplaces(event1, event2), false);
assertEquals(EventSet.eventReplaces(event3, event1), false);
assertEquals(EventSet.eventReplaces(event1, event3), false);
});
Deno.test('EventSet.eventReplaces (parameterized)', () => {
const event0 = { id: '1', kind: 30000, pubkey: 'abc', content: '', created_at: 0, sig: '', tags: [['d', 'a']] };
const event1 = { id: '2', kind: 30000, pubkey: 'abc', content: '', created_at: 1, sig: '', tags: [['d', 'a']] };
const event2 = { id: '3', kind: 30000, pubkey: 'abc', content: '', created_at: 2, sig: '', tags: [['d', 'a']] };
assertEquals(EventSet.eventReplaces(event1, event0), true);
assertEquals(EventSet.eventReplaces(event2, event0), true);
assertEquals(EventSet.eventReplaces(event2, event1), true);
assertEquals(EventSet.eventReplaces(event0, event1), false);
assertEquals(EventSet.eventReplaces(event0, event2), false);
assertEquals(EventSet.eventReplaces(event1, event2), false);
});

77
src/utils/event-set.ts Normal file
View File

@ -0,0 +1,77 @@
import { type Event } from '@/deps.ts';
import { isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts';
/** In-memory store for Nostr events with replaceable event functionality. */
class EventSet<E extends Event = Event> implements Set<E> {
#map = new Map<string, E>();
get size() {
return this.#map.size;
}
add(event: E): this {
if (isReplaceableKind(event.kind) || isParameterizedReplaceableKind(event.kind)) {
for (const e of this.values()) {
if (EventSet.eventReplaces(event, e)) {
this.delete(e);
}
}
}
this.#map.set(event.id, event);
return this;
}
clear(): void {
this.#map.clear();
}
delete(event: E): boolean {
return this.#map.delete(event.id);
}
forEach(callbackfn: (event: E, key: E, set: Set<E>) => void, thisArg?: any): void {
return this.#map.forEach((event, _id) => callbackfn(event, event, this), thisArg);
}
has(event: E): boolean {
return this.#map.has(event.id);
}
*entries(): IterableIterator<[E, E]> {
for (const event of this.#map.values()) {
yield [event, event];
}
}
keys(): IterableIterator<E> {
return this.#map.values();
}
values(): IterableIterator<E> {
return this.#map.values();
}
[Symbol.iterator](): IterableIterator<E> {
return this.#map.values();
}
[Symbol.toStringTag]: string = 'EventSet';
/** Returns true if both events are replaceable, belong to the same pubkey (and `d` tag, for parameterized events), and the first event is newer than the second one. */
static eventReplaces(event: Event, event2: Event): boolean {
if (isReplaceableKind(event.kind)) {
return event.kind === event2.kind && event.pubkey === event2.pubkey && event.created_at > event2.created_at;
} else if (isParameterizedReplaceableKind(event.kind)) {
const d = event.tags.find(([name]) => name === 'd')?.[1] || '';
const d2 = event2.tags.find(([name]) => name === 'd')?.[1] || '';
return event.kind === event2.kind &&
event.pubkey === event2.pubkey &&
d === d2 &&
event.created_at > event2.created_at;
}
return false;
}
}
export { EventSet };