applesauce-core 0.0.0-next-20251205152544 → 0.0.0-next-20251209200210

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.
Files changed (37) hide show
  1. package/dist/event-store/async-delete-manager.d.ts +26 -0
  2. package/dist/event-store/async-delete-manager.js +33 -0
  3. package/dist/event-store/async-event-store.d.ts +29 -19
  4. package/dist/event-store/async-event-store.js +83 -116
  5. package/dist/event-store/delete-manager.d.ts +30 -0
  6. package/dist/event-store/delete-manager.js +99 -0
  7. package/dist/event-store/event-store.d.ts +29 -19
  8. package/dist/event-store/event-store.js +82 -119
  9. package/dist/event-store/expiration-manager.d.ts +37 -0
  10. package/dist/event-store/expiration-manager.js +97 -0
  11. package/dist/event-store/index.d.ts +3 -0
  12. package/dist/event-store/index.js +3 -0
  13. package/dist/event-store/interface.d.ts +44 -0
  14. package/dist/helpers/contacts.js +2 -2
  15. package/dist/helpers/delete.d.ts +8 -1
  16. package/dist/helpers/delete.js +25 -1
  17. package/dist/helpers/event.d.ts +2 -2
  18. package/dist/helpers/event.js +5 -5
  19. package/dist/helpers/factory.js +4 -4
  20. package/dist/helpers/index.d.ts +1 -1
  21. package/dist/helpers/index.js +1 -1
  22. package/dist/helpers/model.d.ts +5 -0
  23. package/dist/helpers/model.js +17 -0
  24. package/dist/helpers/pointers.d.ts +17 -36
  25. package/dist/helpers/pointers.js +69 -84
  26. package/dist/helpers/string.js +8 -2
  27. package/dist/models/index.d.ts +0 -1
  28. package/dist/models/index.js +0 -1
  29. package/dist/operations/client.js +2 -2
  30. package/dist/operations/delete.js +5 -3
  31. package/dist/operations/tag/common.d.ts +4 -3
  32. package/dist/operations/tag/common.js +23 -10
  33. package/package.json +6 -1
  34. package/dist/helpers/lists.d.ts +0 -58
  35. package/dist/helpers/lists.js +0 -110
  36. package/dist/models/relays.d.ts +0 -27
  37. package/dist/models/relays.js +0 -36
@@ -0,0 +1,26 @@
1
+ import { Observable } from "rxjs";
2
+ import { NostrEvent } from "../helpers/event.js";
3
+ import { DeleteEventNotification, IAsyncDeleteManager } from "./interface.js";
4
+ /** Async manager for deletion state, ensuring users can only delete their own events */
5
+ export declare class AsyncDeleteManager implements IAsyncDeleteManager {
6
+ /** A stream of pointers that may have been deleted */
7
+ readonly deleted$: Observable<DeleteEventNotification>;
8
+ /** Internal sync delete manager instance for state */
9
+ private internal;
10
+ constructor();
11
+ /**
12
+ * Process a kind 5 delete event
13
+ * Extracts event pointers and address pointers from the delete event
14
+ * Enforces that users can only delete their own events
15
+ */
16
+ add(deleteEvent: NostrEvent): Promise<DeleteEventNotification[]>;
17
+ /**
18
+ * Check if an event is deleted
19
+ * Verifies the event was deleted by its own author
20
+ */
21
+ check(event: NostrEvent): Promise<boolean>;
22
+ /**
23
+ * Filter out all deleted events from an array of events
24
+ */
25
+ filter(events: NostrEvent[]): Promise<NostrEvent[]>;
26
+ }
@@ -0,0 +1,33 @@
1
+ import { DeleteManager } from "./delete-manager.js";
2
+ /** Async manager for deletion state, ensuring users can only delete their own events */
3
+ export class AsyncDeleteManager {
4
+ /** A stream of pointers that may have been deleted */
5
+ deleted$;
6
+ /** Internal sync delete manager instance for state */
7
+ internal;
8
+ constructor() {
9
+ this.internal = new DeleteManager();
10
+ this.deleted$ = this.internal.deleted$;
11
+ }
12
+ /**
13
+ * Process a kind 5 delete event
14
+ * Extracts event pointers and address pointers from the delete event
15
+ * Enforces that users can only delete their own events
16
+ */
17
+ async add(deleteEvent) {
18
+ return this.internal.add(deleteEvent);
19
+ }
20
+ /**
21
+ * Check if an event is deleted
22
+ * Verifies the event was deleted by its own author
23
+ */
24
+ async check(event) {
25
+ return this.internal.check(event);
26
+ }
27
+ /**
28
+ * Filter out all deleted events from an array of events
29
+ */
30
+ async filter(events) {
31
+ return this.internal.filter(events);
32
+ }
33
+ }
@@ -4,16 +4,38 @@ import { Filter } from "../helpers/filter.js";
4
4
  import { AddressPointer, AddressPointerWithoutD, EventPointer } from "../helpers/pointers.js";
