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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/event-store/common.d.ts +1 -0
  4. package/dist/event-store/common.js +2 -0
  5. package/dist/event-store/database.d.ts +60 -0
  6. package/dist/event-store/database.js +294 -0
  7. package/dist/event-store/event-store.d.ts +26 -0
  8. package/dist/event-store/event-store.js +236 -0
  9. package/dist/event-store/index.d.ts +2 -0
  10. package/dist/event-store/index.js +2 -0
  11. package/dist/helpers/bolt11.d.ts +8 -0
  12. package/dist/helpers/bolt11.js +14 -0
  13. package/dist/helpers/cache.d.ts +5 -0
  14. package/dist/helpers/cache.js +17 -0
  15. package/dist/helpers/emoji.d.ts +2 -0
  16. package/dist/helpers/emoji.js +4 -0
  17. package/dist/helpers/event.d.ts +34 -0
  18. package/dist/helpers/event.js +64 -0
  19. package/dist/helpers/filter.d.ts +12 -0
  20. package/dist/helpers/filter.js +50 -0
  21. package/dist/helpers/hashtag.d.ts +2 -0
  22. package/dist/helpers/hashtag.js +7 -0
  23. package/dist/helpers/index.d.ts +16 -0
  24. package/dist/helpers/index.js +16 -0
  25. package/dist/helpers/json.d.ts +1 -0
  26. package/dist/helpers/json.js +8 -0
  27. package/dist/helpers/lru.d.ts +32 -0
  28. package/dist/helpers/lru.js +148 -0
  29. package/dist/helpers/mailboxes.d.ts +17 -0
  30. package/dist/helpers/mailboxes.js +37 -0
  31. package/dist/helpers/mailboxes.test.d.ts +1 -0
  32. package/dist/helpers/mailboxes.test.js +80 -0
  33. package/dist/helpers/pointers.d.ts +22 -0
  34. package/dist/helpers/pointers.js +127 -0
  35. package/dist/helpers/profile.d.ts +25 -0
  36. package/dist/helpers/profile.js +28 -0
  37. package/dist/helpers/relays.d.ts +12 -0
  38. package/dist/helpers/relays.js +31 -0
  39. package/dist/helpers/string.d.ts +4 -0
  40. package/dist/helpers/string.js +13 -0
  41. package/dist/helpers/tags.d.ts +6 -0
  42. package/dist/helpers/tags.js +18 -0
  43. package/dist/helpers/threading.d.ts +55 -0
  44. package/dist/helpers/threading.js +73 -0
  45. package/dist/helpers/time.d.ts +2 -0
  46. package/dist/helpers/time.js +4 -0
  47. package/dist/helpers/url.d.ts +11 -0
  48. package/dist/helpers/url.js +29 -0
  49. package/dist/helpers/zap.d.ts +12 -0
  50. package/dist/helpers/zap.js +51 -0
  51. package/dist/index.d.ts +5 -0
  52. package/dist/index.js +5 -0
  53. package/dist/logger.d.ts +2 -0
  54. package/dist/logger.js +2 -0
  55. package/dist/observable/getValue.d.ts +2 -0
  56. package/dist/observable/getValue.js +13 -0
  57. package/dist/observable/index.d.ts +2 -0
  58. package/dist/observable/index.js +2 -0
  59. package/dist/observable/share-behavior.d.ts +2 -0
  60. package/dist/observable/share-behavior.js +7 -0
  61. package/dist/observable/share-latest-value.d.ts +8 -0
  62. package/dist/observable/share-latest-value.js +21 -0
  63. package/dist/observable/stateful.d.ts +10 -0
  64. package/dist/observable/stateful.js +60 -0
  65. package/dist/observable/throttle.d.ts +3 -0
  66. package/dist/observable/throttle.js +23 -0
  67. package/dist/promise/deferred.d.ts +5 -0
  68. package/dist/promise/deferred.js +14 -0
  69. package/dist/promise/index.d.ts +1 -0
  70. package/dist/promise/index.js +1 -0
  71. package/dist/queries/index.d.ts +6 -0
  72. package/dist/queries/index.js +6 -0
  73. package/dist/queries/mailboxes.d.ts +5 -0
  74. package/dist/queries/mailboxes.js +12 -0
  75. package/dist/queries/profile.d.ts +3 -0
  76. package/dist/queries/profile.js +11 -0
  77. package/dist/queries/reactions.d.ts +4 -0
  78. package/dist/queries/reactions.js +19 -0
  79. package/dist/queries/simple.d.ts +16 -0
  80. package/dist/queries/simple.js +38 -0
  81. package/dist/queries/thread.d.ts +23 -0
  82. package/dist/queries/thread.js +66 -0
  83. package/dist/queries/zaps.d.ts +4 -0
  84. package/dist/queries/zaps.js +16 -0
  85. package/dist/query-store/index.d.ts +47 -0
  86. package/dist/query-store/index.js +60 -0
  87. package/dist/utils/lru.d.ts +32 -0
  88. package/dist/utils/lru.js +148 -0
  89. 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,2 @@
1
+ const LETTERS = "abcdefghijklmnopqrstuvwxyz";
2
+ export const INDEXABLE_TAGS = new Set((LETTERS + LETTERS.toUpperCase()).split(""));
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./event-store.js";
2
+ export * from "./database.js";