applesauce-core 0.0.0-next-20250729145125 → 0.0.0-next-20250808173123

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.
@@ -9,6 +9,8 @@ export declare class EventStore implements IEventStore {
9
9
  database: EventSet;
10
10
  /** Enable this to keep old versions of replaceable events */
11
11
  keepOldVersions: boolean;
12
+ /** Enable this to keep expired events */
13
+ keepExpired: boolean;
12
14
  /**
13
15
  * A method used to verify new events before added them
14
16
  * @returns true if the event is valid, false if it should be ignored
@@ -24,21 +26,29 @@ export declare class EventStore implements IEventStore {
24
26
  * A method that will be called when an event isn't found in the store
25
27
  * @experimental
26
28
  */
27
- eventLoader?: (pointer: EventPointer) => Observable<NostrEvent>;
29
+ eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
28
30
  /**
29
31
  * A method that will be called when a replaceable event isn't found in the store
30
32
  * @experimental
31
33
  */
32
- replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent>;
34
+ replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
33
35
  /**
34
36
  * A method that will be called when an addressable event isn't found in the store
35
37
  * @experimental
36
38
  */
37
- addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent>;
39
+ addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
38
40
  constructor();
39
41
  protected deletedIds: Set<string>;
40
42
  protected deletedCoords: Map<string, number>;
41
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;
42
52
  protected handleDeleteEvent(deleteEvent: NostrEvent): void;
43
53
  /** Copies important metadata from and identical event to another */
44
54
  static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
@@ -4,9 +4,11 @@ import { EMPTY, filter, finalize, from, merge, mergeMap, ReplaySubject, share, t
4
4
  import hash_sum from "hash-sum";
5
5
  import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
6
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";
10
12
  import { UserBlossomServersModel } from "../models/blossom.js";
11
13
  import { EventModel, EventsModel, ReplaceableModel, ReplaceableSetModel, TimelineModel } from "../models/common.js";
12
14
  import { ContactsModel } from "../models/contacts.js";
@@ -21,6 +23,8 @@ export class EventStore {
21
23
  database;
22
24
  /** Enable this to keep old versions of replaceable events */
23
25
  keepOldVersions = false;
26
+ /** Enable this to keep expired events */
27
+ keepExpired = false;
24
28
  /**
25
29
  * A method used to verify new events before added them
26
30
  * @returns true if the event is valid, false if it should be ignored
@@ -87,6 +91,46 @@ export class EventStore {
87
91
  }
88
92
  return false;
89
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;
133
+ }
90
134
  // handling delete events
91
135
  handleDeleteEvent(deleteEvent) {
92
136
  const ids = getDeleteIds(deleteEvent);
@@ -134,6 +178,10 @@ export class EventStore {
134
178
  // Ignore if the event was deleted
135
179
  if (this.checkDeleted(event))
136
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;
137
185
  // Get the replaceable identifier
138
186
  const identifier = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
139
187
  // Don't insert the event if there is already a newer version
@@ -177,6 +225,9 @@ export class EventStore {
177
225
  return existing[0];
178
226
  }
179
227
  }
228
+ // Add event to expiration map
229
+ if (this.keepExpired === false && expiration)
230
+ this.handleExpiringEvent(inserted);
180
231
  return inserted;
181
232
  }
182
233
  /** Removes an event from the database and updates subscriptions */
@@ -84,14 +84,18 @@ 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
94
  replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
91
95
  replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
92
- eventLoader?: (pointer: EventPointer) => Observable<NostrEvent>;
93
- replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent>;
94
- addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent>;
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>;
95
99
  profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
96
100
  contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
97
101
  mutes(user: string | ProfilePointer): Observable<Mutes | undefined>;
@@ -10,18 +10,24 @@ export interface EncryptedContentSigner {
10
10
  decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
11
11
  };
12
12
  }
13
- /** Various event kinds that can have encrypted content and which encryption method they use */
14
- export declare const EventContentEncryptionMethod: Record<number, "nip04" | "nip44">;
15
- /** Sets the encryption method that is used for the contents of a specific event kind */
16
- export declare function setEncryptedContentEncryptionMethod(kind: number, method: "nip04" | "nip44"): number;
17
- /** Returns either nip04 or nip44 encryption methods depending on event kind */
18
- export declare function getEncryptedContentEncryptionMethods(kind: number, signer: EncryptedContentSigner): {
13
+ export type EncryptionMethod = "nip04" | "nip44";
14
+ /** A pair of encryption methods for encrypting and decrypting event content */
15
+ export interface EncryptionMethods {
19
16
  encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
20
17
  decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
21
- } | {
22
- encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
23
- decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
24
- };
18
+ }
19
+ /** Various event kinds that can have encrypted content and which encryption method they use */
20
+ export declare const EventContentEncryptionMethod: Record<number, EncryptionMethod>;
21
+ /** Sets the encryption method that is used for the contents of a specific event kind */
22
+ export declare function setEncryptedContentEncryptionMethod(kind: number, method: EncryptionMethod): number;
23
+ /**
24
+ * Returns either nip04 or nip44 encryption methods depending on event kind
25
+ * @param kind The event kind to get the encryption method for
26
+ * @param signer The signer to use to get the encryption methods
27
+ * @param override The encryption method to use instead of the default
28
+ * @returns The encryption methods for the event kind
29
+ */
30
+ export declare function getEncryptedContentEncryptionMethods(kind: number, signer: EncryptedContentSigner, override?: EncryptionMethod): EncryptionMethods;
25
31
  /** Checks if an event can have encrypted content */
26
32
  export declare function canHaveEncryptedContent(kind: number): boolean;
27
33
  /** Checks if an event has encrypted content */
@@ -13,9 +13,15 @@ export function setEncryptedContentEncryptionMethod(kind, method) {
13
13
  EventContentEncryptionMethod[kind] = method;
14
14
  return kind;
15
15
  }
16
- /** Returns either nip04 or nip44 encryption methods depending on event kind */
17
- export function getEncryptedContentEncryptionMethods(kind, signer) {
18
- const method = EventContentEncryptionMethod[kind];
16
+ /**
17
+ * Returns either nip04 or nip44 encryption methods depending on event kind
18
+ * @param kind The event kind to get the encryption method for
19
+ * @param signer The signer to use to get the encryption methods
20
+ * @param override The encryption method to use instead of the default
21
+ * @returns The encryption methods for the event kind
22
+ */
23
+ export function getEncryptedContentEncryptionMethods(kind, signer, override) {
24
+ const method = override ?? EventContentEncryptionMethod[kind];
19
25
  if (!method)
20
26
  throw new Error(`Event kind ${kind} does not support encrypted content`);
21
27
  const encryption = signer[method];
@@ -0,0 +1,15 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { IEventStoreStreams } from "../event-store/interface.js";
3
+ /**
4
+ * Setups a process to write batches of new events from an event store to a cache
5
+ * @param eventStore - The event store to read from
6
+ * @param write - The function to write the events to the cache
7
+ * @param opts - The options for the process
8
+ * @param opts.batchTime - The time to wait before writing a batch (default: 5 seconds)
9
+ * @param opts.maxBatchSize - The maximum number of events to write in a batch
10
+ * @returns A function to stop the process
11
+ */
12
+ export declare function presistEventsToCache(eventStore: IEventStoreStreams, write: (events: NostrEvent[]) => Promise<void>, opts?: {
13
+ maxBatchSize?: number;
14
+ batchTime?: number;
15
+ }): () => void;
@@ -0,0 +1,32 @@
1
+ import { bufferTime, filter } from "rxjs";
2
+ import { logger } from "../logger.js";
3
+ import { isFromCache } from "./index.js";
4
+ const log = logger.extend("event-cache");
5
+ /**
6
+ * Setups a process to write batches of new events from an event store to a cache
7
+ * @param eventStore - The event store to read from
8
+ * @param write - The function to write the events to the cache
9
+ * @param opts - The options for the process
10
+ * @param opts.batchTime - The time to wait before writing a batch (default: 5 seconds)
11
+ * @param opts.maxBatchSize - The maximum number of events to write in a batch
12
+ * @returns A function to stop the process
13
+ */
14
+ export function presistEventsToCache(eventStore, write, opts) {
15
+ const time = opts?.batchTime ?? 5_000;
16
+ // Save all new events to the cache
17
+ const sub = eventStore.insert$
18
+ .pipe(
19
+ // Only select events that are not from the cache
20
+ filter((e) => !isFromCache(e)),
21
+ // Buffer events for 5 seconds
22
+ opts?.maxBatchSize ? bufferTime(time, undefined, opts?.maxBatchSize ?? 100) : bufferTime(time),
23
+ // Only select buffers with events
24
+ filter((b) => b.length > 0))
25
+ .subscribe((events) => {
26
+ // Save all new events to the cache
27
+ write(events)
28
+ .then(() => log(`Saved ${events.length} events to cache`))
29
+ .catch((e) => log(`Failed to save ${events.length} events to cache`, e));
30
+ });
31
+ return () => sub.unsubscribe();
32
+ }
@@ -1,11 +1,10 @@
1
1
  import { getOrComputeCachedValue } from "./cache.js";
2
2
  import { unixNow } from "./time.js";
3
- import { getTagValue } from "./event-tags.js";
4
3
  export const ExpirationTimestampSymbol = Symbol("expiration-timestamp");
5
4
  /** Returns the NIP-40 expiration timestamp for an event */
6
5
  export function getExpirationTimestamp(event) {
7
6
  return getOrComputeCachedValue(event, ExpirationTimestampSymbol, () => {
8
- const expiration = getTagValue(event, "expiration");
7
+ const expiration = event.tags.find((t) => t[0] === "expiration")?.[1];
9
8
  return expiration ? parseInt(expiration) : undefined;
10
9
  });
11
10
  }
@@ -1,4 +1,4 @@
1
- import { EncryptedContentSigner, getEncryptedContentEncryptionMethods } from "./encrypted-content.js";
1
+ import { EncryptedContentSigner, EncryptionMethod, getEncryptedContentEncryptionMethods } from "./encrypted-content.js";
2
2
  export declare const HiddenContentSymbol: symbol;
3
3
  export interface HiddenContentSigner extends EncryptedContentSigner {
4
4
  }
@@ -6,7 +6,7 @@ export declare const getHiddenContentEncryptionMethods: typeof getEncryptedConte
6
6
  /** Various event kinds that can have hidden content */
7
7
  export declare const HiddenContentKinds: Set<number>;
8
8
  /** Sets the encryption method for hidden content on a kind */
9
- export declare function setHiddenContentEncryptionMethod(kind: number, method: "nip04" | "nip44"): number;
9
+ export declare function setHiddenContentEncryptionMethod(kind: number, method: EncryptionMethod): number;
10
10
  /** Checks if an event can have hidden content */
11
11
  export declare function canHaveHiddenContent(kind: number): boolean;
12
12
  /** Checks if an event has hidden content */
@@ -31,7 +31,7 @@ export declare function unlockHiddenContent<T extends {
31
31
  kind: number;
32
32
  pubkey: string;
33
33
  content: string;
34
- }>(event: T, signer: EncryptedContentSigner): Promise<string>;
34
+ }>(event: T, signer: EncryptedContentSigner, override?: EncryptionMethod): Promise<string>;
35
35
  /**
36
36
  * Sets the hidden content on an event and updates it if its part of an event store
37
37
  * @throws
@@ -34,10 +34,10 @@ export function getHiddenContent(event) {
34
34
  * @param signer A signer to use to decrypt the content
35
35
  * @throws
36
36
  */
37
- export async function unlockHiddenContent(event, signer) {
37
+ export async function unlockHiddenContent(event, signer, override) {
38
38
  if (!canHaveHiddenContent(event.kind))
39
39
  throw new Error("Event kind does not support hidden content");
40
- const encryption = getEncryptedContentEncryptionMethods(event.kind, signer);
40
+ const encryption = getEncryptedContentEncryptionMethods(event.kind, signer, override);
41
41
  const plaintext = await encryption.decrypt(event.pubkey, event.content);
42
42
  setHiddenContentCache(event, plaintext);
43
43
  return plaintext;
@@ -1,11 +1,12 @@
1
1
  import { HiddenContentSigner } from "./hidden-content.js";
2
+ import { EncryptionMethod } from "./encrypted-content.js";
2
3
  export declare const HiddenTagsSymbol: unique symbol;
3
4
  /** Various event kinds that can have hidden tags */
4
5
  export declare const HiddenTagsKinds: Set<number>;
5
6
  /** Checks if an event can have hidden tags */
6
7
  export declare function canHaveHiddenTags(kind: number): boolean;
7
8
  /** Sets the type of encryption to use for hidden tags on a kind */
8
- export declare function setHiddenTagsEncryptionMethod(kind: number, method: "nip04" | "nip44"): number;
9
+ export declare function setHiddenTagsEncryptionMethod(kind: number, method: EncryptionMethod): number;
9
10
  /** Checks if an event has hidden tags */
10
11
  export declare function hasHiddenTags<T extends {
11
12
  kind: number;
@@ -19,25 +20,19 @@ export declare function getHiddenTags<T extends {
19
20
  /** Checks if the hidden tags are locked */
20
21
  export declare function isHiddenTagsLocked<T extends object>(event: T): boolean;
21
22
  /** Returns either nip04 or nip44 encryption method depending on list kind */
22
- export declare function getHiddenTagsEncryptionMethods(kind: number, signer: HiddenContentSigner): {
23
- encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
24
- decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
25
- } | {
26
- encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
27
- decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
28
- };
23
+ export declare function getHiddenTagsEncryptionMethods(kind: number, signer: HiddenContentSigner): import("./encrypted-content.js").EncryptionMethods;
29
24
  /**
30
25
  * Decrypts the private list
31
26
  * @param event The list event to decrypt
32
27
  * @param signer A signer to use to decrypt the tags
33
- * @param store An optional EventStore to notify about the update
28
+ * @param override The encryption method to use instead of the default
34
29
  * @throws
35
30
  */
36
31
  export declare function unlockHiddenTags<T extends {
37
32
  kind: number;
38
33
  pubkey: string;
39
34
  content: string;
40
- }>(event: T, signer: HiddenContentSigner): Promise<string[][]>;
35
+ }>(event: T, signer: HiddenContentSigner, override?: EncryptionMethod): Promise<string[][]>;
41
36
  /**
42
37
  * Sets the hidden tags on an event and updates it if its part of an event store
43
38
  * @throws
@@ -58,15 +58,15 @@ export function getHiddenTagsEncryptionMethods(kind, signer) {
58
58
  * Decrypts the private list
59
59
  * @param event The list event to decrypt
60
60
  * @param signer A signer to use to decrypt the tags
61
- * @param store An optional EventStore to notify about the update
61
+ * @param override The encryption method to use instead of the default
62
62
  * @throws
63
63
  */
64
- export async function unlockHiddenTags(event, signer) {
64
+ export async function unlockHiddenTags(event, signer, override) {
65
65
  if (!canHaveHiddenTags(event.kind))
66
66
  throw new Error("Event kind does not support hidden tags");
67
67
  // unlock hidden content is needed
68
68
  if (isHiddenContentLocked(event))
69
- await unlockHiddenContent(event, signer);
69
+ await unlockHiddenContent(event, signer, override);
70
70
  return getHiddenTags(event);
71
71
  }
72
72
  /**
@@ -17,6 +17,7 @@ export * from "./emoji.js";
17
17
  export * from "./encrypted-content-cache.js";
18
18
  export * from "./encrypted-content.js";
19
19
  export * from "./encryption.js";
20
+ export * from "./event-cache.js";
20
21
  export * from "./event-tags.js";
21
22
  export * from "./event.js";
22
23
  export * from "./expiration.js";
@@ -17,6 +17,7 @@ export * from "./emoji.js";
17
17
  export * from "./encrypted-content-cache.js";
18
18
  export * from "./encrypted-content.js";
19
19
  export * from "./encryption.js";
20
+ export * from "./event-cache.js";
20
21
  export * from "./event-tags.js";
21
22
  export * from "./event.js";
22
23
  export * from "./expiration.js";
@@ -1,4 +1,4 @@
1
- import { combineLatest, defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, map, merge, mergeWith, of, repeat, scan, takeUntil, tap, } from "rxjs";
1
+ import { combineLatest, defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, from, map, merge, mergeWith, of, repeat, scan, takeUntil, tap, } from "rxjs";
2
2
  import { insertEventIntoDescendingList } from "nostr-tools/utils";
3
3
  import { createReplaceableAddress, getEventUID, getReplaceableIdentifier, isReplaceable, matchFilters, } from "../helpers/index.js";
4
4
  import { claimEvents } from "../observable/claim-events.js";
@@ -15,7 +15,9 @@ export function EventModel(pointer) {
15
15
  if (event)
16
16
  return of(event);
17
17
  // If there is a loader, use it to get the event
18
- return events.eventLoader?.(pointer) ?? EMPTY;
18
+ if (!events.eventLoader)
19
+ return EMPTY;
20
+ return from(events.eventLoader(pointer)).pipe(filter((e) => !!e));
19
21
  }),
20
22
  // Listen for new events
21
23
  events.insert$.pipe(filter((e) => e.id === pointer.id)),
@@ -38,10 +40,16 @@ export function ReplaceableModel(pointer) {
38
40
  let event = events.getReplaceable(pointer.kind, pointer.pubkey, pointer.identifier);
39
41
  if (event)
40
42
  return of(event);
41
- else if (pointer.identifier !== undefined)
42
- return events.addressableLoader?.(pointer) ?? EMPTY;
43
- else
44
- return events.replaceableLoader?.(pointer) ?? EMPTY;
43
+ else if (pointer.identifier !== undefined) {
44
+ if (!events.addressableLoader)
45
+ return EMPTY;
46
+ return from(events.addressableLoader(pointer)).pipe(filter((e) => !!e));
47
+ }
48
+ else {
49
+ if (!events.replaceableLoader)
50
+ return EMPTY;
51
+ return from(events.replaceableLoader(pointer)).pipe(filter((e) => !!e));
52
+ }
45
53
  }),
46
54
  // subscribe to new events
47
55
  events.insert$.pipe(filter((e) => e.pubkey == pointer.pubkey &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20250729145125",
3
+ "version": "0.0.0-next-20250808173123",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",