applesauce-core 0.0.0-next-20241122170522 → 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,24 +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;
11
+ protected deletedIds: Set<string>;
12
+ protected deletedCoords: Map<string, number>;
13
+ protected handleDeleteEvent(deleteEvent: NostrEvent): void;
14
+ protected checkDeleted(event: NostrEvent): boolean;
9
15
  /** Add an event to the store and notifies all subscribes it has updated */
10
- update(event: NostrEvent): import("nostr-tools").Event;
11
- getAll(filters: Filter[]): Set<import("nostr-tools").Event>;
12
- hasEvent(uid: string): import("nostr-tools").Event | undefined;
13
- 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;
14
20
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
15
- 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;
16
25
  /** Creates an observable that updates a single event */
17
- event(uid: string): Observable<import("nostr-tools").Event | undefined>;
26
+ event(id: string): Observable<NostrEvent | undefined>;
18
27
  /** Creates an observable that subscribes to multiple events */
19
- events(uids: string[]): Observable<Map<string, import("nostr-tools").Event>>;
20
- /** Creates an observable that updates a single replaceable event */
21
- 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>;
22
31
  /** Creates an observable that streams all events that match the filter */
23
- stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
32
+ stream(filters: Filter[]): Observable<NostrEvent>;
24
33
  /** Creates an observable that updates with an array of sorted events */
25
- timeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>;
34
+ timeline(filters: Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
26
35
  }
@@ -1,21 +1,75 @@
1
+ import { kinds } from "nostr-tools";
1
2
  import { insertEventIntoDescendingList } from "nostr-tools/utils";
3
+ import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
2
4
  import { Observable } from "rxjs";
3
5
  import { Database } from "./database.js";
4
- import { getEventUID, getReplaceableUID } from "../helpers/event.js";
6
+ import { getEventUID, getReplaceableUID, getTagValue, isReplaceable } from "../helpers/event.js";
5
7
  import { matchFilters } from "../helpers/filter.js";
6
8
  import { addSeenRelay } from "../helpers/relays.js";
9
+ import { getDeleteIds } from "../helpers/delete.js";
7
10
  export class EventStore {
8
11
  database;
12
+ /** Whether to keep old versions of replaceable events */
13
+ keepOldVersions = false;
9
14
  constructor() {
10
15
  this.database = new Database();
11
16
  }
12
17
  /** Adds an event to the database */
13
18
  add(event, fromRelay) {
19
+ if (event.kind === kinds.EventDeletion)
20
+ this.handleDeleteEvent(event);
21
+ // ignore if the event was deleted
22
+ if (this.checkDeleted(event))
23
+ return event;
24
+ // insert event into database
14
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
15
39
  if (fromRelay)
16
40
  addSeenRelay(inserted, fromRelay);
17
41
  return inserted;
18
42
  }
43
+ deletedIds = new Set();
44
+ deletedCoords = new Map();
45
+ handleDeleteEvent(deleteEvent) {
46
+ const ids = getDeleteIds(deleteEvent);
47
+ for (const id of ids) {
48
+ this.deletedIds.add(id);
49
+ // remove deleted events in the database
50
+ const event = this.database.getEvent(id);
51
+ if (event)
52
+ this.database.deleteEvent(event);
53
+ }
54
+ const coords = getDeleteIds(deleteEvent);
55
+ for (const coord of coords) {
56
+ this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at));
57
+ // remove deleted events in the database
58
+ const event = this.database.getEvent(coord);
59
+ if (event && event.created_at < deleteEvent.created_at)
60
+ this.database.deleteEvent(event);
61
+ }
62
+ }
63
+ checkDeleted(event) {
64
+ if (this.deletedIds.has(event.id))
65
+ return true;
66
+ if (isParameterizedReplaceableKind(event.kind)) {
67
+ const deleted = this.deletedCoords.get(getEventUID(event));
68
+ if (deleted)
69
+ return deleted > event.created_at;
70
+ }
71
+ return false;
72
+ }
19
73
  /** Add an event to the store and notifies all subscribes it has updated */
