applesauce-core 0.0.0-next-20251203172109 → 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 (38) 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 +30 -31
  4. package/dist/event-store/async-event-store.js +83 -127
  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 +31 -34
  8. package/dist/event-store/event-store.js +83 -133
  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 +49 -9
  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/base.js +2 -19
  28. package/dist/models/index.d.ts +0 -1
  29. package/dist/models/index.js +0 -1
  30. package/dist/operations/client.js +2 -2
  31. package/dist/operations/delete.js +5 -3
  32. package/dist/operations/tag/common.d.ts +4 -3
  33. package/dist/operations/tag/common.js +23 -10
  34. package/package.json +6 -1
  35. package/dist/helpers/lists.d.ts +0 -58
  36. package/dist/helpers/lists.js +0 -110
  37. package/dist/models/relays.d.ts +0 -27
  38. 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 */
@@ -28,34 +50,15 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
28
50
  remove$: Subject<import("nostr-tools/core").Event>;
29
51
  /**
30
52
  * A method that will be called when an event isn't found in the store
31
- * @experimental
32
- */
33
- eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
34
- /**
35
- * A method that will be called when a replaceable event isn't found in the store
36
- * @experimental
37
- */
38
- replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
39
- /**
40
- * A method that will be called when an addressable event isn't found in the store
41
- * @experimental
42
53
  */
