applesauce-core 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/event-store/event-set.js +5 -3
  2. package/dist/event-store/event-store.d.ts +54 -24
  3. package/dist/event-store/event-store.js +139 -47
  4. package/dist/event-store/interface.d.ts +36 -18
  5. package/dist/helpers/calendar-event.d.ts +36 -0
  6. package/dist/helpers/calendar-event.js +110 -0
  7. package/dist/helpers/calendar-rsvp.d.ts +15 -0
  8. package/dist/helpers/calendar-rsvp.js +38 -0
  9. package/dist/helpers/calendar.d.ts +6 -0
  10. package/dist/helpers/calendar.js +11 -0
  11. package/dist/helpers/encrypted-content.d.ts +18 -10
  12. package/dist/helpers/encrypted-content.js +11 -3
  13. package/dist/helpers/event-cache.d.ts +15 -0
  14. package/dist/helpers/event-cache.js +32 -0
  15. package/dist/helpers/event.d.ts +1 -1
  16. package/dist/helpers/event.js +1 -1
  17. package/dist/helpers/expiration.js +1 -2
  18. package/dist/helpers/hidden-content.d.ts +3 -3
  19. package/dist/helpers/hidden-content.js +2 -2
  20. package/dist/helpers/hidden-tags.d.ts +5 -10
  21. package/dist/helpers/hidden-tags.js +3 -3
  22. package/dist/helpers/highlight.d.ts +45 -0
  23. package/dist/helpers/highlight.js +76 -0
  24. package/dist/helpers/index.d.ts +8 -0
  25. package/dist/helpers/index.js +8 -0
  26. package/dist/helpers/pointers.js +1 -1
  27. package/dist/helpers/poll.d.ts +46 -0
  28. package/dist/helpers/poll.js +78 -0
  29. package/dist/helpers/stream-chat.d.ts +4 -0
  30. package/dist/helpers/stream-chat.js +9 -0
  31. package/dist/helpers/stream.d.ts +31 -0
  32. package/dist/helpers/stream.js +81 -0
  33. package/dist/logger.d.ts +1 -0
  34. package/dist/logger.js +1 -0
  35. package/dist/models/blossom.d.ts +2 -1
  36. package/dist/models/blossom.js +4 -2
  37. package/dist/models/calendar.d.ts +6 -0
  38. package/dist/models/calendar.js +15 -0
  39. package/dist/models/common.d.ts +14 -10
  40. package/dist/models/common.js +47 -76
  41. package/dist/models/contacts.d.ts +1 -1
  42. package/dist/models/contacts.js +4 -2
  43. package/dist/models/index.d.ts +1 -0
  44. package/dist/models/index.js +1 -0
  45. package/dist/models/mailboxes.d.ts +2 -1
  46. package/dist/models/mailboxes.js +4 -2
  47. package/dist/models/mutes.d.ts +3 -2
  48. package/dist/models/mutes.js +4 -2
  49. package/dist/models/profile.d.ts +2 -1
  50. package/dist/models/profile.js +4 -2
  51. package/package.json +1 -1
  52. package/dist/event-store/common.d.ts +0 -1
  53. package/dist/event-store/common.js +0 -1
@@ -1,7 +1,7 @@
1
1
  import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
2
2
  import { Subject } from "rxjs";
3
3
  import { getIndexableTags, INDEXABLE_TAGS } from "../helpers/event-tags.js";
4
- import { createReplaceableAddress, getReplaceableAddress, isReplaceable } from "../helpers/event.js";
4
+ import { createReplaceableAddress, isReplaceable } from "../helpers/event.js";
5
5
  import { LRU } from "../helpers/lru.js";
6
6
  import { logger } from "../logger.js";
