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.
- package/dist/__tests__/fixtures.d.ts +8 -0
- package/dist/__tests__/fixtures.js +20 -0
- package/dist/event-store/__tests__/event-store.test.js +272 -0
- package/dist/event-store/database.d.ts +7 -5
- package/dist/event-store/database.js +14 -8
- package/dist/event-store/event-store.d.ts +40 -20
- package/dist/event-store/event-store.js +269 -314
- package/dist/event-store/index.d.ts +1 -0
- package/dist/event-store/index.js +1 -0
- package/dist/event-store/interface.d.ts +27 -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/{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__/nip-19.test.d.ts +1 -0
- package/dist/helpers/__tests__/nip-19.test.js +42 -0
- 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/cache.d.ts +3 -4
- package/dist/helpers/cache.js +1 -1
- 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/direct-messages.d.ts +4 -0
- package/dist/helpers/direct-messages.js +5 -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 +15 -1
- package/dist/helpers/event.js +34 -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/gift-wraps.d.ts +12 -0
- package/dist/helpers/gift-wraps.js +49 -0
- package/dist/helpers/groups.d.ts +24 -0
- package/dist/helpers/groups.js +39 -0
- package/dist/helpers/hidden-content.d.ts +48 -0
- package/dist/helpers/hidden-content.js +88 -0
- package/dist/helpers/hidden-tags.d.ts +17 -35
- package/dist/helpers/hidden-tags.js +26 -83
- package/dist/helpers/index.d.ts +16 -1
- package/dist/helpers/index.js +16 -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 +15 -0
- package/dist/helpers/mutes.js +24 -0
- package/dist/helpers/nip-19.d.ts +4 -0
- package/dist/helpers/nip-19.js +27 -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 +5 -0
- package/dist/observable/claim-latest.js +21 -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/observable/with-immediate-value.d.ts +3 -0
- package/dist/observable/with-immediate-value.js +19 -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 +3 -3
- package/dist/queries/simple.js +3 -3
- 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/__tests__/query-store.test.d.ts +1 -0
- package/dist/query-store/__tests__/query-store.test.js +63 -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 +53 -0
- package/dist/query-store/query-store.js +97 -0
- package/package.json +20 -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 → event-store/interface.js} +0 -0
- /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 {
|
|
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
|
-
|
|
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) {
|
|
41
|
+
// delete state
|
|
42
|
+
deletedIds = new Set();
|
|
43
|
+
deletedCoords = new Map();
|
|
44
|
+
checkDeleted(event) {
|
|
45
45
|
if (typeof event === "string")
|
|
46
|
-
return this.
|
|
47
|
-
else
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
74
|
+
this.database.removeEvent(event);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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.
|
|
132
|
+
return this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
|
|
93
133
|
}
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
/** Check if the store has an event */
|
|
135
|
+
hasEvent(id) {
|
|
136
|
+
return this.database.hasEvent(id);
|
|
96
137
|
}
|
|
97
|
-
getEvent(
|
|
98
|
-
return this.database.getEvent(
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
});
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
227
|
+
else {
|
|
228
|
+
// add even to directory
|
|
229
|
+
return { ...dir, [event.id]: event };
|
|
230
|
+
}
|
|
231
|
+
}, {}));
|
|
197
232
|
}
|
|
198
|
-
/** Creates an observable
|
|
233
|
+
/** Creates an observable that subscribes to the latest version of a replaceable event */
|
|
199
234
|
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
|
-
});
|
|
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
|
|
257
|
+
/** Creates an observable that subscribes to the latest version of an array of replaceable events*/
|
|
243
258
|
replaceableSet(pointers) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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]);
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
}
|