applesauce-core 0.10.0 → 0.12.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 (139) hide show
  1. package/dist/__tests__/fixtures.d.ts +8 -0
  2. package/dist/__tests__/fixtures.js +20 -0
  3. package/dist/event-store/__tests__/event-store.test.js +272 -0
  4. package/dist/event-store/database.d.ts +7 -5
  5. package/dist/event-store/database.js +14 -8
  6. package/dist/event-store/event-store.d.ts +40 -20
  7. package/dist/event-store/event-store.js +269 -314
  8. package/dist/event-store/index.d.ts +1 -0
  9. package/dist/event-store/index.js +1 -0
  10. package/dist/event-store/interface.d.ts +27 -0
  11. package/dist/helpers/__tests__/blossom.test.js +13 -0
  12. package/dist/helpers/__tests__/comment.test.d.ts +1 -0
  13. package/dist/helpers/__tests__/comment.test.js +235 -0
  14. package/dist/helpers/__tests__/emoji.test.d.ts +1 -0
  15. package/dist/helpers/__tests__/emoji.test.js +15 -0
  16. package/dist/helpers/__tests__/event.test.d.ts +1 -0
  17. package/dist/helpers/__tests__/event.test.js +36 -0
  18. package/dist/helpers/__tests__/file-metadata.test.d.ts +1 -0
  19. package/dist/helpers/__tests__/file-metadata.test.js +103 -0
  20. package/dist/helpers/__tests__/hidden-tags.test.d.ts +1 -0
  21. package/dist/helpers/{hidden-tags.test.js → __tests__/hidden-tags.test.js} +2 -1
  22. package/dist/helpers/__tests__/mailboxes.test.d.ts +1 -0
  23. package/dist/helpers/{mailboxes.test.js → __tests__/mailboxes.test.js} +1 -1
  24. package/dist/helpers/__tests__/nip-19.test.d.ts +1 -0
  25. package/dist/helpers/__tests__/nip-19.test.js +42 -0
  26. package/dist/helpers/__tests__/relays.test.d.ts +1 -0
  27. package/dist/helpers/__tests__/relays.test.js +21 -0
  28. package/dist/helpers/__tests__/tags.test.d.ts +1 -0
  29. package/dist/helpers/__tests__/tags.test.js +24 -0
  30. package/dist/helpers/__tests__/threading.test.d.ts +1 -0
  31. package/dist/helpers/{threading.test.js → __tests__/threading.test.js} +1 -1
  32. package/dist/helpers/blossom.d.ts +9 -0
  33. package/dist/helpers/blossom.js +22 -0
  34. package/dist/helpers/bookmarks.d.ts +15 -0
  35. package/dist/helpers/bookmarks.js +27 -0
  36. package/dist/helpers/cache.d.ts +3 -4
  37. package/dist/helpers/cache.js +1 -1
  38. package/dist/helpers/channels.d.ts +10 -0
  39. package/dist/helpers/channels.js +27 -0
  40. package/dist/helpers/comment.d.ts +3 -4
  41. package/dist/helpers/comment.js +20 -16
  42. package/dist/helpers/contacts.d.ts +3 -0
  43. package/dist/helpers/contacts.js +25 -0
  44. package/dist/helpers/direct-messages.d.ts +4 -0
  45. package/dist/helpers/direct-messages.js +5 -0
  46. package/dist/helpers/dns-identity.d.ts +7 -0
  47. package/dist/helpers/dns-identity.js +10 -0
  48. package/dist/helpers/emoji.d.ts +3 -1
  49. package/dist/helpers/emoji.js +2 -2
  50. package/dist/helpers/event.d.ts +15 -1
  51. package/dist/helpers/event.js +34 -11
  52. package/dist/helpers/file-metadata.d.ts +55 -0
  53. package/dist/helpers/file-metadata.js +99 -0
  54. package/dist/helpers/filter.d.ts +4 -0
  55. package/dist/helpers/filter.js +34 -1
  56. package/dist/helpers/gift-wraps.d.ts +12 -0
  57. package/dist/helpers/gift-wraps.js +49 -0
  58. package/dist/helpers/groups.d.ts +24 -0
  59. package/dist/helpers/groups.js +39 -0
  60. package/dist/helpers/hidden-content.d.ts +48 -0
  61. package/dist/helpers/hidden-content.js +88 -0
  62. package/dist/helpers/hidden-tags.d.ts +17 -35
  63. package/dist/helpers/hidden-tags.js +26 -83
  64. package/dist/helpers/index.d.ts +16 -1
  65. package/dist/helpers/index.js +16 -1
  66. package/dist/helpers/lists.d.ts +28 -0
  67. package/dist/helpers/lists.js +65 -0
  68. package/dist/helpers/mailboxes.js +16 -9
  69. package/dist/helpers/mutes.d.ts +15 -0
  70. package/dist/helpers/mutes.js +24 -0
  71. package/dist/helpers/nip-19.d.ts +4 -0
  72. package/dist/helpers/nip-19.js +27 -0
  73. package/dist/helpers/picture-post.d.ts +4 -0
  74. package/dist/helpers/picture-post.js +6 -0
  75. package/dist/helpers/pointers.js +13 -17
  76. package/dist/helpers/profile.d.ts +6 -1
  77. package/dist/helpers/profile.js +4 -0
  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/tags.d.ts +17 -0
  83. package/dist/helpers/tags.js +28 -6
  84. package/dist/helpers/threading.js +3 -3
  85. package/dist/helpers/url.d.ts +7 -0
  86. package/dist/helpers/url.js +27 -0
  87. package/dist/helpers/user-status.d.ts +18 -0
  88. package/dist/helpers/user-status.js +21 -0
  89. package/dist/observable/__tests__/claim-events.test.d.ts +1 -0
  90. package/dist/observable/__tests__/claim-events.test.js +23 -0
  91. package/dist/observable/__tests__/claim-latest.test.d.ts +1 -0
  92. package/dist/observable/__tests__/claim-latest.test.js +37 -0
  93. package/dist/observable/__tests__/simple-timeout.test.d.ts +1 -0
  94. package/dist/observable/__tests__/simple-timeout.test.js +34 -0
  95. package/dist/observable/claim-events.d.ts +5 -0
  96. package/dist/observable/claim-events.js +28 -0
  97. package/dist/observable/claim-latest.d.ts +5 -0
  98. package/dist/observable/claim-latest.js +21 -0
  99. package/dist/observable/{get-value.d.ts → get-observable-value.d.ts} +1 -1
  100. package/dist/observable/{get-value.js → get-observable-value.js} +3 -8
  101. package/dist/observable/index.d.ts +2 -1
  102. package/dist/observable/index.js +2 -1
  103. package/dist/observable/share-latest-value.d.ts +2 -4
  104. package/dist/observable/share-latest-value.js +19 -16
  105. package/dist/observable/simple-timeout.d.ts +4 -0
  106. package/dist/observable/simple-timeout.js +6 -0
  107. package/dist/observable/with-immediate-value.d.ts +3 -0
  108. package/dist/observable/with-immediate-value.js +19 -0
  109. package/dist/queries/blossom.d.ts +2 -0
  110. package/dist/queries/blossom.js +10 -0
  111. package/dist/queries/bookmarks.d.ts +8 -0
  112. package/dist/queries/bookmarks.js +23 -0
  113. package/dist/queries/channels.d.ts +11 -0
  114. package/dist/queries/channels.js +73 -0
  115. package/dist/queries/contacts.d.ts +3 -0
  116. package/dist/queries/contacts.js +12 -0
  117. package/dist/queries/index.d.ts +6 -0
  118. package/dist/queries/index.js +6 -0
  119. package/dist/queries/mutes.d.ts +8 -0
  120. package/dist/queries/mutes.js +23 -0
  121. package/dist/queries/pins.d.ts +3 -0
  122. package/dist/queries/pins.js +12 -0
  123. package/dist/queries/simple.d.ts +3 -3
  124. package/dist/queries/simple.js +3 -3
  125. package/dist/queries/thread.js +1 -1
  126. package/dist/queries/user-status.d.ts +11 -0
  127. package/dist/queries/user-status.js +39 -0
  128. package/dist/query-store/__tests__/query-store.test.d.ts +1 -0
  129. package/dist/query-store/__tests__/query-store.test.js +63 -0
  130. package/dist/query-store/index.d.ts +1 -57
  131. package/dist/query-store/index.js +1 -66
  132. package/dist/query-store/query-store.d.ts +53 -0
  133. package/dist/query-store/query-store.js +97 -0
  134. package/package.json +20 -8
  135. package/dist/helpers/media-attachment.d.ts +0 -33
  136. package/dist/helpers/media-attachment.js +0 -60
  137. /package/dist/{helpers/hidden-tags.test.d.ts → event-store/__tests__/event-store.test.d.ts} +0 -0
  138. /package/dist/{helpers/mailboxes.test.d.ts → event-store/interface.js} +0 -0
  139. /package/dist/helpers/{threading.test.d.ts → __tests__/blossom.test.d.ts} +0 -0
