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
@@ -1,236 +1,328 @@
1
+ import { kinds } from "nostr-tools";
1
2
  import { insertEventIntoDescendingList } from "nostr-tools/utils";
2
- import { Observable } from "rxjs";
3
+ import { isParameterizedReplaceableKind } 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";
3
5
  import { Database } from "./database.js";
4
- import { getEventUID, getReplaceableUID } from "../helpers/event.js";
6
+ import { getEventUID, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
5
7
  import { matchFilters } from "../helpers/filter.js";
6
- import { addSeenRelay } from "../helpers/relays.js";
8
+ import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
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
+ function sortDesc(a, b) {
13
+ return b.created_at - a.created_at;
14
+ }
7
15
  export class EventStore {
8
16
  database;
17
+ /** Enable this to keep old versions of replaceable events */
18
+ keepOldVersions = false;
19
+ /** A method used to verify new events before added them */
20
+ verifyEvent;
9
21
  constructor() {
10
22
  this.database = new Database();
23
+ this.database.onBeforeInsert = (event) => {
24
+ // reject events that are invalid
25
+ if (this.verifyEvent && this.verifyEvent(event) === false)
26
+ throw new Error("Invalid event");
27
+ };
28
+ }
29
+ // delete state
30
+ deletedIds = new Set();
31
+ deletedCoords = new Map();
32
+ checkDeleted(event) {
33
+ if (typeof event === "string")
34
+ return this.deletedIds.has(event);
35
+ else {
36
+ if (this.deletedIds.has(event.id))
37
+ return true;
38
+ if (isParameterizedReplaceableKind(event.kind)) {
39
+ const deleted = this.deletedCoords.get(getEventUID(event));
40
+ if (deleted)
41
+ return deleted > event.created_at;
42
+ }
43
+ return false;
44
+ }
45
+ }
46
+ // handling delete events
47
+ handleDeleteEvent(deleteEvent) {
48
+ const ids = getDeleteIds(deleteEvent);
49
+ for (const id of ids) {
50
+ this.deletedIds.add(id);
51
+ // remove deleted events in the database
52
+ const event = this.database.getEvent(id);
53
+ if (event)
54
+ this.database.removeEvent(event);
55
+ }
56
+ const coords = getDeleteCoordinates(deleteEvent);
57
+ for (const coord of coords) {
58
+ this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at));
59
+ // remove deleted events in the database
60
+ const event = this.database.getEvent(coord);
61
+ if (event && event.created_at < deleteEvent.created_at)
62
+ this.database.removeEvent(event);
63
+ }
11
64
  }
12
- /** Adds an event to the database */
65
+ /** Copies important metadata from and identical event to another */
66
+ static mergeDuplicateEvent(source, dest) {
67
+ const relays = getSeenRelays(source);
68
+ if (relays) {
69
+ for (const relay of relays)
70
+ addSeenRelay(dest, relay);
71
+ }
72
+ }
73
+ /**
74
+ * Adds an event to the database and update subscriptions
75
+ * @throws
76
+ */
13
77
  add(event, fromRelay) {
78
+ if (event.kind === kinds.EventDeletion)
79
+ this.handleDeleteEvent(event);
80
+ // Ignore if the event was deleted
81
+ if (this.checkDeleted(event))
82
+ return event;
83
+ // Insert event into database
14
84
  const inserted = this.database.addEvent(event);
85
+ // Copy cached data if its a duplicate event
86
+ if (event !== inserted)
87
+ EventStore.mergeDuplicateEvent(event, inserted);
88
+ // attach relay this event was from
15
89
  if (fromRelay)
16
90
  addSeenRelay(inserted, fromRelay);
91
+ // remove all old version of the replaceable event
92
+ if (!this.keepOldVersions && isReplaceable(event.kind)) {
93
+ const existing = this.database.getReplaceable(event.kind, event.pubkey, getTagValue(event, "d"));
94
+ if (existing) {
95
+ const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
96
+ for (const old of older)
97
+ this.database.removeEvent(old);
98
+ // return the newest version of the replaceable event
99
+ // most of the time this will be === event, but not always
100
+ if (existing.length !== older.length)
101
+ return existing[0];
102
+ }
103
+ }
17
104
  return inserted;
18
105
  }
