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.
- package/dist/event-store/event-store.d.ts +13 -3
- package/dist/event-store/event-store.js +51 -0
- package/dist/event-store/interface.d.ts +7 -3
- package/dist/helpers/event-cache.d.ts +15 -0
- package/dist/helpers/event-cache.js +32 -0
- package/dist/helpers/expiration.js +1 -2
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/models/common.js +14 -6
- package/package.json +1 -1
|
@@ -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 =
|
|
7
|
+
const expiration = event.tags.find((t) => t[0] === "expiration")?.[1];
|
|
9
8
|
return expiration ? parseInt(expiration) : undefined;
|
|
10
9
|
});
|
|
11
10
|
}
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/helpers/index.js
CHANGED
|
@@ -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";
|
package/dist/models/common.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
return events.
|
|
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 &&
|