applesauce-core 0.9.0 → 0.11.0
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 +1 -1
- package/dist/__tests__/fixtures.d.ts +8 -0
- package/dist/__tests__/fixtures.js +20 -0
- package/dist/event-store/__tests__/event-store.test.js +259 -0
- package/dist/event-store/database.d.ts +22 -16
- package/dist/event-store/database.js +62 -39
- package/dist/event-store/event-store.d.ts +52 -15
- package/dist/event-store/event-store.js +283 -191
- package/dist/helpers/__tests__/blossom.test.d.ts +1 -0
- package/dist/helpers/__tests__/blossom.test.js +13 -0
- package/dist/helpers/__tests__/comment.test.d.ts +1 -0
- package/dist/helpers/__tests__/comment.test.js +235 -0
- package/dist/helpers/__tests__/emoji.test.d.ts +1 -0
- package/dist/helpers/__tests__/emoji.test.js +15 -0
- package/dist/helpers/__tests__/event.test.d.ts +1 -0
- package/dist/helpers/__tests__/event.test.js +36 -0
- package/dist/helpers/__tests__/file-metadata.test.d.ts +1 -0
- package/dist/helpers/__tests__/file-metadata.test.js +103 -0
- package/dist/helpers/__tests__/hidden-tags.test.d.ts +1 -0
- package/dist/helpers/__tests__/hidden-tags.test.js +29 -0
- package/dist/helpers/__tests__/mailboxes.test.d.ts +1 -0
- package/dist/helpers/{mailboxes.test.js → __tests__/mailboxes.test.js} +14 -13
- package/dist/helpers/__tests__/relays.test.d.ts +1 -0
- package/dist/helpers/__tests__/relays.test.js +21 -0
- package/dist/helpers/__tests__/tags.test.d.ts +1 -0
- package/dist/helpers/__tests__/tags.test.js +24 -0
- package/dist/helpers/__tests__/threading.test.d.ts +1 -0
- package/dist/helpers/__tests__/threading.test.js +41 -0
- package/dist/helpers/blossom.d.ts +9 -0
- package/dist/helpers/blossom.js +22 -0
- package/dist/helpers/bolt11.d.ts +1 -0
- package/dist/helpers/bolt11.js +1 -0
- package/dist/helpers/bookmarks.d.ts +15 -0
- package/dist/helpers/bookmarks.js +27 -0
- package/dist/helpers/channels.d.ts +10 -0
- package/dist/helpers/channels.js +27 -0
- package/dist/helpers/comment.d.ts +47 -0
- package/dist/helpers/comment.js +120 -0
- package/dist/helpers/contacts.d.ts +3 -0
- package/dist/helpers/contacts.js +25 -0
- package/dist/helpers/content.d.ts +3 -0
- package/dist/helpers/content.js +8 -0
- package/dist/helpers/delete.d.ts +3 -0
- package/dist/helpers/delete.js +7 -0
- package/dist/helpers/dns-identity.d.ts +7 -0
- package/dist/helpers/dns-identity.js +10 -0
- package/dist/helpers/emoji.d.ts +12 -1
- package/dist/helpers/emoji.js +13 -1
- package/dist/helpers/event.d.ts +17 -3
- package/dist/helpers/event.js +54 -12
- package/dist/helpers/external-id.d.ts +29 -0
- package/dist/helpers/external-id.js +20 -0
- package/dist/helpers/file-metadata.d.ts +55 -0
- package/dist/helpers/file-metadata.js +99 -0
- package/dist/helpers/filter.d.ts +4 -2
- package/dist/helpers/filter.js +36 -7
- package/dist/helpers/groups.d.ts +24 -0
- package/dist/helpers/groups.js +39 -0
- package/dist/helpers/hidden-tags.d.ts +48 -0
- package/dist/helpers/hidden-tags.js +86 -0
- package/dist/helpers/index.d.ts +28 -8
- package/dist/helpers/index.js +28 -8
- package/dist/helpers/json.d.ts +1 -0
- package/dist/helpers/json.js +1 -0
- package/dist/helpers/lists.d.ts +28 -0
- package/dist/helpers/lists.js +65 -0
- package/dist/helpers/lnurl.d.ts +4 -0
- package/dist/helpers/lnurl.js +40 -0
- package/dist/helpers/mailboxes.js +16 -9
- package/dist/helpers/mutes.d.ts +14 -0
- package/dist/helpers/mutes.js +23 -0
- package/dist/helpers/picture-post.d.ts +4 -0
- package/dist/helpers/picture-post.js +6 -0
- package/dist/helpers/pointers.d.ts +38 -5
- package/dist/helpers/pointers.js +105 -25
- package/dist/helpers/profile.d.ts +6 -1
- package/dist/helpers/profile.js +5 -1
- package/dist/helpers/relays.d.ts +6 -3
- package/dist/helpers/relays.js +25 -18
- package/dist/helpers/share.d.ts +4 -0
- package/dist/helpers/share.js +12 -0
- package/dist/helpers/string.d.ts +6 -0
- package/dist/helpers/string.js +2 -0
- package/dist/helpers/tags.d.ts +23 -0
- package/dist/helpers/tags.js +34 -6
- package/dist/helpers/threading.d.ts +6 -6
- package/dist/helpers/threading.js +30 -9
- package/dist/helpers/url.d.ts +11 -1
- package/dist/helpers/url.js +31 -3
- package/dist/helpers/user-status.d.ts +18 -0
- package/dist/helpers/user-status.js +21 -0
- package/dist/helpers/zap.d.ts +25 -0
- package/dist/helpers/zap.js +32 -3
- package/dist/observable/__tests__/claim-events.test.d.ts +1 -0
- package/dist/observable/__tests__/claim-events.test.js +23 -0
- package/dist/observable/__tests__/claim-latest.test.d.ts +1 -0
- package/dist/observable/__tests__/claim-latest.test.js +37 -0
- package/dist/observable/__tests__/simple-timeout.test.d.ts +1 -0
- package/dist/observable/__tests__/simple-timeout.test.js +34 -0
- package/dist/observable/claim-events.d.ts +5 -0
- package/dist/observable/claim-events.js +28 -0
- package/dist/observable/claim-latest.d.ts +4 -0
- package/dist/observable/claim-latest.js +20 -0
- package/dist/observable/get-observable-value.d.ts +3 -0
- package/dist/observable/get-observable-value.js +9 -0
- package/dist/observable/index.d.ts +2 -1
- package/dist/observable/index.js +2 -1
- package/dist/observable/share-latest-value.d.ts +2 -4
- package/dist/observable/share-latest-value.js +19 -16
- package/dist/observable/simple-timeout.d.ts +4 -0
- package/dist/observable/simple-timeout.js +6 -0
- package/dist/promise/deferred.d.ts +1 -0
- package/dist/promise/deferred.js +1 -0
- package/dist/queries/blossom.d.ts +2 -0
- package/dist/queries/blossom.js +10 -0
- package/dist/queries/bookmarks.d.ts +8 -0
- package/dist/queries/bookmarks.js +23 -0
- package/dist/queries/channels.d.ts +11 -0
- package/dist/queries/channels.js +73 -0
- package/dist/queries/comments.d.ts +4 -0
- package/dist/queries/comments.js +14 -0
- package/dist/queries/contacts.d.ts +3 -0
- package/dist/queries/contacts.js +12 -0
- package/dist/queries/index.d.ts +9 -2
- package/dist/queries/index.js +9 -2
- package/dist/queries/mailboxes.d.ts +1 -0
- package/dist/queries/mailboxes.js +1 -0
- package/dist/queries/mutes.d.ts +8 -0
- package/dist/queries/mutes.js +23 -0
- package/dist/queries/pins.d.ts +3 -0
- package/dist/queries/pins.js +12 -0
- package/dist/queries/profile.d.ts +1 -0
- package/dist/queries/profile.js +1 -0
- package/dist/queries/reactions.d.ts +1 -1
- package/dist/queries/reactions.js +1 -1
- package/dist/queries/simple.d.ts +4 -4
- package/dist/queries/simple.js +13 -13
- package/dist/queries/thread.d.ts +2 -0
- package/dist/queries/thread.js +30 -4
- package/dist/queries/user-status.d.ts +11 -0
- package/dist/queries/user-status.js +39 -0
- package/dist/queries/zaps.d.ts +1 -0
- package/dist/queries/zaps.js +1 -0
- package/dist/query-store/index.d.ts +1 -47
- package/dist/query-store/index.js +1 -60
- package/dist/query-store/query-store.d.ts +51 -0
- package/dist/query-store/query-store.js +88 -0
- package/dist/query-store/query-store.test.d.ts +1 -0
- package/dist/query-store/query-store.test.js +33 -0
- package/package.json +24 -21
- package/dist/observable/getValue.d.ts +0 -2
- package/dist/observable/getValue.js +0 -13
- /package/dist/{helpers/mailboxes.test.d.ts → event-store/__tests__/event-store.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# applesauce-core
|
|
2
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)
|
|
3
|
+
AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/typedoc/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/typedoc/modules/Queries)
|
|
4
4
|
|
|
5
5
|
# Example
|
|
6
6
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NostrEvent } from "nostr-tools";
|
|
2
|
+
export declare class FakeUser {
|
|
3
|
+
key: Uint8Array<ArrayBufferLike>;
|
|
4
|
+
pubkey: string;
|
|
5
|
+
event(data?: Partial<NostrEvent>): NostrEvent;
|
|
6
|
+
note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
7
|
+
profile(profile: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { finalizeEvent, generateSecretKey, getPublicKey, kinds } from "nostr-tools";
|
|
2
|
+
import { unixNow } from "../helpers/time.js";
|
|
3
|
+
export class FakeUser {
|
|
4
|
+
key = generateSecretKey();
|
|
5
|
+
pubkey = getPublicKey(this.key);
|
|
6
|
+
event(data) {
|
|
7
|
+
return finalizeEvent({
|
|
8
|
+
kind: data?.kind ?? kinds.ShortTextNote,
|
|
9
|
+
content: data?.content || "",
|
|
10
|
+
created_at: data?.created_at ?? unixNow(),
|
|
11
|
+
tags: data?.tags || [],
|
|
12
|
+
}, this.key);
|
|
13
|
+
}
|
|
14
|
+
note(content = "Hello World", extra) {
|
|
15
|
+
return this.event({ kind: kinds.ShortTextNote, content, ...extra });
|
|
16
|
+
}
|
|
17
|
+
profile(profile, extra) {
|
|
18
|
+
return this.event({ kind: kinds.Metadata, content: JSON.stringify({ ...profile }), ...extra });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { kinds } from "nostr-tools";
|
|
3
|
+
import { subscribeSpyTo } from "@hirez_io/observer-spy";
|
|
4
|
+
import { EventStore } from "../event-store.js";
|
|
5
|
+
import { addSeenRelay, getSeenRelays } from "../../helpers/relays.js";
|
|
6
|
+
import { getEventUID } from "../../helpers/event.js";
|
|
7
|
+
import { FakeUser } from "../../__tests__/fixtures.js";
|
|
8
|
+
let eventStore;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
eventStore = new EventStore();
|
|
11
|
+
});
|
|
12
|
+
const user = new FakeUser();
|
|
13
|
+
const profile = user.profile({ name: "fake user" });
|
|
14
|
+
const note = user.note();
|
|
15
|
+
describe("add", () => {
|
|
16
|
+
it("should return original event in case of duplicates", () => {
|
|
17
|
+
const a = { ...profile };
|
|
18
|
+
expect(eventStore.add(a)).toBe(a);
|
|
19
|
+
const b = { ...profile };
|
|
20
|
+
expect(eventStore.add(b)).toBe(a);
|
|
21
|
+
const c = { ...profile };
|
|
22
|
+
expect(eventStore.add(c)).toBe(a);
|
|
23
|
+
});
|
|
24
|
+
it("should merge seen relays on duplicate events", () => {
|
|
25
|
+
const a = { ...profile };
|
|
26
|
+
addSeenRelay(a, "wss://relay.a.com");
|
|
27
|
+
eventStore.add(a);
|
|
28
|
+
const b = { ...profile };
|
|
29
|
+
addSeenRelay(b, "wss://relay.b.com");
|
|
30
|
+
eventStore.add(b);
|
|
31
|
+
expect(eventStore.getEvent(profile.id)).toBeDefined();
|
|
32
|
+
expect([...getSeenRelays(eventStore.getEvent(profile.id))]).toEqual(expect.arrayContaining(["wss://relay.a.com", "wss://relay.b.com"]));
|
|
33
|
+
});
|
|
34
|
+
it("should ignore deleted events", () => {
|
|
35
|
+
const deleteEvent = {
|
|
36
|
+
id: "delete event id",
|
|
37
|
+
kind: kinds.EventDeletion,
|
|
38
|
+
created_at: profile.created_at + 100,
|
|
39
|
+
pubkey: user.pubkey,
|
|
40
|
+
tags: [["e", profile.id]],
|
|
41
|
+
sig: "this should be ignored for the test",
|
|
42
|
+
content: "test",
|
|
43
|
+
};
|
|
44
|
+
// add delete event first
|
|
45
|
+
eventStore.add(deleteEvent);
|
|
46
|
+
// now event should be ignored
|
|
47
|
+
eventStore.add(profile);
|
|
48
|
+
expect(eventStore.getEvent(profile.id)).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("verifyEvent", () => {
|
|
52
|
+
it("should be called for all events added", () => {
|
|
53
|
+
const verifyEvent = vi.fn().mockReturnValue(true);
|
|
54
|
+
eventStore.verifyEvent = verifyEvent;
|
|
55
|
+
eventStore.add(profile);
|
|
56
|
+
expect(verifyEvent).toHaveBeenCalledWith(profile);
|
|
57
|
+
});
|
|
58
|
+
it("should not be called for duplicate events", () => {
|
|
59
|
+
const verifyEvent = vi.fn().mockReturnValue(true);
|
|
60
|
+
eventStore.verifyEvent = verifyEvent;
|
|
61
|
+
const a = { ...profile };
|
|
62
|
+
eventStore.add(a);
|
|
63
|
+
expect(verifyEvent).toHaveBeenCalledWith(a);
|
|
64
|
+
const b = { ...profile };
|
|
65
|
+
eventStore.add(b);
|
|
66
|
+
expect(verifyEvent).toHaveBeenCalledTimes(1);
|
|
67
|
+
const c = { ...profile };
|
|
68
|
+
eventStore.add(c);
|
|
69
|
+
expect(verifyEvent).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("deleted", () => {
|
|
73
|
+
it("should complete when event is removed", () => {
|
|
74
|
+
eventStore.add(profile);
|
|
75
|
+
const spy = subscribeSpyTo(eventStore.removed(profile.id));
|
|
76
|
+
eventStore.remove(profile);
|
|
77
|
+
expect(spy.getValues()).toEqual([]);
|
|
78
|
+
expect(spy.receivedComplete()).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("event", () => {
|
|
82
|
+
it("should emit existing event", () => {
|
|
83
|
+
eventStore.add(profile);
|
|
84
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
85
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
86
|
+
});
|
|
87
|
+
it("should emit then event when its added", () => {
|
|
88
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
89
|
+
expect(spy.getValues()).toEqual([]);
|
|
90
|
+
eventStore.add(profile);
|
|
91
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
92
|
+
});
|
|
93
|
+
it("should emit undefined when event is removed", () => {
|
|
94
|
+
eventStore.add(profile);
|
|
95
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
96
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
97
|
+
eventStore.remove(profile);
|
|
98
|
+
expect(spy.getValues()).toEqual([profile, undefined]);
|
|
99
|
+
});
|
|
100
|
+
it("should emit new value if event is re-added", () => {
|
|
101
|
+
eventStore.add(profile);
|
|
102
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
103
|
+
eventStore.remove(profile);
|
|
104
|
+
eventStore.add(profile);
|
|
105
|
+
expect(spy.getValues()).toEqual([profile, undefined, profile]);
|
|
106
|
+
});
|
|
107
|
+
it("should not complete when event is removed", () => {
|
|
108
|
+
eventStore.add(profile);
|
|
109
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
110
|
+
eventStore.remove(profile);
|
|
111
|
+
expect(spy.receivedComplete()).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
it("should not emit any values if there are no events", () => {
|
|
114
|
+
const spy = subscribeSpyTo(eventStore.event(profile.id));
|
|
115
|
+
expect(spy.receivedNext()).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("events", () => {
|
|
119
|
+
it("should emit existing events", () => {
|
|
120
|
+
eventStore.add(profile);
|
|
121
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
122
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }]);
|
|
123
|
+
});
|
|
124
|
+
it("should remove events when they are removed", () => {
|
|
125
|
+
eventStore.add(profile);
|
|
126
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
127
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }]);
|
|
128
|
+
eventStore.remove(profile);
|
|
129
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }, {}]);
|
|
130
|
+
});
|
|
131
|
+
it("should add events back if then are re-added", () => {
|
|
132
|
+
eventStore.add(profile);
|
|
133
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
134
|
+
eventStore.remove(profile);
|
|
135
|
+
eventStore.add(profile);
|
|
136
|
+
expect(spy.getValues()).toEqual([{ [profile.id]: profile }, {}, { [profile.id]: profile }]);
|
|
137
|
+
});
|
|
138
|
+
it("should not emit any values if there are no events", () => {
|
|
139
|
+
const spy = subscribeSpyTo(eventStore.events([profile.id]));
|
|
140
|
+
expect(spy.receivedNext()).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("replaceable", () => {
|
|
144
|
+
it("should not emit till there is an event", () => {
|
|
145
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
146
|
+
expect(spy.receivedNext()).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it("should emit existing events", () => {
|
|
149
|
+
eventStore.add(profile);
|
|
150
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
151
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
152
|
+
});
|
|
153
|
+
it("should emit undefined when event is removed", () => {
|
|
154
|
+
eventStore.add(profile);
|
|
155
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
156
|
+
eventStore.remove(profile);
|
|
157
|
+
expect(spy.getValues()).toEqual([profile, undefined]);
|
|
158
|
+
});
|
|
159
|
+
it("should not complete when event is removed", () => {
|
|
160
|
+
eventStore.add(profile);
|
|
161
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
162
|
+
eventStore.remove(profile);
|
|
163
|
+
expect(spy.receivedComplete()).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
it("should emit event when re-added", () => {
|
|
166
|
+
eventStore.add(profile);
|
|
167
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
168
|
+
eventStore.remove(profile);
|
|
169
|
+
eventStore.add(profile);
|
|
170
|
+
expect(spy.getValues()).toEqual([profile, undefined, profile]);
|
|
171
|
+
});
|
|
172
|
+
it("should claim event", () => {
|
|
173
|
+
eventStore.add(profile);
|
|
174
|
+
eventStore.replaceable(0, user.pubkey).subscribe();
|
|
175
|
+
expect(eventStore.database.isClaimed(profile)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
it("should remove claim when event is removed", () => {
|
|
178
|
+
eventStore.add(profile);
|
|
179
|
+
eventStore.replaceable(0, user.pubkey).subscribe();
|
|
180
|
+
eventStore.remove(profile);
|
|
181
|
+
expect(eventStore.database.isClaimed(profile)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
it("should ignore older events added later", () => {
|
|
184
|
+
eventStore.add(profile);
|
|
185
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
186
|
+
eventStore.add(user.profile({ name: "old name" }, { created_at: profile.created_at - 500 }));
|
|
187
|
+
eventStore.add(user.profile({ name: "really old name" }, { created_at: profile.created_at - 1000 }));
|
|
188
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe("timeline", () => {
|
|
192
|
+
it("should emit an empty array if there are not events", () => {
|
|
193
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [1] }));
|
|
194
|
+
expect(spy.getValues()).toEqual([[]]);
|
|
195
|
+
});
|
|
196
|
+
it("should emit existing events", () => {
|
|
197
|
+
eventStore.add(profile);
|
|
198
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
199
|
+
expect(spy.getValues()).toEqual([[profile]]);
|
|
200
|
+
});
|
|
201
|
+
it("should emit new events", () => {
|
|
202
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0, 1] }));
|
|
203
|
+
eventStore.add(profile);
|
|
204
|
+
eventStore.add(note);
|
|
205
|
+
expect(spy.getValues()).toEqual([[], [profile], [note, profile]]);
|
|
206
|
+
});
|
|
207
|
+
it("should remove event when its removed", () => {
|
|
208
|
+
eventStore.add(profile);
|
|
209
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
210
|
+
eventStore.remove(profile);
|
|
211
|
+
expect(spy.getValues()).toEqual([[profile], []]);
|
|
212
|
+
});
|
|
213
|
+
it("should not emit when other events are removed", () => {
|
|
214
|
+
eventStore.add(profile);
|
|
215
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
216
|
+
eventStore.add(note);
|
|
217
|
+
eventStore.remove(note);
|
|
218
|
+
expect(spy.getValues()).toEqual([[profile]]);
|
|
219
|
+
});
|
|
220
|
+
it("should ignore older events added later", () => {
|
|
221
|
+
eventStore.add(profile);
|
|
222
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
223
|
+
eventStore.add(user.profile({ name: "old-name" }, { created_at: profile.created_at - 1000 }));
|
|
224
|
+
expect(spy.getValues()).toEqual([[profile]]);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe("replaceableSet", () => {
|
|
228
|
+
it("should not emit if there are not events", () => {
|
|
229
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
230
|
+
expect(spy.receivedNext()).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
it("should emit existing events", () => {
|
|
233
|
+
eventStore.add(profile);
|
|
234
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
235
|
+
expect(spy.getValues()).toEqual([{ [getEventUID(profile)]: profile }]);
|
|
236
|
+
});
|
|
237
|
+
it("should remove event when removed", () => {
|
|
238
|
+
eventStore.add(profile);
|
|
239
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
240
|
+
eventStore.remove(profile);
|
|
241
|
+
expect(spy.getValues()).toEqual([{ [getEventUID(profile)]: profile }, {}]);
|
|
242
|
+
});
|
|
243
|
+
it("should replace older events", () => {
|
|
244
|
+
const event2 = { ...profile, created_at: profile.created_at + 100, id: "newer-event" };
|
|
245
|
+
const uid = getEventUID(profile);
|
|
246
|
+
eventStore.add(profile);
|
|
247
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
248
|
+
eventStore.add(event2);
|
|
249
|
+
expect(spy.getValues()).toEqual([{ [uid]: profile }, { [uid]: event2 }]);
|
|
250
|
+
});
|
|
251
|
+
it("should ignore old events added later", () => {
|
|
252
|
+
const old = user.profile({ name: "old-name" }, { created_at: profile.created_at - 1000 });
|
|
253
|
+
const uid = getEventUID(profile);
|
|
254
|
+
eventStore.add(profile);
|
|
255
|
+
const spy = subscribeSpyTo(eventStore.replaceableSet([{ kind: 0, pubkey: user.pubkey }]));
|
|
256
|
+
eventStore.add(old);
|
|
257
|
+
expect(spy.getValues()).toEqual([{ [uid]: profile }]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -3,6 +3,7 @@ import { Subject } from "rxjs";
|
|
|
3
3
|
import { LRU } from "../helpers/lru.js";
|
|
4
4
|
/**
|
|
5
5
|
* An in-memory database for nostr events
|
|
6
|
+
* NOTE: does not handle replaceable events
|
|
6
7
|
*/
|
|
7
8
|
export declare class Database {
|
|
8
9
|
protected log: import("debug").Debugger;
|
|
@@ -13,12 +14,15 @@ export declare class Database {
|
|
|
13
14
|
protected created_at: NostrEvent[];
|
|
14
15
|
/** LRU cache of last events touched */
|
|
15
16
|
events: LRU<import("nostr-tools").Event>;
|
|
17
|
+
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
16
18
|
/** A stream of events inserted into the database */
|
|
17
19
|
inserted: Subject<import("nostr-tools").Event>;
|
|
18
20
|
/** A stream of events that have been updated */
|
|
19
21
|
updated: Subject<import("nostr-tools").Event>;
|
|
20
|
-
/** A stream of events removed
|
|
21
|
-
|
|
22
|
+
/** A stream of events removed from the database */
|
|
23
|
+
removed: Subject<import("nostr-tools").Event>;
|
|
24
|
+
/** A method thats called before a new event is inserted */
|
|
25
|
+
onBeforeInsert?: (event: NostrEvent) => void;
|
|
22
26
|
get size(): number;
|
|
23
27
|
protected claims: WeakMap<import("nostr-tools").Event, any>;
|
|
24
28
|
/** Index helper methods */
|
|
@@ -27,18 +31,20 @@ export declare class Database {
|
|
|
27
31
|
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
28
32
|
/** Moves an event to the top of the LRU cache */
|
|
29
33
|
touch(event: NostrEvent): void;
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
/** Checks if the database contains an event without touching it */
|
|
35
|
+
hasEvent(id: string): boolean;
|
|
36
|
+
/** Gets a single event based on id */
|
|
37
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
32
38
|
/** Checks if the database contains a replaceable event without touching it */
|
|
33
39
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
34
|
-
/** Gets
|
|
35
|
-
getReplaceable(kind: number, pubkey: string, d?: string):
|
|
40
|
+
/** Gets an array of replaceable events */
|
|
41
|
+
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
|
|
36
42
|
/** Inserts an event into the database and notifies all subscriptions */
|
|
37
|
-
addEvent(event: NostrEvent):
|
|
43
|
+
addEvent(event: NostrEvent): NostrEvent;
|
|
38
44
|
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
39
|
-
updateEvent(event: NostrEvent):
|
|
40
|
-
/**
|
|
41
|
-
|
|
45
|
+
updateEvent(event: NostrEvent): NostrEvent;
|
|
46
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
47
|
+
removeEvent(eventOrId: string | NostrEvent): boolean;
|
|
42
48
|
/** Sets the claim on the event and touches it */
|
|
43
49
|
claimEvent(event: NostrEvent, claim: any): void;
|
|
44
50
|
/** Checks if an event is claimed by anything */
|
|
@@ -47,14 +53,14 @@ export declare class Database {
|
|
|
47
53
|
removeClaim(event: NostrEvent, claim: any): void;
|
|
48
54
|
/** Removes all claims on an event */
|
|
49
55
|
clearClaim(event: NostrEvent): void;
|
|
50
|
-
iterateAuthors(authors: Iterable<string>): Generator<
|
|
51
|
-
iterateTag(tag: string, values: Iterable<string>): Generator<
|
|
52
|
-
iterateKinds(kinds: Iterable<number>): Generator<
|
|
53
|
-
iterateTime(since: number | undefined, until: number | undefined): Generator<
|
|
54
|
-
iterateIds(ids: Iterable<string>): Generator<
|
|
56
|
+
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
|
|
57
|
+
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
|
|
58
|
+
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
|
|
59
|
+
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
|
|
60
|
+
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
55
61
|
/** Returns all events that match the filter */
|
|
56
62
|
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
57
|
-
getForFilters(filters: Filter[]): Set<
|
|
63
|
+
getForFilters(filters: Filter[]): Set<NostrEvent>;
|
|
58
64
|
/** Remove the oldest events that are not claimed */
|
|
59
65
|
prune(limit?: number): number;
|
|
60
66
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import { Subject } from "rxjs";
|
|
3
|
-
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID } from "../helpers/event.js";
|
|
3
|
+
import { FromCacheSymbol, getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
|
|
4
4
|
import { INDEXABLE_TAGS } from "./common.js";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
6
|
import { LRU } from "../helpers/lru.js";
|
|
7
7
|
/**
|
|
8
8
|
* An in-memory database for nostr events
|
|
9
|
+
* NOTE: does not handle replaceable events
|
|
9
10
|
*/
|
|
10
11
|
export class Database {
|
|
11
12
|
log = logger.extend("Database");
|
|
@@ -16,12 +17,15 @@ export class Database {
|
|
|
16
17
|
created_at = [];
|
|
17
18
|
/** LRU cache of last events touched */
|
|
18
19
|
events = new LRU();
|
|
20
|
+
replaceable = new Map();
|
|
19
21
|
/** A stream of events inserted into the database */
|
|
20
22
|
inserted = new Subject();
|
|
21
23
|
/** A stream of events that have been updated */
|
|
22
24
|
updated = new Subject();
|
|
23
|
-
/** A stream of events removed
|
|
24
|
-
|
|
25
|
+
/** A stream of events removed from the database */
|
|
26
|
+
removed = new Subject();
|
|
27
|
+
/** A method thats called before a new event is inserted */
|
|
28
|
+
onBeforeInsert;
|
|
25
29
|
get size() {
|
|
26
30
|
return this.events.size;
|
|
27
31
|
}
|
|
@@ -56,35 +60,37 @@ export class Database {
|
|
|
56
60
|
}
|
|
57
61
|
/** Moves an event to the top of the LRU cache */
|
|
58
62
|
touch(event) {
|
|
59
|
-
this.events.set(
|
|
63
|
+
this.events.set(event.id, event);
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
/** Checks if the database contains an event without touching it */
|
|
66
|
+
hasEvent(id) {
|
|
67
|
+
return this.events.has(id);
|
|
63
68
|
}
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
/** Gets a single event based on id */
|
|
70
|
+
getEvent(id) {
|
|
71
|
+
return this.events.get(id);
|
|
66
72
|
}
|
|
67
73
|
/** Checks if the database contains a replaceable event without touching it */
|
|
68
74
|
hasReplaceable(kind, pubkey, d) {
|
|
69
|
-
|
|
75
|
+
const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d));
|
|
76
|
+
return !!events && events.length > 0;
|
|
70
77
|
}
|
|
71
|
-
/** Gets
|
|
78
|
+
/** Gets an array of replaceable events */
|
|
72
79
|
getReplaceable(kind, pubkey, d) {
|
|
73
|
-
return this.
|
|
80
|
+
return this.replaceable.get(getReplaceableUID(kind, pubkey, d));
|
|
74
81
|
}
|
|
75
82
|
/** Inserts an event into the database and notifies all subscriptions */
|
|
76
83
|
addEvent(event) {
|
|
77
|
-
const
|
|
78
|
-
const current = this.events.get(
|
|
79
|
-
if (current
|
|
80
|
-
// if this is a duplicate event, transfer some
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
84
|
-
}
|
|
84
|
+
const id = event.id;
|
|
85
|
+
const current = this.events.get(id);
|
|
86
|
+
if (current) {
|
|
87
|
+
// if this is a duplicate event, transfer some important symbols
|
|
88
|
+
if (event[FromCacheSymbol])
|
|
89
|
+
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
85
90
|
return current;
|
|
86
91
|
}
|
|
87
|
-
this.
|
|
92
|
+
this.onBeforeInsert?.(event);
|
|
93
|
+
this.events.set(id, event);
|
|
88
94
|
this.getKindIndex(event.kind).add(event);
|
|
89
95
|
this.getAuthorsIndex(event.pubkey).add(event);
|
|
90
96
|
for (const tag of getIndexableTags(event)) {
|
|
@@ -92,7 +98,18 @@ export class Database {
|
|
|
92
98
|
this.getTagIndex(tag).add(event);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
101
|
+
// insert into time index
|
|
95
102
|
insertEventIntoDescendingList(this.created_at, event);
|
|
103
|
+
// insert into replaceable index
|
|
104
|
+
if (isReplaceable(event.kind)) {
|
|
105
|
+
const uid = getEventUID(event);
|
|
106
|
+
let array = this.replaceable.get(uid);
|
|
107
|
+
if (!this.replaceable.has(uid)) {
|
|
108
|
+
array = [];
|
|
109
|
+
this.replaceable.set(uid, array);
|
|
110
|
+
}
|
|
111
|
+
insertEventIntoDescendingList(array, event);
|
|
112
|
+
}
|
|
96
113
|
this.inserted.next(event);
|
|
97
114
|
return event;
|
|
98
115
|
}
|
|
@@ -102,14 +119,14 @@ export class Database {
|
|
|
102
119
|
this.updated.next(inserted);
|
|
103
120
|
return inserted;
|
|
104
121
|
}
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
let event = typeof
|
|
122
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
123
|
+
removeEvent(eventOrId) {
|
|
124
|
+
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
|
|
108
125
|
if (!event)
|
|
109
126
|
throw new Error("Missing event");
|
|
110
|
-
const
|
|
127
|
+
const id = event.id;
|
|
111
128
|
// only remove events that are known
|
|
112
|
-
if (!this.events.has(
|
|
129
|
+
if (!this.events.has(id))
|
|
113
130
|
return false;
|
|
114
131
|
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
115
132
|
this.getKindIndex(event.kind).delete(event);
|
|
@@ -121,8 +138,20 @@ export class Database {
|
|
|
121
138
|
// remove from created_at index
|
|
122
139
|
const i = this.created_at.indexOf(event);
|
|
123
140
|
this.created_at.splice(i, 1);
|
|
124
|
-
this.events.delete(
|
|
125
|
-
|
|
141
|
+
this.events.delete(id);
|
|
142
|
+
// remove from replaceable index
|
|
143
|
+
if (isReplaceable(event.kind)) {
|
|
144
|
+
const uid = getEventUID(event);
|
|
145
|
+
const array = this.replaceable.get(uid);
|
|
146
|
+
if (array && array.includes(event)) {
|
|
147
|
+
const idx = array.indexOf(event);
|
|
148
|
+
array.splice(idx, 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// remove any claims this event has
|
|
152
|
+
this.claims.delete(event);
|
|
153
|
+
// notify subscribers this event was removed
|
|
154
|
+
this.removed.next(event);
|
|
126
155
|
return true;
|
|
127
156
|
}
|
|
128
157
|
/** Sets the claim on the event and touches it */
|
|
@@ -179,27 +208,21 @@ export class Database {
|
|
|
179
208
|
let sinceIndex = this.created_at.length - 1;
|
|
180
209
|
let start = until
|
|
181
210
|
? binarySearch(this.created_at, (mid) => {
|
|
182
|
-
if (mid.created_at === until)
|
|
183
|
-
return -1;
|
|
184
211
|
return mid.created_at - until;
|
|
185
212
|
})
|
|
186
213
|
: undefined;
|
|
187
|
-
if (start
|
|
214
|
+
if (start)
|
|
188
215
|
untilIndex = start[0];
|
|
189
216
|
const end = since
|
|
190
217
|
? binarySearch(this.created_at, (mid) => {
|
|
191
|
-
|
|
192
|
-
return 1;
|
|
193
|
-
return since - mid.created_at;
|
|
218
|
+
return mid.created_at - since;
|
|
194
219
|
})
|
|
195
220
|
: undefined;
|
|
196
|
-
if (end
|
|
221
|
+
if (end)
|
|
197
222
|
sinceIndex = end[0];
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
events.add(this.created_at[i]);
|
|
223
|
+
for (let i = untilIndex; i < sinceIndex; i++) {
|
|
224
|
+
yield this.created_at[i];
|
|
201
225
|
}
|
|
202
|
-
return events;
|
|
203
226
|
}
|
|
204
227
|
*iterateIds(ids) {
|
|
205
228
|
for (const id of ids) {
|
|
@@ -282,7 +305,7 @@ export class Database {
|
|
|
282
305
|
while (cursor) {
|
|
283
306
|
const event = cursor.value;
|
|
284
307
|
if (!this.isClaimed(event)) {
|
|
285
|
-
this.
|
|
308
|
+
this.removeEvent(event);
|
|
286
309
|
removed++;
|
|
287
310
|
if (removed >= limit)
|
|
288
311
|
break;
|
|
@@ -3,24 +3,61 @@ import { Observable } from "rxjs";
|
|
|
3
3
|
import { Database } from "./database.js";
|
|
4
4
|
export declare class EventStore {
|
|
5
5
|
database: Database;
|
|
6
|
+
/** Enable this to keep old versions of replaceable events */
|
|
7
|
+
keepOldVersions: boolean;
|
|
8
|
+
/** A method used to verify new events before added them */
|
|
9
|
+
verifyEvent?: (event: NostrEvent) => boolean;
|
|
6
10
|
constructor();
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
protected deletedIds: Set<string>;
|
|
12
|
+
protected deletedCoords: Map<string, number>;
|
|
13
|
+
protected checkDeleted(event: string | NostrEvent): boolean;
|
|
14
|
+
protected handleDeleteEvent(deleteEvent: NostrEvent): void;
|
|
15
|
+
/** Copies important metadata from and identical event to another */
|
|
16
|
+
static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
|
|
17
|
+
/**
|
|
18
|
+
* Adds an event to the database and update subscriptions
|
|
19
|
+
* @throws
|
|
20
|
+
*/
|
|
21
|
+
add(event: NostrEvent, fromRelay?: string): NostrEvent;
|
|
22
|
+
/** Removes an event from the database and updates subscriptions */
|
|
23
|
+
remove(event: string | NostrEvent): boolean;
|
|
24
|
+
/** Removes any event that is not being used by a subscription */
|
|
25
|
+
prune(max?: number): number;
|
|
9
26
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
10
|
-
update(event: NostrEvent):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
update(event: NostrEvent): NostrEvent;
|
|
28
|
+
/** Get all events matching a filter */
|
|
29
|
+
getAll(filters: Filter[]): Set<NostrEvent>;
|
|
30
|
+
/** Check if the store has an event */
|
|
31
|
+
hasEvent(uid: string): boolean;
|
|
32
|
+
getEvent(uid: string): NostrEvent | undefined;
|
|
33
|
+
/** Check if the store has a replaceable event */
|
|
14
34
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
/** Gets the latest version of a replaceable event */
|
|
36
|
+
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined;
|
|
37
|
+
/** Returns all versions of a replaceable event */
|
|
38
|
+
getReplaceableHistory(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Creates an observable that streams all events that match the filter and remains open
|
|
41
|
+
* @param filters
|
|
42
|
+
* @param [onlyNew=false] Only subscribe to new events
|
|
43
|
+
*/
|
|
44
|
+
filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
|
|
45
|
+
/** Returns an observable that completes when an event is removed */
|
|
46
|
+
removed(id: string): Observable<never>;
|
|
47
|
+
/** Creates an observable that emits when event is updated */
|
|
48
|
+
updated(id: string): Observable<NostrEvent>;
|
|
49
|
+
/** Creates an observable that subscribes to a single event */
|
|
50
|
+
event(id: string): Observable<NostrEvent | undefined>;
|
|
18
51
|
/** Creates an observable that subscribes to multiple events */
|
|
19
|
-
events(
|
|
20
|
-
/** Creates an observable that
|
|
21
|
-
replaceable(kind: number, pubkey: string, d?: string): Observable<
|
|
22
|
-
/** Creates an observable that
|
|
23
|
-
|
|
52
|
+
events(ids: string[]): Observable<Record<string, NostrEvent>>;
|
|
53
|
+
/** Creates an observable that subscribes to the latest version of a replaceable event */
|
|
54
|
+
replaceable(kind: number, pubkey: string, d?: string): Observable<NostrEvent | undefined>;
|
|
55
|
+
/** Creates an observable that subscribes to the latest version of an array of replaceable events*/
|
|
56
|
+
replaceableSet(pointers: {
|
|
57
|
+
kind: number;
|
|
58
|
+
pubkey: string;
|
|
59
|
+
identifier?: string;
|
|
60
|
+
}[]): Observable<Record<string, NostrEvent>>;
|
|
24
61
|
/** Creates an observable that updates with an array of sorted events */
|
|
25
|
-
timeline(filters: Filter[]): Observable<
|
|
62
|
+
timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
|
|
26
63
|
}
|