5
5
  import { EventMemory } from "./event-memory.js";
6
6
  import { EventModels } from "./event-models.js";
7
- import { IAsyncEventDatabase, IAsyncEventStore } from "./interface.js";
7
+ import { IAsyncEventDatabase, IAsyncEventStore, IAsyncDeleteManager, IExpirationManager } from "./interface.js";
8
+ export type AsyncEventStoreOptions = {
9
+ /** Keep deleted events in the store */
10
+ keepDeleted?: boolean;
11
+ /** Keep expired events in the store */
12
+ keepExpired?: boolean;
13
+ /** Enable this to keep old versions of replaceable events */
14
+ keepOldVersions?: boolean;
15
+ /** The database to use for storing events */
16
+ database: IAsyncEventDatabase;
17
+ /** Custom {@link IAsyncDeleteManager} implementation */
18
+ deleteManager?: IAsyncDeleteManager;
19
+ /** Custom {@link IExpirationManager} implementation */
20
+ expirationManager?: IExpirationManager;
21
+ /** The method used to verify events */
22
+ verifyEvent?: (event: NostrEvent) => boolean;
23
+ };
8
24
  /** An async wrapper around an async event database that handles replaceable events, deletes, and models */
9
25
  export declare class AsyncEventStore extends EventModels implements IAsyncEventStore {
10
26
  database: IAsyncEventDatabase;
11
27
  /** Optional memory database for ensuring single event instances */
12
28
  memory: EventMemory;
29
+ /** Manager for handling event deletions with authorization */
30
+ private deletes;
31
+ /** Manager for handling event expirations */
32
+ private expiration;
13
33
  /** Enable this to keep old versions of replaceable events */
14
34
  keepOldVersions: boolean;
15
- /** Enable this to keep expired events */
35
+ /** Keep expired events in the store */
16
36
  keepExpired: boolean;
37
+ /** Keep deleted events in the store */
38
+ keepDeleted: boolean;
17
39
  /** The method used to verify events */
18
40
  private _verifyEventMethod?;
19
41
  /** Get the method used to verify events */
@@ -30,21 +52,13 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
30
52
  * A method that will be called when an event isn't found in the store
31
53
  */
32
54
  eventLoader?: (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
33
- constructor(database: IAsyncEventDatabase);
55
+ constructor(options: AsyncEventStoreOptions);
34
56
  /** A method to add all events to memory to ensure there is only ever a single instance of an event */
35
57
  private mapToMemory;
36
- protected deletedIds: Set<string>;
37
- protected deletedCoords: Map<string, number>;
38
- protected checkDeleted(event: string | NostrEvent): boolean;
39
- protected expirations: Map<string, number>;
40
- /** Adds an event to the expiration map */
41
- protected addExpiration(event: NostrEvent): void;
42
- protected expirationTimeout: number | null;
43
- protected nextExpirationCheck: number | null;
44
- protected handleExpiringEvent(event: NostrEvent): void;
45
- /** Remove expired events from the store */
46
- protected pruneExpired(): Promise<void>;
47
- protected handleDeleteEvent(deleteEvent: NostrEvent): Promise<void>;
58
+ /** Handle a delete event by pointer */
59
+ private handleDeleteNotification;
60
+ /** Handle an expired event by id */
61
+ private handleExpiredNotification;
48
62
  /** Copies important metadata from and identical event to another */
49
63
  static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
50
64
  /**
@@ -86,8 +100,4 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
86
100
  unclaimed(): Generator<NostrEvent>;
87
101
  /** Removes any event that is not being used by a subscription */
88
102
  prune(limit?: number): number;
89
- /** Returns an observable that completes when an event is removed */
90
- removed(id: string): Observable<never>;
91
- /** Creates an observable that emits when event is updated */
92
- updated(event: string | NostrEvent): Observable<NostrEvent>;
93
103
  }
@@ -1,22 +1,29 @@
1
- import { EMPTY, filter, mergeMap, Subject, take } from "rxjs";
2
- import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
3
- import { createReplaceableAddress, EventStoreSymbol, FromCacheSymbol, isAddressableKind, isReplaceable, kinds, } from "../helpers/event.js";
1
+ import { Subject } from "rxjs";
2
+ import { EventStoreSymbol, FromCacheSymbol, getReplaceableIdentifier, isReplaceable, kinds, } from "../helpers/event.js";
4
3
  import { getExpirationTimestamp } from "../helpers/expiration.js";
5
- import { parseCoordinate } from "../helpers/pointers.js";
4
+ import { eventMatchesPointer, isEventPointer, isAddressPointer, } from "../helpers/pointers.js";
6
5
  import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
7
6
  import { unixNow } from "../helpers/time.js";
8
7
  import { EventMemory } from "./event-memory.js";
9
8
  import { EventModels } from "./event-models.js";
10
9
  import { verifyEvent as coreVerifyEvent } from "nostr-tools/pure";
10
+ import { AsyncDeleteManager } from "./async-delete-manager.js";
11
+ import { ExpirationManager } from "./expiration-manager.js";
11
12
  /** An async wrapper around an async event database that handles replaceable events, deletes, and models */
12
13
  export class AsyncEventStore extends EventModels {
13
14
  database;
14
15
  /** Optional memory database for ensuring single event instances */
15
16
  memory;
17
+ /** Manager for handling event deletions with authorization */
18
+ deletes;
19
+ /** Manager for handling event expirations */
20
+ expiration;
16
21
  /** Enable this to keep old versions of replaceable events */
17
22
  keepOldVersions = false;
18
- /** Enable this to keep expired events */
23
+ /** Keep expired events in the store */
19
24
  keepExpired = false;
25
+ /** Keep deleted events in the store */
26
+ keepDeleted = false;
20
27
  /** The method used to verify events */
21
28
  _verifyEventMethod = coreVerifyEvent;
22
29
  /** Get the method used to verify events */
@@ -26,9 +33,8 @@ export class AsyncEventStore extends EventModels {
26
33
  /** Sets the method used to verify events */
27
34
  set verifyEvent(method) {
28
35
  this._verifyEventMethod = method;
29
- if (method === undefined) {
36
+ if (method === undefined)
30
37
  console.warn("[applesauce-core] AsyncEventStore.verifyEvent is undefined; signature checks are disabled.");
31
- }
32
38
  }
33
39
  /** A stream of new events added to the store */
34
40
  insert$ = new Subject();
@@ -40,10 +46,35 @@ export class AsyncEventStore extends EventModels {
40
46
  * A method that will be called when an event isn't found in the store
41
47
  */
42
48
  eventLoader;
43
- constructor(database) {
49
+ constructor(options) {
44
50
  super();
45
- this.database = database;
51
+ this.database = options.database;
46
52
  this.memory = new EventMemory();
53
+ // Set options if provided
54
+ if (options.keepDeleted !== undefined)
55
+ this.keepDeleted = options.keepDeleted;
56
+ if (options.keepExpired !== undefined)
57
+ this.keepExpired = options.keepExpired;
58
+ if (options.keepOldVersions !== undefined)
59
+ this.keepOldVersions = options.keepOldVersions;
60
+ if (options.verifyEvent)
61
+ this.verifyEvent = options.verifyEvent;
62
+ // Use provided delete manager or create a default one
63
+ this.deletes = options.deleteManager ?? new AsyncDeleteManager();
64
+ // Listen to delete notifications and remove matching events
65
+ this.deletes.deleted$.subscribe((notification) => {
66
+ this.handleDeleteNotification(notification).catch((error) => {
67
+ console.error("[applesauce-core] Error handling delete notification:", error);
68
+ });
69
+ });
70
+ // Create expiration manager
71
+ this.expiration = options.expirationManager ?? new ExpirationManager();
72
+ // Listen to expired events and remove them from the store
73
+ this.expiration.expired$.subscribe((id) => {
74
+ this.handleExpiredNotification(id).catch((error) => {
75
+ console.error("[applesauce-core] Error handling expired notification:", error);
76
+ });
77
+ });
47
78
  // when events are added to the database, add the symbol
48
79
  this.insert$.subscribe((event) => {
49
80
  Reflect.set(event, EventStoreSymbol, this);
@@ -60,89 +91,37 @@ export class AsyncEventStore extends EventModels {
60
91
  return event;
61
92
  return this.memory.add(event);
62
93
  }
63
- // delete state
64
- deletedIds = new Set();
65
- deletedCoords = new Map();
66
- checkDeleted(event) {
67
- if (typeof event === "string")
68
- return this.deletedIds.has(event);
69
- else {
70
- if (this.deletedIds.has(event.id))
71
- return true;
72
- if (isAddressableKind(event.kind)) {
73
- const identifier = event.tags.find((t) => t[0] === "d")?.[1];
74
- const deleted = this.deletedCoords.get(createReplaceableAddress(event.kind, event.pubkey, identifier));
75
- if (deleted)
76
- return deleted > event.created_at;
77
- }
78
- }
79
- return false;
80
- }
81
- expirations = new Map();
82
- /** Adds an event to the expiration map */
83
- addExpiration(event) {
84
- const expiration = getExpirationTimestamp(event);
85
- if (expiration && Number.isFinite(expiration))
86
- this.expirations.set(event.id, expiration);
87
- }
88
- expirationTimeout = null;
89
- nextExpirationCheck = null;
90
- handleExpiringEvent(event) {
91
- const expiration = getExpirationTimestamp(event);
92
- if (!expiration)
94
+ /** Handle a delete event by pointer */
95
+ async handleDeleteNotification({ pointer, until }) {
96
+ // Skip if keeping deleted events
97
+ if (this.keepDeleted)
93
98
  return;
94
- // Add event to expiration map
95
- this.expirations.set(event.id, expiration);
96
- // Exit if the next check is already less than the next expiration
97
- if (this.expirationTimeout && this.nextExpirationCheck && this.nextExpirationCheck < expiration)
98
- return;
99
- // Set timeout to prune expired events
100
- if (this.expirationTimeout)
101
- clearTimeout(this.expirationTimeout);
102
- const timeout = expiration - unixNow();
103
- this.expirationTimeout = setTimeout(this.pruneExpired.bind(this), timeout * 1000 + 10);
104
- this.nextExpirationCheck = expiration;
105
- }
106
- /** Remove expired events from the store */
107
- async pruneExpired() {
108
- const now = unixNow();
109
- for (const [id, expiration] of this.expirations) {
110
- if (expiration <= now) {
111
- this.expirations.delete(id);
112
- await this.remove(id);
113
- }
114
- }
115
- // Cleanup timers
116
- if (this.expirationTimeout)
117
- clearTimeout(this.expirationTimeout);
118
- this.nextExpirationCheck = null;
119
- this.expirationTimeout = null;
120
- }
121
- // handling delete events
122
- async handleDeleteEvent(deleteEvent) {
123
- const ids = getDeleteIds(deleteEvent);
124
- for (const id of ids) {
125
- this.deletedIds.add(id);
126
- // remove deleted events in the database
127
- await this.remove(id);
99
+ if (isEventPointer(pointer)) {
100
+ // For event pointers, get the event by ID and remove if it exists
101
+ const event = await this.getEvent(pointer.id);
102
+ if (event && until >= event.created_at && eventMatchesPointer(event, pointer))
103
+ await this.remove(event);
128
104
  }
129
- const coords = getDeleteCoordinates(deleteEvent);
130
- for (const coord of coords) {
131
- this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at));
132
- // Parse the nostr address coordinate
133
- const parsed = parseCoordinate(coord);
134
- if (!parsed)
135
- continue;
136
- // Remove older versions of replaceable events
137
- const events = await this.database.getReplaceableHistory(parsed.kind, parsed.pubkey, parsed.identifier);
105
+ else if (isAddressPointer(pointer)) {
106
+ // For address pointers, get all events matching the address and remove if deleted
107
+ const events = await this.getReplaceableHistory(pointer.kind, pointer.pubkey, pointer.identifier);
138
108
  if (events) {
139
109
  for (const event of events) {
140
- if (event.created_at < deleteEvent.created_at)
110
+ // Remove the event if its older than the delete notification and matches the pointer
111
+ if (until >= event.created_at && eventMatchesPointer(event, pointer)) {
141
112
  await this.remove(event);
113
+ }
142
114
  }
143
115
  }
144
116
  }
145
117
  }
118
+ /** Handle an expired event by id */
119
+ async handleExpiredNotification(id) {
120
+ // Skip if keeping expired events
121
+ if (this.keepExpired)
122
+ return;
123
+ await this.remove(id);
124
+ }
146
125
  /** Copies important metadata from and identical event to another */
147
126
  static mergeDuplicateEvent(source, dest) {
148
127
  const relays = getSeenRelays(source);
@@ -161,19 +140,21 @@ export class AsyncEventStore extends EventModels {
161
140
  */
162
141
  async add(event, fromRelay) {
163
142
  // Handle delete events differently
164
- if (event.kind === kinds.EventDeletion)
165
- await this.handleDeleteEvent(event);
143
+ if (event.kind === kinds.EventDeletion) {
144
+ await this.deletes.add(event);
145
+ return event;
146
+ }
166
147
  // Ignore if the event was deleted
167
- if (this.checkDeleted(event))
148
+ if (await this.deletes.check(event))
168
149
  return event;
169
150
  // Reject expired events if keepExpired is false
170
151
  const expiration = getExpirationTimestamp(event);
171
152
  if (this.keepExpired === false && expiration && expiration <= unixNow())
172
153
  return null;
173
154
  // Get the replaceable identifier
174
- const identifier = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
155
+ const identifier = isReplaceable(event.kind) ? getReplaceableIdentifier(event) : undefined;
175
156
  // Don't insert the event if there is already a newer version
176
- if (!this.keepOldVersions && isReplaceable(event.kind)) {
157
+ if (this.keepOldVersions === false && isReplaceable(event.kind)) {
177
158
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
178
159
  // If there is already a newer version, copy cached symbols and return existing event
179
160
  if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
@@ -207,7 +188,7 @@ export class AsyncEventStore extends EventModels {
207
188
  if (inserted === event)
208
189
  this.insert$.next(inserted);
209
190
  // remove all old version of the replaceable event
210
- if (!this.keepOldVersions && isReplaceable(event.kind)) {
191
+ if (this.keepOldVersions === false && isReplaceable(event.kind)) {
211
192
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
212
193
  if (existing && existing.length > 0) {
213
194
  const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
@@ -219,14 +200,17 @@ export class AsyncEventStore extends EventModels {
219
200
  return existing[0];
220
201
  }
221
202
  }
222
- // Add event to expiration map
223
- if (this.keepExpired === false && expiration)
224
- this.handleExpiringEvent(inserted);
203
+ // Add event to expiration manager if it has an expiration tag
204
+ if (this.keepExpired === false && expiration !== undefined)
205
+ this.expiration.track(inserted);
225
206
  return inserted;
226
207
  }
227
208
  /** Removes an event from the store and updates subscriptions */
228
209
  async remove(event) {
229
- let instance = this.memory?.getEvent(typeof event === "string" ? event : event.id);
210
+ const eventId = typeof event === "string" ? event : event.id;
211
+ let instance = this.memory?.getEvent(eventId);
212
+ // Remove from expiration manager
213
+ this.expiration.forget(eventId);
230
214
  // Remove from memory if available
231
215
  if (this.memory)
232
216
  this.memory.remove(event);
@@ -242,6 +226,9 @@ export class AsyncEventStore extends EventModels {
242
226
  async removeByFilters(filters) {
243
227
  // Get events that will be removed for notification
244
228
  const eventsToRemove = await this.getByFilters(filters);
229
+ // Remove from expiration manager
230
+ for (const event of eventsToRemove)
231
+ this.expiration.forget(event.id);
245
232
  // Remove from memory if available
246
233
  if (this.memory)
247
234
  this.memory.removeByFilters(filters);
@@ -287,11 +274,8 @@ export class AsyncEventStore extends EventModels {
287
274
  /** Returns all versions of a replaceable event */
288
275
  async getReplaceableHistory(kind, pubkey, identifier) {
289
276
  // Get the events from memory first, then from the database
290
- const memoryEvents = this.memory?.getReplaceableHistory(kind, pubkey, identifier);
291
- if (memoryEvents)
292
- return memoryEvents;
293
- const dbEvents = await this.database.getReplaceableHistory(kind, pubkey, identifier);
294
- return dbEvents?.map((e) => this.mapToMemory(e) ?? e);
277
+ return (this.memory?.getReplaceableHistory(kind, pubkey, identifier) ??
278
+ (await this.database.getReplaceableHistory(kind, pubkey, identifier))?.map((e) => this.mapToMemory(e) ?? e));
295
279
  }
296
280
  /** Get all events matching a filter */
297
281
  async getByFilters(filters) {
@@ -299,7 +283,7 @@ export class AsyncEventStore extends EventModels {
299
283
  const events = await this.database.getByFilters(filters);
300
284
  // Map events to memory if available for better performance
301
285
  if (this.memory)
302
- return events.map((e) => this.mapToMemory(e) ?? e);
286
+ return events.map((e) => this.mapToMemory(e));
303
287
  else
304
288
  return events;
305
289
  }
@@ -339,21 +323,4 @@ export class AsyncEventStore extends EventModels {
339
323
  prune(limit) {
340
324
  return this.memory?.prune(limit) ?? 0;
341
325
  }
342
- /** Returns an observable that completes when an event is removed */
343
- removed(id) {
344
- const deleted = this.checkDeleted(id);
345
- if (deleted)
346
- return EMPTY;
347
- return this.remove$.pipe(
348
- // listen for removed events
349
- filter((e) => e.id === id),
350
- // complete as soon as we find a matching removed event
351
- take(1),
352
- // switch to empty
353
- mergeMap(() => EMPTY));
354
- }
355
- /** Creates an observable that emits when event is updated */
356
- updated(event) {
357
- return this.update$.pipe(filter((e) => e.id === event || e === event));
358
- }
359
326
  }
@@ -0,0 +1,30 @@
1
+ import { Observable } from "rxjs";
2
+ import { NostrEvent } from "../helpers/event.js";
3
+ import { DeleteEventNotification, IDeleteManager } from "./interface.js";
4
+ /** Manages deletion state for events, ensuring users can only delete their own events */
5
+ export declare class DeleteManager implements IDeleteManager {
6
+ /** A stream of pointers that may have been deleted */
7
+ readonly deleted$: Observable<DeleteEventNotification>;
8
+ /** Internal subject for deleted$ observable */
9
+ private deletedSubject;
10
+ /** Maps author pubkey to Set of event IDs they have deleted */
11
+ private deletedIds;
12
+ /** Maps of author pubkey to Map of kind + "|" + identifier and timestamp of the delete event */
13
+ private deletedIdentifiers;
14
+ constructor();
15
+ /**
16
+ * Process a kind 5 delete event
17
+ * Extracts event pointers and address pointers from the delete event
18
+ * Enforces that users can only delete their own events
19
+ */
20
+ add(deleteEvent: NostrEvent): DeleteEventNotification[];
21
+ /**
22
+ * Check if an event is deleted
23
+ * Verifies the event was deleted by its own author
24
+ */
25
+ check(event: NostrEvent): boolean;
26
+ /**
27
+ * Filter out all deleted events from an array of events
28
+ */
29
+ filter(events: NostrEvent[]): NostrEvent[];
30
+ }
@@ -0,0 +1,99 @@
1
+ import { Subject } from "rxjs";
2
+ import { getDeleteAddressPointers, getDeleteEventPointers } from "../helpers/delete.js";
3
+ import { getReplaceableIdentifier, isAddressableKind, isReplaceableKind, kinds } from "../helpers/event.js";
4
+ /** Manages deletion state for events, ensuring users can only delete their own events */
5
+ export class DeleteManager {
6
+ /** A stream of pointers that may have been deleted */
7
+ deleted$;
8
+ /** Internal subject for deleted$ observable */
9
+ deletedSubject = new Subject();
10
+ /** Maps author pubkey to Set of event IDs they have deleted */
11
+ deletedIds = new Map();
12
+ /** Maps of author pubkey to Map of kind + "|" + identifier and timestamp of the delete event */
13
+ deletedIdentifiers = new Map();
14
+ constructor() {
15
+ this.deleted$ = this.deletedSubject.asObservable();
16
+ }
17
+ /**
18
+ * Process a kind 5 delete event
19
+ * Extracts event pointers and address pointers from the delete event
20
+ * Enforces that users can only delete their own events
21
+ */
22
+ add(deleteEvent) {
23
+ // SKip non-delete events
24
+ if (deleteEvent.kind !== kinds.EventDeletion)
25
+ return [];
26
+ const author = deleteEvent.pubkey;
27
+ const notifications = [];
28
+ // Extract event pointers from "e" tags (already filtered and author set by helper)
29
+ const eventPointers = getDeleteEventPointers(deleteEvent);
30
+ if (eventPointers.length > 0) {
31
+ let ids = this.deletedIds.get(author);
32
+ if (!ids) {
33
+ ids = new Set();
34
+ this.deletedIds.set(author, ids);
35
+ }
36
+ for (const pointer of eventPointers) {
37
+ ids.add(pointer.id);
38
+ const notification = {
39
+ pointer,
40
+ until: deleteEvent.created_at,
41
+ };
42
+ notifications.push(notification);
43
+ this.deletedSubject.next(notification);
44
+ }
45
+ }
46
+ // Add address pointers to memory
47
+ const addressPointers = getDeleteAddressPointers(deleteEvent);
48
+ if (addressPointers.length > 0) {
49
+ let identifiers = this.deletedIdentifiers.get(author);
50
+ if (!identifiers) {
51
+ identifiers = new Map();
52
+ this.deletedIdentifiers.set(author, identifiers);
53
+ }
54
+ for (const pointer of addressPointers) {
55
+ const key = pointer.kind + "|" + pointer.identifier;
56
+ identifiers.set(key, deleteEvent.created_at);
57
+ const notification = {
58
+ pointer,
59
+ until: deleteEvent.created_at,
60
+ };
61
+ notifications.push(notification);
62
+ this.deletedSubject.next(notification);
63
+ }
64
+ }
65
+ return notifications;
66
+ }
67
+ /**
68
+ * Check if an event is deleted
69
+ * Verifies the event was deleted by its own author
70
+ */
71
+ check(event) {
72
+ const author = event.pubkey;
73
+ if (isReplaceableKind(event.kind) || isAddressableKind(event.kind)) {
74
+ const identifiers = this.deletedIdentifiers.get(author);
75
+ if (!identifiers)
76
+ return false;
77
+ const identifier = getReplaceableIdentifier(event);
78
+ const key = event.kind + "|" + identifier;
79
+ const timestamp = identifiers.get(key);
80
+ if (timestamp === undefined)
81
+ return false;
82
+ // Check that the delete event timestamp is newer than the event
83
+ return timestamp >= event.created_at;
84
+ }
85
+ else {
86
+ const ids = this.deletedIds.get(author);
87
+ if (!ids)
88
+ return false;
89
+ // Check if that event id was deleted by the author
90
+ return ids.has(event.id);
91
+ }
92
+ }
93
+ /**
94
+ * Filter out all deleted events from an array of events
95
+ */
96
+ filter(events) {
97
+ return events.filter((event) => this.check(event) === false);
98
+ }
99
+ }
@@ -3,17 +3,39 @@ import { NostrEvent } from "../helpers/event.js";
3
3
  import { Filter } from "../helpers/filter.js";
4
4
  import { AddressPointer, AddressPointerWithoutD, EventPointer } from "../helpers/pointers.js";
5
5
  import { EventMemory } from "./event-memory.js";
6
- import { IEventDatabase, IEventStore } from "./interface.js";
7
6
  import { EventModels } from "./event-models.js";
7
+ import { IDeleteManager, IEventDatabase, IEventStore, IExpirationManager } from "./interface.js";
8
+ export type EventStoreOptions = {
9
+ /** Keep deleted events in the store */
10
+ keepDeleted?: boolean;
11
+ /** Keep expired events in the store */
12
+ keepExpired?: boolean;
13
+ /** Enable this to keep old versions of replaceable events */
14
+ keepOldVersions?: boolean;
15
+ /** The database to use for storing events */
16
+ database?: IEventDatabase;
17
+ /** Custom {@link IDeleteManager} implementation */
18
+ deleteManager?: IDeleteManager;
19
+ /** Custom {@link IExpirationManager} implementation */
20
+ expirationManager?: IExpirationManager;
21
+ /** The method used to verify events */
22
+ verifyEvent?: (event: NostrEvent) => boolean;
23
+ };
8
24
  /** A wrapper around an event database that handles replaceable events, deletes, and models */
9
25
  export declare class EventStore extends EventModels implements IEventStore {
10
26
  database: IEventDatabase;
11
27
  /** Optional memory database for ensuring single event instances */
12
28
  memory: EventMemory;
29
+ /** Manager for handling event deletions with authorization */
30
+ private deletes;
31
+ /** Manager for handling event expirations */
32
+ private expiration;
13
33
  /** Enable this to keep old versions of replaceable events */
14
34
  keepOldVersions: boolean;
15
- /** Enable this to keep expired events */
35
+ /** Keep expired events in the store */
16
36
  keepExpired: boolean;
37
+ /** Keep deleted events in the store */
38
+ keepDeleted: boolean;
17
39
  /** The method used to verify events */
18
40
  private _verifyEventMethod?;
19
41
  /** Get the method used to verify events */
@@ -28,21 +50,13 @@ export declare class EventStore extends EventModels implements IEventStore {
28
50
  remove$: Subject<import("nostr-tools/core").Event>;
29
51
  /** A method that will be called when an event isn't found in the store */
30
52
  eventLoader?: (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
31
- constructor(database?: IEventDatabase);
53
+ constructor(options?: EventStoreOptions);
32
54
  /** A method to add all events to memory to ensure there is only ever a single instance of an event */
33
55
  private mapToMemory;
34
- protected deletedIds: Set<string>;
35
- protected deletedCoords: Map<string, number>;
36
- protected checkDeleted(event: string | NostrEvent): boolean;
37
- protected expirations: Map<string, number>;
38
- /** Adds an event to the expiration map */
39
- protected addExpiration(event: NostrEvent): void;
40
- protected expirationTimeout: number | null;
41
- protected nextExpirationCheck: number | null;
42
- protected handleExpiringEvent(event: NostrEvent): void;
43
- /** Remove expired events from the store */
44
- protected pruneExpired(): void;
45
- protected handleDeleteEvent(deleteEvent: NostrEvent): void;
56
+ /** Handle a delete event by pointer */
57
+ private handleDeleteNotification;
58
+ /** Handle an expired event by id */
59
+ private handleExpiredNotification;
46
60
  /** Copies important metadata from and identical event to another */
47
61
  static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
48
62
  /**
@@ -84,8 +98,4 @@ export declare class EventStore extends EventModels implements IEventStore {
84
98
  unclaimed(): Generator<NostrEvent>;
85
99
  /** Removes any event that is not being used by a subscription */
86
100
  prune(limit?: number): number;
87
- /** Returns an observable that completes when an event is removed */
88
- removed(id: string): Observable<never>;
89
- /** Creates an observable that emits when event is updated */
90
- updated(event: string | NostrEvent): Observable<NostrEvent>;
91
101
  }