applesauce-core 0.0.0-next-20241125223343 → 0.0.0-next-20241126192236

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.
@@ -3,6 +3,7 @@ import { Subject } from "rxjs";
3
3
  import { LRU } from "../helpers/lru.js";
4
4
  /**
5
5
  * An in-memory database for nostr events
6
+ * NOTE: does not handle replaceable events
6
7
  */
7
8
  export declare class Database {
8
9
  protected log: import("debug").Debugger;
@@ -13,6 +14,7 @@ export declare class Database {
13
14
  protected created_at: NostrEvent[];
14
15
  /** LRU cache of last events touched */
15
16
  events: LRU<import("nostr-tools").Event>;
17
+ protected replaceable: Map<string, import("nostr-tools").Event[]>;
16
18
  /** A stream of events inserted into the database */
17
19
  inserted: Subject<import("nostr-tools").Event>;
18
20
  /** A stream of events that have been updated */
@@ -27,18 +29,20 @@ export declare class Database {
27
29
  protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
28
30
  /** Moves an event to the top of the LRU cache */
29
31
  touch(event: NostrEvent): void;
30
- hasEvent(uid: string): import("nostr-tools").Event | undefined;
31
- getEvent(uid: string): import("nostr-tools").Event | undefined;
32
+ /** Checks if the database contains an event without touching it */
33
+ hasEvent(id: string): boolean;
34
+ /** Gets a single event based on id */
35
+ getEvent(id: string): NostrEvent | undefined;
32
36
  /** Checks if the database contains a replaceable event without touching it */
33
37
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
34
- /** Gets a replaceable event and touches it */
35
- getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
38
+ /** Gets an array of replaceable events */
39
+ getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
36
40
  /** Inserts an event into the database and notifies all subscriptions */
37
- addEvent(event: NostrEvent): import("nostr-tools").Event;
41
+ addEvent(event: NostrEvent): NostrEvent;
38
42
  /** Inserts and event into the database and notifies all subscriptions that the event has updated */
39
- updateEvent(event: NostrEvent): import("nostr-tools").Event;
43
+ updateEvent(event: NostrEvent): NostrEvent;
40
44
  /** Deletes an event from the database and notifies all subscriptions */
41
- deleteEvent(eventOrUID: string | NostrEvent): boolean;
45
+ deleteEvent(eventOrId: string | NostrEvent): boolean;
42
46
  /** Sets the claim on the event and touches it */
43
47
  claimEvent(event: NostrEvent, claim: any): void;
44
48
  /** Checks if an event is claimed by anything */
@@ -47,14 +51,14 @@ export declare class Database {
47
51
  removeClaim(event: NostrEvent, claim: any): void;
48
52
  /** Removes all claims on an event */
49
53
  clearClaim(event: NostrEvent): void;
50
- iterateAuthors(authors: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
51
- iterateTag(tag: string, values: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
52
- iterateKinds(kinds: Iterable<number>): Generator<import("nostr-tools").Event, void, unknown>;
53
- iterateTime(since: number | undefined, until: number | undefined): Generator<never, Set<import("nostr-tools").Event>, unknown>;
54
- iterateIds(ids: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
54
+ iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
55
+ iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
56
+ iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
57
+ iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
58
+ iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
55
59
  /** Returns all events that match the filter */
56
60
  getEventsForFilter(filter: Filter): Set<NostrEvent>;
57
- getForFilters(filters: Filter[]): Set<import("nostr-tools").Event>;
61
+ getForFilters(filters: Filter[]): Set<NostrEvent>;
58
62
  /** Remove the oldest events that are not claimed */
59
63
  prune(limit?: number): number;
60
64
  }
@@ -1,11 +1,12 @@
1
1
  import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
2
2
  import { Subject } from "rxjs";
3
- import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID } from "../helpers/event.js";
3
+ import { FromCacheSymbol, 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";
7
7
  /**
8
8
  * An in-memory database for nostr events
9
+ * NOTE: does not handle replaceable events
9
10
  */
10
11
  export class Database {
11
12
  log = logger.extend("Database");
@@ -16,6 +17,7 @@ export class Database {
16
17
  created_at = [];
17
18
  /** LRU cache of last events touched */
18
19
  events = new LRU();
20
+ replaceable = new Map();
19
21
  /** A stream of events inserted into the database */
20
22
  inserted = new Subject();
21
23
  /** A stream of events that have been updated */
@@ -56,35 +58,36 @@ export class Database {
56
58
  }
57
59
  /** Moves an event to the top of the LRU cache */
58
60
  touch(event) {
59
- this.events.set(getEventUID(event), event);
61
+ this.events.set(event.id, event);
60
62
  }
61
- hasEvent(uid) {
62
- return this.events.get(uid);
63
+ /** Checks if the database contains an event without touching it */
64
+ hasEvent(id) {
65
+ return this.events.has(id);
63
66
  }
64
- getEvent(uid) {
65
- return this.events.get(uid);
67
+ /** Gets a single event based on id */
68
+ getEvent(id) {
69
+ return this.events.get(id);
66
70
  }
67
71
  /** Checks if the database contains a replaceable event without touching it */
68
72
  hasReplaceable(kind, pubkey, d) {
69
- return this.events.has(getReplaceableUID(kind, pubkey, d));
73
+ const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d));
74
+ return !!events && events.length > 0;
70
75
  }
71
- /** Gets a replaceable event and touches it */
76
+ /** Gets an array of replaceable events */
72
77
  getReplaceable(kind, pubkey, d) {
73
- return this.events.get(getReplaceableUID(kind, pubkey, d));
78
+ return this.replaceable.get(getReplaceableUID(kind, pubkey, d));
74
79
  }
75
80
  /** Inserts an event into the database and notifies all subscriptions */
76
81
  addEvent(event) {
77
- const uid = getEventUID(event);
78
- const current = this.events.get(uid);
79
- if (current && event.created_at <= current.created_at) {
82
+ const id = event.id;
83
+ const current = this.events.get(id);
84
+ if (current) {
80
85
  // if this is a duplicate event, transfer some import symbols
81
- if (current.id === event.id) {
82
- if (event[FromCacheSymbol])
83
- current[FromCacheSymbol] = event[FromCacheSymbol];
84
- }
86
+ if (event[FromCacheSymbol])
87
+ current[FromCacheSymbol] = event[FromCacheSymbol];
85
88
  return current;
86
89
  }
87
- this.events.set(uid, event);
90
+ this.events.set(id, event);
88
91
  this.getKindIndex(event.kind).add(event);
89
92
  this.getAuthorsIndex(event.pubkey).add(event);
90
93
  for (const tag of getIndexableTags(event)) {
@@ -92,7 +95,18 @@ export class Database {
92
95
  this.getTagIndex(tag).add(event);
93
96
  }
94
97
  }
98
+ // insert into time index
95
99
  insertEventIntoDescendingList(this.created_at, event);
100
+ // insert into replaceable index
101
+ if (isReplaceable(event.kind)) {
102
+ const uid = getEventUID(event);
103
+ let array = this.replaceable.get(uid);
104
+ if (!this.replaceable.has(uid)) {
105
+ array = [];
106
+ this.replaceable.set(uid, array);
107
+ }
108
+ insertEventIntoDescendingList(array, event);
109
+ }
96
110
  this.inserted.next(event);
97
111
  return event;
98
112
  }
@@ -103,13 +117,13 @@ export class Database {
103
117
  return inserted;
104
118
  }
105
119
  /** Deletes an event from the database and notifies all subscriptions */
106
- deleteEvent(eventOrUID) {
107
- let event = typeof eventOrUID === "string" ? this.events.get(eventOrUID) : eventOrUID;
120
+ deleteEvent(eventOrId) {
121
+ let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
108
122
  if (!event)
109
123
  throw new Error("Missing event");
110
- const uid = getEventUID(event);
124
+ const id = event.id;
111
125
  // only remove events that are known
112
- if (!this.events.has(uid))
126
+ if (!this.events.has(id))
113
127
  return false;
114
128
  this.getAuthorsIndex(event.pubkey).delete(event);
115
129
  this.getKindIndex(event.kind).delete(event);
@@ -121,7 +135,16 @@ export class Database {
121
135
  // remove from created_at index
122
136
  const i = this.created_at.indexOf(event);
123
137
  this.created_at.splice(i, 1);
124
- this.events.delete(uid);
138
+ this.events.delete(id);
139
+ // remove from replaceable index
140
+ if (isReplaceable(event.kind)) {
141
+ const uid = getEventUID(event);
142
+ const array = this.replaceable.get(uid);
143
+ if (array && array.includes(event)) {
144
+ const idx = array.indexOf(event);
145
+ array.splice(idx, 1);
146
+ }
147
+ }
125
148
  this.deleted.next(event);
126
149
  return true;
127
150
  }
@@ -3,28 +3,39 @@ import { Observable } from "rxjs";
3
3
  import { Database } from "./database.js";
4
4
  export declare class EventStore {
5
5
  database: Database;
6
+ /** Whether to keep old versions of replaceable events */
7
+ keepOldVersions: boolean;
6
8
  constructor();
7
9
  /** Adds an event to the database */
8
- add(event: NostrEvent, fromRelay?: string): import("nostr-tools").Event;
10
+ add(event: NostrEvent, fromRelay?: string): NostrEvent;
9
11
  protected deletedIds: Set<string>;
10
12
  protected deletedCoords: Map<string, number>;
11
13
  protected handleDeleteEvent(deleteEvent: NostrEvent): void;
12
14
  protected checkDeleted(event: NostrEvent): boolean;
13
15
  /** Add an event to the store and notifies all subscribes it has updated */
14
- update(event: NostrEvent): import("nostr-tools").Event;
15
- getAll(filters: Filter[]): Set<import("nostr-tools").Event>;
16
- hasEvent(uid: string): import("nostr-tools").Event | undefined;
17
- getEvent(uid: string): import("nostr-tools").Event | undefined;
16
+ update(event: NostrEvent): NostrEvent;
17
+ getAll(filters: Filter[]): Set<NostrEvent>;
18
+ hasEvent(uid: string): boolean;
19
+ getEvent(uid: string): NostrEvent | undefined;
18
20
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
19
- getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
21
+ /** Gets the latest version of a replaceable event */
22
+ getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined;
23
+ /** Returns all versions of a replaceable event */
24
+ getAllReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
20
25
  /** Creates an observable that updates a single event */
21
- event(uid: string): Observable<import("nostr-tools").Event | undefined>;
26
+ event(id: string): Observable<NostrEvent | undefined>;
22
27
  /** Creates an observable that subscribes to multiple events */
23
- events(uids: string[]): Observable<Map<string, import("nostr-tools").Event>>;
24
- /** Creates an observable that updates a single replaceable event */
25
- replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>;
28
+ events(ids: string[]): Observable<Map<string, NostrEvent>>;
29
+ /** Creates an observable with the latest version of a replaceable event */
30
+ replaceable(kind: number, pubkey: string, d?: string): Observable<NostrEvent | undefined>;
31
+ /** Creates an observable with the latest versions of replaceable events */
32
+ replaceableSet(pointers: {
33
+ kind: number;
34
+ pubkey: string;
35
+ identifier?: string;
36
+ }[]): Observable<Map<string, NostrEvent>>;
26
37
  /** Creates an observable that streams all events that match the filter */
27
- stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
38
+ stream(filters: Filter[]): Observable<NostrEvent>;
28
39
  /** Creates an observable that updates with an array of sorted events */
29
- timeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>;
40
+ timeline(filters: Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
30
41
  }
@@ -1,14 +1,16 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { insertEventIntoDescendingList } from "nostr-tools/utils";
3
+ import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
3
4
  import { Observable } from "rxjs";
4
5
  import { Database } from "./database.js";
5
- import { getEventUID, getReplaceableUID } from "../helpers/event.js";
6
+ import { getEventUID, getReplaceableUID, getTagValue, isReplaceable } from "../helpers/event.js";
6
7
  import { matchFilters } from "../helpers/filter.js";
7
8
  import { addSeenRelay } from "../helpers/relays.js";
8
9
  import { getDeleteIds } from "../helpers/delete.js";
9
- import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
10
10
  export class EventStore {
11
11
  database;
12
+ /** Whether to keep old versions of replaceable events */
13
+ keepOldVersions = false;
12
14
  constructor() {
13
15
  this.database = new Database();
14
16
  }
@@ -16,9 +18,24 @@ export class EventStore {
16
18
  add(event, fromRelay) {
17
19
  if (event.kind === kinds.EventDeletion)
18
20
  this.handleDeleteEvent(event);
21
+ // ignore if the event was deleted
19
22
  if (this.checkDeleted(event))
20
23
  return event;
24
+ // insert event into database
21
25
  const inserted = this.database.addEvent(event);
26
+ // remove all old version of the replaceable event
27
+ if (!this.keepOldVersions && isReplaceable(event.kind)) {
28
+ const current = this.database.getReplaceable(event.kind, event.pubkey, getTagValue(event, "d"));
29
+ if (current) {
30
+ const older = Array.from(current).filter((e) => e.created_at < event.created_at);
31
+ for (const old of older)
32
+ this.database.deleteEvent(old);
33
+ // skip inserting this event because its not the newest
34
+ if (current.length !== older.length)
35
+ return current[0];
36
+ }
37
+ }
38
+ // attach relay this event was from
22
39
  if (fromRelay)
23
40
  addSeenRelay(inserted, fromRelay);
24
41
  return inserted;
@@ -69,13 +86,107 @@ export class EventStore {
69
86
  hasReplaceable(kind, pubkey, d) {
70
87
  return this.database.hasReplaceable(kind, pubkey, d);
71
88
  }
89
+ /** Gets the latest version of a replaceable event */
72
90
  getReplaceable(kind, pubkey, d) {
91
+ return this.database.getReplaceable(kind, pubkey, d)?.[0];
92
+ }
93
+ /** Returns all versions of a replaceable event */
94
+ getAllReplaceable(kind, pubkey, d) {
73
95
  return this.database.getReplaceable(kind, pubkey, d);
74
96
  }
75
97
  /** Creates an observable that updates a single event */
76
- event(uid) {
98
+ event(id) {
99
+ return new Observable((observer) => {
100
+ let current = this.database.getEvent(id);
101
+ if (current) {
102
+ observer.next(current);
103
+ this.database.claimEvent(current, observer);
104
+ }
105
+ // subscribe to future events
106
+ const inserted = this.database.inserted.subscribe((event) => {
107
+ if (event.id === id) {
108
+ current = event;
109
+ observer.next(event);
110
+ this.database.claimEvent(event, observer);
111
+ }
112
+ });
113
+ // subscribe to updated events
114
+ const updated = this.database.updated.subscribe((event) => {
115
+ if (event.id === id)
116
+ observer.next(current);
117
+ });
118
+ // subscribe to deleted events
119
+ const deleted = this.database.deleted.subscribe((event) => {
120
+ if (current?.id === event.id) {
121
+ this.database.removeClaim(current, observer);
122
+ current = undefined;
123
+ observer.next(undefined);
124
+ }
125
+ });
126
+ return () => {
127
+ deleted.unsubscribe();
128
+ updated.unsubscribe();
129
+ inserted.unsubscribe();
130
+ if (current)
131
+ this.database.removeClaim(current, observer);
132
+ };
133
+ });
134
+ }
135
+ /** Creates an observable that subscribes to multiple events */
136
+ events(ids) {
137
+ return new Observable((observer) => {
138
+ const events = new Map();
139
+ for (const id of ids) {
140
+ const event = this.getEvent(id);
141
+ if (event) {
142
+ events.set(id, event);
143
+ this.database.claimEvent(event, observer);
144
+ }
145
+ }
146
+ observer.next(events);
147
+ // subscribe to future events
148
+ const inserted = this.database.inserted.subscribe((event) => {
149
+ const id = event.id;
150
+ if (ids.includes(id) && !events.has(id)) {
151
+ events.set(id, event);
152
+ observer.next(events);
153
+ // claim new event
154
+ this.database.claimEvent(event, observer);
155
+ }
156
+ });
157
+ // subscribe to updated events
158
+ const updated = this.database.updated.subscribe((event) => {
159
+ if (ids.includes(event.id))
160
+ observer.next(events);
161
+ });
162
+ // subscribe to deleted events
163
+ const deleted = this.database.deleted.subscribe((event) => {
164
+ const id = event.id;
165
+ if (ids.includes(id)) {
166
+ const current = events.get(id);
167
+ if (current) {
168
+ this.database.removeClaim(current, observer);
169
+ events.delete(id);
170
+ observer.next(events);
171
+ }
172
+ }
173
+ });
174
+ return () => {
175
+ inserted.unsubscribe();
176
+ deleted.unsubscribe();
177
+ updated.unsubscribe();
178
+ for (const [_uid, event] of events) {
179
+ this.database.removeClaim(event, observer);
180
+ }
181
+ };
182
+ });
183
+ }
184
+ /** Creates an observable with the latest version of a replaceable event */
185
+ replaceable(kind, pubkey, d) {
77
186
  return new Observable((observer) => {
78
- let current = this.database.getEvent(uid);
187
+ const uid = getReplaceableUID(kind, pubkey, d);
188
+ // get latest version
189
+ let current = this.database.getReplaceable(kind, pubkey, d)?.[0];
79
190
  if (current) {
80
191
  observer.next(current);
81
192
  this.database.claimEvent(current, observer);
@@ -92,14 +203,14 @@ export class EventStore {
92
203
  this.database.claimEvent(current, observer);
93
204
  }
94
205
  });
95
- // subscribe to updates
206
+ // subscribe to updated events
96
207
  const updated = this.database.updated.subscribe((event) => {
97
208
  if (event === current)
98
209
  observer.next(event);
99
210
  });
100
211
  // subscribe to deleted events
101
212
  const deleted = this.database.deleted.subscribe((event) => {
102
- if (getEventUID(event) === uid && current) {
213
+ if (getEventUID(event) === uid && event === current) {
103
214
  this.database.removeClaim(current, observer);
104
215
  current = undefined;
105
216
  observer.next(undefined);
@@ -114,66 +225,63 @@ export class EventStore {
114
225
  };
115
226
  });
116
227
  }
117
- /** Creates an observable that subscribes to multiple events */
118
- events(uids) {
228
+ /** Creates an observable with the latest versions of replaceable events */
229
+ replaceableSet(pointers) {
119
230
  return new Observable((observer) => {
231
+ const coords = pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier));
120
232
  const events = new Map();
121
- for (const uid of uids) {
122
- const e = this.getEvent(uid);
123
- if (e) {
124
- events.set(uid, e);
125
- this.database.claimEvent(e, observer);
233
+ const handleEvent = (event) => {
234
+ const uid = getEventUID(event);
235
+ const current = events.get(uid);
236
+ if (current) {
237
+ if (event.created_at > current.created_at) {
238
+ this.database.removeClaim(current, observer);
239
+ }
240
+ else
241
+ return;
126
242
  }
243
+ events.set(uid, event);
244
+ this.database.claimEvent(event, observer);
245
+ };
246
+ // get latest version
247
+ for (const pointer of pointers) {
248
+ const events = this.database.getReplaceable(pointer.kind, pointer.pubkey, pointer.identifier);
249
+ if (events)
250
+ for (const event of events)
251
+ handleEvent(event);
127
252
  }
128
- observer.next(events);
129
253
  // subscribe to future events
130
254
  const inserted = this.database.inserted.subscribe((event) => {
131
- const uid = getEventUID(event);
132
- if (uids.includes(uid)) {
133
- const current = events.get(uid);
134
- // remove old claim
135
- if (!current || event.created_at > current.created_at) {
136
- if (current)
137
- this.database.removeClaim(current, observer);
138
- events.set(uid, event);
139
- observer.next(events);
140
- // claim new event
141
- this.database.claimEvent(event, observer);
142
- }
255
+ if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) {
256
+ handleEvent(event);
257
+ observer.next(events);
143
258
  }
144
259
  });
145
- // subscribe to updates
260
+ // subscribe to updated events
146
261
  const updated = this.database.updated.subscribe((event) => {
147
- const uid = getEventUID(event);
148
- if (uids.includes(uid))
262
+ if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) {
149
263
  observer.next(events);
264
+ }
150
265
  });
151
266
  // subscribe to deleted events
152
267
  const deleted = this.database.deleted.subscribe((event) => {
153
268
  const uid = getEventUID(event);
154
- if (uids.includes(uid)) {
155
- const current = events.get(uid);
156
- if (current) {
157
- this.database.removeClaim(current, observer);
158
- events.delete(uid);
159
- observer.next(events);
160
- }
269
+ if (events.has(uid)) {
270
+ events.delete(uid);
271
+ this.database.removeClaim(event, observer);
272
+ observer.next(events);
161
273
  }
162
274
  });
163
275
  return () => {
164
276
  inserted.unsubscribe();
165
277
  deleted.unsubscribe();
166
278
  updated.unsubscribe();
167
- for (const [_uid, event] of events) {
279
+ for (const [_id, event] of events) {
168
280
  this.database.removeClaim(event, observer);
169
281
  }
170
282
  };
171
283
  });
172
284
  }
173
- /** Creates an observable that updates a single replaceable event */
174
- replaceable(kind, pubkey, d) {
175
- return this.event(getReplaceableUID(kind, pubkey, d));
176
- }
177
285
  /** Creates an observable that streams all events that match the filter */
178
286
  stream(filters) {
179
287
  return new Observable((observer) => {
@@ -202,60 +310,58 @@ export class EventStore {
202
310
  });
203
311
  }
204
312
  /** Creates an observable that updates with an array of sorted events */
205
- timeline(filters) {
313
+ timeline(filters, keepOldVersions = this.keepOldVersions) {
206
314
  return new Observable((observer) => {
207
315
  const seen = new Map();
208
316
  const timeline = [];
209
- // build initial timeline
210
- const events = this.database.getForFilters(filters);
211
- for (const event of events) {
317
+ // NOTE: only call this if we know the event is in timeline
318
+ const removeFromTimeline = (event) => {
319
+ timeline.splice(timeline.indexOf(event), 1);
320
+ if (!keepOldVersions && isReplaceable(event.kind))
321
+ seen.delete(getEventUID(event));
322
+ this.database.removeClaim(event, observer);
323
+ };
324
+ // inserts an event into the timeline and handles replaceable events
325
+ const insertIntoTimeline = (event) => {
326
+ // remove old versions
327
+ if (!keepOldVersions && isReplaceable(event.kind)) {
328
+ const uid = getEventUID(event);
329
+ const old = seen.get(uid);
330
+ if (old) {
331
+ if (event.created_at > old.created_at)
332
+ removeFromTimeline(old);
333
+ else
334
+ return;
335
+ }
336
+ seen.set(uid, event);
337
+ }
338
+ // insert into timeline
212
339
  insertEventIntoDescendingList(timeline, event);
213
340
  this.database.claimEvent(event, observer);
214
- seen.set(getEventUID(event), event);
215
- }
341
+ };
342
+ // build initial timeline
343
+ const events = this.database.getForFilters(filters);
344
+ for (const event of events)
345
+ insertIntoTimeline(event);
216
346
  observer.next([...timeline]);
217
347
  // subscribe to future events
218
348
  const inserted = this.database.inserted.subscribe((event) => {
219
349
  if (matchFilters(filters, event)) {
220
- const uid = getEventUID(event);
221
- let current = seen.get(uid);
222
- if (current) {
223
- if (event.created_at > current.created_at) {
224
- // replace event
225
- timeline.splice(timeline.indexOf(current), 1, event);
226
- observer.next([...timeline]);
227
- // update the claim
228
- seen.set(uid, event);
229
- this.database.removeClaim(current, observer);
230
- this.database.claimEvent(event, observer);
231
- }
232
- }
233
- else {
234
- insertEventIntoDescendingList(timeline, event);
235
- observer.next([...timeline]);
236
- // claim new event
237
- this.database.claimEvent(event, observer);
238
- seen.set(getEventUID(event), event);
239
- }
350
+ insertIntoTimeline(event);
351
+ observer.next([...timeline]);
240
352
  }
241
353
  });
242
- // subscribe to updates
354
+ // subscribe to updated events
243
355
  const updated = this.database.updated.subscribe((event) => {
244
- if (seen.has(getEventUID(event))) {
356
+ if (timeline.includes(event)) {
245
357
  observer.next([...timeline]);
246
358
  }
247
359
  });
248
- // subscribe to removed events
360
+ // subscribe to deleted events
249
361
  const deleted = this.database.deleted.subscribe((event) => {
250
- const uid = getEventUID(event);
251
- let current = seen.get(uid);
252
- if (current) {
253
- // remove the event
254
- timeline.splice(timeline.indexOf(current), 1);
362
+ if (timeline.includes(event)) {
363
+ removeFromTimeline(event);
255
364
  observer.next([...timeline]);
256
- // remove the claim
257
- seen.delete(uid);
258
- this.database.removeClaim(current, observer);
259
365
  }
260
366
  });
261
367
  return () => {
@@ -263,9 +369,10 @@ export class EventStore {
263
369
  deleted.unsubscribe();
264
370
  updated.unsubscribe();
265
371
  // remove all claims
266
- for (const [_, event] of seen) {
372
+ for (const event of timeline) {
267
373
  this.database.removeClaim(event, observer);
268
374
  }
375
+ // forget seen replaceable events
269
376
  seen.clear();
270
377
  };
271
378
  });
@@ -6,7 +6,5 @@ import { Filter, NostrEvent } from "nostr-tools";
6
6
  export declare function matchFilter(filter: Filter, event: NostrEvent): boolean;
7
7
  /** Copied from nostr-tools */
8
8
  export declare function matchFilters(filters: Filter[], event: NostrEvent): boolean;
9
- /** Stringify filters in a predictable way */
10
- export declare function stringifyFilter(filter: Filter | Filter[]): string;
11
9
  /** Check if two filters are equal */
12
10
  export declare function isFilterEqual(a: Filter | Filter[], b: Filter | Filter[]): boolean;
@@ -1,5 +1,5 @@
1
1
  import { getIndexableTags } from "./event.js";
2
- import stringify from "json-stringify-deterministic";
2
+ import equal from "fast-deep-equal";
3
3
  /**
4
4
  * Copied from nostr-tools and modified to use getIndexableTags
5
5
  * @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
@@ -40,11 +40,7 @@ export function matchFilters(filters, event) {
40
40
  }
41
41
  return false;
42
42
  }
43
- /** Stringify filters in a predictable way */
44
- export function stringifyFilter(filter) {
45
- return stringify(filter);
46
- }
47
43
  /** Check if two filters are equal */
48
44
  export function isFilterEqual(a, b) {
49
- return stringifyFilter(a) === stringifyFilter(b);
45
+ return equal(a, b);
50
46
  }
@@ -0,0 +1,2 @@
1
+ import { Observable } from "rxjs";
2
+ export declare function getValue<T>(observable: Observable<T>): T | Promise<T>;
@@ -0,0 +1,13 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ export function getValue(observable) {
3
+ if (observable instanceof BehaviorSubject)
4
+ return observable.value;
5
+ if (Reflect.has(observable, "value"))
6
+ return Reflect.get(observable, "value");
7
+ return new Promise((res) => {
8
+ const sub = observable.subscribe((v) => {
9
+ res(v);
10
+ sub.unsubscribe();
11
+ });
12
+ });
13
+ }
@@ -1,2 +1,2 @@
1
- export * from "./getValue.js";
1
+ export * from "./get-value.js";
2
2
  export * from "./share-latest-value.js";
@@ -1,2 +1,2 @@
1
- export * from "./getValue.js";
1
+ export * from "./get-value.js";
2
2
  export * from "./share-latest-value.js";
@@ -1,13 +1,13 @@
1
1
  import { Filter, NostrEvent } from "nostr-tools";
2
2
  import { Query } from "../query-store/index.js";
3
3
  /** Creates a Query that returns a single event or undefined */
4
- export declare function SingleEventQuery(uid: string): Query<NostrEvent | undefined>;
4
+ export declare function SingleEventQuery(id: string): Query<NostrEvent | undefined>;
5
5
  /** Creates a Query that returns a multiple events in a map */
6
- export declare function MultipleEventsQuery(uids: string[]): Query<Map<string, NostrEvent>>;
6
+ export declare function MultipleEventsQuery(ids: string[]): Query<Map<string, NostrEvent>>;
7
7
  /** Creates a Query returning the latest version of a replaceable event */
8
8
  export declare function ReplaceableQuery(kind: number, pubkey: string, d?: string): Query<NostrEvent | undefined>;
9
9
  /** Creates a Query that returns an array of sorted events matching the filters */
10
- export declare function TimelineQuery(filters: Filter | Filter[]): Query<NostrEvent[]>;
10
+ export declare function TimelineQuery(filters: Filter | Filter[], keepOldVersions?: boolean): Query<NostrEvent[]>;
11
11
  /** Creates a Query that returns a directory of events by their UID */
12
12
  export declare function ReplaceableSetQuery(pointers: {
13
13
  kind: number;
@@ -1,17 +1,17 @@
1
- import stringify from "json-stringify-deterministic";
2
1
  import { getReplaceableUID } from "../helpers/event.js";
2
+ import hash_sum from "hash-sum";
3
3
  /** Creates a Query that returns a single event or undefined */
4
- export function SingleEventQuery(uid) {
4
+ export function SingleEventQuery(id) {
5
5
  return {
6
- key: uid,
7
- run: (events) => events.event(uid),
6
+ key: id,
7
+ run: (events) => events.event(id),
8
8
  };
9
9
  }
10
10
  /** Creates a Query that returns a multiple events in a map */
11
- export function MultipleEventsQuery(uids) {
11
+ export function MultipleEventsQuery(ids) {
12
12
  return {
13
- key: uids.join(","),
14
- run: (events) => events.events(uids),
13
+ key: ids.join(","),
14
+ run: (events) => events.events(ids),
15
15
  };
16
16
  }
17
17
  /** Creates a Query returning the latest version of a replaceable event */
@@ -22,17 +22,16 @@ export function ReplaceableQuery(kind, pubkey, d) {
22
22
  };
23
23
  }
24
24
  /** Creates a Query that returns an array of sorted events matching the filters */
25
- export function TimelineQuery(filters) {
25
+ export function TimelineQuery(filters, keepOldVersions) {
26
26
  return {
27
- key: stringify(filters),
28
- run: (events) => events.timeline(Array.isArray(filters) ? filters : [filters]),
27
+ key: hash_sum(filters) + (keepOldVersions ? "-history" : ""),
28
+ run: (events) => events.timeline(Array.isArray(filters) ? filters : [filters], keepOldVersions),
29
29
  };
30
30
  }
31
31
  /** Creates a Query that returns a directory of events by their UID */
32
32
  export function ReplaceableSetQuery(pointers) {
33
- const cords = pointers.map((pointer) => getReplaceableUID(pointer.kind, pointer.pubkey, pointer.identifier));
34
33
  return {
35
- key: stringify(pointers),
36
- run: (events) => events.events(cords),
34
+ key: hash_sum(pointers),
35
+ run: (events) => events.replaceableSet(pointers),
37
36
  };
38
37
  }
@@ -32,7 +32,7 @@ export declare class QueryStore {
32
32
  identifier?: string;
33
33
  }[]): Observable<Map<string, import("nostr-tools").Event>>;
34
34
  /** Returns an array of events that match the filter */
35
- timeline(filters: Filter | Filter[]): Observable<import("nostr-tools").Event[]>;
35
+ timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<import("nostr-tools").Event[]>;
36
36
  /** Returns the parsed profile (0) for a pubkey */
37
37
  profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
38
38
  /** Returns all reactions for an event (supports replaceable events) */
@@ -38,8 +38,8 @@ export class QueryStore {
38
38
  return this.runQuery(Queries.ReplaceableSetQuery)(pointers);
39
39
  }
40
40
  /** Returns an array of events that match the filter */
41
- timeline(filters) {
42
- return this.runQuery(Queries.TimelineQuery)(filters);
41
+ timeline(filters, keepOldVersions) {
42
+ return this.runQuery(Queries.TimelineQuery)(filters, keepOldVersions);
43
43
  }
44
44
  /** Returns the parsed profile (0) for a pubkey */
45
45
  profile(pubkey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20241125223343",
3
+ "version": "0.0.0-next-20241126192236",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,7 +54,8 @@
54
54
  "dependencies": {
55
55
  "@scure/base": "^1.1.9",
56
56
  "debug": "^4.3.7",
57
- "json-stringify-deterministic": "^1.0.12",
57
+ "fast-deep-equal": "^3.1.3",
58
+ "hash-sum": "^2.0.0",
58
59
  "light-bolt11-decoder": "^3.2.0",
59
60
  "nanoid": "^5.0.7",
60
61
  "nostr-tools": "^2.10.3",
@@ -63,6 +64,7 @@
63
64
  "devDependencies": {
64
65
  "@jest/globals": "^29.7.0",
65
66
  "@types/debug": "^4.1.12",
67
+ "@types/hash-sum": "^1.0.2",
66
68
  "@types/jest": "^29.5.13",
67
69
  "jest": "^29.7.0",
68
70
  "jest-extended": "^4.0.2",