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.
@@ -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?: EventMemory;
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 | undefined;
119
- /** Sets the claim on the event and touches it */
120
- claim(event: NostrEvent, claim: any): void;
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
- /** Removes a claim from an event */
124
- removeClaim(event: NostrEvent, claim: any): void;
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
- /** Sets the claim on the event and touches it */
324
- claim(event, claim) {
325
- return this.memory?.claim(event, claim);
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
- /** Removes a claim from an event */
332
- removeClaim(event, claim) {
333
- return this.memory?.removeClaim(event, claim);
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 { Filter, NostrEvent } from "nostr-tools";
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 event set */
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: Filter | Filter[]): NostrEvent[];
32
+ getByFilters(filters: FilterWithAnd | FilterWithAnd[]): NostrEvent[];
30
33
  /** Gets a timeline of events that match the filters */
31
- getTimeline(filters: Filter | Filter[]): NostrEvent[];
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: Filter | Filter[]): number;
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 that are claimed by other things */
41
- protected claims: WeakMap<import("nostr-tools").Event, any>;
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
- /** Sets the claim on the event and touches it */
45
- claim(event: NostrEvent, claim: any): void;
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
- /** Removes a claim from an event */
49
- removeClaim(event: NostrEvent, claim: any): void;
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: Filter): Set<NostrEvent>;
80
+ protected getEventsForFilter(filter: FilterWithAnd): Set<NostrEvent>;
72
81
  /** Returns all events that match the filters */
73
- protected getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
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 event set */
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
- const i = this.created_at.indexOf(event);
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 && array.includes(event)) {
115
- const idx = array.indexOf(event);
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 that are claimed by other things */
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
- /** Sets the claim on the event and touches it */
149
- claim(event, claim) {
150
- if (!this.claims.has(event)) {
151
- this.claims.set(event, claim);
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
- return this.claims.has(event);
162
+ const count = this.claims.get(event);
163
+ return count !== undefined && count > 0;
159
164
  }
160
- /** Removes a claim from an event */
161
- removeClaim(event, claim) {
162
- const current = this.claims.get(event);
163
- if (current === claim)
164
- this.claims.delete(event);
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 untilIndex = 0;
255
- let sinceIndex = this.created_at.length - 1;
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
- untilIndex = start[0];
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
- sinceIndex = end[0];
270
- for (let i = untilIndex; i < sinceIndex; i++) {
271
- yield this.created_at[i];
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
- and(this.iterateTag(t, values));
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?: EventMemory;
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 | undefined;
119
- /** Sets the claim on the event and touches it */
120
- claim(event: NostrEvent, claim: any): void;
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
- /** Removes a claim from an event */
124
- removeClaim(event: NostrEvent, claim: any): void;
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
- /** Sets the claim on the event and touches it */
326
- claim(event, claim) {
327
- return this.memory?.claim(event, claim);
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
- /** Removes a claim from an event */
334
- removeClaim(event, claim) {
335
- return this.memory?.removeClaim(event, claim);
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 { Filter, NostrEvent } from "nostr-tools";
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: Filter | Filter[]): NostrEvent[];
22
+ getByFilters(filters: FilterWithAnd | FilterWithAnd[]): NostrEvent[];
22
23
  /** Get a timeline of events that match the filters */
23
- getTimeline(filters: Filter | Filter[]): NostrEvent[];
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: Filter | Filter[]): Promise<NostrEvent[]>;
39
+ getByFilters(filters: FilterWithAnd | FilterWithAnd[]): Promise<NostrEvent[]>;
39
40
  /** Get a timeline of events that match the filters */
40
- getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
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
- /** Sets the claim on the event and touches it */
74
- claim(event: NostrEvent, claim: any): void;
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
- /** Removes a claim from an event */
78
- removeClaim(event: NostrEvent, claim: any): void;
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: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
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: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent[]>;
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: Filter | Filter[]): number;
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: Filter | Filter[]): Promise<number>;
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
  }
@@ -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 (isReplaceable(event.kind))
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 (!isReplaceable(event.kind))
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
- const identifier = isAddressableKind(event.kind) ? getReplaceableIdentifier(event) : undefined;
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 */
@@ -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: Filter, event: NostrEvent): boolean;
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: Filter[], event: NostrEvent): boolean;
10
- /** Copied from nostr-tools and modified to support undefined values */
11
- export declare function mergeFilters(...filters: Filter[]): Filter;
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: Filter | Filter[], b: Filter | Filter[]): boolean;
27
+ export declare function isFilterEqual(a: FilterWithAnd | FilterWithAnd[], b: FilterWithAnd | FilterWithAnd[]): boolean;
@@ -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 (values.some((v) => tags.has(tagName + ":" + v)) === false)
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" || property === "ids" || property === "authors" || property[0] === "#") {
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
  /**
@@ -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, mergeRelaySets } from "./relays.js";
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: mergeRelaySets(relays, 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 = mergeRelaySets(a.relays, b.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 = mergeRelaySets(a.relays, b.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 = mergeRelaySets(a.relays, b.relays);
270
+ const relays = relaySet(a.relays, b.relays);
243
271
  return { ...a, relays };
244
272
  }
@@ -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;
@@ -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 { isATag } from "./tags.js";
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, () => nip18.getRepostedEventPointer(event));
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
+ }
@@ -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
- return bolt11 ? parseBolt11(bolt11) : undefined;
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, source);
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, source);
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, source);
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
- // remove old claim
8
- if (latest)
9
- claims.removeClaim(latest, source);
10
- // claim new event
11
- if (event)
12
- claims.claim(event, source);
13
- // update state
14
- latest = event;
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, source);
21
+ claims.removeClaim(latest);
19
22
  }));
20
23
  };
21
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "4.2.0",
3
+ "version": "4.4.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",