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.
- 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 +30 -31
- package/dist/event-store/async-event-store.js +83 -127
- 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 +31 -34
- package/dist/event-store/event-store.js +83 -133
- 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 +49 -9
- 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/base.js +2 -19
- 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 */
|
|
@@ -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
|
-
|
|
44
|
-
constructor(
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
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();
|
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
234
|
-
if (this.keepExpired === false && expiration)
|
|
235
|
-
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);
|
|
236
206
|
return inserted;
|
|
237
207
|
}
|
|
238
208
|
/** Removes an event from the store and updates subscriptions */
|
|
239
209
|
async remove(event) {
|
|
240
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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)
|
|
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
|
+
}
|