applesauce-core 0.0.0-next-20251209200210 → 0.0.0-next-20251220152312

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.
@@ -1,4 +1,5 @@
1
1
  import { EncryptedContentSymbol } from "../helpers/encrypted-content.js";
2
+ import { isEvent } from "../helpers/event.js";
2
3
  import { eventPipe } from "../helpers/pipeline.js";
3
4
  import { unixNow } from "../helpers/time.js";
4
5
  import { setClient } from "../operations/client.js";
@@ -36,5 +37,8 @@ export async function createEvent(context, blueprint, ...args) {
36
37
  }
37
38
  /** Modifies an event using a context and a set of operations */
38
39
  export async function modifyEvent(event, context, ...operations) {
40
+ // NOTE: Unwrapping evnet object in order to handle cast events from applesauce-common
41
+ if ("event" in event && isEvent(event.event))
42
+ event = event.event;
39
43
  return await wrapCommon(stripSignature(), stripStamp(), updateCreatedAt(), ...operations)(event, context);
40
44
  }
@@ -4,7 +4,7 @@ import { Filter } from "../helpers/filter.js";
4
4
  import { AddressPointer, AddressPointerWithoutD, EventPointer } from "../helpers/pointers.js";
5
5
  import { EventMemory } from "./event-memory.js";
6
6
  import { EventModels } from "./event-models.js";
7
- import { IAsyncEventDatabase, IAsyncEventStore, IAsyncDeleteManager, IExpirationManager } from "./interface.js";
7
+ import { IAsyncDeleteManager, IAsyncEventDatabase, IAsyncEventStore, IExpirationManager } from "./interface.js";
8
8
  export type AsyncEventStoreOptions = {
9
9
  /** Keep deleted events in the store */
10
10
  keepDeleted?: boolean;
@@ -44,7 +44,7 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
44
44
  set verifyEvent(method: undefined | ((event: NostrEvent) => boolean));
45
45
  /** A stream of new events added to the store */
46
46
  insert$: Subject<import("nostr-tools/core").Event>;
47
- /** A stream of events that have been updated */
47
+ /** A stream of events that have been updated (Warning: this is a very noisy stream, use with caution) */
48
48
  update$: Subject<import("nostr-tools/core").Event>;
49
49
  /** A stream of events that have been removed */
50
50
  remove$: Subject<import("nostr-tools/core").Event>;
@@ -59,8 +59,6 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
59
59
  private handleDeleteNotification;
60
60
  /** Handle an expired event by id */
61
61
  private handleExpiredNotification;
62
- /** Copies important metadata from and identical event to another */
63
- static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
64
62
  /**
65
63
  * Adds an event to the store and update subscriptions
66
64
  * @returns The existing event or the event that was added, if it was ignored returns null
@@ -73,9 +71,9 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
73
71
  /** Add an event to the store and notifies all subscribes it has updated */
74
72
  update(event: NostrEvent): Promise<void>;
75
73
  /** Check if the store has an event by id */
76
- hasEvent(id: string): Promise<boolean>;
74
+ hasEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): Promise<boolean>;
77
75
  /** Get an event by id from the store */
78
- getEvent(id: string): Promise<NostrEvent | undefined>;
76
+ getEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): Promise<NostrEvent | undefined>;
79
77
  /** Check if the store has a replaceable event */
80
78
  hasReplaceable(kind: number, pubkey: string, d?: string): Promise<boolean>;
81
79
  /** Gets the latest version of a replaceable event */
@@ -1,13 +1,14 @@
1
+ import { verifyEvent as coreVerifyEvent } from "nostr-tools/pure";
1
2
  import { Subject } from "rxjs";
2
- import { EventStoreSymbol, FromCacheSymbol, getReplaceableIdentifier, isReplaceable, kinds, } from "../helpers/event.js";
3
+ import { EventStoreSymbol, getReplaceableIdentifier, isReplaceable, kinds } from "../helpers/event.js";
3
4
  import { getExpirationTimestamp } from "../helpers/expiration.js";
4
- import { eventMatchesPointer, isEventPointer, isAddressPointer, } from "../helpers/pointers.js";
5
- import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
5
+ import { eventMatchesPointer, isAddressPointer, isEventPointer, } from "../helpers/pointers.js";
6
+ import { addSeenRelay } from "../helpers/relays.js";
6
7
  import { unixNow } from "../helpers/time.js";
8
+ import { AsyncDeleteManager } from "./async-delete-manager.js";
7
9
  import { EventMemory } from "./event-memory.js";
8
10
  import { EventModels } from "./event-models.js";
9
- import { verifyEvent as coreVerifyEvent } from "nostr-tools/pure";
10
- import { AsyncDeleteManager } from "./async-delete-manager.js";
11
+ import { EventStore } from "./event-store.js";
11
12
  import { ExpirationManager } from "./expiration-manager.js";
12
13
  /** An async wrapper around an async event database that handles replaceable events, deletes, and models */
13
14
  export class AsyncEventStore extends EventModels {
@@ -38,7 +39,7 @@ export class AsyncEventStore extends EventModels {
38
39
  }
39
40
  /** A stream of new events added to the store */
40
41
  insert$ = new Subject();
41
- /** A stream of events that have been updated */
42
+ /** A stream of events that have been updated (Warning: this is a very noisy stream, use with caution) */
42
43
  update$ = new Subject();
43
44
  /** A stream of events that have been removed */
44
45
  remove$ = new Subject();
@@ -75,14 +76,6 @@ export class AsyncEventStore extends EventModels {
75
76
  console.error("[applesauce-core] Error handling expired notification:", error);
76
77
  });
77
78
  });
78
- // when events are added to the database, add the symbol
79
- this.insert$.subscribe((event) => {
80
- Reflect.set(event, EventStoreSymbol, this);
81
- });
82
- // when events are removed from the database, remove the symbol
83
- this.remove$.subscribe((event) => {
84
- Reflect.deleteProperty(event, EventStoreSymbol);
85
- });
86
79
  }
