applesauce-core 4.2.0 → 4.4.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/async-event-store.d.ts +6 -6
- package/dist/event-store/async-event-store.js +6 -6
- package/dist/event-store/event-memory.d.ts +22 -13
- package/dist/event-store/event-memory.js +149 -33
- package/dist/event-store/event-store.d.ts +6 -6
- package/dist/event-store/event-store.js +6 -6
- package/dist/event-store/interface.d.ts +14 -13
- package/dist/helpers/event.js +3 -4
- package/dist/helpers/filter.d.ts +19 -5
- package/dist/helpers/filter.js +42 -14
- package/dist/helpers/pointers.d.ts +9 -8
- package/dist/helpers/pointers.js +44 -16
- package/dist/helpers/share.d.ts +7 -1
- package/dist/helpers/share.js +23 -5
- package/dist/helpers/zap.js +7 -1
- package/dist/observable/claim-events.d.ts +1 -1
- package/dist/observable/claim-events.js +7 -5
- package/dist/observable/claim-latest.js +12 -9
- package/package.json +1 -1
|
@@ -41,7 +41,7 @@ declare const AsyncEventStore_base: {
|
|
|
41
41
|
export declare class AsyncEventStore extends AsyncEventStore_base implements IAsyncEventStore {
|
|
42
42
|
database: IAsyncEventDatabase;
|
|
43
43
|
/** Optional memory database for ensuring single event instances */
|
|
44
|
-
memory
|
|
44
|
+
memory: EventMemory;
|
|
45
45
|
/** Enable this to keep old versions of replaceable events */
|
|
46
46
|
keepOldVersions: boolean;
|
|
47
47
|
/** Enable this to keep expired events */
|
|
@@ -115,13 +115,13 @@ export declare class AsyncEventStore extends AsyncEventStore_base implements IAs
|
|
|
115
115
|
/** Returns a timeline of events that match filters */
|
|
116
116
|
getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
|
|
117
117
|
/** Passthrough method for the database.touch */
|
|
118
|
-
touch(event: NostrEvent): void
|
|
119
|
-
/**
|
|
120
|
-
claim(event: NostrEvent
|
|
118
|
+
touch(event: NostrEvent): void;
|
|
119
|
+
/** Increments the claim count on the event and touches it */
|
|
120
|
+
claim(event: NostrEvent): void;
|
|
121
121
|
/** Checks if an event is claimed by anything */
|
|
122
122
|
isClaimed(event: NostrEvent): boolean;
|
|
123
|
-
/**
|
|
124
|
-
removeClaim(event: NostrEvent
|
|
123
|
+
/** Decrements the claim count on an event */
|
|
124
|
+
removeClaim(event: NostrEvent): void;
|
|
125
125
|
/** Removes all claims on an event */
|
|
126
126
|
clearClaim(event: NostrEvent): void;
|
|
127
127
|
/** Pass through method for the database.unclaimed */
|
|
@@ -320,17 +320,17 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
|
|
|
320
320
|
touch(event) {
|
|
321
321
|
return this.memory?.touch(event);
|
|
322
322
|
}
|
|
323
|
-
/**
|
|
324
|
-
claim(event
|
|
325
|
-
return this.memory?.claim(event
|
|
323
|
+
/** Increments the claim count on the event and touches it */
|
|
324
|
+
claim(event) {
|
|
325
|
+
return this.memory?.claim(event);
|
|
326
326
|
}
|
|
327
327
|
/** Checks if an event is claimed by anything */
|
|
328
328
|
isClaimed(event) {
|
|
329
329
|
return this.memory?.isClaimed(event) ?? false;
|
|
330
330
|
}
|
|
331
|
-
/**
|
|
332
|
-
removeClaim(event
|
|
333
|
-
return this.memory?.removeClaim(event
|
|
331
|
+
/** Decrements the claim count on an event */
|
|
332
|
+
removeClaim(event) {
|
|
333
|
+
return this.memory?.removeClaim(event);
|
|
334
334
|
}
|
|
335
335
|
/** Removes all claims on an event */
|
|
336
336
|
clearClaim(event) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { FilterWithAnd } from "../helpers/filter.js";
|
|
2
3
|
import { LRU } from "../helpers/lru.js";
|
|
3
4
|
import { IEventMemory } from "./interface.js";
|
|
4
5
|
/** An in-memory database of events */
|
|
@@ -9,11 +10,13 @@ export declare class EventMemory implements IEventMemory {
|
|
|
9
10
|
protected authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
10
11
|
protected tags: LRU<Set<import("nostr-tools").Event>>;
|
|
11
12
|
protected created_at: NostrEvent[];
|
|
13
|
+
/** Composite index for kind+author queries (common pattern) */
|
|
14
|
+
protected kindAuthor: Map<string, Set<import("nostr-tools").Event>>;
|
|
12
15
|
/** LRU cache of last events touched */
|
|
13
16
|
events: LRU<import("nostr-tools").Event>;
|
|
14
17
|
/** A sorted array of replaceable events by address */
|
|
15
18
|
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
16
|
-
/** The number of events in the
|
|
19
|
+
/** The number of events in the database */
|
|
17
20
|
get size(): number;
|
|
18
21
|
/** Checks if the database contains an event without touching it */
|
|
19
22
|
hasEvent(id: string): boolean;
|
|
@@ -26,27 +29,27 @@ export declare class EventMemory implements IEventMemory {
|
|
|
26
29
|
/** Gets the history of a replaceable event */
|
|
27
30
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
28
31
|
/** Gets all events that match the filters */
|
|
29
|
-
getByFilters(filters:
|
|
32
|
+
getByFilters(filters: FilterWithAnd | FilterWithAnd[]): NostrEvent[];
|
|
30
33
|
/** Gets a timeline of events that match the filters */
|
|
31
|
-
getTimeline(filters:
|
|
34
|
+
getTimeline(filters: FilterWithAnd | FilterWithAnd[]): NostrEvent[];
|
|
32
35
|
/** Inserts an event into the database and notifies all subscriptions */
|
|
33
36
|
add(event: NostrEvent): NostrEvent;
|
|
34
37
|
/** Removes an event from the database and notifies all subscriptions */
|
|
35
38
|
remove(eventOrId: string | NostrEvent): boolean;
|
|
36
39
|
/** Remove multiple events that match the given filters */
|
|
37
|
-
removeByFilters(filters:
|
|
40
|
+
removeByFilters(filters: FilterWithAnd | FilterWithAnd[]): number;
|
|
38
41
|
/** Notify the database that an event has updated */
|
|
39
42
|
update(_event: NostrEvent): void;
|
|
40
|
-
/** A weak map of events
|
|
41
|
-
protected claims: WeakMap<import("nostr-tools").Event,
|
|
43
|
+
/** A weak map of events to claim reference counts */
|
|
44
|
+
protected claims: WeakMap<import("nostr-tools").Event, number>;
|
|
42
45
|
/** Moves an event to the top of the LRU cache */
|
|
43
46
|
touch(event: NostrEvent): void;
|
|
44
|
-
/**
|
|
45
|
-
claim(event: NostrEvent
|
|
47
|
+
/** Increments the claim count on the event and touches it */
|
|
48
|
+
claim(event: NostrEvent): void;
|
|
46
49
|
/** Checks if an event is claimed by anything */
|
|
47
50
|
isClaimed(event: NostrEvent): boolean;
|
|
48
|
-
/**
|
|
49
|
-
removeClaim(event: NostrEvent
|
|
51
|
+
/** Decrements the claim count on an event */
|
|
52
|
+
removeClaim(event: NostrEvent): void;
|
|
50
53
|
/** Removes all claims on an event */
|
|
51
54
|
clearClaim(event: NostrEvent): void;
|
|
52
55
|
/** Returns a generator of unclaimed events in order of least used */
|
|
@@ -56,7 +59,13 @@ export declare class EventMemory implements IEventMemory {
|
|
|
56
59
|
/** Index helper methods */
|
|
57
60
|
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
|
|
58
61
|
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
|
|
62
|
+
protected getKindAuthorIndex(kind: number, pubkey: string): Set<import("nostr-tools").Event>;
|
|
59
63
|
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
64
|
+
/**
|
|
65
|
+
* Helper method to remove an event from a sorted array using binary search.
|
|
66
|
+
* Falls back to indexOf if binary search doesn't find exact match.
|
|
67
|
+
*/
|
|
68
|
+
protected removeFromSortedArray(array: NostrEvent[], event: NostrEvent): void;
|
|
60
69
|
/** Iterates over all events by author */
|
|
61
70
|
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
|
|
62
71
|
/** Iterates over all events by indexable tag and value */
|
|
@@ -68,9 +77,9 @@ export declare class EventMemory implements IEventMemory {
|
|
|
68
77
|
/** Iterates over all events by id */
|
|
69
78
|
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
70
79
|
/** Returns all events that match the filter */
|
|
71
|
-
protected getEventsForFilter(filter:
|
|
80
|
+
protected getEventsForFilter(filter: FilterWithAnd): Set<NostrEvent>;
|
|
72
81
|
/** Returns all events that match the filters */
|
|
73
|
-
protected getEventsForFilters(filters:
|
|
82
|
+
protected getEventsForFilters(filters: FilterWithAnd[]): Set<NostrEvent>;
|
|
74
83
|
/** Resets the event set */
|
|
75
84
|
reset(): void;
|
|
76
85
|
}
|
|
@@ -11,11 +11,13 @@ export class EventMemory {
|
|
|
11
11
|
authors = new Map();
|
|
12
12
|
tags = new LRU();
|
|
13
13
|
created_at = [];
|
|
14
|
+
/** Composite index for kind+author queries (common pattern) */
|
|
15
|
+
kindAuthor = new Map();
|
|
14
16
|
/** LRU cache of last events touched */
|
|
15
17
|
events = new LRU();
|
|
16
18
|
/** A sorted array of replaceable events by address */
|
|
17
19
|
replaceable = new Map();
|
|
18
|
-
/** The number of events in the
|
|
20
|
+
/** The number of events in the database */
|
|
19
21
|
get size() {
|
|
20
22
|
return this.events.size;
|
|
21
23
|
}
|
|
@@ -64,6 +66,7 @@ export class EventMemory {
|
|
|
64
66
|
this.events.set(id, event);
|
|
65
67
|
this.getKindIndex(event.kind).add(event);
|
|
66
68
|
this.getAuthorsIndex(event.pubkey).add(event);
|
|
69
|
+
this.getKindAuthorIndex(event.kind, event.pubkey).add(event);
|
|
67
70
|
// Add the event to the tag indexes if they exist
|
|
68
71
|
for (const tag of getIndexableTags(event)) {
|
|
69
72
|
if (this.tags.has(tag))
|
|
@@ -97,24 +100,26 @@ export class EventMemory {
|
|
|
97
100
|
return false;
|
|
98
101
|
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
99
102
|
this.getKindIndex(event.kind).delete(event);
|
|
103
|
+
// Remove from composite kind+author index
|
|
104
|
+
const kindAuthorKey = `${event.kind}:${event.pubkey}`;
|
|
105
|
+
if (this.kindAuthor.has(kindAuthorKey)) {
|
|
106
|
+
this.kindAuthor.get(kindAuthorKey).delete(event);
|
|
107
|
+
}
|
|
100
108
|
for (const tag of getIndexableTags(event)) {
|
|
101
109
|
if (this.tags.has(tag)) {
|
|
102
110
|
this.getTagIndex(tag).delete(event);
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
|
-
// remove from created_at index
|
|
106
|
-
|
|
107
|
-
this.created_at.splice(i, 1);
|
|
113
|
+
// remove from created_at index using binary search
|
|
114
|
+
this.removeFromSortedArray(this.created_at, event);
|
|
108
115
|
this.events.delete(id);
|
|
109
|
-
// remove from replaceable index
|
|
116
|
+
// remove from replaceable index using binary search
|
|
110
117
|
if (isReplaceable(event.kind)) {
|
|
111
118
|
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
112
119
|
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
113
120
|
const array = this.replaceable.get(address);
|
|
114
|
-
if (array
|
|
115
|
-
|
|
116
|
-
array.splice(idx, 1);
|
|
117
|
-
}
|
|
121
|
+
if (array)
|
|
122
|
+
this.removeFromSortedArray(array, event);
|
|
118
123
|
}
|
|
119
124
|
// remove any claims this event has
|
|
120
125
|
this.claims.delete(event);
|
|
@@ -135,7 +140,7 @@ export class EventMemory {
|
|
|
135
140
|
update(_event) {
|
|
136
141
|
// Do nothing
|
|
137
142
|
}
|
|
138
|
-
/** A weak map of events
|
|
143
|
+
/** A weak map of events to claim reference counts */
|
|
139
144
|
claims = new WeakMap();
|
|
140
145
|
/** Moves an event to the top of the LRU cache */
|
|
141
146
|
touch(event) {
|
|
@@ -145,23 +150,30 @@ export class EventMemory {
|
|
|
145
150
|
// Move to the top of the LRU
|
|
146
151
|
this.events.set(event.id, event);
|
|
147
152
|
}
|
|
148
|
-
/**
|
|
149
|
-
claim(event
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
+
/** Increments the claim count on the event and touches it */
|
|
154
|
+
claim(event) {
|
|
155
|
+
const currentCount = this.claims.get(event) || 0;
|
|
156
|
+
this.claims.set(event, currentCount + 1);
|
|
153
157
|
// always touch event
|
|
154
158
|
this.touch(event);
|
|
155
159
|
}
|
|
156
160
|
/** Checks if an event is claimed by anything */
|
|
157
161
|
isClaimed(event) {
|
|
158
|
-
|
|
162
|
+
const count = this.claims.get(event);
|
|
163
|
+
return count !== undefined && count > 0;
|
|
159
164
|
}
|
|
160
|
-
/**
|
|
161
|
-
removeClaim(event
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
+
/** Decrements the claim count on an event */
|
|
166
|
+
removeClaim(event) {
|
|
167
|
+
const currentCount = this.claims.get(event);
|
|
168
|
+
if (currentCount !== undefined && currentCount > 0) {
|
|
169
|
+
const newCount = currentCount - 1;
|
|
170
|
+
if (newCount === 0) {
|
|
171
|
+
this.claims.delete(event);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.claims.set(event, newCount);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
165
177
|
}
|
|
166
178
|
/** Removes all claims on an event */
|
|
167
179
|
clearClaim(event) {
|
|
@@ -202,6 +214,12 @@ export class EventMemory {
|
|
|
202
214
|
this.authors.set(author, new Set());
|
|
203
215
|
return this.authors.get(author);
|
|
204
216
|
}
|
|
217
|
+
getKindAuthorIndex(kind, pubkey) {
|
|
218
|
+
const key = `${kind}:${pubkey}`;
|
|
219
|
+
if (!this.kindAuthor.has(key))
|
|
220
|
+
this.kindAuthor.set(key, new Set());
|
|
221
|
+
return this.kindAuthor.get(key);
|
|
222
|
+
}
|
|
205
223
|
getTagIndex(tagAndValue) {
|
|
206
224
|
if (!this.tags.has(tagAndValue)) {
|
|
207
225
|
// build new tag index from existing events
|
|
@@ -219,6 +237,50 @@ export class EventMemory {
|
|
|
219
237
|
}
|
|
220
238
|
return this.tags.get(tagAndValue);
|
|
221
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Helper method to remove an event from a sorted array using binary search.
|
|
242
|
+
* Falls back to indexOf if binary search doesn't find exact match.
|
|
243
|
+
*/
|
|
244
|
+
removeFromSortedArray(array, event) {
|
|
245
|
+
if (array.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
// Use binary search to find the approximate position
|
|
248
|
+
const result = binarySearch(array, (mid) => mid.created_at - event.created_at);
|
|
249
|
+
if (result) {
|
|
250
|
+
let index = result[0];
|
|
251
|
+
// Binary search finds the position, but we need to find the exact event
|
|
252
|
+
// since multiple events can have the same created_at timestamp.
|
|
253
|
+
// Search backwards and forwards from the found position
|
|
254
|
+
let found = false;
|
|
255
|
+
// Check the found position first
|
|
256
|
+
if (array[index] === event) {
|
|
257
|
+
array.splice(index, 1);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Search backwards
|
|
261
|
+
for (let i = index - 1; i >= 0 && array[i].created_at === event.created_at; i--) {
|
|
262
|
+
if (array[i] === event) {
|
|
263
|
+
array.splice(i, 1);
|
|
264
|
+
found = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (found)
|
|
269
|
+
return;
|
|
270
|
+
// Search forwards
|
|
271
|
+
for (let i = index + 1; i < array.length && array[i].created_at === event.created_at; i++) {
|
|
272
|
+
if (array[i] === event) {
|
|
273
|
+
array.splice(i, 1);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Fallback to indexOf if binary search doesn't find the event
|
|
279
|
+
// This should rarely happen, but ensures correctness
|
|
280
|
+
const idx = array.indexOf(event);
|
|
281
|
+
if (idx !== -1)
|
|
282
|
+
array.splice(idx, 1);
|
|
283
|
+
}
|
|
222
284
|
/** Iterates over all events by author */
|
|
223
285
|
*iterateAuthors(authors) {
|
|
224
286
|
for (const author of authors) {
|
|
@@ -251,24 +313,32 @@ export class EventMemory {
|
|
|
251
313
|
}
|
|
252
314
|
/** Iterates over all events by time */
|
|
253
315
|
*iterateTime(since, until) {
|
|
254
|
-
let
|
|
255
|
-
let
|
|
316
|
+
let startIndex = 0;
|
|
317
|
+
let endIndex = this.created_at.length - 1;
|
|
318
|
+
// If until is set, use binary search to find better start index
|
|
256
319
|
let start = until
|
|
257
320
|
? binarySearch(this.created_at, (mid) => {
|
|
258
321
|
return mid.created_at - until;
|
|
259
322
|
})
|
|
260
323
|
: undefined;
|
|
261
324
|
if (start)
|
|
262
|
-
|
|
325
|
+
startIndex = start[0];
|
|
326
|
+
// If since is set, use binary search to find better end index
|
|
263
327
|
const end = since
|
|
264
328
|
? binarySearch(this.created_at, (mid) => {
|
|
265
329
|
return mid.created_at - since;
|
|
266
330
|
})
|
|
267
331
|
: undefined;
|
|
268
332
|
if (end)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
333
|
+
endIndex = end[0];
|
|
334
|
+
// Yield events in the range, filtering by exact bounds
|
|
335
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
336
|
+
const event = this.created_at[i];
|
|
337
|
+
if (until !== undefined && event.created_at > until)
|
|
338
|
+
continue;
|
|
339
|
+
if (since !== undefined && event.created_at < since)
|
|
340
|
+
break;
|
|
341
|
+
yield event;
|
|
272
342
|
}
|
|
273
343
|
}
|
|
274
344
|
/** Iterates over all events by id */
|
|
@@ -307,21 +377,66 @@ export class EventMemory {
|
|
|
307
377
|
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
308
378
|
and(time);
|
|
309
379
|
}
|
|
380
|
+
// Process AND tag filters (& prefix) first - NIP-ND
|
|
381
|
+
// AND takes precedence and requires ALL values to be present
|
|
382
|
+
for (const t of INDEXABLE_TAGS) {
|
|
383
|
+
const key = `&${t}`;
|
|
384
|
+
const values = filter[key];
|
|
385
|
+
if (values?.length) {
|
|
386
|
+
// For AND logic, we need to intersect events that have ALL the specified tag values
|
|
387
|
+
// We do this by iterating through each value and intersecting the results
|
|
388
|
+
for (const value of values) {
|
|
389
|
+
and(this.iterateTag(t, [value]));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Process OR tag filters (# prefix)
|
|
394
|
+
// Skip values that are in AND tags (NIP-ND rule)
|
|
310
395
|
for (const t of INDEXABLE_TAGS) {
|
|
311
396
|
const key = `#${t}`;
|
|
312
397
|
const values = filter[key];
|
|
313
|
-
if (values?.length)
|
|
314
|
-
|
|
398
|
+
if (values?.length) {
|
|
399
|
+
// Check if there's a corresponding AND filter for this tag
|
|
400
|
+
const andKey = `&${t}`;
|
|
401
|
+
const andValues = filter[andKey];
|
|
402
|
+
// Filter out values that are in AND tags (NIP-ND rule)
|
|
403
|
+
const filteredValues = andValues ? values.filter((v) => !andValues.includes(v)) : values;
|
|
404
|
+
// Only apply OR filter if there are values left after filtering
|
|
405
|
+
if (filteredValues.length > 0)
|
|
406
|
+
and(this.iterateTag(t, filteredValues));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Optimize: Use composite kind+author index when both are present and the cross-product is small
|
|
410
|
+
if (filter.authors && filter.kinds && filter.authors.length * filter.kinds.length <= 20) {
|
|
411
|
+
const combined = new Set();
|
|
412
|
+
for (const kind of filter.kinds) {
|
|
413
|
+
for (const author of filter.authors) {
|
|
414
|
+
const key = `${kind}:${author}`;
|
|
415
|
+
const kindAuthorEvents = this.kindAuthor.get(key);
|
|
416
|
+
if (kindAuthorEvents) {
|
|
417
|
+
for (const event of kindAuthorEvents)
|
|
418
|
+
combined.add(event);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
and(combined);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Use separate indexes
|
|
426
|
+
if (filter.authors)
|
|
427
|
+
and(this.iterateAuthors(filter.authors));
|
|
428
|
+
if (filter.kinds)
|
|
429
|
+
and(this.iterateKinds(filter.kinds));
|
|
315
430
|
}
|
|
316
|
-
if (filter.authors)
|
|
317
|
-
and(this.iterateAuthors(filter.authors));
|
|
318
|
-
if (filter.kinds)
|
|
319
|
-
and(this.iterateKinds(filter.kinds));
|
|
320
431
|
// query for time last if only until is set
|
|
321
432
|
if (filter.since === undefined && filter.until !== undefined) {
|
|
322
433
|
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
323
434
|
and(time);
|
|
324
435
|
}
|
|
436
|
+
// If no filters were applied (empty filter), return all events
|
|
437
|
+
if (first) {
|
|
438
|
+
return new Set(this.events.values());
|
|
439
|
+
}
|
|
325
440
|
// if the filter queried on time and has a limit. truncate the events now
|
|
326
441
|
if (filter.limit && time) {
|
|
327
442
|
const limited = new Set();
|
|
@@ -352,6 +467,7 @@ export class EventMemory {
|
|
|
352
467
|
this.events.clear();
|
|
353
468
|
this.kinds.clear();
|
|
354
469
|
this.authors.clear();
|
|
470
|
+
this.kindAuthor.clear();
|
|
355
471
|
this.tags.clear();
|
|
356
472
|
this.created_at = [];
|
|
357
473
|
this.replaceable.clear();
|
|
@@ -41,7 +41,7 @@ declare const EventStore_base: {
|
|
|
41
41
|
export declare class EventStore extends EventStore_base implements IEventStore {
|
|
42
42
|
database: IEventDatabase;
|
|
43
43
|
/** Optional memory database for ensuring single event instances */
|
|
44
|
-
memory
|
|
44
|
+
memory: EventMemory;
|
|
45
45
|
/** Enable this to keep old versions of replaceable events */
|
|
46
46
|
keepOldVersions: boolean;
|
|
47
47
|
/** Enable this to keep expired events */
|
|
@@ -115,13 +115,13 @@ export declare class EventStore extends EventStore_base implements IEventStore {
|
|
|
115
115
|
/** Returns a timeline of events that match filters */
|
|
116
116
|
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
117
117
|
/** Passthrough method for the database.touch */
|
|
118
|
-
touch(event: NostrEvent): void
|
|
119
|
-
/**
|
|
120
|
-
claim(event: NostrEvent
|
|
118
|
+
touch(event: NostrEvent): void;
|
|
119
|
+
/** Increments the claim count on the event and touches it */
|
|
120
|
+
claim(event: NostrEvent): void;
|
|
121
121
|
/** Checks if an event is claimed by anything */
|
|
122
122
|
isClaimed(event: NostrEvent): boolean;
|
|
123
|
-
/**
|
|
124
|
-
removeClaim(event: NostrEvent
|
|
123
|
+
/** Decrements the claim count on an event */
|
|
124
|
+
removeClaim(event: NostrEvent): void;
|
|
125
125
|
/** Removes all claims on an event */
|
|
126
126
|
clearClaim(event: NostrEvent): void;
|
|
127
127
|
/** Pass through method for the database.unclaimed */
|
|
@@ -322,17 +322,17 @@ export class EventStore extends EventStoreModelMixin(class {
|
|
|
322
322
|
touch(event) {
|
|
323
323
|
return this.memory?.touch(event);
|
|
324
324
|
}
|
|
325
|
-
/**
|
|
326
|
-
claim(event
|
|
327
|
-
return this.memory?.claim(event
|
|
325
|
+
/** Increments the claim count on the event and touches it */
|
|
326
|
+
claim(event) {
|
|
327
|
+
return this.memory?.claim(event);
|
|
328
328
|
}
|
|
329
329
|
/** Checks if an event is claimed by anything */
|
|
330
330
|
isClaimed(event) {
|
|
331
331
|
return this.memory?.isClaimed(event) ?? false;
|
|
332
332
|
}
|
|
333
|
-
/**
|
|
334
|
-
removeClaim(event
|
|
335
|
-
return this.memory?.removeClaim(event
|
|
333
|
+
/** Decrements the claim count on an event */
|
|
334
|
+
removeClaim(event) {
|
|
335
|
+
return this.memory?.removeClaim(event);
|
|
336
336
|
}
|
|
337
337
|
/** Removes all claims on an event */
|
|
338
338
|
clearClaim(event) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
2
|
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
|
|
3
3
|
import { Observable } from "rxjs";
|
|
4
|
+
import { FilterWithAnd } from "../helpers/filter.js";
|
|
4
5
|
import { AddressPointerWithoutD } from "../helpers/pointers.js";
|
|
5
6
|
import { ProfileContent } from "../helpers/profile.js";
|
|
6
7
|
import { Mutes } from "../helpers/mutes.js";
|
|
@@ -18,9 +19,9 @@ export interface IEventStoreRead {
|
|
|
18
19
|
/** Get the history of a replaceable event */
|
|
19
20
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
20
21
|
/** Get all events that match the filters */
|
|
21
|
-
getByFilters(filters:
|
|
22
|
+
getByFilters(filters: FilterWithAnd | FilterWithAnd[]): NostrEvent[];
|
|
22
23
|
/** Get a timeline of events that match the filters */
|
|
23
|
-
getTimeline(filters:
|
|
24
|
+
getTimeline(filters: FilterWithAnd | FilterWithAnd[]): NostrEvent[];
|
|
24
25
|
}
|
|
25
26
|
/** The async read interface for an event store */
|
|
26
27
|
export interface IAsyncEventStoreRead {
|
|
@@ -35,9 +36,9 @@ export interface IAsyncEventStoreRead {
|
|
|
35
36
|
/** Get the history of a replaceable event */
|
|
36
37
|
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
|
|
37
38
|
/** Get all events that match the filters */
|
|
38
|
-
getByFilters(filters:
|
|
39
|
+
getByFilters(filters: FilterWithAnd | FilterWithAnd[]): Promise<NostrEvent[]>;
|
|
39
40
|
/** Get a timeline of events that match the filters */
|
|
40
|
-
getTimeline(filters:
|
|
41
|
+
getTimeline(filters: FilterWithAnd | FilterWithAnd[]): Promise<NostrEvent[]>;
|
|
41
42
|
}
|
|
42
43
|
/** The stream interface for an event store */
|
|
43
44
|
export interface IEventStoreStreams {
|
|
@@ -70,12 +71,12 @@ export interface IAsyncEventStoreActions {
|
|
|
70
71
|
export interface IEventClaims {
|
|
71
72
|
/** Tell the store that this event was used */
|
|
72
73
|
touch(event: NostrEvent): void;
|
|
73
|
-
/**
|
|
74
|
-
claim(event: NostrEvent
|
|
74
|
+
/** Increments the claim count on the event */
|
|
75
|
+
claim(event: NostrEvent): void;
|
|
75
76
|
/** Checks if an event is claimed by anything */
|
|
76
77
|
isClaimed(event: NostrEvent): boolean;
|
|
77
|
-
/**
|
|
78
|
-
removeClaim(event: NostrEvent
|
|
78
|
+
/** Decrements the claim count on an event */
|
|
79
|
+
removeClaim(event: NostrEvent): void;
|
|
79
80
|
/** Removes all claims on an event */
|
|
80
81
|
clearClaim(event: NostrEvent): void;
|
|
81
82
|
/** Returns a generator of unclaimed events in order of least used */
|
|
@@ -92,9 +93,9 @@ export interface IEventSubscriptions {
|
|
|
92
93
|
/** Subscribe to an addressable event by pointer */
|
|
93
94
|
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
|
|
94
95
|
/** Subscribe to a batch of events that match the filters */
|
|
95
|
-
filters(filters:
|
|
96
|
+
filters(filters: FilterWithAnd | FilterWithAnd[], onlyNew?: boolean): Observable<NostrEvent>;
|
|
96
97
|
/** Subscribe to a sorted timeline of events that match the filters */
|
|
97
|
-
timeline(filters:
|
|
98
|
+
timeline(filters: FilterWithAnd | FilterWithAnd[], onlyNew?: boolean): Observable<NostrEvent[]>;
|
|
98
99
|
}
|
|
99
100
|
/** @deprecated use {@link IEventSubscriptions} instead */
|
|
100
101
|
export interface IEventStoreSubscriptions extends IEventSubscriptions {
|
|
@@ -147,7 +148,7 @@ export interface IEventDatabase extends IEventStoreRead {
|
|
|
147
148
|
/** Remove an event from the database */
|
|
148
149
|
remove(event: string | NostrEvent): boolean;
|
|
149
150
|
/** Remove multiple events that match the given filters */
|
|
150
|
-
removeByFilters(filters:
|
|
151
|
+
removeByFilters(filters: FilterWithAnd | FilterWithAnd[]): number;
|
|
151
152
|
/** Notifies the database that an event has updated */
|
|
152
153
|
update?: (event: NostrEvent) => void;
|
|
153
154
|
}
|
|
@@ -158,7 +159,7 @@ export interface IAsyncEventDatabase extends IAsyncEventStoreRead {
|
|
|
158
159
|
/** Remove an event from the database */
|
|
159
160
|
remove(event: string | NostrEvent): Promise<boolean>;
|
|
160
161
|
/** Remove multiple events that match the given filters */
|
|
161
|
-
removeByFilters(filters:
|
|
162
|
+
removeByFilters(filters: FilterWithAnd | FilterWithAnd[]): Promise<number>;
|
|
162
163
|
/** Notifies the database that an event has updated */
|
|
163
164
|
update?: (event: NostrEvent) => void;
|
|
164
165
|
}
|
package/dist/helpers/event.js
CHANGED
|
@@ -43,7 +43,7 @@ export function isReplaceable(kind) {
|
|
|
43
43
|
export function getEventUID(event) {
|
|
44
44
|
let uid = Reflect.get(event, EventUIDSymbol);
|
|
45
45
|
if (!uid) {
|
|
46
|
-
if (
|
|
46
|
+
if (isAddressableKind(event.kind) || isReplaceableKind(event.kind))
|
|
47
47
|
uid = getReplaceableAddress(event);
|
|
48
48
|
else
|
|
49
49
|
uid = event.id;
|
|
@@ -53,11 +53,10 @@ export function getEventUID(event) {
|
|
|
53
53
|
}
|
|
54
54
|
/** Returns the replaceable event address for an addressable event */
|
|
55
55
|
export function getReplaceableAddress(event) {
|
|
56
|
-
if (!
|
|
56
|
+
if (!isAddressableKind(event.kind) && !isReplaceableKind(event.kind))
|
|
57
57
|
throw new Error("Event is not replaceable or addressable");
|
|
58
58
|
return getOrComputeCachedValue(event, ReplaceableAddressSymbol, () => {
|
|
59
|
-
|
|
60
|
-
return createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
59
|
+
return createReplaceableAddress(event.kind, event.pubkey, getReplaceableIdentifier(event));
|
|
61
60
|
});
|
|
62
61
|
}
|
|
63
62
|
/** Creates a replaceable event address from a kind, pubkey, and identifier */
|
package/dist/helpers/filter.d.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
2
|
export { Filter } from "nostr-tools/filter";
|
|
3
|
+
/**
|
|
4
|
+
* Extended Filter type that supports NIP-ND AND operator
|
|
5
|
+
* Uses `&` prefix for tag filters that require ALL values to match (AND logic)
|
|
6
|
+
* @example
|
|
7
|
+
* {
|
|
8
|
+
* kinds: [1],
|
|
9
|
+
* "&t": ["meme", "cat"], // Must have BOTH "meme" AND "cat" tags
|
|
10
|
+
* "#t": ["black", "white"] // Must have "black" OR "white" tags
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
export type FilterWithAnd = Filter & {
|
|
14
|
+
[key: `&${string}`]: string[] | undefined;
|
|
15
|
+
};
|
|
3
16
|
/**
|
|
4
17
|
* Copied from nostr-tools and modified to use {@link getIndexableTags}
|
|
18
|
+
* Extended to support NIP-ND AND operator with `&` prefix
|
|
5
19
|
* @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
|
|
6
20
|
*/
|
|
7
|
-
export declare function matchFilter(filter:
|
|
21
|
+
export declare function matchFilter(filter: FilterWithAnd, event: NostrEvent): boolean;
|
|
8
22
|
/** Copied from nostr-tools and modified to use {@link matchFilter} */
|
|
9
|
-
export declare function matchFilters(filters:
|
|
10
|
-
/** Copied from nostr-tools and modified to support undefined values */
|
|
11
|
-
export declare function mergeFilters(...filters:
|
|
23
|
+
export declare function matchFilters(filters: FilterWithAnd[], event: NostrEvent): boolean;
|
|
24
|
+
/** Copied from nostr-tools and modified to support undefined values and NIP-ND AND operator */
|
|
25
|
+
export declare function mergeFilters(...filters: FilterWithAnd[]): FilterWithAnd;
|
|
12
26
|
/** Check if two filters are equal */
|
|
13
|
-
export declare function isFilterEqual(a:
|
|
27
|
+
export declare function isFilterEqual(a: FilterWithAnd | FilterWithAnd[], b: FilterWithAnd | FilterWithAnd[]): boolean;
|
package/dist/helpers/filter.js
CHANGED
|
@@ -2,45 +2,69 @@ import equal from "fast-deep-equal";
|
|
|
2
2
|
import { getIndexableTags } from "./event-tags.js";
|
|
3
3
|
/**
|
|
4
4
|
* Copied from nostr-tools and modified to use {@link getIndexableTags}
|
|
5
|
+
* Extended to support NIP-ND AND operator with `&` prefix
|
|
5
6
|
* @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
|
|
6
7
|
*/
|
|
7
8
|
export function matchFilter(filter, event) {
|
|
8
|
-
if (filter.ids && filter.ids.indexOf(event.id) === -1)
|
|
9
|
+
if (filter.ids && filter.ids.indexOf(event.id) === -1)
|
|
9
10
|
return false;
|
|
10
|
-
|
|
11
|
-
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) {
|
|
11
|
+
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1)
|
|
12
12
|
return false;
|
|
13
|
-
|
|
14
|
-
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
|
|
13
|
+
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1)
|
|
15
14
|
return false;
|
|
15
|
+
if (filter.since && event.created_at < filter.since)
|
|
16
|
+
return false;
|
|
17
|
+
if (filter.until && event.created_at > filter.until)
|
|
18
|
+
return false;
|
|
19
|
+
// Process AND tag filters (& prefix) first - NIP-ND
|
|
20
|
+
// AND takes precedence and requires ALL values to be present
|
|
21
|
+
for (let f in filter) {
|
|
22
|
+
if (f[0] === "&") {
|
|
23
|
+
let tagName = f.slice(1);
|
|
24
|
+
let values = filter[f];
|
|
25
|
+
if (values && values.length > 0) {
|
|
26
|
+
const tags = getIndexableTags(event);
|
|
27
|
+
// ALL values must be present (AND logic)
|
|
28
|
+
for (const value of values) {
|
|
29
|
+
if (!tags.has(tagName + ":" + value)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
16
35
|
}
|
|
36
|
+
// Process OR tag filters (# prefix)
|
|
37
|
+
// Skip values that are in AND tags (NIP-ND rule)
|
|
17
38
|
for (let f in filter) {
|
|
18
39
|
if (f[0] === "#") {
|
|
19
40
|
let tagName = f.slice(1);
|
|
20
41
|
let values = filter[f];
|
|
21
42
|
if (values) {
|
|
43
|
+
// Check if there's a corresponding AND filter for this tag
|
|
44
|
+
const andKey = `&${tagName}`;
|
|
45
|
+
const andValues = filter[andKey];
|
|
46
|
+
// Filter out values that are in AND tags (NIP-ND rule)
|
|
47
|
+
const filteredValues = andValues ? values.filter((v) => !andValues.includes(v)) : values;
|
|
48
|
+
// If there are no values left after filtering, skip this check
|
|
49
|
+
if (filteredValues.length === 0)
|
|
50
|
+
continue;
|
|
22
51
|
const tags = getIndexableTags(event);
|
|
23
|
-
if (
|
|
52
|
+
if (filteredValues.some((v) => tags.has(tagName + ":" + v)) === false)
|
|
24
53
|
return false;
|
|
25
54
|
}
|
|
26
55
|
}
|
|
27
56
|
}
|
|
28
|
-
if (filter.since && event.created_at < filter.since)
|
|
29
|
-
return false;
|
|
30
|
-
if (filter.until && event.created_at > filter.until)
|
|
31
|
-
return false;
|
|
32
57
|
return true;
|
|
33
58
|
}
|
|
34
59
|
/** Copied from nostr-tools and modified to use {@link matchFilter} */
|
|
35
60
|
export function matchFilters(filters, event) {
|
|
36
61
|
for (let i = 0; i < filters.length; i++) {
|
|
37
|
-
if (matchFilter(filters[i], event))
|
|
62
|
+
if (matchFilter(filters[i], event))
|
|
38
63
|
return true;
|
|
39
|
-
}
|
|
40
64
|
}
|
|
41
65
|
return false;
|
|
42
66
|
}
|
|
43
|
-
/** Copied from nostr-tools and modified to support undefined values */
|
|
67
|
+
/** Copied from nostr-tools and modified to support undefined values and NIP-ND AND operator */
|
|
44
68
|
export function mergeFilters(...filters) {
|
|
45
69
|
let result = {};
|
|
46
70
|
for (let i = 0; i < filters.length; i++) {
|
|
@@ -49,7 +73,11 @@ export function mergeFilters(...filters) {
|
|
|
49
73
|
// skip undefined
|
|
50
74
|
if (values === undefined)
|
|
51
75
|
return;
|
|
52
|
-
if (property === "kinds" ||
|
|
76
|
+
if (property === "kinds" ||
|
|
77
|
+
property === "ids" ||
|
|
78
|
+
property === "authors" ||
|
|
79
|
+
property[0] === "#" ||
|
|
80
|
+
property[0] === "&") {
|
|
53
81
|
// @ts-ignore
|
|
54
82
|
result[property] = result[property] || [];
|
|
55
83
|
// @ts-ignore
|
|
@@ -16,6 +16,8 @@ export declare function parseCoordinate(a: string, requireD: true, silent: true)
|
|
|
16
16
|
export declare function parseCoordinate(a: string, requireD: false, silent: true): AddressPointerWithoutD | null;
|
|
17
17
|
/** Extra a pubkey from the result of nip19.decode */
|
|
18
18
|
export declare function getPubkeyFromDecodeResult(result?: DecodeResult): string | undefined;
|
|
19
|
+
/** Gets the relays from a decode result */
|
|
20
|
+
export declare function getRelaysFromDecodeResult(result?: DecodeResult): string[] | undefined;
|
|
19
21
|
/** Encodes the result of nip19.decode */
|
|
20
22
|
export declare function encodeDecodeResult(result: DecodeResult): "" | `nevent1${string}` | `naddr1${string}` | `nprofile1${string}` | `nsec1${string}` | `npub1${string}` | `note1${string}`;
|
|
21
23
|
/**
|
|
@@ -25,17 +27,17 @@ export declare function encodeDecodeResult(result: DecodeResult): "" | `nevent1$
|
|
|
25
27
|
export declare function getEventPointerFromETag(tag: string[]): EventPointer;
|
|
26
28
|
/**
|
|
27
29
|
* Gets an EventPointer form a common "q" tag
|
|
28
|
-
* @throws
|
|
30
|
+
* @throws if the tag is invalid
|
|
29
31
|
*/
|
|
30
32
|
export declare function getEventPointerFromQTag(tag: string[]): EventPointer;
|
|
31
33
|
/**
|
|
32
34
|
* Get an AddressPointer from a common "a" tag
|
|
33
|
-
* @throws
|
|
35
|
+
* @throws if the tag is invalid
|
|
34
36
|
*/
|
|
35
37
|
export declare function getAddressPointerFromATag(tag: string[]): AddressPointer;
|
|
36
38
|
/**
|
|
37
39
|
* Gets a ProfilePointer from a common "p" tag
|
|
38
|
-
* @throws
|
|
40
|
+
* @throws if the tag is invalid
|
|
39
41
|
*/
|
|
40
42
|
export declare function getProfilePointerFromPTag(tag: string[]): ProfilePointer;
|
|
41
43
|
/** Checks if a pointer is an AddressPointer */
|
|
@@ -46,15 +48,12 @@ export declare function isEventPointer(pointer: DecodeResult["data"]): pointer i
|
|
|
46
48
|
export declare function getCoordinateFromAddressPointer(pointer: AddressPointer): string;
|
|
47
49
|
/**
|
|
48
50
|
* Returns an AddressPointer for a replaceable event
|
|
49
|
-
* @throws
|
|
51
|
+
* @throws if the event is not replaceable or addressable
|
|
50
52
|
*/
|
|
51
53
|
export declare function getAddressPointerForEvent(event: NostrEvent, relays?: string[]): AddressPointer;
|
|
52
54
|
/** Returns an EventPointer for an event */
|
|
53
55
|
export declare function getEventPointerForEvent(event: NostrEvent, relays?: string[]): EventPointer;
|
|
54
|
-
/**
|
|
55
|
-
* Returns a pointer for a given event
|
|
56
|
-
* @throws
|
|
57
|
-
*/
|
|
56
|
+
/** Returns a pointer for a given event */
|
|
58
57
|
export declare function getPointerForEvent(event: NostrEvent, relays?: string[]): DecodeResult;
|
|
59
58
|
/** Adds relay hints to a pointer object that has a relays array */
|
|
60
59
|
export declare function addRelayHintsToPointer<T extends {
|
|
@@ -62,6 +61,8 @@ export declare function addRelayHintsToPointer<T extends {
|
|
|
62
61
|
}>(pointer: T, relays?: Iterable<string>): T;
|
|
63
62
|
/** Gets the hex pubkey from any nip-19 encoded string */
|
|
64
63
|
export declare function normalizeToPubkey(str: string): string;
|
|
64
|
+
/** Gets a ProfilePointer from any nip-19 encoded string */
|
|
65
|
+
export declare function normalizeToProfilePointer(str: string): ProfilePointer;
|
|
65
66
|
/** Converts hex to nsec strings into Uint8 secret keys */
|
|
66
67
|
export declare function normalizeToSecretKey(str: string | Uint8Array): Uint8Array;
|
|
67
68
|
/**
|
package/dist/helpers/pointers.js
CHANGED
|
@@ -3,8 +3,8 @@ import { getPublicKey, kinds, nip19 } from "nostr-tools";
|
|
|
3
3
|
// export nip-19 helpers
|
|
4
4
|
export { naddrEncode, neventEncode, noteEncode, nprofileEncode, npubEncode, nsecEncode, decode as decodePointer, } from "nostr-tools/nip19";
|
|
5
5
|
import { getReplaceableIdentifier } from "./event.js";
|
|
6
|
-
import { isAddressableKind } from "nostr-tools/kinds";
|
|
7
|
-
import { isSafeRelayURL,
|
|
6
|
+
import { isAddressableKind, isReplaceableKind } from "nostr-tools/kinds";
|
|
7
|
+
import { isSafeRelayURL, relaySet } from "./relays.js";
|
|
8
8
|
import { isHexKey } from "./string.js";
|
|
9
9
|
import { hexToBytes } from "@noble/hashes/utils";
|
|
10
10
|
import { normalizeURL } from "./url.js";
|
|
@@ -53,6 +53,20 @@ export function getPubkeyFromDecodeResult(result) {
|
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/** Gets the relays from a decode result */
|
|
57
|
+
export function getRelaysFromDecodeResult(result) {
|
|
58
|
+
if (!result)
|
|
59
|
+
return;
|
|
60
|
+
switch (result.type) {
|
|
61
|
+
case "naddr":
|
|
62
|
+
return result.data.relays;
|
|
63
|
+
case "nprofile":
|
|
64
|
+
return result.data.relays;
|
|
65
|
+
case "nevent":
|
|
66
|
+
return result.data.relays;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
56
70
|
/** Encodes the result of nip19.decode */
|
|
57
71
|
export function encodeDecodeResult(result) {
|
|
58
72
|
switch (result.type) {
|
|
@@ -85,7 +99,7 @@ export function getEventPointerFromETag(tag) {
|
|
|
85
99
|
}
|
|
86
100
|
/**
|
|
87
101
|
* Gets an EventPointer form a common "q" tag
|
|
88
|
-
* @throws
|
|
102
|
+
* @throws if the tag is invalid
|
|
89
103
|
*/
|
|
90
104
|
export function getEventPointerFromQTag(tag) {
|
|
91
105
|
if (!tag[1])
|
|
@@ -99,7 +113,7 @@ export function getEventPointerFromQTag(tag) {
|
|
|
99
113
|
}
|
|
100
114
|
/**
|
|
101
115
|
* Get an AddressPointer from a common "a" tag
|
|
102
|
-
* @throws
|
|
116
|
+
* @throws if the tag is invalid
|
|
103
117
|
*/
|
|
104
118
|
export function getAddressPointerFromATag(tag) {
|
|
105
119
|
if (!tag[1])
|
|
@@ -111,7 +125,7 @@ export function getAddressPointerFromATag(tag) {
|
|
|
111
125
|
}
|
|
112
126
|
/**
|
|
113
127
|
* Gets a ProfilePointer from a common "p" tag
|
|
114
|
-
* @throws
|
|
128
|
+
* @throws if the tag is invalid
|
|
115
129
|
*/
|
|
116
130
|
export function getProfilePointerFromPTag(tag) {
|
|
117
131
|
if (!tag[1])
|
|
@@ -140,10 +154,10 @@ export function getCoordinateFromAddressPointer(pointer) {
|
|
|
140
154
|
}
|
|
141
155
|
/**
|
|
142
156
|
* Returns an AddressPointer for a replaceable event
|
|
143
|
-
* @throws
|
|
157
|
+
* @throws if the event is not replaceable or addressable
|
|
144
158
|
*/
|
|
145
159
|
export function getAddressPointerForEvent(event, relays) {
|
|
146
|
-
if (!isAddressableKind(event.kind))
|
|
160
|
+
if (!isAddressableKind(event.kind) && !isReplaceableKind(event.kind))
|
|
147
161
|
throw new Error("Cant get AddressPointer for non-replaceable event");
|
|
148
162
|
const d = getReplaceableIdentifier(event);
|
|
149
163
|
return {
|
|
@@ -162,12 +176,9 @@ export function getEventPointerForEvent(event, relays) {
|
|
|
162
176
|
relays,
|
|
163
177
|
};
|
|
164
178
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Returns a pointer for a given event
|
|
167
|
-
* @throws
|
|
168
|
-
*/
|
|
179
|
+
/** Returns a pointer for a given event */
|
|
169
180
|
export function getPointerForEvent(event, relays) {
|
|
170
|
-
if (kinds.isAddressableKind(event.kind)) {
|
|
181
|
+
if (kinds.isAddressableKind(event.kind) || kinds.isReplaceableKind(event.kind)) {
|
|
171
182
|
return {
|
|
172
183
|
type: "naddr",
|
|
173
184
|
data: getAddressPointerForEvent(event, relays),
|
|
@@ -185,7 +196,7 @@ export function addRelayHintsToPointer(pointer, relays) {
|
|
|
185
196
|
if (!relays)
|
|
186
197
|
return pointer;
|
|
187
198
|
else
|
|
188
|
-
return { ...pointer, relays:
|
|
199
|
+
return { ...pointer, relays: relaySet(relays, pointer.relays) };
|
|
189
200
|
}
|
|
190
201
|
/** Gets the hex pubkey from any nip-19 encoded string */
|
|
191
202
|
export function normalizeToPubkey(str) {
|
|
@@ -199,6 +210,23 @@ export function normalizeToPubkey(str) {
|
|
|
199
210
|
return pubkey;
|
|
200
211
|
}
|
|
201
212
|
}
|
|
213
|
+
/** Gets a ProfilePointer from any nip-19 encoded string */
|
|
214
|
+
export function normalizeToProfilePointer(str) {
|
|
215
|
+
if (isHexKey(str))
|
|
216
|
+
return { pubkey: str.toLowerCase() };
|
|
217
|
+
else {
|
|
218
|
+
const decode = nip19.decode(str);
|
|
219
|
+
// Return it if it's a profile pointer
|
|
220
|
+
if (decode.type === "nprofile")
|
|
221
|
+
return decode.data;
|
|
222
|
+
// fallback to just getting the pubkey
|
|
223
|
+
const pubkey = getPubkeyFromDecodeResult(decode);
|
|
224
|
+
if (!pubkey)
|
|
225
|
+
throw new Error(`Cant find pubkey in ${decode.type}`);
|
|
226
|
+
const relays = getRelaysFromDecodeResult(decode);
|
|
227
|
+
return { pubkey, relays };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
202
230
|
/** Converts hex to nsec strings into Uint8 secret keys */
|
|
203
231
|
export function normalizeToSecretKey(str) {
|
|
204
232
|
if (str instanceof Uint8Array)
|
|
@@ -219,7 +247,7 @@ export function normalizeToSecretKey(str) {
|
|
|
219
247
|
export function mergeEventPointers(a, b) {
|
|
220
248
|
if (a.id !== b.id)
|
|
221
249
|
throw new Error("Cant merge event pointers with different ids");
|
|
222
|
-
const relays =
|
|
250
|
+
const relays = relaySet(a.relays, b.relays);
|
|
223
251
|
return { id: a.id, kind: a.kind ?? b.kind, author: a.author ?? b.author, relays };
|
|
224
252
|
}
|
|
225
253
|
/**
|
|
@@ -229,7 +257,7 @@ export function mergeEventPointers(a, b) {
|
|
|
229
257
|
export function mergeAddressPointers(a, b) {
|
|
230
258
|
if (a.kind !== b.kind || a.pubkey !== b.pubkey || a.identifier !== b.identifier)
|
|
231
259
|
throw new Error("Cant merge address pointers with different kinds, pubkeys, or identifiers");
|
|
232
|
-
const relays =
|
|
260
|
+
const relays = relaySet(a.relays, b.relays);
|
|
233
261
|
return { ...a, relays };
|
|
234
262
|
}
|
|
235
263
|
/**
|
|
@@ -239,6 +267,6 @@ export function mergeAddressPointers(a, b) {
|
|
|
239
267
|
export function mergeProfilePointers(a, b) {
|
|
240
268
|
if (a.pubkey !== b.pubkey)
|
|
241
269
|
throw new Error("Cant merge profile pointers with different pubkeys");
|
|
242
|
-
const relays =
|
|
270
|
+
const relays = relaySet(a.relays, b.relays);
|
|
243
271
|
return { ...a, relays };
|
|
244
272
|
}
|
package/dist/helpers/share.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { NostrEvent } from "nostr-tools";
|
|
1
|
+
import { kinds, NostrEvent } from "nostr-tools";
|
|
2
2
|
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
|
3
|
+
import { KnownEvent } from "./index.js";
|
|
4
|
+
/** Type of a known share event */
|
|
5
|
+
export type ShareEvent = KnownEvent<kinds.Repost | kinds.GenericRepost>;
|
|
3
6
|
export declare const SharedEventSymbol: unique symbol;
|
|
4
7
|
export declare const SharedEventPointerSymbol: unique symbol;
|
|
5
8
|
export declare const SharedAddressPointerSymbol: unique symbol;
|
|
6
9
|
/** Returns the event pointer of a kind 6 or 16 share event */
|
|
10
|
+
export declare function getSharedEventPointer(event: ShareEvent): EventPointer;
|
|
7
11
|
export declare function getSharedEventPointer(event: NostrEvent): EventPointer | undefined;
|
|
8
12
|
/** Returns the address pointer of a kind 6 or 16 share event */
|
|
9
13
|
export declare function getSharedAddressPointer(event: NostrEvent): AddressPointer | undefined;
|
|
@@ -11,3 +15,5 @@ export declare function getSharedAddressPointer(event: NostrEvent): AddressPoint
|
|
|
11
15
|
export declare function getEmbededSharedEvent(event: NostrEvent): NostrEvent | undefined;
|
|
12
16
|
/** @deprecated use getEmbededSharedEvent instead */
|
|
13
17
|
export declare const parseSharedEvent: typeof getEmbededSharedEvent;
|
|
18
|
+
/** Validates that an event is a valid share event */
|
|
19
|
+
export declare function isValidShare(event?: NostrEvent): event is ShareEvent;
|
package/dist/helpers/share.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { nip18 } from "nostr-tools";
|
|
1
|
+
import { kinds, nip18 } from "nostr-tools";
|
|
2
|
+
import { isKind } from "nostr-tools/kinds";
|
|
2
3
|
import { getOrComputeCachedValue } from "./cache.js";
|
|
3
|
-
import {
|
|
4
|
-
import { getAddressPointerFromATag } from "./pointers.js";
|
|
4
|
+
import { getTagValue } from "./index.js";
|
|
5
|
+
import { getAddressPointerFromATag, getEventPointerFromETag } from "./pointers.js";
|
|
6
|
+
import { isATag, isETag } from "./tags.js";
|
|
5
7
|
export const SharedEventSymbol = Symbol.for("shared-event");
|
|
6
8
|
export const SharedEventPointerSymbol = Symbol.for("shared-event-pointer");
|
|
7
9
|
export const SharedAddressPointerSymbol = Symbol.for("shared-address-pointer");
|
|
8
|
-
/** Returns the event pointer of a kind 6 or 16 share event */
|
|
9
10
|
export function getSharedEventPointer(event) {
|
|
10
|
-
return getOrComputeCachedValue(event, SharedEventPointerSymbol, () =>
|
|
11
|
+
return getOrComputeCachedValue(event, SharedEventPointerSymbol, () => {
|
|
12
|
+
const e = event.tags.find(isETag);
|
|
13
|
+
if (!e)
|
|
14
|
+
return undefined;
|
|
15
|
+
// Get kind from k tag if it exists
|
|
16
|
+
const kStr = getTagValue(event, "k");
|
|
17
|
+
const k = kStr ? parseInt(kStr) : undefined;
|
|
18
|
+
const pointer = getEventPointerFromETag(e);
|
|
19
|
+
if (k !== undefined)
|
|
20
|
+
pointer.kind = k;
|
|
21
|
+
return pointer;
|
|
22
|
+
});
|
|
11
23
|
}
|
|
12
24
|
/** Returns the address pointer of a kind 6 or 16 share event */
|
|
13
25
|
export function getSharedAddressPointer(event) {
|
|
@@ -24,3 +36,9 @@ export function getEmbededSharedEvent(event) {
|
|
|
24
36
|
}
|
|
25
37
|
/** @deprecated use getEmbededSharedEvent instead */
|
|
26
38
|
export const parseSharedEvent = getEmbededSharedEvent;
|
|
39
|
+
/** Validates that an event is a valid share event */
|
|
40
|
+
export function isValidShare(event) {
|
|
41
|
+
if (!event)
|
|
42
|
+
return false;
|
|
43
|
+
return isKind(event, [kinds.Repost, kinds.GenericRepost]) && getSharedEventPointer(event) !== undefined;
|
|
44
|
+
}
|
package/dist/helpers/zap.js
CHANGED
|
@@ -23,7 +23,13 @@ export function getZapRecipient(zap) {
|
|
|
23
23
|
export function getZapPayment(zap) {
|
|
24
24
|
return getOrComputeCachedValue(zap, ZapInvoiceSymbol, () => {
|
|
25
25
|
const bolt11 = getTagValue(zap, "bolt11");
|
|
26
|
-
|
|
26
|
+
try {
|
|
27
|
+
// Catch errors with parsing the bolt11 invoice
|
|
28
|
+
return bolt11 ? parseBolt11(bolt11) : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
35
|
export function getZapAmount(zap) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NostrEvent } from "nostr-tools";
|
|
2
2
|
import { MonoTypeOperatorFunction } from "rxjs";
|
|
3
3
|
import { IEventClaims } from "../event-store/interface.js";
|
|
4
|
-
/** keep a claim on any event that goes through this observable, claims are removed when the observable completes */
|
|
4
|
+
/** keep a claim on any event that goes through this observable, claims are removed when the observable is unsubscribed or completes */
|
|
5
5
|
export declare function claimEvents<T extends NostrEvent[] | NostrEvent | undefined>(claims: IEventClaims): MonoTypeOperatorFunction<T>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { finalize, tap } from "rxjs";
|
|
2
|
-
/** keep a claim on any event that goes through this observable, claims are removed when the observable completes */
|
|
2
|
+
/** keep a claim on any event that goes through this observable, claims are removed when the observable is unsubscribed or completes */
|
|
3
3
|
export function claimEvents(claims) {
|
|
4
4
|
return (source) => {
|
|
5
5
|
const seen = new Set();
|
|
@@ -10,19 +10,21 @@ export function claimEvents(claims) {
|
|
|
10
10
|
return;
|
|
11
11
|
if (Array.isArray(message)) {
|
|
12
12
|
for (const event of message) {
|
|
13
|
+
if (seen.has(event))
|
|
14
|
+
continue;
|
|
13
15
|
seen.add(event);
|
|
14
|
-
claims.claim(event
|
|
16
|
+
claims.claim(event);
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
|
-
else {
|
|
19
|
+
else if (!seen.has(message)) {
|
|
18
20
|
seen.add(message);
|
|
19
|
-
claims.claim(message
|
|
21
|
+
claims.claim(message);
|
|
20
22
|
}
|
|
21
23
|
}),
|
|
22
24
|
// remove claims on cleanup
|
|
23
25
|
finalize(() => {
|
|
24
26
|
for (const e of seen)
|
|
25
|
-
claims.removeClaim(e
|
|
27
|
+
claims.removeClaim(e);
|
|
26
28
|
}));
|
|
27
29
|
};
|
|
28
30
|
}
|
|
@@ -4,18 +4,21 @@ export function claimLatest(claims) {
|
|
|
4
4
|
return (source) => {
|
|
5
5
|
let latest = undefined;
|
|
6
6
|
return source.pipe(tap((event) => {
|
|
7
|
-
//
|
|
8
|
-
if (latest)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
// only update if the event changed
|
|
8
|
+
if (latest !== event) {
|
|
9
|
+
// remove old claim
|
|
10
|
+
if (latest)
|
|
11
|
+
claims.removeClaim(latest);
|
|
12
|
+
// claim new event
|
|
13
|
+
if (event)
|
|
14
|
+
claims.claim(event);
|
|
15
|
+
// update state
|
|
16
|
+
latest = event;
|
|
17
|
+
}
|
|
15
18
|
}), finalize(() => {
|
|
16
19
|
// remove latest claim
|
|
17
20
|
if (latest)
|
|
18
|
-
claims.removeClaim(latest
|
|
21
|
+
claims.removeClaim(latest);
|
|
19
22
|
}));
|
|
20
23
|
};
|
|
21
24
|
}
|