applesauce-core 1.2.0 → 2.1.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 (204) hide show
  1. package/README.md +7 -13
  2. package/dist/event-store/{database.d.ts → event-set.d.ts} +36 -21
  3. package/dist/event-store/{database.js → event-set.js} +98 -67
  4. package/dist/event-store/event-store.d.ts +64 -25
  5. package/dist/event-store/event-store.js +164 -207
  6. package/dist/event-store/index.d.ts +1 -1
  7. package/dist/event-store/index.js +1 -1
  8. package/dist/event-store/interface.d.ts +71 -13
  9. package/dist/helpers/app-handlers.d.ts +23 -0
  10. package/dist/helpers/app-handlers.js +68 -0
  11. package/dist/helpers/article.d.ts +9 -0
  12. package/dist/helpers/article.js +21 -0
  13. package/dist/helpers/bolt11.d.ts +1 -0
  14. package/dist/helpers/bolt11.js +2 -0
  15. package/dist/helpers/bookmarks.js +1 -2
  16. package/dist/helpers/emoji.d.ts +10 -2
  17. package/dist/helpers/emoji.js +21 -3
  18. package/dist/helpers/encrypted-content-cache.d.ts +15 -0
  19. package/dist/helpers/encrypted-content-cache.js +125 -0
  20. package/dist/helpers/encrypted-content.d.ts +48 -0
  21. package/dist/helpers/encrypted-content.js +65 -0
  22. package/dist/helpers/encryption.d.ts +5 -0
  23. package/dist/helpers/encryption.js +10 -0
  24. package/dist/helpers/event.d.ts +5 -8
  25. package/dist/helpers/event.js +25 -11
  26. package/dist/helpers/expiration.d.ts +6 -0
  27. package/dist/helpers/expiration.js +16 -0
  28. package/dist/helpers/filter.d.ts +1 -3
  29. package/dist/helpers/filter.js +1 -3
  30. package/dist/helpers/gift-wraps.d.ts +17 -5
  31. package/dist/helpers/gift-wraps.js +65 -27
  32. package/dist/helpers/groups.d.ts +5 -0
  33. package/dist/helpers/groups.js +12 -2
  34. package/dist/helpers/hidden-content.d.ts +27 -32
  35. package/dist/helpers/hidden-content.js +35 -65
  36. package/dist/helpers/hidden-tags.d.ts +23 -4
  37. package/dist/helpers/hidden-tags.js +39 -4
  38. package/dist/helpers/index.d.ts +11 -1
  39. package/dist/helpers/index.js +11 -1
  40. package/dist/helpers/legacy-messages.d.ts +21 -0
  41. package/dist/helpers/legacy-messages.js +39 -0
  42. package/dist/helpers/lists.d.ts +3 -1
  43. package/dist/helpers/lists.js +9 -3
  44. package/dist/helpers/messages.d.ts +11 -0
  45. package/dist/helpers/messages.js +19 -0
  46. package/dist/helpers/mutes.js +1 -1
  47. package/dist/helpers/pointers.d.ts +33 -9
  48. package/dist/helpers/pointers.js +80 -44
  49. package/dist/helpers/profile.d.ts +10 -2
  50. package/dist/helpers/profile.js +33 -4
  51. package/dist/helpers/reactions.d.ts +8 -0
  52. package/dist/helpers/reactions.js +56 -0
  53. package/dist/helpers/reports.d.ts +28 -0
  54. package/dist/helpers/reports.js +38 -0
  55. package/dist/helpers/share.d.ts +10 -1
  56. package/dist/helpers/share.js +22 -8
  57. package/dist/helpers/url.d.ts +4 -0
  58. package/dist/helpers/url.js +20 -0
  59. package/dist/helpers/user-status.js +2 -1
  60. package/dist/helpers/wrapped-messages.d.ts +23 -0
  61. package/dist/helpers/wrapped-messages.js +38 -0
  62. package/dist/helpers/zap.d.ts +8 -5
  63. package/dist/helpers/zap.js +11 -6
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.js +2 -2
  66. package/dist/models/blossom.d.ts +3 -0
  67. package/dist/models/blossom.js +8 -0
  68. package/dist/models/bookmarks.d.ts +8 -0
  69. package/dist/{queries → models}/bookmarks.js +9 -9
  70. package/dist/models/channels.d.ts +11 -0
  71. package/dist/{queries → models}/channels.js +9 -9
  72. package/dist/models/comments.d.ts +4 -0
  73. package/dist/models/comments.js +11 -0
  74. package/dist/models/common.d.ts +16 -0
  75. package/dist/models/common.js +176 -0
  76. package/dist/models/contacts.d.ts +8 -0
  77. package/dist/{queries → models}/contacts.js +10 -10
  78. package/dist/models/encrypted-content.d.ts +4 -0
  79. package/dist/models/encrypted-content.js +11 -0
  80. package/dist/models/gift-wrap.d.ts +7 -0
  81. package/dist/models/gift-wrap.js +20 -0
  82. package/dist/{queries → models}/index.d.ts +6 -2
  83. package/dist/{queries → models}/index.js +6 -2
  84. package/dist/models/legacy-messages.d.ts +8 -0
  85. package/dist/models/legacy-messages.js +29 -0
  86. package/dist/models/mailboxes.d.ts +6 -0
  87. package/dist/{queries → models}/mailboxes.js +2 -2
  88. package/dist/models/mutes.d.ts +8 -0
  89. package/dist/{queries → models}/mutes.js +9 -9
  90. package/dist/models/pins.d.ts +4 -0
  91. package/dist/{queries → models}/pins.js +3 -3
  92. package/dist/models/profile.d.ts +4 -0
  93. package/dist/models/profile.js +14 -0
  94. package/dist/models/reactions.d.ts +4 -0
  95. package/dist/{queries → models}/reactions.js +2 -2
  96. package/dist/models/relays.d.ts +27 -0
  97. package/dist/{queries → models}/relays.js +13 -13
  98. package/dist/{queries → models}/thread.d.ts +6 -5
  99. package/dist/{queries → models}/thread.js +4 -3
  100. package/dist/models/user-status.d.ts +11 -0
  101. package/dist/{queries → models}/user-status.js +5 -5
  102. package/dist/models/wrapped-messages.d.ts +25 -0
  103. package/dist/models/wrapped-messages.js +61 -0
  104. package/dist/models/zaps.d.ts +9 -0
  105. package/dist/{queries → models}/zaps.js +11 -3
  106. package/dist/observable/claim-events.d.ts +3 -3
  107. package/dist/observable/claim-events.js +4 -4
  108. package/dist/observable/claim-latest.d.ts +3 -3
  109. package/dist/observable/claim-latest.js +4 -4
  110. package/dist/observable/index.d.ts +3 -1
  111. package/dist/observable/index.js +3 -1
  112. package/dist/observable/map-events-timeline.d.ts +7 -0
  113. package/dist/observable/map-events-timeline.js +9 -0
  114. package/dist/observable/map-events-to-store.d.ts +5 -0
  115. package/dist/observable/map-events-to-store.js +12 -0
  116. package/dist/observable/simple-timeout.d.ts +1 -0
  117. package/dist/observable/simple-timeout.js +1 -0
  118. package/dist/observable/watch-event-updates.d.ts +7 -0
  119. package/dist/observable/watch-event-updates.js +25 -0
  120. package/package.json +11 -16
  121. package/dist/__tests__/exports.test.d.ts +0 -1
  122. package/dist/__tests__/exports.test.js +0 -17
  123. package/dist/__tests__/fixtures.d.ts +0 -8
  124. package/dist/__tests__/fixtures.js +0 -20
  125. package/dist/event-store/__tests__/event-store.test.d.ts +0 -1
  126. package/dist/event-store/__tests__/event-store.test.js +0 -354
  127. package/dist/helpers/__tests__/blossom.test.d.ts +0 -1
  128. package/dist/helpers/__tests__/blossom.test.js +0 -13
  129. package/dist/helpers/__tests__/bookmarks.test.d.ts +0 -1
  130. package/dist/helpers/__tests__/bookmarks.test.js +0 -88
  131. package/dist/helpers/__tests__/comment.test.d.ts +0 -1
  132. package/dist/helpers/__tests__/comment.test.js +0 -249
  133. package/dist/helpers/__tests__/contacts.test.d.ts +0 -1
  134. package/dist/helpers/__tests__/contacts.test.js +0 -34
  135. package/dist/helpers/__tests__/emoji.test.d.ts +0 -1
  136. package/dist/helpers/__tests__/emoji.test.js +0 -15
  137. package/dist/helpers/__tests__/event.test.d.ts +0 -1
  138. package/dist/helpers/__tests__/event.test.js +0 -36
  139. package/dist/helpers/__tests__/events.test.d.ts +0 -1
  140. package/dist/helpers/__tests__/events.test.js +0 -32
  141. package/dist/helpers/__tests__/exports.test.d.ts +0 -1
  142. package/dist/helpers/__tests__/exports.test.js +0 -220
  143. package/dist/helpers/__tests__/file-metadata.test.d.ts +0 -1
  144. package/dist/helpers/__tests__/file-metadata.test.js +0 -103
  145. package/dist/helpers/__tests__/hidden-tags.test.d.ts +0 -1
  146. package/dist/helpers/__tests__/hidden-tags.test.js +0 -29
  147. package/dist/helpers/__tests__/mailboxes.test.d.ts +0 -1
  148. package/dist/helpers/__tests__/mailboxes.test.js +0 -81
  149. package/dist/helpers/__tests__/mutes.test.d.ts +0 -1
  150. package/dist/helpers/__tests__/mutes.test.js +0 -55
  151. package/dist/helpers/__tests__/nip-19.test.d.ts +0 -1
  152. package/dist/helpers/__tests__/nip-19.test.js +0 -42
  153. package/dist/helpers/__tests__/relays.test.d.ts +0 -1
  154. package/dist/helpers/__tests__/relays.test.js +0 -21
  155. package/dist/helpers/__tests__/tags.test.d.ts +0 -1
  156. package/dist/helpers/__tests__/tags.test.js +0 -24
  157. package/dist/helpers/__tests__/threading.test.d.ts +0 -1
  158. package/dist/helpers/__tests__/threading.test.js +0 -41
  159. package/dist/helpers/direct-messages.d.ts +0 -4
  160. package/dist/helpers/direct-messages.js +0 -5
  161. package/dist/helpers/nip-19.d.ts +0 -18
  162. package/dist/helpers/nip-19.js +0 -56
  163. package/dist/observable/__tests__/claim-events.test.d.ts +0 -1
  164. package/dist/observable/__tests__/claim-events.test.js +0 -23
  165. package/dist/observable/__tests__/claim-latest.test.d.ts +0 -1
  166. package/dist/observable/__tests__/claim-latest.test.js +0 -37
  167. package/dist/observable/__tests__/exports.test.d.ts +0 -1
  168. package/dist/observable/__tests__/exports.test.js +0 -18
  169. package/dist/observable/__tests__/listen-latest-updates.test.d.ts +0 -1
  170. package/dist/observable/__tests__/listen-latest-updates.test.js +0 -55
  171. package/dist/observable/__tests__/simple-timeout.test.d.ts +0 -1
  172. package/dist/observable/__tests__/simple-timeout.test.js +0 -34
  173. package/dist/observable/listen-latest-updates.d.ts +0 -5
  174. package/dist/observable/listen-latest-updates.js +0 -12
  175. package/dist/promise/__tests__/exports.test.d.ts +0 -1
  176. package/dist/promise/__tests__/exports.test.js +0 -11
  177. package/dist/queries/__tests__/exports.test.d.ts +0 -1
  178. package/dist/queries/__tests__/exports.test.js +0 -41
  179. package/dist/queries/blossom.d.ts +0 -2
  180. package/dist/queries/blossom.js +0 -5
  181. package/dist/queries/bookmarks.d.ts +0 -8
  182. package/dist/queries/channels.d.ts +0 -11
  183. package/dist/queries/comments.d.ts +0 -4
  184. package/dist/queries/comments.js +0 -11
  185. package/dist/queries/contacts.d.ts +0 -8
  186. package/dist/queries/mailboxes.d.ts +0 -6
  187. package/dist/queries/mutes.d.ts +0 -8
  188. package/dist/queries/pins.d.ts +0 -4
  189. package/dist/queries/profile.d.ts +0 -4
  190. package/dist/queries/profile.js +0 -7
  191. package/dist/queries/reactions.d.ts +0 -4
  192. package/dist/queries/relays.d.ts +0 -27
  193. package/dist/queries/simple.d.ts +0 -16
  194. package/dist/queries/simple.js +0 -21
  195. package/dist/queries/user-status.d.ts +0 -11
  196. package/dist/queries/zaps.d.ts +0 -5
  197. package/dist/query-store/__tests__/exports.test.d.ts +0 -1
  198. package/dist/query-store/__tests__/exports.test.js +0 -12
  199. package/dist/query-store/__tests__/query-store.test.d.ts +0 -1
  200. package/dist/query-store/__tests__/query-store.test.js +0 -63
  201. package/dist/query-store/index.d.ts +0 -1
  202. package/dist/query-store/index.js +0 -1
  203. package/dist/query-store/query-store.d.ts +0 -54
  204. package/dist/query-store/query-store.js +0 -102
