applesauce-core 0.0.0-next-20241125223343 → 0.0.0-next-20241126184016

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,33 @@ 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>;
26
31
  /** Creates an observable that streams all events that match the filter */
27
- stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
32
+ stream(filters: Filter[]): Observable<NostrEvent>;
28
33
  /** Creates an observable that updates with an array of sorted events */
29
- timeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>;
34
+ timeline(filters: Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
30
35
  }
@@ -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,93 +86,87 @@ 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) {
77
99
  return new Observable((observer) => {
78
- let current = this.database.getEvent(uid);
100
+ let current = this.database.getEvent(id);
79
101
  if (current) {
80
102
  observer.next(current);
81
103
  this.database.claimEvent(current, observer);
82
104
  }
83
105
  // subscribe to future events
84
106
  const inserted = this.database.inserted.subscribe((event) => {
85
- if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
86
- // remove old claim
87
- if (current)
88
- this.database.removeClaim(current, observer);
107
+ if (event.id === id) {
89
108
  current = event;
90
109
  observer.next(event);
91
- // claim new event
92
- this.database.claimEvent(current, observer);
110
+ this.database.claimEvent(event, observer);
93
111
  }
94
112
  });
95
- // subscribe to updates
113
+ // subscribe to updated events
96
114
  const updated = this.database.updated.subscribe((event) => {
97
- if (event === current)
98
- observer.next(event);
115
+ if (event.id === id)
116
+ observer.next(current);
99
117
  });
100
118
  // subscribe to deleted events
101
119
  const deleted = this.database.deleted.subscribe((event) => {
102
- if (getEventUID(event) === uid && current) {
120
+ if (current?.id === event.id) {
103
121
  this.database.removeClaim(current, observer);
104
122
  current = undefined;
105
123
  observer.next(undefined);
106
124
  }
107
125
  });
108
126
  return () => {
109
- inserted.unsubscribe();
110
127
  deleted.unsubscribe();
111
128
  updated.unsubscribe();
129
+ inserted.unsubscribe();
112
130
  if (current)
113
131
  this.database.removeClaim(current, observer);
114
132
  };
115
133
  });
116
134
  }
117
135
  /** Creates an observable that subscribes to multiple events */
118
- events(uids) {
136
+ events(ids) {
119
137
  return new Observable((observer) => {
120
138
  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);
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);
126
144
  }
127
145
  }
128
146
  observer.next(events);
129
147
  // subscribe to future events
130
148
  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
- }
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);
143
155
  }
144
156
  });
145
- // subscribe to updates
157
+ // subscribe to updated events
146
158
  const updated = this.database.updated.subscribe((event) => {
147
- const uid = getEventUID(event);
148
- if (uids.includes(uid))
159
+ if (ids.includes(event.id))
149
160
  observer.next(events);
150
161
  });
151
162
  // subscribe to deleted events
152
163
  const deleted = this.database.deleted.subscribe((event) => {
153
- const uid = getEventUID(event);
154
- if (uids.includes(uid)) {
155
- const current = events.get(uid);
164
+ const id = event.id;
165
+ if (ids.includes(id)) {
166
+ const current = events.get(id);
156
167
  if (current) {
157
168
  this.database.removeClaim(current, observer);
158
- events.delete(uid);
169
+ events.delete(id);
159
170
  observer.next(events);
160
171
  }
161
172
  }
@@ -170,9 +181,49 @@ export class EventStore {
170
181
  };
171
182
  });
172
183
  }
173
- /** Creates an observable that updates a single replaceable event */
184
+ /** Creates an observable with the latest version of a replaceable event */
174
185
  replaceable(kind, pubkey, d) {
175
- return this.event(getReplaceableUID(kind, pubkey, d));
186
+ return new Observable((observer) => {
187
+ const uid = getReplaceableUID(kind, pubkey, d);
188
+ // get latest version
189
+ let current = this.database.getReplaceable(kind, pubkey, d)?.[0];
190
+ if (current) {
191
+ observer.next(current);
192
+ this.database.claimEvent(current, observer);
193
+ }
194
+ // subscribe to future events
195
+ const inserted = this.database.inserted.subscribe((event) => {
196
+ if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
197
+ // remove old claim
198
+ if (current)
199
+ this.database.removeClaim(current, observer);
200
+ current = event;
201
+ observer.next(event);
202
+ // claim new event
203
+ this.database.claimEvent(current, observer);
204
+ }
205
+ });
206
+ // subscribe to updated events
207
+ const updated = this.database.updated.subscribe((event) => {
208
+ if (event === current)
209
+ observer.next(event);
210
+ });
211
+ // subscribe to deleted events
212
+ const deleted = this.database.deleted.subscribe((event) => {
213
+ if (getEventUID(event) === uid && event === current) {
214
+ this.database.removeClaim(current, observer);
215
+ current = undefined;
216
+ observer.next(undefined);
217
+ }
218
+ });
219
+ return () => {
220
+ inserted.unsubscribe();
221
+ deleted.unsubscribe();
222
+ updated.unsubscribe();
223
+ if (current)
224
+ this.database.removeClaim(current, observer);
225
+ };
226
+ });
176
227
  }