43
- addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
44
- constructor(database: IAsyncEventDatabase);
54
+ eventLoader?: (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
55
+ constructor(options: AsyncEventStoreOptions);
45
56
  /** A method to add all events to memory to ensure there is only ever a single instance of an event */
46
57
  private mapToMemory;
47
- protected deletedIds: Set<string>;
48
- protected deletedCoords: Map<string, number>;
49
- protected checkDeleted(event: string | NostrEvent): boolean;
50
- protected expirations: Map<string, number>;
51
- /** Adds an event to the expiration map */
52
- protected addExpiration(event: NostrEvent): void;
53
- protected expirationTimeout: number | null;
54
- protected nextExpirationCheck: number | null;
55
- protected handleExpiringEvent(event: NostrEvent): void;
56
- /** Remove expired events from the store */
57
- protected pruneExpired(): Promise<void>;
58
- 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;
59
62
  /** Copies important metadata from and identical event to another */
60
63
  static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
61
64
  /**
@@ -97,8 +100,4 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
97
100
  unclaimed(): Generator<NostrEvent>;
98
101
  /** Removes any event that is not being used by a subscription */
99
102
  prune(limit?: number): number;
100
- /** Returns an observable that completes when an event is removed */
101
- removed(id: string): Observable<never>;
102
- /** Creates an observable that emits when event is updated */
103
- updated(event: string | NostrEvent): Observable<NostrEvent>;
104
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();
@@ -38,23 +44,37 @@ export class AsyncEventStore extends EventModels {
38
44
  remove$ = new Subject();
39
45
  /**
40
46
  * A method that will be called when an event isn't found in the store
41
- * @experimental
42
47
  */
43
48
  eventLoader;
44
- /**
45
- * A method that will be called when a replaceable event isn't found in the store
46
- * @experimental
47
- */
48
- replaceableLoader;
49
- /**
50
- * A method that will be called when an addressable event isn't found in the store
51
- * @experimental
52
- */
53
- addressableLoader;
54
- constructor(database) {
49
+ constructor(options) {
55
50
  super();
56
- this.database = database;
51
+ this.database = options.database;
57
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
+ });
58
78
  // when events are added to the database, add the symbol
59
79
  this.insert$.subscribe((event) => {
60
80
  Reflect.set(event, EventStoreSymbol, this);
@@ -71,89 +91,37 @@ export class AsyncEventStore extends EventModels {
71
91
  return event;
72
92
  return this.memory.add(event);
73
93
  }
74
- // delete state
75
- deletedIds = new Set();
76
- deletedCoords = new Map();
77
- checkDeleted(event) {
78
- if (typeof event === "string")
79
- return this.deletedIds.has(event);
80
- else {
81
- if (this.deletedIds.has(event.id))
82
- return true;
83
- if (isAddressableKind(event.kind)) {
84
- const identifier = event.tags.find((t) => t[0] === "d")?.[1];
85
- const deleted = this.deletedCoords.get(createReplaceableAddress(event.kind, event.pubkey, identifier));
86
- if (deleted)
87
- return deleted > event.created_at;
88
- }
89
- }
90
- return false;
91
- }
92
- expirations = new Map();
93
- /** Adds an event to the expiration map */
94
- addExpiration(event) {
95
- const expiration = getExpirationTimestamp(event);
96
- if (expiration && Number.isFinite(expiration))
97
- this.expirations.set(event.id, expiration);
98
- }
99
- expirationTimeout = null;
100
- nextExpirationCheck = null;
101
- handleExpiringEvent(event) {
102
- const expiration = getExpirationTimestamp(event);
103
- if (!expiration)
94
+ /** Handle a delete event by pointer */
95
+ async handleDeleteNotification({ pointer, until }) {
96
+ // Skip if keeping deleted events
97
+ if (this.keepDeleted)
104
98
  return;
105
- // Add event to expiration map
106
- this.expirations.set(event.id, expiration);
107
- // Exit if the next check is already less than the next expiration
108
- if (this.expirationTimeout && this.nextExpirationCheck && this.nextExpirationCheck < expiration)
109
- return;
110
- // Set timeout to prune expired events
111
- if (this.expirationTimeout)
112
- clearTimeout(this.expirationTimeout);
113
- const timeout = expiration - unixNow();
114
- this.expirationTimeout = setTimeout(this.pruneExpired.bind(this), timeout * 1000 + 10);
115
- this.nextExpirationCheck = expiration;
116
- }
117
- /** Remove expired events from the store */
118
- async pruneExpired() {
119
- const now = unixNow();
120
- for (const [id, expiration] of this.expirations) {
121
- if (expiration <= now) {
122
- this.expirations.delete(id);
123
- await this.remove(id);
124
- }
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);
125
104
  }
126
- // Cleanup timers
127
- if (this.expirationTimeout)
128
- clearTimeout(this.expirationTimeout);
129
- this.nextExpirationCheck = null;
130
- this.expirationTimeout = null;
131
- }
132
- // handling delete events
133
- async handleDeleteEvent(deleteEvent) {
134
- const ids = getDeleteIds(deleteEvent);
135
- for (const id of ids) {
136
- this.deletedIds.add(id);
137
- // remove deleted events in the database
138
- await this.remove(id);
139
- }
140
- const coords = getDeleteCoordinates(deleteEvent);
141
- for (const coord of coords) {
142
- this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at));
143
- // Parse the nostr address coordinate
144
- const parsed = parseCoordinate(coord);
145
- if (!parsed)
146
- continue;
147
- // Remove older versions of replaceable events
148
- 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);
149
108
  if (events) {
150
109
  for (const event of events) {
151
- 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)) {
152
112
  await this.remove(event);
113
+ }
153
114
  }
154
115
  }
155
116
  }
156
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
+ }
157
125
  /** Copies important metadata from and identical event to another */
