applesauce-core 0.0.0-next-20250729145125 → 0.0.0-next-20250806165639

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>;
@@ -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
  }
@@ -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-20250806165639",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",