@@ -1,57 +1,61 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { insertEventIntoDescendingList } from "nostr-tools/utils";
3
3
  import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
4
- import { Observable } from "rxjs";
4
+ import { defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, from, map, merge, mergeMap, mergeWith, of, repeat, scan, take, takeUntil, tap, } from "rxjs";
5
5
  import { Database } from "./database.js";
6
- import { getEventUID, getReplaceableUID, getTagValue, isReplaceable } from "../helpers/event.js";
6
+ import { getEventUID, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
7
7
  import { matchFilters } from "../helpers/filter.js";
8
- import { addSeenRelay } from "../helpers/relays.js";
8
+ import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
9
9
  import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
10
+ import { claimEvents } from "../observable/claim-events.js";
11
+ import { claimLatest } from "../observable/claim-latest.js";
12
+ export const EventStoreSymbol = Symbol.for("event-store");
13
+ function sortDesc(a, b) {
14
+ return b.created_at - a.created_at;
15
+ }
10
16
  export class EventStore {
11
17
  database;
12
18
  /** Enable this to keep old versions of replaceable events */
13
19
  keepOldVersions = false;
20
+ /** A method used to verify new events before added them */
21
+ verifyEvent;
22
+ /** A stream of events that have been updated */
23
+ updates;
14
24
  constructor() {
15
25
  this.database = new Database();
26
+ this.database.onBeforeInsert = (event) => {
27
+ // reject events that are invalid
28
+ if (this.verifyEvent && this.verifyEvent(event) === false)
29
+ throw new Error("Invalid event");
30
+ };
31
+ // when events are added to the database, add the symbol
32
+ this.database.inserted.subscribe((event) => {
33
+ Reflect.set(event, EventStoreSymbol, this);
34
+ });
35
+ // when events are removed from the database, remove the symbol
36
+ this.database.removed.subscribe((event) => {
37
+ Reflect.deleteProperty(event, EventStoreSymbol);
38
+ });
39
+ this.updates = this.database.updated;
16
40
  }
17
- /** Adds an event to the database and update subscriptions */
18
- add(event, fromRelay) {
19
- if (event.kind === kinds.EventDeletion)
20
- this.handleDeleteEvent(event);
21
- // ignore if the event was deleted
22
- if (this.checkDeleted(event))
23
- return event;
24
- // insert event into database
25
- const inserted = this.database.addEvent(event);
26
- // remove all old version of the replaceable event
27
- if (!this.keepOldVersions && isReplaceable(event.kind)) {
28
- const current = this.database.getReplaceable(event.kind, event.pubkey, getTagValue(event, "d"));
29
- if (current) {
30
- const older = Array.from(current).filter((e) => e.created_at < event.created_at);
31
- for (const old of older)
32
- this.database.deleteEvent(old);
33
- // skip inserting this event because its not the newest
34
- if (current.length !== older.length)
35
- return current[0];
36
- }
37
- }
38
- // attach relay this event was from
39
- if (fromRelay)
40
- addSeenRelay(inserted, fromRelay);
41
- return inserted;
42
- }
43
- /** Removes an event from the database and updates subscriptions */
44
- remove(event) {
41
+ // delete state
42
+ deletedIds = new Set();
43
+ deletedCoords = new Map();
44
+ checkDeleted(event) {
45
45
  if (typeof event === "string")
46
- return this.database.deleteEvent(event);
47
- else if (this.database.hasEvent(event.id)) {
48
- return this.database.deleteEvent(event.id);
49
- }
50
- else
46
+ return this.deletedIds.has(event);
47
+ else {
48
+ if (this.deletedIds.has(event.id))
49
+ return true;
50
+ if (isParameterizedReplaceableKind(event.kind)) {
51
+ const deleted = this.deletedCoords.get(getEventUID(event));
52
+ if (deleted)
53
+ return deleted > event.created_at;
54
+ }
51
55
  return false;
56
+ }
52
57
  }
53
- deletedIds = new Set();
54
- deletedCoords = new Map();
58
+ // handling delete events
55
59
  handleDeleteEvent(deleteEvent) {
56
60
  const ids = getDeleteIds(deleteEvent);
57
61
  for (const id of ids) {
@@ -59,7 +63,7 @@ export class EventStore {
59
63
  // remove deleted events in the database
60
64
  const event = this.database.getEvent(id);
61
65
  if (event)
62
- this.database.deleteEvent(event);
66
+ this.database.removeEvent(event);
63
67
  }
64
68
  const coords = getDeleteCoordinates(deleteEvent);
65
69
  for (const coord of coords) {
@@ -67,18 +71,53 @@ export class EventStore {
67
71
  // remove deleted events in the database
68
72
  const event = this.database.getEvent(coord);
69
73
  if (event && event.created_at < deleteEvent.created_at)
70
- this.database.deleteEvent(event);
74
+ this.database.removeEvent(event);
71
75
  }
72
76
  }
73
- checkDeleted(event) {
74
- if (this.deletedIds.has(event.id))
75
- return true;
76
- if (isParameterizedReplaceableKind(event.kind)) {
77
- const deleted = this.deletedCoords.get(getEventUID(event));
78
- if (deleted)
79
- return deleted > event.created_at;
77
+ /** Copies important metadata from and identical event to another */
78
+ static mergeDuplicateEvent(source, dest) {
79
+ const relays = getSeenRelays(source);
80
+ if (relays) {
81
+ for (const relay of relays)
82
+ addSeenRelay(dest, relay);
83
+ }
84
+ }
85
+ /**
86
+ * Adds an event to the database and update subscriptions
87
+ * @throws
88
+ */
89
+ add(event, fromRelay) {
90
+ if (event.kind === kinds.EventDeletion)
91
+ this.handleDeleteEvent(event);
92
+ // Ignore if the event was deleted
93
+ if (this.checkDeleted(event))
94
+ return event;
95
+ // Insert event into database
96
+ const inserted = this.database.addEvent(event);
97
+ // Copy cached data if its a duplicate event
98
+ if (event !== inserted)
99
+ EventStore.mergeDuplicateEvent(event, inserted);
100
+ // attach relay this event was from
101
+ if (fromRelay)
102
+ addSeenRelay(inserted, fromRelay);
103
+ // remove all old version of the replaceable event
104
+ if (!this.keepOldVersions && isReplaceable(event.kind)) {
105
+ const existing = this.database.getReplaceable(event.kind, event.pubkey, getTagValue(event, "d"));
106
+ if (existing) {
107
+ const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
108
+ for (const old of older)
109
+ this.database.removeEvent(old);
110
+ // return the newest version of the replaceable event
111
+ // most of the time this will be === event, but not always
112
+ if (existing.length !== older.length)
113
+ return existing[0];
114
+ }
80
115
  }
81
- return false;
116
+ return inserted;
117
+ }
118
+ /** Removes an event from the database and updates subscriptions */
119
+ remove(event) {
120
+ return this.database.removeEvent(event);
82
121
  }
83
122
  /** Removes any event that is not being used by a subscription */
84
123
  prune(max) {
@@ -88,15 +127,18 @@ export class EventStore {
88
127
  update(event) {
89
128
  return this.database.updateEvent(event);
90
129
  }
130
+ /** Get all events matching a filter */
91
131
  getAll(filters) {
92
- return this.database.getForFilters(filters);
132
+ return this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
93
133
  }
94
- hasEvent(uid) {
95
- return this.database.hasEvent(uid);
134
+ /** Check if the store has an event */
135
+ hasEvent(id) {
136
+ return this.database.hasEvent(id);
96
137
  }
97
- getEvent(uid) {
98
- return this.database.getEvent(uid);
138
+ getEvent(id) {
139
+ return this.database.getEvent(id);
99
140
  }
141
+ /** Check if the store has a replaceable event */
100
142
  hasReplaceable(kind, pubkey, d) {
101
143
  return this.database.hasReplaceable(kind, pubkey, d);
102
144
  }
@@ -108,282 +150,195 @@ export class EventStore {
108
150
  getReplaceableHistory(kind, pubkey, d) {
109
151
  return this.database.getReplaceable(kind, pubkey, d);
110
152
  }
111
- /** Creates an observable that updates a single event */
153
+ /** Returns a timeline of events that match filters */
154
+ getTimeline(filters) {
155
+ return Array.from(this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters])).sort(sortDesc);
156
+ }
157
+ /**
158
+ * Creates an observable that streams all events that match the filter and remains open
159
+ * @param filters
160
+ * @param [onlyNew=false] Only subscribe to new events
161
+ */
162
+ filters(filters, onlyNew = false) {
163
+ filters = Array.isArray(filters) ? filters : [filters];
164
+ return merge(
165
+ // merge existing events
166
+ onlyNew ? EMPTY : from(this.getAll(filters)),
167
+ // subscribe to future events
168
+ this.database.inserted.pipe(filter((e) => matchFilters(filters, e))));
169
+ }
170
+ /** Returns an observable that completes when an event is removed */
171
+ removed(id) {
172
+ const deleted = this.checkDeleted(id);
173
+ if (deleted)
174
+ return EMPTY;
175
+ return this.database.removed.pipe(
176
+ // listen for removed events
177
+ filter((e) => e.id === id),
178
+ // complete as soon as we find a matching removed event
179
+ take(1),
180
+ // switch to empty
181
+ mergeMap(() => EMPTY));
182
+ }
183
+ /** Creates an observable that emits when event is updated */
184
+ updated(event) {
185
+ return this.database.updated.pipe(filter((e) => e.id === event || e === event));
186
+ }
187
+ /** Creates an observable that subscribes to a single event */
112
188
  event(id) {
113
- return new Observable((observer) => {
114
- let current = this.database.getEvent(id);
115
- if (current) {
116
- observer.next(current);
117
- this.database.claimEvent(current, observer);
118
- }
119
- // subscribe to future events
120
- const inserted = this.database.inserted.subscribe((event) => {
121
- if (event.id === id) {
122
- current = event;
123
- observer.next(event);
124
- this.database.claimEvent(event, observer);
125
- }
126
- });
127
- // subscribe to updated events
128
- const updated = this.database.updated.subscribe((event) => {
129
- if (event.id === id)
130
- observer.next(current);
131
- });
132
- // subscribe to deleted events
133
- const deleted = this.database.deleted.subscribe((event) => {
134
- if (current?.id === event.id) {
135
- this.database.removeClaim(current, observer);
136
- current = undefined;
137
- observer.next(undefined);
138
- }
139
- });
140
- return () => {
141
- deleted.unsubscribe();
142
- updated.unsubscribe();
143
- inserted.unsubscribe();
144
- if (current)
145
- this.database.removeClaim(current, observer);
146
- };
147
- });
189
+ return merge(
190
+ // get current event and ignore if there is none
191
+ defer(() => {
192
+ let event = this.getEvent(id);
193
+ return event ? of(event) : EMPTY;
194
+ }),
195
+ // subscribe to updates
196
+ this.database.inserted.pipe(filter((e) => e.id === id)),
197
+ // subscribe to updates
198
+ this.updated(id),
199
+ // emit undefined when deleted
200
+ this.removed(id).pipe(endWith(undefined))).pipe(
201
+ // claim all events
202
+ claimLatest(this.database));
148
203
  }
149
204
  /** Creates an observable that subscribes to multiple events */
150
205
  events(ids) {
151
- return new Observable((observer) => {
152
- const events = new Map();
153
- for (const id of ids) {
154
- const event = this.getEvent(id);
155
- if (event) {
156
- events.set(id, event);
157
- this.database.claimEvent(event, observer);
158
- }
206
+ return merge(
207
+ // lazily get existing events
208
+ defer(() => from(ids.map((id) => this.getEvent(id)))),
209
+ // subscribe to new events
210
+ this.database.inserted.pipe(filter((e) => ids.includes(e.id))),
211
+ // subscribe to updates
212
+ this.database.updated.pipe(filter((e) => ids.includes(e.id)))).pipe(
213
+ // ignore empty messages
214
+ filter((e) => !!e),
215
+ // claim all events until cleanup
216
+ claimEvents(this.database),
217
+ // watch for removed events
218
+ mergeWith(this.database.removed.pipe(filter((e) => ids.includes(e.id)), map((e) => e.id))),
219
+ // merge all events into a directory
220
+ scan((dir, event) => {
221
+ if (typeof event === "string") {
222
+ // delete event by id
223
+ const clone = { ...dir };
224
+ delete clone[event];
225
+ return clone;
159
226
  }
160
- observer.next(events);
161
- // subscribe to future events
162
- const inserted = this.database.inserted.subscribe((event) => {
163
- const id = event.id;
164
- if (ids.includes(id) && !events.has(id)) {
165
- events.set(id, event);
166
- observer.next(events);
167
- // claim new event
168
- this.database.claimEvent(event, observer);
169
- }
170
- });
171
- // subscribe to updated events
172
- const updated = this.database.updated.subscribe((event) => {
173
- if (ids.includes(event.id))
174
- observer.next(events);
175
- });
176
- // subscribe to deleted events
177
- const deleted = this.database.deleted.subscribe((event) => {
178
- const id = event.id;
179
- if (ids.includes(id)) {
180
- const current = events.get(id);
181
- if (current) {
182
- this.database.removeClaim(current, observer);
183
- events.delete(id);
184
- observer.next(events);
185
- }
186
- }
187
- });
188
- return () => {
189
- inserted.unsubscribe();
190
- deleted.unsubscribe();
191
- updated.unsubscribe();
192
- for (const [_uid, event] of events) {
193
- this.database.removeClaim(event, observer);
194
- }
195
- };
196
- });
227
+ else {
228
+ // add even to directory
229
+ return { ...dir, [event.id]: event };
230
+ }
231
+ }, {}));
197
232
  }
198
- /** Creates an observable with the latest version of a replaceable event */
233
+ /** Creates an observable that subscribes to the latest version of a replaceable event */
199
234
  replaceable(kind, pubkey, d) {
200
- return new Observable((observer) => {
201
- const uid = getReplaceableUID(kind, pubkey, d);
202
- // get latest version
203
- let current = this.database.getReplaceable(kind, pubkey, d)?.[0];
204
- if (current) {
205
- observer.next(current);
206
- this.database.claimEvent(current, observer);
207
- }
208
- // subscribe to future events
209
- const inserted = this.database.inserted.subscribe((event) => {
210
- if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
211
- // remove old claim
212
- if (current)
213
- this.database.removeClaim(current, observer);
214
- current = event;
215
- observer.next(event);
216
- // claim new event
217
- this.database.claimEvent(current, observer);
218
- }
219
- });
220
- // subscribe to updated events
221
- const updated = this.database.updated.subscribe((event) => {
222
- if (event === current)
223
- observer.next(event);
224
- });
225
- // subscribe to deleted events
226
- const deleted = this.database.deleted.subscribe((event) => {
227
- if (getEventUID(event) === uid && event === current) {
228
- this.database.removeClaim(current, observer);
229
- current = undefined;
230
- observer.next(undefined);
231
- }
232
- });
233
- return () => {
234
- inserted.unsubscribe();
235
- deleted.unsubscribe();
236
- updated.unsubscribe();
237
- if (current)
238
- this.database.removeClaim(current, observer);
239
- };
240
- });
235
+ let current = undefined;
236
+ return merge(
237
+ // lazily get current event
238
+ defer(() => {
239
+ let event = this.getReplaceable(kind, pubkey, d);
240
+ return event ? of(event) : EMPTY;
241
+ }),
242
+ // subscribe to new events
243
+ this.database.inserted.pipe(filter((e) => e.pubkey == pubkey && e.kind === kind && (d !== undefined ? getReplaceableIdentifier(e) === d : true)))).pipe(
244
+ // only update if event is newer
245
+ distinctUntilChanged((prev, event) => prev.created_at >= event.created_at),
246
+ // Hacky way to extract the current event so takeUntil can access it
247
+ tap((event) => (current = event)),
248
+ // complete when event is removed
249
+ takeUntil(this.database.removed.pipe(filter((e) => e.id === current?.id))),
250
+ // emit undefined when removed
251
+ endWith(undefined),
252
+ // keep the observable hot
253
+ repeat(),
254
+ // claim latest event
255
+ claimLatest(this.database));
241
256
  }
242
- /** Creates an observable with the latest versions of replaceable events */
257
+ /** Creates an observable that subscribes to the latest version of an array of replaceable events*/
243
258
  replaceableSet(pointers) {
244
- return new Observable((observer) => {
245
- const coords = pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier));
246
- const events = new Map();
247
- const handleEvent = (event) => {
248
- const uid = getEventUID(event);
249
- const current = events.get(uid);
250
- if (current) {
251
- if (event.created_at > current.created_at) {
252
- this.database.removeClaim(current, observer);
253
- }
254
- else
255
- return;
256
- }
257
- events.set(uid, event);
258
- this.database.claimEvent(event, observer);
259
- };
260
- // get latest version
261
- for (const pointer of pointers) {
262
- const events = this.database.getReplaceable(pointer.kind, pointer.pubkey, pointer.identifier);
263
- if (events)
264
- handleEvent(events[0]);
259
+ const uids = new Set(pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier)));
260
+ return merge(
261
+ // start with existing events
262
+ defer(() => from(pointers.map((p) => this.getReplaceable(p.kind, p.pubkey, p.identifier)))),
263
+ // subscribe to new events
264
+ this.database.inserted.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))))).pipe(
265
+ // filter out undefined
266
+ filter((e) => !!e),
267
+ // claim all events
268
+ claimEvents(this.database),
269
+ // convert events to add commands
270
+ map((e) => ["add", e]),
271
+ // watch for removed events
272
+ mergeWith(this.database.removed.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))), map((e) => ["remove", e]))),
273
+ // reduce events into directory
274
+ scan((dir, [action, event]) => {
275
+ const uid = getEventUID(event);
276
+ if (action === "add") {
277
+ // add event to dir if its newer
278
+ if (!dir[uid] || dir[uid].created_at < event.created_at)
279
+ return { ...dir, [uid]: event };
265
280
  }
266
- observer.next(events);
267
- // subscribe to future events
268
- const inserted = this.database.inserted.subscribe((event) => {
269
- if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) {
270
- handleEvent(event);
271
- observer.next(events);
272
- }
273
- });
274
- // subscribe to updated events
275
- const updated = this.database.updated.subscribe((event) => {
276
- if (isReplaceable(event.kind) && coords.includes(getEventUID(event))) {
277
- observer.next(events);
278
- }
279
- });
280
- // subscribe to deleted events
281
- const deleted = this.database.deleted.subscribe((event) => {
282
- const uid = getEventUID(event);
283
- if (events.has(uid)) {
284
- events.delete(uid);
285
- this.database.removeClaim(event, observer);
286
- observer.next(events);
287
- }
288
- });
289
- return () => {
290
- inserted.unsubscribe();
291
- deleted.unsubscribe();
292
- updated.unsubscribe();
293
- for (const [_id, event] of events) {
294
- this.database.removeClaim(event, observer);
295
- }
296
- };
297
- });
298
- }
299
- /**
300
- * Creates an observable that streams all events that match the filter
301
- * @param filters
302
- * @param [onlyNew=false] Only subscribe to new events
303
- */
304
- stream(filters, onlyNew = false) {
305
- filters = Array.isArray(filters) ? filters : [filters];
306
- return new Observable((observer) => {
307
- if (!onlyNew) {
308
- let events = this.database.getForFilters(filters);
309
- for (const event of events)
310
- observer.next(event);
281
+ else if (action === "remove" && dir[uid] === event) {
282
+ // remove event from dir
283
+ let newDir = { ...dir };
284
+ delete newDir[uid];
285
+ return newDir;
311
286
  }
312
- // subscribe to future events
313
- const sub = this.database.inserted.subscribe((event) => {
314
- if (matchFilters(filters, event))
315
- observer.next(event);
316
- });
317
- return () => sub.unsubscribe();
318
- });
287
+ return dir;
288
+ }, {}),
289
+ // ignore changes that do not modify the directory
290
+ distinctUntilChanged());
319
291
  }
