applesauce-core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/fixtures.d.ts +8 -0
- package/dist/__tests__/fixtures.js +20 -0
- package/dist/event-store/__tests__/event-store.test.js +259 -0
- package/dist/event-store/database.d.ts +6 -4
- package/dist/event-store/database.js +13 -7
- package/dist/event-store/event-store.d.ts +30 -16
- package/dist/event-store/event-store.js +248 -309
- package/dist/helpers/__tests__/blossom.test.js +13 -0
- package/dist/helpers/__tests__/comment.test.js +235 -0
- package/dist/helpers/__tests__/emoji.test.d.ts +1 -0
- package/dist/helpers/__tests__/emoji.test.js +15 -0
- package/dist/helpers/__tests__/event.test.d.ts +1 -0
- package/dist/helpers/__tests__/event.test.js +36 -0
- package/dist/helpers/__tests__/file-metadata.test.d.ts +1 -0
- package/dist/helpers/__tests__/file-metadata.test.js +103 -0
- package/dist/helpers/__tests__/hidden-tags.test.d.ts +1 -0
- package/dist/helpers/{hidden-tags.test.js → __tests__/hidden-tags.test.js} +2 -1
- package/dist/helpers/__tests__/mailboxes.test.d.ts +1 -0
- package/dist/helpers/{mailboxes.test.js → __tests__/mailboxes.test.js} +1 -1
- package/dist/helpers/__tests__/relays.test.d.ts +1 -0
- package/dist/helpers/__tests__/relays.test.js +21 -0
- package/dist/helpers/__tests__/tags.test.d.ts +1 -0
- package/dist/helpers/__tests__/tags.test.js +24 -0
- package/dist/helpers/__tests__/threading.test.d.ts +1 -0
- package/dist/helpers/{threading.test.js → __tests__/threading.test.js} +1 -1
- package/dist/helpers/blossom.d.ts +9 -0
- package/dist/helpers/blossom.js +22 -0
- package/dist/helpers/bookmarks.d.ts +15 -0
- package/dist/helpers/bookmarks.js +27 -0
- package/dist/helpers/channels.d.ts +10 -0
- package/dist/helpers/channels.js +27 -0
- package/dist/helpers/comment.d.ts +3 -4
- package/dist/helpers/comment.js +20 -16
- package/dist/helpers/contacts.d.ts +3 -0
- package/dist/helpers/contacts.js +25 -0
- package/dist/helpers/dns-identity.d.ts +7 -0
- package/dist/helpers/dns-identity.js +10 -0
- package/dist/helpers/emoji.d.ts +3 -1
- package/dist/helpers/emoji.js +2 -2
- package/dist/helpers/event.d.ts +8 -2
- package/dist/helpers/event.js +29 -11
- package/dist/helpers/file-metadata.d.ts +55 -0
- package/dist/helpers/file-metadata.js +99 -0
- package/dist/helpers/filter.d.ts +4 -0
- package/dist/helpers/filter.js +34 -1
- package/dist/helpers/groups.d.ts +24 -0
- package/dist/helpers/groups.js +39 -0
- package/dist/helpers/hidden-tags.d.ts +15 -15
- package/dist/helpers/hidden-tags.js +9 -31
- package/dist/helpers/index.d.ts +13 -1
- package/dist/helpers/index.js +13 -1
- package/dist/helpers/lists.d.ts +28 -0
- package/dist/helpers/lists.js +65 -0
- package/dist/helpers/mailboxes.js +16 -9
- package/dist/helpers/mutes.d.ts +14 -0
- package/dist/helpers/mutes.js +23 -0
- package/dist/helpers/picture-post.d.ts +4 -0
- package/dist/helpers/picture-post.js +6 -0
- package/dist/helpers/pointers.js +13 -17
- package/dist/helpers/profile.d.ts +6 -1
- package/dist/helpers/profile.js +4 -0
- package/dist/helpers/relays.d.ts +6 -3
- package/dist/helpers/relays.js +25 -18
- package/dist/helpers/share.d.ts +4 -0
- package/dist/helpers/share.js +12 -0
- package/dist/helpers/tags.d.ts +17 -0
- package/dist/helpers/tags.js +28 -6
- package/dist/helpers/threading.js +3 -3
- package/dist/helpers/url.d.ts +7 -0
- package/dist/helpers/url.js +27 -0
- package/dist/helpers/user-status.d.ts +18 -0
- package/dist/helpers/user-status.js +21 -0
- package/dist/observable/__tests__/claim-events.test.d.ts +1 -0
- package/dist/observable/__tests__/claim-events.test.js +23 -0
- package/dist/observable/__tests__/claim-latest.test.d.ts +1 -0
- package/dist/observable/__tests__/claim-latest.test.js +37 -0
- package/dist/observable/__tests__/simple-timeout.test.d.ts +1 -0
- package/dist/observable/__tests__/simple-timeout.test.js +34 -0
- package/dist/observable/claim-events.d.ts +5 -0
- package/dist/observable/claim-events.js +28 -0
- package/dist/observable/claim-latest.d.ts +4 -0
- package/dist/observable/claim-latest.js +20 -0
- package/dist/observable/{get-value.d.ts → get-observable-value.d.ts} +1 -1
- package/dist/observable/{get-value.js → get-observable-value.js} +3 -8
- package/dist/observable/index.d.ts +2 -1
- package/dist/observable/index.js +2 -1
- package/dist/observable/share-latest-value.d.ts +2 -4
- package/dist/observable/share-latest-value.js +19 -16
- package/dist/observable/simple-timeout.d.ts +4 -0
- package/dist/observable/simple-timeout.js +6 -0
- package/dist/queries/blossom.d.ts +2 -0
- package/dist/queries/blossom.js +10 -0
- package/dist/queries/bookmarks.d.ts +8 -0
- package/dist/queries/bookmarks.js +23 -0
- package/dist/queries/channels.d.ts +11 -0
- package/dist/queries/channels.js +73 -0
- package/dist/queries/contacts.d.ts +3 -0
- package/dist/queries/contacts.js +12 -0
- package/dist/queries/index.d.ts +6 -0
- package/dist/queries/index.js +6 -0
- package/dist/queries/mutes.d.ts +8 -0
- package/dist/queries/mutes.js +23 -0
- package/dist/queries/pins.d.ts +3 -0
- package/dist/queries/pins.js +12 -0
- package/dist/queries/simple.d.ts +2 -2
- package/dist/queries/thread.js +1 -1
- package/dist/queries/user-status.d.ts +11 -0
- package/dist/queries/user-status.js +39 -0
- package/dist/query-store/index.d.ts +1 -57
- package/dist/query-store/index.js +1 -66
- package/dist/query-store/query-store.d.ts +51 -0
- package/dist/query-store/query-store.js +88 -0
- package/dist/query-store/query-store.test.d.ts +1 -0
- package/dist/query-store/query-store.test.js +33 -0
- package/package.json +19 -8
- package/dist/helpers/media-attachment.d.ts +0 -33
- package/dist/helpers/media-attachment.js +0 -60
- /package/dist/{helpers/hidden-tags.test.d.ts → event-store/__tests__/event-store.test.d.ts} +0 -0
- /package/dist/helpers/{mailboxes.test.d.ts → __tests__/blossom.test.d.ts} +0 -0
- /package/dist/helpers/{threading.test.d.ts → __tests__/comment.test.d.ts} +0 -0
|
@@ -1,57 +1,49 @@
|
|
|
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 {
|
|
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
|
+
function sortDesc(a, b) {
|
|
13
|
+
return b.created_at - a.created_at;
|
|
14
|
+
}
|
|
10
15
|
export class EventStore {
|
|
11
16
|
database;
|
|
12
17
|
/** Enable this to keep old versions of replaceable events */
|
|
13
18
|
keepOldVersions = false;
|
|
19
|
+
/** A method used to verify new events before added them */
|
|
20
|
+
verifyEvent;
|
|
14
21
|
constructor() {
|
|
15
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
|
+
};
|
|
16
28
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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) {
|
|
29
|
+
// delete state
|
|
30
|
+
deletedIds = new Set();
|
|
31
|
+
deletedCoords = new Map();
|
|
32
|
+
checkDeleted(event) {
|
|
45
33
|
if (typeof event === "string")
|
|
46
|
-
return this.
|
|
47
|
-
else
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
43
|
return false;
|
|
44
|
+
}
|
|
52
45
|
}
|
|
53
|
-
|
|
54
|
-
deletedCoords = new Map();
|
|
46
|
+
// handling delete events
|
|
55
47
|
handleDeleteEvent(deleteEvent) {
|
|
56
48
|
const ids = getDeleteIds(deleteEvent);
|
|
57
49
|
for (const id of ids) {
|
|
@@ -59,7 +51,7 @@ export class EventStore {
|
|
|
59
51
|
// remove deleted events in the database
|
|
60
52
|
const event = this.database.getEvent(id);
|
|
61
53
|
if (event)
|
|
62
|
-
this.database.
|
|
54
|
+
this.database.removeEvent(event);
|
|
63
55
|
}
|
|
64
56
|
const coords = getDeleteCoordinates(deleteEvent);
|
|
65
57
|
for (const coord of coords) {
|
|
@@ -67,18 +59,53 @@ export class EventStore {
|
|
|
67
59
|
// remove deleted events in the database
|
|
68
60
|
const event = this.database.getEvent(coord);
|
|
69
61
|
if (event && event.created_at < deleteEvent.created_at)
|
|
70
|
-
this.database.
|
|
62
|
+
this.database.removeEvent(event);
|
|
71
63
|
}
|
|
72
64
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
*/
|
|
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
|
|
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
|
|
89
|
+
if (fromRelay)
|
|
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
|
+
}
|
|
80
103
|
}
|
|
81
|
-
return
|
|
104
|
+
return inserted;
|
|
105
|
+
}
|
|
106
|
+
/** Removes an event from the database and updates subscriptions */
|
|
107
|
+
remove(event) {
|
|
108
|
+
return this.database.removeEvent(event);
|
|
82
109
|
}
|
|
83
110
|
/** Removes any event that is not being used by a subscription */
|
|
84
111
|
prune(max) {
|
|
@@ -88,15 +115,18 @@ export class EventStore {
|
|
|
88
115
|
update(event) {
|
|
89
116
|
return this.database.updateEvent(event);
|
|
90
117
|
}
|
|
118
|
+
/** Get all events matching a filter */
|
|
91
119
|
getAll(filters) {
|
|
92
120
|
return this.database.getForFilters(filters);
|
|
93
121
|
}
|
|
122
|
+
/** Check if the store has an event */
|
|
94
123
|
hasEvent(uid) {
|
|
95
124
|
return this.database.hasEvent(uid);
|
|
96
125
|
}
|
|
97
126
|
getEvent(uid) {
|
|
98
127
|
return this.database.getEvent(uid);
|
|
99
128
|
}
|
|
129
|
+
/** Check if the store has a replaceable event */
|
|
100
130
|
hasReplaceable(kind, pubkey, d) {
|
|
101
131
|
return this.database.hasReplaceable(kind, pubkey, d);
|
|
102
132
|
}
|
|
@@ -108,282 +138,191 @@ export class EventStore {
|
|
|
108
138
|
getReplaceableHistory(kind, pubkey, d) {
|
|
109
139
|
return this.database.getReplaceable(kind, pubkey, d);
|
|
110
140
|
}
|
|
111
|
-
/**
|
|
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 */
|
|
112
172
|
event(id) {
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
});
|
|
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));
|
|
148
187
|
}
|
|
149
188
|
/** Creates an observable that subscribes to multiple events */
|
|
150
189
|
events(ids) {
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
159
210
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
});
|
|
211
|
+
else {
|
|
212
|
+
// add even to directory
|
|
213
|
+
return { ...dir, [event.id]: event };
|
|
214
|
+
}
|
|
215
|
+
}, {}));
|
|
197
216
|
}
|
|
198
|
-
/** Creates an observable
|
|
217
|
+
/** Creates an observable that subscribes to the latest version of a replaceable event */
|
|
199
218
|
replaceable(kind, pubkey, d) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
});
|
|
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));
|
|
241
240
|
}
|
|
242
|
-
/** Creates an observable
|
|
241
|
+
/** Creates an observable that subscribes to the latest version of an array of replaceable events*/
|
|
243
242
|
replaceableSet(pointers) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
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 };
|
|
265
264
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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);
|
|
265
|
+
else if (action === "remove" && dir[uid] === event) {
|
|
266
|
+
// remove event from dir
|
|
267
|
+
let newDir = { ...dir };
|
|
268
|
+
delete newDir[uid];
|
|
269
|
+
return newDir;
|
|
311
270
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
317
|
-
return () => sub.unsubscribe();
|
|
318
|
-
});
|
|
271
|
+
return dir;
|
|
272
|
+
}, {}),
|
|
273
|
+
// ignore changes that do not modify the directory
|
|
274
|
+
distinctUntilChanged());
|
|
319
275
|
}
|
|
320
276
|
/** Creates an observable that updates with an array of sorted events */
|
|
321
277
|
timeline(filters, keepOldVersions = false) {
|
|
322
278
|
filters = Array.isArray(filters) ? filters : [filters];
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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]);
|
|
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);
|
|
367
301
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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)) {
|
|
308
|
+
const uid = getEventUID(event);
|
|
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()));
|
|
388
327
|
}
|
|
389
328
|
}
|
|
@@ -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
|
+
});
|