87
80
  mapToMemory(event) {
88
81
  if (event === undefined)
@@ -122,18 +115,6 @@ export class AsyncEventStore extends EventModels {
122
115
  return;
123
116
  await this.remove(id);
124
117
  }
125
- /** Copies important metadata from and identical event to another */
126
- static mergeDuplicateEvent(source, dest) {
127
- const relays = getSeenRelays(source);
128
- if (relays) {
129
- for (const relay of relays)
130
- addSeenRelay(dest, relay);
131
- }
132
- // copy the from cache symbol only if its true
133
- const fromCache = Reflect.get(source, FromCacheSymbol);
134
- if (fromCache && !Reflect.get(dest, FromCacheSymbol))
135
- Reflect.set(dest, FromCacheSymbol, fromCache);
136
- }
137
118
  /**
138
119
  * Adds an event to the store and update subscriptions
139
120
  * @returns The existing event or the event that was added, if it was ignored returns null
@@ -151,6 +132,9 @@ export class AsyncEventStore extends EventModels {
151
132
  const expiration = getExpirationTimestamp(event);
152
133
  if (this.keepExpired === false && expiration && expiration <= unixNow())
153
134
  return null;
135
+ // Attach relay this event was from
136
+ if (fromRelay)
137
+ addSeenRelay(event, fromRelay);
154
138
  // Get the replaceable identifier
155
139
  const identifier = isReplaceable(event.kind) ? getReplaceableIdentifier(event) : undefined;
156
140
  // Don't insert the event if there is already a newer version
@@ -158,7 +142,8 @@ export class AsyncEventStore extends EventModels {
158
142
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
159
143
  // If there is already a newer version, copy cached symbols and return existing event
160
144
  if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
161
- AsyncEventStore.mergeDuplicateEvent(event, existing[0]);
145
+ if (EventStore.copySymbolsToDuplicateEvent(event, existing[0]))
146
+ await this.update(existing[0]);
162
147
  return existing[0];
163
148
  }
164
149
  }
@@ -166,27 +151,28 @@ export class AsyncEventStore extends EventModels {
166
151
  if (this.verifyEvent && this.verifyEvent(event) === false)
167
152
  return null;
168
153
  // Always add event to memory
169
- const existing = this.memory?.add(event);
154
+ const existing = this.memory.add(event);
170
155
  // If the memory returned a different instance, this is a duplicate event
171
156
  if (existing && existing !== event) {
172
157
  // Copy cached symbols and return existing event
173
- AsyncEventStore.mergeDuplicateEvent(event, existing);
174
- // attach relay this event was from
175
- if (fromRelay)
176
- addSeenRelay(existing, fromRelay);
158
+ if (EventStore.copySymbolsToDuplicateEvent(event, existing))
159
+ await this.update(existing);
177
160
  return existing;
178
161
  }
179
162
  // Insert event into database
180
163
  const inserted = this.mapToMemory(await this.database.add(event));
181
- // Copy cached data if its a duplicate event
182
- if (event !== inserted)
183
- AsyncEventStore.mergeDuplicateEvent(event, inserted);
184
- // attach relay this event was from
185
- if (fromRelay)
186
- addSeenRelay(inserted, fromRelay);
187
- // Emit insert$ signal
188
- if (inserted === event)
164
+ // If the event is the same as the inserted event, its a new event
165
+ if (inserted === event) {
166
+ // Set the event store on the event
167
+ Reflect.set(inserted, EventStoreSymbol, this);
168
+ // Emit insert$ signal
189
169
  this.insert$.next(inserted);
170
+ }
171
+ else {
172
+ // Copy cached data if its a duplicate event
173
+ if (EventStore.copySymbolsToDuplicateEvent(event, inserted))
174
+ await this.update(inserted);
175
+ }
190
176
  // remove all old version of the replaceable event
191
177
  if (this.keepOldVersions === false && isReplaceable(event.kind)) {
192
178
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
@@ -208,18 +194,20 @@ export class AsyncEventStore extends EventModels {
208
194
  /** Removes an event from the store and updates subscriptions */
209
195
  async remove(event) {
210
196
  const eventId = typeof event === "string" ? event : event.id;
211
- let instance = this.memory?.getEvent(eventId);
197
+ let instance = this.memory.getEvent(eventId);
212
198
  // Remove from expiration manager
213
199
  this.expiration.forget(eventId);
200
+ // Remove the event store from the event
201
+ if (instance)
202
+ Reflect.deleteProperty(instance, EventStoreSymbol);
214
203
  // Remove from memory if available
215
204
  if (this.memory)
216
205
  this.memory.remove(event);
217
206
  // Remove the event from the database
218
207
  const removed = await this.database.remove(event);
219
208
  // If the event was removed, notify the subscriptions
220
- if (removed && instance) {
209
+ if (removed && instance)
221
210
  this.remove$.next(instance);
222
- }
223
211
  return removed;
224
212
  }
225
213
  /** Remove multiple events that match the given filters */
@@ -252,29 +240,40 @@ export class AsyncEventStore extends EventModels {
252
240
  }
253
241
  /** Check if the store has an event by id */
254
242
  async hasEvent(id) {
255
- // Check if the event exists in memory first, then in the database
256
- return (this.memory?.hasEvent(id) ?? false) || (await this.database.hasEvent(id));
243
+ if (typeof id === "string")
244
+ return this.memory.hasEvent(id) || this.database.hasEvent(id);
245
+ // If its a pointer, use the advanced has event method to resolve
246
+ else if (isEventPointer(id))
247
+ return this.memory.hasEvent(id.id) || this.database.hasEvent(id.id);
248
+ else
249
+ return this.hasReplaceable(id.kind, id.pubkey, id.identifier);
257
250
  }
258
251
  /** Get an event by id from the store */
259
252
  async getEvent(id) {
260
253
  // Get the event from memory first, then from the database
261
- return this.memory?.getEvent(id) ?? this.mapToMemory(await this.database.getEvent(id));
254
+ if (typeof id === "string")
255
+ return this.memory.getEvent(id) ?? this.mapToMemory(await this.database.getEvent(id));
256
+ // If its a pointer, use the advanced get event method to resolve
257
+ else if (isEventPointer(id))
258
+ return this.memory.getEvent(id.id) ?? this.mapToMemory(await this.database.getEvent(id.id));
259
+ else
260
+ return this.getReplaceable(id.kind, id.pubkey, id.identifier);
262
261
  }
263
262
  /** Check if the store has a replaceable event */
264
263
  async hasReplaceable(kind, pubkey, d) {
265
264
  // Check if the event exists in memory first, then in the database
266
- return ((this.memory?.hasReplaceable(kind, pubkey, d) ?? false) || (await this.database.hasReplaceable(kind, pubkey, d)));
265
+ return this.memory.hasReplaceable(kind, pubkey, d) || this.database.hasReplaceable(kind, pubkey, d);
267
266
  }
268
267
  /** Gets the latest version of a replaceable event */
269
268
  async getReplaceable(kind, pubkey, identifier) {
270
269
  // Get the event from memory first, then from the database
271
- return (this.memory?.getReplaceable(kind, pubkey, identifier) ??
270
+ return (this.memory.getReplaceable(kind, pubkey, identifier) ??
272
271
  this.mapToMemory(await this.database.getReplaceable(kind, pubkey, identifier)));
273
272
  }
274
273
  /** Returns all versions of a replaceable event */
275
274
  async getReplaceableHistory(kind, pubkey, identifier) {
276
275
  // Get the events from memory first, then from the database
277
- return (this.memory?.getReplaceableHistory(kind, pubkey, identifier) ??
276
+ return (this.memory.getReplaceableHistory(kind, pubkey, identifier) ??
278
277
  (await this.database.getReplaceableHistory(kind, pubkey, identifier))?.map((e) => this.mapToMemory(e) ?? e));
279
278
  }
280
279
  /** Get all events matching a filter */
@@ -297,30 +296,30 @@ export class AsyncEventStore extends EventModels {
297
296
  }
298
297
  /** Passthrough method for the database.touch */
299
298
  touch(event) {
300
- return this.memory?.touch(event);
299
+ return this.memory.touch(event);
301
300
  }
302
301
  /** Increments the claim count on the event and touches it */
303
302
  claim(event) {
304
- return this.memory?.claim(event);
303
+ return this.memory.claim(event);
305
304
  }
306
305
  /** Checks if an event is claimed by anything */
307
306
  isClaimed(event) {
308
- return this.memory?.isClaimed(event) ?? false;
307
+ return this.memory.isClaimed(event) ?? false;
309
308
  }
310
309
  /** Decrements the claim count on an event */
311
310
  removeClaim(event) {
312
- return this.memory?.removeClaim(event);
311
+ return this.memory.removeClaim(event);
313
312
  }
314
313
  /** Removes all claims on an event */
315
314
  clearClaim(event) {
316
- return this.memory?.clearClaim(event);
315
+ return this.memory.clearClaim(event);
317
316
  }
318
317
  /** Pass through method for the database.unclaimed */
319
318
  unclaimed() {
320
- return this.memory?.unclaimed() || (function* () { })();
319
+ return this.memory.unclaimed() || (function* () { })();
321
320
  }
322
321
  /** Removes any event that is not being used by a subscription */
323
322
  prune(limit) {
324
- return this.memory?.prune(limit) ?? 0;
323
+ return this.memory.prune(limit) ?? 0;
325
324
  }
326
325
  }
@@ -2,24 +2,7 @@ import { Observable } from "rxjs";
2
2
  import { NostrEvent } from "../helpers/event.js";
3
3
  import { Filter } from "../helpers/filter.js";
4
4
  import { AddressPointer, AddressPointerWithoutD, EventPointer, ProfilePointer } from "../helpers/pointers.js";
5
- import { ProfileContent } from "../helpers/profile.js";
6
- import { IAsyncEventStore, IEventStore, ModelConstructor } from "./interface.js";
7
- /**
8
- * Core helpful subscriptions interface.
9
- * Contains only methods that use models from the core package.
10
- * Other packages (like applesauce-common) can extend this interface via module augmentation.
11
- */
12
- export interface IEventStoreModels {
13
- /** Subscribe to a users profile */
14
- profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
15
- /** Subscribe to a users contacts */
16
- contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
17
- /** Subscribe to a users mailboxes */
18
- mailboxes(user: string | ProfilePointer): Observable<{
19
- inboxes: string[];
20
- outboxes: string[];
21
- } | undefined>;
22
- }
5
+ import { IAsyncEventStore, IEventStore, IEventSubscriptions, ModelConstructor } from "./interface.js";
23
6
  /**
24
7
  * Base class that provides model functionality for both sync and async event stores.
25
8
  * This class can be extended by other packages to add additional helpful subscription methods.
@@ -42,7 +25,7 @@ export interface IEventStoreModels {
42
25
  * }
43
26
  * ```
44
27
  */
45
- export declare class EventModels<TStore extends IEventStore | IAsyncEventStore = IEventStore | IAsyncEventStore> implements IEventStoreModels {
28
+ export declare class EventModels<TStore extends IEventStore | IAsyncEventStore = IEventStore | IAsyncEventStore> implements IEventSubscriptions {
46
29
  /** A directory of all active models */
47
30
  models: Map<ModelConstructor<any, any[], TStore>, Map<string, Observable<any>>>;
48
31
  /** How long a model should be kept "warm" while nothing is subscribed to it */
@@ -55,8 +38,8 @@ export declare class EventModels<TStore extends IEventStore | IAsyncEventStore =
55
38
  * @param [onlyNew=false] Only subscribe to new events
56
39
  */
57
40
  filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
58
- /** Creates a {@link EventModel} */
59
- event(pointer: string | EventPointer): Observable<NostrEvent | undefined>;
41
+ /** Subscribe to an event by pointer */
42
+ event(pointer: string | EventPointer | AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
60
43
  /** Subscribe to a replaceable event by pointer */
61
44
  replaceable(pointer: AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
62
45
  replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
@@ -65,7 +48,7 @@ export declare class EventModels<TStore extends IEventStore | IAsyncEventStore =
65
48
  /** Creates a {@link TimelineModel} */
66
49
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
67
50
  /** Subscribe to a users profile */
68
- profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
51
+ profile(user: string | ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
69
52
  /** Subscribe to a users contacts */
70
53
  contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
71
54
  /** Subscribe to a users mailboxes */
@@ -1,5 +1,6 @@
1
1
  import hash_sum from "hash-sum";
2
- import { ReplaySubject, finalize, share, timer } from "rxjs";
2
+ import { finalize, ReplaySubject, share, timer } from "rxjs";
3
+ import { isEventPointer, } from "../helpers/pointers.js";
3
4
  import { EventModel, FiltersModel, ReplaceableModel, TimelineModel } from "../models/base.js";
4
5
  import { ContactsModel } from "../models/contacts.js";
5
6
  import { MailboxesModel } from "../models/mailboxes.js";
@@ -67,14 +68,16 @@ export class EventModels {
67
68
  * @param [onlyNew=false] Only subscribe to new events
68
69
  */
69
70
  filters(filters, onlyNew = false) {
71
+ if (!Array.isArray(filters))
72
+ filters = [filters];
70
73
  return this.model(FiltersModel, filters, onlyNew);
71
74
  }
72
- // Helper methods for creating models
73
- /** Creates a {@link EventModel} */
75
+ /** Subscribe to an event by pointer */
74
76
  event(pointer) {
75
- if (typeof pointer === "string")
76
- pointer = { id: pointer };
77
- return this.model(EventModel, pointer);
77
+ if (typeof pointer === "string" || isEventPointer(pointer))
78
+ return this.model(EventModel, pointer);
79
+ else
80
+ return this.replaceable(pointer);
78
81
  }
79
82
  replaceable(...args) {
80
83
  let pointer;
@@ -92,7 +95,7 @@ export class EventModels {
92
95
  }
93
96
  /** Subscribe to an addressable event by pointer */
94
97
  addressable(pointer) {
95
- return this.model(ReplaceableModel, pointer);
98
+ return this.replaceable(pointer);
96
99
  }
97
100
  /** Creates a {@link TimelineModel} */
98
101
  timeline(filters, includeOldVersion = false) {
@@ -44,7 +44,7 @@ export declare class EventStore extends EventModels implements IEventStore {
44
44
  set verifyEvent(method: undefined | ((event: NostrEvent) => boolean));
45
45
  /** A stream of new events added to the store */
46
46
  insert$: Subject<import("nostr-tools/core").Event>;
47
- /** A stream of events that have been updated */
47
+ /** A stream of events that have been updated (Warning: this is a very noisy stream, use with caution) */
48
48
  update$: Subject<import("nostr-tools/core").Event>;
49
49
  /** A stream of events that have been removed */
50
50
  remove$: Subject<import("nostr-tools/core").Event>;
@@ -58,7 +58,7 @@ export declare class EventStore extends EventModels implements IEventStore {
58
58
  /** Handle an expired event by id */
59
59
  private handleExpiredNotification;
60
60
  /** Copies important metadata from and identical event to another */
61
- static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
61
+ static copySymbolsToDuplicateEvent(source: NostrEvent, dest: NostrEvent): boolean;
62
62
  /**
63
63
  * Adds an event to the store and update subscriptions
64
64
  * @returns The existing event or the event that was added, if it was ignored returns null
@@ -71,9 +71,9 @@ export declare class EventStore extends EventModels implements IEventStore {
71
71
  /** Add an event to the store and notifies all subscribes it has updated */
72
72
  update(event: NostrEvent): boolean;
73
73
  /** Check if the store has an event by id */
74
- hasEvent(id: string): boolean;
74
+ hasEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): boolean;
75
75
  /** Get an event by id from the store */
76
- getEvent(id: string): NostrEvent | undefined;
76
+ getEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): NostrEvent | undefined;
77
77
  /** Check if the store has a replaceable event */
78
78
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
79
79
  /** Gets the latest version of a replaceable event */
@@ -1,6 +1,7 @@
1
- import { verifyEvent as coreVerifyEvent } from "nostr-tools/pure";
1
+ import { verifyEvent as coreVerifyEvent, verifiedSymbol } from "nostr-tools/pure";
2
2
  import { Subject } from "rxjs";
3
- import { EventStoreSymbol, FromCacheSymbol, getReplaceableIdentifier, isReplaceable, kinds, } from "../helpers/event.js";
3
+ import { EncryptedContentSymbol } from "../helpers/encrypted-content.js";
4
+ import { EventStoreSymbol, FromCacheSymbol, getReplaceableIdentifier, isRegularKind, isReplaceable, kinds, } from "../helpers/event.js";
4
5
  import { getExpirationTimestamp } from "../helpers/expiration.js";
5
6
  import { eventMatchesPointer, isAddressPointer, isEventPointer, } from "../helpers/pointers.js";
6
7
  import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
@@ -38,7 +39,7 @@ export class EventStore extends EventModels {
38
39
  }
39
40
  /** A stream of new events added to the store */
40
41
  insert$ = new Subject();
41
- /** A stream of events that have been updated */
42
+ /** A stream of events that have been updated (Warning: this is a very noisy stream, use with caution) */
42
43
  update$ = new Subject();
43
44
  /** A stream of events that have been removed */
44
45
  remove$ = new Subject();
@@ -71,14 +72,6 @@ export class EventStore extends EventModels {
71
72
  this.expiration = options?.expirationManager ?? new ExpirationManager();
72
73
  // Listen to expired events and remove them from the store
73
74
  this.expiration.expired$.subscribe(this.handleExpiredNotification.bind(this));
74
- // when events are added to the database, add the symbol
75
- this.insert$.subscribe((event) => {
76
- Reflect.set(event, EventStoreSymbol, this);
77
- });
78
- // when events are removed from the database, remove the symbol
79
- this.remove$.subscribe((event) => {
80
- Reflect.deleteProperty(event, EventStoreSymbol);
81
- });
82
75
  }
83
76
  mapToMemory(event) {
84
77
  if (event === undefined)
@@ -120,16 +113,31 @@ export class EventStore extends EventModels {
120
113
  this.remove(id);
121
114
  }
122
115
  /** Copies important metadata from and identical event to another */
123
- static mergeDuplicateEvent(source, dest) {
116
+ static copySymbolsToDuplicateEvent(source, dest) {
117
+ if (source.kind !== dest.kind)
118
+ throw new Error("Source and destination events must have the same kind");
119
+ if (isRegularKind(source.kind) && source.id !== dest.id)
120
+ throw new Error("Source and destination events must have the same ID");
121
+ if (isReplaceable(source.kind) &&
122
+ source.pubkey !== dest.pubkey &&
123
+ getReplaceableIdentifier(source) !== getReplaceableIdentifier(dest))
124
+ throw new Error("Source and destination events must have the same pubkey and replaceable identifier");
125
+ let changed = false;
126
+ // Merge seen relays
124
127
  const relays = getSeenRelays(source);
125
128
  if (relays) {
126
129
  for (const relay of relays)
127
130
  addSeenRelay(dest, relay);
131
+ changed = true;
128
132
  }
129
- // copy the from cache symbol only if its true
130
- const fromCache = Reflect.get(source, FromCacheSymbol);
131
- if (fromCache && !Reflect.get(dest, FromCacheSymbol))
132
- Reflect.set(dest, FromCacheSymbol, fromCache);
133
+ const symbols = [FromCacheSymbol, verifiedSymbol, EncryptedContentSymbol];
134
+ for (const symbol of symbols) {
135
+ if (symbol in source && !(symbol in dest)) {
136
+ Reflect.set(dest, symbol, Reflect.get(source, symbol));
137
+ changed = true;
138
+ }
139
+ }
140
+ return changed;
133
141
  }
134
142
  /**
135
143
  * Adds an event to the store and update subscriptions
@@ -148,6 +156,9 @@ export class EventStore extends EventModels {
148
156
  const expiration = getExpirationTimestamp(event);
149
157
  if (this.keepExpired === false && expiration && expiration <= unixNow())
150
158
  return null;
159
+ // Attach relay this event was from
160
+ if (fromRelay)
161
+ addSeenRelay(event, fromRelay);
151
162
  // Get the replaceable identifier
152
163
  const identifier = isReplaceable(event.kind) ? getReplaceableIdentifier(event) : undefined;
153
164
  // Don't insert the event if there is already a newer version
@@ -155,7 +166,8 @@ export class EventStore extends EventModels {
155
166
  const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
156
167
  // If there is already a newer version, copy cached symbols and return existing event
157
168
  if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
158
- EventStore.mergeDuplicateEvent(event, existing[0]);
169
+ if (EventStore.copySymbolsToDuplicateEvent(event, existing[0]))
170
+ this.update(existing[0]);
159
171
  return existing[0];
160
172
  }
161
173
  }
@@ -163,27 +175,28 @@ export class EventStore extends EventModels {
163
175
  if (this.verifyEvent && this.verifyEvent(event) === false)
164
176
  return null;
165
177
  // Always add event to memory
166
- const existing = this.memory?.add(event);
178
+ const existing = this.memory.add(event);
167
179
  // If the memory returned a different instance, this is a duplicate event
168
180
  if (existing && existing !== event) {
169
181
  // Copy cached symbols and return existing event
170
- EventStore.mergeDuplicateEvent(event, existing);
171
- // attach relay this event was from
172
- if (fromRelay)
173
- addSeenRelay(existing, fromRelay);
182
+ if (EventStore.copySymbolsToDuplicateEvent(event, existing))
183
+ this.update(existing);
174
184
  return existing;
175
185
  }
176
186
  // Insert event into database
177
187
  const inserted = this.mapToMemory(this.database.add(event));
178
- // Copy cached data if its a duplicate event
179
- if (event !== inserted)
180
- EventStore.mergeDuplicateEvent(event, inserted);
181
- // attach relay this event was from
182
- if (fromRelay)
183
- addSeenRelay(inserted, fromRelay);
184
- // Emit insert$ signal
185
- if (inserted === event)
188
+ // If the event is the same as the inserted event, its a new event
189
+ if (inserted === event) {
190
+ // Set the event store on the event
191
+ Reflect.set(inserted, EventStoreSymbol, this);
192
+ // Emit insert$ signal
186
193
  this.insert$.next(inserted);
194
+ }
195
+ else {
196
+ // Copy cached data if its a duplicate event
197
+ if (EventStore.copySymbolsToDuplicateEvent(event, inserted))
198
+ this.update(inserted);
199
+ }
187
200
  // remove all old version of the replaceable event
188
201
  if (this.keepOldVersions === false && isReplaceable(event.kind)) {
189
202
  const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
@@ -205,18 +218,20 @@ export class EventStore extends EventModels {
205
218
  /** Removes an event from the store and updates subscriptions */
206
219
  remove(event) {
207
220
  const eventId = typeof event === "string" ? event : event.id;
208
- let instance = this.memory?.getEvent(eventId);
221
+ let instance = this.memory.getEvent(eventId);
209
222
  // Remove from expiration manager
210
223
  this.expiration.forget(eventId);
224
+ // Remove the event store from the event
225
+ if (instance)
226
+ Reflect.deleteProperty(instance, EventStoreSymbol);
211
227
  // Remove from memory if it's not the same as the database
212
228
  if (this.memory !== this.database)
213
229
  this.memory.remove(event);
214
230
  // Remove the event from the database
215
231
  const removed = this.database.remove(event);
216
232
  // If the event was removed, notify the subscriptions
217
- if (removed && instance) {
233
+ if (removed && instance)
218
234
  this.remove$.next(instance);
219
- }
220
235
  return removed;
221
236
  }
222
237
  /** Remove multiple events that match the given filters */
@@ -250,29 +265,40 @@ export class EventStore extends EventModels {
250
265
  }
251
266
  /** Check if the store has an event by id */
252
267
  hasEvent(id) {
253
- // Check if the event exists in memory first, then in the database
254
- return this.memory?.hasEvent(id) || this.database.hasEvent(id);
268
+ if (typeof id === "string")
269
+ return this.memory.hasEvent(id) || this.database.hasEvent(id);
270
+ // If its a pointer, use the advanced has event method to resolve
271
+ else if (isEventPointer(id))
272
+ return this.memory.hasEvent(id.id) || this.database.hasEvent(id.id);
273
+ else
274
+ return this.hasReplaceable(id.kind, id.pubkey, id.identifier);
255
275
  }
256
276
  /** Get an event by id from the store */
257
277
  getEvent(id) {
258
278
  // Get the event from memory first, then from the database
259
- return this.memory?.getEvent(id) ?? this.mapToMemory(this.database.getEvent(id));
279
+ if (typeof id === "string")
280
+ return this.memory.getEvent(id) ?? this.mapToMemory(this.database.getEvent(id));
281
+ // If its a pointer, use the advanced get event method to resolve
282
+ else if (isEventPointer(id))
283
+ return this.memory.getEvent(id.id) ?? this.mapToMemory(this.database.getEvent(id.id));
284
+ else
285
+ return this.getReplaceable(id.kind, id.pubkey, id.identifier);
260
286
  }
261
287
  /** Check if the store has a replaceable event */
262
288
  hasReplaceable(kind, pubkey, d) {
263
289
  // Check if the event exists in memory first, then in the database
264
- return this.memory?.hasReplaceable(kind, pubkey, d) || this.database.hasReplaceable(kind, pubkey, d);
290
+ return this.memory.hasReplaceable(kind, pubkey, d) || this.database.hasReplaceable(kind, pubkey, d);
265
291
  }
266
292
  /** Gets the latest version of a replaceable event */
267
293
  getReplaceable(kind, pubkey, identifier) {
268
294
  // Get the event from memory first, then from the database
269
- return (this.memory?.getReplaceable(kind, pubkey, identifier) ??
295
+ return (this.memory.getReplaceable(kind, pubkey, identifier) ??
270
296
  this.mapToMemory(this.database.getReplaceable(kind, pubkey, identifier)));
271
297
  }
272
298
  /** Returns all versions of a replaceable event */
273
299
  getReplaceableHistory(kind, pubkey, identifier) {
274
300
  // Get the events from memory first, then from the database
275
- return (this.memory?.getReplaceableHistory(kind, pubkey, identifier) ??
301
+ return (this.memory.getReplaceableHistory(kind, pubkey, identifier) ??
276
302
  this.database.getReplaceableHistory(kind, pubkey, identifier)?.map((e) => this.mapToMemory(e) ?? e));
277
303
  }
278
304
  /** Get all events matching a filter */
@@ -295,30 +321,30 @@ export class EventStore extends EventModels {
295
321
  }
296
322
  /** Passthrough method for the database.touch */
297
323
  touch(event) {
298
- return this.memory?.touch(event);
324
+ return this.memory.touch(event);
299
325
  }
300
326
  /** Increments the claim count on the event and touches it */
301
327
  claim(event) {
302
- return this.memory?.claim(event);
328
+ return this.memory.claim(event);
303
329
  }
304
330
  /** Checks if an event is claimed by anything */
305
331
  isClaimed(event) {
306
- return this.memory?.isClaimed(event) ?? false;
332
+ return this.memory.isClaimed(event) ?? false;
307
333
  }
308
334
  /** Decrements the claim count on an event */
309
335
  removeClaim(event) {
310
- return this.memory?.removeClaim(event);
336
+ return this.memory.removeClaim(event);
311
337
  }
312
338
  /** Removes all claims on an event */
313
339
  clearClaim(event) {
314
- return this.memory?.clearClaim(event);
340
+ return this.memory.clearClaim(event);
315
341
  }
316
342
  /** Pass through method for the database.unclaimed */
317
343
  unclaimed() {
318
- return this.memory?.unclaimed() || (function* () { })();
344
+ return this.memory.unclaimed() || (function* () { })();
319
345
  }
320
346
  /** Removes any event that is not being used by a subscription */
321
347
  prune(limit) {
322
- return this.memory?.prune(limit) ?? 0;
348
+ return this.memory.prune(limit) ?? 0;
323
349
  }
324
350
  }
@@ -1,8 +1,8 @@
1
1
  import { Observable } from "rxjs";
2
2
  import { NostrEvent } from "../helpers/event.js";
3
3
  import { Filter } from "../helpers/filter.js";
4
- import { AddressPointer, AddressPointerWithoutD, EventPointer } from "../helpers/pointers.js";
5
- import { IEventStoreModels } from "./event-models.js";
4
+ import { AddressPointer, AddressPointerWithoutD, EventPointer, ProfilePointer } from "../helpers/pointers.js";
5
+ import { ProfileContent } from "../helpers/profile.js";
6
6
  /** The read interface for an event store */
7
7
  export interface IEventStoreRead {
8
8
  /** Check if the event store has an event with id */
@@ -37,6 +37,20 @@ export interface IAsyncEventStoreRead {
37
37
  /** Get a timeline of events that match the filters */
38
38
  getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
39
39
  }
40
+ /** An extended read interface for an event store that supports pointers */
41
+ export interface IEventStoreReadAdvanced extends Omit<IEventStoreRead, "hasEvent" | "getEvent"> {
42
+ /** Check if the event store has an event with id */
43
+ hasEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): boolean;
44
+ /** Get an event by id */
45
+ getEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): NostrEvent | undefined;
46
+ }
47
+ /** An extended async read interface for an event store that supports pointers */
48
+ export interface IAsyncEventStoreReadAdvanced extends Omit<IAsyncEventStoreRead, "hasEvent" | "getEvent"> {
49
+ /** Check if the event store has an event with id */
50
+ hasEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): Promise<boolean>;
51
+ /** Get an event by id */
52
+ getEvent(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): Promise<NostrEvent | undefined>;
53
+ }
40
54
  /** The stream interface for an event store */
41
55
  export interface IEventStoreStreams {
42
56
  /** A stream of new events added to the store */
@@ -81,8 +95,8 @@ export interface IEventClaims {
81
95
  }
82
96
  /** An event store that can be subscribed to */
83
97
  export interface IEventSubscriptions {
84
- /** Subscribe to an event by id */
85
- event(id: string | EventPointer): Observable<NostrEvent | undefined>;
98
+ /** Subscribe to an event by id or pointer */
99
+ event(id: string | EventPointer | AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
86
100
  /** Subscribe to a replaceable event by pointer */
87
101
  replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
88
102
  /** Subscribe to a replaceable event with legacy arguments */
@@ -93,6 +107,15 @@ export interface IEventSubscriptions {
93
107
  filters(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
94
108
  /** Subscribe to a sorted timeline of events that match the filters */
95
109
  timeline(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent[]>;
110
+ /** Subscribe to a users profile */
111
+ profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
112
+ /** Subscribe to a users contacts */
113
+ contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
114
+ /** Subscribe to a users mailboxes */
115
+ mailboxes(user: string | ProfilePointer): Observable<{
116
+ inboxes: string[];
117
+ outboxes: string[];
118
+ } | undefined>;
96
119
  }
97
120
  /** Methods for creating common models */
98
121
  export interface IEventModelMixin<TStore extends IEventStore | IAsyncEventStore> {
@@ -185,8 +208,8 @@ export interface IMissingEventLoader {
185
208
  eventLoader?: (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
186
209
  }
187
210
  /** Generic async event store interface */
188
- export interface IAsyncEventStore extends IAsyncEventStoreRead, IEventStoreStreams, IEventSubscriptions, IAsyncEventStoreActions, IEventModelMixin<IAsyncEventStore>, IEventClaims, IMissingEventLoader, IEventStoreModels {
211
+ export interface IAsyncEventStore extends IAsyncEventStoreReadAdvanced, IEventStoreStreams, IEventSubscriptions, IAsyncEventStoreActions, IEventModelMixin<IAsyncEventStore>, IEventClaims, IMissingEventLoader {
189
212
  }
190
213
  /** Generic sync event store interface */
191
- export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventSubscriptions, IEventStoreActions, IEventModelMixin<IEventStore>, IEventClaims, IMissingEventLoader, IEventStoreModels {
214
+ export interface IEventStore extends IEventStoreReadAdvanced, IEventStoreStreams, IEventSubscriptions, IEventStoreActions, IEventModelMixin<IEventStore>, IEventClaims, IMissingEventLoader {
192
215
  }
@@ -18,6 +18,7 @@ export declare function getPublicContacts(event: NostrEvent): ProfilePointer[];
18
18
  /** Checks if the hidden contacts are unlocked */
19
19
  export declare function isHiddenContactsUnlocked<T extends NostrEvent>(event: T): event is T & UnlockedContacts;
20
20
  /** Returns only the hidden contacts from a contacts list event */
21
+ export declare function getHiddenContacts(event: UnlockedContacts): ProfilePointer[];
21
22
  export declare function getHiddenContacts(event: NostrEvent): ProfilePointer[] | undefined;
22
23
  /** Unlocks the hidden contacts */
23
24
  export declare function unlockHiddenContacts(event: NostrEvent, signer: HiddenContentSigner): Promise<ProfilePointer[]>;
@@ -54,11 +54,11 @@ export function getPublicContacts(event) {
54
54
  }
55
55
  /** Checks if the hidden contacts are unlocked */
56
56
  export function isHiddenContactsUnlocked(event) {
57
- return isHiddenTagsUnlocked(event) && Reflect.has(event, HiddenContactsSymbol);
57
+ // No need for try catch or proactivly parsing here since it only depends on hidden tags
58
+ return isHiddenTagsUnlocked(event);
58
59
  }
59
- /** Returns only the hidden contacts from a contacts list event */
60
60
  export function getHiddenContacts(event) {
61
- if (isHiddenContactsUnlocked(event))
61
+ if (HiddenContactsSymbol in event)
62
62
  return event[HiddenContactsSymbol];
63
63
  // Get hidden tags
64
64
  const tags = getHiddenTags(event);
@@ -45,7 +45,9 @@ export declare function hasEncryptedContent<T extends {
45
45
  export declare function getEncryptedContent<T extends UnlockedEncryptedContent>(event: T): string;
46
46
  export declare function getEncryptedContent<T extends object>(event: T): string | undefined;
47
47
  /** Checks if the encrypted content is unlocked and casts it to the {@link UnlockedEncryptedContent} type */
48
- export declare function isEncryptedContentUnlocked<T extends object>(event: T): event is T & UnlockedEncryptedContent;
48
+ export declare function isEncryptedContentUnlocked<T extends {
49
+ kind: number;
50
+ }>(event: T): event is T & UnlockedEncryptedContent;
49
51
  /**
50
52
  * Unlocks the encrypted content in an event and caches it
51
53
  * @param event The event with content to decrypt
@@ -1,6 +1,6 @@
1
1
  import { bufferTime, filter } from "rxjs";
2
2
  import { logger } from "../logger.js";
3
- import { isFromCache } from "./index.js";
3
+ import { isFromCache } from "./event.js";
4
4
  const log = logger.extend("event-cache");
5
5
  /**
6
6
  * Setups a process to write batches of new events from an event store to a cache
@@ -1,7 +1,7 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { isAddressableKind, isEphemeralKind, isRegularKind, isReplaceableKind } from "nostr-tools/kinds";
3
3
  import { NostrEvent, VerifiedEvent } from "nostr-tools/pure";
4
- import { IEventStore } from "../event-store/interface.js";
4
+ import { IAsyncEventStore, IEventStore } from "../event-store/interface.js";
5
5
  export { EventTemplate, finalizeEvent, getEventHash, NostrEvent, serializeEvent, UnsignedEvent, VerifiedEvent, verifiedSymbol, verifyEvent, } from "nostr-tools/pure";
6
6
  export { binarySearch, bytesToHex, hexToBytes, insertEventIntoAscendingList, insertEventIntoDescendingList, } from "nostr-tools/utils";
7
7
  export { isAddressableKind, isEphemeralKind, isRegularKind, isReplaceableKind, kinds };
@@ -51,7 +51,7 @@ export declare function markFromCache(event: NostrEvent): void;
51
51
  /** Returns if an event was from a cache */
52
52
  export declare function isFromCache(event: NostrEvent): boolean;
53
53
  /** Returns the EventStore of an event if its been added to one */
54
- export declare function getParentEventStore<T extends object>(event: T): IEventStore | undefined;
54
+ export declare function getParentEventStore<T extends object>(event: T): IEventStore | IAsyncEventStore | undefined;
55
55
  /** Notifies the events parent store that an event has been updated */
56
56
  export declare function notifyEventUpdate(event: any): void;
57
57
  /** Returns the replaceable identifier for a replaceable event */
@@ -28,8 +28,8 @@ export function isHiddenContentUnlocked(event) {
28
28
  export function getHiddenContent(event) {
29
29
  if (!canHaveHiddenContent(event.kind))
30
30
  return undefined;
31
- if (isHiddenContentUnlocked(event))
32
- return event[EncryptedContentSymbol];
31
+ if (HiddenContentSymbol in event)
32
+ return Reflect.get(event, HiddenContentSymbol);
33
33
  return getEncryptedContent(event);
34
34
  }
35
35
  /**
@@ -42,8 +42,8 @@ export async function unlockHiddenContent(event, signer, override) {
42
42
  if (!canHaveHiddenContent(event.kind))
43
43
  throw new Error("Event kind does not support hidden content");
44
44
  // If the encrypted content is already unlocked, return the cached value
45
- if (isEncryptedContentUnlocked(event))
46
- return event[EncryptedContentSymbol];
45
+ if (HiddenContentSymbol in event)
46
+ return Reflect.get(event, HiddenContentSymbol);
47
47
  // Get the encryption method from the signer
48
48
  const encryption = getEncryptedContentEncryptionMethods(event.kind, signer, override);
49
49
  // Decrypt the content using the events pubkey
@@ -23,7 +23,10 @@ export declare function getHiddenTagsEncryptionMethods(kind: number, signer: Hid
23
23
  export declare function isHiddenTagsUnlocked<T extends {
24
24
  kind: number;
25
25
  }>(event: T): event is T & UnlockedHiddenTags;
26
- /** Returns the hidden tags for an event if they are unlocked */
26
+ /**
27
+ * Returns the hidden tags for an event if they are unlocked
28
+ * @throws {Error} If the hidden content is not an array of tags
29
+ */
27
30
  export declare function getHiddenTags<T extends {
28
31
  kind: number;
29
32
  } & UnlockedHiddenTags>(event: T): string[][];
@@ -35,7 +38,8 @@ export declare function getHiddenTags<T extends {
35
38
  * @param event The list event to decrypt
36
39
  * @param signer A signer to use to decrypt the tags
37
40
  * @param override The encryption method to use instead of the default
38
- * @throws
41
+ * @throws {Error} If the event kind does not support hidden tags
42
+ * @throws {Error} If the hidden content is not an array of tags
39
43
  */
40
44
  export declare function unlockHiddenTags<T extends {
41
45
  kind: number;
@@ -42,13 +42,18 @@ export function getHiddenTagsEncryptionMethods(kind, signer) {
42
42
  export function isHiddenTagsUnlocked(event) {
43
43
  if (!canHaveHiddenTags(event.kind))
44
44
  return false;
45
- return isHiddenContentUnlocked(event) && Reflect.has(event, `HiddenTagsSymbol`);
45
+ // Wrap in try catch to avoid throwing validation errors
46
+ try {
47
+ return HiddenTagsSymbol in event || (isHiddenContentUnlocked(event) && getHiddenTags(event) !== undefined);
48
+ }
49
+ catch { }
50
+ return false;
46
51
  }
47
52
  export function getHiddenTags(event) {
48
53
  if (!canHaveHiddenTags(event.kind))
49
54
  return undefined;
50
55
  // If the hidden tags are already unlocked, return the cached value
51
- if (isHiddenTagsUnlocked(event))
56
+ if (HiddenTagsSymbol in event)
52
57
  return event[HiddenTagsSymbol];
53
58
  // unlock hidden content is needed
54
59
  const content = getHiddenContent(event);
@@ -71,7 +76,8 @@ export function getHiddenTags(event) {
71
76
  * @param event The list event to decrypt
72
77
  * @param signer A signer to use to decrypt the tags
73
78
  * @param override The encryption method to use instead of the default
74
- * @throws
79
+ * @throws {Error} If the event kind does not support hidden tags
80
+ * @throws {Error} If the hidden content is not an array of tags
75
81
  */
76
82
  export async function unlockHiddenTags(event, signer, override) {
77
83
  if (!canHaveHiddenTags(event.kind))
@@ -29,9 +29,9 @@ export declare function getAddressPointerFromATag(tag: string[]): AddressPointer
29
29
  /** Gets a ProfilePointer from a common "p" tag */
30
30
  export declare function getProfilePointerFromPTag(tag: string[]): ProfilePointer | null;
31
31
  /** Checks if a pointer is an AddressPointer */
32
- export declare function isAddressPointer(pointer: DecodeResult["data"]): pointer is AddressPointer;
32
+ export declare function isAddressPointer(pointer: any): pointer is AddressPointer;
33
33
  /** Checks if a pointer is an EventPointer */
34
- export declare function isEventPointer(pointer: DecodeResult["data"]): pointer is EventPointer;
34
+ export declare function isEventPointer(pointer: any): pointer is EventPointer;
35
35
  /** Returns the stringified address pointer */
36
36
  export declare function getReplaceableAddressFromPointer(pointer: AddressPointer): string;
37
37
  /** Returns an AddressPointer for a replaceable event */
@@ -40,13 +40,15 @@ export function parseReplaceableAddress(address, requireIdentifier = false) {
40
40
  const parts = address.split(":");
41
41
  const kind = parts[0] ? parseInt(parts[0]) : undefined;
42
42
  const pubkey = parts[1];
43
- const identifier = parts[2] ?? "";
44
43
  // Check valid kind
45
44
  if (kind === undefined)
46
45
  return null;
47
46
  // Check valid pubkey
48
47
  if (pubkey === undefined || pubkey === "" || !isHexKey(pubkey))
49
48
  return null;
49
+ // Reconstruct identifier by joining all remaining parts after pubkey
50
+ // This handles cases where the identifier contains colons (e.g., URLs)
51
+ const identifier = parts.slice(2).join(":");
50
52
  // Return null if identifier is required and missing
51
53
  if (requireIdentifier && identifier === "")
52
54
  return null;
@@ -149,11 +151,18 @@ export function getProfilePointerFromPTag(tag) {
149
151
  }
150
152
  /** Checks if a pointer is an AddressPointer */
151
153
  export function isAddressPointer(pointer) {
152
- return typeof pointer !== "string" && "identifier" in pointer && "pubkey" in pointer && "kind" in pointer;
154
+ return (typeof pointer === "object" &&
155
+ pointer !== null &&
156
+ "identifier" in pointer &&
157
+ "pubkey" in pointer &&
158
+ "kind" in pointer &&
159
+ typeof pointer.identifier === "string" &&
160
+ typeof pointer.pubkey === "string" &&
161
+ typeof pointer.kind === "number");
153
162
  }
154
163
  /** Checks if a pointer is an EventPointer */
155
164
  export function isEventPointer(pointer) {
156
- return typeof pointer !== "string" && "id" in pointer;
165
+ return typeof pointer === "object" && pointer !== null && "id" in pointer && typeof pointer.id === "string";
157
166
  }
158
167
  /** Returns the stringified address pointer */
159
168
  export function getReplaceableAddressFromPointer(pointer) {
@@ -1,5 +1,4 @@
1
- import { NostrEvent, kinds } from "./event.js";
2
- import { KnownEvent } from "./index.js";
1
+ import { KnownEvent, NostrEvent, kinds } from "./event.js";
3
2
  export declare const ProfileContentSymbol: unique symbol;
4
3
  export type ProfileContent = {
5
4
  name?: string;
@@ -1,6 +1,6 @@
1
1
  import { getOrComputeCachedValue } from "./cache.js";
2
2
  import { kinds } from "./event.js";
3
- import { safeParse } from "./index.js";
3
+ import { safeParse } from "./json.js";
4
4
  import { npubEncode } from "./pointers.js";
5
5
  export const ProfileContentSymbol = Symbol.for("profile-content");
6
6
  export function getProfileContent(event) {
@@ -9,6 +9,8 @@ declare module "nostr-tools" {
9
9
  export declare function addSeenRelay(event: NostrEvent, relay: string): Set<string>;
10
10
  /** Returns the set of relays this event was seen on */
11
11
  export declare function getSeenRelays(event: NostrEvent): Set<string> | undefined;
12
+ /** Checks if an event was received from a specific relay */
13
+ export declare function isFromRelay(event: NostrEvent, relay: string): boolean;
12
14
  /** A fast check to make sure relay hints are safe to connect to */
13
15
  export declare function isSafeRelayURL(relay: string): boolean;
14
16
  /** Merge multiple sets of relays and remove duplicates (ignores invalid URLs) */
@@ -17,6 +17,10 @@ export function addSeenRelay(event, relay) {
17
17
  export function getSeenRelays(event) {
18
18
  return Reflect.get(event, SeenRelaysSymbol);
19
19
  }
20
+ /** Checks if an event was received from a specific relay */
21
+ export function isFromRelay(event, relay) {
22
+ return getSeenRelays(event)?.has(relay) === true;
23
+ }
20
24
  const WEBSOCKET_URL_CHECK = /^wss?:\/\/([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}|localhost)\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)$/;
21
25
  /** A fast check to make sure relay hints are safe to connect to */
22
26
  export function isSafeRelayURL(relay) {
@@ -12,8 +12,10 @@ export declare function isRTag(tag: string[]): tag is ["r", string, ...string[]]
12
12
  export declare function isDTag(tag: string[]): tag is ["d", string, ...string[]];
13
13
  /** Checks if tag is an "a" tag and has at least one value */
14
14
  export declare function isATag(tag: string[]): tag is ["a", string, ...string[]];
15
- /** Checks if tag is an "a" tag and has at least one value */
15
+ /** Checks if tag is an "t" tag and has at least one value */
16
16
  export declare function isTTag(tag: string[]): tag is ["t", string, ...string[]];
17
+ /** Checks if tag is an "q" tag and has at least one value */
18
+ export declare function isQTag(tag: string[]): tag is ["q", string, ...string[]];
17
19
  /** Filter and transform tags */
18
20
  export declare const processTags: TagPipe;
19
21
  /** A pipeline that filters and maps each tag */
@@ -22,10 +22,14 @@ export function isDTag(tag) {
22
22
  export function isATag(tag) {
23
23
  return isNameValueTag(tag, "a");
24
24
  }
25
- /** Checks if tag is an "a" tag and has at least one value */
25
+ /** Checks if tag is an "t" tag and has at least one value */
26
26
  export function isTTag(tag) {
27
27
  return isNameValueTag(tag, "t");
28
28
  }
29
+ /** Checks if tag is an "q" tag and has at least one value */
30
+ export function isQTag(tag) {
31
+ return isNameValueTag(tag, "q");
32
+ }
29
33
  /** Filter and transform tags */
30
34
  export const processTags = (tags, ...fns) => {
31
35
  return fns.reduce((step, fn) => {
@@ -1,9 +1,8 @@
1
- import { defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, from, ignoreElements, map, merge, mergeMap, mergeWith, of, repeat, scan, switchMap, take, takeUntil, tap, } from "rxjs";
1
+ import { defer, distinctUntilChanged, EMPTY, filter, finalize, from, identity, map, merge, mergeMap, mergeWith, of, scan, startWith, switchMap, take, tap, } from "rxjs";
2
2
  import { getEventUID, getReplaceableIdentifier, insertEventIntoDescendingList, isReplaceable, } from "../helpers/event.js";
3
3
  import { matchFilters } from "../helpers/filter.js";
4
4
  import { claimEvents } from "../observable/claim-events.js";
5
5
  import { claimLatest } from "../observable/claim-latest.js";
6
- import { defined } from "../observable/defined.js";
7
6
  /** Gets a single event from both types of event stores and returns an observable that completes */
8
7
  function getEventFromStores(store, pointer) {
9
8
  const r = store.getEvent(pointer.id);
@@ -35,10 +34,13 @@ function loadEventUsingFallback(store, pointer) {
35
34
  return switchMap((event) => {
36
35
  if (event)
37
36
  return of(event);
38
- // If event was not found, attempt to load
37
+ // If no loader pass value through, should never happen
39
38
  if (!store.eventLoader)
40
- return EMPTY;
41
- return from(store.eventLoader(pointer)).pipe(filter((e) => !!e));
39
+ return of(event);
40
+ // If event was not found, attempt to load
41
+ return from(store.eventLoader(pointer)).pipe(
42
+ // Start with `undefined` since its not loaded yet
43
+ startWith(undefined));
42
44
  });
43
45
  }
44
46
  /** A model that returns a single event or undefined when its removed */
@@ -46,22 +48,22 @@ export function EventModel(pointer) {
46
48
  if (typeof pointer === "string")
47
49
  pointer = { id: pointer };
48
50
  return (store) => merge(
49
- // get current event and ignore if there is none
51
+ // get current event
50
52
  defer(() => getEventFromStores(store, pointer)).pipe(
51
53
  // If the event isn't found, attempt to load using the fallback loader
52
- loadEventUsingFallback(store, pointer),
53
- // Only emit found events
54
- defined()),
54
+ store.eventLoader ? loadEventUsingFallback(store, pointer) : identity),
55
55
  // Listen for new events
56
56
  store.insert$.pipe(filter((e) => e.id === pointer.id)),
57
57
  // emit undefined when deleted
58
- store.remove$.pipe(filter((e) => e.id === pointer.id), take(1), ignoreElements(),
58
+ store.remove$.pipe(filter((e) => e.id === pointer.id),
59
+ // Complete when the event is removed
60
+ take(1),
59
61
  // Emit undefined when deleted
60
- endWith(undefined))).pipe(
62
+ map(() => undefined))).pipe(
63
+ // ignore duplicate events (true === same)
64
+ distinctUntilChanged((a, b) => a?.id === b?.id),
61
65
  // claim all events
62
- claimLatest(store),
63
- // ignore duplicate events
64
- distinctUntilChanged((a, b) => a?.id === b?.id));
66
+ claimLatest(store));
65
67
  }
66
68
  /** A model that returns the latest version of a replaceable event or undefined if its removed */
67
69
  export function ReplaceableModel(pointer) {
@@ -71,26 +73,30 @@ export function ReplaceableModel(pointer) {
71
73
  // lazily get current event
72
74
  defer(() => getReplaceableFromStores(store, pointer)).pipe(
73
75
  // If the event isn't found, attempt to load using the fallback loader
74
- loadEventUsingFallback(store, pointer),
75
- // Only emit found events
76
- defined()),
76
+ store.eventLoader ? loadEventUsingFallback(store, pointer) : identity),
77
77
  // Subscribe to new events that match the pointer
78
78
  store.insert$.pipe(filter((e) => e.pubkey == pointer.pubkey &&
79
79
  e.kind === pointer.kind &&
80
80
  (pointer.identifier !== undefined ? getReplaceableIdentifier(e) === pointer.identifier : true)))).pipe(
81
- // only update if event is newer
81
+ // Hacky way to extract the current event so it can be used in the remove$ stream
82
+ tap((event) => (current = event)),
83
+ // Subscribe to the event being removed
84
+ mergeWith(store.remove$.pipe(filter((e) => {
85
+ return e.id === current?.id;
86
+ }),
87
+ // Emit undefined when the event is removed
88
+ map(() => {
89
+ return undefined;
90
+ }))),
91
+ // only update if event is newer (true === same)
82
92
  distinctUntilChanged((prev, event) => {
83
- // are the events the same? i.e. is the prev event older
84
- return prev.created_at >= event.created_at;
93
+ // If the event has changed from undefined to defined or vice versa
94
+ if (prev === undefined || event === undefined) {
95
+ return prev === event;
96
+ }
97
+ // Return if event is newer than the previous event
98
+ return event.created_at < prev.created_at;
85
99
  }),
86
- // Hacky way to extract the current event so takeUntil can access it
87
- tap((event) => (current = event)),
88
- // complete when event is removed
89
- takeUntil(store.remove$.pipe(filter((e) => e.id === current?.id))),
90
- // emit undefined when removed
91
- endWith(undefined),
92
- // keep the observable hot
93
- repeat(),
94
100
  // claim latest event
95
101
  claimLatest(store));
96
102
  };
@@ -128,7 +134,8 @@ export function TimelineModel(filters, includeOldVersion) {
128
134
  if (isReplaceable(e.kind))
129
135
  seen.set(getEventUID(e), e);
130
136
  }
131
- return event;
137
+ // Always return a new array instance to ensure UI libraries detect changes
138
+ return [...event];
132
139
  }
133
140
  // create a new timeline and insert the event into it
134
141
  let newTimeline = [...timeline];
@@ -136,14 +143,17 @@ export function TimelineModel(filters, includeOldVersion) {
136
143
  if (!includeOldVersion && isReplaceable(event.kind)) {
137
144
  const uid = getEventUID(event);
138
145
  const existing = seen.get(uid);
139
- // if this is an older replaceable event, exit
146
+ // if this is an older replaceable event, return a new array instance
140
147
  if (existing && event.created_at < existing.created_at)
141
- return timeline;
148
+ return [...timeline];
142
149
  // update latest version
143
150
  seen.set(uid, event);
144
151
  // remove old event from timeline
145
- if (existing)
146
- newTimeline.slice(newTimeline.indexOf(existing), 1);
152
+ if (existing) {
153
+ const index = newTimeline.indexOf(existing);
154
+ if (index !== -1)
155
+ newTimeline.splice(index, 1);
156
+ }
147
157
  }
148
158
  // add event into timeline
149
159
  insertEventIntoDescendingList(newTimeline, event);
@@ -5,3 +5,5 @@ import { IEventStoreStreams } from "../event-store/interface.js";
5
5
  export declare function watchEventUpdates(eventStore: IEventStoreStreams): MonoTypeOperatorFunction<NostrEvent | undefined>;
6
6
  /** Watches for any updates to the latest array of events and remits the array of events when updated */
7
7
  export declare function watchEventsUpdates(eventStore: IEventStoreStreams): MonoTypeOperatorFunction<NostrEvent[]>;
8
+ /** Alias for watchEventsUpdates */
9
+ export declare const watchTimelineUpdates: typeof watchEventsUpdates;
@@ -23,3 +23,5 @@ export function watchEventsUpdates(eventStore) {
23
23
  map(() => latest)));
24
24
  };
25
25
  }
26
+ /** Alias for watchEventsUpdates */
27
+ export const watchTimelineUpdates = watchEventsUpdates;
@@ -5,7 +5,7 @@ import { AddressPointer, EventPointer, ProfilePointer } from "../../helpers/poin
5
5
  export declare function addProfilePointerTag(pubkey: string | ProfilePointer, replace?: boolean): TagOperation;
6
6
  /** Removes all "p" tags matching a pubkey */
7
7
  export declare function removeProfilePointerTag(pubkey: string | ProfilePointer): TagOperation;
8
- /** Adds a a single "e" tag for an EventPointer */
8
+ /** Adds a single "e" tag for an EventPointer */
9
9
  export declare function addEventPointerTag(id: string | EventPointer | NostrEvent, replace?: boolean): TagOperation;
10
10
  /** Removes all "e" tags matching EventPointer or id */
11
11
  export declare function removeEventPointerTag(id: string | EventPointer): TagOperation;
@@ -24,7 +24,7 @@ export function removeProfilePointerTag(pubkey) {
24
24
  pubkey = typeof pubkey !== "string" ? pubkey.pubkey : pubkey;
25
25
  return (tags) => tags.filter((t) => !(t[0] === "p" && t[1] === pubkey));
26
26
  }
27
- /** Adds a a single "e" tag for an EventPointer */
27
+ /** Adds a single "e" tag for an EventPointer */
28
28
  export function addEventPointerTag(id, replace = true) {
29
29
  return async (tags, { getEventRelayHint }) => {
30
30
  const pointer = typeof id === "string" ? { id } : isEvent(id) ? getEventPointerForEvent(id) : id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20251209200210",
3
+ "version": "0.0.0-next-20251220152312",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",