applesauce-core 0.0.0-next-20241122170522 → 0.0.0-next-20241126184016
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/event-store/database.d.ts +17 -13
- package/dist/event-store/database.js +45 -22
- package/dist/event-store/event-store.d.ts +21 -12
- package/dist/event-store/event-store.js +166 -79
- package/dist/helpers/delete.d.ts +5 -0
- package/dist/helpers/delete.js +38 -0
- package/dist/helpers/filter.d.ts +0 -2
- package/dist/helpers/filter.js +2 -6
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/observable/get-value.d.ts +2 -0
- package/dist/observable/get-value.js +13 -0
- package/dist/observable/index.d.ts +1 -1
- package/dist/observable/index.js +1 -1
- package/dist/queries/simple.d.ts +3 -3
- package/dist/queries/simple.js +11 -11
- package/dist/query-store/index.d.ts +1 -1
- package/dist/query-store/index.js +2 -2
- package/package.json +4 -2
|
@@ -3,6 +3,7 @@ import { Subject } from "rxjs";
|
|
|
3
3
|
import { LRU } from "../helpers/lru.js";
|
|
4
4
|
/**
|
|
5
5
|
* An in-memory database for nostr events
|
|
6
|
+
* NOTE: does not handle replaceable events
|
|
6
7
|
*/
|
|
7
8
|
export declare class Database {
|
|
8
9
|
protected log: import("debug").Debugger;
|
|
@@ -13,6 +14,7 @@ export declare class Database {
|
|
|
13
14
|
protected created_at: NostrEvent[];
|
|
14
15
|
/** LRU cache of last events touched */
|
|
15
16
|
events: LRU<import("nostr-tools").Event>;
|
|
17
|
+
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
16
18
|
/** A stream of events inserted into the database */
|
|
17
19
|
inserted: Subject<import("nostr-tools").Event>;
|
|
18
20
|
/** A stream of events that have been updated */
|
|
@@ -27,18 +29,20 @@ export declare class Database {
|
|
|
27
29
|
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
28
30
|
/** Moves an event to the top of the LRU cache */
|
|
29
31
|
touch(event: NostrEvent): void;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
/** Checks if the database contains an event without touching it */
|
|
33
|
+
hasEvent(id: string): boolean;
|
|
34
|
+
/** Gets a single event based on id */
|
|
35
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
32
36
|
/** Checks if the database contains a replaceable event without touching it */
|
|
33
37
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
34
|
-
/** Gets
|
|
35
|
-
getReplaceable(kind: number, pubkey: string, d?: string):
|
|
38
|
+
/** Gets an array of replaceable events */
|
|
39
|
+
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
|
|
36
40
|
/** Inserts an event into the database and notifies all subscriptions */
|
|
37
|
-
addEvent(event: NostrEvent):
|
|
41
|
+
addEvent(event: NostrEvent): NostrEvent;
|
|
38
42
|
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
39
|
-
updateEvent(event: NostrEvent):
|
|
43
|
+
updateEvent(event: NostrEvent): NostrEvent;
|
|
40
44
|
/** Deletes an event from the database and notifies all subscriptions */
|
|
41
|
-
deleteEvent(
|
|
45
|
+
deleteEvent(eventOrId: string | NostrEvent): boolean;
|
|
42
46
|
/** Sets the claim on the event and touches it */
|
|
43
47
|
claimEvent(event: NostrEvent, claim: any): void;
|
|
44
48
|
/** Checks if an event is claimed by anything */
|
|
@@ -47,14 +51,14 @@ export declare class Database {
|
|
|
47
51
|
removeClaim(event: NostrEvent, claim: any): void;
|
|
48
52
|
/** Removes all claims on an event */
|
|
49
53
|
clearClaim(event: NostrEvent): void;
|
|
50
|
-
iterateAuthors(authors: Iterable<string>): Generator<
|
|
51
|
-
iterateTag(tag: string, values: Iterable<string>): Generator<
|
|
52
|
-
iterateKinds(kinds: Iterable<number>): Generator<
|
|
53
|
-
iterateTime(since: number | undefined, until: number | undefined): Generator<
|
|
54
|
-
iterateIds(ids: Iterable<string>): Generator<
|
|
54
|
+
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
|
|
55
|
+
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
|
|
56
|
+
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
|
|
57
|
+
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
|
|
58
|
+
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
55
59
|
/** Returns all events that match the filter */
|
|
56
60
|
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
57
|
-
getForFilters(filters: Filter[]): Set<
|
|
61
|
+
getForFilters(filters: Filter[]): Set<NostrEvent>;
|
|
58
62
|
/** Remove the oldest events that are not claimed */
|
|
59
63
|
prune(limit?: number): number;
|
|
60
64
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import { Subject } from "rxjs";
|
|
3
|
-
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID } from "../helpers/event.js";
|
|
3
|
+
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
|
|
4
4
|
import { INDEXABLE_TAGS } from "./common.js";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
6
|
import { LRU } from "../helpers/lru.js";
|
|
7
7
|
/**
|
|
8
8
|
* An in-memory database for nostr events
|
|
9
|
+
* NOTE: does not handle replaceable events
|
|
9
10
|
*/
|
|
10
11
|
export class Database {
|
|
11
12
|
log = logger.extend("Database");
|
|
@@ -16,6 +17,7 @@ export class Database {
|
|
|
16
17
|
created_at = [];
|
|
17
18
|
/** LRU cache of last events touched */
|
|
18
19
|
events = new LRU();
|
|
20
|
+
replaceable = new Map();
|
|
19
21
|
/** A stream of events inserted into the database */
|
|
20
22
|
inserted = new Subject();
|
|
21
23
|
/** A stream of events that have been updated */
|
|
@@ -56,35 +58,36 @@ export class Database {
|
|
|
56
58
|
}
|
|
57
59
|
/** Moves an event to the top of the LRU cache */
|
|
58
60
|
touch(event) {
|
|
59
|
-
this.events.set(
|
|
61
|
+
this.events.set(event.id, event);
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
/** Checks if the database contains an event without touching it */
|
|
64
|
+
hasEvent(id) {
|
|
65
|
+
return this.events.has(id);
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
/** Gets a single event based on id */
|
|
68
|
+
getEvent(id) {
|
|
69
|
+
return this.events.get(id);
|
|
66
70
|
}
|
|
67
71
|
/** Checks if the database contains a replaceable event without touching it */
|
|
68
72
|
hasReplaceable(kind, pubkey, d) {
|
|
69
|
-
|
|
73
|
+
const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d));
|
|
74
|
+
return !!events && events.length > 0;
|
|
70
75
|
}
|
|
71
|
-
/** Gets
|
|
76
|
+
/** Gets an array of replaceable events */
|
|
72
77
|
getReplaceable(kind, pubkey, d) {
|
|
73
|
-
return this.
|
|
78
|
+
return this.replaceable.get(getReplaceableUID(kind, pubkey, d));
|
|
74
79
|
}
|
|
75
80
|
/** Inserts an event into the database and notifies all subscriptions */
|
|
76
81
|
addEvent(event) {
|
|
77
|
-
const
|
|
78
|
-
const current = this.events.get(
|
|
79
|
-
if (current
|
|
82
|
+
const id = event.id;
|
|
83
|
+
const current = this.events.get(id);
|
|
84
|
+
if (current) {
|
|
80
85
|
// if this is a duplicate event, transfer some import symbols
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
84
|
-
}
|
|
86
|
+
if (event[FromCacheSymbol])
|
|
87
|
+
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
85
88
|
return current;
|
|
86
89
|
}
|
|
87
|
-
this.events.set(
|
|
90
|
+
this.events.set(id, event);
|
|
88
91
|
this.getKindIndex(event.kind).add(event);
|
|
89
92
|
this.getAuthorsIndex(event.pubkey).add(event);
|
|
90
93
|
for (const tag of getIndexableTags(event)) {
|
|
@@ -92,7 +95,18 @@ export class Database {
|
|
|
92
95
|
this.getTagIndex(tag).add(event);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
98
|
+
// insert into time index
|
|
95
99
|
insertEventIntoDescendingList(this.created_at, event);
|
|
100
|
+
// insert into replaceable index
|
|
101
|
+
if (isReplaceable(event.kind)) {
|
|
102
|
+
const uid = getEventUID(event);
|
|
103
|
+
let array = this.replaceable.get(uid);
|
|
104
|
+
if (!this.replaceable.has(uid)) {
|
|
105
|
+
array = [];
|
|
106
|
+
this.replaceable.set(uid, array);
|
|
107
|
+
}
|
|
108
|
+
insertEventIntoDescendingList(array, event);
|
|
109
|
+
}
|
|
96
110
|
this.inserted.next(event);
|
|
97
111
|
return event;
|
|
98
112
|
}
|
|
@@ -103,13 +117,13 @@ export class Database {
|
|
|
103
117
|
return inserted;
|
|
104
118
|
}
|
|
105
119
|
/** Deletes an event from the database and notifies all subscriptions */
|
|
106
|
-
deleteEvent(
|
|
107
|
-
let event = typeof
|
|
120
|
+
deleteEvent(eventOrId) {
|
|
121
|
+
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
|
|
108
122
|
if (!event)
|
|
109
123
|
throw new Error("Missing event");
|
|
110
|
-
const
|
|
124
|
+
const id = event.id;
|
|
111
125
|
// only remove events that are known
|
|
112
|
-
if (!this.events.has(
|
|
126
|
+
if (!this.events.has(id))
|
|
113
127
|
return false;
|
|
114
128
|
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
115
129
|
this.getKindIndex(event.kind).delete(event);
|
|
@@ -121,7 +135,16 @@ export class Database {
|
|
|
121
135
|
// remove from created_at index
|
|
122
136
|
const i = this.created_at.indexOf(event);
|
|
123
137
|
this.created_at.splice(i, 1);
|
|
124
|
-
this.events.delete(
|
|
138
|
+
this.events.delete(id);
|
|
139
|
+
// remove from replaceable index
|
|
140
|
+
if (isReplaceable(event.kind)) {
|
|
141
|
+
const uid = getEventUID(event);
|
|
142
|
+
const array = this.replaceable.get(uid);
|
|
143
|
+
if (array && array.includes(event)) {
|
|
144
|
+
const idx = array.indexOf(event);
|
|
145
|
+
array.splice(idx, 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
125
148
|
this.deleted.next(event);
|
|
126
149
|
return true;
|
|
127
150
|
}
|
|
@@ -3,24 +3,33 @@ import { Observable } from "rxjs";
|
|
|
3
3
|
import { Database } from "./database.js";
|
|
4
4
|
export declare class EventStore {
|
|
5
5
|
database: Database;
|
|
6
|
+
/** Whether to keep old versions of replaceable events */
|
|
7
|
+
keepOldVersions: boolean;
|
|
6
8
|
constructor();
|
|
7
9
|
/** Adds an event to the database */
|
|
8
|
-
add(event: NostrEvent, fromRelay?: string):
|
|
10
|
+
add(event: NostrEvent, fromRelay?: string): NostrEvent;
|
|
11
|
+
protected deletedIds: Set<string>;
|
|
12
|
+
protected deletedCoords: Map<string, number>;
|
|
13
|
+
protected handleDeleteEvent(deleteEvent: NostrEvent): void;
|
|
14
|
+
protected checkDeleted(event: NostrEvent): boolean;
|
|
9
15
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
10
|
-
update(event: NostrEvent):
|
|
11
|
-
getAll(filters: Filter[]): Set<
|
|
12
|
-
hasEvent(uid: string):
|
|
13
|
-
getEvent(uid: string):
|
|
16
|
+
update(event: NostrEvent): NostrEvent;
|
|
17
|
+
getAll(filters: Filter[]): Set<NostrEvent>;
|
|
18
|
+
hasEvent(uid: string): boolean;
|
|
19
|
+
getEvent(uid: string): NostrEvent | undefined;
|
|
14
20
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
15
|
-
|
|
21
|
+
/** Gets the latest version of a replaceable event */
|
|
22
|
+
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined;
|
|
23
|
+
/** Returns all versions of a replaceable event */
|
|
24
|
+
getAllReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
|
|
16
25
|
/** Creates an observable that updates a single event */
|
|
17
|
-
event(
|
|
26
|
+
event(id: string): Observable<NostrEvent | undefined>;
|
|
18
27
|
/** Creates an observable that subscribes to multiple events */
|
|
19
|
-
events(
|
|
20
|
-
/** Creates an observable
|
|
21
|
-
replaceable(kind: number, pubkey: string, d?: string): Observable<
|
|
28
|
+
events(ids: string[]): Observable<Map<string, NostrEvent>>;
|
|
29
|
+
/** Creates an observable with the latest version of a replaceable event */
|
|
30
|
+
replaceable(kind: number, pubkey: string, d?: string): Observable<NostrEvent | undefined>;
|
|
22
31
|
/** Creates an observable that streams all events that match the filter */
|
|
23
|
-
stream(filters: Filter[]): Observable<
|
|
32
|
+
stream(filters: Filter[]): Observable<NostrEvent>;
|
|
24
33
|
/** Creates an observable that updates with an array of sorted events */
|
|
25
|
-
timeline(filters: Filter[]): Observable<
|
|
34
|
+
timeline(filters: Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
|
|
26
35
|
}
|
|
@@ -1,21 +1,75 @@
|
|
|
1
|
+
import { kinds } from "nostr-tools";
|
|
1
2
|
import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
3
|
+
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
2
4
|
import { Observable } from "rxjs";
|
|
3
5
|
import { Database } from "./database.js";
|
|
4
|
-
import { getEventUID, getReplaceableUID } from "../helpers/event.js";
|
|
6
|
+
import { getEventUID, getReplaceableUID, getTagValue, isReplaceable } from "../helpers/event.js";
|
|
5
7
|
import { matchFilters } from "../helpers/filter.js";
|
|
6
8
|
import { addSeenRelay } from "../helpers/relays.js";
|
|
9
|
+
import { getDeleteIds } from "../helpers/delete.js";
|
|
7
10
|
export class EventStore {
|
|
8
11
|
database;
|
|
12
|
+
/** Whether to keep old versions of replaceable events */
|
|
13
|
+
keepOldVersions = false;
|
|
9
14
|
constructor() {
|
|
10
15
|
this.database = new Database();
|
|
11
16
|
}
|
|
12
17
|
/** Adds an event to the database */
|
|
13
18
|
add(event, fromRelay) {
|
|
19
|
+
if (event.kind === kinds.EventDeletion)
|
|
20
|
+
this.handleDeleteEvent(event);
|
|
21
|
+
// ignore if the event was deleted
|
|
22
|
+
if (this.checkDeleted(event))
|
|
23
|
+
return event;
|
|
24
|
+
// insert event into database
|
|
14
25
|
const inserted = this.database.addEvent(event);
|
|
26
|
+
// remove all old version of the replaceable event
|
|
27
|
+
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
28
|
+
const current = this.database.getReplaceable(event.kind, event.pubkey, getTagValue(event, "d"));
|
|
29
|
+
if (current) {
|
|
30
|
+
const older = Array.from(current).filter((e) => e.created_at < event.created_at);
|
|
31
|
+
for (const old of older)
|
|
32
|
+
this.database.deleteEvent(old);
|
|
33
|
+
// skip inserting this event because its not the newest
|
|
34
|
+
if (current.length !== older.length)
|
|
35
|
+
return current[0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// attach relay this event was from
|
|
15
39
|
if (fromRelay)
|
|
16
40
|
addSeenRelay(inserted, fromRelay);
|
|
17
41
|
return inserted;
|
|
18
42
|
}
|
|
43
|
+
deletedIds = new Set();
|
|
44
|
+
deletedCoords = new Map();
|
|
45
|
+
handleDeleteEvent(deleteEvent) {
|
|
46
|
+
const ids = getDeleteIds(deleteEvent);
|
|
47
|
+
for (const id of ids) {
|
|
48
|
+
this.deletedIds.add(id);
|
|
49
|
+
// remove deleted events in the database
|
|
50
|
+
const event = this.database.getEvent(id);
|
|
51
|
+
if (event)
|
|
52
|
+
this.database.deleteEvent(event);
|
|
53
|
+
}
|
|
54
|
+
const coords = getDeleteIds(deleteEvent);
|
|
55
|
+
for (const coord of coords) {
|
|
56
|
+
this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at));
|
|
57
|
+
// remove deleted events in the database
|
|
58
|
+
const event = this.database.getEvent(coord);
|
|
59
|
+
if (event && event.created_at < deleteEvent.created_at)
|
|
60
|
+
this.database.deleteEvent(event);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
checkDeleted(event) {
|
|
64
|
+
if (this.deletedIds.has(event.id))
|
|
65
|
+
return true;
|
|
66
|
+
if (isParameterizedReplaceableKind(event.kind)) {
|
|
67
|
+
const deleted = this.deletedCoords.get(getEventUID(event));
|
|
68
|
+
if (deleted)
|
|
69
|
+
return deleted > event.created_at;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
19
73
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
20
74
|
update(event) {
|
|
21
75
|
return this.database.updateEvent(event);
|
|
@@ -32,93 +86,87 @@ export class EventStore {
|
|
|
32
86
|
hasReplaceable(kind, pubkey, d) {
|
|
33
87
|
return this.database.hasReplaceable(kind, pubkey, d);
|
|
34
88
|
}
|
|
89
|
+
/** Gets the latest version of a replaceable event */
|
|
35
90
|
getReplaceable(kind, pubkey, d) {
|
|
91
|
+
return this.database.getReplaceable(kind, pubkey, d)?.[0];
|
|
92
|
+
}
|
|
93
|
+
/** Returns all versions of a replaceable event */
|
|
94
|
+
getAllReplaceable(kind, pubkey, d) {
|
|
36
95
|
return this.database.getReplaceable(kind, pubkey, d);
|
|
37
96
|
}
|
|
38
97
|
/** Creates an observable that updates a single event */
|
|
39
|
-
event(
|
|
98
|
+
event(id) {
|
|
40
99
|
return new Observable((observer) => {
|
|
41
|
-
let current = this.database.getEvent(
|
|
100
|
+
let current = this.database.getEvent(id);
|
|
42
101
|
if (current) {
|
|
43
102
|
observer.next(current);
|
|
44
103
|
this.database.claimEvent(current, observer);
|
|
45
104
|
}
|
|
46
105
|
// subscribe to future events
|
|
47
106
|
const inserted = this.database.inserted.subscribe((event) => {
|
|
48
|
-
if (
|
|
49
|
-
// remove old claim
|
|
50
|
-
if (current)
|
|
51
|
-
this.database.removeClaim(current, observer);
|
|
107
|
+
if (event.id === id) {
|
|
52
108
|
current = event;
|
|
53
109
|
observer.next(event);
|
|
54
|
-
|
|
55
|
-
this.database.claimEvent(current, observer);
|
|
110
|
+
this.database.claimEvent(event, observer);
|
|
56
111
|
}
|
|
57
112
|
});
|
|
58
|
-
// subscribe to
|
|
113
|
+
// subscribe to updated events
|
|
59
114
|
const updated = this.database.updated.subscribe((event) => {
|
|
60
|
-
if (event ===
|
|
61
|
-
observer.next(
|
|
115
|
+
if (event.id === id)
|
|
116
|
+
observer.next(current);
|
|
62
117
|
});
|
|
63
118
|
// subscribe to deleted events
|
|
64
119
|
const deleted = this.database.deleted.subscribe((event) => {
|
|
65
|
-
if (
|
|
120
|
+
if (current?.id === event.id) {
|
|
66
121
|
this.database.removeClaim(current, observer);
|
|
67
122
|
current = undefined;
|
|
68
123
|
observer.next(undefined);
|
|
69
124
|
}
|
|
70
125
|
});
|
|
71
126
|
return () => {
|
|
72
|
-
inserted.unsubscribe();
|
|
73
127
|
deleted.unsubscribe();
|
|
74
128
|
updated.unsubscribe();
|
|
129
|
+
inserted.unsubscribe();
|
|
75
130
|
if (current)
|
|
76
131
|
this.database.removeClaim(current, observer);
|
|
77
132
|
};
|
|
78
133
|
});
|
|
79
134
|
}
|
|
80
135
|
/** Creates an observable that subscribes to multiple events */
|
|
81
|
-
events(
|
|
136
|
+
events(ids) {
|
|
82
137
|
return new Observable((observer) => {
|
|
83
138
|
const events = new Map();
|
|
84
|
-
for (const
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
events.set(
|
|
88
|
-
this.database.claimEvent(
|
|
139
|
+
for (const id of ids) {
|
|
140
|
+
const event = this.getEvent(id);
|
|
141
|
+
if (event) {
|
|
142
|
+
events.set(id, event);
|
|
143
|
+
this.database.claimEvent(event, observer);
|
|
89
144
|
}
|
|
90
145
|
}
|
|
91
146
|
observer.next(events);
|
|
92
147
|
// subscribe to future events
|
|
93
148
|
const inserted = this.database.inserted.subscribe((event) => {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.database.removeClaim(current, observer);
|
|
101
|
-
events.set(uid, event);
|
|
102
|
-
observer.next(events);
|
|
103
|
-
// claim new event
|
|
104
|
-
this.database.claimEvent(event, observer);
|
|
105
|
-
}
|
|
149
|
+
const id = event.id;
|
|
150
|
+
if (ids.includes(id) && !events.has(id)) {
|
|
151
|
+
events.set(id, event);
|
|
152
|
+
observer.next(events);
|
|
153
|
+
// claim new event
|
|
154
|
+
this.database.claimEvent(event, observer);
|
|
106
155
|
}
|
|
107
156
|
});
|
|
108
|
-
// subscribe to
|
|
157
|
+
// subscribe to updated events
|
|
109
158
|
const updated = this.database.updated.subscribe((event) => {
|
|
110
|
-
|
|
111
|
-
if (uids.includes(uid))
|
|
159
|
+
if (ids.includes(event.id))
|
|
112
160
|
observer.next(events);
|
|
113
161
|
});
|
|
114
162
|
// subscribe to deleted events
|
|
115
163
|
const deleted = this.database.deleted.subscribe((event) => {
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
const current = events.get(
|
|
164
|
+
const id = event.id;
|
|
165
|
+
if (ids.includes(id)) {
|
|
166
|
+
const current = events.get(id);
|
|
119
167
|
if (current) {
|
|
120
168
|
this.database.removeClaim(current, observer);
|
|
121
|
-
events.delete(
|
|
169
|
+
events.delete(id);
|
|
122
170
|
observer.next(events);
|
|
123
171
|
}
|
|
124
172
|
}
|
|
@@ -133,9 +181,49 @@ export class EventStore {
|
|
|
133
181
|
};
|
|
134
182
|
});
|
|
135
183
|
}
|
|
136
|
-
/** Creates an observable
|
|
184
|
+
/** Creates an observable with the latest version of a replaceable event */
|
|
137
185
|
replaceable(kind, pubkey, d) {
|
|
138
|
-
return
|
|
186
|
+
return new Observable((observer) => {
|
|
187
|
+
const uid = getReplaceableUID(kind, pubkey, d);
|
|
188
|
+
// get latest version
|
|
189
|
+
let current = this.database.getReplaceable(kind, pubkey, d)?.[0];
|
|
190
|
+
if (current) {
|
|
191
|
+
observer.next(current);
|
|
192
|
+
this.database.claimEvent(current, observer);
|
|
193
|
+
}
|
|
194
|
+
// subscribe to future events
|
|
195
|
+
const inserted = this.database.inserted.subscribe((event) => {
|
|
196
|
+
if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
|
|
197
|
+
// remove old claim
|
|
198
|
+
if (current)
|
|
199
|
+
this.database.removeClaim(current, observer);
|
|
200
|
+
current = event;
|
|
201
|
+
observer.next(event);
|
|
202
|
+
// claim new event
|
|
203
|
+
this.database.claimEvent(current, observer);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// subscribe to updated events
|
|
207
|
+
const updated = this.database.updated.subscribe((event) => {
|
|
208
|
+
if (event === current)
|
|
209
|
+
observer.next(event);
|
|
210
|
+
});
|
|
211
|
+
// subscribe to deleted events
|
|
212
|
+
const deleted = this.database.deleted.subscribe((event) => {
|
|
213
|
+
if (getEventUID(event) === uid && event === current) {
|
|
214
|
+
this.database.removeClaim(current, observer);
|
|
215
|
+
current = undefined;
|
|
216
|
+
observer.next(undefined);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return () => {
|
|
220
|
+
inserted.unsubscribe();
|
|
221
|
+
deleted.unsubscribe();
|
|
222
|
+
updated.unsubscribe();
|
|
223
|
+
if (current)
|
|
224
|
+
this.database.removeClaim(current, observer);
|
|
225
|
+
};
|
|
226
|
+
});
|
|
139
227
|
}
|
|
140
228
|
/** Creates an observable that streams all events that match the filter */
|
|
141
229
|
stream(filters) {
|
|
@@ -165,60 +253,58 @@ export class EventStore {
|
|
|
165
253
|
});
|
|
166
254
|
}
|
|
167
255
|
/** Creates an observable that updates with an array of sorted events */
|
|
168
|
-
timeline(filters) {
|
|
256
|
+
timeline(filters, keepOldVersions = this.keepOldVersions) {
|
|
169
257
|
return new Observable((observer) => {
|
|
170
258
|
const seen = new Map();
|
|
171
259
|
const timeline = [];
|
|
172
|
-
//
|
|
173
|
-
const
|
|
174
|
-
|
|
260
|
+
// NOTE: only call this if we know the event is in timeline
|
|
261
|
+
const removeFromTimeline = (event) => {
|
|
262
|
+
timeline.splice(timeline.indexOf(event), 1);
|
|
263
|
+
if (!keepOldVersions && isReplaceable(event.kind))
|
|
264
|
+
seen.delete(getEventUID(event));
|
|
265
|
+
this.database.removeClaim(event, observer);
|
|
266
|
+
};
|
|
267
|
+
// inserts an event into the timeline and handles replaceable events
|
|
268
|
+
const insertIntoTimeline = (event) => {
|
|
269
|
+
// remove old versions
|
|
270
|
+
if (!keepOldVersions && isReplaceable(event.kind)) {
|
|
271
|
+
const uid = getEventUID(event);
|
|
272
|
+
const old = seen.get(uid);
|
|
273
|
+
if (old) {
|
|
274
|
+
if (event.created_at > old.created_at)
|
|
275
|
+
removeFromTimeline(old);
|
|
276
|
+
else
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
seen.set(uid, event);
|
|
280
|
+
}
|
|
281
|
+
// insert into timeline
|
|
175
282
|
insertEventIntoDescendingList(timeline, event);
|
|
176
283
|
this.database.claimEvent(event, observer);
|
|
177
|
-
|
|
178
|
-
|
|
284
|
+
};
|
|
285
|
+
// build initial timeline
|
|
286
|
+
const events = this.database.getForFilters(filters);
|
|
287
|
+
for (const event of events)
|
|
288
|
+
insertIntoTimeline(event);
|
|
179
289
|
observer.next([...timeline]);
|
|
180
290
|
// subscribe to future events
|
|
181
291
|
const inserted = this.database.inserted.subscribe((event) => {
|
|
182
292
|
if (matchFilters(filters, event)) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (current) {
|
|
186
|
-
if (event.created_at > current.created_at) {
|
|
187
|
-
// replace event
|
|
188
|
-
timeline.splice(timeline.indexOf(current), 1, event);
|
|
189
|
-
observer.next([...timeline]);
|
|
190
|
-
// update the claim
|
|
191
|
-
seen.set(uid, event);
|
|
192
|
-
this.database.removeClaim(current, observer);
|
|
193
|
-
this.database.claimEvent(event, observer);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
insertEventIntoDescendingList(timeline, event);
|
|
198
|
-
observer.next([...timeline]);
|
|
199
|
-
// claim new event
|
|
200
|
-
this.database.claimEvent(event, observer);
|
|
201
|
-
seen.set(getEventUID(event), event);
|
|
202
|
-
}
|
|
293
|
+
insertIntoTimeline(event);
|
|
294
|
+
observer.next([...timeline]);
|
|
203
295
|
}
|
|
204
296
|
});
|
|
205
|
-
// subscribe to
|
|
297
|
+
// subscribe to updated events
|
|
206
298
|
const updated = this.database.updated.subscribe((event) => {
|
|
207
|
-
if (
|
|
299
|
+
if (timeline.includes(event)) {
|
|
208
300
|
observer.next([...timeline]);
|
|
209
301
|
}
|
|
210
302
|
});
|
|
211
|
-
// subscribe to
|
|
303
|
+
// subscribe to deleted events
|
|
212
304
|
const deleted = this.database.deleted.subscribe((event) => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (current) {
|
|
216
|
-
// remove the event
|
|
217
|
-
timeline.splice(timeline.indexOf(current), 1);
|
|
305
|
+
if (timeline.includes(event)) {
|
|
306
|
+
removeFromTimeline(event);
|
|
218
307
|
observer.next([...timeline]);
|
|
219
|
-
// remove the claim
|
|
220
|
-
seen.delete(uid);
|
|
221
|
-
this.database.removeClaim(current, observer);
|
|
222
308
|
}
|
|
223
309
|
});
|
|
224
310
|
return () => {
|
|
@@ -226,9 +312,10 @@ export class EventStore {
|
|
|
226
312
|
deleted.unsubscribe();
|
|
227
313
|
updated.unsubscribe();
|
|
228
314
|
// remove all claims
|
|
229
|
-
for (const
|
|
315
|
+
for (const event of timeline) {
|
|
230
316
|
this.database.removeClaim(event, observer);
|
|
231
317
|
}
|
|
318
|
+
// forget seen replaceable events
|
|
232
319
|
seen.clear();
|
|
233
320
|
};
|
|
234
321
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EventTemplate, NostrEvent } from "nostr-tools";
|
|
2
|
+
export declare function getDeleteIds(deleteEvent: NostrEvent): string[];
|
|
3
|
+
export declare function getDeleteCoordinates(deleteEvent: NostrEvent): string[];
|
|
4
|
+
/** Creates a NIP-09 delete event for an array of events */
|
|
5
|
+
export declare function createDeleteEvent(events: NostrEvent[], message?: string): EventTemplate;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { kinds } from "nostr-tools";
|
|
2
|
+
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
3
|
+
import { isATag, isETag } from "./tags.js";
|
|
4
|
+
import { unixNow } from "./time.js";
|
|
5
|
+
import { getATagFromAddressPointer, getETagFromEventPointer } from "./pointers.js";
|
|
6
|
+
import { getTagValue } from "./index.js";
|
|
7
|
+
export function getDeleteIds(deleteEvent) {
|
|
8
|
+
return deleteEvent.tags.filter(isETag).map((t) => t[1]);
|
|
9
|
+
}
|
|
10
|
+
export function getDeleteCoordinates(deleteEvent) {
|
|
11
|
+
return deleteEvent.tags.filter(isATag).map((t) => t[1]);
|
|
12
|
+
}
|
|
13
|
+
/** Creates a NIP-09 delete event for an array of events */
|
|
14
|
+
export function createDeleteEvent(events, message) {
|
|
15
|
+
const eventPointers = [];
|
|
16
|
+
const addressPointers = [];
|
|
17
|
+
const eventKinds = new Set();
|
|
18
|
+
for (const event of events) {
|
|
19
|
+
eventKinds.add(event.kind);
|
|
20
|
+
eventPointers.push({ id: event.id });
|
|
21
|
+
if (isParameterizedReplaceableKind(event.kind)) {
|
|
22
|
+
const identifier = getTagValue(event, "d");
|
|
23
|
+
if (!identifier)
|
|
24
|
+
throw new Error("Event missing identifier");
|
|
25
|
+
addressPointers.push({ pubkey: event.pubkey, kind: event.kind, identifier });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
kind: kinds.EventDeletion,
|
|
30
|
+
content: message ?? "",
|
|
31
|
+
tags: [
|
|
32
|
+
...eventPointers.map(getETagFromEventPointer),
|
|
33
|
+
...addressPointers.map(getATagFromAddressPointer),
|
|
34
|
+
...Array.from(eventKinds).map((k) => ["k", String(k)]),
|
|
35
|
+
],
|
|
36
|
+
created_at: unixNow(),
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/helpers/filter.d.ts
CHANGED
|
@@ -6,7 +6,5 @@ import { Filter, NostrEvent } from "nostr-tools";
|
|
|
6
6
|
export declare function matchFilter(filter: Filter, event: NostrEvent): boolean;
|
|
7
7
|
/** Copied from nostr-tools */
|
|
8
8
|
export declare function matchFilters(filters: Filter[], event: NostrEvent): boolean;
|
|
9
|
-
/** Stringify filters in a predictable way */
|
|
10
|
-
export declare function stringifyFilter(filter: Filter | Filter[]): string;
|
|
11
9
|
/** Check if two filters are equal */
|
|
12
10
|
export declare function isFilterEqual(a: Filter | Filter[], b: Filter | Filter[]): boolean;
|
package/dist/helpers/filter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getIndexableTags } from "./event.js";
|
|
2
|
-
import
|
|
2
|
+
import equal from "fast-deep-equal";
|
|
3
3
|
/**
|
|
4
4
|
* Copied from nostr-tools and modified to use getIndexableTags
|
|
5
5
|
* @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
|
|
@@ -40,11 +40,7 @@ export function matchFilters(filters, event) {
|
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
-
/** Stringify filters in a predictable way */
|
|
44
|
-
export function stringifyFilter(filter) {
|
|
45
|
-
return stringify(filter);
|
|
46
|
-
}
|
|
47
43
|
/** Check if two filters are equal */
|
|
48
44
|
export function isFilterEqual(a, b) {
|
|
49
|
-
return
|
|
45
|
+
return equal(a, b);
|
|
50
46
|
}
|
package/dist/helpers/index.d.ts
CHANGED
package/dist/helpers/index.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
export function getValue(observable) {
|
|
3
|
+
if (observable instanceof BehaviorSubject)
|
|
4
|
+
return observable.value;
|
|
5
|
+
if (Reflect.has(observable, "value"))
|
|
6
|
+
return Reflect.get(observable, "value");
|
|
7
|
+
return new Promise((res) => {
|
|
8
|
+
const sub = observable.subscribe((v) => {
|
|
9
|
+
res(v);
|
|
10
|
+
sub.unsubscribe();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./get-value.js";
|
|
2
2
|
export * from "./share-latest-value.js";
|
package/dist/observable/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./get-value.js";
|
|
2
2
|
export * from "./share-latest-value.js";
|
package/dist/queries/simple.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Query } from "../query-store/index.js";
|
|
3
3
|
/** Creates a Query that returns a single event or undefined */
|
|
4
|
-
export declare function SingleEventQuery(
|
|
4
|
+
export declare function SingleEventQuery(id: string): Query<NostrEvent | undefined>;
|
|
5
5
|
/** Creates a Query that returns a multiple events in a map */
|
|
6
|
-
export declare function MultipleEventsQuery(
|
|
6
|
+
export declare function MultipleEventsQuery(ids: string[]): Query<Map<string, NostrEvent>>;
|
|
7
7
|
/** Creates a Query returning the latest version of a replaceable event */
|
|
8
8
|
export declare function ReplaceableQuery(kind: number, pubkey: string, d?: string): Query<NostrEvent | undefined>;
|
|
9
9
|
/** Creates a Query that returns an array of sorted events matching the filters */
|
|
10
|
-
export declare function TimelineQuery(filters: Filter | Filter[]): Query<NostrEvent[]>;
|
|
10
|
+
export declare function TimelineQuery(filters: Filter | Filter[], keepOldVersions?: boolean): Query<NostrEvent[]>;
|
|
11
11
|
/** Creates a Query that returns a directory of events by their UID */
|
|
12
12
|
export declare function ReplaceableSetQuery(pointers: {
|
|
13
13
|
kind: number;
|
package/dist/queries/simple.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import stringify from "json-stringify-deterministic";
|
|
2
1
|
import { getReplaceableUID } from "../helpers/event.js";
|
|
2
|
+
import hash_sum from "hash-sum";
|
|
3
3
|
/** Creates a Query that returns a single event or undefined */
|
|
4
|
-
export function SingleEventQuery(
|
|
4
|
+
export function SingleEventQuery(id) {
|
|
5
5
|
return {
|
|
6
|
-
key:
|
|
7
|
-
run: (events) => events.event(
|
|
6
|
+
key: id,
|
|
7
|
+
run: (events) => events.event(id),
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
/** Creates a Query that returns a multiple events in a map */
|
|
11
|
-
export function MultipleEventsQuery(
|
|
11
|
+
export function MultipleEventsQuery(ids) {
|
|
12
12
|
return {
|
|
13
|
-
key:
|
|
14
|
-
run: (events) => events.events(
|
|
13
|
+
key: ids.join(","),
|
|
14
|
+
run: (events) => events.events(ids),
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
/** Creates a Query returning the latest version of a replaceable event */
|
|
@@ -22,17 +22,17 @@ export function ReplaceableQuery(kind, pubkey, d) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
/** Creates a Query that returns an array of sorted events matching the filters */
|
|
25
|
-
export function TimelineQuery(filters) {
|
|
25
|
+
export function TimelineQuery(filters, keepOldVersions) {
|
|
26
26
|
return {
|
|
27
|
-
key:
|
|
28
|
-
run: (events) => events.timeline(Array.isArray(filters) ? filters : [filters]),
|
|
27
|
+
key: hash_sum(filters) + (keepOldVersions ? "-history" : ""),
|
|
28
|
+
run: (events) => events.timeline(Array.isArray(filters) ? filters : [filters], keepOldVersions),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
/** Creates a Query that returns a directory of events by their UID */
|
|
32
32
|
export function ReplaceableSetQuery(pointers) {
|
|
33
33
|
const cords = pointers.map((pointer) => getReplaceableUID(pointer.kind, pointer.pubkey, pointer.identifier));
|
|
34
34
|
return {
|
|
35
|
-
key:
|
|
35
|
+
key: hash_sum(pointers),
|
|
36
36
|
run: (events) => events.events(cords),
|
|
37
37
|
};
|
|
38
38
|
}
|
|
@@ -32,7 +32,7 @@ export declare class QueryStore {
|
|
|
32
32
|
identifier?: string;
|
|
33
33
|
}[]): Observable<Map<string, import("nostr-tools").Event>>;
|
|
34
34
|
/** Returns an array of events that match the filter */
|
|
35
|
-
timeline(filters: Filter | Filter[]): Observable<import("nostr-tools").Event[]>;
|
|
35
|
+
timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<import("nostr-tools").Event[]>;
|
|
36
36
|
/** Returns the parsed profile (0) for a pubkey */
|
|
37
37
|
profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
|
|
38
38
|
/** Returns all reactions for an event (supports replaceable events) */
|
|
@@ -38,8 +38,8 @@ export class QueryStore {
|
|
|
38
38
|
return this.runQuery(Queries.ReplaceableSetQuery)(pointers);
|
|
39
39
|
}
|
|
40
40
|
/** Returns an array of events that match the filter */
|
|
41
|
-
timeline(filters) {
|
|
42
|
-
return this.runQuery(Queries.TimelineQuery)(filters);
|
|
41
|
+
timeline(filters, keepOldVersions) {
|
|
42
|
+
return this.runQuery(Queries.TimelineQuery)(filters, keepOldVersions);
|
|
43
43
|
}
|
|
44
44
|
/** Returns the parsed profile (0) for a pubkey */
|
|
45
45
|
profile(pubkey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-core",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20241126184016",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@scure/base": "^1.1.9",
|
|
56
56
|
"debug": "^4.3.7",
|
|
57
|
-
"
|
|
57
|
+
"fast-deep-equal": "^3.1.3",
|
|
58
|
+
"hash-sum": "^2.0.0",
|
|
58
59
|
"light-bolt11-decoder": "^3.2.0",
|
|
59
60
|
"nanoid": "^5.0.7",
|
|
60
61
|
"nostr-tools": "^2.10.3",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@jest/globals": "^29.7.0",
|
|
65
66
|
"@types/debug": "^4.1.12",
|
|
67
|
+
"@types/hash-sum": "^1.0.2",
|
|
66
68
|
"@types/jest": "^29.5.13",
|
|
67
69
|
"jest": "^29.7.0",
|
|
68
70
|
"jest-extended": "^4.0.2",
|