106
+ /** Removes an event from the database and updates subscriptions */
107
+ remove(event) {
108
+ return this.database.removeEvent(event);
109
+ }
110
+ /** Removes any event that is not being used by a subscription */
111
+ prune(max) {
112
+ return this.database.prune(max);
113
+ }
19
114
  /** Add an event to the store and notifies all subscribes it has updated */
20
115
  update(event) {
21
116
  return this.database.updateEvent(event);
22
117
  }
118
+ /** Get all events matching a filter */
23
119
  getAll(filters) {
24
120
  return this.database.getForFilters(filters);
25
121
  }
122
+ /** Check if the store has an event */
26
123
  hasEvent(uid) {
27
124
  return this.database.hasEvent(uid);
28
125
  }
29
126
  getEvent(uid) {
30
127
  return this.database.getEvent(uid);
31
128
  }
129
+ /** Check if the store has a replaceable event */
32
130
  hasReplaceable(kind, pubkey, d) {
33
131
  return this.database.hasReplaceable(kind, pubkey, d);
34
132
  }
133
+ /** Gets the latest version of a replaceable event */
35
134
  getReplaceable(kind, pubkey, d) {
135
+ return this.database.getReplaceable(kind, pubkey, d)?.[0];
136
+ }
137
+ /** Returns all versions of a replaceable event */
138
+ getReplaceableHistory(kind, pubkey, d) {
36
139
  return this.database.getReplaceable(kind, pubkey, d);
37
140
  }
38
- /** Creates an observable that updates a single event */
39
- event(uid) {
40
- return new Observable((observer) => {
41
- let current = this.database.getEvent(uid);
42
- if (current) {
43
- observer.next(current);
44
- this.database.claimEvent(current, observer);
45
- }
46
- // subscribe to future events
47
- const inserted = this.database.inserted.subscribe((event) => {
48
- if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
49
- // remove old claim
50
- if (current)
51
- this.database.removeClaim(current, observer);
52
- current = event;
53
- observer.next(event);
54
- // claim new event
55
- this.database.claimEvent(current, observer);
56
- }
57
- });
58
- // subscribe to updates
59
- const updated = this.database.updated.subscribe((event) => {
60
- if (event === current)
61
- observer.next(event);
62
- });
63
- // subscribe to deleted events
64
- const deleted = this.database.deleted.subscribe((event) => {
65
- if (getEventUID(event) === uid && current) {
66
- this.database.removeClaim(current, observer);
67
- current = undefined;
68
- observer.next(undefined);
69
- }
70
- });
71
- return () => {
72
- inserted.unsubscribe();
73
- deleted.unsubscribe();
74
- updated.unsubscribe();
75
- if (current)
76
- this.database.removeClaim(current, observer);
77
- };
78
- });
141
+ /**
142
+ * Creates an observable that streams all events that match the filter and remains open
143
+ * @param filters
144
+ * @param [onlyNew=false] Only subscribe to new events
145
+ */
146
+ filters(filters, onlyNew = false) {
147
+ filters = Array.isArray(filters) ? filters : [filters];
148
+ return merge(
149
+ // merge existing events
150
+ onlyNew ? EMPTY : from(this.getAll(filters)),
151
+ // subscribe to future events
152
+ this.database.inserted.pipe(filter((e) => matchFilters(filters, e))));
153
+ }
154
+ /** Returns an observable that completes when an event is removed */
155
+ removed(id) {
156
+ const deleted = this.checkDeleted(id);
157
+ if (deleted)
158
+ return EMPTY;
159
+ return this.database.removed.pipe(
160
+ // listen for removed events
161
+ filter((e) => e.id === id),
162
+ // complete as soon as we find a matching removed event
163
+ take(1),
164
+ // switch to empty
165
+ mergeMap(() => EMPTY));
166
+ }
167
+ /** Creates an observable that emits when event is updated */
168
+ updated(id) {
169
+ return this.database.updated.pipe(filter((e) => e.id === id));
170
+ }
171
+ /** Creates an observable that subscribes to a single event */
172
+ event(id) {
173
+ return merge(
174
+ // get current event and ignore if there is none
175
+ defer(() => {
176
+ let event = this.getEvent(id);
177
+ return event ? of(event) : EMPTY;
178
+ }),
179
+ // subscribe to updates
180
+ this.database.inserted.pipe(filter((e) => e.id === id)),
181
+ // subscribe to updates
182
+ this.updated(id),
183
+ // emit undefined when deleted
184
+ this.removed(id).pipe(endWith(undefined))).pipe(
185
+ // claim all events
186
+ claimEvents(this.database));
79
187
  }
