applesauce-core 0.0.0-next-20250913205403 → 0.0.0-next-20250916120818
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/event-store/async-event-store.d.ts +16 -11
- package/dist/event-store/async-event-store.js +75 -49
- package/dist/event-store/event-memory.d.ts +74 -0
- package/dist/event-store/event-memory.js +349 -0
- package/dist/event-store/event-store.d.ts +12 -7
- package/dist/event-store/event-store.js +76 -48
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/interface.d.ts +24 -23
- package/dist/helpers/gift-wraps.d.ts +2 -2
- package/dist/helpers/gift-wraps.js +2 -2
- package/dist/observable/claim-events.d.ts +2 -2
- package/dist/observable/claim-latest.d.ts +2 -2
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { Filter, NostrEvent } from "nostr-tools";
|
|
|
2
2
|
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
|
3
3
|
import { Observable, Subject } from "rxjs";
|
|
4
4
|
import { AddressPointerWithoutD } from "../helpers/pointers.js";
|
|
5
|
+
import { EventMemory } from "./event-memory.js";
|
|
5
6
|
import { IAsyncEventDatabase, IAsyncEventStore } from "./interface.js";
|
|
6
7
|
declare const AsyncEventStore_base: {
|
|
7
8
|
new (...args: any[]): {
|
|
@@ -39,6 +40,8 @@ declare const AsyncEventStore_base: {
|
|
|
39
40
|
/** An async wrapper around an async event database that handles replaceable events, deletes, and models */
|
|
40
41
|
export declare class AsyncEventStore extends AsyncEventStore_base implements IAsyncEventStore {
|
|
41
42
|
database: IAsyncEventDatabase;
|
|
43
|
+
/** Optional memory database for ensuring single event instances */
|
|
44
|
+
memory?: EventMemory;
|
|
42
45
|
/** Enable this to keep old versions of replaceable events */
|
|
43
46
|
keepOldVersions: boolean;
|
|
44
47
|
/** Enable this to keep expired events */
|
|
@@ -70,6 +73,8 @@ export declare class AsyncEventStore extends AsyncEventStore_base implements IAs
|
|
|
70
73
|
*/
|
|
71
74
|
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
72
75
|
constructor(database: IAsyncEventDatabase);
|
|
76
|
+
/** A method to add all events to memory to ensure there is only ever a single instance of an event */
|
|
77
|
+
private mapToMemory;
|
|
73
78
|
protected deletedIds: Set<string>;
|
|
74
79
|
protected deletedCoords: Map<string, number>;
|
|
75
80
|
protected checkDeleted(event: string | NostrEvent): boolean;
|
|
@@ -93,12 +98,6 @@ export declare class AsyncEventStore extends AsyncEventStore_base implements IAs
|
|
|
93
98
|
remove(event: string | NostrEvent): Promise<boolean>;
|
|
94
99
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
95
100
|
update(event: NostrEvent): Promise<void>;
|
|
96
|
-
/** Passthrough method for the database.touch */
|
|
97
|
-
touch(event: NostrEvent): Promise<void>;
|
|
98
|
-
/** Pass through method for the database.unclaimed */
|
|
99
|
-
unclaimed(): AsyncGenerator<NostrEvent>;
|
|
100
|
-
/** Removes any event that is not being used by a subscription */
|
|
101
|
-
prune(limit?: number): Promise<number>;
|
|
102
101
|
/** Check if the store has an event by id */
|
|
103
102
|
hasEvent(id: string): Promise<boolean>;
|
|
104
103
|
/** Get an event by id from the store */
|
|
@@ -110,17 +109,23 @@ export declare class AsyncEventStore extends AsyncEventStore_base implements IAs
|
|
|
110
109
|
/** Returns all versions of a replaceable event */
|
|
111
110
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
|
|
112
111
|
/** Get all events matching a filter */
|
|
113
|
-
getByFilters(filters: Filter | Filter[]): Promise<
|
|
112
|
+
getByFilters(filters: Filter | Filter[]): Promise<NostrEvent[]>;
|
|
114
113
|
/** Returns a timeline of events that match filters */
|
|
115
114
|
getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
|
|
115
|
+
/** Passthrough method for the database.touch */
|
|
116
|
+
touch(event: NostrEvent): void | undefined;
|
|
116
117
|
/** Sets the claim on the event and touches it */
|
|
117
|
-
claim(event: NostrEvent, claim: any):
|
|
118
|
+
claim(event: NostrEvent, claim: any): void;
|
|
118
119
|
/** Checks if an event is claimed by anything */
|
|
119
|
-
isClaimed(event: NostrEvent):
|
|
120
|
+
isClaimed(event: NostrEvent): boolean;
|
|
120
121
|
/** Removes a claim from an event */
|
|
121
|
-
removeClaim(event: NostrEvent, claim: any):
|
|
122
|
+
removeClaim(event: NostrEvent, claim: any): void;
|
|
122
123
|
/** Removes all claims on an event */
|
|
123
|
-
clearClaim(event: NostrEvent):
|
|
124
|
+
clearClaim(event: NostrEvent): void;
|
|
125
|
+
/** Pass through method for the database.unclaimed */
|
|
126
|
+
unclaimed(): Generator<NostrEvent>;
|
|
127
|
+
/** Removes any event that is not being used by a subscription */
|
|
128
|
+
prune(limit?: number): number;
|
|
124
129
|
/** Returns an observable that completes when an event is removed */
|
|
125
130
|
removed(id: string): Observable<never>;
|
|
126
131
|
/** Creates an observable that emits when event is updated */
|
|
@@ -7,12 +7,14 @@ import { getExpirationTimestamp } from "../helpers/expiration.js";
|
|
|
7
7
|
import { parseCoordinate } from "../helpers/pointers.js";
|
|
8
8
|
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
|
|
9
9
|
import { unixNow } from "../helpers/time.js";
|
|
10
|
-
|
|
10
|
+
import { EventMemory } from "./event-memory.js";
|
|
11
11
|
import { EventStoreModelMixin } from "./model-mixin.js";
|
|
12
12
|
/** An async wrapper around an async event database that handles replaceable events, deletes, and models */
|
|
13
13
|
export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
14
14
|
}) {
|
|
15
15
|
database;
|
|
16
|
+
/** Optional memory database for ensuring single event instances */
|
|
17
|
+
memory;
|
|
16
18
|
/** Enable this to keep old versions of replaceable events */
|
|
17
19
|
keepOldVersions = false;
|
|
18
20
|
/** Enable this to keep expired events */
|
|
@@ -46,6 +48,7 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
|
46
48
|
constructor(database) {
|
|
47
49
|
super();
|
|
48
50
|
this.database = database;
|
|
51
|
+
this.memory = new EventMemory();
|
|
49
52
|
// when events are added to the database, add the symbol
|
|
50
53
|
this.insert$.subscribe((event) => {
|
|
51
54
|
Reflect.set(event, EventStoreSymbol, this);
|
|
@@ -55,6 +58,13 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
|
55
58
|
Reflect.deleteProperty(event, EventStoreSymbol);
|
|
56
59
|
});
|
|
57
60
|
}
|
|
61
|
+
mapToMemory(event) {
|
|
62
|
+
if (event === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (!this.memory)
|
|
65
|
+
return event;
|
|
66
|
+
return this.memory.add(event);
|
|
67
|
+
}
|
|
58
68
|
// delete state
|
|
59
69
|
deletedIds = new Set();
|
|
60
70
|
deletedCoords = new Map();
|
|
@@ -176,22 +186,22 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
|
176
186
|
return existing[0];
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
|
-
else if (await this.database.hasEvent(event.id)) {
|
|
180
|
-
// Duplicate event, copy symbols and return existing event
|
|
181
|
-
const existing = await this.database.getEvent(event.id);
|
|
182
|
-
if (existing) {
|
|
183
|
-
AsyncEventStore.mergeDuplicateEvent(event, existing);
|
|
184
|
-
return existing;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
189
|
// Verify event before inserting into the database
|
|
188
190
|
if (this.verifyEvent && this.verifyEvent(event) === false)
|
|
189
191
|
return null;
|
|
192
|
+
// Always add event to memory
|
|
193
|
+
const existing = this.memory?.add(event);
|
|
194
|
+
// If the memory returned a different instance, this is a duplicate event
|
|
195
|
+
if (existing && existing !== event) {
|
|
196
|
+
// Copy cached symbols and return existing event
|
|
197
|
+
AsyncEventStore.mergeDuplicateEvent(event, existing);
|
|
198
|
+
// attach relay this event was from
|
|
199
|
+
if (fromRelay)
|
|
200
|
+
addSeenRelay(existing, fromRelay);
|
|
201
|
+
return existing;
|
|
202
|
+
}
|
|
190
203
|
// Insert event into database
|
|
191
|
-
const inserted = await this.database.add(event);
|
|
192
|
-
// If the event was ignored, return null
|
|
193
|
-
if (inserted === null)
|
|
194
|
-
return null;
|
|
204
|
+
const inserted = this.mapToMemory(await this.database.add(event));
|
|
195
205
|
// Copy cached data if its a duplicate event
|
|
196
206
|
if (event !== inserted)
|
|
197
207
|
AsyncEventStore.mergeDuplicateEvent(event, inserted);
|
|
@@ -225,6 +235,9 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
|
225
235
|
const e = await this.database.getEvent(typeof event === "string" ? event : event.id);
|
|
226
236
|
if (!e)
|
|
227
237
|
return false;
|
|
238
|
+
// Remove from memory if available
|
|
239
|
+
if (this.memory)
|
|
240
|
+
this.memory.remove(typeof event === "string" ? event : event.id);
|
|
228
241
|
const removed = await this.database.remove(event);
|
|
229
242
|
if (removed && e)
|
|
230
243
|
this.remove$.next(e);
|
|
@@ -236,72 +249,85 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
|
236
249
|
const e = await this.database.add(event);
|
|
237
250
|
if (!e)
|
|
238
251
|
return;
|
|
239
|
-
|
|
252
|
+
// Notify the database that the event has updated
|
|
253
|
+
this.database.update?.(event);
|
|
240
254
|
this.update$.next(event);
|
|
241
255
|
}
|
|
242
|
-
/** Passthrough method for the database.touch */
|
|
243
|
-
async touch(event) {
|
|
244
|
-
return await this.database.touch(event);
|
|
245
|
-
}
|
|
246
|
-
/** Pass through method for the database.unclaimed */
|
|
247
|
-
unclaimed() {
|
|
248
|
-
return this.database.unclaimed();
|
|
249
|
-
}
|
|
250
|
-
/** Removes any event that is not being used by a subscription */
|
|
251
|
-
async prune(limit) {
|
|
252
|
-
let removed = 0;
|
|
253
|
-
const generator = this.database.unclaimed();
|
|
254
|
-
for await (const event of generator) {
|
|
255
|
-
await this.remove(event);
|
|
256
|
-
removed++;
|
|
257
|
-
if (limit && removed >= limit)
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
return removed;
|
|
261
|
-
}
|
|
262
256
|
/** Check if the store has an event by id */
|
|
263
257
|
async hasEvent(id) {
|
|
264
|
-
|
|
258
|
+
// Check if the event exists in memory first, then in the database
|
|
259
|
+
return (this.memory?.hasEvent(id) ?? false) || (await this.database.hasEvent(id));
|
|
265
260
|
}
|
|
266
261
|
/** Get an event by id from the store */
|
|
267
262
|
async getEvent(id) {
|
|
268
|
-
|
|
263
|
+
// Get the event from memory first, then from the database
|
|
264
|
+
return this.memory?.getEvent(id) ?? this.mapToMemory(await this.database.getEvent(id));
|
|
269
265
|
}
|
|
270
266
|
/** Check if the store has a replaceable event */
|
|
271
267
|
async hasReplaceable(kind, pubkey, d) {
|
|
272
|
-
|
|
268
|
+
// Check if the event exists in memory first, then in the database
|
|
269
|
+
return ((this.memory?.hasReplaceable(kind, pubkey, d) ?? false) || (await this.database.hasReplaceable(kind, pubkey, d)));
|
|
273
270
|
}
|
|
274
271
|
/** Gets the latest version of a replaceable event */
|
|
275
272
|
async getReplaceable(kind, pubkey, identifier) {
|
|
276
|
-
|
|
273
|
+
// Get the event from memory first, then from the database
|
|
274
|
+
return (this.memory?.getReplaceable(kind, pubkey, identifier) ??
|
|
275
|
+
this.mapToMemory(await this.database.getReplaceable(kind, pubkey, identifier)));
|
|
277
276
|
}
|
|
278
277
|
/** Returns all versions of a replaceable event */
|
|
279
278
|
async getReplaceableHistory(kind, pubkey, identifier) {
|
|
280
|
-
|
|
279
|
+
// Get the events from memory first, then from the database
|
|
280
|
+
const memoryEvents = this.memory?.getReplaceableHistory(kind, pubkey, identifier);
|
|
281
|
+
if (memoryEvents)
|
|
282
|
+
return memoryEvents;
|
|
283
|
+
const dbEvents = await this.database.getReplaceableHistory(kind, pubkey, identifier);
|
|
284
|
+
return dbEvents?.map((e) => this.mapToMemory(e) ?? e);
|
|
281
285
|
}
|
|
282
286
|
/** Get all events matching a filter */
|
|
283
287
|
async getByFilters(filters) {
|
|
284
|
-
|
|
288
|
+
// NOTE: no way to read from memory since memory won't have the full set of events
|
|
289
|
+
const events = await this.database.getByFilters(filters);
|
|
290
|
+
// Map events to memory if available for better performance
|
|
291
|
+
if (this.memory)
|
|
292
|
+
return events.map((e) => this.mapToMemory(e) ?? e);
|
|
293
|
+
else
|
|
294
|
+
return events;
|
|
285
295
|
}
|
|
286
296
|
/** Returns a timeline of events that match filters */
|
|
287
297
|
async getTimeline(filters) {
|
|
288
|
-
|
|
298
|
+
const events = await this.database.getTimeline(filters);
|
|
299
|
+
if (this.memory)
|
|
300
|
+
return events.map((e) => this.mapToMemory(e));
|
|
301
|
+
else
|
|
302
|
+
return events;
|
|
303
|
+
}
|
|
304
|
+
/** Passthrough method for the database.touch */
|
|
305
|
+
touch(event) {
|
|
306
|
+
return this.memory?.touch(event);
|
|
289
307
|
}
|
|
290
308
|
/** Sets the claim on the event and touches it */
|
|
291
|
-
|
|
292
|
-
return
|
|
309
|
+
claim(event, claim) {
|
|
310
|
+
return this.memory?.claim(event, claim);
|
|
293
311
|
}
|
|
294
312
|
/** Checks if an event is claimed by anything */
|
|
295
|
-
|
|
296
|
-
return
|
|
313
|
+
isClaimed(event) {
|
|
314
|
+
return this.memory?.isClaimed(event) ?? false;
|
|
297
315
|
}
|
|
298
316
|
/** Removes a claim from an event */
|
|
299
|
-
|
|
300
|
-
return
|
|
317
|
+
removeClaim(event, claim) {
|
|
318
|
+
return this.memory?.removeClaim(event, claim);
|
|
301
319
|
}
|
|
302
320
|
/** Removes all claims on an event */
|
|
303
|
-
|
|
304
|
-
return
|
|
321
|
+
clearClaim(event) {
|
|
322
|
+
return this.memory?.clearClaim(event);
|
|
323
|
+
}
|
|
324
|
+
/** Pass through method for the database.unclaimed */
|
|
325
|
+
unclaimed() {
|
|
326
|
+
return this.memory?.unclaimed() || (function* () { })();
|
|
327
|
+
}
|
|
328
|
+
/** Removes any event that is not being used by a subscription */
|
|
329
|
+
prune(limit) {
|
|
330
|
+
return this.memory?.prune(limit) ?? 0;
|
|
305
331
|
}
|
|
306
332
|
/** Returns an observable that completes when an event is removed */
|
|
307
333
|
removed(id) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { LRU } from "../helpers/lru.js";
|
|
3
|
+
import { IEventMemory } from "./interface.js";
|
|
4
|
+
/** An in-memory database of events */
|
|
5
|
+
export declare class EventMemory implements IEventMemory {
|
|
6
|
+
protected log: import("debug").Debugger;
|
|
7
|
+
/** Indexes */
|
|
8
|
+
protected kinds: Map<number, Set<import("nostr-tools").Event>>;
|
|
9
|
+
protected authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
10
|
+
protected tags: LRU<Set<import("nostr-tools").Event>>;
|
|
11
|
+
protected created_at: NostrEvent[];
|
|
12
|
+
/** LRU cache of last events touched */
|
|
13
|
+
events: LRU<import("nostr-tools").Event>;
|
|
14
|
+
/** A sorted array of replaceable events by address */
|
|
15
|
+
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
16
|
+
/** The number of events in the event set */
|
|
17
|
+
get size(): number;
|
|
18
|
+
/** Checks if the database contains an event without touching it */
|
|
19
|
+
hasEvent(id: string): boolean;
|
|
20
|
+
/** Gets a single event based on id */
|
|
21
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
22
|
+
/** Checks if the event set has a replaceable event */
|
|
23
|
+
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
24
|
+
/** Gets the latest replaceable event */
|
|
25
|
+
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
26
|
+
/** Gets the history of a replaceable event */
|
|
27
|
+
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
28
|
+
/** Gets all events that match the filters */
|
|
29
|
+
getByFilters(filters: Filter | Filter[]): NostrEvent[];
|
|
30
|
+
/** Gets a timeline of events that match the filters */
|
|
31
|
+
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
32
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
33
|
+
add(event: NostrEvent): NostrEvent;
|
|
34
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
35
|
+
remove(eventOrId: string | NostrEvent): boolean;
|
|
36
|
+
/** Notify the database that an event has updated */
|
|
37
|
+
update(_event: NostrEvent): void;
|
|
38
|
+
/** A weak map of events that are claimed by other things */
|
|
39
|
+
protected claims: WeakMap<import("nostr-tools").Event, any>;
|
|
40
|
+
/** Moves an event to the top of the LRU cache */
|
|
41
|
+
touch(event: NostrEvent): void;
|
|
42
|
+
/** Sets the claim on the event and touches it */
|
|
43
|
+
claim(event: NostrEvent, claim: any): void;
|
|
44
|
+
/** Checks if an event is claimed by anything */
|
|
45
|
+
isClaimed(event: NostrEvent): boolean;
|
|
46
|
+
/** Removes a claim from an event */
|
|
47
|
+
removeClaim(event: NostrEvent, claim: any): void;
|
|
48
|
+
/** Removes all claims on an event */
|
|
49
|
+
clearClaim(event: NostrEvent): void;
|
|
50
|
+
/** Returns a generator of unclaimed events in order of least used */
|
|
51
|
+
unclaimed(): Generator<NostrEvent>;
|
|
52
|
+
/** Removes events that are not claimed (free up memory) */
|
|
53
|
+
prune(limit?: number): number;
|
|
54
|
+
/** Index helper methods */
|
|
55
|
+
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
|
|
56
|
+
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
|
|
57
|
+
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
58
|
+
/** Iterates over all events by author */
|
|
59
|
+
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
|
|
60
|
+
/** Iterates over all events by indexable tag and value */
|
|
61
|
+
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
|
|
62
|
+
/** Iterates over all events by kind */
|
|
63
|
+
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
|
|
64
|
+
/** Iterates over all events by time */
|
|
65
|
+
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
|
|
66
|
+
/** Iterates over all events by id */
|
|
67
|
+
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
68
|
+
/** Returns all events that match the filter */
|
|
69
|
+
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
70
|
+
/** Returns all events that match the filters */
|
|
71
|
+
getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
|
|
72
|
+
/** Resets the event set */
|
|
73
|
+
reset(): void;
|
|
74
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
|
+
import { getIndexableTags, INDEXABLE_TAGS } from "../helpers/event-tags.js";
|
|
3
|
+
import { createReplaceableAddress, isReplaceable } from "../helpers/event.js";
|
|
4
|
+
import { LRU } from "../helpers/lru.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
/** An in-memory database of events */
|
|
7
|
+
export class EventMemory {
|
|
8
|
+
log = logger.extend("EventMemory");
|
|
9
|
+
/** Indexes */
|
|
10
|
+
kinds = new Map();
|
|
11
|
+
authors = new Map();
|
|
12
|
+
tags = new LRU();
|
|
13
|
+
created_at = [];
|
|
14
|
+
/** LRU cache of last events touched */
|
|
15
|
+
events = new LRU();
|
|
16
|
+
/** A sorted array of replaceable events by address */
|
|
17
|
+
replaceable = new Map();
|
|
18
|
+
/** The number of events in the event set */
|
|
19
|
+
get size() {
|
|
20
|
+
return this.events.size;
|
|
21
|
+
}
|
|
22
|
+
/** Checks if the database contains an event without touching it */
|
|
23
|
+
hasEvent(id) {
|
|
24
|
+
return this.events.has(id);
|
|
25
|
+
}
|
|
26
|
+
/** Gets a single event based on id */
|
|
27
|
+
getEvent(id) {
|
|
28
|
+
return this.events.get(id);
|
|
29
|
+
}
|
|
30
|
+
/** Checks if the event set has a replaceable event */
|
|
31
|
+
hasReplaceable(kind, pubkey, identifier) {
|
|
32
|
+
const events = this.replaceable.get(createReplaceableAddress(kind, pubkey, identifier));
|
|
33
|
+
return !!events && events.length > 0;
|
|
34
|
+
}
|
|
35
|
+
/** Gets the latest replaceable event */
|
|
36
|
+
getReplaceable(kind, pubkey, identifier) {
|
|
37
|
+
const address = createReplaceableAddress(kind, pubkey, identifier);
|
|
38
|
+
const events = this.replaceable.get(address);
|
|
39
|
+
return events?.[0];
|
|
40
|
+
}
|
|
41
|
+
/** Gets the history of a replaceable event */
|
|
42
|
+
getReplaceableHistory(kind, pubkey, identifier) {
|
|
43
|
+
const address = createReplaceableAddress(kind, pubkey, identifier);
|
|
44
|
+
return this.replaceable.get(address);
|
|
45
|
+
}
|
|
46
|
+
/** Gets all events that match the filters */
|
|
47
|
+
getByFilters(filters) {
|
|
48
|
+
return Array.from(this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]));
|
|
49
|
+
}
|
|
50
|
+
/** Gets a timeline of events that match the filters */
|
|
51
|
+
getTimeline(filters) {
|
|
52
|
+
const timeline = [];
|
|
53
|
+
const events = this.getByFilters(filters);
|
|
54
|
+
for (const event of events)
|
|
55
|
+
insertEventIntoDescendingList(timeline, event);
|
|
56
|
+
return timeline;
|
|
57
|
+
}
|
|
58
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
59
|
+
add(event) {
|
|
60
|
+
const id = event.id;
|
|
61
|
+
const current = this.events.get(id);
|
|
62
|
+
if (current)
|
|
63
|
+
return current;
|
|
64
|
+
this.events.set(id, event);
|
|
65
|
+
this.getKindIndex(event.kind).add(event);
|
|
66
|
+
this.getAuthorsIndex(event.pubkey).add(event);
|
|
67
|
+
// Add the event to the tag indexes if they exist
|
|
68
|
+
for (const tag of getIndexableTags(event)) {
|
|
69
|
+
if (this.tags.has(tag))
|
|
70
|
+
this.getTagIndex(tag).add(event);
|
|
71
|
+
}
|
|
72
|
+
// Insert into time index
|
|
73
|
+
insertEventIntoDescendingList(this.created_at, event);
|
|
74
|
+
// Insert into replaceable index
|
|
75
|
+
if (isReplaceable(event.kind)) {
|
|
76
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
77
|
+
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
78
|
+
let array = this.replaceable.get(address);
|
|
79
|
+
if (!this.replaceable.has(address)) {
|
|
80
|
+
// add an empty array if there is no array
|
|
81
|
+
array = [];
|
|
82
|
+
this.replaceable.set(address, array);
|
|
83
|
+
}
|
|
84
|
+
// insert the event into the sorted array
|
|
85
|
+
insertEventIntoDescendingList(array, event);
|
|
86
|
+
}
|
|
87
|
+
return event;
|
|
88
|
+
}
|
|
89
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
90
|
+
remove(eventOrId) {
|
|
91
|
+
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
|
|
92
|
+
if (!event)
|
|
93
|
+
throw new Error("Missing event");
|
|
94
|
+
const id = event.id;
|
|
95
|
+
// only remove events that are known
|
|
96
|
+
if (!this.events.has(id))
|
|
97
|
+
return false;
|
|
98
|
+
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
99
|
+
this.getKindIndex(event.kind).delete(event);
|
|
100
|
+
for (const tag of getIndexableTags(event)) {
|
|
101
|
+
if (this.tags.has(tag)) {
|
|
102
|
+
this.getTagIndex(tag).delete(event);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// remove from created_at index
|
|
106
|
+
const i = this.created_at.indexOf(event);
|
|
107
|
+
this.created_at.splice(i, 1);
|
|
108
|
+
this.events.delete(id);
|
|
109
|
+
// remove from replaceable index
|
|
110
|
+
if (isReplaceable(event.kind)) {
|
|
111
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
112
|
+
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
113
|
+
const array = this.replaceable.get(address);
|
|
114
|
+
if (array && array.includes(event)) {
|
|
115
|
+
const idx = array.indexOf(event);
|
|
116
|
+
array.splice(idx, 1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// remove any claims this event has
|
|
120
|
+
this.claims.delete(event);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/** Notify the database that an event has updated */
|
|
124
|
+
update(_event) {
|
|
125
|
+
// Do nothing
|
|
126
|
+
}
|
|
127
|
+
/** A weak map of events that are claimed by other things */
|
|
128
|
+
claims = new WeakMap();
|
|
129
|
+
/** Moves an event to the top of the LRU cache */
|
|
130
|
+
touch(event) {
|
|
131
|
+
// Make sure the event is in the database before adding it to the LRU
|
|
132
|
+
if (!this.events.has(event.id))
|
|
133
|
+
return;
|
|
134
|
+
// Move to the top of the LRU
|
|
135
|
+
this.events.set(event.id, event);
|
|
136
|
+
}
|
|
137
|
+
/** Sets the claim on the event and touches it */
|
|
138
|
+
claim(event, claim) {
|
|
139
|
+
if (!this.claims.has(event)) {
|
|
140
|
+
this.claims.set(event, claim);
|
|
141
|
+
}
|
|
142
|
+
// always touch event
|
|
143
|
+
this.touch(event);
|
|
144
|
+
}
|
|
145
|
+
/** Checks if an event is claimed by anything */
|
|
146
|
+
isClaimed(event) {
|
|
147
|
+
return this.claims.has(event);
|
|
148
|
+
}
|
|
149
|
+
/** Removes a claim from an event */
|
|
150
|
+
removeClaim(event, claim) {
|
|
151
|
+
const current = this.claims.get(event);
|
|
152
|
+
if (current === claim)
|
|
153
|
+
this.claims.delete(event);
|
|
154
|
+
}
|
|
155
|
+
/** Removes all claims on an event */
|
|
156
|
+
clearClaim(event) {
|
|
157
|
+
this.claims.delete(event);
|
|
158
|
+
}
|
|
159
|
+
/** Returns a generator of unclaimed events in order of least used */
|
|
160
|
+
*unclaimed() {
|
|
161
|
+
let removed = 0;
|
|
162
|
+
let cursor = this.events.first;
|
|
163
|
+
while (cursor) {
|
|
164
|
+
const event = cursor.value;
|
|
165
|
+
if (!this.isClaimed(event))
|
|
166
|
+
yield event;
|
|
167
|
+
cursor = cursor.next;
|
|
168
|
+
}
|
|
169
|
+
return removed;
|
|
170
|
+
}
|
|
171
|
+
/** Removes events that are not claimed (free up memory) */
|
|
172
|
+
prune(limit) {
|
|
173
|
+
let removed = 0;
|
|
174
|
+
const unclaimed = this.unclaimed();
|
|
175
|
+
for (const event of unclaimed) {
|
|
176
|
+
this.remove(event);
|
|
177
|
+
removed++;
|
|
178
|
+
if (limit && removed >= limit)
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
return removed;
|
|
182
|
+
}
|
|
183
|
+
/** Index helper methods */
|
|
184
|
+
getKindIndex(kind) {
|
|
185
|
+
if (!this.kinds.has(kind))
|
|
186
|
+
this.kinds.set(kind, new Set());
|
|
187
|
+
return this.kinds.get(kind);
|
|
188
|
+
}
|
|
189
|
+
getAuthorsIndex(author) {
|
|
190
|
+
if (!this.authors.has(author))
|
|
191
|
+
this.authors.set(author, new Set());
|
|
192
|
+
return this.authors.get(author);
|
|
193
|
+
}
|
|
194
|
+
getTagIndex(tagAndValue) {
|
|
195
|
+
if (!this.tags.has(tagAndValue)) {
|
|
196
|
+
// build new tag index from existing events
|
|
197
|
+
const events = new Set();
|
|
198
|
+
const ts = Date.now();
|
|
199
|
+
for (const event of this.events.values()) {
|
|
200
|
+
if (getIndexableTags(event).has(tagAndValue)) {
|
|
201
|
+
events.add(event);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const took = Date.now() - ts;
|
|
205
|
+
if (took > 100)
|
|
206
|
+
this.log(`Built index ${tagAndValue} took ${took}ms`);
|
|
207
|
+
this.tags.set(tagAndValue, events);
|
|
208
|
+
}
|
|
209
|
+
return this.tags.get(tagAndValue);
|
|
210
|
+
}
|
|
211
|
+
/** Iterates over all events by author */
|
|
212
|
+
*iterateAuthors(authors) {
|
|
213
|
+
for (const author of authors) {
|
|
214
|
+
const events = this.authors.get(author);
|
|
215
|
+
if (events) {
|
|
216
|
+
for (const event of events)
|
|
217
|
+
yield event;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Iterates over all events by indexable tag and value */
|
|
222
|
+
*iterateTag(tag, values) {
|
|
223
|
+
for (const value of values) {
|
|
224
|
+
const events = this.getTagIndex(tag + ":" + value);
|
|
225
|
+
if (events) {
|
|
226
|
+
for (const event of events)
|
|
227
|
+
yield event;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Iterates over all events by kind */
|
|
232
|
+
*iterateKinds(kinds) {
|
|
233
|
+
for (const kind of kinds) {
|
|
234
|
+
const events = this.kinds.get(kind);
|
|
235
|
+
if (events) {
|
|
236
|
+
for (const event of events)
|
|
237
|
+
yield event;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Iterates over all events by time */
|
|
242
|
+
*iterateTime(since, until) {
|
|
243
|
+
let untilIndex = 0;
|
|
244
|
+
let sinceIndex = this.created_at.length - 1;
|
|
245
|
+
let start = until
|
|
246
|
+
? binarySearch(this.created_at, (mid) => {
|
|
247
|
+
return mid.created_at - until;
|
|
248
|
+
})
|
|
249
|
+
: undefined;
|
|
250
|
+
if (start)
|
|
251
|
+
untilIndex = start[0];
|
|
252
|
+
const end = since
|
|
253
|
+
? binarySearch(this.created_at, (mid) => {
|
|
254
|
+
return mid.created_at - since;
|
|
255
|
+
})
|
|
256
|
+
: undefined;
|
|
257
|
+
if (end)
|
|
258
|
+
sinceIndex = end[0];
|
|
259
|
+
for (let i = untilIndex; i < sinceIndex; i++) {
|
|
260
|
+
yield this.created_at[i];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Iterates over all events by id */
|
|
264
|
+
*iterateIds(ids) {
|
|
265
|
+
for (const id of ids) {
|
|
266
|
+
if (this.events.has(id))
|
|
267
|
+
yield this.events.get(id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Returns all events that match the filter */
|
|
271
|
+
getEventsForFilter(filter) {
|
|
272
|
+
// search is not supported, return an empty set
|
|
273
|
+
if (filter.search)
|
|
274
|
+
return new Set();
|
|
275
|
+
let first = true;
|
|
276
|
+
let events = new Set();
|
|
277
|
+
const and = (iterable) => {
|
|
278
|
+
const set = iterable instanceof Set ? iterable : new Set(iterable);
|
|
279
|
+
if (first) {
|
|
280
|
+
events = set;
|
|
281
|
+
first = false;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
for (const event of events) {
|
|
285
|
+
if (!set.has(event))
|
|
286
|
+
events.delete(event);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return events;
|
|
290
|
+
};
|
|
291
|
+
if (filter.ids)
|
|
292
|
+
and(this.iterateIds(filter.ids));
|
|
293
|
+
let time = null;
|
|
294
|
+
// query for time first if since is set
|
|
295
|
+
if (filter.since !== undefined) {
|
|
296
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
297
|
+
and(time);
|
|
298
|
+
}
|
|
299
|
+
for (const t of INDEXABLE_TAGS) {
|
|
300
|
+
const key = `#${t}`;
|
|
301
|
+
const values = filter[key];
|
|
302
|
+
if (values?.length)
|
|
303
|
+
and(this.iterateTag(t, values));
|
|
304
|
+
}
|
|
305
|
+
if (filter.authors)
|
|
306
|
+
and(this.iterateAuthors(filter.authors));
|
|
307
|
+
if (filter.kinds)
|
|
308
|
+
and(this.iterateKinds(filter.kinds));
|
|
309
|
+
// query for time last if only until is set
|
|
310
|
+
if (filter.since === undefined && filter.until !== undefined) {
|
|
311
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
312
|
+
and(time);
|
|
313
|
+
}
|
|
314
|
+
// if the filter queried on time and has a limit. truncate the events now
|
|
315
|
+
if (filter.limit && time) {
|
|
316
|
+
const limited = new Set();
|
|
317
|
+
for (const event of time) {
|
|
318
|
+
if (limited.size >= filter.limit)
|
|
319
|
+
break;
|
|
320
|
+
if (events.has(event))
|
|
321
|
+
limited.add(event);
|
|
322
|
+
}
|
|
323
|
+
return limited;
|
|
324
|
+
}
|
|
325
|
+
return events;
|
|
326
|
+
}
|
|
327
|
+
/** Returns all events that match the filters */
|
|
328
|
+
getEventsForFilters(filters) {
|
|
329
|
+
if (filters.length === 0)
|
|
330
|
+
throw new Error("No Filters");
|
|
331
|
+
let events = new Set();
|
|
332
|
+
for (const filter of filters) {
|
|
333
|
+
const filtered = this.getEventsForFilter(filter);
|
|
334
|
+
for (const event of filtered)
|
|
335
|
+
events.add(event);
|
|
336
|
+
}
|
|
337
|
+
return events;
|
|
338
|
+
}
|
|
339
|
+
/** Resets the event set */
|
|
340
|
+
reset() {
|
|
341
|
+
this.events.clear();
|
|
342
|
+
this.kinds.clear();
|
|
343
|
+
this.authors.clear();
|
|
344
|
+
this.tags.clear();
|
|
345
|
+
this.created_at = [];
|
|
346
|
+
this.replaceable.clear();
|
|
347
|
+
this.claims = new WeakMap();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Filter, NostrEvent } from "nostr-tools";
|
|
|
2
2
|
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
|
3
3
|
import { Observable, Subject } from "rxjs";
|
|
4
4
|
import { AddressPointerWithoutD } from "../helpers/pointers.js";
|
|
5
|
+
import { EventMemory } from "./event-memory.js";
|
|
5
6
|
import { IEventDatabase, IEventStore } from "./interface.js";
|
|
6
7
|
declare const EventStore_base: {
|
|
7
8
|
new (...args: any[]): {
|
|
@@ -39,6 +40,8 @@ declare const EventStore_base: {
|
|
|
39
40
|
/** A wrapper around an event database that handles replaceable events, deletes, and models */
|
|
40
41
|
export declare class EventStore extends EventStore_base implements IEventStore {
|
|
41
42
|
database: IEventDatabase;
|
|
43
|
+
/** Optional memory database for ensuring single event instances */
|
|
44
|
+
memory?: EventMemory;
|
|
42
45
|
/** Enable this to keep old versions of replaceable events */
|
|
43
46
|
keepOldVersions: boolean;
|
|
44
47
|
/** Enable this to keep expired events */
|
|
@@ -70,6 +73,8 @@ export declare class EventStore extends EventStore_base implements IEventStore {
|
|
|
70
73
|
*/
|
|
71
74
|
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
72
75
|
constructor(database?: IEventDatabase);
|
|
76
|
+
/** A method to add all events to memory to ensure there is only ever a single instance of an event */
|
|
77
|
+
private mapToMemory;
|
|
73
78
|
protected deletedIds: Set<string>;
|
|
74
79
|
protected deletedCoords: Map<string, number>;
|
|
75
80
|
protected checkDeleted(event: string | NostrEvent): boolean;
|
|
@@ -93,12 +98,6 @@ export declare class EventStore extends EventStore_base implements IEventStore {
|
|
|
93
98
|
remove(event: string | NostrEvent): boolean;
|
|
94
99
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
95
100
|
update(event: NostrEvent): boolean;
|
|
96
|
-
/** Passthrough method for the database.touch */
|
|
97
|
-
touch(event: NostrEvent): void;
|
|
98
|
-
/** Pass through method for the database.unclaimed */
|
|
99
|
-
unclaimed(): Generator<NostrEvent>;
|
|
100
|
-
/** Removes any event that is not being used by a subscription */
|
|
101
|
-
prune(limit?: number): number;
|
|
102
101
|
/** Check if the store has an event by id */
|
|
103
102
|
hasEvent(id: string): boolean;
|
|
104
103
|
/** Get an event by id from the store */
|
|
@@ -110,9 +109,11 @@ export declare class EventStore extends EventStore_base implements IEventStore {
|
|
|
110
109
|
/** Returns all versions of a replaceable event */
|
|
111
110
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
112
111
|
/** Get all events matching a filter */
|
|
113
|
-
getByFilters(filters: Filter | Filter[]):
|
|
112
|
+
getByFilters(filters: Filter | Filter[]): NostrEvent[];
|
|
114
113
|
/** Returns a timeline of events that match filters */
|
|
115
114
|
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
115
|
+
/** Passthrough method for the database.touch */
|
|
116
|
+
touch(event: NostrEvent): void | undefined;
|
|
116
117
|
/** Sets the claim on the event and touches it */
|
|
117
118
|
claim(event: NostrEvent, claim: any): void;
|
|
118
119
|
/** Checks if an event is claimed by anything */
|
|
@@ -121,6 +122,10 @@ export declare class EventStore extends EventStore_base implements IEventStore {
|
|
|
121
122
|
removeClaim(event: NostrEvent, claim: any): void;
|
|
122
123
|
/** Removes all claims on an event */
|
|
123
124
|
clearClaim(event: NostrEvent): void;
|
|
125
|
+
/** Pass through method for the database.unclaimed */
|
|
126
|
+
unclaimed(): Generator<NostrEvent>;
|
|
127
|
+
/** Removes any event that is not being used by a subscription */
|
|
128
|
+
prune(limit?: number): number;
|
|
124
129
|
/** Returns an observable that completes when an event is removed */
|
|
125
130
|
removed(id: string): Observable<never>;
|
|
126
131
|
/** Creates an observable that emits when event is updated */
|
|
@@ -7,13 +7,14 @@ import { getExpirationTimestamp } from "../helpers/expiration.js";
|
|
|
7
7
|
import { parseCoordinate } from "../helpers/pointers.js";
|
|
8
8
|
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
|
|
9
9
|
import { unixNow } from "../helpers/time.js";
|
|
10
|
-
|
|
11
|
-
import { InMemoryEventDatabase } from "./event-database.js";
|
|
10
|
+
import { EventMemory } from "./event-memory.js";
|
|
12
11
|
import { EventStoreModelMixin } from "./model-mixin.js";
|
|
13
12
|
/** A wrapper around an event database that handles replaceable events, deletes, and models */
|
|
14
13
|
export class EventStore extends EventStoreModelMixin(class {
|
|
15
14
|
}) {
|
|
16
15
|
database;
|
|
16
|
+
/** Optional memory database for ensuring single event instances */
|
|
17
|
+
memory;
|
|
17
18
|
/** Enable this to keep old versions of replaceable events */
|
|
18
19
|
keepOldVersions = false;
|
|
19
20
|
/** Enable this to keep expired events */
|
|
@@ -44,9 +45,16 @@ export class EventStore extends EventStoreModelMixin(class {
|
|
|
44
45
|
* @experimental
|
|
45
46
|
*/
|
|
46
47
|
addressableLoader;
|
|
47
|
-
constructor(database = new
|
|
48
|
+
constructor(database = new EventMemory()) {
|
|
48
49
|
super();
|
|
49
|
-
|
|
50
|
+
if (database) {
|
|
51
|
+
this.database = database;
|
|
52
|
+
this.memory = new EventMemory();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// If no database is provided, its the same as having a memory database
|
|
56
|
+
this.database = this.memory = new EventMemory();
|
|
57
|
+
}
|
|
50
58
|
// when events are added to the database, add the symbol
|
|
51
59
|
this.insert$.subscribe((event) => {
|
|
52
60
|
Reflect.set(event, EventStoreSymbol, this);
|
|
@@ -56,6 +64,13 @@ export class EventStore extends EventStoreModelMixin(class {
|
|
|
56
64
|
Reflect.deleteProperty(event, EventStoreSymbol);
|
|
57
65
|
});
|
|
58
66
|
}
|
|
67
|
+
mapToMemory(event) {
|
|
68
|
+
if (event === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!this.memory)
|
|
71
|
+
return event;
|
|
72
|
+
return this.memory.add(event);
|
|
73
|
+
}
|
|
59
74
|
// delete state
|
|
60
75
|
deletedIds = new Set();
|
|
61
76
|
deletedCoords = new Map();
|
|
@@ -175,22 +190,22 @@ export class EventStore extends EventStoreModelMixin(class {
|
|
|
175
190
|
return existing[0];
|
|
176
191
|
}
|
|
177
192
|
}
|
|
178
|
-
else if (this.database.hasEvent(event.id)) {
|
|
179
|
-
// Duplicate event, copy symbols and return existing event
|
|
180
|
-
const existing = this.database.getEvent(event.id);
|
|
181
|
-
if (existing) {
|
|
182
|
-
EventStore.mergeDuplicateEvent(event, existing);
|
|
183
|
-
return existing;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
193
|
// Verify event before inserting into the database
|
|
187
194
|
if (this.verifyEvent && this.verifyEvent(event) === false)
|
|
188
195
|
return null;
|
|
196
|
+
// Always add event to memory
|
|
197
|
+
const existing = this.memory?.add(event);
|
|
198
|
+
// If the memory returned a different instance, this is a duplicate event
|
|
199
|
+
if (existing && existing !== event) {
|
|
200
|
+
// Copy cached symbols and return existing event
|
|
201
|
+
EventStore.mergeDuplicateEvent(event, existing);
|
|
202
|
+
// attach relay this event was from
|
|
203
|
+
if (fromRelay)
|
|
204
|
+
addSeenRelay(existing, fromRelay);
|
|
205
|
+
return existing;
|
|
206
|
+
}
|
|
189
207
|
// Insert event into database
|
|
190
|
-
const inserted = this.database.add(event);
|
|
191
|
-
// If the event was ignored, return null
|
|
192
|
-
if (inserted === null)
|
|
193
|
-
return null;
|
|
208
|
+
const inserted = this.mapToMemory(this.database.add(event));
|
|
194
209
|
// Copy cached data if its a duplicate event
|
|
195
210
|
if (event !== inserted)
|
|
196
211
|
EventStore.mergeDuplicateEvent(event, inserted);
|
|
@@ -224,6 +239,9 @@ export class EventStore extends EventStoreModelMixin(class {
|
|
|
224
239
|
const e = this.database.getEvent(typeof event === "string" ? event : event.id);
|
|
225
240
|
if (!e)
|
|
226
241
|
return false;
|
|
242
|
+
// Remove from memory if available
|
|
243
|
+
if (this.memory)
|
|
244
|
+
this.memory.remove(event);
|
|
227
245
|
const removed = this.database.remove(event);
|
|
228
246
|
if (removed && e)
|
|
229
247
|
this.remove$.next(e);
|
|
@@ -235,73 +253,83 @@ export class EventStore extends EventStoreModelMixin(class {
|
|
|
235
253
|
const e = this.database.add(event);
|
|
236
254
|
if (!e)
|
|
237
255
|
return false;
|
|
238
|
-
|
|
256
|
+
// Notify the database that the event has updated
|
|
257
|
+
this.database.update?.(event);
|
|
239
258
|
this.update$.next(event);
|
|
240
259
|
return true;
|
|
241
260
|
}
|
|
242
|
-
/** Passthrough method for the database.touch */
|
|
243
|
-
touch(event) {
|
|
244
|
-
this.database.touch(event);
|
|
245
|
-
}
|
|
246
|
-
/** Pass through method for the database.unclaimed */
|
|
247
|
-
unclaimed() {
|
|
248
|
-
return this.database.unclaimed();
|
|
249
|
-
}
|
|
250
|
-
/** Removes any event that is not being used by a subscription */
|
|
251
|
-
prune(limit) {
|
|
252
|
-
let removed = 0;
|
|
253
|
-
const unclaimed = this.database.unclaimed();
|
|
254
|
-
for (const event of unclaimed) {
|
|
255
|
-
this.remove(event);
|
|
256
|
-
removed++;
|
|
257
|
-
if (limit && removed >= limit)
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
return removed;
|
|
261
|
-
}
|
|
262
261
|
/** Check if the store has an event by id */
|
|
263
262
|
hasEvent(id) {
|
|
264
|
-
|
|
263
|
+
// Check if the event exists in memory first, then in the database
|
|
264
|
+
return this.memory?.hasEvent(id) || this.database.hasEvent(id);
|
|
265
265
|
}
|
|
266
266
|
/** Get an event by id from the store */
|
|
267
267
|
getEvent(id) {
|
|
268
|
-
|
|
268
|
+
// Get the event from memory first, then from the database
|
|
269
|
+
return this.memory?.getEvent(id) ?? this.mapToMemory(this.database.getEvent(id));
|
|
269
270
|
}
|
|
270
271
|
/** Check if the store has a replaceable event */
|
|
271
272
|
hasReplaceable(kind, pubkey, d) {
|
|
272
|
-
|
|
273
|
+
// Check if the event exists in memory first, then in the database
|
|
274
|
+
return this.memory?.hasReplaceable(kind, pubkey, d) || this.database.hasReplaceable(kind, pubkey, d);
|
|
273
275
|
}
|
|
274
276
|
/** Gets the latest version of a replaceable event */
|
|
275
277
|
getReplaceable(kind, pubkey, identifier) {
|
|
276
|
-
|
|
278
|
+
// Get the event from memory first, then from the database
|
|
279
|
+
return (this.memory?.getReplaceable(kind, pubkey, identifier) ??
|
|
280
|
+
this.mapToMemory(this.database.getReplaceable(kind, pubkey, identifier)));
|
|
277
281
|
}
|
|
278
282
|
/** Returns all versions of a replaceable event */
|
|
279
283
|
getReplaceableHistory(kind, pubkey, identifier) {
|
|
280
|
-
|
|
284
|
+
// Get the events from memory first, then from the database
|
|
285
|
+
return (this.memory?.getReplaceableHistory(kind, pubkey, identifier) ??
|
|
286
|
+
this.database.getReplaceableHistory(kind, pubkey, identifier)?.map((e) => this.mapToMemory(e) ?? e));
|
|
281
287
|
}
|
|
282
288
|
/** Get all events matching a filter */
|
|
283
289
|
getByFilters(filters) {
|
|
284
|
-
|
|
290
|
+
// NOTE: no way to read from memory since memory won't have the full set of events
|
|
291
|
+
const events = this.database.getByFilters(filters);
|
|
292
|
+
// Map events to memory if available for better performance
|
|
293
|
+
if (this.memory)
|
|
294
|
+
return events.map((e) => this.mapToMemory(e));
|
|
295
|
+
else
|
|
296
|
+
return events;
|
|
285
297
|
}
|
|
286
298
|
/** Returns a timeline of events that match filters */
|
|
287
299
|
getTimeline(filters) {
|
|
288
|
-
|
|
300
|
+
const events = this.database.getTimeline(filters);
|
|
301
|
+
if (this.memory)
|
|
302
|
+
return events.map((e) => this.mapToMemory(e));
|
|
303
|
+
else
|
|
304
|
+
return events;
|
|
305
|
+
}
|
|
306
|
+
/** Passthrough method for the database.touch */
|
|
307
|
+
touch(event) {
|
|
308
|
+
return this.memory?.touch(event);
|
|
289
309
|
}
|
|
290
310
|
/** Sets the claim on the event and touches it */
|
|
291
311
|
claim(event, claim) {
|
|
292
|
-
return this.
|
|
312
|
+
return this.memory?.claim(event, claim);
|
|
293
313
|
}
|
|
294
314
|
/** Checks if an event is claimed by anything */
|
|
295
315
|
isClaimed(event) {
|
|
296
|
-
return this.
|
|
316
|
+
return this.memory?.isClaimed(event) ?? false;
|
|
297
317
|
}
|
|
298
318
|
/** Removes a claim from an event */
|
|
299
319
|
removeClaim(event, claim) {
|
|
300
|
-
return this.
|
|
320
|
+
return this.memory?.removeClaim(event, claim);
|
|
301
321
|
}
|
|
302
322
|
/** Removes all claims on an event */
|
|
303
323
|
clearClaim(event) {
|
|
304
|
-
return this.
|
|
324
|
+
return this.memory?.clearClaim(event);
|
|
325
|
+
}
|
|
326
|
+
/** Pass through method for the database.unclaimed */
|
|
327
|
+
unclaimed() {
|
|
328
|
+
return this.memory?.unclaimed() || (function* () { })();
|
|
329
|
+
}
|
|
330
|
+
/** Removes any event that is not being used by a subscription */
|
|
331
|
+
prune(limit) {
|
|
332
|
+
return this.memory?.prune(limit) ?? 0;
|
|
305
333
|
}
|
|
306
334
|
/** Returns an observable that completes when an event is removed */
|
|
307
335
|
removed(id) {
|
|
@@ -18,7 +18,7 @@ export interface IEventStoreRead {
|
|
|
18
18
|
/** Get the history of a replaceable event */
|
|
19
19
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
20
20
|
/** Get all events that match the filters */
|
|
21
|
-
getByFilters(filters: Filter | Filter[]):
|
|
21
|
+
getByFilters(filters: Filter | Filter[]): NostrEvent[];
|
|
22
22
|
/** Get a timeline of events that match the filters */
|
|
23
23
|
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
24
24
|
}
|
|
@@ -35,7 +35,7 @@ export interface IAsyncEventStoreRead {
|
|
|
35
35
|
/** Get the history of a replaceable event */
|
|
36
36
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
|
|
37
37
|
/** Get all events that match the filters */
|
|
38
|
-
getByFilters(filters: Filter | Filter[]): Promise<
|
|
38
|
+
getByFilters(filters: Filter | Filter[]): Promise<NostrEvent[]>;
|
|
39
39
|
/** Get a timeline of events that match the filters */
|
|
40
40
|
getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
|
|
41
41
|
}
|
|
@@ -81,21 +81,6 @@ export interface IEventClaims {
|
|
|
81
81
|
/** Returns a generator of unclaimed events in order of least used */
|
|
82
82
|
unclaimed(): Generator<NostrEvent>;
|
|
83
83
|
}
|
|
84
|
-
/** The async claim interface for an event store */
|
|
85
|
-
export interface IAsyncEventClaims {
|
|
86
|
-
/** Tell the store that this event was used */
|
|
87
|
-
touch(event: NostrEvent): Promise<void>;
|
|
88
|
-
/** Sets the claim on the event and touches it */
|
|
89
|
-
claim(event: NostrEvent, claim: any): Promise<void>;
|
|
90
|
-
/** Checks if an event is claimed by anything */
|
|
91
|
-
isClaimed(event: NostrEvent): Promise<boolean>;
|
|
92
|
-
/** Removes a claim from an event */
|
|
93
|
-
removeClaim(event: NostrEvent, claim: any): Promise<void>;
|
|
94
|
-
/** Removes all claims on an event */
|
|
95
|
-
clearClaim(event: NostrEvent): Promise<void>;
|
|
96
|
-
/** Returns a generator of unclaimed events in order of least used */
|
|
97
|
-
unclaimed(): AsyncGenerator<NostrEvent>;
|
|
98
|
-
}
|
|
99
84
|
/** An event store that can be subscribed to */
|
|
100
85
|
export interface IEventSubscriptions {
|
|
101
86
|
/** Subscribe to an event by id */
|
|
@@ -155,14 +140,30 @@ export type Model<T extends unknown, TStore extends IEventStore | IAsyncEventSto
|
|
|
155
140
|
export type ModelConstructor<T extends unknown, Args extends Array<any>, TStore extends IEventStore | IAsyncEventStore = IEventStore> = ((...args: Args) => Model<T, TStore>) & {
|
|
156
141
|
getKey?: (...args: Args) => string;
|
|
157
142
|
};
|
|
158
|
-
/** The base interface for a
|
|
159
|
-
export interface IEventDatabase extends IEventStoreRead
|
|
143
|
+
/** The base interface for a database of events */
|
|
144
|
+
export interface IEventDatabase extends IEventStoreRead {
|
|
145
|
+
/** Add an event to the database */
|
|
146
|
+
add(event: NostrEvent): NostrEvent;
|
|
147
|
+
/** Remove an event from the database */
|
|
148
|
+
remove(event: string | NostrEvent): boolean;
|
|
149
|
+
/** Notifies the database that an event has updated */
|
|
150
|
+
update?: (event: NostrEvent) => void;
|
|
160
151
|
}
|
|
161
152
|
/** The async base interface for a set of events */
|
|
162
|
-
export interface IAsyncEventDatabase extends IAsyncEventStoreRead
|
|
153
|
+
export interface IAsyncEventDatabase extends IAsyncEventStoreRead {
|
|
154
|
+
/** Add an event to the database */
|
|
155
|
+
add(event: NostrEvent): Promise<NostrEvent>;
|
|
156
|
+
/** Remove an event from the database */
|
|
157
|
+
remove(event: string | NostrEvent): Promise<boolean>;
|
|
158
|
+
/** Notifies the database that an event has updated */
|
|
159
|
+
update?: (event: NostrEvent) => void;
|
|
163
160
|
}
|
|
164
|
-
/**
|
|
165
|
-
export interface
|
|
161
|
+
/** The base interface for the in-memory database of events */
|
|
162
|
+
export interface IEventMemory extends IEventStoreRead, IEventClaims {
|
|
163
|
+
/** Add an event to the store */
|
|
164
|
+
add(event: NostrEvent): NostrEvent;
|
|
165
|
+
/** Remove an event from the store */
|
|
166
|
+
remove(event: string | NostrEvent): boolean;
|
|
166
167
|
}
|
|
167
168
|
/** A set of methods that an event store will use to load single events it does not have */
|
|
168
169
|
export interface IEventFallbackLoaders {
|
|
@@ -174,7 +175,7 @@ export interface IEventFallbackLoaders {
|
|
|
174
175
|
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
175
176
|
}
|
|
176
177
|
/** The async event store interface */
|
|
177
|
-
export interface IAsyncEventStore extends IAsyncEventStoreRead, IEventStoreStreams, IEventSubscriptions, IAsyncEventStoreActions, IEventModelMixin<IAsyncEventStore>, IEventHelpfulSubscriptions,
|
|
178
|
+
export interface IAsyncEventStore extends IAsyncEventStoreRead, IEventStoreStreams, IEventSubscriptions, IAsyncEventStoreActions, IEventModelMixin<IAsyncEventStore>, IEventHelpfulSubscriptions, IEventClaims, IEventFallbackLoaders {
|
|
178
179
|
}
|
|
179
180
|
/** The sync event store interface */
|
|
180
181
|
export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventSubscriptions, IEventStoreActions, IEventModelMixin<IEventStore>, IEventHelpfulSubscriptions, IEventClaims, IEventFallbackLoaders {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { NostrEvent, UnsignedEvent } from "nostr-tools";
|
|
2
|
-
import {
|
|
2
|
+
import { EventMemory } from "../event-store/event-memory.js";
|
|
3
3
|
import { EncryptedContentSigner } from "./encrypted-content.js";
|
|
4
4
|
/**
|
|
5
5
|
* An internal event set to keep track of seals and rumors
|
|
6
6
|
* This is intentionally isolated from the main applications event store so to prevent seals and rumors from being leaked
|
|
7
7
|
*/
|
|
8
|
-
export declare const internalGiftWrapEvents:
|
|
8
|
+
export declare const internalGiftWrapEvents: EventMemory;
|
|
9
9
|
export type Rumor = UnsignedEvent & {
|
|
10
10
|
id: string;
|
|
11
11
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { verifyEvent } from "nostr-tools";
|
|
2
|
-
import {
|
|
2
|
+
import { EventMemory } from "../event-store/event-memory.js";
|
|
3
3
|
import { getEncryptedContent, isEncryptedContentLocked, lockEncryptedContent, unlockEncryptedContent, } from "./encrypted-content.js";
|
|
4
4
|
import { notifyEventUpdate } from "./event.js";
|
|
5
5
|
/**
|
|
6
6
|
* An internal event set to keep track of seals and rumors
|
|
7
7
|
* This is intentionally isolated from the main applications event store so to prevent seals and rumors from being leaked
|
|
8
8
|
*/
|
|
9
|
-
export const internalGiftWrapEvents = new
|
|
9
|
+
export const internalGiftWrapEvents = new EventMemory();
|
|
10
10
|
/** Used to store a reference to the seal event on gift wraps (downstream) or the seal event on rumors (upstream[]) */
|
|
11
11
|
export const SealSymbol = Symbol.for("seal");
|
|
12
12
|
/** Used to store a reference to the rumor on seals (downstream) */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NostrEvent } from "nostr-tools";
|
|
2
2
|
import { MonoTypeOperatorFunction } from "rxjs";
|
|
3
|
-
import {
|
|
3
|
+
import { IEventClaims } from "../event-store/interface.js";
|
|
4
4
|
/** keep a claim on any event that goes through this observable, claims are removed when the observable completes */
|
|
5
|
-
export declare function claimEvents<T extends NostrEvent[] | NostrEvent | undefined>(claims: IEventClaims
|
|
5
|
+
export declare function claimEvents<T extends NostrEvent[] | NostrEvent | undefined>(claims: IEventClaims): MonoTypeOperatorFunction<T>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NostrEvent } from "nostr-tools";
|
|
2
2
|
import { MonoTypeOperatorFunction } from "rxjs";
|
|
3
|
-
import {
|
|
3
|
+
import { IEventClaims } from "../event-store/interface.js";
|
|
4
4
|
/** An operator that claims the latest event with the database */
|
|
5
|
-
export declare function claimLatest<T extends NostrEvent | undefined>(claims: IEventClaims
|
|
5
|
+
export declare function claimLatest<T extends NostrEvent | undefined>(claims: IEventClaims): MonoTypeOperatorFunction<T>;
|