177
228
  /** Creates an observable that streams all events that match the filter */
178
229
  stream(filters) {
@@ -202,60 +253,58 @@ export class EventStore {
202
253
  });
203
254
  }
204
255
  /** Creates an observable that updates with an array of sorted events */
205
- timeline(filters) {
256
+ timeline(filters, keepOldVersions = this.keepOldVersions) {
206
257
  return new Observable((observer) => {
207
258
  const seen = new Map();
208
259
  const timeline = [];
209
- // build initial timeline
210
- const events = this.database.getForFilters(filters);
211
- for (const event of events) {
260
+ // NOTE: only call this if we know the event is in timeline
261
+ const removeFromTimeline = (event) => {
262
+ timeline.splice(timeline.indexOf(event), 1);
263
+ if (!keepOldVersions && isReplaceable(event.kind))
264
+ seen.delete(getEventUID(event));
265
+ this.database.removeClaim(event, observer);
266
+ };
267
+ // inserts an event into the timeline and handles replaceable events
268
+ const insertIntoTimeline = (event) => {
269
+ // remove old versions
270
+ if (!keepOldVersions && isReplaceable(event.kind)) {
271
+ const uid = getEventUID(event);
272
+ const old = seen.get(uid);
273
+ if (old) {
274
+ if (event.created_at > old.created_at)
275
+ removeFromTimeline(old);
276
+ else
277
+ return;
278
+ }
279
+ seen.set(uid, event);
280
+ }
281
+ // insert into timeline
212
282
  insertEventIntoDescendingList(timeline, event);
213
283
  this.database.claimEvent(event, observer);
214
- seen.set(getEventUID(event), event);
215
- }
284
+ };
285
+ // build initial timeline
286
+ const events = this.database.getForFilters(filters);
287
+ for (const event of events)
288
+ insertIntoTimeline(event);
216
289
  observer.next([...timeline]);
217
290
  // subscribe to future events
218
291
  const inserted = this.database.inserted.subscribe((event) => {
219
292
  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
- }
293
+ insertIntoTimeline(event);
294
+ observer.next([...timeline]);
240
295
  }
241
296
  });
242
- // subscribe to updates
297
+ // subscribe to updated events
243
298
  const updated = this.database.updated.subscribe((event) => {
244
- if (seen.has(getEventUID(event))) {
299
+ if (timeline.includes(event)) {
245
300
  observer.next([...timeline]);
246
301
  }
247
302
  });
248
- // subscribe to removed events
303
+ // subscribe to deleted events
249
304
  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);
305
+ if (timeline.includes(event)) {
306
+ removeFromTimeline(event);
255
307
  observer.next([...timeline]);
256
- // remove the claim
257
- seen.delete(uid);
258
- this.database.removeClaim(current, observer);
259
308
  }
260
309
  });
261
310
  return () => {
@@ -263,9 +312,10 @@ export class EventStore {
263
312
  deleted.unsubscribe();
264
313
  updated.unsubscribe();
265
314
  // remove all claims
266
- for (const [_, event] of seen) {
315
+ for (const event of timeline) {
267
316
  this.database.removeClaim(event, observer);
268
317
  }
318
+ // forget seen replaceable events
269
319
  seen.clear();
270
320
  };
271
321
  });
@@ -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,17 @@ 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
33
  const cords = pointers.map((pointer) => getReplaceableUID(pointer.kind, pointer.pubkey, pointer.identifier));
34
34
  return {
35
- key: stringify(pointers),
35
+ key: hash_sum(pointers),
36
36
  run: (events) => events.events(cords),
37
37
  };
38
38
  }
@@ -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-20241126184016",
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",