@@ -1,49 +1,59 @@
1
1
  import { kinds } from "nostr-tools";
2
- import { insertEventIntoDescendingList } from "nostr-tools/utils";
3
2
  import { isAddressableKind } from "nostr-tools/kinds";
4
- import { defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, from, map, merge, mergeMap, mergeWith, of, repeat, scan, take, takeUntil, tap, } from "rxjs";
5
- import { Database } from "./database.js";
6
- import { FromCacheSymbol, getEventUID, getReplaceableIdentifier, createReplaceableAddress, getTagValue, isReplaceable, } from "../helpers/event.js";
7
- import { matchFilters } from "../helpers/filter.js";
8
- import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
3
+ import { EMPTY, filter, finalize, from, merge, mergeMap, ReplaySubject, share, take, timer } from "rxjs";
4
+ import hash_sum from "hash-sum";
9
5
  import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
10
- import { claimEvents } from "../observable/claim-events.js";
11
- import { claimLatest } from "../observable/claim-latest.js";
6
+ import { FromCacheSymbol, getReplaceableAddress, getTagValue, isReplaceable } from "../helpers/event.js";
7
+ import { matchFilters } from "../helpers/filter.js";
12
8
  import { parseCoordinate } from "../helpers/pointers.js";
9
+ import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
10
+ import { EventModel, EventsModel, ReplaceableModel, ReplaceableSetModel, TimelineModel } from "../models/common.js";
11
+ import { EventSet } from "./event-set.js";
12
+ import { ProfileModel } from "../models/profile.js";
13
+ import { ContactsModel } from "../models/contacts.js";
14
+ import { MuteModel } from "../models/mutes.js";
15
+ import { ReactionsModel } from "../models/reactions.js";
16
+ import { MailboxesModel } from "../models/mailboxes.js";
17
+ import { UserBlossomServersModel } from "../models/blossom.js";
18
+ import { CommentsModel, ThreadModel } from "../models/index.js";
19
+ /** A symbol on an event that marks which event store its part of */
13
20
  export const EventStoreSymbol = Symbol.for("event-store");
