Add Optimizer storage with EventSet
This commit is contained in:
parent
48ce1ba6c9
commit
939eeae25a
|
@ -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 };
|
|
@ -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);
|
||||
});
|
|
@ -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 };
|
Loading…
Reference in New Issue