7
7
  /**
@@ -92,7 +92,8 @@ export class EventSet {
92
92
  insertEventIntoDescendingList(this.created_at, event);
93
93
  // Insert into replaceable index
94
94
  if (isReplaceable(event.kind)) {
95
- const address = getReplaceableAddress(event);
95
+ const identifier = event.tags.find((t) => t[0] === "d")?.[1];
96
+ const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
96
97
  let array = this.replaceable.get(address);
97
98
  if (!this.replaceable.has(address)) {
98
99
  // add an empty array if there is no array
@@ -135,7 +136,8 @@ export class EventSet {
135
136
  this.events.delete(id);
136
137
  // remove from replaceable index
137
138
  if (isReplaceable(event.kind)) {
138
- const address = getReplaceableAddress(event);
139
+ const identifier = event.tags.find((t) => t[0] === "d")?.[1];
140
+ const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
139
141
  const array = this.replaceable.get(address);
140
142
  if (array && array.includes(event)) {
141
143
  const idx = array.indexOf(event);
@@ -1,12 +1,16 @@
1
1
  import { Filter, NostrEvent } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
+ import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
4
+ import { AddressPointerWithoutD } from "../helpers/pointers.js";
3
5
  import { EventSet } from "./event-set.js";
4
6
  import { IEventStore, ModelConstructor } from "./interface.js";
5
- import { AddressPointer, EventPointer } from "nostr-tools/nip19";
7
+ /** An extended {@link EventSet} that handles replaceable events, delets, and models */
6
8
  export declare class EventStore implements IEventStore {
7
9
  database: EventSet;
8
10
  /** Enable this to keep old versions of replaceable events */
9
11
  keepOldVersions: boolean;
12
+ /** Enable this to keep expired events */
13
+ keepExpired: boolean;
10
14
  /**
11
15
  * A method used to verify new events before added them
12
16
  * @returns true if the event is valid, false if it should be ignored
@@ -18,10 +22,33 @@ export declare class EventStore implements IEventStore {
18
22
  update$: Observable<NostrEvent>;
19
23
  /** A stream of events that have been removed */
20
24
  remove$: Observable<NostrEvent>;
25
+ /**
26
+ * A method that will be called when an event isn't found in the store
27
+ * @experimental
28
+ */
29
+ eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
30
+ /**
31
+ * A method that will be called when a replaceable event isn't found in the store
32
+ * @experimental
33
+ */
34
+ replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
35
+ /**
36
+ * A method that will be called when an addressable event isn't found in the store
37
+ * @experimental
38
+ */
39
+ addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
21
40
  constructor();
22
41
  protected deletedIds: Set<string>;
23
42
  protected deletedCoords: Map<string, number>;
24
43
  protected checkDeleted(event: string | NostrEvent): boolean;
44
+ protected expirations: Map<string, number>;
45
+ /** Adds an event to the expiration map */
46
+ protected addExpiration(event: NostrEvent): void;
47
+ protected expirationTimeout: number | null;
48
+ protected nextExpirationCheck: number | null;
49
+ protected handleExpiringEvent(event: NostrEvent): void;
50
+ /** Remove expired events from the store */
51
+ protected pruneExpired(): void;
25
52
  protected handleDeleteEvent(deleteEvent: NostrEvent): void;
26
53
  /** Copies important metadata from and identical event to another */
27
54
  static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
@@ -75,36 +102,39 @@ export declare class EventStore implements IEventStore {
75
102
  /** Creates an observable that emits when event is updated */
76
103
  updated(event: string | NostrEvent): Observable<NostrEvent>;
77
104
  /** Creates a {@link EventModel} */
78
- event(id: string): Observable<NostrEvent | undefined>;
105
+ event(pointer: string | EventPointer): Observable<NostrEvent | undefined>;
79
106
  /** Creates a {@link ReplaceableModel} */
107
+ replaceable(pointer: AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
80
108
  replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
109
+ /** Subscribe to an addressable event by pointer */
110
+ addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
81
111
  /** Creates a {@link TimelineModel} */
82
112
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
83
- /** Creates a {@link EventsModel} */
84
- events(ids: string[]): Observable<Record<string, NostrEvent>>;
85
- /** Creates a {@link ReplaceableSetModel} */
86
- replaceableSet(pointers: {
87
- kind: number;
88
- pubkey: string;
89
- identifier?: string;
90
- }[]): Observable<Record<string, NostrEvent>>;
91
- /** Creates a {@link ProfileModel} */
92
- profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
93
- /** Creates a {@link ContactsModel} */
94
- contacts(pubkey: string): Observable<import("nostr-tools/nip19").ProfilePointer[]>;
95
- /** Creates a {@link MuteModel} */
96
- mutes(pubkey: string): Observable<import("../helpers/mutes.js").Mutes | undefined>;
97
- /** Creates a {@link ReactionsModel} */
98
- reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
99
- /** Creates a {@link MailboxesModel} */
100
- mailboxes(pubkey: string): Observable<{
113
+ /** Subscribe to a users profile */
114
+ profile(user: string | ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
115
+ /** Subscribe to a users contacts */
116
+ contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
117
+ /** Subscribe to a users mutes */
118
+ mutes(user: string | ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
119
+ /** Subscribe to a users NIP-65 mailboxes */
120
+ mailboxes(user: string | ProfilePointer): Observable<{
101
121
  inboxes: string[];
102
122
  outboxes: string[];
103
123
  } | undefined>;
104
- /** Creates a {@link UserBlossomServersModel} */
105
- blossomServers(pubkey: string): Observable<URL[]>;
106
- /** Creates a {@link ThreadModel} */
124
+ /** Subscribe to a users blossom servers */
125
+ blossomServers(user: string | ProfilePointer): Observable<URL[]>;
126
+ /** Subscribe to an event's reactions */
127
+ reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
128
+ /** Subscribe to a thread */
107
129
  thread(root: string | EventPointer | AddressPointer): Observable<import("../models/thread.js").Thread>;
108
- /** Creates a {@link CommentsModel} */
130
+ /** Subscribe to a event's comments */
109
131
  comments(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
132
+ /** @deprecated use multiple {@link EventModel} instead */
133
+ events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
134
+ /** @deprecated use multiple {@link ReplaceableModel} instead */
135
+ replaceableSet(pointers: {
136
+ kind: number;
137
+ pubkey: string;
138
+ identifier?: string;
139
+ }[]): Observable<Record<string, NostrEvent | undefined>>;
110
140
  }
@@ -3,23 +3,28 @@ import { isAddressableKind } from "nostr-tools/kinds";
3
3
  import { EMPTY, filter, finalize, from, merge, mergeMap, ReplaySubject, share, take, timer } from "rxjs";
4
4
  import hash_sum from "hash-sum";
5
5
  import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
6
- import { EventStoreSymbol, FromCacheSymbol, getReplaceableAddress, isReplaceable } from "../helpers/event.js";
6
+ import { createReplaceableAddress, EventStoreSymbol, FromCacheSymbol, isReplaceable } from "../helpers/event.js";
7
+ import { getExpirationTimestamp } from "../helpers/expiration.js";
7
8
  import { matchFilters } from "../helpers/filter.js";
8
9
  import { parseCoordinate } from "../helpers/pointers.js";
9
10
  import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
11
+ import { unixNow } from "../helpers/time.js";
12
+ import { UserBlossomServersModel } from "../models/blossom.js";
10
13
  import { EventModel, EventsModel, ReplaceableModel, ReplaceableSetModel, TimelineModel } from "../models/common.js";
11
- import { EventSet } from "./event-set.js";
12
- import { ProfileModel } from "../models/profile.js";
13
14
  import { ContactsModel } from "../models/contacts.js";
15
+ import { CommentsModel, ThreadModel } from "../models/index.js";
16
+ import { MailboxesModel } from "../models/mailboxes.js";
14
17
  import { MuteModel } from "../models/mutes.js";
18
+ import { ProfileModel } from "../models/profile.js";
15
19
  import { ReactionsModel } from "../models/reactions.js";
16
- import { MailboxesModel } from "../models/mailboxes.js";
17
- import { UserBlossomServersModel } from "../models/blossom.js";
18
- import { CommentsModel, ThreadModel } from "../models/index.js";
20
+ import { EventSet } from "./event-set.js";
21
+ /** An extended {@link EventSet} that handles replaceable events, delets, and models */
19
22
  export class EventStore {
20
23
  database;
21
24
  /** Enable this to keep old versions of replaceable events */
22
25
  keepOldVersions = false;
26
+ /** Enable this to keep expired events */
27
+ keepExpired = false;
23
28
  /**
24
29
  * A method used to verify new events before added them
25
30
  * @returns true if the event is valid, false if it should be ignored
@@ -31,6 +36,21 @@ export class EventStore {
31
36
  update$;
32
37
  /** A stream of events that have been removed */
33
38
  remove$;
39
+ /**
40
+ * A method that will be called when an event isn't found in the store
41
+ * @experimental
42
+ */
43
+ eventLoader;
44
+ /**
45
+ * A method that will be called when a replaceable event isn't found in the store
46
+ * @experimental
47
+ */
48
+ replaceableLoader;
49
+ /**
50
+ * A method that will be called when an addressable event isn't found in the store
51
+ * @experimental
52
+ */
53
+ addressableLoader;
34
54
  constructor() {
35
55
  this.database = new EventSet();
36
56
  // verify events before they are added to the database
@@ -63,12 +83,53 @@ export class EventStore {
63
83
  if (this.deletedIds.has(event.id))
64
84
  return true;
65
85
  if (isAddressableKind(event.kind)) {
66
- const deleted = this.deletedCoords.get(getReplaceableAddress(event));
86
+ const identifier = event.tags.find((t) => t[0] === "d")?.[1];
87
+ const deleted = this.deletedCoords.get(createReplaceableAddress(event.kind, event.pubkey, identifier));
67
88
  if (deleted)
68
89
  return deleted > event.created_at;
69
90
  }
70
- return false;
71
91
  }
92
+ return false;
93
+ }
94
+ expirations = new Map();
95
+ /** Adds an event to the expiration map */
96
+ addExpiration(event) {
97
+ const expiration = getExpirationTimestamp(event);
98
+ if (expiration && Number.isFinite(expiration))
99
+ this.expirations.set(event.id, expiration);
100
+ }
101
+ expirationTimeout = null;
102
+ nextExpirationCheck = null;
103
+ handleExpiringEvent(event) {
104
+ const expiration = getExpirationTimestamp(event);
105
+ if (!expiration)
106
+ return;
107
+ // Add event to expiration map
108
+ this.expirations.set(event.id, expiration);
109
+ // Exit if the next check is already less than the next expiration
110
+ if (this.expirationTimeout && this.nextExpirationCheck && this.nextExpirationCheck < expiration)
111
+ return;
112
+ // Set timeout to prune expired events
113
+ if (this.expirationTimeout)
114
+ clearTimeout(this.expirationTimeout);
115
+ const timeout = expiration - unixNow();
116
+ this.expirationTimeout = setTimeout(this.pruneExpired.bind(this), timeout * 1000 + 10);
117
+ this.nextExpirationCheck = expiration;
118
+ }
119
+ /** Remove expired events from the store */
120
+ pruneExpired() {
121
+ const now = unixNow();
122
+ for (const [id, expiration] of this.expirations) {
123
+ if (expiration <= now) {
124
+ this.expirations.delete(id);
125
+ this.remove(id);
126
+ }
127
+ }
128
+ // Cleanup timers
129
+ if (this.expirationTimeout)
130
+ clearTimeout(this.expirationTimeout);
131
+ this.nextExpirationCheck = null;
132
+ this.expirationTimeout = null;
72
133
  }
73
134
  // handling delete events
74
135
  handleDeleteEvent(deleteEvent) {
@@ -117,11 +178,15 @@ export class EventStore {
117
178
  // Ignore if the event was deleted
118
179
  if (this.checkDeleted(event))
119
180
  return event;
181
+ // Reject expired events if keepExpired is false
182
+ const expiration = getExpirationTimestamp(event);
183
+ if (this.keepExpired === false && expiration && expiration <= unixNow())
184
+ return null;
120
185
  // Get the replaceable identifier
121
- const d = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
186
+ const identifier = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
122
187
  // Don't insert the event if there is already a newer version
123
188
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
124
- const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, d);
189
+ const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
125
190
  // If there is already a newer version, copy cached symbols and return existing event
126
191
  if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
127
192
  EventStore.mergeDuplicateEvent(event, existing[0]);
@@ -149,7 +214,7 @@ export class EventStore {
149
214
  addSeenRelay(inserted, fromRelay);
150
215
  // remove all old version of the replaceable event
151
216
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
152
- const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, d);
217
+ const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
153
218
  if (existing) {
154
219
  const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
155
220
  for (const old of older)
@@ -160,6 +225,9 @@ export class EventStore {
160
225
  return existing[0];
161
226
  }
162
227
  }
228
+ // Add event to expiration map
229
+ if (this.keepExpired === false && expiration)
230
+ this.handleExpiringEvent(inserted);
163
231
  return inserted;
164
232
  }
165
233
  /** Removes an event from the database and updates subscriptions */
@@ -284,55 +352,79 @@ export class EventStore {
284
352
  }
285
353
  // Helper methods for creating models
286
354
  /** Creates a {@link EventModel} */
287
- event(id) {
288
- return this.model(EventModel, id);
355
+ event(pointer) {
356
+ if (typeof pointer === "string")
357
+ pointer = { id: pointer };
358
+ return this.model(EventModel, pointer);
359
+ }
360
+ replaceable(...args) {
361
+ let pointer;
362
+ // Parse arguments
363
+ if (args.length === 1) {
364
+ pointer = args[0];
365
+ }
366
+ else if (args.length === 3 || args.length === 2) {
367
+ let [kind, pubkey, identifier] = args;
368
+ pointer = { kind, pubkey, identifier };
369
+ }
370
+ if (!pointer)
371
+ throw new Error("Invalid arguments, expected address pointer or kind, pubkey, identifier");
372
+ return this.model(ReplaceableModel, pointer);
289
373
  }
290
- /** Creates a {@link ReplaceableModel} */
291
- replaceable(kind, pubkey, identifier) {
292
- return this.model(ReplaceableModel, kind, pubkey, identifier);
374
+ /** Subscribe to an addressable event by pointer */
375
+ addressable(pointer) {
376
+ return this.model(ReplaceableModel, pointer);
293
377
  }
294
378
  /** Creates a {@link TimelineModel} */
295
379
  timeline(filters, includeOldVersion = false) {
296
380
  return this.model(TimelineModel, filters, includeOldVersion);
297
381
  }
298
- /** Creates a {@link EventsModel} */
299
- events(ids) {
300
- return this.model(EventsModel, ids);
301
- }
302
- /** Creates a {@link ReplaceableSetModel} */
303
- replaceableSet(pointers) {
304
- return this.model(ReplaceableSetModel, pointers);
305
- }
306
- /** Creates a {@link ProfileModel} */
307
- profile(pubkey) {
308
- return this.model(ProfileModel, pubkey);
309
- }
310
- /** Creates a {@link ContactsModel} */
311
- contacts(pubkey) {
312
- return this.model(ContactsModel, pubkey);
313
- }
314
- /** Creates a {@link MuteModel} */
315
- mutes(pubkey) {
316
- return this.model(MuteModel, pubkey);
317
- }
318
- /** Creates a {@link ReactionsModel} */
382
+ /** Subscribe to a users profile */
383
+ profile(user) {
384
+ return this.model(ProfileModel, user);
385
+ }
386
+ /** Subscribe to a users contacts */
387
+ contacts(user) {
388
+ if (typeof user === "string")
389
+ user = { pubkey: user };
390
+ return this.model(ContactsModel, user);
391
+ }
392
+ /** Subscribe to a users mutes */
393
+ mutes(user) {
394
+ if (typeof user === "string")
395
+ user = { pubkey: user };
396
+ return this.model(MuteModel, user);
397
+ }
398
+ /** Subscribe to a users NIP-65 mailboxes */
399
+ mailboxes(user) {
400
+ if (typeof user === "string")
401
+ user = { pubkey: user };
402
+ return this.model(MailboxesModel, user);
403
+ }
404
+ /** Subscribe to a users blossom servers */
405
+ blossomServers(user) {
406
+ if (typeof user === "string")
407
+ user = { pubkey: user };
408
+ return this.model(UserBlossomServersModel, user);
409
+ }
410
+ /** Subscribe to an event's reactions */
319
411
  reactions(event) {
320
412
  return this.model(ReactionsModel, event);
321
413
  }
322
- /** Creates a {@link MailboxesModel} */
323
- mailboxes(pubkey) {
324
- return this.model(MailboxesModel, pubkey);
325
- }
326
- /** Creates a {@link UserBlossomServersModel} */
327
- blossomServers(pubkey) {
328
- return this.model(UserBlossomServersModel, pubkey);
329
- }
330
- /** Creates a {@link ThreadModel} */
414
+ /** Subscribe to a thread */
331
415
  thread(root) {
332
416
  return this.model(ThreadModel, root);
333
417
  }
334
- /** Creates a {@link CommentsModel} */
418
+ /** Subscribe to a event's comments */
335
419
  comments(event) {
336
420
  return this.model(CommentsModel, event);
337
421
  }
422
+ /** @deprecated use multiple {@link EventModel} instead */
423
+ events(ids) {
424
+ return this.model(EventsModel, ids);
425
+ }
426
+ /** @deprecated use multiple {@link ReplaceableModel} instead */
427
+ replaceableSet(pointers) {
428
+ return this.model(ReplaceableSetModel, pointers);
429
+ }
338
430
  }
@@ -5,6 +5,7 @@ import { LRU } from "../helpers/lru.js";
5
5
  import { Mutes } from "../helpers/mutes.js";
6
6
  import { ProfileContent } from "../helpers/profile.js";
7
7
  import { Thread } from "../models/thread.js";
8
+ import { AddressPointerWithoutD } from "../helpers/pointers.js";
8
9
  /** The read interface for an event store */
9
10
  export interface IEventStoreRead {
10
11
  /** Check if the event store has an event with id */
@@ -51,27 +52,26 @@ export interface IEventClaims {
51
52
  /** Removes all claims on an event */
52
53
  clearClaim(event: NostrEvent): void;
53
54
  }
55
+ /** An event store that can be subscribed to */
56
+ export interface IEventStoreSubscriptions {
57
+ /** Susbscribe to an event by id */
58
+ event(id: string | EventPointer): Observable<NostrEvent | undefined>;
59
+ /** Subscribe to a replaceable event by pointer */
60
+ replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
61
+ /** Subscribe to an addressable event by pointer */
62
+ addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
63
+ /** Subscribe to a batch of events that match the filters */
64
+ filter(filters: Filter | Filter[]): Observable<NostrEvent[]>;
65
+ }
54
66
  /** Methods for creating common models */
55
67
  export interface IEventStoreModels {
68
+ model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args>, ...args: Args): Observable<T>;
56
69
  event(id: string): Observable<NostrEvent | undefined>;
57
- events(ids: string[]): Observable<Record<string, NostrEvent>>;
58
- replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
59
- replaceableSet(pointers: {
60
- kind: number;
61
- pubkey: string;
62
- identifier?: string;
63
- }[]): Observable<Record<string, NostrEvent>>;
70
+ replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
71
+ addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
64
72
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
65
- profile(pubkey: string): Observable<ProfileContent | undefined>;
66
- contacts(pubkey: string): Observable<ProfilePointer[]>;
67
- mutes(pubkey: string): Observable<Mutes | undefined>;
68
- reactions(event: NostrEvent): Observable<NostrEvent[]>;
69
- mailboxes(pubkey: string): Observable<{
70
- inboxes: string[];
71
- outboxes: string[];
72
- } | undefined>;
73
- blossomServers(pubkey: string): Observable<URL[]>;
74
- thread(root: string | EventPointer | AddressPointer): Observable<Thread>;
73
+ events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
74
+ replaceableSet(pointers: (AddressPointer | AddressPointerWithoutD)[]): Observable<Record<string, NostrEvent | undefined>>;
75
75
  }
76
76
  /** A computed view of an event set or event store */
77
77
  export type Model<T extends unknown> = (events: IEventStore) => Observable<T>;
@@ -84,8 +84,26 @@ export interface IEventSet extends IEventStoreRead, IEventStoreStreams, IEventSt
84
84
  events: LRU<NostrEvent>;
85
85
  }
86
86
  export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventStoreActions, IEventStoreModels, IEventClaims {
87
+ /** Enable this to keep old versions of replaceable events */
88
+ keepOldVersions: boolean;
89
+ /** Enable this to keep expired events */
90
+ keepExpired: boolean;
87
91
  filters(filters: Filter | Filter[]): Observable<NostrEvent>;
88
92
  updated(id: string | NostrEvent): Observable<NostrEvent>;
89
93
  removed(id: string): Observable<never>;
90
- model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args>, ...args: Args): Observable<T>;
94
+ replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
95
+ replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
96
+ eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
97
+ replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
98
+ addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
99
+ profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
100
+ contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
101
+ mutes(user: string | ProfilePointer): Observable<Mutes | undefined>;
102
+ mailboxes(user: string | ProfilePointer): Observable<{
103
+ inboxes: string[];
104
+ outboxes: string[];
105
+ } | undefined>;
106
+ blossomServers(user: string | ProfilePointer): Observable<URL[]>;
107
+ reactions(event: NostrEvent): Observable<NostrEvent[]>;
108
+ thread(root: string | EventPointer | AddressPointer): Observable<Thread>;
91
109
  }
@@ -0,0 +1,36 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { ProfilePointer } from "nostr-tools/nip19";
3
+ export declare const DATE_BASED_CALENDAR_EVENT_KIND = 31922;
4
+ export declare const TIME_BASED_CALENDAR_EVENT_KIND = 31923;
5
+ export type CalendarEventParticipant = ProfilePointer & {
6
+ role?: string;
7
+ };
8
+ export declare const CalendarEventLocationsSymbol: unique symbol;
9
+ export declare const CalendarEventParticipantsSymbol: unique symbol;
10
+ export declare const CalendarEventHashtagsSymbol: unique symbol;
11
+ export declare const CalendarEventReferencesSymbol: unique symbol;
12
+ export declare const CalendarEventGeohashSymbol: unique symbol;
13
+ /** Gets the title of a calendar event or calendar */
14
+ export declare function getCalendarEventTitle(event: NostrEvent): string | undefined;
15
+ /** Gets the summary of a calendar event */
16
+ export declare function getCalendarEventSummary(event: NostrEvent): string | undefined;
17
+ /** Gets the image URL of a calendar event */
18
+ export declare function getCalendarEventImage(event: NostrEvent): string | undefined;
19
+ /** Gets the start Unix timestamp of a calendar event */
20
+ export declare function getCalendarEventStart(event: NostrEvent): number | undefined;
21
+ /** Gets the timezone of the start timestamp of a calendar event */
22
+ export declare function getCalendarEventStartTimezone(event: NostrEvent): string | undefined;
23
+ /** Gets the timezone of the end timestamp of a calendar event */
24
+ export declare function getCalendarEventEndTimezone(event: NostrEvent): string | undefined;
25
+ /** Gets the end Unix timestamp of a calendar event */
26
+ export declare function getCalendarEventEnd(event: NostrEvent): number | undefined;
27
+ /** Gets all locations from a calendar event */
28
+ export declare function getCalendarEventLocations(event: NostrEvent): string[];
29
+ /** Gets the geohash of a calendar event */
30
+ export declare function getCalendarEventGeohash(event: NostrEvent): string | undefined;
31
+ /** Gets all participants from a calendar event */
32
+ export declare function getCalendarEventParticipants(event: NostrEvent): CalendarEventParticipant[];
33
+ /** Gets all hashtags from a calendar event */
34
+ export declare function getCalendarEventHashtags(event: NostrEvent): string[];
35
+ /** Gets all references from a calendar event */
36
+ export declare function getCalendarEventReferences(event: NostrEvent): string[];
@@ -0,0 +1,110 @@
1
+ import { getOrComputeCachedValue } from "./cache.js";
2
+ import { getTagValue } from "./event-tags.js";
3
+ import { getProfilePointerFromPTag } from "./pointers.js";
4
+ import { isPTag, isRTag, isTTag } from "./tags.js";
5
+ // NIP-52 Calendar Event Kinds
6
+ export const DATE_BASED_CALENDAR_EVENT_KIND = 31922;
7
+ export const TIME_BASED_CALENDAR_EVENT_KIND = 31923;
8
+ // Cache symbols for complex operations only
9
+ export const CalendarEventLocationsSymbol = Symbol.for("calendar-event-locations");
10
+ export const CalendarEventParticipantsSymbol = Symbol.for("calendar-event-participants");
11
+ export const CalendarEventHashtagsSymbol = Symbol.for("calendar-event-hashtags");
12
+ export const CalendarEventReferencesSymbol = Symbol.for("calendar-event-references");
13
+ export const CalendarEventGeohashSymbol = Symbol.for("calendar-event-geohash");
14
+ /** Gets the title of a calendar event or calendar */
15
+ export function getCalendarEventTitle(event) {
16
+ return getTagValue(event, "title") || getTagValue(event, "name"); // fallback to deprecated "name" tag
17
+ }
18
+ /** Gets the summary of a calendar event */
19
+ export function getCalendarEventSummary(event) {
20
+ return getTagValue(event, "summary");
21
+ }
22
+ /** Gets the image URL of a calendar event */
23
+ export function getCalendarEventImage(event) {
24
+ return getTagValue(event, "image");
25
+ }
26
+ /** Gets the start Unix timestamp of a calendar event */
27
+ export function getCalendarEventStart(event) {
28
+ const value = getTagValue(event, "start");
29
+ if (!value)
30
+ return undefined;
31
+ if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
32
+ return new Date(value).valueOf() / 1000;
33
+ else if (event.kind === TIME_BASED_CALENDAR_EVENT_KIND)
34
+ return parseInt(value);
35
+ else
36
+ return undefined;
37
+ }
38
+ /** Gets the timezone of the start timestamp of a calendar event */
39
+ export function getCalendarEventStartTimezone(event) {
40
+ if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
41
+ return undefined;
42
+ return getTagValue(event, "start_tzid");
43
+ }
44
+ /** Gets the timezone of the end timestamp of a calendar event */
45
+ export function getCalendarEventEndTimezone(event) {
46
+ if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
47
+ return undefined;
48
+ return getTagValue(event, "end_tzid");
49
+ }
50
+ /** Gets the end Unix timestamp of a calendar event */
51
+ export function getCalendarEventEnd(event) {
52
+ const value = getTagValue(event, "end");
53
+ if (!value)
54
+ return undefined;
55
+ if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
56
+ return new Date(value).valueOf() / 1000;
57
+ else if (event.kind === TIME_BASED_CALENDAR_EVENT_KIND)
58
+ return parseInt(value);
59
+ else
60
+ return undefined;
61
+ }
62
+ /** Gets all locations from a calendar event */
63
+ export function getCalendarEventLocations(event) {
64
+ if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
65
+ throw new Error("Event is not a date-based or time-based calendar event");
66
+ return getOrComputeCachedValue(event, CalendarEventLocationsSymbol, () => {
67
+ return event.tags.filter((t) => t[0] === "location" && t[1]).map((t) => t[1]);
68
+ });
69
+ }
70
+ /** Gets the geohash of a calendar event */
71
+ export function getCalendarEventGeohash(event) {
72
+ if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
73
+ throw new Error("Event is not a date-based or time-based calendar event");
74
+ return getOrComputeCachedValue(event, CalendarEventGeohashSymbol, () => {
75
+ let hash = undefined;
76
+ for (const tag of event.tags) {
77
+ if (tag[0] === "g" && tag[1] && (!hash || tag[1].length > hash.length))
78
+ hash = tag[1];
79
+ }
80
+ return hash;
81
+ });
82
+ }
83
+ /** Gets all participants from a calendar event */
84
+ export function getCalendarEventParticipants(event) {
85
+ if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
86
+ throw new Error("Event is not a date-based or time-based calendar event");
87
+ return getOrComputeCachedValue(event, CalendarEventParticipantsSymbol, () => {
88
+ return event.tags.filter(isPTag).map((tag) => ({
89
+ ...getProfilePointerFromPTag(tag),
90
+ // Third index of tag is optional "role"
91
+ role: tag[3] || undefined,
92
+ }));
93
+ });
94
+ }
95
+ /** Gets all hashtags from a calendar event */
96
+ export function getCalendarEventHashtags(event) {
97
+ if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
98
+ throw new Error("Event is not a date-based or time-based calendar event");
99
+ return getOrComputeCachedValue(event, CalendarEventHashtagsSymbol, () => {
100
+ return event.tags.filter(isTTag).map((t) => t[1]);
101
+ });
102
+ }
103
+ /** Gets all references from a calendar event */
104
+ export function getCalendarEventReferences(event) {
105
+ if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
106
+ throw new Error("Event is not a date-based or time-based calendar event");
107
+ return getOrComputeCachedValue(event, CalendarEventReferencesSymbol, () => {
108
+ return event.tags.filter(isRTag).map((t) => t[1]);
109
+ });
110
+ }
@@ -0,0 +1,15 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
3
+ export declare const CALENDAR_EVENT_RSVP_KIND = 31925;
4
+ export type RSVPStatus = "accepted" | "declined" | "tentative";
5
+ export type RSVPFreeBusy = "free" | "busy";
6
+ /** Gets the RSVP status from a calendar event RSVP */
7
+ export declare function getRSVPStatus(event: NostrEvent): RSVPStatus | undefined;
8
+ /** Gets the free/busy status from a calendar event RSVP (will be undefined if the RSVP is declined) */
9
+ export declare function getRSVPFreeBusy(event: NostrEvent): RSVPFreeBusy | undefined;
10
+ /** Gets the referenced calendar event coordinate that the RSVP is responding to */
11
+ export declare function getRSVPAddressPointer(event: NostrEvent): AddressPointer | undefined;
12
+ /** Gets the referenced calendar event pointer that the RSVP is responding to */
13
+ export declare function getRSVPEventPointer(event: NostrEvent): EventPointer | undefined;
14
+ /** Gets the profile pointer that the RSVP is responding to */
15
+ export declare function getRSVPProfilePointer(event: NostrEvent): ProfilePointer | undefined;