14
- function sortDesc(a, b) {
15
- return b.created_at - a.created_at;
16
- }
17
21
  export class EventStore {
18
22
  database;
19
23
  /** Enable this to keep old versions of replaceable events */
20
24
  keepOldVersions = false;
21
- /** A method used to verify new events before added them */
25
+ /**
26
+ * A method used to verify new events before added them
27
+ * @returns true if the event is valid, false if it should be ignored
28
+ */
22
29
  verifyEvent;
23
30
  /** A stream of new events added to the store */
24
- inserts;
31
+ insert$;
25
32
  /** A stream of events that have been updated */
26
- updates;
33
+ update$;
27
34
  /** A stream of events that have been removed */
28
- removes;
35
+ remove$;
29
36
  constructor() {
30
- this.database = new Database();
37
+ this.database = new EventSet();
38
+ // verify events before they are added to the database
31
39
  this.database.onBeforeInsert = (event) => {
32
- // reject events that are invalid
40
+ // Ignore events that are invalid
33
41
  if (this.verifyEvent && this.verifyEvent(event) === false)
34
- throw new Error("Invalid event");
42
+ return false;
43
+ else
44
+ return true;
35
45
  };
36
46
  // when events are added to the database, add the symbol
37
- this.database.inserted.subscribe((event) => {
47
+ this.database.insert$.subscribe((event) => {
38
48
  Reflect.set(event, EventStoreSymbol, this);
39
49
  });
40
50
  // when events are removed from the database, remove the symbol
41
- this.database.removed.subscribe((event) => {
51
+ this.database.remove$.subscribe((event) => {
42
52
  Reflect.deleteProperty(event, EventStoreSymbol);
43
53
  });
44
- this.inserts = this.database.inserted;
45
- this.updates = this.database.updated;
46
- this.removes = this.database.removed;
54
+ this.insert$ = this.database.insert$;
55
+ this.update$ = this.database.update$;
56
+ this.remove$ = this.database.remove$;
47
57
  }
48
58
  // delete state
49
59
  deletedIds = new Set();
@@ -55,7 +65,7 @@ export class EventStore {
55
65
  if (this.deletedIds.has(event.id))
56
66
  return true;
57
67
  if (isAddressableKind(event.kind)) {
58
- const deleted = this.deletedCoords.get(getEventUID(event));
68
+ const deleted = this.deletedCoords.get(getReplaceableAddress(event));
59
69
  if (deleted)
60
70
  return deleted > event.created_at;
61
71
  }
@@ -70,7 +80,7 @@ export class EventStore {
70
80
  // remove deleted events in the database
71
81
  const event = this.database.getEvent(id);
72
82
  if (event)
73
- this.database.removeEvent(event);
83
+ this.database.remove(event);
74
84
  }
75
85
  const coords = getDeleteCoordinates(deleteEvent);
76
86
  for (const coord of coords) {
@@ -80,10 +90,10 @@ export class EventStore {
80
90
  if (!parsed)
81
91
  continue;
82
92
  // Remove older versions of replaceable events
83
- const events = this.database.getReplaceable(parsed.kind, parsed.pubkey, parsed.identifier) ?? [];
93
+ const events = this.database.getReplaceableHistory(parsed.kind, parsed.pubkey, parsed.identifier) ?? [];
84
94
  for (const event of events) {
85
95
  if (event.created_at < deleteEvent.created_at)
86
- this.database.removeEvent(event);
96
+ this.database.remove(event);
87
97
  }
88
98
  }
89
99
  }
@@ -100,8 +110,8 @@ export class EventStore {
100
110
  Reflect.set(dest, FromCacheSymbol, fromCache);
101
111
  }
102
112
  /**
103
- * Adds an event to the database and update subscriptions
104
- * @throws
113
+ * Adds an event to the store and update subscriptions
114
+ * @returns The existing event or the event that was added, if it was ignored returns null
105
115
  */
106
116
  add(event, fromRelay) {
107
117
  if (event.kind === kinds.EventDeletion)
@@ -113,7 +123,7 @@ export class EventStore {
113
123
  const d = isReplaceable(event.kind) ? getTagValue(event, "d") : undefined;
114
124
  // Don't insert the event if there is already a newer version
115
125
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
116
- const existing = this.database.getReplaceable(event.kind, event.pubkey, d);
126
+ const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, d);
117
127
  // If there is already a newer version, copy cached symbols and return existing event
118
128
  if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
119
129
  EventStore.mergeDuplicateEvent(event, existing[0]);
@@ -129,7 +139,10 @@ export class EventStore {
129
139
  }
130
140
  }
131
141
  // Insert event into database
132
- const inserted = this.database.addEvent(event);
142
+ const inserted = this.database.add(event);
143
+ // If the event was ignored, return null
144
+ if (inserted === null)
145
+ return null;
133
146
  // Copy cached data if its a duplicate event
134
147
  if (event !== inserted)
135
148
  EventStore.mergeDuplicateEvent(event, inserted);
@@ -138,11 +151,11 @@ export class EventStore {
138
151
  addSeenRelay(inserted, fromRelay);
139
152
  // remove all old version of the replaceable event
140
153
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
141
- const existing = this.database.getReplaceable(event.kind, event.pubkey, d);
154
+ const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, d);
142
155
  if (existing) {
143
156
  const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
144
157
  for (const old of older)
145
- this.database.removeEvent(old);
158
+ this.database.remove(old);
146
159
  // return the newest version of the replaceable event
147
160
  // most of the time this will be === event, but not always
148
161
  if (existing.length !== older.length)
@@ -153,24 +166,21 @@ export class EventStore {
153
166
  }
154
167
  /** Removes an event from the database and updates subscriptions */
155
168
  remove(event) {
156
- return this.database.removeEvent(event);
157
- }
158
- /** Removes any event that is not being used by a subscription */
159
- prune(max) {
160
- return this.database.prune(max);
169
+ return this.database.remove(event);
161
170
  }
162
171
  /** Add an event to the store and notifies all subscribes it has updated */
163
172
  update(event) {
164
- return this.database.updateEvent(event);
173
+ return this.database.update(event);
165
174
  }
166
- /** Get all events matching a filter */
167
- getAll(filters) {
168
- return this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
175
+ /** Removes any event that is not being used by a subscription */
176
+ prune(max) {
177
+ return this.database.prune(max);
169
178
  }
170
- /** Check if the store has an event */
179
+ /** Check if the store has an event by id */
171
180
  hasEvent(id) {
172
181
  return this.database.hasEvent(id);
173
182
  }
183
+ /** Get an event by id from the store */
174
184
  getEvent(id) {
175
185
  return this.database.getEvent(id);
176
186
  }
@@ -179,19 +189,73 @@ export class EventStore {
179
189
  return this.database.hasReplaceable(kind, pubkey, d);
180
190
  }
181
191
  /** Gets the latest version of a replaceable event */
182
- getReplaceable(kind, pubkey, d) {
183
- return this.database.getReplaceable(kind, pubkey, d)?.[0];
192
+ getReplaceable(kind, pubkey, identifier) {
193
+ return this.database.getReplaceable(kind, pubkey, identifier);
184
194
  }
185
195
  /** Returns all versions of a replaceable event */
186
- getReplaceableHistory(kind, pubkey, d) {
187
- return this.database.getReplaceable(kind, pubkey, d);
196
+ getReplaceableHistory(kind, pubkey, identifier) {
197
+ return this.database.getReplaceableHistory(kind, pubkey, identifier);
198
+ }
199
+ /** Get all events matching a filter */
200
+ getByFilters(filters) {
201
+ return this.database.getByFilters(filters);
188
202
  }
189
203
  /** Returns a timeline of events that match filters */
190
204
  getTimeline(filters) {
191
- return Array.from(this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters])).sort(sortDesc);
205
+ return this.database.getTimeline(filters);
206
+ }
207
+ /** Sets the claim on the event and touches it */
208
+ claim(event, claim) {
209
+ this.database.claim(event, claim);
210
+ }
211
+ /** Checks if an event is claimed by anything */
212
+ isClaimed(event) {
213
+ return this.database.isClaimed(event);
214
+ }
215
+ /** Removes a claim from an event */
216
+ removeClaim(event, claim) {
217
+ this.database.removeClaim(event, claim);
218
+ }
219
+ /** Removes all claims on an event */
220
+ clearClaim(event) {
221
+ this.database.clearClaim(event);
222
+ }
223
+ /** A directory of all active models */
224
+ models = new Map();
225
+ /** How long a model should be kept "warm" while nothing is subscribed to it */
226
+ modelKeepWarm = 60_000;
227
+ /** Get or create a model on the event store */
228
+ model(constructor, ...args) {
229
+ let models = this.models.get(constructor);
230
+ if (!models) {
231
+ models = new Map();
232
+ this.models.set(constructor, models);
233
+ }
234
+ const key = constructor.getKey ? constructor.getKey(...args) : hash_sum(args);
235
+ let model = models.get(key);
236
+ // Create the model if it does not exist
237
+ if (!model) {
238
+ const cleanup = () => {
239
+ // Remove the model from the cache if its the same one
240
+ if (models.get(key) === model)
241
+ models.delete(key);
242
+ };
243
+ model = constructor(...args)(this).pipe(
244
+ // remove the model when its unsubscribed
245
+ finalize(cleanup),
246
+ // only subscribe to models once for all subscriptions
247
+ share({
248
+ connector: () => new ReplaySubject(1),
249
+ resetOnComplete: () => timer(this.modelKeepWarm),
250
+ resetOnRefCountZero: () => timer(this.modelKeepWarm),
251
+ }));
252
+ // Add the model to the cache
253
+ models.set(key, model);
254
+ }
255
+ return model;
192
256
  }
193
257
  /**
194
- * Creates an observable that streams all events that match the filter and remains open
258
+ * Creates an observable that streams all events that match the filter
195
259
  * @param filters
196
260
  * @param [onlyNew=false] Only subscribe to new events
197
261
  */
@@ -199,16 +263,16 @@ export class EventStore {
199
263
  filters = Array.isArray(filters) ? filters : [filters];
200
264
  return merge(
201
265
  // merge existing events
202
- onlyNew ? EMPTY : from(this.getAll(filters)),
266
+ onlyNew ? EMPTY : from(this.getByFilters(filters)),
203
267
  // subscribe to future events
204
- this.inserts.pipe(filter((e) => matchFilters(filters, e))));
268
+ this.insert$.pipe(filter((e) => matchFilters(filters, e))));
205
269
  }
206
270
  /** Returns an observable that completes when an event is removed */
207
271
  removed(id) {
208
272
  const deleted = this.checkDeleted(id);
209
273
  if (deleted)
210
274
  return EMPTY;
211
- return this.removes.pipe(
275
+ return this.remove$.pipe(
212
276
  // listen for removed events
213
277
  filter((e) => e.id === id),
214
278
  // complete as soon as we find a matching removed event
@@ -218,166 +282,59 @@ export class EventStore {
218
282
  }
219
283
  /** Creates an observable that emits when event is updated */
220
284
  updated(event) {
221
- return this.database.updated.pipe(filter((e) => e.id === event || e === event));
285
+ return this.database.update$.pipe(filter((e) => e.id === event || e === event));
222
286
  }
223
- /** Creates an observable that subscribes to a single event */
287
+ // Helper methods for creating models
288
+ /** Creates a {@link EventModel} */
224
289
  event(id) {
225
- return merge(
226
- // get current event and ignore if there is none
227
- defer(() => {
228
- let event = this.getEvent(id);
229
- return event ? of(event) : EMPTY;
230
- }),
231
- // subscribe to updates
232
- this.inserts.pipe(filter((e) => e.id === id)),
233
- // subscribe to updates
234
- this.updated(id),
235
- // emit undefined when deleted
236
- this.removed(id).pipe(endWith(undefined))).pipe(
237
- // claim all events
238
- claimLatest(this.database));
239
- }
240
- /** Creates an observable that subscribes to multiple events */
290
+ return this.model(EventModel, id);
291
+ }
292
+ /** Creates a {@link ReplaceableModel} */
293
+ replaceable(kind, pubkey, identifier) {
294
+ return this.model(ReplaceableModel, kind, pubkey, identifier);
295
+ }
296
+ /** Creates a {@link TimelineModel} */
297
+ timeline(filters, includeOldVersion = false) {
298
+ return this.model(TimelineModel, filters, includeOldVersion);
299
+ }
300
+ /** Creates a {@link EventsModel} */
241
301
  events(ids) {
242
- return merge(
243
- // lazily get existing events
244
- defer(() => from(ids.map((id) => this.getEvent(id)))),
245
- // subscribe to new events
246
- this.inserts.pipe(filter((e) => ids.includes(e.id))),
247
- // subscribe to updates
248
- this.updates.pipe(filter((e) => ids.includes(e.id)))).pipe(
249
- // ignore empty messages
250
- filter((e) => !!e),
251
- // claim all events until cleanup
252
- claimEvents(this.database),
253
- // watch for removed events
254
- mergeWith(this.removes.pipe(filter((e) => ids.includes(e.id)), map((e) => e.id))),
255
- // merge all events into a directory
256
- scan((dir, event) => {
257
- if (typeof event === "string") {
258
- // delete event by id
259
- const clone = { ...dir };
260
- delete clone[event];
261
- return clone;
262
- }
263
- else {
264
- // add even to directory
265
- return { ...dir, [event.id]: event };
266
- }
267
- }, {}));
302
+ return this.model(EventsModel, ids);
268
303
  }
269
- /** Creates an observable that subscribes to the latest version of a replaceable event */
270
- replaceable(kind, pubkey, d) {
271
- let current = undefined;
272
- return merge(
273
- // lazily get current event
274
- defer(() => {
275
- let event = this.getReplaceable(kind, pubkey, d);
276
- return event ? of(event) : EMPTY;
277
- }),
278
- // subscribe to new events
279
- this.inserts.pipe(filter((e) => e.pubkey == pubkey && e.kind === kind && (d !== undefined ? getReplaceableIdentifier(e) === d : true)))).pipe(
280
- // only update if event is newer
281
- distinctUntilChanged((prev, event) => {
282
- // are the events the same? i.e. is the prev event older
283
- return prev.created_at >= event.created_at;
284
- }),
285
- // Hacky way to extract the current event so takeUntil can access it
286
- tap((event) => (current = event)),
287
- // complete when event is removed
288
- takeUntil(this.removes.pipe(filter((e) => e.id === current?.id))),
289
- // emit undefined when removed
290
- endWith(undefined),
291
- // keep the observable hot
292
- repeat(),
293
- // claim latest event
294
- claimLatest(this.database));
295
- }
296
- /** Creates an observable that subscribes to the latest version of an array of replaceable events*/
304
+ /** Creates a {@link ReplaceableSetModel} */
297
305
  replaceableSet(pointers) {
298
- const uids = new Set(pointers.map((p) => createReplaceableAddress(p.kind, p.pubkey, p.identifier)));
299
- return merge(
300
- // start with existing events
301
- defer(() => from(pointers.map((p) => this.getReplaceable(p.kind, p.pubkey, p.identifier)))),
302
- // subscribe to new events
303
- this.inserts.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))))).pipe(
304
- // filter out undefined
305
- filter((e) => !!e),
306
- // claim all events
307
- claimEvents(this.database),
308
- // convert events to add commands
309
- map((e) => ["add", e]),
310
- // watch for removed events
311
- mergeWith(this.removes.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))), map((e) => ["remove", e]))),
312
- // reduce events into directory
313
- scan((dir, [action, event]) => {
314
- const uid = getEventUID(event);
315
- if (action === "add") {
316
- // add event to dir if its newer
317
- if (!dir[uid] || dir[uid].created_at < event.created_at)
318
- return { ...dir, [uid]: event };
319
- }
320
- else if (action === "remove" && dir[uid] === event) {
321
- // remove event from dir
322
- let newDir = { ...dir };
323
- delete newDir[uid];
324
- return newDir;
325
- }
326
- return dir;
327
- }, {}),
328
- // ignore changes that do not modify the directory
329
- distinctUntilChanged());
306
+ return this.model(ReplaceableSetModel, pointers);
330
307
  }
331
- /** Creates an observable that updates with an array of sorted events */
332
- timeline(filters, keepOldVersions = false) {
333
- filters = Array.isArray(filters) ? filters : [filters];
334
- const seen = new Map();
335
- // get current events
336
- return defer(() => of(Array.from(this.database.getEventsForFilters(filters)).sort(sortDesc))).pipe(
337
- // claim existing events
338
- claimEvents(this.database),
339
- // subscribe to newer events
340
- mergeWith(this.inserts.pipe(filter((e) => matchFilters(filters, e)),
341
- // claim all new events
342
- claimEvents(this.database))),
343
- // subscribe to delete events
344
- mergeWith(this.removes.pipe(filter((e) => matchFilters(filters, e)), map((e) => e.id))),
345
- // build a timeline
346
- scan((timeline, event) => {
347
- // filter out removed events from timeline
348
- if (typeof event === "string")
349
- return timeline.filter((e) => e.id !== event);
350
- // initial timeline array
351
- if (Array.isArray(event)) {
352
- if (!keepOldVersions) {
353
- for (const e of event)
354
- if (isReplaceable(e.kind))
355
- seen.set(getEventUID(e), e);
356
- }
357
- return event;
358
- }
359
- // create a new timeline and insert the event into it
360
- let newTimeline = [...timeline];
361
- // remove old replaceable events if enabled
362
- if (!keepOldVersions && isReplaceable(event.kind)) {
363
- const uid = getEventUID(event);
364
- const existing = seen.get(uid);
365
- // if this is an older replaceable event, exit
366
- if (existing && event.created_at < existing.created_at)
367
- return timeline;
368
- // update latest version
369
- seen.set(uid, event);
370
- // remove old event from timeline
371
- if (existing)
372
- newTimeline.slice(newTimeline.indexOf(existing), 1);
373
- }
374
- // add event into timeline
375
- insertEventIntoDescendingList(newTimeline, event);
376
- return newTimeline;
377
- }, []),
378
- // ignore changes that do not modify the timeline instance
379
- distinctUntilChanged(),
380
- // hacky hack to clear seen on unsubscribe
381
- finalize(() => seen.clear()));
308
+ /** Creates a {@link ProfileModel} */
309
+ profile(pubkey) {
310
+ return this.model(ProfileModel, pubkey);
311
+ }
312
+ /** Creates a {@link ContactsModel} */
313
+ contacts(pubkey) {
314
+ return this.model(ContactsModel, pubkey);
315
+ }
316
+ /** Creates a {@link MuteModel} */
317
+ mutes(pubkey) {
318
+ return this.model(MuteModel, pubkey);
319
+ }
320
+ /** Creates a {@link ReactionsModel} */
321
+ reactions(event) {
322
+ return this.model(ReactionsModel, event);
323
+ }
324
+ /** Creates a {@link MailboxesModel} */
325
+ mailboxes(pubkey) {
326
+ return this.model(MailboxesModel, pubkey);
327
+ }
328
+ /** Creates a {@link UserBlossomServersModel} */
329
+ blossomServers(pubkey) {
330
+ return this.model(UserBlossomServersModel, pubkey);
331
+ }
332
+ /** Creates a {@link ThreadModel} */
333
+ thread(root) {
334
+ return this.model(ThreadModel, root);
335
+ }
336
+ /** Creates a {@link CommentsModel} */
337
+ comments(event) {
338
+ return this.model(CommentsModel, event);
382
339
  }
383
340
  }
@@ -1,3 +1,3 @@
1
1
  export * from "./event-store.js";
2
- export * from "./database.js";
2
+ export * from "./event-set.js";
3
3
  export * from "./interface.js";
@@ -1,3 +1,3 @@
1
1
  export * from "./event-store.js";
2
- export * from "./database.js";
2
+ export * from "./event-set.js";
3
3
  export * from "./interface.js";
@@ -1,21 +1,58 @@
1
1
  import { Filter, NostrEvent } from "nostr-tools";
2
+ import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
2
3
  import { Observable } from "rxjs";
3
- export interface ISyncEventStore {
4
+ import { LRU } from "../helpers/lru.js";
5
+ import { Mutes } from "../helpers/mutes.js";
6
+ import { ProfileContent } from "../helpers/profile.js";
7
+ import { Thread } from "../models/thread.js";
8
+ /** The read interface for an event store */
9
+ export interface IEventStoreRead {
10
+ /** Check if the event store has an event with id */
4
11
  hasEvent(id: string): boolean;
12
+ /** Check if the event store has a replaceable event */
5
13
  hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
14
+ /** Get an event by id */
6
15
  getEvent(id: string): NostrEvent | undefined;
16
+ /** Get a replaceable event */
7
17
  getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
18
+ /** Get the history of a replaceable event */
8
19
  getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
9
- getAll(filters: Filter | Filter[]): Set<NostrEvent>;
20
+ /** Get all events that match the filters */
21
+ getByFilters(filters: Filter | Filter[]): Set<NostrEvent>;
22
+ /** Get a timeline of events that match the filters */
10
23
  getTimeline(filters: Filter | Filter[]): NostrEvent[];
11
24
  }
12
- export interface IStreamEventStore {
13
- inserts: Observable<NostrEvent>;
14
- updates: Observable<NostrEvent>;
15
- removes: Observable<NostrEvent>;
16
- filters(filters: Filter | Filter[]): Observable<NostrEvent>;
17
- updated(id: string | NostrEvent): Observable<NostrEvent>;
18
- removed(id: string): Observable<never>;
25
+ /** The stream interface for an event store */
26
+ export interface IEventStoreStreams {
27
+ /** A stream of new events added to the store */
28
+ insert$: Observable<NostrEvent>;
29
+ /** A stream of events that have been updated */
30
+ update$: Observable<NostrEvent>;
31
+ /** A stream of events that have been removed */
32
+ remove$: Observable<NostrEvent>;
33
+ }
34
+ /** The actions for an event store */
35
+ export interface IEventStoreActions {
36
+ /** Add an event to the store */
37
+ add(event: NostrEvent): NostrEvent | null;
38
+ /** Remove an event from the store */
39
+ remove(event: string | NostrEvent): boolean;
40
+ /** Notify the store that an event has updated */
41
+ update(event: NostrEvent): void;
42
+ }
43
+ /** The claim interface for an event store */
44
+ export interface IEventClaims {
45
+ /** Sets the claim on the event and touches it */
46
+ claim(event: NostrEvent, claim: any): void;
47
+ /** Checks if an event is claimed by anything */
48
+ isClaimed(event: NostrEvent): boolean;
49
+ /** Removes a claim from an event */
50
+ removeClaim(event: NostrEvent, claim: any): void;
51
+ /** Removes all claims on an event */
52
+ clearClaim(event: NostrEvent): void;
53
+ }
54
+ /** Methods for creating common models */
55
+ export interface IEventStoreModels {
19
56
  event(id: string): Observable<NostrEvent | undefined>;
20
57
  events(ids: string[]): Observable<Record<string, NostrEvent>>;
21
58
  replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
@@ -25,9 +62,30 @@ export interface IStreamEventStore {
25
62
  identifier?: string;
26
63
  }[]): Observable<Record<string, NostrEvent>>;
27
64
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
65
+ profile(pubkey: string): Observable<ProfileContent | undefined>;
66
+ contacts(pubkey: string): Observable<ProfilePointer[]>;
67
+ mutes(pubkey: string): Observable<Mutes | undefined>;
68
+ reactions(event: NostrEvent): Observable<NostrEvent[]>;
69
+ mailboxes(pubkey: string): Observable<{
70
+ inboxes: string[];
71
+ outboxes: string[];
72
+ } | undefined>;
73
+ blossomServers(pubkey: string): Observable<URL[]>;
74
+ thread(root: string | EventPointer | AddressPointer): Observable<Thread>;
28
75
  }
29
- export interface IEventStore extends ISyncEventStore, IStreamEventStore {
30
- add(event: NostrEvent, fromRelay?: string): NostrEvent;
31
- remove(event: string | NostrEvent): boolean;
32
- update(event: NostrEvent): NostrEvent;
76
+ /** A computed view of an event set or event store */
77
+ export type Model<T extends unknown> = (events: IEventStore) => Observable<T>;
78
+ /** A constructor for a {@link Model} */
79
+ export type ModelConstructor<T extends unknown, Args extends Array<any>> = ((...args: Args) => Model<T>) & {
80
+ getKey?: (...args: Args) => string;
81
+ };
82
+ /** The base interface for a set of events */
83
+ export interface IEventSet extends IEventStoreRead, IEventStoreStreams, IEventStoreActions, IEventClaims {
84
+ events: LRU<NostrEvent>;
85
+ }
86
+ export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventStoreActions, IEventStoreModels, IEventClaims {
87
+ filters(filters: Filter | Filter[]): Observable<NostrEvent>;
88
+ updated(id: string | NostrEvent): Observable<NostrEvent>;
89
+ removed(id: string): Observable<never>;
90
+ model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args>, ...args: Args): Observable<T>;
33
91
  }
@@ -0,0 +1,23 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
3
+ import { DecodeResult } from "./pointers.js";
4
+ export type HandlerLinkPlatform = "web" | "ios" | "android";
5
+ export type HandlerLinkType = DecodeResult["type"];
6
+ /** Returns an array of supported kinds for a given handler */
7
+ export declare function getHandlerSupportedKinds(handler: NostrEvent): number[];
8
+ /** Returns the name of the handler */
9
+ export declare function getHandlerName(handler: NostrEvent): string;
10
+ /** Returns the picture of the handler */
11
+ export declare function getHandlerPicture(handler: NostrEvent, fallback?: string): string | undefined;
12
+ /** Returns the description of the handler */
13
+ export declare function getHandlerDescription(handler: NostrEvent): string | undefined;
14
+ /** Returns the web link template for a handler and type */
15
+ export declare function getHandlerLinkTemplate(handler: NostrEvent, platform?: HandlerLinkPlatform, type?: HandlerLinkType): string | undefined;
16
+ /** Returns a link for a Profile Pointer */
17
+ export declare function createHandlerProfileLink(handler: NostrEvent, pointer: ProfilePointer, platform?: HandlerLinkPlatform): string | undefined;
18
+ /** Returns a link for an Event Pointer */
19
+ export declare function createHandlerEventLink(handler: NostrEvent, pointer: EventPointer, platform?: HandlerLinkPlatform): string | undefined;
20
+ /** Returns a link for an Address Pointer */
21
+ export declare function createHandlerAddressLink(handler: NostrEvent, pointer: AddressPointer, platform?: HandlerLinkPlatform): string | undefined;
22
+ /** Creates a handler link for a pointer and optionally fallsback to a web link */
23
+ export declare function createHandlerLink(handler: NostrEvent, pointer: AddressPointer | EventPointer | ProfilePointer, platform?: HandlerLinkPlatform, webFallback?: boolean): string | undefined;