applesauce-core 0.0.0-next-20241103143210
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/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/event-store/common.d.ts +1 -0
- package/dist/event-store/common.js +2 -0
- package/dist/event-store/database.d.ts +60 -0
- package/dist/event-store/database.js +294 -0
- package/dist/event-store/event-store.d.ts +26 -0
- package/dist/event-store/event-store.js +236 -0
- package/dist/event-store/index.d.ts +2 -0
- package/dist/event-store/index.js +2 -0
- package/dist/helpers/bolt11.d.ts +8 -0
- package/dist/helpers/bolt11.js +14 -0
- package/dist/helpers/cache.d.ts +5 -0
- package/dist/helpers/cache.js +17 -0
- package/dist/helpers/emoji.d.ts +2 -0
- package/dist/helpers/emoji.js +4 -0
- package/dist/helpers/event.d.ts +34 -0
- package/dist/helpers/event.js +64 -0
- package/dist/helpers/filter.d.ts +12 -0
- package/dist/helpers/filter.js +50 -0
- package/dist/helpers/hashtag.d.ts +2 -0
- package/dist/helpers/hashtag.js +7 -0
- package/dist/helpers/index.d.ts +16 -0
- package/dist/helpers/index.js +16 -0
- package/dist/helpers/json.d.ts +1 -0
- package/dist/helpers/json.js +8 -0
- package/dist/helpers/lru.d.ts +32 -0
- package/dist/helpers/lru.js +148 -0
- package/dist/helpers/mailboxes.d.ts +17 -0
- package/dist/helpers/mailboxes.js +37 -0
- package/dist/helpers/mailboxes.test.d.ts +1 -0
- package/dist/helpers/mailboxes.test.js +80 -0
- package/dist/helpers/pointers.d.ts +22 -0
- package/dist/helpers/pointers.js +127 -0
- package/dist/helpers/profile.d.ts +25 -0
- package/dist/helpers/profile.js +28 -0
- package/dist/helpers/relays.d.ts +12 -0
- package/dist/helpers/relays.js +31 -0
- package/dist/helpers/string.d.ts +4 -0
- package/dist/helpers/string.js +13 -0
- package/dist/helpers/tags.d.ts +6 -0
- package/dist/helpers/tags.js +18 -0
- package/dist/helpers/threading.d.ts +55 -0
- package/dist/helpers/threading.js +73 -0
- package/dist/helpers/time.d.ts +2 -0
- package/dist/helpers/time.js +4 -0
- package/dist/helpers/url.d.ts +11 -0
- package/dist/helpers/url.js +29 -0
- package/dist/helpers/zap.d.ts +12 -0
- package/dist/helpers/zap.js +51 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +2 -0
- package/dist/observable/getValue.d.ts +2 -0
- package/dist/observable/getValue.js +13 -0
- package/dist/observable/index.d.ts +2 -0
- package/dist/observable/index.js +2 -0
- package/dist/observable/share-behavior.d.ts +2 -0
- package/dist/observable/share-behavior.js +7 -0
- package/dist/observable/share-latest-value.d.ts +8 -0
- package/dist/observable/share-latest-value.js +21 -0
- package/dist/observable/stateful.d.ts +10 -0
- package/dist/observable/stateful.js +60 -0
- package/dist/observable/throttle.d.ts +3 -0
- package/dist/observable/throttle.js +23 -0
- package/dist/promise/deferred.d.ts +5 -0
- package/dist/promise/deferred.js +14 -0
- package/dist/promise/index.d.ts +1 -0
- package/dist/promise/index.js +1 -0
- package/dist/queries/index.d.ts +6 -0
- package/dist/queries/index.js +6 -0
- package/dist/queries/mailboxes.d.ts +5 -0
- package/dist/queries/mailboxes.js +12 -0
- package/dist/queries/profile.d.ts +3 -0
- package/dist/queries/profile.js +11 -0
- package/dist/queries/reactions.d.ts +4 -0
- package/dist/queries/reactions.js +19 -0
- package/dist/queries/simple.d.ts +16 -0
- package/dist/queries/simple.js +38 -0
- package/dist/queries/thread.d.ts +23 -0
- package/dist/queries/thread.js +66 -0
- package/dist/queries/zaps.d.ts +4 -0
- package/dist/queries/zaps.js +16 -0
- package/dist/query-store/index.d.ts +47 -0
- package/dist/query-store/index.js +60 -0
- package/dist/utils/lru.d.ts +32 -0
- package/dist/utils/lru.js +148 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 hzrd149
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# applesauce-core
|
|
2
|
+
|
|
3
|
+
AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/modules/Queries)
|
|
4
|
+
|
|
5
|
+
# Example
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { EventStore, QueryStore } from "applesauce-core";
|
|
9
|
+
import { Relay } from "nostr-tools/relay";
|
|
10
|
+
|
|
11
|
+
// The EventStore handles all the events
|
|
12
|
+
const eventStore = new EventStore();
|
|
13
|
+
|
|
14
|
+
// The QueryStore handles queries and makes sure not to run multiple of the same query
|
|
15
|
+
const queryStore = new QueryStore(eventStore);
|
|
16
|
+
|
|
17
|
+
// Use nostr-tools or anything else to talk to relays
|
|
18
|
+
const relay = await Relay.connect("wss://relay.example.com");
|
|
19
|
+
|
|
20
|
+
const sub = relay.subscribe([{ authors: ["266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5"] }], {
|
|
21
|
+
onevent(event) {
|
|
22
|
+
eventStore.add(event);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// This will return an Observable<ProfileContent | undefined> of the parsed metadata
|
|
27
|
+
const profile = queryStore.profile("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
28
|
+
|
|
29
|
+
profile.subscribe((parsed) => {
|
|
30
|
+
if (parsed) console.log(parsed);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// This will return an Observable<NostrEvent[]> of all kind 1 events sorted by created_at
|
|
34
|
+
const timeline = queryStore.timeline({ kinds: [1] });
|
|
35
|
+
|
|
36
|
+
timeline.subscribe((events) => {
|
|
37
|
+
console.log(events);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const INDEXABLE_TAGS: Set<string>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Subject } from "rxjs";
|
|
3
|
+
import { LRU } from "../helpers/lru.js";
|
|
4
|
+
/**
|
|
5
|
+
* An in-memory database for nostr events
|
|
6
|
+
*/
|
|
7
|
+
export declare class Database {
|
|
8
|
+
protected log: import("debug").Debugger;
|
|
9
|
+
/** Indexes */
|
|
10
|
+
protected kinds: Map<number, Set<import("nostr-tools").Event>>;
|
|
11
|
+
protected authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
12
|
+
protected tags: LRU<Set<import("nostr-tools").Event>>;
|
|
13
|
+
protected created_at: NostrEvent[];
|
|
14
|
+
/** LRU cache of last events touched */
|
|
15
|
+
events: LRU<import("nostr-tools").Event>;
|
|
16
|
+
/** A stream of events inserted into the database */
|
|
17
|
+
inserted: Subject<import("nostr-tools").Event>;
|
|
18
|
+
/** A stream of events that have been updated */
|
|
19
|
+
updated: Subject<import("nostr-tools").Event>;
|
|
20
|
+
/** A stream of events removed of the database */
|
|
21
|
+
deleted: Subject<import("nostr-tools").Event>;
|
|
22
|
+
get size(): number;
|
|
23
|
+
protected claims: WeakMap<import("nostr-tools").Event, any>;
|
|
24
|
+
/** Index helper methods */
|
|
25
|
+
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
|
|
26
|
+
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
|
|
27
|
+
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
28
|
+
/** Moves an event to the top of the LRU cache */
|
|
29
|
+
touch(event: NostrEvent): void;
|
|
30
|
+
hasEvent(uid: string): import("nostr-tools").Event | undefined;
|
|
31
|
+
getEvent(uid: string): import("nostr-tools").Event | undefined;
|
|
32
|
+
/** Checks if the database contains a replaceable event without touching it */
|
|
33
|
+
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
34
|
+
/** Gets a replaceable event and touches it */
|
|
35
|
+
getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
|
|
36
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
37
|
+
addEvent(event: NostrEvent): import("nostr-tools").Event;
|
|
38
|
+
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
39
|
+
updateEvent(event: NostrEvent): import("nostr-tools").Event;
|
|
40
|
+
/** Deletes an event from the database and notifies all subscriptions */
|
|
41
|
+
deleteEvent(eventOrUID: string | NostrEvent): boolean;
|
|
42
|
+
/** Sets the claim on the event and touches it */
|
|
43
|
+
claimEvent(event: NostrEvent, claim: any): void;
|
|
44
|
+
/** Checks if an event is claimed by anything */
|
|
45
|
+
isClaimed(event: NostrEvent): boolean;
|
|
46
|
+
/** Removes a claim from an event */
|
|
47
|
+
removeClaim(event: NostrEvent, claim: any): void;
|
|
48
|
+
/** Removes all claims on an event */
|
|
49
|
+
clearClaim(event: NostrEvent): void;
|
|
50
|
+
iterateAuthors(authors: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
51
|
+
iterateTag(tag: string, values: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
52
|
+
iterateKinds(kinds: Iterable<number>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
53
|
+
iterateTime(since: number | undefined, until: number | undefined): Generator<never, Set<import("nostr-tools").Event>, unknown>;
|
|
54
|
+
iterateIds(ids: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
55
|
+
/** Returns all events that match the filter */
|
|
56
|
+
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
57
|
+
getForFilters(filters: Filter[]): Set<import("nostr-tools").Event>;
|
|
58
|
+
/** Remove the oldest events that are not claimed */
|
|
59
|
+
prune(limit?: number): number;
|
|
60
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
|
+
import { Subject } from "rxjs";
|
|
3
|
+
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID } from "../helpers/event.js";
|
|
4
|
+
import { INDEXABLE_TAGS } from "./common.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { LRU } from "../helpers/lru.js";
|
|
7
|
+
/**
|
|
8
|
+
* An in-memory database for nostr events
|
|
9
|
+
*/
|
|
10
|
+
export class Database {
|
|
11
|
+
log = logger.extend("Database");
|
|
12
|
+
/** Indexes */
|
|
13
|
+
kinds = new Map();
|
|
14
|
+
authors = new Map();
|
|
15
|
+
tags = new LRU();
|
|
16
|
+
created_at = [];
|
|
17
|
+
/** LRU cache of last events touched */
|
|
18
|
+
events = new LRU();
|
|
19
|
+
/** A stream of events inserted into the database */
|
|
20
|
+
inserted = new Subject();
|
|
21
|
+
/** A stream of events that have been updated */
|
|
22
|
+
updated = new Subject();
|
|
23
|
+
/** A stream of events removed of the database */
|
|
24
|
+
deleted = new Subject();
|
|
25
|
+
get size() {
|
|
26
|
+
return this.events.size;
|
|
27
|
+
}
|
|
28
|
+
claims = new WeakMap();
|
|
29
|
+
/** Index helper methods */
|
|
30
|
+
getKindIndex(kind) {
|
|
31
|
+
if (!this.kinds.has(kind))
|
|
32
|
+
this.kinds.set(kind, new Set());
|
|
33
|
+
return this.kinds.get(kind);
|
|
34
|
+
}
|
|
35
|
+
getAuthorsIndex(author) {
|
|
36
|
+
if (!this.authors.has(author))
|
|
37
|
+
this.authors.set(author, new Set());
|
|
38
|
+
return this.authors.get(author);
|
|
39
|
+
}
|
|
40
|
+
getTagIndex(tagAndValue) {
|
|
41
|
+
if (!this.tags.has(tagAndValue)) {
|
|
42
|
+
// build new tag index from existing events
|
|
43
|
+
const events = new Set();
|
|
44
|
+
const ts = Date.now();
|
|
45
|
+
for (const event of this.events.values()) {
|
|
46
|
+
if (getIndexableTags(event).has(tagAndValue)) {
|
|
47
|
+
events.add(event);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const took = Date.now() - ts;
|
|
51
|
+
if (took > 100)
|
|
52
|
+
this.log(`Built index ${tagAndValue} took ${took}ms`);
|
|
53
|
+
this.tags.set(tagAndValue, events);
|
|
54
|
+
}
|
|
55
|
+
return this.tags.get(tagAndValue);
|
|
56
|
+
}
|
|
57
|
+
/** Moves an event to the top of the LRU cache */
|
|
58
|
+
touch(event) {
|
|
59
|
+
this.events.set(getEventUID(event), event);
|
|
60
|
+
}
|
|
61
|
+
hasEvent(uid) {
|
|
62
|
+
return this.events.get(uid);
|
|
63
|
+
}
|
|
64
|
+
getEvent(uid) {
|
|
65
|
+
return this.events.get(uid);
|
|
66
|
+
}
|
|
67
|
+
/** Checks if the database contains a replaceable event without touching it */
|
|
68
|
+
hasReplaceable(kind, pubkey, d) {
|
|
69
|
+
return this.events.has(getReplaceableUID(kind, pubkey, d));
|
|
70
|
+
}
|
|
71
|
+
/** Gets a replaceable event and touches it */
|
|
72
|
+
getReplaceable(kind, pubkey, d) {
|
|
73
|
+
return this.events.get(getReplaceableUID(kind, pubkey, d));
|
|
74
|
+
}
|
|
75
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
76
|
+
addEvent(event) {
|
|
77
|
+
const uid = getEventUID(event);
|
|
78
|
+
const current = this.events.get(uid);
|
|
79
|
+
if (current && event.created_at <= current.created_at) {
|
|
80
|
+
// if this is a duplicate event, transfer some import symbols
|
|
81
|
+
if (current.id === event.id) {
|
|
82
|
+
if (event[FromCacheSymbol])
|
|
83
|
+
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
84
|
+
}
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
this.events.set(uid, event);
|
|
88
|
+
this.getKindIndex(event.kind).add(event);
|
|
89
|
+
this.getAuthorsIndex(event.pubkey).add(event);
|
|
90
|
+
for (const tag of getIndexableTags(event)) {
|
|
91
|
+
if (this.tags.has(tag)) {
|
|
92
|
+
this.getTagIndex(tag).add(event);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
insertEventIntoDescendingList(this.created_at, event);
|
|
96
|
+
this.inserted.next(event);
|
|
97
|
+
return event;
|
|
98
|
+
}
|
|
99
|
+
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
100
|
+
updateEvent(event) {
|
|
101
|
+
const inserted = this.addEvent(event);
|
|
102
|
+
this.updated.next(inserted);
|
|
103
|
+
return inserted;
|
|
104
|
+
}
|
|
105
|
+
/** Deletes an event from the database and notifies all subscriptions */
|
|
106
|
+
deleteEvent(eventOrUID) {
|
|
107
|
+
let event = typeof eventOrUID === "string" ? this.events.get(eventOrUID) : eventOrUID;
|
|
108
|
+
if (!event)
|
|
109
|
+
throw new Error("Missing event");
|
|
110
|
+
const uid = getEventUID(event);
|
|
111
|
+
// only remove events that are known
|
|
112
|
+
if (!this.events.has(uid))
|
|
113
|
+
return false;
|
|
114
|
+
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
115
|
+
this.getKindIndex(event.kind).delete(event);
|
|
116
|
+
for (const tag of getIndexableTags(event)) {
|
|
117
|
+
if (this.tags.has(tag)) {
|
|
118
|
+
this.getTagIndex(tag).delete(event);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// remove from created_at index
|
|
122
|
+
const i = this.created_at.indexOf(event);
|
|
123
|
+
this.created_at.splice(i, 1);
|
|
124
|
+
this.events.delete(uid);
|
|
125
|
+
this.deleted.next(event);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
/** Sets the claim on the event and touches it */
|
|
129
|
+
claimEvent(event, claim) {
|
|
130
|
+
if (!this.claims.has(event)) {
|
|
131
|
+
this.claims.set(event, claim);
|
|
132
|
+
}
|
|
133
|
+
// always touch event
|
|
134
|
+
this.touch(event);
|
|
135
|
+
}
|
|
136
|
+
/** Checks if an event is claimed by anything */
|
|
137
|
+
isClaimed(event) {
|
|
138
|
+
return this.claims.has(event);
|
|
139
|
+
}
|
|
140
|
+
/** Removes a claim from an event */
|
|
141
|
+
removeClaim(event, claim) {
|
|
142
|
+
const current = this.claims.get(event);
|
|
143
|
+
if (current === claim)
|
|
144
|
+
this.claims.delete(event);
|
|
145
|
+
}
|
|
146
|
+
/** Removes all claims on an event */
|
|
147
|
+
clearClaim(event) {
|
|
148
|
+
this.claims.delete(event);
|
|
149
|
+
}
|
|
150
|
+
*iterateAuthors(authors) {
|
|
151
|
+
for (const author of authors) {
|
|
152
|
+
const events = this.authors.get(author);
|
|
153
|
+
if (events) {
|
|
154
|
+
for (const event of events)
|
|
155
|
+
yield event;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
*iterateTag(tag, values) {
|
|
160
|
+
for (const value of values) {
|
|
161
|
+
const events = this.getTagIndex(tag + ":" + value);
|
|
162
|
+
if (events) {
|
|
163
|
+
for (const event of events)
|
|
164
|
+
yield event;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
*iterateKinds(kinds) {
|
|
169
|
+
for (const kind of kinds) {
|
|
170
|
+
const events = this.kinds.get(kind);
|
|
171
|
+
if (events) {
|
|
172
|
+
for (const event of events)
|
|
173
|
+
yield event;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
*iterateTime(since, until) {
|
|
178
|
+
let untilIndex = 0;
|
|
179
|
+
let sinceIndex = this.created_at.length - 1;
|
|
180
|
+
let start = until
|
|
181
|
+
? binarySearch(this.created_at, (mid) => {
|
|
182
|
+
if (mid.created_at === until)
|
|
183
|
+
return -1;
|
|
184
|
+
return mid.created_at - until;
|
|
185
|
+
})
|
|
186
|
+
: undefined;
|
|
187
|
+
if (start && start[1])
|
|
188
|
+
untilIndex = start[0];
|
|
189
|
+
const end = since
|
|
190
|
+
? binarySearch(this.created_at, (mid) => {
|
|
191
|
+
if (mid.created_at === since)
|
|
192
|
+
return 1;
|
|
193
|
+
return since - mid.created_at;
|
|
194
|
+
})
|
|
195
|
+
: undefined;
|
|
196
|
+
if (end && end[1])
|
|
197
|
+
sinceIndex = end[0];
|
|
198
|
+
const events = new Set();
|
|
199
|
+
for (let i = untilIndex; i <= sinceIndex; i++) {
|
|
200
|
+
events.add(this.created_at[i]);
|
|
201
|
+
}
|
|
202
|
+
return events;
|
|
203
|
+
}
|
|
204
|
+
*iterateIds(ids) {
|
|
205
|
+
for (const id of ids) {
|
|
206
|
+
if (this.events.has(id))
|
|
207
|
+
yield this.events.get(id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Returns all events that match the filter */
|
|
211
|
+
getEventsForFilter(filter) {
|
|
212
|
+
// search is not supported, return an empty set
|
|
213
|
+
if (filter.search)
|
|
214
|
+
return new Set();
|
|
215
|
+
let first = true;
|
|
216
|
+
let events = new Set();
|
|
217
|
+
const and = (iterable) => {
|
|
218
|
+
const set = iterable instanceof Set ? iterable : new Set(iterable);
|
|
219
|
+
if (first) {
|
|
220
|
+
events = set;
|
|
221
|
+
first = false;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
for (const event of events) {
|
|
225
|
+
if (!set.has(event))
|
|
226
|
+
events.delete(event);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return events;
|
|
230
|
+
};
|
|
231
|
+
if (filter.ids)
|
|
232
|
+
and(this.iterateIds(filter.ids));
|
|
233
|
+
let time = null;
|
|
234
|
+
// query for time first if since is set
|
|
235
|
+
if (filter.since !== undefined) {
|
|
236
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
237
|
+
and(time);
|
|
238
|
+
}
|
|
239
|
+
for (const t of INDEXABLE_TAGS) {
|
|
240
|
+
const key = `#${t}`;
|
|
241
|
+
const values = filter[key];
|
|
242
|
+
if (values?.length)
|
|
243
|
+
and(this.iterateTag(t, values));
|
|
244
|
+
}
|
|
245
|
+
if (filter.authors)
|
|
246
|
+
and(this.iterateAuthors(filter.authors));
|
|
247
|
+
if (filter.kinds)
|
|
248
|
+
and(this.iterateKinds(filter.kinds));
|
|
249
|
+
// query for time last if only until is set
|
|
250
|
+
if (filter.since === undefined && filter.until !== undefined) {
|
|
251
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
252
|
+
and(time);
|
|
253
|
+
}
|
|
254
|
+
// if the filter queried on time and has a limit. truncate the events now
|
|
255
|
+
if (filter.limit && time) {
|
|
256
|
+
const limited = new Set();
|
|
257
|
+
for (const event of time) {
|
|
258
|
+
if (limited.size >= filter.limit)
|
|
259
|
+
break;
|
|
260
|
+
if (events.has(event))
|
|
261
|
+
limited.add(event);
|
|
262
|
+
}
|
|
263
|
+
return limited;
|
|
264
|
+
}
|
|
265
|
+
return events;
|
|
266
|
+
}
|
|
267
|
+
getForFilters(filters) {
|
|
268
|
+
if (filters.length === 0)
|
|
269
|
+
throw new Error("No Filters");
|
|
270
|
+
let events = new Set();
|
|
271
|
+
for (const filter of filters) {
|
|
272
|
+
const filtered = this.getEventsForFilter(filter);
|
|
273
|
+
for (const event of filtered)
|
|
274
|
+
events.add(event);
|
|
275
|
+
}
|
|
276
|
+
return events;
|
|
277
|
+
}
|
|
278
|
+
/** Remove the oldest events that are not claimed */
|
|
279
|
+
prune(limit = 1000) {
|
|
280
|
+
let removed = 0;
|
|
281
|
+
let cursor = this.events.first;
|
|
282
|
+
while (cursor) {
|
|
283
|
+
const event = cursor.value;
|
|
284
|
+
if (!this.isClaimed(event)) {
|
|
285
|
+
this.deleteEvent(event);
|
|
286
|
+
removed++;
|
|
287
|
+
if (removed >= limit)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
cursor = cursor.next;
|
|
291
|
+
}
|
|
292
|
+
return removed;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { Database } from "./database.js";
|
|
4
|
+
export declare class EventStore {
|
|
5
|
+
database: Database;
|
|
6
|
+
constructor();
|
|
7
|
+
/** Adds an event to the database */
|
|
8
|
+
add(event: NostrEvent, fromRelay?: string): import("nostr-tools").Event;
|
|
9
|
+
/** Add an event to the store and notifies all subscribes it has updated */
|
|
10
|
+
update(event: NostrEvent): import("nostr-tools").Event;
|
|
11
|
+
getAll(filters: Filter[]): Set<import("nostr-tools").Event>;
|
|
12
|
+
hasEvent(uid: string): import("nostr-tools").Event | undefined;
|
|
13
|
+
getEvent(uid: string): import("nostr-tools").Event | undefined;
|
|
14
|
+
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
15
|
+
getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
|
|
16
|
+
/** Creates an observable that updates a single event */
|
|
17
|
+
event(uid: string): Observable<import("nostr-tools").Event | undefined>;
|
|
18
|
+
/** Creates an observable that subscribes to multiple events */
|
|
19
|
+
events(uids: string[]): Observable<Map<string, import("nostr-tools").Event>>;
|
|
20
|
+
/** Creates an observable that updates a single replaceable event */
|
|
21
|
+
replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>;
|
|
22
|
+
/** Creates an observable that streams all events that match the filter */
|
|
23
|
+
stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
|
|
24
|
+
/** Creates an observable that updates with an array of sorted events */
|
|
25
|
+
timeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { Database } from "./database.js";
|
|
4
|
+
import { getEventUID, getReplaceableUID } from "../helpers/event.js";
|
|
5
|
+
import { matchFilters } from "../helpers/filter.js";
|
|
6
|
+
import { addSeenRelay } from "../helpers/relays.js";
|
|
7
|
+
export class EventStore {
|
|
8
|
+
database;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.database = new Database();
|
|
11
|
+
}
|
|
12
|
+
/** Adds an event to the database */
|
|
13
|
+
add(event, fromRelay) {
|
|
14
|
+
const inserted = this.database.addEvent(event);
|
|
15
|
+
if (fromRelay)
|
|
16
|
+
addSeenRelay(inserted, fromRelay);
|
|
17
|
+
return inserted;
|
|
18
|
+
}
|
|
19
|
+
/** Add an event to the store and notifies all subscribes it has updated */
|
|
20
|
+
update(event) {
|
|
21
|
+
return this.database.updateEvent(event);
|
|
22
|
+
}
|
|
23
|
+
getAll(filters) {
|
|
24
|
+
return this.database.getForFilters(filters);
|
|
25
|
+
}
|
|
26
|
+
hasEvent(uid) {
|
|
27
|
+
return this.database.hasEvent(uid);
|
|
28
|
+
}
|
|
29
|
+
getEvent(uid) {
|
|
30
|
+
return this.database.getEvent(uid);
|
|
31
|
+
}
|
|
32
|
+
hasReplaceable(kind, pubkey, d) {
|
|
33
|
+
return this.database.hasReplaceable(kind, pubkey, d);
|
|
34
|
+
}
|
|
35
|
+
getReplaceable(kind, pubkey, d) {
|
|
36
|
+
return this.database.getReplaceable(kind, pubkey, d);
|
|
37
|
+
}
|
|
38
|
+
/** Creates an observable that updates a single event */
|
|
39
|
+
event(uid) {
|
|
40
|
+
return new Observable((observer) => {
|
|
41
|
+
let current = this.database.getEvent(uid);
|
|
42
|
+
if (current) {
|
|
43
|
+
observer.next(current);
|
|
44
|
+
this.database.claimEvent(current, observer);
|
|
45
|
+
}
|
|
46
|
+
// subscribe to future events
|
|
47
|
+
const inserted = this.database.inserted.subscribe((event) => {
|
|
48
|
+
if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
|
|
49
|
+
// remove old claim
|
|
50
|
+
if (current)
|
|
51
|
+
this.database.removeClaim(current, observer);
|
|
52
|
+
current = event;
|
|
53
|
+
observer.next(event);
|
|
54
|
+
// claim new event
|
|
55
|
+
this.database.claimEvent(current, observer);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// subscribe to updates
|
|
59
|
+
const updated = this.database.updated.subscribe((event) => {
|
|
60
|
+
if (event === current)
|
|
61
|
+
observer.next(event);
|
|
62
|
+
});
|
|
63
|
+
// subscribe to deleted events
|
|
64
|
+
const deleted = this.database.deleted.subscribe((event) => {
|
|
65
|
+
if (getEventUID(event) === uid && current) {
|
|
66
|
+
this.database.removeClaim(current, observer);
|
|
67
|
+
current = undefined;
|
|
68
|
+
observer.next(undefined);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
inserted.unsubscribe();
|
|
73
|
+
deleted.unsubscribe();
|
|
74
|
+
updated.unsubscribe();
|
|
75
|
+
if (current)
|
|
76
|
+
this.database.removeClaim(current, observer);
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Creates an observable that subscribes to multiple events */
|
|
81
|
+
events(uids) {
|
|
82
|
+
return new Observable((observer) => {
|
|
83
|
+
const events = new Map();
|
|
84
|
+
for (const uid of uids) {
|
|
85
|
+
const e = this.getEvent(uid);
|
|
86
|
+
if (e) {
|
|
87
|
+
events.set(uid, e);
|
|
88
|
+
this.database.claimEvent(e, observer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
observer.next(events);
|
|
92
|
+
// subscribe to future events
|
|
93
|
+
const inserted = this.database.inserted.subscribe((event) => {
|
|
94
|
+
const uid = getEventUID(event);
|
|
95
|
+
if (uids.includes(uid)) {
|
|
96
|
+
const current = events.get(uid);
|
|
97
|
+
// remove old claim
|
|
98
|
+
if (!current || event.created_at > current.created_at) {
|
|
99
|
+
if (current)
|
|
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// subscribe to updates
|
|
109
|
+
const updated = this.database.updated.subscribe((event) => {
|
|
110
|
+
const uid = getEventUID(event);
|
|
111
|
+
if (uids.includes(uid))
|
|
112
|
+
observer.next(events);
|
|
113
|
+
});
|
|
114
|
+
// subscribe to deleted events
|
|
115
|
+
const deleted = this.database.deleted.subscribe((event) => {
|
|
116
|
+
const uid = getEventUID(event);
|
|
117
|
+
if (uids.includes(uid)) {
|
|
118
|
+
const current = events.get(uid);
|
|
119
|
+
if (current) {
|
|
120
|
+
this.database.removeClaim(current, observer);
|
|
121
|
+
events.delete(uid);
|
|
122
|
+
observer.next(events);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return () => {
|
|
127
|
+
inserted.unsubscribe();
|
|
128
|
+
deleted.unsubscribe();
|
|
129
|
+
updated.unsubscribe();
|
|
130
|
+
for (const [_uid, event] of events) {
|
|
131
|
+
this.database.removeClaim(event, observer);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Creates an observable that updates a single replaceable event */
|
|
137
|
+
replaceable(kind, pubkey, d) {
|
|
138
|
+
return this.event(getReplaceableUID(kind, pubkey, d));
|
|
139
|
+
}
|
|
140
|
+
/** Creates an observable that streams all events that match the filter */
|
|
141
|
+
stream(filters) {
|
|
142
|
+
return new Observable((observer) => {
|
|
143
|
+
let claimed = new Set();
|
|
144
|
+
let events = this.database.getForFilters(filters);
|
|
145
|
+
for (const event of events) {
|
|
146
|
+
observer.next(event);
|
|
147
|
+
this.database.claimEvent(event, observer);
|
|
148
|
+
claimed.add(event);
|
|
149
|
+
}
|
|
150
|
+
// subscribe to future events
|
|
151
|
+
const sub = this.database.inserted.subscribe((event) => {
|
|
152
|
+
if (matchFilters(filters, event)) {
|
|
153
|
+
observer.next(event);
|
|
154
|
+
this.database.claimEvent(event, observer);
|
|
155
|
+
claimed.add(event);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return () => {
|
|
159
|
+
sub.unsubscribe();
|
|
160
|
+
// remove all claims
|
|
161
|
+
for (const event of claimed)
|
|
162
|
+
this.database.removeClaim(event, observer);
|
|
163
|
+
claimed.clear();
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Creates an observable that updates with an array of sorted events */
|
|
168
|
+
timeline(filters) {
|
|
169
|
+
return new Observable((observer) => {
|
|
170
|
+
const seen = new Map();
|
|
171
|
+
const timeline = [];
|
|
172
|
+
// build initial timeline
|
|
173
|
+
const events = this.database.getForFilters(filters);
|
|
174
|
+
for (const event of events) {
|
|
175
|
+
insertEventIntoDescendingList(timeline, event);
|
|
176
|
+
this.database.claimEvent(event, observer);
|
|
177
|
+
seen.set(getEventUID(event), event);
|
|
178
|
+
}
|
|
179
|
+
observer.next([...timeline]);
|
|
180
|
+
// subscribe to future events
|
|
181
|
+
const inserted = this.database.inserted.subscribe((event) => {
|
|
182
|
+
if (matchFilters(filters, event)) {
|
|
183
|
+
const uid = getEventUID(event);
|
|
184
|
+
let current = seen.get(uid);
|
|
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
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// subscribe to updates
|
|
206
|
+
const updated = this.database.updated.subscribe((event) => {
|
|
207
|
+
if (seen.has(getEventUID(event))) {
|
|
208
|
+
observer.next([...timeline]);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// subscribe to removed events
|
|
212
|
+
const deleted = this.database.deleted.subscribe((event) => {
|
|
213
|
+
const uid = getEventUID(event);
|
|
214
|
+
let current = seen.get(uid);
|
|
215
|
+
if (current) {
|
|
216
|
+
// remove the event
|
|
217
|
+
timeline.splice(timeline.indexOf(current), 1);
|
|
218
|
+
observer.next([...timeline]);
|
|
219
|
+
// remove the claim
|
|
220
|
+
seen.delete(uid);
|
|
221
|
+
this.database.removeClaim(current, observer);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return () => {
|
|
225
|
+
inserted.unsubscribe();
|
|
226
|
+
deleted.unsubscribe();
|
|
227
|
+
updated.unsubscribe();
|
|
228
|
+
// remove all claims
|
|
229
|
+
for (const [_, event] of seen) {
|
|
230
|
+
this.database.removeClaim(event, observer);
|
|
231
|
+
}
|
|
232
|
+
seen.clear();
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|