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