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.
Files changed (153) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/fixtures.d.ts +8 -0
  3. package/dist/__tests__/fixtures.js +20 -0
  4. package/dist/event-store/__tests__/event-store.test.js +259 -0
  5. package/dist/event-store/database.d.ts +22 -16
  6. package/dist/event-store/database.js +62 -39
  7. package/dist/event-store/event-store.d.ts +52 -15
  8. package/dist/event-store/event-store.js +283 -191
  9. package/dist/helpers/__tests__/blossom.test.d.ts +1 -0
  10. package/dist/helpers/__tests__/blossom.test.js +13 -0
  11. package/dist/helpers/__tests__/comment.test.d.ts +1 -0
  12. package/dist/helpers/__tests__/comment.test.js +235 -0
  13. package/dist/helpers/__tests__/emoji.test.d.ts +1 -0
  14. package/dist/helpers/__tests__/emoji.test.js +15 -0
  15. package/dist/helpers/__tests__/event.test.d.ts +1 -0
  16. package/dist/helpers/__tests__/event.test.js +36 -0
  17. package/dist/helpers/__tests__/file-metadata.test.d.ts +1 -0
  18. package/dist/helpers/__tests__/file-metadata.test.js +103 -0
  19. package/dist/helpers/__tests__/hidden-tags.test.d.ts +1 -0
  20. package/dist/helpers/__tests__/hidden-tags.test.js +29 -0
  21. package/dist/helpers/__tests__/mailboxes.test.d.ts +1 -0
  22. package/dist/helpers/{mailboxes.test.js → __tests__/mailboxes.test.js} +14 -13
  23. package/dist/helpers/__tests__/relays.test.d.ts +1 -0
  24. package/dist/helpers/__tests__/relays.test.js +21 -0
  25. package/dist/helpers/__tests__/tags.test.d.ts +1 -0
  26. package/dist/helpers/__tests__/tags.test.js +24 -0
  27. package/dist/helpers/__tests__/threading.test.d.ts +1 -0
  28. package/dist/helpers/__tests__/threading.test.js +41 -0
  29. package/dist/helpers/blossom.d.ts +9 -0
  30. package/dist/helpers/blossom.js +22 -0
  31. package/dist/helpers/bolt11.d.ts +1 -0
  32. package/dist/helpers/bolt11.js +1 -0
  33. package/dist/helpers/bookmarks.d.ts +15 -0
  34. package/dist/helpers/bookmarks.js +27 -0
  35. package/dist/helpers/channels.d.ts +10 -0
  36. package/dist/helpers/channels.js +27 -0
  37. package/dist/helpers/comment.d.ts +47 -0
  38. package/dist/helpers/comment.js +120 -0
  39. package/dist/helpers/contacts.d.ts +3 -0
  40. package/dist/helpers/contacts.js +25 -0
  41. package/dist/helpers/content.d.ts +3 -0
  42. package/dist/helpers/content.js +8 -0
  43. package/dist/helpers/delete.d.ts +3 -0
  44. package/dist/helpers/delete.js +7 -0
  45. package/dist/helpers/dns-identity.d.ts +7 -0
  46. package/dist/helpers/dns-identity.js +10 -0
  47. package/dist/helpers/emoji.d.ts +12 -1
  48. package/dist/helpers/emoji.js +13 -1
  49. package/dist/helpers/event.d.ts +17 -3
  50. package/dist/helpers/event.js +54 -12
  51. package/dist/helpers/external-id.d.ts +29 -0
  52. package/dist/helpers/external-id.js +20 -0
  53. package/dist/helpers/file-metadata.d.ts +55 -0
  54. package/dist/helpers/file-metadata.js +99 -0
  55. package/dist/helpers/filter.d.ts +4 -2
  56. package/dist/helpers/filter.js +36 -7
  57. package/dist/helpers/groups.d.ts +24 -0
  58. package/dist/helpers/groups.js +39 -0
  59. package/dist/helpers/hidden-tags.d.ts +48 -0
  60. package/dist/helpers/hidden-tags.js +86 -0
  61. package/dist/helpers/index.d.ts +28 -8
  62. package/dist/helpers/index.js +28 -8
  63. package/dist/helpers/json.d.ts +1 -0
  64. package/dist/helpers/json.js +1 -0
  65. package/dist/helpers/lists.d.ts +28 -0
  66. package/dist/helpers/lists.js +65 -0
  67. package/dist/helpers/lnurl.d.ts +4 -0
  68. package/dist/helpers/lnurl.js +40 -0
  69. package/dist/helpers/mailboxes.js +16 -9
  70. package/dist/helpers/mutes.d.ts +14 -0
  71. package/dist/helpers/mutes.js +23 -0
  72. package/dist/helpers/picture-post.d.ts +4 -0
  73. package/dist/helpers/picture-post.js +6 -0
  74. package/dist/helpers/pointers.d.ts +38 -5
  75. package/dist/helpers/pointers.js +105 -25
  76. package/dist/helpers/profile.d.ts +6 -1
  77. package/dist/helpers/profile.js +5 -1
  78. package/dist/helpers/relays.d.ts +6 -3
  79. package/dist/helpers/relays.js +25 -18
  80. package/dist/helpers/share.d.ts +4 -0
  81. package/dist/helpers/share.js +12 -0
  82. package/dist/helpers/string.d.ts +6 -0
  83. package/dist/helpers/string.js +2 -0
  84. package/dist/helpers/tags.d.ts +23 -0
  85. package/dist/helpers/tags.js +34 -6
  86. package/dist/helpers/threading.d.ts +6 -6
  87. package/dist/helpers/threading.js +30 -9
  88. package/dist/helpers/url.d.ts +11 -1
  89. package/dist/helpers/url.js +31 -3
  90. package/dist/helpers/user-status.d.ts +18 -0
  91. package/dist/helpers/user-status.js +21 -0
  92. package/dist/helpers/zap.d.ts +25 -0
  93. package/dist/helpers/zap.js +32 -3
  94. package/dist/observable/__tests__/claim-events.test.d.ts +1 -0
  95. package/dist/observable/__tests__/claim-events.test.js +23 -0
  96. package/dist/observable/__tests__/claim-latest.test.d.ts +1 -0
  97. package/dist/observable/__tests__/claim-latest.test.js +37 -0
  98. package/dist/observable/__tests__/simple-timeout.test.d.ts +1 -0
  99. package/dist/observable/__tests__/simple-timeout.test.js +34 -0
  100. package/dist/observable/claim-events.d.ts +5 -0
  101. package/dist/observable/claim-events.js +28 -0
  102. package/dist/observable/claim-latest.d.ts +4 -0
  103. package/dist/observable/claim-latest.js +20 -0
  104. package/dist/observable/get-observable-value.d.ts +3 -0
  105. package/dist/observable/get-observable-value.js +9 -0
  106. package/dist/observable/index.d.ts +2 -1
  107. package/dist/observable/index.js +2 -1
  108. package/dist/observable/share-latest-value.d.ts +2 -4
  109. package/dist/observable/share-latest-value.js +19 -16
  110. package/dist/observable/simple-timeout.d.ts +4 -0
  111. package/dist/observable/simple-timeout.js +6 -0
  112. package/dist/promise/deferred.d.ts +1 -0
  113. package/dist/promise/deferred.js +1 -0
  114. package/dist/queries/blossom.d.ts +2 -0
  115. package/dist/queries/blossom.js +10 -0
  116. package/dist/queries/bookmarks.d.ts +8 -0
  117. package/dist/queries/bookmarks.js +23 -0
  118. package/dist/queries/channels.d.ts +11 -0
  119. package/dist/queries/channels.js +73 -0
  120. package/dist/queries/comments.d.ts +4 -0
  121. package/dist/queries/comments.js +14 -0
  122. package/dist/queries/contacts.d.ts +3 -0
  123. package/dist/queries/contacts.js +12 -0
  124. package/dist/queries/index.d.ts +9 -2
  125. package/dist/queries/index.js +9 -2
  126. package/dist/queries/mailboxes.d.ts +1 -0
  127. package/dist/queries/mailboxes.js +1 -0
  128. package/dist/queries/mutes.d.ts +8 -0
  129. package/dist/queries/mutes.js +23 -0
  130. package/dist/queries/pins.d.ts +3 -0
  131. package/dist/queries/pins.js +12 -0
  132. package/dist/queries/profile.d.ts +1 -0
  133. package/dist/queries/profile.js +1 -0
  134. package/dist/queries/reactions.d.ts +1 -1
  135. package/dist/queries/reactions.js +1 -1
  136. package/dist/queries/simple.d.ts +4 -4
  137. package/dist/queries/simple.js +13 -13
  138. package/dist/queries/thread.d.ts +2 -0
  139. package/dist/queries/thread.js +30 -4
  140. package/dist/queries/user-status.d.ts +11 -0
  141. package/dist/queries/user-status.js +39 -0
  142. package/dist/queries/zaps.d.ts +1 -0
  143. package/dist/queries/zaps.js +1 -0
  144. package/dist/query-store/index.d.ts +1 -47
  145. package/dist/query-store/index.js +1 -60
  146. package/dist/query-store/query-store.d.ts +51 -0
  147. package/dist/query-store/query-store.js +88 -0
  148. package/dist/query-store/query-store.test.d.ts +1 -0
  149. package/dist/query-store/query-store.test.js +33 -0
  150. package/package.json +24 -21
  151. package/dist/observable/getValue.d.ts +0 -2
  152. package/dist/observable/getValue.js +0 -13
  153. /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 of the database */
