applesauce-core 0.11.0 → 0.12.1
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/event-store/__tests__/event-store.test.js +45 -1
- package/dist/event-store/database.d.ts +2 -1
- package/dist/event-store/database.js +6 -7
- package/dist/event-store/event-store.d.ts +15 -5
- package/dist/event-store/event-store.js +72 -24
- package/dist/event-store/index.d.ts +1 -0
- package/dist/event-store/index.js +1 -0
- package/dist/event-store/interface.d.ts +29 -0
- package/dist/helpers/__tests__/nip-19.test.d.ts +1 -0
- package/dist/helpers/__tests__/nip-19.test.js +42 -0
- package/dist/helpers/cache.d.ts +3 -4
- package/dist/helpers/cache.js +1 -1
- package/dist/helpers/direct-messages.d.ts +4 -0
- package/dist/helpers/direct-messages.js +5 -0
- package/dist/helpers/event.d.ts +10 -2
- package/dist/helpers/event.js +5 -0
- package/dist/helpers/gift-wraps.d.ts +12 -0
- package/dist/helpers/gift-wraps.js +49 -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 +10 -28
- package/dist/helpers/hidden-tags.js +24 -59
- package/dist/helpers/index.d.ts +3 -0
- package/dist/helpers/index.js +3 -0
- package/dist/helpers/mutes.d.ts +1 -0
- package/dist/helpers/mutes.js +2 -1
- package/dist/helpers/nip-19.d.ts +4 -0
- package/dist/helpers/nip-19.js +27 -0
- package/dist/observable/claim-latest.d.ts +3 -2
- package/dist/observable/claim-latest.js +2 -1
- package/dist/observable/with-immediate-value.d.ts +3 -0
- package/dist/observable/with-immediate-value.js +19 -0
- package/dist/queries/mutes.js +1 -1
- package/dist/queries/simple.d.ts +1 -1
- package/dist/queries/simple.js +3 -3
- package/dist/query-store/__tests__/query-store.test.d.ts +1 -0
- package/dist/query-store/{query-store.test.js → __tests__/query-store.test.js} +33 -3
- package/dist/query-store/query-store.d.ts +6 -4
- package/dist/query-store/query-store.js +12 -3
- package/package.json +5 -4
- /package/dist/{query-store/query-store.test.d.ts → event-store/interface.js} +0 -0
|
@@ -48,6 +48,30 @@ describe("add", () => {
|
|
|
48
48
|
expect(eventStore.getEvent(profile.id)).toBeUndefined();
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
+
describe("inserts", () => {
|
|
52
|
+
it("should emit newer replaceable events", () => {
|
|
53
|
+
const spy = subscribeSpyTo(eventStore.inserts);
|
|
54
|
+
eventStore.add(profile);
|
|
55
|
+
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 100 });
|
|
56
|
+
eventStore.add(newer);
|
|
57
|
+
expect(spy.getValues()).toEqual([profile, newer]);
|
|
58
|
+
});
|
|
59
|
+
it("should not emit when older replaceable event is added", () => {
|
|
60
|
+
const spy = subscribeSpyTo(eventStore.inserts);
|
|
61
|
+
eventStore.add(profile);
|
|
62
|
+
eventStore.add(user.profile({ name: "new name" }, { created_at: profile.created_at - 1000 }));
|
|
63
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("removes", () => {
|
|
67
|
+
it("should emit older replaceable events when the newest replaceable event is added", () => {
|
|
68
|
+
const spy = subscribeSpyTo(eventStore.removes);
|
|
69
|
+
eventStore.add(profile);
|
|
70
|
+
const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
|
|
71
|
+
eventStore.add(newer);
|
|
72
|
+
expect(spy.getValues()).toEqual([profile]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
51
75
|
describe("verifyEvent", () => {
|
|
52
76
|
it("should be called for all events added", () => {
|
|
53
77
|
const verifyEvent = vi.fn().mockReturnValue(true);
|
|
@@ -69,7 +93,7 @@ describe("verifyEvent", () => {
|
|
|
69
93
|
expect(verifyEvent).toHaveBeenCalledTimes(1);
|
|
70
94
|
});
|
|
71
95
|
});
|
|
72
|
-
describe("
|
|
96
|
+
describe("removed", () => {
|
|
73
97
|
it("should complete when event is removed", () => {
|
|
74
98
|
eventStore.add(profile);
|
|
75
99
|
const spy = subscribeSpyTo(eventStore.removed(profile.id));
|
|
@@ -187,6 +211,13 @@ describe("replaceable", () => {
|
|
|
187
211
|
eventStore.add(user.profile({ name: "really old name" }, { created_at: profile.created_at - 1000 }));
|
|
188
212
|
expect(spy.getValues()).toEqual([profile]);
|
|
189
213
|
});
|
|
214
|
+
it("should emit newer events", () => {
|
|
215
|
+
const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
|
|
216
|
+
eventStore.add(profile);
|
|
217
|
+
const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 500 });
|
|
218
|
+
eventStore.add(newProfile);
|
|
219
|
+
expect(spy.getValues()).toEqual([profile, newProfile]);
|
|
220
|
+
});
|
|
190
221
|
});
|
|
191
222
|
describe("timeline", () => {
|
|
192
223
|
it("should emit an empty array if there are not events", () => {
|
|
@@ -223,6 +254,19 @@ describe("timeline", () => {
|
|
|
223
254
|
eventStore.add(user.profile({ name: "old-name" }, { created_at: profile.created_at - 1000 }));
|
|
224
255
|
expect(spy.getValues()).toEqual([[profile]]);
|
|
225
256
|
});
|
|
257
|
+
it("should return new array for every value", () => {
|
|
258
|
+
const first = user.note("first note");
|
|
259
|
+
const second = user.note("second note");
|
|
260
|
+
const third = user.note("third note");
|
|
261
|
+
eventStore.add(first);
|
|
262
|
+
const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
|
|
263
|
+
eventStore.add(second);
|
|
264
|
+
eventStore.add(third);
|
|
265
|
+
const hasDuplicates = (arr) => {
|
|
266
|
+
return new Set(arr).size !== arr.length;
|
|
267
|
+
};
|
|
268
|
+
expect(hasDuplicates(spy.getValues())).toBe(false);
|
|
269
|
+
});
|
|
226
270
|
});
|
|
227
271
|
describe("replaceableSet", () => {
|
|
228
272
|
it("should not emit if there are not events", () => {
|
|
@@ -14,6 +14,7 @@ export declare class Database {
|
|
|
14
14
|
protected created_at: NostrEvent[];
|
|
15
15
|
/** LRU cache of last events touched */
|
|
16
16
|
events: LRU<import("nostr-tools").Event>;
|
|
17
|
+
/** A sorted array of replaceable events by uid */
|
|
17
18
|
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
18
19
|
/** A stream of events inserted into the database */
|
|
19
20
|
inserted: Subject<import("nostr-tools").Event>;
|
|
@@ -60,7 +61,7 @@ export declare class Database {
|
|
|
60
61
|
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
61
62
|
/** Returns all events that match the filter */
|
|
62
63
|
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
63
|
-
|
|
64
|
+
getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
|
|
64
65
|
/** Remove the oldest events that are not claimed */
|
|
65
66
|
prune(limit?: number): number;
|
|
66
67
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import { Subject } from "rxjs";
|
|
3
|
-
import {
|
|
3
|
+
import { getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
|
|
4
4
|
import { INDEXABLE_TAGS } from "./common.js";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
6
|
import { LRU } from "../helpers/lru.js";
|
|
@@ -17,6 +17,7 @@ export class Database {
|
|
|
17
17
|
created_at = [];
|
|
18
18
|
/** LRU cache of last events touched */
|
|
19
19
|
events = new LRU();
|
|
20
|
+
/** A sorted array of replaceable events by uid */
|
|
20
21
|
replaceable = new Map();
|
|
21
22
|
/** A stream of events inserted into the database */
|
|
22
23
|
inserted = new Subject();
|
|
@@ -83,12 +84,8 @@ export class Database {
|
|
|
83
84
|
addEvent(event) {
|
|
84
85
|
const id = event.id;
|
|
85
86
|
const current = this.events.get(id);
|
|
86
|
-
if (current)
|
|
87
|
-
// if this is a duplicate event, transfer some important symbols
|
|
88
|
-
if (event[FromCacheSymbol])
|
|
89
|
-
current[FromCacheSymbol] = event[FromCacheSymbol];
|
|
87
|
+
if (current)
|
|
90
88
|
return current;
|
|
91
|
-
}
|
|
92
89
|
this.onBeforeInsert?.(event);
|
|
93
90
|
this.events.set(id, event);
|
|
94
91
|
this.getKindIndex(event.kind).add(event);
|
|
@@ -105,9 +102,11 @@ export class Database {
|
|
|
105
102
|
const uid = getEventUID(event);
|
|
106
103
|
let array = this.replaceable.get(uid);
|
|
107
104
|
if (!this.replaceable.has(uid)) {
|
|
105
|
+
// add an empty array if there is no array
|
|
108
106
|
array = [];
|
|
109
107
|
this.replaceable.set(uid, array);
|
|
110
108
|
}
|
|
109
|
+
// insert the event into the sorted array
|
|
111
110
|
insertEventIntoDescendingList(array, event);
|
|
112
111
|
}
|
|
113
112
|
this.inserted.next(event);
|
|
@@ -287,7 +286,7 @@ export class Database {
|
|
|
287
286
|
}
|
|
288
287
|
return events;
|
|
289
288
|
}
|
|
290
|
-
|
|
289
|
+
getEventsForFilters(filters) {
|
|
291
290
|
if (filters.length === 0)
|
|
292
291
|
throw new Error("No Filters");
|
|
293
292
|
let events = new Set();
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
import { Database } from "./database.js";
|
|
4
|
-
|
|
4
|
+
import { IEventStore } from "./interface.js";
|
|
5
|
+
export declare const EventStoreSymbol: unique symbol;
|
|
6
|
+
export declare class EventStore implements IEventStore {
|
|
5
7
|
database: Database;
|
|
6
8
|
/** Enable this to keep old versions of replaceable events */
|
|
7
9
|
keepOldVersions: boolean;
|
|
8
10
|
/** A method used to verify new events before added them */
|
|
9
11
|
verifyEvent?: (event: NostrEvent) => boolean;
|
|
12
|
+
/** A stream of new events added to the store */
|
|
13
|
+
inserts: Observable<NostrEvent>;
|
|
14
|
+
/** A stream of events that have been updated */
|
|
15
|
+
updates: Observable<NostrEvent>;
|
|
16
|
+
/** A stream of events that have been removed */
|
|
17
|
+
removes: Observable<NostrEvent>;
|
|
10
18
|
constructor();
|
|
11
19
|
protected deletedIds: Set<string>;
|
|
12
20
|
protected deletedCoords: Map<string, number>;
|
|
@@ -26,16 +34,18 @@ export declare class EventStore {
|
|
|
26
34
|
/** Add an event to the store and notifies all subscribes it has updated */
|
|
27
35
|
update(event: NostrEvent): NostrEvent;
|
|
28
36
|
/** Get all events matching a filter */
|
|
29
|
-
getAll(filters: Filter[]): Set<NostrEvent>;
|
|
37
|
+
getAll(filters: Filter | Filter[]): Set<NostrEvent>;
|
|
30
38
|
/** Check if the store has an event */
|
|
31
|
-
hasEvent(
|
|
32
|
-
getEvent(
|
|
39
|
+
hasEvent(id: string): boolean;
|
|
40
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
33
41
|
/** Check if the store has a replaceable event */
|
|
34
42
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
35
43
|
/** Gets the latest version of a replaceable event */
|
|
36
44
|
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined;
|
|
37
45
|
/** Returns all versions of a replaceable event */
|
|
38
46
|
getReplaceableHistory(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
|
|
47
|
+
/** Returns a timeline of events that match filters */
|
|
48
|
+
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
39
49
|
/**
|
|
40
50
|
* Creates an observable that streams all events that match the filter and remains open
|
|
41
51
|
* @param filters
|
|
@@ -45,7 +55,7 @@ export declare class EventStore {
|
|
|
45
55
|
/** Returns an observable that completes when an event is removed */
|
|
46
56
|
removed(id: string): Observable<never>;
|
|
47
57
|
/** Creates an observable that emits when event is updated */
|
|
48
|
-
updated(
|
|
58
|
+
updated(event: string | NostrEvent): Observable<NostrEvent>;
|
|
49
59
|
/** Creates an observable that subscribes to a single event */
|
|
50
60
|
event(id: string): Observable<NostrEvent | undefined>;
|
|
51
61
|
/** Creates an observable that subscribes to multiple events */
|
|
@@ -3,12 +3,13 @@ import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
|
3
3
|
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
4
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, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
|
|
6
|
+
import { FromCacheSymbol, getEventUID, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
|
|
7
7
|
import { matchFilters } from "../helpers/filter.js";
|
|
8
8
|
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
|
|
9
9
|
import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
|
|
10
10
|
import { claimEvents } from "../observable/claim-events.js";
|
|
11
11
|
import { claimLatest } from "../observable/claim-latest.js";
|
|
12
|
+
export const EventStoreSymbol = Symbol.for("event-store");
|
|
12
13
|
function sortDesc(a, b) {
|
|
13
14
|
return b.created_at - a.created_at;
|
|
14
15
|
}
|
|
@@ -18,6 +19,12 @@ export class EventStore {
|
|
|
18
19
|
keepOldVersions = false;
|
|
19
20
|
/** A method used to verify new events before added them */
|
|
20
21
|
verifyEvent;
|
|
22
|
+
/** A stream of new events added to the store */
|
|
23
|
+
inserts;
|
|
24
|
+
/** A stream of events that have been updated */
|
|
25
|
+
updates;
|
|
26
|
+
/** A stream of events that have been removed */
|
|
27
|
+
removes;
|
|
21
28
|
constructor() {
|
|
22
29
|
this.database = new Database();
|
|
23
30
|
this.database.onBeforeInsert = (event) => {
|
|
@@ -25,6 +32,17 @@ export class EventStore {
|
|
|
25
32
|
if (this.verifyEvent && this.verifyEvent(event) === false)
|
|
26
33
|
throw new Error("Invalid event");
|
|
27
34
|
};
|
|
35
|
+
// when events are added to the database, add the symbol
|
|
36
|
+
this.database.inserted.subscribe((event) => {
|
|
37
|
+
Reflect.set(event, EventStoreSymbol, this);
|
|
38
|
+
});
|
|
39
|
+
// when events are removed from the database, remove the symbol
|
|
40
|
+
this.database.removed.subscribe((event) => {
|
|
41
|
+
Reflect.deleteProperty(event, EventStoreSymbol);
|
|
42
|
+
});
|
|
43
|
+
this.inserts = this.database.inserted;
|
|
44
|
+
this.updates = this.database.updated;
|
|
45
|
+
this.removes = this.database.removed;
|
|
28
46
|
}
|
|
29
47
|
// delete state
|
|
30
48
|
deletedIds = new Set();
|
|
@@ -69,6 +87,10 @@ export class EventStore {
|
|
|
69
87
|
for (const relay of relays)
|
|
70
88
|
addSeenRelay(dest, relay);
|
|
71
89
|
}
|
|
90
|
+
// copy the from cache symbol only if its true
|
|
91
|
+
const fromCache = Reflect.get(source, FromCacheSymbol);
|
|
92
|
+
if (fromCache && !Reflect.get(dest, FromCacheSymbol))
|
|
93
|
+
Reflect.set(dest, FromCacheSymbol, fromCache);
|
|
72
94
|
}
|
|
73
95
|
/**
|
|
74
96
|
* Adds an event to the database and update subscriptions
|
|
@@ -80,6 +102,25 @@ export class EventStore {
|
|
|
80
102
|
// Ignore if the event was deleted
|
|
81
103
|
if (this.checkDeleted(event))
|
|
82
104
|
return event;
|
|
105
|
+
// Get the replaceable identifier
|
|
106
|
+
const d = isReplaceable(event.kind) ? getTagValue(event, "d") : undefined;
|
|
107
|
+
// Don't insert the event if there is already a newer version
|
|
108
|
+
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
109
|
+
const existing = this.database.getReplaceable(event.kind, event.pubkey, d);
|
|
110
|
+
// If there is already a newer version, copy cached symbols and return existing event
|
|
111
|
+
if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
|
|
112
|
+
EventStore.mergeDuplicateEvent(event, existing[0]);
|
|
113
|
+
return existing[0];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (this.database.hasEvent(event.id)) {
|
|
117
|
+
// Duplicate event, copy symbols and return existing event
|
|
118
|
+
const existing = this.database.getEvent(event.id);
|
|
119
|
+
if (existing) {
|
|
120
|
+
EventStore.mergeDuplicateEvent(event, existing);
|
|
121
|
+
return existing;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
83
124
|
// Insert event into database
|
|
84
125
|
const inserted = this.database.addEvent(event);
|
|
85
126
|
// Copy cached data if its a duplicate event
|
|
@@ -90,7 +131,7 @@ export class EventStore {
|
|
|
90
131
|
addSeenRelay(inserted, fromRelay);
|
|
91
132
|
// remove all old version of the replaceable event
|
|
92
133
|
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
93
|
-
const existing = this.database.getReplaceable(event.kind, event.pubkey,
|
|
134
|
+
const existing = this.database.getReplaceable(event.kind, event.pubkey, d);
|
|
94
135
|
if (existing) {
|
|
95
136
|
const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
|
|
96
137
|
for (const old of older)
|
|
@@ -117,14 +158,14 @@ export class EventStore {
|
|
|
117
158
|
}
|
|
118
159
|
/** Get all events matching a filter */
|
|
119
160
|
getAll(filters) {
|
|
120
|
-
return this.database.
|
|
161
|
+
return this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
|
|
121
162
|
}
|
|
122
163
|
/** Check if the store has an event */
|
|
123
|
-
hasEvent(
|
|
124
|
-
return this.database.hasEvent(
|
|
164
|
+
hasEvent(id) {
|
|
165
|
+
return this.database.hasEvent(id);
|
|
125
166
|
}
|
|
126
|
-
getEvent(
|
|
127
|
-
return this.database.getEvent(
|
|
167
|
+
getEvent(id) {
|
|
168
|
+
return this.database.getEvent(id);
|
|
128
169
|
}
|
|
129
170
|
/** Check if the store has a replaceable event */
|
|
130
171
|
hasReplaceable(kind, pubkey, d) {
|
|
@@ -138,6 +179,10 @@ export class EventStore {
|
|
|
138
179
|
getReplaceableHistory(kind, pubkey, d) {
|
|
139
180
|
return this.database.getReplaceable(kind, pubkey, d);
|
|
140
181
|
}
|
|
182
|
+
/** Returns a timeline of events that match filters */
|
|
183
|
+
getTimeline(filters) {
|
|
184
|
+
return Array.from(this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters])).sort(sortDesc);
|
|
185
|
+
}
|
|
141
186
|
/**
|
|
142
187
|
* Creates an observable that streams all events that match the filter and remains open
|
|
143
188
|
* @param filters
|
|
@@ -149,14 +194,14 @@ export class EventStore {
|
|
|
149
194
|
// merge existing events
|
|
150
195
|
onlyNew ? EMPTY : from(this.getAll(filters)),
|
|
151
196
|
// subscribe to future events
|
|
152
|
-
this.
|
|
197
|
+
this.inserts.pipe(filter((e) => matchFilters(filters, e))));
|
|
153
198
|
}
|
|
154
199
|
/** Returns an observable that completes when an event is removed */
|
|
155
200
|
removed(id) {
|
|
156
201
|
const deleted = this.checkDeleted(id);
|
|
157
202
|
if (deleted)
|
|
158
203
|
return EMPTY;
|
|
159
|
-
return this.
|
|
204
|
+
return this.removes.pipe(
|
|
160
205
|
// listen for removed events
|
|
161
206
|
filter((e) => e.id === id),
|
|
162
207
|
// complete as soon as we find a matching removed event
|
|
@@ -165,8 +210,8 @@ export class EventStore {
|
|
|
165
210
|
mergeMap(() => EMPTY));
|
|
166
211
|
}
|
|
167
212
|
/** Creates an observable that emits when event is updated */
|
|
168
|
-
updated(
|
|
169
|
-
return this.database.updated.pipe(filter((e) => e.id ===
|
|
213
|
+
updated(event) {
|
|
214
|
+
return this.database.updated.pipe(filter((e) => e.id === event || e === event));
|
|
170
215
|
}
|
|
171
216
|
/** Creates an observable that subscribes to a single event */
|
|
172
217
|
event(id) {
|
|
@@ -177,13 +222,13 @@ export class EventStore {
|
|
|
177
222
|
return event ? of(event) : EMPTY;
|
|
178
223
|
}),
|
|
179
224
|
// subscribe to updates
|
|
180
|
-
this.
|
|
225
|
+
this.inserts.pipe(filter((e) => e.id === id)),
|
|
181
226
|
// subscribe to updates
|
|
182
227
|
this.updated(id),
|
|
183
228
|
// emit undefined when deleted
|
|
184
229
|
this.removed(id).pipe(endWith(undefined))).pipe(
|
|
185
230
|
// claim all events
|
|
186
|
-
|
|
231
|
+
claimLatest(this.database));
|
|
187
232
|
}
|
|
188
233
|
/** Creates an observable that subscribes to multiple events */
|
|
189
234
|
events(ids) {
|
|
@@ -191,15 +236,15 @@ export class EventStore {
|
|
|
191
236
|
// lazily get existing events
|
|
192
237
|
defer(() => from(ids.map((id) => this.getEvent(id)))),
|
|
193
238
|
// subscribe to new events
|
|
194
|
-
this.
|
|
239
|
+
this.inserts.pipe(filter((e) => ids.includes(e.id))),
|
|
195
240
|
// subscribe to updates
|
|
196
|
-
this.
|
|
241
|
+
this.updates.pipe(filter((e) => ids.includes(e.id)))).pipe(
|
|
197
242
|
// ignore empty messages
|
|
198
243
|
filter((e) => !!e),
|
|
199
244
|
// claim all events until cleanup
|
|
200
245
|
claimEvents(this.database),
|
|
201
246
|
// watch for removed events
|
|
202
|
-
mergeWith(this.
|
|
247
|
+
mergeWith(this.removes.pipe(filter((e) => ids.includes(e.id)), map((e) => e.id))),
|
|
203
248
|
// merge all events into a directory
|
|
204
249
|
scan((dir, event) => {
|
|
205
250
|
if (typeof event === "string") {
|
|
@@ -224,13 +269,16 @@ export class EventStore {
|
|
|
224
269
|
return event ? of(event) : EMPTY;
|
|
225
270
|
}),
|
|
226
271
|
// subscribe to new events
|
|
227
|
-
this.
|
|
272
|
+
this.inserts.pipe(filter((e) => e.pubkey == pubkey && e.kind === kind && (d !== undefined ? getReplaceableIdentifier(e) === d : true)))).pipe(
|
|
228
273
|
// only update if event is newer
|
|
229
|
-
distinctUntilChanged((prev, event) =>
|
|
274
|
+
distinctUntilChanged((prev, event) => {
|
|
275
|
+
// are the events the same? i.e. is the prev event older
|
|
276
|
+
return prev.created_at >= event.created_at;
|
|
277
|
+
}),
|
|
230
278
|
// Hacky way to extract the current event so takeUntil can access it
|
|
231
279
|
tap((event) => (current = event)),
|
|
232
280
|
// complete when event is removed
|
|
233
|
-
takeUntil(this.
|
|
281
|
+
takeUntil(this.removes.pipe(filter((e) => e.id === current?.id))),
|
|
234
282
|
// emit undefined when removed
|
|
235
283
|
endWith(undefined),
|
|
236
284
|
// keep the observable hot
|
|
@@ -245,7 +293,7 @@ export class EventStore {
|
|
|
245
293
|
// start with existing events
|
|
246
294
|
defer(() => from(pointers.map((p) => this.getReplaceable(p.kind, p.pubkey, p.identifier)))),
|
|
247
295
|
// subscribe to new events
|
|
248
|
-
this.
|
|
296
|
+
this.inserts.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))))).pipe(
|
|
249
297
|
// filter out undefined
|
|
250
298
|
filter((e) => !!e),
|
|
251
299
|
// claim all events
|
|
@@ -253,7 +301,7 @@ export class EventStore {
|
|
|
253
301
|
// convert events to add commands
|
|
254
302
|
map((e) => ["add", e]),
|
|
255
303
|
// watch for removed events
|
|
256
|
-
mergeWith(this.
|
|
304
|
+
mergeWith(this.removes.pipe(filter((e) => isReplaceable(e.kind) && uids.has(getEventUID(e))), map((e) => ["remove", e]))),
|
|
257
305
|
// reduce events into directory
|
|
258
306
|
scan((dir, [action, event]) => {
|
|
259
307
|
const uid = getEventUID(event);
|
|
@@ -278,15 +326,15 @@ export class EventStore {
|
|
|
278
326
|
filters = Array.isArray(filters) ? filters : [filters];
|
|
279
327
|
const seen = new Map();
|
|
280
328
|
// get current events
|
|
281
|
-
return defer(() => of(Array.from(this.database.
|
|
329
|
+
return defer(() => of(Array.from(this.database.getEventsForFilters(filters)).sort(sortDesc))).pipe(
|
|
282
330
|
// claim existing events
|
|
283
331
|
claimEvents(this.database),
|
|
284
332
|
// subscribe to newer events
|
|
285
|
-
mergeWith(this.
|
|
333
|
+
mergeWith(this.inserts.pipe(filter((e) => matchFilters(filters, e)),
|
|
286
334
|
// claim all new events
|
|
287
335
|
claimEvents(this.database))),
|
|
288
336
|
// subscribe to delete events
|
|
289
|
-
mergeWith(this.
|
|
337
|
+
mergeWith(this.removes.pipe(filter((e) => matchFilters(filters, e)), map((e) => e.id))),
|
|
290
338
|
// build a timeline
|
|
291
339
|
scan((timeline, event) => {
|
|
292
340
|
// filter out removed events from timeline
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
export interface IEventStore {
|
|
4
|
+
inserts: Observable<NostrEvent>;
|
|
5
|
+
updates: Observable<NostrEvent>;
|
|
6
|
+
removes: Observable<NostrEvent>;
|
|
7
|
+
add(event: NostrEvent, fromRelay?: string): NostrEvent;
|
|
8
|
+
remove(event: string | NostrEvent): boolean;
|
|
9
|
+
update(event: NostrEvent): NostrEvent;
|
|
10
|
+
hasEvent(id: string): boolean;
|
|
11
|
+
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
12
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
13
|
+
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
14
|
+
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
15
|
+
getAll(filters: Filter | Filter[]): Set<NostrEvent>;
|
|
16
|
+
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
17
|
+
filters(filters: Filter | Filter[]): Observable<NostrEvent>;
|
|
18
|
+
updated(id: string | NostrEvent): Observable<NostrEvent>;
|
|
19
|
+
removed(id: string): Observable<never>;
|
|
20
|
+
event(id: string): Observable<NostrEvent | undefined>;
|
|
21
|
+
events(ids: string[]): Observable<Record<string, NostrEvent>>;
|
|
22
|
+
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
|
|
23
|
+
replaceableSet(pointers: {
|
|
24
|
+
kind: number;
|
|
25
|
+
pubkey: string;
|
|
26
|
+
identifier?: string;
|
|
27
|
+
}[]): Observable<Record<string, NostrEvent>>;
|
|
28
|
+
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
3
|
+
import { normalizeToPubkey, normalizeToSecretKey } from "../nip-19.js";
|
|
4
|
+
describe("normalizeToPubkey", () => {
|
|
5
|
+
it("should get pubkey from npub", () => {
|
|
6
|
+
expect(normalizeToPubkey("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
7
|
+
});
|
|
8
|
+
it("should get pubkey from nprofile", () => {
|
|
9
|
+
expect(normalizeToPubkey("nprofile1qyw8wumn8ghj7umpw3jkcmrfw3jju6r6wfjrzdpe9e3k7mf0qyf8wumn8ghj7mn0wd68yat99e3k7mf0qqszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fg5g7lja")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
10
|
+
});
|
|
11
|
+
it("should return hex pubkey", () => {
|
|
12
|
+
expect(normalizeToPubkey("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
13
|
+
});
|
|
14
|
+
it("should throw on invalid hex pubkey", () => {
|
|
15
|
+
expect(() => {
|
|
16
|
+
normalizeToPubkey("5028372");
|
|
17
|
+
}).toThrow();
|
|
18
|
+
});
|
|
19
|
+
it("should throw on invalid string", () => {
|
|
20
|
+
expect(() => {
|
|
21
|
+
normalizeToPubkey("testing");
|
|
22
|
+
}).toThrow();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("normalizeToSecretKey", () => {
|
|
26
|
+
it("should get secret key from nsec", () => {
|
|
27
|
+
expect(bytesToHex(normalizeToSecretKey("nsec1xe7znq745x5n68566l32ru72aajz3pk2cys9lnf3tuexvkw0dldsj8v2lm"))).toEqual("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb");
|
|
28
|
+
});
|
|
29
|
+
it("should get secret key from raw hex", () => {
|
|
30
|
+
expect(bytesToHex(normalizeToSecretKey("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb"))).toEqual("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb");
|
|
31
|
+
});
|
|
32
|
+
it("should throw on invalid hex key", () => {
|
|
33
|
+
expect(() => {
|
|
34
|
+
normalizeToSecretKey("209573290");
|
|
35
|
+
}).toThrow();
|
|
36
|
+
});
|
|
37
|
+
it("should throw on npub", () => {
|
|
38
|
+
expect(() => {
|
|
39
|
+
normalizeToSecretKey("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr");
|
|
40
|
+
}).toThrow();
|
|
41
|
+
});
|
|
42
|
+
});
|
package/dist/helpers/cache.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function setCachedValue<T extends unknown>(event: NostrEvent | EventTemplate, symbol: symbol, value: T): void;
|
|
1
|
+
export declare function getCachedValue<T extends unknown>(event: any, symbol: symbol): T | undefined;
|
|
2
|
+
export declare function setCachedValue<T extends unknown>(event: any, symbol: symbol, value: T): void;
|
|
4
3
|
/** Internal method used to cache computed values on events */
|
|
5
|
-
export declare function getOrComputeCachedValue<T extends unknown>(event:
|
|
4
|
+
export declare function getOrComputeCachedValue<T extends unknown>(event: any, symbol: symbol, compute: () => T): T;
|
package/dist/helpers/cache.js
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { HiddenContentSigner } from "./hidden-content.js";
|
|
3
|
+
/** Returns the decrypted content of a direct message */
|
|
4
|
+
export declare function decryptDirectMessage(message: NostrEvent, signer: HiddenContentSigner): Promise<string>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { getHiddenContent, unlockHiddenContent } from "./hidden-content.js";
|
|
2
|
+
/** Returns the decrypted content of a direct message */
|
|
3
|
+
export async function decryptDirectMessage(message, signer) {
|
|
4
|
+
return getHiddenContent(message) || (await unlockHiddenContent(message, signer));
|
|
5
|
+
}
|
package/dist/helpers/event.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NostrEvent, VerifiedEvent } from "nostr-tools";
|
|
2
|
+
import { IEventStore } from "../event-store/interface.js";
|
|
2
3
|
export declare const EventUIDSymbol: unique symbol;
|
|
3
4
|
export declare const EventIndexableTagsSymbol: unique symbol;
|
|
4
5
|
export declare const FromCacheSymbol: unique symbol;
|
|
@@ -34,13 +35,20 @@ export declare function getIndexableTags(event: NostrEvent): Set<string>;
|
|
|
34
35
|
* Returns the second index ( tag[1] ) of the first tag that matches the name
|
|
35
36
|
* If the event has any hidden tags they will be searched first
|
|
36
37
|
*/
|
|
37
|
-
export declare function getTagValue
|
|
38
|
+
export declare function getTagValue<T extends {
|
|
39
|
+
kind: number;
|
|
40
|
+
tags: string[][];
|
|
41
|
+
content: string;
|
|
42
|
+
pubkey: string;
|
|
43
|
+
}>(event: T, name: string): string | undefined;
|
|
38
44
|
/** Sets events verified flag without checking anything */
|
|
39
45
|
export declare function fakeVerifyEvent(event: NostrEvent): event is VerifiedEvent;
|
|
40
46
|
/** Marks an event as being from a cache */
|
|
41
47
|
export declare function markFromCache(event: NostrEvent): void;
|
|
42
48
|
/** Returns if an event was from a cache */
|
|
43
49
|
export declare function isFromCache(event: NostrEvent): boolean;
|
|
50
|
+
/** Returns the EventStore of an event if its been added to one */
|
|
51
|
+
export declare function getParentEventStore<T extends object>(event: T): IEventStore | undefined;
|
|
44
52
|
/**
|
|
45
53
|
* Returns the replaceable identifier for a replaceable event
|
|
46
54
|
* @throws
|
package/dist/helpers/event.js
CHANGED
|
@@ -3,6 +3,7 @@ import { INDEXABLE_TAGS } from "../event-store/common.js";
|
|
|
3
3
|
import { getHiddenTags } from "./hidden-tags.js";
|
|
4
4
|
import { getOrComputeCachedValue } from "./cache.js";
|
|
5
5
|
import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
|
|
6
|
+
import { EventStoreSymbol } from "../event-store/event-store.js";
|
|
6
7
|
export const EventUIDSymbol = Symbol.for("event-uid");
|
|
7
8
|
export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
|
|
8
9
|
export const FromCacheSymbol = Symbol.for("from-cache");
|
|
@@ -90,6 +91,10 @@ export function markFromCache(event) {
|
|
|
90
91
|
export function isFromCache(event) {
|
|
91
92
|
return !!event[FromCacheSymbol];
|
|
92
93
|
}
|
|
94
|
+
/** Returns the EventStore of an event if its been added to one */
|
|
95
|
+
export function getParentEventStore(event) {
|
|
96
|
+
return Reflect.get(event, EventStoreSymbol);
|
|
97
|
+
}
|
|
93
98
|
/**
|
|
94
99
|
* Returns the replaceable identifier for a replaceable event
|
|
95
100
|
* @throws
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NostrEvent, UnsignedEvent } from "nostr-tools";
|
|
2
|
+
import { HiddenContentSigner } from "./hidden-content.js";
|
|
3
|
+
export declare const GiftWrapSealSymbol: unique symbol;
|
|
4
|
+
export declare const GiftWrapEventSymbol: unique symbol;
|
|
5
|
+
/** Returns the unsigned seal event in a gift-wrap event */
|
|
6
|
+
export declare function getGiftWrapSeal(gift: NostrEvent): NostrEvent | undefined;
|
|
7
|
+
/** Returns the unsigned event in the gift-wrap seal */
|
|
8
|
+
export declare function getGiftWrapEvent(gift: NostrEvent): UnsignedEvent | undefined;
|
|
9
|
+
/** Returns if a gift-wrap event or gift-wrap seal is locked */
|
|
10
|
+
export declare function isGiftWrapLocked(gift: NostrEvent): boolean;
|
|
11
|
+
/** Unlocks and returns the unsigned seal event in a gift-wrap */
|
|
12
|
+
export declare function unlockGiftWrap(gift: NostrEvent, signer: HiddenContentSigner): Promise<UnsignedEvent>;
|