158
126
  static mergeDuplicateEvent(source, dest) {
159
127
  const relays = getSeenRelays(source);
@@ -172,19 +140,21 @@ export class AsyncEventStore extends EventModels {
172
140
  */
173
141
  async add(event, fromRelay) {
174
142
  // Handle delete events differently
175
- if (event.kind === kinds.EventDeletion)
176
- await this.handleDeleteEvent(event);
143
+ if (event.kind === kinds.EventDeletion) {
144
+ await this.deletes.add(event);
145
+ return event;
146
+ }
177
147
  // Ignore if the event was deleted
178
- if (this.checkDeleted(event))
148
+ if (await this.deletes.check(event))
179
149
  return event;
180
150
  // Reject expired events if keepExpired is false
181
151
  const expiration = getExpirationTimestamp(event);
182
152
  if (this.keepExpired === false && expiration && expiration <= unixNow())
183
153
  return null;
184
154
  // Get the replaceable identifier
185
- const identifier = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
155
+ const identifier = isReplaceable(event.kind) ? getReplaceableIdentifier(event) : undefined;
186
156
  // Don't insert the event if there is already a newer version
187
- if (!this.keepOldVersions && isReplaceable(event.kind)) {
157
+ if (this.keepOldVersions === false && isReplaceable(event.kind)) {
188
158
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
189
159
  // If there is already a newer version, copy cached symbols and return existing event
190
160
  if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
@@ -218,7 +188,7 @@ export class AsyncEventStore extends EventModels {
218
188
  if (inserted === event)
219
189
  this.insert$.next(inserted);
220
190
  // remove all old version of the replaceable event
221
- if (!this.keepOldVersions && isReplaceable(event.kind)) {
191
+ if (this.keepOldVersions === false && isReplaceable(event.kind)) {
222
192
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
223
193
  if (existing && existing.length > 0) {
224
194
  const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
@@ -230,14 +200,17 @@ export class AsyncEventStore extends EventModels {
230
200
  return existing[0];
231
201
  }
232
202
  }
233
- // Add event to expiration map
234
- if (this.keepExpired === false && expiration)
235
- 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);
236
206
  return inserted;
237
207
  }
238
208
  /** Removes an event from the store and updates subscriptions */
239
209
  async remove(event) {
240
- 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);
241
214
  // Remove from memory if available
242
215
  if (this.memory)
243
216
  this.memory.remove(event);
@@ -253,6 +226,9 @@ export class AsyncEventStore extends EventModels {
253
226
  async removeByFilters(filters) {
254
227
  // Get events that will be removed for notification
255
228
  const eventsToRemove = await this.getByFilters(filters);
229
+ // Remove from expiration manager
230
+ for (const event of eventsToRemove)
231
+ this.expiration.forget(event.id);
256
232
  // Remove from memory if available
257
233
  if (this.memory)
258
234
  this.memory.removeByFilters(filters);
@@ -298,11 +274,8 @@ export class AsyncEventStore extends EventModels {
298
274
  /** Returns all versions of a replaceable event */
299
275
  async getReplaceableHistory(kind, pubkey, identifier) {
300
276
  // Get the events from memory first, then from the database
301
- const memoryEvents = this.memory?.getReplaceableHistory(kind, pubkey, identifier);
302
- if (memoryEvents)
303
- return memoryEvents;
304
- const dbEvents = await this.database.getReplaceableHistory(kind, pubkey, identifier);
305
- 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));
306
279
  }
307
280
  /** Get all events matching a filter */
308
281
  async getByFilters(filters) {
@@ -310,7 +283,7 @@ export class AsyncEventStore extends EventModels {
310
283
  const events = await this.database.getByFilters(filters);
311
284
  // Map events to memory if available for better performance
312
285
  if (this.memory)
313
- return events.map((e) => this.mapToMemory(e) ?? e);
286
+ return events.map((e) => this.mapToMemory(e));
314
287
  else
315
288
  return events;
316
289
  }
@@ -350,21 +323,4 @@ export class AsyncEventStore extends EventModels {
350
323
  prune(limit) {
351
324
  return this.memory?.prune(limit) ?? 0;
352
325
  }
353
- /** Returns an observable that completes when an event is removed */
354
- removed(id) {
355
- const deleted = this.checkDeleted(id);
356
- if (deleted)
357
- return EMPTY;
358
- return this.remove$.pipe(
359
- // listen for removed events
360
- filter((e) => e.id === id),
361
- // complete as soon as we find a matching removed event
362
- take(1),
363
- // switch to empty
364
- mergeMap(() => EMPTY));
365
- }
366
- /** Creates an observable that emits when event is updated */
367
- updated(event) {
368
- return this.update$.pipe(filter((e) => e.id === event || e === event));
369
- }
370
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
+ }