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.
- package/dist/event-store/async-delete-manager.d.ts +26 -0
- package/dist/event-store/async-delete-manager.js +33 -0
- package/dist/event-store/async-event-store.d.ts +29 -19
- package/dist/event-store/async-event-store.js +83 -116
- package/dist/event-store/delete-manager.d.ts +30 -0
- package/dist/event-store/delete-manager.js +99 -0
- package/dist/event-store/event-store.d.ts +29 -19
- package/dist/event-store/event-store.js +82 -119
- package/dist/event-store/expiration-manager.d.ts +37 -0
- package/dist/event-store/expiration-manager.js +97 -0
- package/dist/event-store/index.d.ts +3 -0
- package/dist/event-store/index.js +3 -0
- package/dist/event-store/interface.d.ts +44 -0
- package/dist/helpers/contacts.js +2 -2
- package/dist/helpers/delete.d.ts +8 -1
- package/dist/helpers/delete.js +25 -1
- package/dist/helpers/event.d.ts +2 -2
- package/dist/helpers/event.js +5 -5
- package/dist/helpers/factory.js +4 -4
- package/dist/helpers/index.d.ts +1 -1
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/model.d.ts +5 -0
- package/dist/helpers/model.js +17 -0
- package/dist/helpers/pointers.d.ts +17 -36
- package/dist/helpers/pointers.js +69 -84
- package/dist/helpers/string.js +8 -2
- package/dist/models/index.d.ts +0 -1
- package/dist/models/index.js +0 -1
- package/dist/operations/client.js +2 -2
- package/dist/operations/delete.js +5 -3
- package/dist/operations/tag/common.d.ts +4 -3
- package/dist/operations/tag/common.js +23 -10
- package/package.json +6 -1
- package/dist/helpers/lists.d.ts +0 -58
- package/dist/helpers/lists.js +0 -110
- package/dist/models/relays.d.ts +0 -27
- 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
|
-
/**
|
|
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(
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
223
|
-
if (this.keepExpired === false && expiration)
|
|
224
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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)
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
}
|