applesauce-core 0.12.0 → 0.12.1
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/__tests__/event-store.test.js +31 -0
- package/dist/event-store/database.d.ts +1 -0
- package/dist/event-store/database.js +5 -6
- package/dist/event-store/event-store.d.ts +4 -0
- package/dist/event-store/event-store.js +47 -15
- package/dist/event-store/interface.d.ts +2 -0
- package/package.json +1 -1
|
@@ -48,6 +48,30 @@ describe("add", () => {
|
|
|
48
48
|
expect(eventStore.getEvent(profile.id)).toBeUndefined();
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
+
describe("inserts", () => {
|
|
52
|
+
it("should emit newer replaceable events", () => {
|
|
53
|
+
const spy = subscribeSpyTo(eventStore.inserts);
|
|
54
|
+
eventStore.add(profile);
|
|
55
|
+
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 100 });
|
|
56
|
+
eventStore.add(newer);
|
|
57
|
+
expect(spy.getValues()).toEqual([profile, newer]);
|
|
58
|
+
});
|
|
59
|
+
it("should not emit when older replaceable event is added", () => {
|
|
60
|
+
const spy = subscribeSpyTo(eventStore.inserts);
|
|
61
|
+
eventStore.add(profile);
|
|
62
|
+
eventStore.add(user.profile({ name: "new name" }, { created_at: profile.created_at - 1000 }));
|
|
63
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("removes", () => {
|
|
67
|
+
it("should emit older replaceable events when the newest replaceable event is added", () => {
|
|
68
|
+
const spy = subscribeSpyTo(eventStore.removes);
|
|
69
|
+
eventStore.add(profile);
|
|
70
|
+
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
|
|
71
|
+
eventStore.add(newer);
|
|
72
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
51
75
|
describe("verifyEvent", () => {
|
|
52
76
|
it("should be called for all events added", () => {
|
|
53
77
|
const verifyEvent = vi.fn().mockReturnValue(true);
|
|
@@ -187,6 +211,13 @@ describe("replaceable", () => {
|
|
|
187
211
|
eventStore.add(user.profile({ name: "really old name" }, { created_at: profile.created_at - 1000 }));
|
|
188
212
|
expect(spy.getValues()).toEqual([profile]);
|
|
189
213
|
});
|
|
214
|
+
it("should emit newer events", () => {
|
|
215
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
216
|
+
eventStore.add(profile);
|
|
217
|
+
const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 500 });
|
|
218
|
+
eventStore.add(newProfile);
|
|
219
|
+
expect(spy.getValues()).toEqual([profile, newProfile]);
|
|
220
|
+
});
|
|
190
221
|
});
|
|
191
222
|
describe("timeline", () => {
|
|
192
223
|
it("should emit an empty array if there are not events", () => {
|
|
@@ -14,6 +14,7 @@ export declare class Database {
|
|
|
14
14
|
protected created_at: NostrEvent[];
|
|
15
15
|
/** LRU cache of last events touched */
|
|
16
16
|
events: LRU<import("nostr-tools").Event>;
|
|
17
|
+
/** A sorted array of replaceable events by uid */
|
|
17
18
|
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
18
19
|
/** A stream of events inserted into the database */
|
|
19
20
|
inserted: Subject<import("nostr-tools").Event>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import { Subject } from "rxjs";
|
|
3
|
-
import {
|
|
3
|
+
import { getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
|
|
4
4
|
import { INDEXABLE_TAGS } from "./common.js";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
6
|
import { LRU } from "../helpers/lru.js";
|
|
@@ -17,6 +17,7 @@ export class Database {
|
|
|
17
17
|
created_at = [];
|
|
18
18
|
/** LRU cache of last events touched */
|
|
19
19
|
events = new LRU();
|
|
20
|
+
/** A sorted array of replaceable events by uid */
|
|
20
21
|
replaceable = new Map();
|
|
21
22
|
/** A stream of events inserted into the database */
|
|
22
23
|
inserted = new Subject();
|
|
@@ -83,12 +84,8 @@ export class Database {
|
|
|
83
84
|
addEvent(event) {
|
|
84
85
|
const id = event.id;
|
|
85
86
|
const current = this.events.get(id);
|
|
86
|
-
if (current)
|
|
87
|
-
// if this is a duplicate event, transfer some important symbols
|
|
88
|
-
if (event[FromCacheSymbol])
|
|
89
|
-
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
87
|
+
if (current)
|
|
90
88
|
return current;
|
|
91
|
-
}
|
|
92
89
|
this.onBeforeInsert?.(event);
|
|
93
90
|
this.events.set(id, event);
|
|
94
91
|
this.getKindIndex(event.kind).add(event);
|
|
@@ -105,9 +102,11 @@ export class Database {
|
|
|
105
102
|
const uid = getEventUID(event);
|
|
106
103
|
let array = this.replaceable.get(uid);
|
|
107
104
|
if (!this.replaceable.has(uid)) {
|
|
105
|
+
// add an empty array if there is no array
|
|
108
106
|
array = [];
|
|
109
107
|
this.replaceable.set(uid, array);
|
|
110
108
|
}
|
|
109
|
+
// insert the event into the sorted array
|
|
111
110
|
insertEventIntoDescendingList(array, event);
|
|
112
111
|
}
|
|
113
112
|
this.inserted.next(event);
|
|
@@ -9,8 +9,12 @@ export declare class EventStore implements IEventStore {
|
|
|
9
9
|
keepOldVersions: boolean;
|
|
10
10
|
/** A method used to verify new events before added them */
|
|
11
11
|
verifyEvent?: (event: NostrEvent) => boolean;
|
|
12
|
+
/** A stream of new events added to the store */
|
|
13
|
+
inserts: Observable<NostrEvent>;
|
|
12
14
|
/** A stream of events that have been updated */
|
|
13
15
|
updates: Observable<NostrEvent>;
|
|
16
|
+
/** A stream of events that have been removed */
|
|
17
|
+
removes: Observable<NostrEvent>;
|
|
14
18
|
constructor();
|
|
15
19
|
protected deletedIds: Set<string>;
|
|
16
20
|
protected deletedCoords: Map<string, number>;
|
|
@@ -3,7 +3,7 @@ import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
|
3
3
|
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
4
4
|
import { defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, from, map, merge, mergeMap, mergeWith, of, repeat, scan, take, takeUntil, tap, } from "rxjs";
|
|
5
5
|
import { Database } from "./database.js";
|
|
6
|
-
import { getEventUID, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
|
|
6
|
+
import { FromCacheSymbol, getEventUID, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
|
|
7
7
|
import { matchFilters } from "../helpers/filter.js";
|
|
8
8
|
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
|
|
9
9
|
import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
|
|
@@ -19,8 +19,12 @@ export class EventStore {
|
|
|
19
19
|
keepOldVersions = false;
|
|
20
20
|
/** A method used to verify new events before added them */
|
|
21
21
|
verifyEvent;
|
|
22
|
+
/** A stream of new events added to the store */
|
|
23
|
+
inserts;
|
|
22
24
|
/** A stream of events that have been updated */
|
|
23
25
|
updates;
|
|
26
|
+
/** A stream of events that have been removed */
|
|
27
|
+
removes;
|
|
24
28
|
constructor() {
|
|
25
29
|
this.database = new Database();
|
|
26
30
|
this.database.onBeforeInsert = (event) => {
|
|
@@ -36,7 +40,9 @@ export class EventStore {
|
|
|
36
40
|
this.database.removed.subscribe((event) => {
|
|
37
41
|
Reflect.deleteProperty(event, EventStoreSymbol);
|
|
38
42
|
});
|
|
43
|
+
this.inserts = this.database.inserted;
|
|
39
44
|
this.updates = this.database.updated;
|
|
45
|
+
this.removes = this.database.removed;
|
|
40
46
|
}
|
|
41
47
|
// delete state
|
|
42
48
|
deletedIds = new Set();
|
|
@@ -81,6 +87,10 @@ export class EventStore {
|
|
|
81
87
|
for (const relay of relays)
|
|
82
88
|
addSeenRelay(dest, relay);
|
|
83
89
|
}
|
|
90
|
+
// copy the from cache symbol only if its true
|
|
91
|
+
const fromCache = Reflect.get(source, FromCacheSymbol);
|
|
92
|
+
if (fromCache && !Reflect.get(dest, FromCacheSymbol))
|
|
93
|
+
Reflect.set(dest, FromCacheSymbol, fromCache);
|
|
84
94
|
}
|
|
85
95
|
/**
|
|
86
96
|
* Adds an event to the database and update subscriptions
|
|
@@ -92,6 +102,25 @@ export class EventStore {
|
|
|
92
102
|
// Ignore if the event was deleted
|
|
93
103
|
if (this.checkDeleted(event))
|
|
94
104
|
return event;
|
|
105
|
+
// Get the replaceable identifier
|
|
106
|
+
const d = isReplaceable(event.kind) ? getTagValue(event, "d") : undefined;
|
|
107
|
+
// Don't insert the event if there is already a newer version
|
|
108
|
+
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
109
|
+
const existing = this.database.getReplaceable(event.kind, event.pubkey, d);
|
|
110
|
+
// If there is already a newer version, copy cached symbols and return existing event
|
|
111
|
+
if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
|
|
112
|
+
EventStore.mergeDuplicateEvent(event, existing[0]);
|
|
113
|
+
return existing[0];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (this.database.hasEvent(event.id)) {
|
|
117
|
+
// Duplicate event, copy symbols and return existing event
|
|
118
|
+
const existing = this.database.getEvent(event.id);
|
|
119
|
+
if (existing) {
|
|
120
|
+
EventStore.mergeDuplicateEvent(event, existing);
|
|
121
|
+
return existing;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
95
124
|
// Insert event into database
|
|
96
125
|
const inserted = this.database.addEvent(event);
|
|
97
126
|
// Copy cached data if its a duplicate event
|
|
@@ -102,7 +131,7 @@ export class EventStore {
|
|
|
102
131
|
addSeenRelay(inserted, fromRelay);
|
|
103
132
|
// remove all old version of the replaceable event
|
|
104
133
|
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
105
|
-
const existing = this.database.getReplaceable(event.kind, event.pubkey,
|
|
134
|
+
const existing = this.database.getReplaceable(event.kind, event.pubkey, d);
|
|
106
135
|
if (existing) {
|
|
107
136
|
const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
|
|
108
137
|
for (const old of older)
|
|
@@ -165,14 +194,14 @@ export class EventStore {
|
|
|
165
194
|
// merge existing events
|
|
166
195
|
onlyNew ? EMPTY : from(this.getAll(filters)),
|
|
167
196
|
// subscribe to future events
|
|
168
|
-
this.
|
|
197
|
+
this.inserts.pipe(filter((e) => matchFilters(filters, e))));
|
|
169
198
|
}
|
|
170
199
|
/** Returns an observable that completes when an event is removed */
|
|
171
200
|
removed(id) {
|
|
172
201
|
const deleted = this.checkDeleted(id);
|
|
173
202
|
if (deleted)
|
|
174
203
|
return EMPTY;
|
|
175
|
-
return this.
|
|
204
|
+
return this.removes.pipe(
|
|
176
205
|
// listen for removed events
|
|
177
206
|
filter((e) => e.id === id),
|
|
178
207
|
// complete as soon as we find a matching removed event
|
|
@@ -193,7 +222,7 @@ export class EventStore {
|
|
|
193
222
|
return event ? of(event) : EMPTY;
|
|
194
223
|
}),
|
|
195
224
|
// subscribe to updates
|
|
196
|
-
this.
|
|
225
|
+
this.inserts.pipe(filter((e) => e.id === id)),
|
|
197
226
|
// subscribe to updates
|
|
198
227
|
this.updated(id),
|
|
199
228
|
// emit undefined when deleted
|
|
@@ -207,15 +236,15 @@ export class EventStore {
|
|
|
207
236
|
// lazily get existing events
|
|
208
237
|
defer(() => from(ids.map((id) => this.getEvent(id)))),
|
|
209
238
|
// subscribe to new events
|
|
210
|
-
this.
|
|
239
|
+
this.inserts.pipe(filter((e) => ids.includes(e.id))),
|
|
211
240
|
// subscribe to updates
|
|
212
|
-
this.
|
|
241
|
+
this.updates.pipe(filter((e) => ids.includes(e.id)))).pipe(
|
|
213
242
|
// ignore empty messages
|
|
214
243
|
filter((e) => !!e),
|
|
215
244
|
// claim all events until cleanup
|
|
216
245
|
claimEvents(this.database),
|
|
217
246
|
// watch for removed events
|
|
218
|
-
mergeWith(this.
|
|
247
|
+
mergeWith(this.removes.pipe(filter((e) => ids.includes(e.id)), map((e) => e.id))),
|
|
219
248
|
// merge all events into a directory
|
|
220
249
|
scan((dir, event) => {
|
|
221
250
|
if (typeof event === "string") {
|
|
@@ -240,13 +269,16 @@ export class EventStore {
|
|
|
240
269
|
return event ? of(event) : EMPTY;
|
|
241
270
|
}),
|
|
242
271
|
// subscribe to new events
|
|
243
|
-
this.
|
|
272
|
+
this.inserts.pipe(filter((e) => e.pubkey == pubkey && e.kind === kind && (d !== undefined ? getReplaceableIdentifier(e) === d : true)))).pipe(
|
|
244
273
|
// only update if event is newer
|
|
245
|
-
distinctUntilChanged((prev, event) =>
|
|
274
|
+
distinctUntilChanged((prev, event) => {
|
|
275
|
+
// are the events the same? i.e. is the prev event older
|
|
276
|
+
return prev.created_at >= event.created_at;
|
|
277
|
+
}),
|
|
246
278
|
// Hacky way to extract the current event so takeUntil can access it
|
|
247
279
|
tap((event) => (current = event)),
|
|
248
280
|
// complete when event is removed
|
|
249
|
-
takeUntil(this.
|
|
281
|
+
takeUntil(this.removes.pipe(filter((e) => e.id === current?.id))),
|
|
250
282
|
// emit undefined when removed
|
|
251
283
|
endWith(undefined),
|
|
252
284
|
// keep the observable hot
|
|
@@ -261,7 +293,7 @@ export class EventStore {
|
|
|
261
293
|
// start with existing events
|
|
262
294
|
defer(() => from(pointers.map((p) => this.getReplaceable(p.kind, p.pubkey, p.identifier)))),
|
|
263
295
|
// subscribe to new events
|
|
264
|
-
this.
|
|
296
|
+
this.inserts.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))))).pipe(
|
|
265
297
|
// filter out undefined
|
|
266
298
|
filter((e) => !!e),
|
|
267
299
|
// claim all events
|
|
@@ -269,7 +301,7 @@ export class EventStore {
|
|
|
269
301
|
// convert events to add commands
|
|
270
302
|
map((e) => ["add", e]),
|
|
271
303
|
// watch for removed events
|
|
272
|
-
mergeWith(this.
|
|
304
|
+
mergeWith(this.removes.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))), map((e) => ["remove", e]))),
|
|
273
305
|
// reduce events into directory
|
|
274
306
|
scan((dir, [action, event]) => {
|
|
275
307
|
const uid = getEventUID(event);
|
|
@@ -298,11 +330,11 @@ export class EventStore {
|
|
|
298
330
|
// claim existing events
|
|
299
331
|
claimEvents(this.database),
|
|
300
332
|
// subscribe to newer events
|
|
301
|
-
mergeWith(this.
|
|
333
|
+
mergeWith(this.inserts.pipe(filter((e) => matchFilters(filters, e)),
|
|
302
334
|
// claim all new events
|
|
303
335
|
claimEvents(this.database))),
|
|
304
336
|
// subscribe to delete events
|
|
305
|
-
mergeWith(this.
|
|
337
|
+
mergeWith(this.removes.pipe(filter((e) => matchFilters(filters, e)), map((e) => e.id))),
|
|
306
338
|
// build a timeline
|
|
307
339
|
scan((timeline, event) => {
|
|
308
340
|
// filter out removed events from timeline
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
export interface IEventStore {
|
|
4
|
+
inserts: Observable<NostrEvent>;
|
|
4
5
|
updates: Observable<NostrEvent>;
|
|
6
|
+
removes: Observable<NostrEvent>;
|
|
5
7
|
add(event: NostrEvent, fromRelay?: string): NostrEvent;
|
|
6
8
|
remove(event: string | NostrEvent): boolean;
|
|
7
9
|
update(event: NostrEvent): NostrEvent;
|