applesauce-core 0.0.0-next-20250526151506 → 0.0.0-next-20250606170247
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/README.md +7 -13
- package/dist/__tests__/exports.test.js +3 -4
- package/dist/__tests__/fixtures.d.ts +10 -1
- package/dist/__tests__/fixtures.js +9 -1
- package/dist/event-store/__tests__/event-store.test.js +43 -17
- package/dist/event-store/event-set.d.ts +82 -0
- package/dist/event-store/event-set.js +347 -0
- package/dist/event-store/event-store.d.ts +55 -20
- package/dist/event-store/event-store.js +147 -200
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/interface.d.ts +71 -13
- package/dist/helpers/__tests__/encrypted-content-cache.test.d.ts +1 -0
- package/dist/helpers/__tests__/encrypted-content-cache.test.js +65 -0
- package/dist/helpers/__tests__/encryption.test.d.ts +1 -0
- package/dist/helpers/__tests__/encryption.test.js +21 -0
- package/dist/helpers/__tests__/exports.test.js +52 -5
- package/dist/helpers/__tests__/pointers.test.d.ts +1 -0
- package/dist/helpers/__tests__/pointers.test.js +118 -0
- package/dist/helpers/article.d.ts +9 -0
- package/dist/helpers/article.js +21 -0
- package/dist/helpers/bookmarks.js +1 -2
- package/dist/helpers/encrypted-content-cache.d.ts +15 -0
- package/dist/helpers/encrypted-content-cache.js +125 -0
- package/dist/helpers/encrypted-content.d.ts +48 -0
- package/dist/helpers/encrypted-content.js +65 -0
- package/dist/helpers/encryption.d.ts +5 -0
- package/dist/helpers/encryption.js +10 -0
- package/dist/helpers/event.d.ts +4 -1
- package/dist/helpers/event.js +13 -3
- package/dist/helpers/expiration.d.ts +6 -0
- package/dist/helpers/expiration.js +16 -0
- package/dist/helpers/filter.d.ts +1 -3
- package/dist/helpers/filter.js +1 -3
- package/dist/helpers/gift-wraps.d.ts +17 -5
- package/dist/helpers/gift-wraps.js +65 -27
- package/dist/helpers/groups.js +1 -1
- package/dist/helpers/hidden-content.d.ts +27 -32
- package/dist/helpers/hidden-content.js +35 -65
- package/dist/helpers/hidden-tags.d.ts +23 -4
- package/dist/helpers/hidden-tags.js +39 -4
- package/dist/helpers/index.d.ts +7 -1
- package/dist/helpers/index.js +7 -1
- package/dist/helpers/legacy-direct-messages.d.ts +19 -0
- package/dist/helpers/legacy-direct-messages.js +35 -0
- package/dist/helpers/legacy-messages.d.ts +21 -0
- package/dist/helpers/legacy-messages.js +39 -0
- package/dist/helpers/lists.d.ts +1 -1
- package/dist/helpers/lists.js +2 -2
- package/dist/helpers/messages.d.ts +4 -2
- package/dist/helpers/mutes.js +1 -1
- package/dist/helpers/pointers.d.ts +19 -0
- package/dist/helpers/pointers.js +55 -1
- package/dist/helpers/user-status.js +2 -1
- package/dist/helpers/wrapped-direct-messages.d.ts +10 -0
- package/dist/helpers/wrapped-direct-messages.js +19 -0
- package/dist/helpers/wrapped-messages.d.ts +23 -0
- package/dist/helpers/wrapped-messages.js +38 -0
- package/dist/helpers/zap.d.ts +8 -5
- package/dist/helpers/zap.js +11 -6
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/models/__tests__/comments.test.d.ts +1 -0
- package/dist/models/__tests__/comments.test.js +36 -0
- package/dist/models/__tests__/exports.test.d.ts +1 -0
- package/dist/models/__tests__/exports.test.js +53 -0
- package/dist/models/blossom.d.ts +3 -0
- package/dist/models/blossom.js +8 -0
- package/dist/models/bookmarks.d.ts +8 -0
- package/dist/models/bookmarks.js +24 -0
- package/dist/models/channels.d.ts +11 -0
- package/dist/models/channels.js +61 -0
- package/dist/models/comments.d.ts +4 -0
- package/dist/models/comments.js +11 -0
- package/dist/models/common.d.ts +16 -0
- package/dist/models/common.js +176 -0
- package/dist/models/contacts.d.ts +8 -0
- package/dist/models/contacts.js +24 -0
- package/dist/models/encrypted-content.d.ts +4 -0
- package/dist/models/encrypted-content.js +11 -0
- package/dist/models/gift-wrap.d.ts +7 -0
- package/dist/models/gift-wrap.js +20 -0
- package/dist/models/index.d.ts +18 -0
- package/dist/models/index.js +18 -0
- package/dist/models/legacy-direct-messages.d.ts +5 -0
- package/dist/models/legacy-direct-messages.js +16 -0
- package/dist/models/legacy-messages.d.ts +8 -0
- package/dist/models/legacy-messages.js +29 -0
- package/dist/models/mailboxes.d.ts +6 -0
- package/dist/models/mailboxes.js +10 -0
- package/dist/models/mutes.d.ts +8 -0
- package/dist/models/mutes.js +24 -0
- package/dist/models/pins.d.ts +4 -0
- package/dist/models/pins.js +10 -0
- package/dist/models/profile.d.ts +4 -0
- package/dist/models/profile.js +14 -0
- package/dist/models/reactions.d.ts +4 -0
- package/dist/models/reactions.js +16 -0
- package/dist/models/relays.d.ts +27 -0
- package/dist/models/relays.js +44 -0
- package/dist/models/simple.d.ts +16 -0
- package/dist/models/simple.js +21 -0
- package/dist/models/thread.d.ts +26 -0
- package/dist/models/thread.js +87 -0
- package/dist/models/user-status.d.ts +11 -0
- package/dist/models/user-status.js +33 -0
- package/dist/models/wrapped-direct-messages.d.ts +14 -0
- package/dist/models/wrapped-direct-messages.js +30 -0
- package/dist/models/wrapped-messages.d.ts +25 -0
- package/dist/models/wrapped-messages.js +61 -0
- package/dist/models/zaps.d.ts +9 -0
- package/dist/models/zaps.js +26 -0
- package/dist/observable/__tests__/claim-events.test.js +4 -4
- package/dist/observable/__tests__/claim-latest.test.js +5 -5
- package/dist/observable/__tests__/exports.test.js +1 -1
- package/dist/observable/claim-events.d.ts +3 -3
- package/dist/observable/claim-events.js +4 -4
- package/dist/observable/claim-latest.d.ts +3 -3
- package/dist/observable/claim-latest.js +4 -4
- package/dist/observable/simple-timeout.d.ts +1 -0
- package/dist/observable/simple-timeout.js +1 -0
- package/dist/observable/watch-event-updates.d.ts +5 -5
- package/dist/observable/watch-event-updates.js +16 -5
- package/dist/queries/__tests__/exports.test.js +3 -0
- package/dist/queries/gift-wrap.d.ts +4 -0
- package/dist/queries/gift-wrap.js +12 -0
- package/dist/queries/index.d.ts +2 -1
- package/dist/queries/index.js +2 -1
- package/dist/queries/zaps.d.ts +4 -0
- package/dist/queries/zaps.js +8 -0
- package/dist/query-store/__tests__/query-store.test.js +2 -2
- package/dist/query-store/query-store.d.ts +13 -16
- package/dist/query-store/query-store.js +3 -3
- package/package.json +9 -14
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# applesauce-core
|
|
2
2
|
|
|
3
|
-
AppleSauce is a collection of utilities for building reactive nostr applications. The core package provides an in-memory event database and reactive
|
|
3
|
+
AppleSauce is a collection of utilities for building reactive nostr applications. The core package provides an in-memory event database and reactive models to help you build nostr UIs with less code.
|
|
4
4
|
|
|
5
5
|
## Key Components
|
|
6
6
|
|
|
7
7
|
- **Helpers**: Core utility methods for parsing and extracting data from nostr events
|
|
8
8
|
- **EventStore**: In-memory database for storing and subscribing to nostr events
|
|
9
|
-
- **
|
|
10
|
-
- **Queries**: Complex subscriptions for common nostr data patterns
|
|
9
|
+
- **Models**: Complex subscriptions for common nostr data patterns
|
|
11
10
|
|
|
12
11
|
## Documentation
|
|
13
12
|
|
|
@@ -19,15 +18,13 @@ For detailed documentation and guides, visit:
|
|
|
19
18
|
## Example
|
|
20
19
|
|
|
21
20
|
```js
|
|
22
|
-
import { EventStore
|
|
21
|
+
import { EventStore } from "applesauce-core";
|
|
22
|
+
import { ProfileModel, TimelineModel } from "applesauce-core/models";
|
|
23
23
|
import { Relay } from "nostr-tools/relay";
|
|
24
24
|
|
|
25
25
|
// Create a single EventStore instance for your app
|
|
26
26
|
const eventStore = new EventStore();
|
|
27
27
|
|
|
28
|
-
// Create a QueryStore to manage subscriptions efficiently
|
|
29
|
-
const queryStore = new QueryStore(eventStore);
|
|
30
|
-
|
|
31
28
|
// Use any nostr library for relay connections (nostr-tools, ndk, nostrify, etc...)
|
|
32
29
|
const relay = await Relay.connect("wss://relay.example.com");
|
|
33
30
|
|
|
@@ -38,18 +35,15 @@ const sub = relay.subscribe([{ authors: ["3bf0c63fcb93463407af97a5e5ee64fa883d10
|
|
|
38
35
|
},
|
|
39
36
|
});
|
|
40
37
|
|
|
41
|
-
// Subscribe to profile changes using
|
|
42
|
-
const profile =
|
|
43
|
-
ProfileQuery,
|
|
44
|
-
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
|
45
|
-
);
|
|
38
|
+
// Subscribe to profile changes using ProfileModel
|
|
39
|
+
const profile = eventStore.model(ProfileModel, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");
|
|
46
40
|
|
|
47
41
|
profile.subscribe((parsed) => {
|
|
48
42
|
if (parsed) console.log(parsed);
|
|
49
43
|
});
|
|
50
44
|
|
|
51
45
|
// Subscribe to a timeline of events
|
|
52
|
-
const timeline =
|
|
46
|
+
const timeline = eventStore.model(TimelineModel, { kinds: [1] });
|
|
53
47
|
|
|
54
48
|
timeline.subscribe((events) => {
|
|
55
49
|
console.log(events);
|
|
@@ -4,23 +4,22 @@ describe("exports", () => {
|
|
|
4
4
|
it("should export the expected functions", () => {
|
|
5
5
|
expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
|
|
6
6
|
[
|
|
7
|
-
"
|
|
7
|
+
"EventSet",
|
|
8
8
|
"EventStore",
|
|
9
9
|
"EventStoreSymbol",
|
|
10
10
|
"Helpers",
|
|
11
|
-
"
|
|
12
|
-
"QueryStore",
|
|
11
|
+
"Models",
|
|
13
12
|
"TimeoutError",
|
|
14
13
|
"defined",
|
|
15
14
|
"firstValueFrom",
|
|
16
15
|
"getObservableValue",
|
|
17
16
|
"lastValueFrom",
|
|
18
|
-
"listenLatestUpdates",
|
|
19
17
|
"logger",
|
|
20
18
|
"mapEventsToStore",
|
|
21
19
|
"mapEventsToTimeline",
|
|
22
20
|
"simpleTimeout",
|
|
23
21
|
"watchEventUpdates",
|
|
22
|
+
"watchEventsUpdates",
|
|
24
23
|
"withImmediateValueOrDefault",
|
|
25
24
|
]
|
|
26
25
|
`);
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { NostrEvent } from "nostr-tools";
|
|
2
|
-
|
|
2
|
+
import { EncryptedContentSigner } from "../helpers/encrypted-content.js";
|
|
3
|
+
export declare class FakeUser implements EncryptedContentSigner {
|
|
3
4
|
key: Uint8Array<ArrayBufferLike>;
|
|
4
5
|
pubkey: string;
|
|
6
|
+
nip04: {
|
|
7
|
+
encrypt: (pubkey: string, plaintext: string) => string;
|
|
8
|
+
decrypt: (pubkey: string, ciphertext: string) => string;
|
|
9
|
+
};
|
|
10
|
+
nip44: {
|
|
11
|
+
encrypt: (pubkey: string, plaintext: string) => string;
|
|
12
|
+
decrypt: (pubkey: string, ciphertext: string) => string;
|
|
13
|
+
};
|
|
5
14
|
event(data?: Partial<NostrEvent>): NostrEvent;
|
|
6
15
|
note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
7
16
|
profile(profile: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { finalizeEvent, generateSecretKey, getPublicKey, kinds } from "nostr-tools";
|
|
1
|
+
import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04, nip44 } from "nostr-tools";
|
|
2
2
|
import { unixNow } from "../helpers/time.js";
|
|
3
3
|
export class FakeUser {
|
|
4
4
|
key = generateSecretKey();
|
|
5
5
|
pubkey = getPublicKey(this.key);
|
|
6
|
+
nip04 = {
|
|
7
|
+
encrypt: (pubkey, plaintext) => nip04.encrypt(this.key, pubkey, plaintext),
|
|
8
|
+
decrypt: (pubkey, ciphertext) => nip04.decrypt(this.key, pubkey, ciphertext),
|
|
9
|
+
};
|
|
10
|
+
nip44 = {
|
|
11
|
+
encrypt: (pubkey, plaintext) => nip44.encrypt(plaintext, nip44.getConversationKey(this.key, pubkey)),
|
|
12
|
+
decrypt: (pubkey, ciphertext) => nip44.decrypt(ciphertext, nip44.getConversationKey(this.key, pubkey)),
|
|
13
|
+
};
|
|
6
14
|
event(data) {
|
|
7
15
|
return finalizeEvent({
|
|
8
16
|
kind: data?.kind ?? kinds.ShortTextNote,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { kinds } from "nostr-tools";
|
|
3
1
|
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { getEventUID } from "../../helpers/event.js";
|
|
2
|
+
import { kinds } from "nostr-tools";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
4
|
import { FakeUser } from "../../__tests__/fixtures.js";
|
|
5
|
+
import { getEventUID } from "../../helpers/event.js";
|
|
6
|
+
import { addSeenRelay, getSeenRelays } from "../../helpers/relays.js";
|
|
7
|
+
import { EventModel } from "../../models/common.js";
|
|
8
|
+
import { ProfileModel } from "../../models/profile.js";
|
|
9
|
+
import { EventStore } from "../event-store.js";
|
|
8
10
|
let eventStore;
|
|
9
11
|
beforeEach(() => {
|
|
10
12
|
eventStore = new EventStore();
|
|
@@ -107,14 +109,14 @@ describe("add", () => {
|
|
|
107
109
|
});
|
|
108
110
|
describe("inserts", () => {
|
|
109
111
|
it("should emit newer replaceable events", () => {
|
|
110
|
-
const spy = subscribeSpyTo(eventStore.
|
|
112
|
+
const spy = subscribeSpyTo(eventStore.insert$);
|
|
111
113
|
eventStore.add(profile);
|
|
112
114
|
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 100 });
|
|
113
115
|
eventStore.add(newer);
|
|
114
116
|
expect(spy.getValues()).toEqual([profile, newer]);
|
|
115
117
|
});
|
|
116
118
|
it("should not emit when older replaceable event is added", () => {
|
|
117
|
-
const spy = subscribeSpyTo(eventStore.
|
|
119
|
+
const spy = subscribeSpyTo(eventStore.insert$);
|
|
118
120
|
eventStore.add(profile);
|
|
119
121
|
eventStore.add(user.profile({ name: "new name" }, { created_at: profile.created_at - 1000 }));
|
|
120
122
|
expect(spy.getValues()).toEqual([profile]);
|
|
@@ -122,7 +124,7 @@ describe("inserts", () => {
|
|
|
122
124
|
});
|
|
123
125
|
describe("removes", () => {
|
|
124
126
|
it("should emit older replaceable events when the newest replaceable event is added", () => {
|
|
125
|
-
const spy = subscribeSpyTo(eventStore.
|
|
127
|
+
const spy = subscribeSpyTo(eventStore.remove$);
|
|
126
128
|
eventStore.add(profile);
|
|
127
129
|
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
|
|
128
130
|
eventStore.add(newer);
|
|
@@ -159,6 +161,34 @@ describe("removed", () => {
|
|
|
159
161
|
expect(spy.receivedComplete()).toBe(true);
|
|
160
162
|
});
|
|
161
163
|
});
|
|
164
|
+
describe("model", () => {
|
|
165
|
+
it("should emit synchronous value if it exists", () => {
|
|
166
|
+
let value = undefined;
|
|
167
|
+
eventStore.add(profile);
|
|
168
|
+
eventStore.model(ProfileModel, user.pubkey).subscribe((v) => (value = v));
|
|
169
|
+
expect(value).not.toBe(undefined);
|
|
170
|
+
});
|
|
171
|
+
it("should not emit undefined if value exists", () => {
|
|
172
|
+
eventStore.add(profile);
|
|
173
|
+
const spy = subscribeSpyTo(eventStore.model(EventModel, profile.id));
|
|
174
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
175
|
+
});
|
|
176
|
+
it("should emit synchronous undefined if value does not exists", () => {
|
|
177
|
+
let value = 0;
|
|
178
|
+
eventStore.model(ProfileModel, user.pubkey).subscribe((v) => {
|
|
179
|
+
value = v;
|
|
180
|
+
});
|
|
181
|
+
expect(value).not.toBe(0);
|
|
182
|
+
expect(value).toBe(undefined);
|
|
183
|
+
});
|
|
184
|
+
it("should share latest value", () => {
|
|
185
|
+
eventStore.add(profile);
|
|
186
|
+
const spy = subscribeSpyTo(eventStore.model(EventModel, profile.id));
|
|
187
|
+
const spy2 = subscribeSpyTo(eventStore.model(EventModel, profile.id));
|
|
188
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
189
|
+
expect(spy2.getValues()).toEqual([profile]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
162
192
|
describe("event", () => {
|
|
163
193
|
it("should emit existing event", () => {
|
|
164
194
|
eventStore.add(profile);
|
|
@@ -167,9 +197,9 @@ describe("event", () => {
|
|
|
167
197
|
});
|
|
168
198
|
it("should emit then event when its added", () => {
|
|
169
199
|
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
170
|
-
expect(spy.getValues()).toEqual([]);
|
|
200
|
+
expect(spy.getValues()).toEqual([undefined]);
|
|
171
201
|
eventStore.add(profile);
|
|
172
|
-
expect(spy.getValues()).toEqual([profile]);
|
|
202
|
+
expect(spy.getValues()).toEqual([undefined, profile]);
|
|
173
203
|
});
|
|
174
204
|
it("should emit undefined when event is removed", () => {
|
|
175
205
|
eventStore.add(profile);
|
|
@@ -191,9 +221,9 @@ describe("event", () => {
|
|
|
191
221
|
eventStore.remove(profile);
|
|
192
222
|
expect(spy.receivedComplete()).toBe(false);
|
|
193
223
|
});
|
|
194
|
-
it("should
|
|
224
|
+
it("should emit undefined if event is not found", () => {
|
|
195
225
|
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
196
|
-
expect(spy.
|
|
226
|
+
expect(spy.getValues()).toEqual([undefined]);
|
|
197
227
|
});
|
|
198
228
|
});
|
|
199
229
|
describe("events", () => {
|
|
@@ -222,10 +252,6 @@ describe("events", () => {
|
|
|
222
252
|
});
|
|
223
253
|
});
|
|
224
254
|
describe("replaceable", () => {
|
|
225
|
-
it("should not emit till there is an event", () => {
|
|
226
|
-
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
227
|
-
expect(spy.receivedNext()).toBe(false);
|
|
228
|
-
});
|
|
229
255
|
it("should emit existing events", () => {
|
|
230
256
|
eventStore.add(profile);
|
|
231
257
|
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
@@ -269,8 +295,8 @@ describe("replaceable", () => {
|
|
|
269
295
|
expect(spy.getValues()).toEqual([profile]);
|
|
270
296
|
});
|
|
271
297
|
it("should emit newer events", () => {
|
|
272
|
-
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
273
298
|
eventStore.add(profile);
|
|
299
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
274
300
|
const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 500 });
|
|
275
301
|
eventStore.add(newProfile);
|
|
276
302
|
expect(spy.getValues()).toEqual([profile, newProfile]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Subject } from "rxjs";
|
|
3
|
+
import { LRU } from "../helpers/lru.js";
|
|
4
|
+
import { IEventSet } from "./interface.js";
|
|
5
|
+
/**
|
|
6
|
+
* A set of nostr events that can be queried and subscribed to
|
|
7
|
+
* NOTE: does not handle replaceable events or any deletion logic
|
|
8
|
+
*/
|
|
9
|
+
export declare class EventSet implements IEventSet {
|
|
10
|
+
protected log: import("debug").Debugger;
|
|
11
|
+
/** Indexes */
|
|
12
|
+
protected kinds: Map<number, Set<import("nostr-tools").Event>>;
|
|
13
|
+
protected authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
14
|
+
protected tags: LRU<Set<import("nostr-tools").Event>>;
|
|
15
|
+
protected created_at: NostrEvent[];
|
|
16
|
+
/** LRU cache of last events touched */
|
|
17
|
+
events: LRU<import("nostr-tools").Event>;
|
|
18
|
+
/** A sorted array of replaceable events by uid */
|
|
19
|
+
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
20
|
+
/** A stream of events inserted into the database */
|
|
21
|
+
insert$: Subject<import("nostr-tools").Event>;
|
|
22
|
+
/** A stream of events that have been updated */
|
|
23
|
+
update$: Subject<import("nostr-tools").Event>;
|
|
24
|
+
/** A stream of events removed from the database */
|
|
25
|
+
remove$: Subject<import("nostr-tools").Event>;
|
|
26
|
+
/** A method thats called before a new event is inserted */
|
|
27
|
+
onBeforeInsert?: (event: NostrEvent) => boolean;
|
|
28
|
+
/** The number of events in the event set */
|
|
29
|
+
get size(): number;
|
|
30
|
+
/** Moves an event to the top of the LRU cache */
|
|
31
|
+
touch(event: NostrEvent): void;
|
|
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;
|
|
36
|
+
/** Checks if the event set has a replaceable event */
|
|
37
|
+
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
38
|
+
/** Gets the latest replaceable event */
|
|
39
|
+
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
40
|
+
/** Gets the history of a replaceable event */
|
|
41
|
+
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
42
|
+
/** Gets all events that match the filters */
|
|
43
|
+
getByFilters(filters: Filter | Filter[]): Set<NostrEvent>;
|
|
44
|
+
/** Gets a timeline of events that match the filters */
|
|
45
|
+
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
46
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
47
|
+
add(event: NostrEvent): NostrEvent | null;
|
|
48
|
+
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
49
|
+
update(event: NostrEvent): boolean;
|
|
50
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
51
|
+
remove(eventOrId: string | NostrEvent): boolean;
|
|
52
|
+
/** A weak map of events that are claimed by other things */
|
|
53
|
+
protected claims: WeakMap<import("nostr-tools").Event, any>;
|
|
54
|
+
/** Sets the claim on the event and touches it */
|
|
55
|
+
claim(event: NostrEvent, claim: any): void;
|
|
56
|
+
/** Checks if an event is claimed by anything */
|
|
57
|
+
isClaimed(event: NostrEvent): boolean;
|
|
58
|
+
/** Removes a claim from an event */
|
|
59
|
+
removeClaim(event: NostrEvent, claim: any): void;
|
|
60
|
+
/** Removes all claims on an event */
|
|
61
|
+
clearClaim(event: NostrEvent): void;
|
|
62
|
+
/** Index helper methods */
|
|
63
|
+
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
|
|
64
|
+
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
|
|
65
|
+
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
66
|
+
/** Iterates over all events by author */
|
|
67
|
+
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
|
|
68
|
+
/** Iterates over all events by indexable tag and value */
|
|
69
|
+
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
|
|
70
|
+
/** Iterates over all events by kind */
|
|
71
|
+
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
|
|
72
|
+
/** Iterates over all events by time */
|
|
73
|
+
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
|
|
74
|
+
/** Iterates over all events by id */
|
|
75
|
+
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
76
|
+
/** Returns all events that match the filter */
|
|
77
|
+
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
78
|
+
/** Returns all events that match the filters */
|
|
79
|
+
getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
|
|
80
|
+
/** Remove the oldest events that are not claimed */
|
|
81
|
+
prune(limit?: number): number;
|
|
82
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
|
+
import { Subject } from "rxjs";
|
|
3
|
+
import { createReplaceableAddress, getEventUID, getIndexableTags, isReplaceable } from "../helpers/event.js";
|
|
4
|
+
import { LRU } from "../helpers/lru.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { INDEXABLE_TAGS } from "./common.js";
|
|
7
|
+
/**
|
|
8
|
+
* A set of nostr events that can be queried and subscribed to
|
|
9
|
+
* NOTE: does not handle replaceable events or any deletion logic
|
|
10
|
+
*/
|
|
11
|
+
export class EventSet {
|
|
12
|
+
log = logger.extend("EventSet");
|
|
13
|
+
/** Indexes */
|
|
14
|
+
kinds = new Map();
|
|
15
|
+
authors = new Map();
|
|
16
|
+
tags = new LRU();
|
|
17
|
+
created_at = [];
|
|
18
|
+
/** LRU cache of last events touched */
|
|
19
|
+
events = new LRU();
|
|
20
|
+
/** A sorted array of replaceable events by uid */
|
|
21
|
+
replaceable = new Map();
|
|
22
|
+
/** A stream of events inserted into the database */
|
|
23
|
+
insert$ = new Subject();
|
|
24
|
+
/** A stream of events that have been updated */
|
|
25
|
+
update$ = new Subject();
|
|
26
|
+
/** A stream of events removed from the database */
|
|
27
|
+
remove$ = new Subject();
|
|
28
|
+
/** A method thats called before a new event is inserted */
|
|
29
|
+
onBeforeInsert;
|
|
30
|
+
/** The number of events in the event set */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.events.size;
|
|
33
|
+
}
|
|
34
|
+
/** Moves an event to the top of the LRU cache */
|
|
35
|
+
touch(event) {
|
|
36
|
+
this.events.set(event.id, event);
|
|
37
|
+
}
|
|
38
|
+
/** Checks if the database contains an event without touching it */
|
|
39
|
+
hasEvent(id) {
|
|
40
|
+
return this.events.has(id);
|
|
41
|
+
}
|
|
42
|
+
/** Gets a single event based on id */
|
|
43
|
+
getEvent(id) {
|
|
44
|
+
return this.events.get(id);
|
|
45
|
+
}
|
|
46
|
+
/** Checks if the event set has a replaceable event */
|
|
47
|
+
hasReplaceable(kind, pubkey, identifier) {
|
|
48
|
+
const events = this.replaceable.get(createReplaceableAddress(kind, pubkey, identifier));
|
|
49
|
+
return !!events && events.length > 0;
|
|
50
|
+
}
|
|
51
|
+
/** Gets the latest replaceable event */
|
|
52
|
+
getReplaceable(kind, pubkey, identifier) {
|
|
53
|
+
const address = createReplaceableAddress(kind, pubkey, identifier);
|
|
54
|
+
const events = this.replaceable.get(address);
|
|
55
|
+
return events?.[0];
|
|
56
|
+
}
|
|
57
|
+
/** Gets the history of a replaceable event */
|
|
58
|
+
getReplaceableHistory(kind, pubkey, identifier) {
|
|
59
|
+
const address = createReplaceableAddress(kind, pubkey, identifier);
|
|
60
|
+
return this.replaceable.get(address);
|
|
61
|
+
}
|
|
62
|
+
/** Gets all events that match the filters */
|
|
63
|
+
getByFilters(filters) {
|
|
64
|
+
return this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
|
|
65
|
+
}
|
|
66
|
+
/** Gets a timeline of events that match the filters */
|
|
67
|
+
getTimeline(filters) {
|
|
68
|
+
const timeline = [];
|
|
69
|
+
const events = this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
|
|
70
|
+
for (const event of events)
|
|
71
|
+
insertEventIntoDescendingList(timeline, event);
|
|
72
|
+
return timeline;
|
|
73
|
+
}
|
|
74
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
75
|
+
add(event) {
|
|
76
|
+
const id = event.id;
|
|
77
|
+
const current = this.events.get(id);
|
|
78
|
+
if (current)
|
|
79
|
+
return current;
|
|
80
|
+
// Ignore events if before insert returns false
|
|
81
|
+
if (this.onBeforeInsert?.(event) === false)
|
|
82
|
+
return null;
|
|
83
|
+
this.events.set(id, event);
|
|
84
|
+
this.getKindIndex(event.kind).add(event);
|
|
85
|
+
this.getAuthorsIndex(event.pubkey).add(event);
|
|
86
|
+
// Add the event to the tag indexes if they exist
|
|
87
|
+
for (const tag of getIndexableTags(event)) {
|
|
88
|
+
if (this.tags.has(tag))
|
|
89
|
+
this.getTagIndex(tag).add(event);
|
|
90
|
+
}
|
|
91
|
+
// Insert into time index
|
|
92
|
+
insertEventIntoDescendingList(this.created_at, event);
|
|
93
|
+
// Insert into replaceable index
|
|
94
|
+
if (isReplaceable(event.kind)) {
|
|
95
|
+
const uid = getEventUID(event);
|
|
96
|
+
let array = this.replaceable.get(uid);
|
|
97
|
+
if (!this.replaceable.has(uid)) {
|
|
98
|
+
// add an empty array if there is no array
|
|
99
|
+
array = [];
|
|
100
|
+
this.replaceable.set(uid, array);
|
|
101
|
+
}
|
|
102
|
+
// insert the event into the sorted array
|
|
103
|
+
insertEventIntoDescendingList(array, event);
|
|
104
|
+
}
|
|
105
|
+
// Notify subscribers that the event was inserted
|
|
106
|
+
this.insert$.next(event);
|
|
107
|
+
return event;
|
|
108
|
+
}
|
|
109
|
+
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
110
|
+
update(event) {
|
|
111
|
+
const inserted = this.add(event);
|
|
112
|
+
if (inserted)
|
|
113
|
+
this.update$.next(inserted);
|
|
114
|
+
return inserted !== null;
|
|
115
|
+
}
|
|
116
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
117
|
+
remove(eventOrId) {
|
|
118
|
+
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
|
|
119
|
+
if (!event)
|
|
120
|
+
throw new Error("Missing event");
|
|
121
|
+
const id = event.id;
|
|
122
|
+
// only remove events that are known
|
|
123
|
+
if (!this.events.has(id))
|
|
124
|
+
return false;
|
|
125
|
+
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
126
|
+
this.getKindIndex(event.kind).delete(event);
|
|
127
|
+
for (const tag of getIndexableTags(event)) {
|
|
128
|
+
if (this.tags.has(tag)) {
|
|
129
|
+
this.getTagIndex(tag).delete(event);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// remove from created_at index
|
|
133
|
+
const i = this.created_at.indexOf(event);
|
|
134
|
+
this.created_at.splice(i, 1);
|
|
135
|
+
this.events.delete(id);
|
|
136
|
+
// remove from replaceable index
|
|
137
|
+
if (isReplaceable(event.kind)) {
|
|
138
|
+
const uid = getEventUID(event);
|
|
139
|
+
const array = this.replaceable.get(uid);
|
|
140
|
+
if (array && array.includes(event)) {
|
|
141
|
+
const idx = array.indexOf(event);
|
|
142
|
+
array.splice(idx, 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// remove any claims this event has
|
|
146
|
+
this.claims.delete(event);
|
|
147
|
+
// notify subscribers this event was removed
|
|
148
|
+
this.remove$.next(event);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/** A weak map of events that are claimed by other things */
|
|
152
|
+
claims = new WeakMap();
|
|
153
|
+
/** Sets the claim on the event and touches it */
|
|
154
|
+
claim(event, claim) {
|
|
155
|
+
if (!this.claims.has(event)) {
|
|
156
|
+
this.claims.set(event, claim);
|
|
157
|
+
}
|
|
158
|
+
// always touch event
|
|
159
|
+
this.touch(event);
|
|
160
|
+
}
|
|
161
|
+
/** Checks if an event is claimed by anything */
|
|
162
|
+
isClaimed(event) {
|
|
163
|
+
return this.claims.has(event);
|
|
164
|
+
}
|
|
165
|
+
/** Removes a claim from an event */
|
|
166
|
+
removeClaim(event, claim) {
|
|
167
|
+
const current = this.claims.get(event);
|
|
168
|
+
if (current === claim)
|
|
169
|
+
this.claims.delete(event);
|
|
170
|
+
}
|
|
171
|
+
/** Removes all claims on an event */
|
|
172
|
+
clearClaim(event) {
|
|
173
|
+
this.claims.delete(event);
|
|
174
|
+
}
|
|
175
|
+
/** Index helper methods */
|
|
176
|
+
getKindIndex(kind) {
|
|
177
|
+
if (!this.kinds.has(kind))
|
|
178
|
+
this.kinds.set(kind, new Set());
|
|
179
|
+
return this.kinds.get(kind);
|
|
180
|
+
}
|
|
181
|
+
getAuthorsIndex(author) {
|
|
182
|
+
if (!this.authors.has(author))
|
|
183
|
+
this.authors.set(author, new Set());
|
|
184
|
+
return this.authors.get(author);
|
|
185
|
+
}
|
|
186
|
+
getTagIndex(tagAndValue) {
|
|
187
|
+
if (!this.tags.has(tagAndValue)) {
|
|
188
|
+
// build new tag index from existing events
|
|
189
|
+
const events = new Set();
|
|
190
|
+
const ts = Date.now();
|
|
191
|
+
for (const event of this.events.values()) {
|
|
192
|
+
if (getIndexableTags(event).has(tagAndValue)) {
|
|
193
|
+
events.add(event);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const took = Date.now() - ts;
|
|
197
|
+
if (took > 100)
|
|
198
|
+
this.log(`Built index ${tagAndValue} took ${took}ms`);
|
|
199
|
+
this.tags.set(tagAndValue, events);
|
|
200
|
+
}
|
|
201
|
+
return this.tags.get(tagAndValue);
|
|
202
|
+
}
|
|
203
|
+
/** Iterates over all events by author */
|
|
204
|
+
*iterateAuthors(authors) {
|
|
205
|
+
for (const author of authors) {
|
|
206
|
+
const events = this.authors.get(author);
|
|
207
|
+
if (events) {
|
|
208
|
+
for (const event of events)
|
|
209
|
+
yield event;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Iterates over all events by indexable tag and value */
|
|
214
|
+
*iterateTag(tag, values) {
|
|
215
|
+
for (const value of values) {
|
|
216
|
+
const events = this.getTagIndex(tag + ":" + value);
|
|
217
|
+
if (events) {
|
|
218
|
+
for (const event of events)
|
|
219
|
+
yield event;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Iterates over all events by kind */
|
|
224
|
+
*iterateKinds(kinds) {
|
|
225
|
+
for (const kind of kinds) {
|
|
226
|
+
const events = this.kinds.get(kind);
|
|
227
|
+
if (events) {
|
|
228
|
+
for (const event of events)
|
|
229
|
+
yield event;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Iterates over all events by time */
|
|
234
|
+
*iterateTime(since, until) {
|
|
235
|
+
let untilIndex = 0;
|
|
236
|
+
let sinceIndex = this.created_at.length - 1;
|
|
237
|
+
let start = until
|
|
238
|
+
? binarySearch(this.created_at, (mid) => {
|
|
239
|
+
return mid.created_at - until;
|
|
240
|
+
})
|
|
241
|
+
: undefined;
|
|
242
|
+
if (start)
|
|
243
|
+
untilIndex = start[0];
|
|
244
|
+
const end = since
|
|
245
|
+
? binarySearch(this.created_at, (mid) => {
|
|
246
|
+
return mid.created_at - since;
|
|
247
|
+
})
|
|
248
|
+
: undefined;
|
|
249
|
+
if (end)
|
|
250
|
+
sinceIndex = end[0];
|
|
251
|
+
for (let i = untilIndex; i < sinceIndex; i++) {
|
|
252
|
+
yield this.created_at[i];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Iterates over all events by id */
|
|
256
|
+
*iterateIds(ids) {
|
|
257
|
+
for (const id of ids) {
|
|
258
|
+
if (this.events.has(id))
|
|
259
|
+
yield this.events.get(id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Returns all events that match the filter */
|
|
263
|
+
getEventsForFilter(filter) {
|
|
264
|
+
// search is not supported, return an empty set
|
|
265
|
+
if (filter.search)
|
|
266
|
+
return new Set();
|
|
267
|
+
let first = true;
|
|
268
|
+
let events = new Set();
|
|
269
|
+
const and = (iterable) => {
|
|
270
|
+
const set = iterable instanceof Set ? iterable : new Set(iterable);
|
|
271
|
+
if (first) {
|
|
272
|
+
events = set;
|
|
273
|
+
first = false;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
for (const event of events) {
|
|
277
|
+
if (!set.has(event))
|
|
278
|
+
events.delete(event);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return events;
|
|
282
|
+
};
|
|
283
|
+
if (filter.ids)
|
|
284
|
+
and(this.iterateIds(filter.ids));
|
|
285
|
+
let time = null;
|
|
286
|
+
// query for time first if since is set
|
|
287
|
+
if (filter.since !== undefined) {
|
|
288
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
289
|
+
and(time);
|
|
290
|
+
}
|
|
291
|
+
for (const t of INDEXABLE_TAGS) {
|
|
292
|
+
const key = `#${t}`;
|
|
293
|
+
const values = filter[key];
|
|
294
|
+
if (values?.length)
|
|
295
|
+
and(this.iterateTag(t, values));
|
|
296
|
+
}
|
|
297
|
+
if (filter.authors)
|
|
298
|
+
and(this.iterateAuthors(filter.authors));
|
|
299
|
+
if (filter.kinds)
|
|
300
|
+
and(this.iterateKinds(filter.kinds));
|
|
301
|
+
// query for time last if only until is set
|
|
302
|
+
if (filter.since === undefined && filter.until !== undefined) {
|
|
303
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
304
|
+
and(time);
|
|
305
|
+
}
|
|
306
|
+
// if the filter queried on time and has a limit. truncate the events now
|
|
307
|
+
if (filter.limit && time) {
|
|
308
|
+
const limited = new Set();
|
|
309
|
+
for (const event of time) {
|
|
310
|
+
if (limited.size >= filter.limit)
|
|
311
|
+
break;
|
|
312
|
+
if (events.has(event))
|
|
313
|
+
limited.add(event);
|
|
314
|
+
}
|
|
315
|
+
return limited;
|
|
316
|
+
}
|
|
317
|
+
return events;
|
|
318
|
+
}
|
|
319
|
+
/** Returns all events that match the filters */
|
|
320
|
+
getEventsForFilters(filters) {
|
|
321
|
+
if (filters.length === 0)
|
|
322
|
+
throw new Error("No Filters");
|
|
323
|
+
let events = new Set();
|
|
324
|
+
for (const filter of filters) {
|
|
325
|
+
const filtered = this.getEventsForFilter(filter);
|
|
326
|
+
for (const event of filtered)
|
|
327
|
+
events.add(event);
|
|
328
|
+
}
|
|
329
|
+
return events;
|
|
330
|
+
}
|
|
331
|
+
/** Remove the oldest events that are not claimed */
|
|
332
|
+
prune(limit = 1000) {
|
|
333
|
+
let removed = 0;
|
|
334
|
+
let cursor = this.events.first;
|
|
335
|
+
while (cursor) {
|
|
336
|
+
const event = cursor.value;
|
|
337
|
+
if (!this.isClaimed(event)) {
|
|
338
|
+
this.remove(event);
|
|
339
|
+
removed++;
|
|
340
|
+
if (removed >= limit)
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
cursor = cursor.next;
|
|
344
|
+
}
|
|
345
|
+
return removed;
|
|
346
|
+
}
|
|
347
|
+
}
|