80
188
  /** Creates an observable that subscribes to multiple events */
81
- events(uids) {
82
- return new Observable((observer) => {
83
- const events = new Map();
84
- for (const uid of uids) {
85
- const e = this.getEvent(uid);
86
- if (e) {
87
- events.set(uid, e);
88
- this.database.claimEvent(e, observer);
89
- }
189
+ events(ids) {
190
+ return merge(
191
+ // lazily get existing events
192
+ defer(() => from(ids.map((id) => this.getEvent(id)))),
193
+ // subscribe to new events
194
+ this.database.inserted.pipe(filter((e) => ids.includes(e.id))),
195
+ // subscribe to updates
196
+ this.database.updated.pipe(filter((e) => ids.includes(e.id)))).pipe(
197
+ // ignore empty messages
198
+ filter((e) => !!e),
199
+ // claim all events until cleanup
200
+ claimEvents(this.database),
201
+ // watch for removed events
202
+ mergeWith(this.database.removed.pipe(filter((e) => ids.includes(e.id)), map((e) => e.id))),
203
+ // merge all events into a directory
204
+ scan((dir, event) => {
205
+ if (typeof event === "string") {
206
+ // delete event by id
207
+ const clone = { ...dir };
208
+ delete clone[event];
209
+ return clone;
90
210
  }
91
- observer.next(events);
92
- // subscribe to future events
93
- const inserted = this.database.inserted.subscribe((event) => {
94
- const uid = getEventUID(event);
95
- if (uids.includes(uid)) {
96
- const current = events.get(uid);
97
- // remove old claim
98
- if (!current || event.created_at > current.created_at) {
99
- if (current)
100
- this.database.removeClaim(current, observer);
101
- events.set(uid, event);
102
- observer.next(events);
103
- // claim new event
104
- this.database.claimEvent(event, observer);
105
- }
106
- }
107
- });
108
- // subscribe to updates
109
- const updated = this.database.updated.subscribe((event) => {
110
- const uid = getEventUID(event);
111
- if (uids.includes(uid))
112
- observer.next(events);
113
- });
114
- // subscribe to deleted events
115
- const deleted = this.database.deleted.subscribe((event) => {
116
- const uid = getEventUID(event);
117
- if (uids.includes(uid)) {
118
- const current = events.get(uid);
119
- if (current) {
120
- this.database.removeClaim(current, observer);
121
- events.delete(uid);
122
- observer.next(events);
123
- }
124
- }
125
- });
126
- return () => {
127
- inserted.unsubscribe();
128
- deleted.unsubscribe();
129
- updated.unsubscribe();
130
- for (const [_uid, event] of events) {
131
- this.database.removeClaim(event, observer);
132
- }
133
- };
134
- });
211
+ else {
212
+ // add even to directory
213
+ return { ...dir, [event.id]: event };
214
+ }
215
+ }, {}));
135
216
  }
136
- /** Creates an observable that updates a single replaceable event */
217
+ /** Creates an observable that subscribes to the latest version of a replaceable event */
137
218
  replaceable(kind, pubkey, d) {
138
- return this.event(getReplaceableUID(kind, pubkey, d));
139
- }
140
- /** Creates an observable that streams all events that match the filter */
141
- stream(filters) {
142
- return new Observable((observer) => {
143
- let claimed = new Set();
144
- let events = this.database.getForFilters(filters);
145
- for (const event of events) {
146
- observer.next(event);
147
- this.database.claimEvent(event, observer);
148
- claimed.add(event);
219
+ let current = undefined;
220
+ return merge(
221
+ // lazily get current event
222
+ defer(() => {
223
+ let event = this.getReplaceable(kind, pubkey, d);
224
+ return event ? of(event) : EMPTY;
225
+ }),
226
+ // subscribe to new events
227
+ this.database.inserted.pipe(filter((e) => e.pubkey == pubkey && e.kind === kind && (d !== undefined ? getReplaceableIdentifier(e) === d : true)))).pipe(
228
+ // only update if event is newer
229
+ distinctUntilChanged((prev, event) => prev.created_at >= event.created_at),
230
+ // Hacky way to extract the current event so takeUntil can access it
231
+ tap((event) => (current = event)),
232
+ // complete when event is removed
233
+ takeUntil(this.database.removed.pipe(filter((e) => e.id === current?.id))),
234
+ // emit undefined when removed
235
+ endWith(undefined),
236
+ // keep the observable hot
237
+ repeat(),
238
+ // claim latest event
239
+ claimLatest(this.database));
240
+ }
241
+ /** Creates an observable that subscribes to the latest version of an array of replaceable events*/
242
+ replaceableSet(pointers) {
243
+ const uids = new Set(pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier)));
244
+ return merge(
245
+ // start with existing events
246
+ defer(() => from(pointers.map((p) => this.getReplaceable(p.kind, p.pubkey, p.identifier)))),
247
+ // subscribe to new events
248
+ this.database.inserted.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))))).pipe(
249
+ // filter out undefined
250
+ filter((e) => !!e),
251
+ // claim all events
252
+ claimEvents(this.database),
253
+ // convert events to add commands
254
+ map((e) => ["add", e]),
255
+ // watch for removed events
256
+ mergeWith(this.database.removed.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))), map((e) => ["remove", e]))),
257
+ // reduce events into directory
258
+ scan((dir, [action, event]) => {
259
+ const uid = getEventUID(event);
260
+ if (action === "add") {
261
+ // add event to dir if its newer
262
+ if (!dir[uid] || dir[uid].created_at < event.created_at)
263
+ return { ...dir, [uid]: event };
149
264
  }
150
- // subscribe to future events
151
- const sub = this.database.inserted.subscribe((event) => {
152
- if (matchFilters(filters, event)) {
153
- observer.next(event);
154
- this.database.claimEvent(event, observer);
155
- claimed.add(event);
156
- }
157
- });
158
- return () => {
159
- sub.unsubscribe();
160
- // remove all claims
161
- for (const event of claimed)
162
- this.database.removeClaim(event, observer);
163
- claimed.clear();
164
- };
165
- });
265
+ else if (action === "remove" && dir[uid] === event) {
266
+ // remove event from dir
267
+ let newDir = { ...dir };
268
+ delete newDir[uid];
269
+ return newDir;
270
+ }
271
+ return dir;
272
+ }, {}),
273
+ // ignore changes that do not modify the directory
274
+ distinctUntilChanged());
166
275
  }
