applesauce-core 0.0.0-next-20241125223343 → 0.0.0-next-20241126192236
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 +23 -12
- package/dist/event-store/event-store.js +186 -79
- package/dist/helpers/filter.d.ts +0 -2
- package/dist/helpers/filter.js +2 -6
- 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 +12 -13
- 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,28 +3,39 @@ 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;
|
|
9
11
|
protected deletedIds: Set<string>;
|
|
10
12
|
protected deletedCoords: Map<string, number>;
|
|
11
13
|
protected handleDeleteEvent(deleteEvent: NostrEvent): void;
|
|
12
14
|
protected checkDeleted(event: NostrEvent): boolean;
|
|
13
15
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
14
|
-
update(event: NostrEvent):
|
|
15
|
-
getAll(filters: Filter[]): Set<
|
|
16
|
-
hasEvent(uid: string):
|
|
17
|
-
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;
|
|
18
20
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
19
|
-
|
|
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;
|
|
20
25
|
/** Creates an observable that updates a single event */
|
|
21
|
-
event(
|
|
26
|
+
event(id: string): Observable<NostrEvent | undefined>;
|
|
22
27
|
/** Creates an observable that subscribes to multiple events */
|
|
23
|
-
events(
|
|
24
|
-
/** Creates an observable
|
|
25
|
-
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>;
|
|
31
|
+
/** Creates an observable with the latest versions of replaceable events */
|
|
32
|
+
replaceableSet(pointers: {
|
|
33
|
+
kind: number;
|
|
34
|
+
pubkey: string;
|
|
35
|
+
identifier?: string;
|
|
36
|
+
}[]): Observable<Map<string, NostrEvent>>;
|
|
26
37
|
/** Creates an observable that streams all events that match the filter */
|
|
27
|
-
stream(filters: Filter[]): Observable<
|
|
38
|
+
stream(filters: Filter[]): Observable<NostrEvent>;
|
|
28
39
|
/** Creates an observable that updates with an array of sorted events */
|
|
29
|
-
timeline(filters: Filter[]): Observable<
|
|
40
|
+
timeline(filters: Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
|
|
30
41
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { kinds } from "nostr-tools";
|
|
2
2
|
import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
3
|
+
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
3
4
|
import { Observable } from "rxjs";
|
|
4
5
|
import { Database } from "./database.js";
|
|
5
|
-
import { getEventUID, getReplaceableUID } from "../helpers/event.js";
|
|
6
|
+
import { getEventUID, getReplaceableUID, getTagValue, isReplaceable } from "../helpers/event.js";
|
|
6
7
|
import { matchFilters } from "../helpers/filter.js";
|
|
7
8
|
import { addSeenRelay } from "../helpers/relays.js";
|
|
8
9
|
import { getDeleteIds } from "../helpers/delete.js";
|
|
9
|
-
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
10
10
|
export class EventStore {
|
|
11
11
|
database;
|
|
12
|
+
/** Whether to keep old versions of replaceable events */
|
|
13
|
+
keepOldVersions = false;
|
|
12
14
|
constructor() {
|
|
13
15
|
this.database = new Database();
|
|
14
16
|
}
|
|
@@ -16,9 +18,24 @@ export class EventStore {
|
|
|
16
18
|
add(event, fromRelay) {
|
|
17
19
|
if (event.kind === kinds.EventDeletion)
|
|
18
20
|
this.handleDeleteEvent(event);
|
|
21
|
+
// ignore if the event was deleted
|
|
19
22
|
if (this.checkDeleted(event))
|
|
20
23
|
return event;
|
|
24
|
+
// insert event into database
|
|
21
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
|
|
22
39
|
if (fromRelay)
|
|
23
40
|
addSeenRelay(inserted, fromRelay);
|
|
24
41
|
return inserted;
|
|
@@ -69,13 +86,107 @@ export class EventStore {
|
|
|
69
86
|
hasReplaceable(kind, pubkey, d) {
|
|
70
87
|
return this.database.hasReplaceable(kind, pubkey, d);
|
|
71
88
|
}
|
|
89
|
+
/** Gets the latest version of a replaceable event */
|
|
72
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) {
|
|
73
95
|
return this.database.getReplaceable(kind, pubkey, d);
|
|
74
96
|
}
|
|
75
97
|
/** Creates an observable that updates a single event */
|
|
76
|
-
event(
|
|
98
|
+
event(id) {
|
|
99
|
+
return new Observable((observer) => {
|
|
100
|
+
let current = this.database.getEvent(id);
|
|
101
|
+
if (current) {
|
|
102
|
+
observer.next(current);
|
|
103
|
+
this.database.claimEvent(current, observer);
|
|
104
|
+
}
|
|
105
|
+
// subscribe to future events
|
|
106
|
+
const inserted = this.database.inserted.subscribe((event) => {
|
|
107
|
+
if (event.id === id) {
|
|
108
|
+
current = event;
|
|
109
|
+
observer.next(event);
|
|
110
|
+
this.database.claimEvent(event, observer);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// subscribe to updated events
|
|
114
|
+
const updated = this.database.updated.subscribe((event) => {
|
|
115
|
+
if (event.id === id)
|
|
116
|
+
observer.next(current);
|
|
117
|
+
});
|
|
118
|
+
// subscribe to deleted events
|
|
119
|
+
const deleted = this.database.deleted.subscribe((event) => {
|
|
120
|
+
if (current?.id === event.id) {
|
|
121
|
+
this.database.removeClaim(current, observer);
|
|
122
|
+
current = undefined;
|
|
123
|
+
observer.next(undefined);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return () => {
|
|
127
|
+
deleted.unsubscribe();
|
|
128
|
+
updated.unsubscribe();
|
|
129
|
+
inserted.unsubscribe();
|
|
130
|
+
if (current)
|
|
131
|
+
this.database.removeClaim(current, observer);
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Creates an observable that subscribes to multiple events */
|
|
136
|
+
events(ids) {
|
|
137
|
+
return new Observable((observer) => {
|
|
138
|
+
const events = new Map();
|
|
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);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
observer.next(events);
|
|
147
|
+
// subscribe to future events
|
|
148
|
+
const inserted = this.database.inserted.subscribe((event) => {
|
|
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);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// subscribe to updated events
|
|
158
|
+
const updated = this.database.updated.subscribe((event) => {
|
|
159
|
+
if (ids.includes(event.id))
|
|
160
|
+
observer.next(events);
|
|
161
|
+
});
|
|
162
|
+
// subscribe to deleted events
|
|
163
|
+
const deleted = this.database.deleted.subscribe((event) => {
|
|
164
|
+
const id = event.id;
|
|
165
|
+
if (ids.includes(id)) {
|
|
166
|
+
const current = events.get(id);
|
|
167
|
+
if (current) {
|
|
168
|
+
this.database.removeClaim(current, observer);
|
|
169
|
+
events.delete(id);
|
|
170
|
+
observer.next(events);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return () => {
|
|
175
|
+
inserted.unsubscribe();
|
|
176
|
+
deleted.unsubscribe();
|
|
177
|
+
updated.unsubscribe();
|
|
178
|
+
for (const [_uid, event] of events) {
|
|
179
|
+
this.database.removeClaim(event, observer);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Creates an observable with the latest version of a replaceable event */
|
|
185
|
+
replaceable(kind, pubkey, d) {
|
|
77
186
|
return new Observable((observer) => {
|
|
78
|
-
|
|
187
|
+
const uid = getReplaceableUID(kind, pubkey, d);
|
|
188
|
+
// get latest version
|
|
189
|
+
let current = this.database.getReplaceable(kind, pubkey, d)?.[0];
|
|
79
190
|
if (current) {
|
|
80
191
|
observer.next(current);
|
|
81
192
|
this.database.claimEvent(current, observer);
|
|
@@ -92,14 +203,14 @@ export class EventStore {
|
|
|
92
203
|
this.database.claimEvent(current, observer);
|
|
93
204
|
}
|
|
94
205
|
});
|
|
95
|
-
// subscribe to
|
|
206
|
+
// subscribe to updated events
|
|
96
207
|
const updated = this.database.updated.subscribe((event) => {
|
|
97
208
|
if (event === current)
|
|
98
209
|
observer.next(event);
|
|
99
210
|
});
|
|
100
211
|
// subscribe to deleted events
|
|
101
212
|
const deleted = this.database.deleted.subscribe((event) => {
|
|
102
|
-
if (getEventUID(event) === uid && current) {
|
|
213
|
+
if (getEventUID(event) === uid && event === current) {
|
|
103
214
|
this.database.removeClaim(current, observer);
|
|
104
215
|
current = undefined;
|
|
105
216
|
observer.next(undefined);
|
|
@@ -114,66 +225,63 @@ export class EventStore {
|
|
|
114
225
|
};
|
|
115
226
|
});
|
|
116
227
|
}
|
|
117
|
-
/** Creates an observable
|
|
118
|
-
|
|
228
|
+
/** Creates an observable with the latest versions of replaceable events */
|
|
229
|
+
replaceableSet(pointers) {
|
|
119
230
|
return new Observable((observer) => {
|
|
231
|
+
const coords = pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier));
|
|
120
232
|
const events = new Map();
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
233
|
+
const handleEvent = (event) => {
|
|
234
|
+
const uid = getEventUID(event);
|
|
235
|
+
const current = events.get(uid);
|
|
236
|
+
if (current) {
|
|
237
|
+
if (event.created_at > current.created_at) {
|
|
238
|
+
this.database.removeClaim(current, observer);
|
|
239
|
+
}
|
|
240
|
+
else
|
|
241
|
+
return;
|
|
126
242
|
}
|
|
243
|
+
events.set(uid, event);
|
|
244
|
+
this.database.claimEvent(event, observer);
|
|
245
|
+
};
|
|
246
|
+
// get latest version
|
|
247
|
+
for (const pointer of pointers) {
|
|
248
|
+
const events = this.database.getReplaceable(pointer.kind, pointer.pubkey, pointer.identifier);
|
|
249
|
+
if (events)
|
|
250
|
+
for (const event of events)
|
|
251
|
+
handleEvent(event);
|
|
127
252
|
}
|
|
128
|
-
observer.next(events);
|
|
129
253
|
// subscribe to future events
|
|
130
254
|
const inserted = this.database.inserted.subscribe((event) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// remove old claim
|
|
135
|
-
if (!current || event.created_at > current.created_at) {
|
|
136
|
-
if (current)
|
|
137
|
-
this.database.removeClaim(current, observer);
|
|
138
|
-
events.set(uid, event);
|
|
139
|
-
observer.next(events);
|
|
140
|
-
// claim new event
|
|
141
|
-
this.database.claimEvent(event, observer);
|
|
142
|
-
}
|
|
255
|
+
if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) {
|
|
256
|
+
handleEvent(event);
|
|
257
|
+
observer.next(events);
|
|
143
258
|
}
|
|
144
259
|
});
|
|
145
|
-
// subscribe to
|
|
260
|
+
// subscribe to updated events
|
|
146
261
|
const updated = this.database.updated.subscribe((event) => {
|
|
147
|
-
|
|
148
|
-
if (uids.includes(uid))
|
|
262
|
+
if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) {
|
|
149
263
|
observer.next(events);
|
|
264
|
+
}
|
|
150
265
|
});
|
|
151
266
|
// subscribe to deleted events
|
|
152
267
|
const deleted = this.database.deleted.subscribe((event) => {
|
|
153
268
|
const uid = getEventUID(event);
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
events.delete(uid);
|
|
159
|
-
observer.next(events);
|
|
160
|
-
}
|
|
269
|
+
if (events.has(uid)) {
|
|
270
|
+
events.delete(uid);
|
|
271
|
+
this.database.removeClaim(event, observer);
|
|
272
|
+
observer.next(events);
|
|
161
273
|
}
|
|
162
274
|
});
|
|
163
275
|
return () => {
|
|
164
276
|
inserted.unsubscribe();
|
|
165
277
|
deleted.unsubscribe();
|
|
166
278
|
updated.unsubscribe();
|
|
167
|
-
for (const [
|
|
279
|
+
for (const [_id, event] of events) {
|
|
168
280
|
this.database.removeClaim(event, observer);
|
|
169
281
|
}
|
|
170
282
|
};
|
|
171
283
|
});
|
|
172
284
|
}
|
|
173
|
-
/** Creates an observable that updates a single replaceable event */
|
|
174
|
-
replaceable(kind, pubkey, d) {
|
|
175
|
-
return this.event(getReplaceableUID(kind, pubkey, d));
|
|
176
|
-
}
|
|
177
285
|
/** Creates an observable that streams all events that match the filter */
|
|
178
286
|
stream(filters) {
|
|
179
287
|
return new Observable((observer) => {
|
|
@@ -202,60 +310,58 @@ export class EventStore {
|
|
|
202
310
|
});
|
|
203
311
|
}
|
|
204
312
|
/** Creates an observable that updates with an array of sorted events */
|
|
205
|
-
timeline(filters) {
|
|
313
|
+
timeline(filters, keepOldVersions = this.keepOldVersions) {
|
|
206
314
|
return new Observable((observer) => {
|
|
207
315
|
const seen = new Map();
|
|
208
316
|
const timeline = [];
|
|
209
|
-
//
|
|
210
|
-
const
|
|
211
|
-
|
|
317
|
+
// NOTE: only call this if we know the event is in timeline
|
|
318
|
+
const removeFromTimeline = (event) => {
|
|
319
|
+
timeline.splice(timeline.indexOf(event), 1);
|
|
320
|
+
if (!keepOldVersions && isReplaceable(event.kind))
|
|
321
|
+
seen.delete(getEventUID(event));
|
|
322
|
+
this.database.removeClaim(event, observer);
|
|
323
|
+
};
|
|
324
|
+
// inserts an event into the timeline and handles replaceable events
|
|
325
|
+
const insertIntoTimeline = (event) => {
|
|
326
|
+
// remove old versions
|
|
327
|
+
if (!keepOldVersions && isReplaceable(event.kind)) {
|
|
328
|
+
const uid = getEventUID(event);
|
|
329
|
+
const old = seen.get(uid);
|
|
330
|
+
if (old) {
|
|
331
|
+
if (event.created_at > old.created_at)
|
|
332
|
+
removeFromTimeline(old);
|
|
333
|
+
else
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
seen.set(uid, event);
|
|
337
|
+
}
|
|
338
|
+
// insert into timeline
|
|
212
339
|
insertEventIntoDescendingList(timeline, event);
|
|
213
340
|
this.database.claimEvent(event, observer);
|
|
214
|
-
|
|
215
|
-
|
|
341
|
+
};
|
|
342
|
+
// build initial timeline
|
|
343
|
+
const events = this.database.getForFilters(filters);
|
|
344
|
+
for (const event of events)
|
|
345
|
+
insertIntoTimeline(event);
|
|
216
346
|
observer.next([...timeline]);
|
|
217
347
|
// subscribe to future events
|
|
218
348
|
const inserted = this.database.inserted.subscribe((event) => {
|
|
219
349
|
if (matchFilters(filters, event)) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (current) {
|
|
223
|
-
if (event.created_at > current.created_at) {
|
|
224
|
-
// replace event
|
|
225
|
-
timeline.splice(timeline.indexOf(current), 1, event);
|
|
226
|
-
observer.next([...timeline]);
|
|
227
|
-
// update the claim
|
|
228
|
-
seen.set(uid, event);
|
|
229
|
-
this.database.removeClaim(current, observer);
|
|
230
|
-
this.database.claimEvent(event, observer);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
insertEventIntoDescendingList(timeline, event);
|
|
235
|
-
observer.next([...timeline]);
|
|
236
|
-
// claim new event
|
|
237
|
-
this.database.claimEvent(event, observer);
|
|
238
|
-
seen.set(getEventUID(event), event);
|
|
239
|
-
}
|
|
350
|
+
insertIntoTimeline(event);
|
|
351
|
+
observer.next([...timeline]);
|
|
240
352
|
}
|
|
241
353
|
});
|
|
242
|
-
// subscribe to
|
|
354
|
+
// subscribe to updated events
|
|
243
355
|
const updated = this.database.updated.subscribe((event) => {
|
|
244
|
-
if (
|
|
356
|
+
if (timeline.includes(event)) {
|
|
245
357
|
observer.next([...timeline]);
|
|
246
358
|
}
|
|
247
359
|
});
|
|
248
|
-
// subscribe to
|
|
360
|
+
// subscribe to deleted events
|
|
249
361
|
const deleted = this.database.deleted.subscribe((event) => {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (current) {
|
|
253
|
-
// remove the event
|
|
254
|
-
timeline.splice(timeline.indexOf(current), 1);
|
|
362
|
+
if (timeline.includes(event)) {
|
|
363
|
+
removeFromTimeline(event);
|
|
255
364
|
observer.next([...timeline]);
|
|
256
|
-
// remove the claim
|
|
257
|
-
seen.delete(uid);
|
|
258
|
-
this.database.removeClaim(current, observer);
|
|
259
365
|
}
|
|
260
366
|
});
|
|
261
367
|
return () => {
|
|
@@ -263,9 +369,10 @@ export class EventStore {
|
|
|
263
369
|
deleted.unsubscribe();
|
|
264
370
|
updated.unsubscribe();
|
|
265
371
|
// remove all claims
|
|
266
|
-
for (const
|
|
372
|
+
for (const event of timeline) {
|
|
267
373
|
this.database.removeClaim(event, observer);
|
|
268
374
|
}
|
|
375
|
+
// forget seen replaceable events
|
|
269
376
|
seen.clear();
|
|
270
377
|
};
|
|
271
378
|
});
|
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
|
}
|
|
@@ -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,16 @@ 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
|
-
const cords = pointers.map((pointer) => getReplaceableUID(pointer.kind, pointer.pubkey, pointer.identifier));
|
|
34
33
|
return {
|
|
35
|
-
key:
|
|
36
|
-
run: (events) => events.
|
|
34
|
+
key: hash_sum(pointers),
|
|
35
|
+
run: (events) => events.replaceableSet(pointers),
|
|
37
36
|
};
|
|
38
37
|
}
|
|
@@ -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-20241126192236",
|
|
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",
|