20
74
  update(event) {
21
75
  return this.database.updateEvent(event);
@@ -32,93 +86,87 @@ export class EventStore {
32
86
  hasReplaceable(kind, pubkey, d) {
33
87
  return this.database.hasReplaceable(kind, pubkey, d);
34
88
  }
89
+ /** Gets the latest version of a replaceable event */
35
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) {
36
95
  return this.database.getReplaceable(kind, pubkey, d);
37
96
  }
38
97
  /** Creates an observable that updates a single event */
39
- event(uid) {
98
+ event(id) {
40
99
  return new Observable((observer) => {
41
- let current = this.database.getEvent(uid);
100
+ let current = this.database.getEvent(id);
42
101
  if (current) {
43
102
  observer.next(current);
44
103
  this.database.claimEvent(current, observer);
45
104
  }
46
105
  // subscribe to future events
47
106
  const inserted = this.database.inserted.subscribe((event) => {
48
- if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
49
- // remove old claim
50
- if (current)
51
- this.database.removeClaim(current, observer);
107
+ if (event.id === id) {
52
108
  current = event;
53
109
  observer.next(event);
54
- // claim new event
55
- this.database.claimEvent(current, observer);
110
+ this.database.claimEvent(event, observer);
56
111
  }
57
112
  });
58
- // subscribe to updates
113
+ // subscribe to updated events
59
114
  const updated = this.database.updated.subscribe((event) => {
60
- if (event === current)
61
- observer.next(event);
115
+ if (event.id === id)
116
+ observer.next(current);
62
117
  });
63
118
  // subscribe to deleted events
64
119
  const deleted = this.database.deleted.subscribe((event) => {
65
- if (getEventUID(event) === uid && current) {
120
+ if (current?.id === event.id) {
66
121
  this.database.removeClaim(current, observer);
67
122
  current = undefined;
68
123
  observer.next(undefined);
69
124
  }
70
125
  });
71
126
  return () => {
72
- inserted.unsubscribe();
73
127
  deleted.unsubscribe();
74
128
  updated.unsubscribe();
129
+ inserted.unsubscribe();
75
130
  if (current)
76
131
  this.database.removeClaim(current, observer);
77
132
  };
78
133
  });
79
134
  }
80
135
  /** Creates an observable that subscribes to multiple events */
81
- events(uids) {
136
+ events(ids) {
82
137
  return new Observable((observer) => {
83
138
  const events = new Map();
84
- for (const uid of uids) {
85
- const e = this.getEvent(uid);
86
- if (e) {
87
- events.set(uid, e);
88
- 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);
89
144
  }
90
145
  }
91
146
  observer.next(events);
92
147
  // subscribe to future events
93
148
  const inserted = this.database.inserted.subscribe((event) => {
94
- const uid = getEventUID(event);
95
- if (uids.includes(uid)) {
96
- const current = events.get(uid);
97
- // remove old claim
98
- if (!current || event.created_at > current.created_at) {
99
- if (current)
100
- this.database.removeClaim(current, observer);
101
- events.set(uid, event);
102
- observer.next(events);
103
- // claim new event
104
- this.database.claimEvent(event, observer);
105
- }
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);
106
155
  }
107
156
  });
108
- // subscribe to updates
157
+ // subscribe to updated events
109
158
  const updated = this.database.updated.subscribe((event) => {
110
- const uid = getEventUID(event);
111
- if (uids.includes(uid))
159
+ if (ids.includes(event.id))
112
160
  observer.next(events);
113
161
  });
114
162
  // subscribe to deleted events
115
163
  const deleted = this.database.deleted.subscribe((event) => {
116
- const uid = getEventUID(event);
117
- if (uids.includes(uid)) {
118
- const current = events.get(uid);
164
+ const id = event.id;
165
+ if (ids.includes(id)) {
166
+ const current = events.get(id);
119
167
  if (current) {
120
168
  this.database.removeClaim(current, observer);
121
- events.delete(uid);
169
+ events.delete(id);
122
170
  observer.next(events);
123
171
  }
124
172
  }
@@ -133,9 +181,49 @@ export class EventStore {
133
181
  };
134
182
  });
135
183
  }
136
- /** Creates an observable that updates a single replaceable event */
184
+ /** Creates an observable with the latest version of a replaceable event */
137
185
  replaceable(kind, pubkey, d) {
138
- 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
+ });
139
227
  }
140
228
  /** Creates an observable that streams all events that match the filter */
141
229
  stream(filters) {
@@ -165,60 +253,58 @@ export class EventStore {
165
253
  });
166
254
  }
167
255
  /** Creates an observable that updates with an array of sorted events */
168
- timeline(filters) {
256
+ timeline(filters, keepOldVersions = this.keepOldVersions) {
169
257
  return new Observable((observer) => {
170
258
  const seen = new Map();
171
259
  const timeline = [];
172
- // build initial timeline
173
- const events = this.database.getForFilters(filters);
174
- 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
175
282
  insertEventIntoDescendingList(timeline, event);
176
283
  this.database.claimEvent(event, observer);
177
- seen.set(getEventUID(event), event);
178
- }
284
+ };
285
+ // build initial timeline
286
+ const events = this.database.getForFilters(filters);
287
+ for (const event of events)
288
+ insertIntoTimeline(event);
179
289
  observer.next([...timeline]);
180
290
  // subscribe to future events
181
291
  const inserted = this.database.inserted.subscribe((event) => {
182
292
  if (matchFilters(filters, event)) {
183
- const uid = getEventUID(event);
184
- let current = seen.get(uid);
185
- if (current) {
186
- if (event.created_at > current.created_at) {
187
- // replace event
188
- timeline.splice(timeline.indexOf(current), 1, event);
189
- observer.next([...timeline]);
190
- // update the claim
191
- seen.set(uid, event);
192
- this.database.removeClaim(current, observer);
193
- this.database.claimEvent(event, observer);
194
- }
195
- }
196
- else {
197
- insertEventIntoDescendingList(timeline, event);
198
- observer.next([...timeline]);
199
- // claim new event
200
- this.database.claimEvent(event, observer);
201
- seen.set(getEventUID(event), event);
202
- }
293
+ insertIntoTimeline(event);
294
+ observer.next([...timeline]);
203
295
  }
204
296
  });
205
- // subscribe to updates
297
+ // subscribe to updated events
206
298
  const updated = this.database.updated.subscribe((event) => {
207
- if (seen.has(getEventUID(event))) {
299
+ if (timeline.includes(event)) {
208
300
  observer.next([...timeline]);
209
301
  }
210
302
  });
211
- // subscribe to removed events
303
+ // subscribe to deleted events
212
304
  const deleted = this.database.deleted.subscribe((event) => {
213
- const uid = getEventUID(event);
214
- let current = seen.get(uid);
215
- if (current) {
216
- // remove the event
217
- timeline.splice(timeline.indexOf(current), 1);
305
+ if (timeline.includes(event)) {
306
+ removeFromTimeline(event);
218
307
  observer.next([...timeline]);
219
- // remove the claim
220
- seen.delete(uid);
221
- this.database.removeClaim(current, observer);
222
308
  }
223
309
  });
224
310
  return () => {
@@ -226,9 +312,10 @@ export class EventStore {
226
312
  deleted.unsubscribe();
227
313
  updated.unsubscribe();
228
314
  // remove all claims
229
- for (const [_, event] of seen) {
315
+ for (const event of timeline) {
230
316
  this.database.removeClaim(event, observer);
231
317
  }
318
+ // forget seen replaceable events
232
319
  seen.clear();
233
320
  };
234
321
  });
@@ -0,0 +1,5 @@
1
+ import { EventTemplate, NostrEvent } from "nostr-tools";
2
+ export declare function getDeleteIds(deleteEvent: NostrEvent): string[];
3
+ export declare function getDeleteCoordinates(deleteEvent: NostrEvent): string[];
4
+ /** Creates a NIP-09 delete event for an array of events */
5
+ export declare function createDeleteEvent(events: NostrEvent[], message?: string): EventTemplate;
@@ -0,0 +1,38 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
3
+ import { isATag, isETag } from "./tags.js";
4
+ import { unixNow } from "./time.js";
5
+ import { getATagFromAddressPointer, getETagFromEventPointer } from "./pointers.js";
6
+ import { getTagValue } from "./index.js";
7
+ export function getDeleteIds(deleteEvent) {
8
+ return deleteEvent.tags.filter(isETag).map((t) => t[1]);
9
+ }
10
+ export function getDeleteCoordinates(deleteEvent) {
11
+ return deleteEvent.tags.filter(isATag).map((t) => t[1]);
12
+ }
13
+ /** Creates a NIP-09 delete event for an array of events */
14
+ export function createDeleteEvent(events, message) {
15
+ const eventPointers = [];
16
+ const addressPointers = [];
17
+ const eventKinds = new Set();
18
+ for (const event of events) {
19
+ eventKinds.add(event.kind);
20
+ eventPointers.push({ id: event.id });
21
+ if (isParameterizedReplaceableKind(event.kind)) {
22
+ const identifier = getTagValue(event, "d");
23
+ if (!identifier)
24
+ throw new Error("Event missing identifier");
25
+ addressPointers.push({ pubkey: event.pubkey, kind: event.kind, identifier });
26
+ }
27
+ }
28
+ return {
29
+ kind: kinds.EventDeletion,
30
+ content: message ?? "",
31
+ tags: [
32
+ ...eventPointers.map(getETagFromEventPointer),
33
+ ...addressPointers.map(getATagFromAddressPointer),
34
+ ...Array.from(eventKinds).map((k) => ["k", String(k)]),
35
+ ],
36
+ created_at: unixNow(),
37
+ };
38
+ }
@@ -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
  }
@@ -16,3 +16,4 @@ export * from "./zap.js";
16
16
  export * from "./hidden-tags.js";
17
17
  export * from "./bolt11.js";
18
18
  export * from "./lnurl.js";
19
+ export * from "./delete.js";
@@ -16,3 +16,4 @@ export * from "./zap.js";
16
16
  export * from "./hidden-tags.js";
17
17
  export * from "./bolt11.js";
18
18
  export * from "./lnurl.js";
19
+ export * from "./delete.js";
@@ -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-20241122170522",
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",