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.
- package/README.md +1 -1
- 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 +22 -16
- package/dist/event-store/database.js +62 -39
- package/dist/event-store/event-store.d.ts +52 -15
- package/dist/event-store/event-store.js +283 -191
- package/dist/helpers/__tests__/blossom.test.d.ts +1 -0
- package/dist/helpers/__tests__/blossom.test.js +13 -0
- package/dist/helpers/__tests__/comment.test.d.ts +1 -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/__tests__/hidden-tags.test.js +29 -0
- package/dist/helpers/__tests__/mailboxes.test.d.ts +1 -0
- package/dist/helpers/{mailboxes.test.js → __tests__/mailboxes.test.js} +14 -13
- 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/__tests__/threading.test.js +41 -0
- package/dist/helpers/blossom.d.ts +9 -0
- package/dist/helpers/blossom.js +22 -0
- package/dist/helpers/bolt11.d.ts +1 -0
- package/dist/helpers/bolt11.js +1 -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 +47 -0
- package/dist/helpers/comment.js +120 -0
- package/dist/helpers/contacts.d.ts +3 -0
- package/dist/helpers/contacts.js +25 -0
- package/dist/helpers/content.d.ts +3 -0
- package/dist/helpers/content.js +8 -0
- package/dist/helpers/delete.d.ts +3 -0
- package/dist/helpers/delete.js +7 -0
- package/dist/helpers/dns-identity.d.ts +7 -0
- package/dist/helpers/dns-identity.js +10 -0
- package/dist/helpers/emoji.d.ts +12 -1
- package/dist/helpers/emoji.js +13 -1
- package/dist/helpers/event.d.ts +17 -3
- package/dist/helpers/event.js +54 -12
- package/dist/helpers/external-id.d.ts +29 -0
- package/dist/helpers/external-id.js +20 -0
- package/dist/helpers/file-metadata.d.ts +55 -0
- package/dist/helpers/file-metadata.js +99 -0
- package/dist/helpers/filter.d.ts +4 -2
- package/dist/helpers/filter.js +36 -7
- package/dist/helpers/groups.d.ts +24 -0
- package/dist/helpers/groups.js +39 -0
- package/dist/helpers/hidden-tags.d.ts +48 -0
- package/dist/helpers/hidden-tags.js +86 -0
- package/dist/helpers/index.d.ts +28 -8
- package/dist/helpers/index.js +28 -8
- package/dist/helpers/json.d.ts +1 -0
- package/dist/helpers/json.js +1 -0
- package/dist/helpers/lists.d.ts +28 -0
- package/dist/helpers/lists.js +65 -0
- package/dist/helpers/lnurl.d.ts +4 -0
- package/dist/helpers/lnurl.js +40 -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.d.ts +38 -5
- package/dist/helpers/pointers.js +105 -25
- package/dist/helpers/profile.d.ts +6 -1
- package/dist/helpers/profile.js +5 -1
- 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/string.d.ts +6 -0
- package/dist/helpers/string.js +2 -0
- package/dist/helpers/tags.d.ts +23 -0
- package/dist/helpers/tags.js +34 -6
- package/dist/helpers/threading.d.ts +6 -6
- package/dist/helpers/threading.js +30 -9
- package/dist/helpers/url.d.ts +11 -1
- package/dist/helpers/url.js +31 -3
- package/dist/helpers/user-status.d.ts +18 -0
- package/dist/helpers/user-status.js +21 -0
- package/dist/helpers/zap.d.ts +25 -0
- package/dist/helpers/zap.js +32 -3
- 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-observable-value.d.ts +3 -0
- package/dist/observable/get-observable-value.js +9 -0
- 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/promise/deferred.d.ts +1 -0
- package/dist/promise/deferred.js +1 -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/comments.d.ts +4 -0
- package/dist/queries/comments.js +14 -0
- package/dist/queries/contacts.d.ts +3 -0
- package/dist/queries/contacts.js +12 -0
- package/dist/queries/index.d.ts +9 -2
- package/dist/queries/index.js +9 -2
- package/dist/queries/mailboxes.d.ts +1 -0
- package/dist/queries/mailboxes.js +1 -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/profile.d.ts +1 -0
- package/dist/queries/profile.js +1 -0
- package/dist/queries/reactions.d.ts +1 -1
- package/dist/queries/reactions.js +1 -1
- package/dist/queries/simple.d.ts +4 -4
- package/dist/queries/simple.js +13 -13
- package/dist/queries/thread.d.ts +2 -0
- package/dist/queries/thread.js +30 -4
- package/dist/queries/user-status.d.ts +11 -0
- package/dist/queries/user-status.js +39 -0
- package/dist/queries/zaps.d.ts +1 -0
- package/dist/queries/zaps.js +1 -0
- package/dist/query-store/index.d.ts +1 -47
- package/dist/query-store/index.js +1 -60
- 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 +24 -21
- package/dist/observable/getValue.d.ts +0 -2
- package/dist/observable/getValue.js +0 -13
- /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 {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
217
|
+
/** Creates an observable that subscribes to the latest version of a replaceable event */
|
|
137
218
|
replaceable(kind, pubkey, d) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
seen.set(
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
timeline
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 {};
|