21
- deleted: Subject<import("nostr-tools").Event>;
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
- hasEvent(uid: string): import("nostr-tools").Event | undefined;
31
- getEvent(uid: string): import("nostr-tools").Event | undefined;
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 a replaceable event and touches it */
35
- getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
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): import("nostr-tools").Event;
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): import("nostr-tools").Event;
40
- /** Deletes an event from the database and notifies all subscriptions */
41
- deleteEvent(eventOrUID: string | NostrEvent): boolean;
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<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>;
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<import("nostr-tools").Event>;
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 of the database */
24
- deleted = new Subject();
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(getEventUID(event), event);
63
+ this.events.set(event.id, event);
60
64
  }
61
- hasEvent(uid) {
62
- return this.events.get(uid);
65
+ /** Checks if the database contains an event without touching it */
66
+ hasEvent(id) {
67
+ return this.events.has(id);
63
68
  }
64
- getEvent(uid) {
65
- return this.events.get(uid);
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
- return this.events.has(getReplaceableUID(kind, pubkey, d));
75
+ const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d));
76
+ return !!events && events.length > 0;
70
77
  }
71
- /** Gets a replaceable event and touches it */
78
+ /** Gets an array of replaceable events */
72
79
  getReplaceable(kind, pubkey, d) {
73
- return this.events.get(getReplaceableUID(kind, pubkey, d));
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 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
- }
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.events.set(uid, event);
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
- /** Deletes an event from the database and notifies all subscriptions */
106
- deleteEvent(eventOrUID) {
107
- let event = typeof eventOrUID === "string" ? this.events.get(eventOrUID) : eventOrUID;
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 uid = getEventUID(event);
127
+ const id = event.id;
111
128
  // only remove events that are known
112
- if (!this.events.has(uid))
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(uid);
125
- this.deleted.next(event);
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 && start[1])
214
+ if (start)
188
215
  untilIndex = start[0];
189
216
  const end = since
190
217
  ? binarySearch(this.created_at, (mid) => {
191
- if (mid.created_at === since)
192
- return 1;
193
- return since - mid.created_at;
218
+ return mid.created_at - since;
194
219
  })
195
220
  : undefined;
196
- if (end && end[1])
221
+ if (end)
197
222
  sinceIndex = end[0];
198
- const events = new Set();
199
- for (let i = untilIndex; i <= sinceIndex; i++) {
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.deleteEvent(event);
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
- /** Adds an event to the database */
8
- add(event: NostrEvent, fromRelay?: string): import("nostr-tools").Event;
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): 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;
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
- 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>;
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(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>;
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<import("nostr-tools").Event[]>;
62
+ timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
26
63
  }