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.
@@ -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 { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
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, getTagValue(event, "d"));
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.database.inserted.pipe(filter((e) => matchFilters(filters, e))));
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.database.removed.pipe(
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.database.inserted.pipe(filter((e) => e.id === id)),
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.database.inserted.pipe(filter((e) => ids.includes(e.id))),
239
+ this.inserts.pipe(filter((e) => ids.includes(e.id))),
211
240
  // subscribe to updates
212
- this.database.updated.pipe(filter((e) => ids.includes(e.id)))).pipe(
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.database.removed.pipe(filter((e) => ids.includes(e.id)), map((e) => e.id))),
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.database.inserted.pipe(filter((e) => e.pubkey == pubkey && e.kind === kind && (d !== undefined ? getReplaceableIdentifier(e) === d : true)))).pipe(
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) => prev.created_at >= event.created_at),
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.database.removed.pipe(filter((e) => e.id === current?.id))),
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.database.inserted.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))))).pipe(
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.database.removed.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))), map((e) => ["remove", e]))),
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.database.inserted.pipe(filter((e) => matchFilters(filters, e)),
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.database.removed.pipe(filter((e) => matchFilters(filters, e)), map((e) => e.id))),
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",