167
276
  /** Creates an observable that updates with an array of sorted events */
168
- timeline(filters) {
169
- return new Observable((observer) => {
170
- const seen = new Map();
171
- const timeline = [];
172
- // build initial timeline
173
- const events = this.database.getForFilters(filters);
174
- for (const event of events) {
175
- insertEventIntoDescendingList(timeline, event);
176
- this.database.claimEvent(event, observer);
177
- seen.set(getEventUID(event), event);
178
- }
179
- observer.next([...timeline]);
180
- // subscribe to future events
181
- const inserted = this.database.inserted.subscribe((event) => {
182
- if (matchFilters(filters, event)) {
183
- const uid = getEventUID(event);
184
- let current = seen.get(uid);
185
- if (current) {
186
- if (event.created_at > current.created_at) {
187
- // replace event
188
- timeline.splice(timeline.indexOf(current), 1, event);
189
- observer.next([...timeline]);
190
- // update the claim
191
- seen.set(uid, event);
192
- this.database.removeClaim(current, observer);
193
- this.database.claimEvent(event, observer);
194
- }
195
- }
196
- else {
197
- insertEventIntoDescendingList(timeline, event);
198
- observer.next([...timeline]);
199
- // claim new event
200
- this.database.claimEvent(event, observer);
201
- seen.set(getEventUID(event), event);
202
- }
203
- }
204
- });
205
- // subscribe to updates
206
- const updated = this.database.updated.subscribe((event) => {
207
- if (seen.has(getEventUID(event))) {
208
- observer.next([...timeline]);
277
+ timeline(filters, keepOldVersions = false) {
278
+ filters = Array.isArray(filters) ? filters : [filters];
279
+ const seen = new Map();
280
+ // get current events
281
+ return defer(() => of(Array.from(this.database.getForFilters(filters)).sort(sortDesc))).pipe(
282
+ // claim existing events
283
+ claimEvents(this.database),
284
+ // subscribe to newer events
285
+ mergeWith(this.database.inserted.pipe(filter((e) => matchFilters(filters, e)),
286
+ // claim all new events
287
+ claimEvents(this.database))),
288
+ // subscribe to delete events
289
+ mergeWith(this.database.removed.pipe(filter((e) => matchFilters(filters, e)), map((e) => e.id))),
290
+ // build a timeline
291
+ scan((timeline, event) => {
292
+ // filter out removed events from timeline
293
+ if (typeof event === "string")
294
+ return timeline.filter((e) => e.id !== event);
295
+ // initial timeline array
296
+ if (Array.isArray(event)) {
297
+ if (!keepOldVersions) {
298
+ for (const e of event)
299
+ if (isReplaceable(e.kind))
300
+ seen.set(getEventUID(e), e);
209
301
  }
210
- });
211
- // subscribe to removed events
212
- const deleted = this.database.deleted.subscribe((event) => {
302
+ return event;
303
+ }
304
+ // create a new timeline and insert the event into it
305
+ let newTimeline = [...timeline];
306
+ // remove old replaceable events if enabled
307
+ if (!keepOldVersions && isReplaceable(event.kind)) {
213
308
  const uid = getEventUID(event);
214
- let current = seen.get(uid);
215
- if (current) {
216
- // remove the event
217
- timeline.splice(timeline.indexOf(current), 1);
218
- observer.next([...timeline]);
219
- // remove the claim
220
- seen.delete(uid);
221
- this.database.removeClaim(current, observer);
222
- }
223
- });
224
- return () => {
225
- inserted.unsubscribe();
226
- deleted.unsubscribe();
227
- updated.unsubscribe();
228
- // remove all claims
229
- for (const [_, event] of seen) {
230
- this.database.removeClaim(event, observer);
231
- }
232
- seen.clear();
233
- };
234
- });
309
+ const existing = seen.get(uid);
310
+ // if this is an older replaceable event, exit
311
+ if (existing && event.created_at < existing.created_at)
312
+ return timeline;
313
+ // update latest version
314
+ seen.set(uid, event);
315
+ // remove old event from timeline
316
+ if (existing)
317
+ newTimeline.slice(newTimeline.indexOf(existing), 1);
318
+ }
319
+ // add event into timeline
320
+ insertEventIntoDescendingList(newTimeline, event);
321
+ return newTimeline;
322
+ }, []),
323
+ // ignore changes that do not modify the timeline instance
324
+ distinctUntilChanged(),
325
+ // hacky hack to clear seen on unsubscribe
326
+ finalize(() => seen.clear()));
235
327
  }
236
328
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { areBlossomServersEqual } from "../blossom.js";
3
+ describe("areBlossomServersEqual", () => {
4
+ it("should ignore path", () => {
5
+ expect(areBlossomServersEqual("https://cdn.server.com/pathname", "https://cdn.server.com")).toBe(true);
6
+ });
7
+ it("should not ignore protocol", () => {
8
+ expect(areBlossomServersEqual("http://cdn.server.com", "https://cdn.server.com")).toBe(false);
9
+ });
10
+ it("should not ignore port", () => {
11
+ expect(areBlossomServersEqual("http://cdn.server.com:4658", "https://cdn.server.com")).toBe(false);
12
+ });
13
+ });
@@ -0,0 +1 @@
1
+ export {};