320
292
  /** Creates an observable that updates with an array of sorted events */
321
293
  timeline(filters, keepOldVersions = false) {
322
294
  filters = Array.isArray(filters) ? filters : [filters];
323
- return new Observable((observer) => {
324
- const seen = new Map();
325
- const timeline = [];
326
- // NOTE: only call this if we know the event is in timeline
327
- const removeFromTimeline = (event) => {
328
- timeline.splice(timeline.indexOf(event), 1);
329
- if (!keepOldVersions && isReplaceable(event.kind))
330
- seen.delete(getEventUID(event));
331
- this.database.removeClaim(event, observer);
332
- };
333
- // inserts an event into the timeline and handles replaceable events
334
- const insertIntoTimeline = (event) => {
335
- // remove old versions
336
- if (!keepOldVersions && isReplaceable(event.kind)) {
337
- const uid = getEventUID(event);
338
- const old = seen.get(uid);
339
- if (old) {
340
- if (event.created_at > old.created_at)
341
- removeFromTimeline(old);
342
- else
343
- return;
344
- }
345
- seen.set(uid, event);
346
- }
347
- // insert into timeline
348
- insertEventIntoDescendingList(timeline, event);
349
- this.database.claimEvent(event, observer);
350
- };
351
- // build initial timeline
352
- const events = this.database.getForFilters(filters);
353
- for (const event of events)
354
- insertIntoTimeline(event);
355
- observer.next([...timeline]);
356
- // subscribe to future events
357
- const inserted = this.database.inserted.subscribe((event) => {
358
- if (matchFilters(filters, event)) {
359
- insertIntoTimeline(event);
360
- observer.next([...timeline]);
361
- }
362
- });
363
- // subscribe to updated events
364
- const updated = this.database.updated.subscribe((event) => {
365
- if (timeline.includes(event)) {
366
- observer.next([...timeline]);
295
+ const seen = new Map();
296
+ // get current events
297
+ return defer(() => of(Array.from(this.database.getEventsForFilters(filters)).sort(sortDesc))).pipe(
298
+ // claim existing events
299
+ claimEvents(this.database),
300
+ // subscribe to newer events
301
+ mergeWith(this.database.inserted.pipe(filter((e) => matchFilters(filters, e)),
302
+ // claim all new events
303
+ claimEvents(this.database))),
304
+ // subscribe to delete events
305
+ mergeWith(this.database.removed.pipe(filter((e) => matchFilters(filters, e)), map((e) => e.id))),
306
+ // build a timeline
307
+ scan((timeline, event) => {
308
+ // filter out removed events from timeline
309
+ if (typeof event === "string")
310
+ return timeline.filter((e) => e.id !== event);
311
+ // initial timeline array
312
+ if (Array.isArray(event)) {
313
+ if (!keepOldVersions) {
314
+ for (const e of event)
315
+ if (isReplaceable(e.kind))
316
+ seen.set(getEventUID(e), e);
367
317
  }
368
- });
369
- // subscribe to deleted events
370
- const deleted = this.database.deleted.subscribe((event) => {
371
- if (timeline.includes(event)) {
372
- removeFromTimeline(event);
373
- observer.next([...timeline]);
374
- }
375
- });
376
- return () => {
377
- inserted.unsubscribe();
378
- deleted.unsubscribe();
379
- updated.unsubscribe();
380
- // remove all claims
381
- for (const event of timeline) {
382
- this.database.removeClaim(event, observer);
383
- }
384
- // forget seen replaceable events
385
- seen.clear();
386
- };
387
- });
318
+ return event;
319
+ }
320
+ // create a new timeline and insert the event into it
321
+ let newTimeline = [...timeline];
322
+ // remove old replaceable events if enabled
323
+ if (!keepOldVersions && isReplaceable(event.kind)) {
324
+ const uid = getEventUID(event);
325
+ const existing = seen.get(uid);
326
+ // if this is an older replaceable event, exit
327
+ if (existing && event.created_at < existing.created_at)
328
+ return timeline;
329
+ // update latest version
330
+ seen.set(uid, event);
331
+ // remove old event from timeline
332
+ if (existing)
333
+ newTimeline.slice(newTimeline.indexOf(existing), 1);
334
+ }
335
+ // add event into timeline
336
+ insertEventIntoDescendingList(newTimeline, event);
337
+ return newTimeline;
338
+ }, []),
339
+ // ignore changes that do not modify the timeline instance
340
+ distinctUntilChanged(),
341
+ // hacky hack to clear seen on unsubscribe
342
+ finalize(() => seen.clear()));
388
343
  }
389
344
  }
@@ -1,2 +1,3 @@
1
1
  export * from "./event-store.js";
2
2
  export * from "./database.js";
3
+ export * from "./interface.js";