applesauce-core 6.1.0 → 6.2.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.
- package/dist/casts/pubkey.d.ts +9 -3
- package/dist/casts/pubkey.js +17 -3
- package/dist/casts/user.d.ts +2 -2
- package/dist/casts/user.js +1 -1
- package/dist/event-store/async-event-store.d.ts +10 -0
- package/dist/event-store/async-event-store.js +33 -5
- package/dist/event-store/event-memory.js +31 -7
- package/dist/event-store/event-models.d.ts +11 -1
- package/dist/event-store/event-models.js +20 -4
- package/dist/event-store/event-store.d.ts +10 -0
- package/dist/event-store/event-store.js +31 -3
- package/dist/event-store/expiration-manager.d.ts +7 -0
- package/dist/event-store/expiration-manager.js +16 -0
- package/dist/event-store/interface.d.ts +2 -0
- package/dist/models/base.js +25 -11
- package/package.json +1 -1
package/dist/casts/pubkey.d.ts
CHANGED
|
@@ -4,11 +4,15 @@ import type { ProfilePointer } from "../helpers/pointers.js";
|
|
|
4
4
|
import { ChainableObservable } from "../observable/chainable.js";
|
|
5
5
|
import { CastRefEventStore } from "./cast.js";
|
|
6
6
|
export type PubkeyCastConstructor<C extends PubkeyCast> = (new (pointer: ProfilePointer, store: CastRefEventStore) => C) & {
|
|
7
|
-
cache
|
|
7
|
+
cache?: Map<string, WeakRef<C>>;
|
|
8
|
+
cacheRegistry?: FinalizationRegistry<string>;
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
11
|
* Cast a pubkey to a specific class instance.
|
|
11
12
|
* Works like {@link castUser} - returns a cached singleton per pubkey+relay-hints combination.
|
|
13
|
+
*
|
|
14
|
+
* @note Instances are held weakly so unused casts can be garbage collected instead of
|
|
15
|
+
* accumulating one instance per pubkey for the lifetime of the process.
|
|
12
16
|
*/
|
|
13
17
|
export declare function castPubkey<C extends PubkeyCast>(pubkey: string | NostrEvent | ProfilePointer, cls: PubkeyCastConstructor<C>, store: CastRefEventStore): C;
|
|
14
18
|
/** Base class for pubkey-based casts (analogous to {@link EventCast} for events) */
|
|
@@ -16,8 +20,10 @@ export declare class PubkeyCast {
|
|
|
16
20
|
#private;
|
|
17
21
|
readonly pointer: ProfilePointer;
|
|
18
22
|
readonly store: CastRefEventStore;
|
|
19
|
-
/** A global cache of pubkey -> instance, populated by {@link castPubkey} */
|
|
20
|
-
static cache: Map<string, PubkeyCast
|
|
23
|
+
/** A global cache of pubkey -> weak instance reference, populated by {@link castPubkey} */
|
|
24
|
+
static cache: Map<string, WeakRef<PubkeyCast>>;
|
|
25
|
+
/** Cleans up dead {@link cache} entries when their instances are garbage collected */
|
|
26
|
+
static cacheRegistry?: FinalizationRegistry<string>;
|
|
21
27
|
constructor(pointer: ProfilePointer, store: CastRefEventStore);
|
|
22
28
|
/** The hex pubkey represented by this cast */
|
|
23
29
|
get pubkey(): string;
|
package/dist/casts/pubkey.js
CHANGED
|
@@ -4,6 +4,9 @@ import { chainable } from "../observable/chainable.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Cast a pubkey to a specific class instance.
|
|
6
6
|
* Works like {@link castUser} - returns a cached singleton per pubkey+relay-hints combination.
|
|
7
|
+
*
|
|
8
|
+
* @note Instances are held weakly so unused casts can be garbage collected instead of
|
|
9
|
+
* accumulating one instance per pubkey for the lifetime of the process.
|
|
7
10
|
*/
|
|
8
11
|
export function castPubkey(pubkey, cls, store) {
|
|
9
12
|
if (isEvent(pubkey))
|
|
@@ -14,19 +17,30 @@ export function castPubkey(pubkey, cls, store) {
|
|
|
14
17
|
const cacheKey = pointer.relays?.length ? `${pointer.pubkey}:${JSON.stringify(pointer.relays)}` : pointer.pubkey;
|
|
15
18
|
if (!cls.cache)
|
|
16
19
|
cls.cache = new Map();
|
|
17
|
-
|
|
20
|
+
// Clean up dead cache entries when their instances are garbage collected
|
|
21
|
+
if (!cls.cacheRegistry)
|
|
22
|
+
cls.cacheRegistry = new FinalizationRegistry((key) => {
|
|
23
|
+
// Only drop the entry if it still points at a collected instance; a re-created
|
|
24
|
+
// instance under the same key has a live ref and must be kept.
|
|
25
|
+
if (cls.cache.get(key)?.deref() === undefined)
|
|
26
|
+
cls.cache.delete(key);
|
|
27
|
+
});
|
|
28
|
+
const existing = cls.cache.get(cacheKey)?.deref();
|
|
18
29
|
if (existing)
|
|
19
30
|
return existing;
|
|
20
31
|
const instance = new cls(pointer, store);
|
|
21
|
-
cls.cache.set(cacheKey, instance);
|
|
32
|
+
cls.cache.set(cacheKey, new WeakRef(instance));
|
|
33
|
+
cls.cacheRegistry.register(instance, cacheKey);
|
|
22
34
|
return instance;
|
|
23
35
|
}
|
|
24
36
|
/** Base class for pubkey-based casts (analogous to {@link EventCast} for events) */
|
|
25
37
|
export class PubkeyCast {
|
|
26
38
|
pointer;
|
|
27
39
|
store;
|
|
28
|
-
/** A global cache of pubkey -> instance, populated by {@link castPubkey} */
|
|
40
|
+
/** A global cache of pubkey -> weak instance reference, populated by {@link castPubkey} */
|
|
29
41
|
static cache = new Map();
|
|
42
|
+
/** Cleans up dead {@link cache} entries when their instances are garbage collected */
|
|
43
|
+
static cacheRegistry;
|
|
30
44
|
constructor(pointer, store) {
|
|
31
45
|
this.pointer = pointer;
|
|
32
46
|
this.store = store;
|
package/dist/casts/user.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ export declare function castUser(event: NostrEvent, store: CastRefEventStore): U
|
|
|
11
11
|
export declare function castUser(user: string | ProfilePointer, store: CastRefEventStore): User;
|
|
12
12
|
/** A class representing a Nostr user */
|
|
13
13
|
export declare class User extends PubkeyCast {
|
|
14
|
-
/** A global cache of pubkey -> {@link User} */
|
|
15
|
-
static cache: Map<string, User
|
|
14
|
+
/** A global cache of pubkey -> weak {@link User} reference */
|
|
15
|
+
static cache: Map<string, WeakRef<User>>;
|
|
16
16
|
/** Returns the NIP-19 npub for this user */
|
|
17
17
|
get npub(): `npub1${string}`;
|
|
18
18
|
/** Returns the NIP-19 nprofile for this user */
|
package/dist/casts/user.js
CHANGED
|
@@ -8,7 +8,7 @@ export function castUser(user, store) {
|
|
|
8
8
|
}
|
|
9
9
|
/** A class representing a Nostr user */
|
|
10
10
|
export class User extends PubkeyCast {
|
|
11
|
-
/** A global cache of pubkey -> {@link User} */
|
|
11
|
+
/** A global cache of pubkey -> weak {@link User} reference */
|
|
12
12
|
static cache = new Map();
|
|
13
13
|
/** Returns the NIP-19 npub for this user */
|
|
14
14
|
get npub() {
|
|
@@ -52,6 +52,8 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
|
|
|
52
52
|
* A method that will be called when an event isn't found in the store
|
|
53
53
|
*/
|
|
54
54
|
eventLoader?: (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
55
|
+
/** Internal subscriptions (delete + expiration managers) torn down on dispose */
|
|
56
|
+
private internalSubscriptions;
|
|
55
57
|
constructor(options: AsyncEventStoreOptions);
|
|
56
58
|
/** A method to add all events to memory to ensure there is only ever a single instance of an event */
|
|
57
59
|
private mapToMemory;
|
|
@@ -98,4 +100,12 @@ export declare class AsyncEventStore extends EventModels implements IAsyncEventS
|
|
|
98
100
|
unclaimed(): Generator<NostrEvent>;
|
|
99
101
|
/** Removes any event that is not being used by a subscription */
|
|
100
102
|
prune(limit?: number): number;
|
|
103
|
+
/**
|
|
104
|
+
* Tears down the store: disposes the attached event loader, completes the event streams, releases
|
|
105
|
+
* model keep-warm timers, unsubscribes internal manager listeners, and cancels any pending expiration timer.
|
|
106
|
+
* @note This is a terminal operation; the store should be discarded after calling it.
|
|
107
|
+
*/
|
|
108
|
+
dispose(): void;
|
|
109
|
+
/** Allows the store to be used with the `using` keyword */
|
|
110
|
+
[Symbol.dispose](): void;
|
|
101
111
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { verifyEvent as coreVerifyEvent } from "nostr-tools/pure";
|
|
2
|
-
import { Subject } from "rxjs";
|
|
2
|
+
import { Subject, Subscription } from "rxjs";
|
|
3
3
|
import { EventStoreSymbol, getReplaceableIdentifier, isReplaceable, kinds } from "../helpers/event.js";
|
|
4
4
|
import { getExpirationTimestamp } from "../helpers/expiration.js";
|
|
5
5
|
import { eventMatchesPointer, isAddressPointer, isEventPointer, } from "../helpers/pointers.js";
|
|
@@ -47,6 +47,8 @@ export class AsyncEventStore extends EventModels {
|
|
|
47
47
|
* A method that will be called when an event isn't found in the store
|
|
48
48
|
*/
|
|
49
49
|
eventLoader;
|
|
50
|
+
/** Internal subscriptions (delete + expiration managers) torn down on dispose */
|
|
51
|
+
internalSubscriptions = new Subscription();
|
|
50
52
|
constructor(options) {
|
|
51
53
|
super();
|
|
52
54
|
this.database = options.database;
|
|
@@ -63,19 +65,19 @@ export class AsyncEventStore extends EventModels {
|
|
|
63
65
|
// Use provided delete manager or create a default one
|
|
64
66
|
this.deletes = options.deleteManager ?? new AsyncDeleteManager();
|
|
65
67
|
// Listen to delete notifications and remove matching events
|
|
66
|
-
this.deletes.deleted$.subscribe((notification) => {
|
|
68
|
+
this.internalSubscriptions.add(this.deletes.deleted$.subscribe((notification) => {
|
|
67
69
|
this.handleDeleteNotification(notification).catch((error) => {
|
|
68
70
|
console.error("[applesauce-core] Error handling delete notification:", error);
|
|
69
71
|
});
|
|
70
|
-
});
|
|
72
|
+
}));
|
|
71
73
|
// Create expiration manager
|
|
72
74
|
this.expiration = options.expirationManager ?? new ExpirationManager();
|
|
73
75
|
// Listen to expired events and remove them from the store
|
|
74
|
-
this.expiration.expired$.subscribe((id) => {
|
|
76
|
+
this.internalSubscriptions.add(this.expiration.expired$.subscribe((id) => {
|
|
75
77
|
this.handleExpiredNotification(id).catch((error) => {
|
|
76
78
|
console.error("[applesauce-core] Error handling expired notification:", error);
|
|
77
79
|
});
|
|
78
|
-
});
|
|
80
|
+
}));
|
|
79
81
|
}
|
|
80
82
|
mapToMemory(event) {
|
|
81
83
|
if (event === undefined)
|
|
@@ -339,4 +341,30 @@ export class AsyncEventStore extends EventModels {
|
|
|
339
341
|
prune(limit) {
|
|
340
342
|
return this.memory.prune(limit) ?? 0;
|
|
341
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Tears down the store: disposes the attached event loader, completes the event streams, releases
|
|
346
|
+
* model keep-warm timers, unsubscribes internal manager listeners, and cancels any pending expiration timer.
|
|
347
|
+
* @note This is a terminal operation; the store should be discarded after calling it.
|
|
348
|
+
*/
|
|
349
|
+
dispose() {
|
|
350
|
+
// Tear down the attached event loader if it supports disposal
|
|
351
|
+
const loader = this.eventLoader;
|
|
352
|
+
if (loader && typeof loader[Symbol.dispose] === "function")
|
|
353
|
+
loader[Symbol.dispose]();
|
|
354
|
+
this.eventLoader = undefined;
|
|
355
|
+
// Complete all models and release their keep-warm timers
|
|
356
|
+
super.dispose();
|
|
357
|
+
// Tear down internal manager subscriptions
|
|
358
|
+
this.internalSubscriptions.unsubscribe();
|
|
359
|
+
// Cancel any pending expiration timer
|
|
360
|
+
this.expiration.dispose?.();
|
|
361
|
+
// Complete the event streams
|
|
362
|
+
this.insert$.complete();
|
|
363
|
+
this.update$.complete();
|
|
364
|
+
this.remove$.complete();
|
|
365
|
+
}
|
|
366
|
+
/** Allows the store to be used with the `using` keyword */
|
|
367
|
+
[Symbol.dispose]() {
|
|
368
|
+
this.dispose();
|
|
369
|
+
}
|
|
342
370
|
}
|
|
@@ -97,16 +97,36 @@ export class EventMemory {
|
|
|
97
97
|
// only remove events that are known
|
|
98
98
|
if (!this.events.has(id))
|
|
99
99
|
return false;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// Remove from author index, dropping the index entry when it becomes empty so
|
|
101
|
+
// the map does not accumulate empty Sets keyed by every pubkey ever seen
|
|
102
|
+
const authorIndex = this.authors.get(event.pubkey);
|
|
103
|
+
if (authorIndex) {
|
|
104
|
+
authorIndex.delete(event);
|
|
105
|
+
if (authorIndex.size === 0)
|
|
106
|
+
this.authors.delete(event.pubkey);
|
|
107
|
+
}
|
|
108
|
+
// Remove from kind index, dropping the index entry when it becomes empty
|
|
109
|
+
const kindIndex = this.kinds.get(event.kind);
|
|
110
|
+
if (kindIndex) {
|
|
111
|
+
kindIndex.delete(event);
|
|
112
|
+
if (kindIndex.size === 0)
|
|
113
|
+
this.kinds.delete(event.kind);
|
|
114
|
+
}
|
|
115
|
+
// Remove from composite kind+author index, dropping the entry when empty
|
|
103
116
|
const kindAuthorKey = `${event.kind}:${event.pubkey}`;
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
const kindAuthorIndex = this.kindAuthor.get(kindAuthorKey);
|
|
118
|
+
if (kindAuthorIndex) {
|
|
119
|
+
kindAuthorIndex.delete(event);
|
|
120
|
+
if (kindAuthorIndex.size === 0)
|
|
121
|
+
this.kindAuthor.delete(kindAuthorKey);
|
|
106
122
|
}
|
|
107
123
|
for (const tag of getIndexableTags(event)) {
|
|
108
124
|
if (this.tags.has(tag)) {
|
|
109
|
-
this.getTagIndex(tag)
|
|
125
|
+
const tagIndex = this.getTagIndex(tag);
|
|
126
|
+
tagIndex.delete(event);
|
|
127
|
+
// Drop the tag index when empty so it does not retain the key forever
|
|
128
|
+
if (tagIndex.size === 0)
|
|
129
|
+
this.tags.delete(tag);
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
// remove from created_at index using binary search
|
|
@@ -117,8 +137,12 @@ export class EventMemory {
|
|
|
117
137
|
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
118
138
|
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
119
139
|
const array = this.replaceable.get(address);
|
|
120
|
-
if (array)
|
|
140
|
+
if (array) {
|
|
121
141
|
this.removeFromSortedArray(array, event);
|
|
142
|
+
// Drop the empty array so the map does not retain an address key forever
|
|
143
|
+
if (array.length === 0)
|
|
144
|
+
this.replaceable.delete(address);
|
|
145
|
+
}
|
|
122
146
|
}
|
|
123
147
|
// remove any claims this event has
|
|
124
148
|
this.claims.delete(event);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Observable } from "rxjs";
|
|
1
|
+
import { Observable, ReplaySubject } 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";
|
|
@@ -30,6 +30,11 @@ export declare class EventModels<TStore extends IEventStore | IAsyncEventStore =
|
|
|
30
30
|
models: Map<ModelConstructor<any, any[], TStore>, Map<string, Observable<any>>>;
|
|
31
31
|
/** How long a model should be kept "warm" while nothing is subscribed to it */
|
|
32
32
|
modelKeepWarm: number;
|
|
33
|
+
/**
|
|
34
|
+
* Emits when {@link EventModels.dispose} is called to tear down all models.
|
|
35
|
+
* A {@link ReplaySubject} so reset notifiers subscribed after disposal fire immediately.
|
|
36
|
+
*/
|
|
37
|
+
protected destroy$: ReplaySubject<void>;
|
|
33
38
|
/** Get or create a model on the event store */
|
|
34
39
|
model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args, TStore>, ...args: Args): Observable<T>;
|
|
35
40
|
/**
|
|
@@ -56,4 +61,9 @@ export declare class EventModels<TStore extends IEventStore | IAsyncEventStore =
|
|
|
56
61
|
inboxes: string[];
|
|
57
62
|
outboxes: string[];
|
|
58
63
|
} | undefined>;
|
|
64
|
+
/**
|
|
65
|
+
* Tears down all models, completing active subscriptions and releasing their keep-warm timers immediately.
|
|
66
|
+
* @note This is a terminal operation; the instance should be discarded after calling it.
|
|
67
|
+
*/
|
|
68
|
+
dispose(): void;
|
|
59
69
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import hash_sum from "hash-sum";
|
|
2
|
-
import { finalize, ReplaySubject, share, timer } from "rxjs";
|
|
2
|
+
import { finalize, merge, ReplaySubject, share, takeUntil, timer } from "rxjs";
|
|
3
3
|
import { isEventPointer, } from "../helpers/pointers.js";
|
|
4
4
|
import { EventModel, FiltersModel, ReplaceableModel, TimelineModel } from "../models/base.js";
|
|
5
5
|
import { ContactsModel } from "../models/contacts.js";
|
|
@@ -32,6 +32,11 @@ export class EventModels {
|
|
|
32
32
|
models = new Map();
|
|
33
33
|
/** How long a model should be kept "warm" while nothing is subscribed to it */
|
|
34
34
|
modelKeepWarm = 60_000;
|
|
35
|
+
/**
|
|
36
|
+
* Emits when {@link EventModels.dispose} is called to tear down all models.
|
|
37
|
+
* A {@link ReplaySubject} so reset notifiers subscribed after disposal fire immediately.
|
|
38
|
+
*/
|
|
39
|
+
destroy$ = new ReplaySubject(1);
|
|
35
40
|
/** Get or create a model on the event store */
|
|
36
41
|
model(constructor, ...args) {
|
|
37
42
|
let models = this.models.get(constructor);
|
|
@@ -54,9 +59,12 @@ export class EventModels {
|
|
|
54
59
|
// only subscribe to models once for all subscriptions
|
|
55
60
|
share({
|
|
56
61
|
connector: () => new ReplaySubject(1),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
// Reset after the keep-warm window OR immediately when the store is disposed
|
|
63
|
+
resetOnComplete: () => merge(timer(this.modelKeepWarm), this.destroy$),
|
|
64
|
+
resetOnRefCountZero: () => merge(timer(this.modelKeepWarm), this.destroy$),
|
|
65
|
+
}),
|
|
66
|
+
// Complete any active subscriptions when the store is disposed
|
|
67
|
+
takeUntil(this.destroy$));
|
|
60
68
|
// Add the model to the cache
|
|
61
69
|
models.set(key, model);
|
|
62
70
|
}
|
|
@@ -118,4 +126,12 @@ export class EventModels {
|
|
|
118
126
|
user = { pubkey: user };
|
|
119
127
|
return this.model(MailboxesModel, user);
|
|
120
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Tears down all models, completing active subscriptions and releasing their keep-warm timers immediately.
|
|
131
|
+
* @note This is a terminal operation; the instance should be discarded after calling it.
|
|
132
|
+
*/
|
|
133
|
+
dispose() {
|
|
134
|
+
this.destroy$.next();
|
|
135
|
+
this.destroy$.complete();
|
|
136
|
+
}
|
|
121
137
|
}
|
|
@@ -50,6 +50,8 @@ export declare class EventStore extends EventModels implements IEventStore {
|
|
|
50
50
|
remove$: Subject<NostrEvent>;
|
|
51
51
|
/** A method that will be called when an event isn't found in the store */
|
|
52
52
|
eventLoader?: (pointer: EventPointer | AddressPointer | AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
53
|
+
/** Internal subscriptions (delete + expiration managers) torn down on dispose */
|
|
54
|
+
private internalSubscriptions;
|
|
53
55
|
constructor(options?: EventStoreOptions);
|
|
54
56
|
/** A method to add all events to memory to ensure there is only ever a single instance of an event */
|
|
55
57
|
private mapToMemory;
|
|
@@ -98,4 +100,12 @@ export declare class EventStore extends EventModels implements IEventStore {
|
|
|
98
100
|
unclaimed(): Generator<NostrEvent>;
|
|
99
101
|
/** Removes any event that is not being used by a subscription */
|
|
100
102
|
prune(limit?: number): number;
|
|
103
|
+
/**
|
|
104
|
+
* Tears down the store: disposes the attached event loader, completes the event streams, releases
|
|
105
|
+
* model keep-warm timers, and unsubscribes internal manager listeners.
|
|
106
|
+
* @note This is a terminal operation; the store should be discarded after calling it.
|
|
107
|
+
*/
|
|
108
|
+
dispose(): void;
|
|
109
|
+
/** Allows the store to be used with the `using` keyword */
|
|
110
|
+
[Symbol.dispose](): void;
|
|
101
111
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { verifyEvent as coreVerifyEvent, verifiedSymbol } from "nostr-tools/pure";
|
|
2
|
-
import { Subject } from "rxjs";
|
|
2
|
+
import { Subject, Subscription } from "rxjs";
|
|
3
3
|
import { EncryptedContentSymbol } from "../helpers/encrypted-content.js";
|
|
4
4
|
import { EventStoreSymbol, FromCacheSymbol, getReplaceableIdentifier, isRegularKind, isReplaceable, kinds, } from "../helpers/event.js";
|
|
5
5
|
import { getExpirationTimestamp } from "../helpers/expiration.js";
|
|
@@ -45,6 +45,8 @@ export class EventStore extends EventModels {
|
|
|
45
45
|
remove$ = new Subject();
|
|
46
46
|
/** A method that will be called when an event isn't found in the store */
|
|
47
47
|
eventLoader;
|
|
48
|
+
/** Internal subscriptions (delete + expiration managers) torn down on dispose */
|
|
49
|
+
internalSubscriptions = new Subscription();
|
|
48
50
|
constructor(options) {
|
|
49
51
|
super();
|
|
50
52
|
if (options?.database) {
|
|
@@ -67,11 +69,11 @@ export class EventStore extends EventModels {
|
|
|
67
69
|
// Use provided delete manager or create a default one
|
|
68
70
|
this.deletes = options?.deleteManager ?? new DeleteManager();
|
|
69
71
|
// Listen to delete notifications and remove matching events
|
|
70
|
-
this.deletes.deleted$.subscribe(this.handleDeleteNotification.bind(this));
|
|
72
|
+
this.internalSubscriptions.add(this.deletes.deleted$.subscribe(this.handleDeleteNotification.bind(this)));
|
|
71
73
|
// Create expiration manager
|
|
72
74
|
this.expiration = options?.expirationManager ?? new ExpirationManager();
|
|
73
75
|
// Listen to expired events and remove them from the store
|
|
74
|
-
this.expiration.expired$.subscribe(this.handleExpiredNotification.bind(this));
|
|
76
|
+
this.internalSubscriptions.add(this.expiration.expired$.subscribe(this.handleExpiredNotification.bind(this)));
|
|
75
77
|
}
|
|
76
78
|
mapToMemory(event) {
|
|
77
79
|
if (event === undefined)
|
|
@@ -364,4 +366,30 @@ export class EventStore extends EventModels {
|
|
|
364
366
|
prune(limit) {
|
|
365
367
|
return this.memory.prune(limit) ?? 0;
|
|
366
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Tears down the store: disposes the attached event loader, completes the event streams, releases
|
|
371
|
+
* model keep-warm timers, and unsubscribes internal manager listeners.
|
|
372
|
+
* @note This is a terminal operation; the store should be discarded after calling it.
|
|
373
|
+
*/
|
|
374
|
+
dispose() {
|
|
375
|
+
// Tear down the attached event loader if it supports disposal
|
|
376
|
+
const loader = this.eventLoader;
|
|
377
|
+
if (loader && typeof loader[Symbol.dispose] === "function")
|
|
378
|
+
loader[Symbol.dispose]();
|
|
379
|
+
this.eventLoader = undefined;
|
|
380
|
+
// Complete all models and release their keep-warm timers
|
|
381
|
+
super.dispose();
|
|
382
|
+
// Tear down internal manager subscriptions
|
|
383
|
+
this.internalSubscriptions.unsubscribe();
|
|
384
|
+
// Cancel any pending expiration timer
|
|
385
|
+
this.expiration.dispose?.();
|
|
386
|
+
// Complete the event streams
|
|
387
|
+
this.insert$.complete();
|
|
388
|
+
this.update$.complete();
|
|
389
|
+
this.remove$.complete();
|
|
390
|
+
}
|
|
391
|
+
/** Allows the store to be used with the `using` keyword */
|
|
392
|
+
[Symbol.dispose]() {
|
|
393
|
+
this.dispose();
|
|
394
|
+
}
|
|
367
395
|
}
|
|
@@ -30,6 +30,13 @@ export declare class ExpirationManager implements IExpirationManager {
|
|
|
30
30
|
* @returns true if the event has expired, false otherwise
|
|
31
31
|
*/
|
|
32
32
|
check(event: NostrEvent): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Tears down the manager: cancels any pending timer and completes the expired$ stream
|
|
35
|
+
* @note This is a terminal operation; the manager should be discarded after calling it.
|
|
36
|
+
*/
|
|
37
|
+
dispose(): void;
|
|
38
|
+
/** Allows the manager to be used with the `using` keyword */
|
|
39
|
+
[Symbol.dispose](): void;
|
|
33
40
|
/**
|
|
34
41
|
* Remove expired events from the store and emit them
|
|
35
42
|
*/
|
|
@@ -62,6 +62,22 @@ export class ExpirationManager {
|
|
|
62
62
|
return false;
|
|
63
63
|
return expiration <= unixNow();
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Tears down the manager: cancels any pending timer and completes the expired$ stream
|
|
67
|
+
* @note This is a terminal operation; the manager should be discarded after calling it.
|
|
68
|
+
*/
|
|
69
|
+
dispose() {
|
|
70
|
+
if (this.timer)
|
|
71
|
+
clearTimeout(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
this.nextCheck = null;
|
|
74
|
+
this.expirations.clear();
|
|
75
|
+
this.expiredSubject.complete();
|
|
76
|
+
}
|
|
77
|
+
/** Allows the manager to be used with the `using` keyword */
|
|
78
|
+
[Symbol.dispose]() {
|
|
79
|
+
this.dispose();
|
|
80
|
+
}
|
|
65
81
|
/**
|
|
66
82
|
* Remove expired events from the store and emit them
|
|
67
83
|
*/
|
|
@@ -172,6 +172,8 @@ export interface IExpirationManager {
|
|
|
172
172
|
forget(eventId: string): void;
|
|
173
173
|
/** Check if an event is expired */
|
|
174
174
|
check(event: NostrEvent): boolean;
|
|
175
|
+
/** Tears down the manager and cancels any pending timers */
|
|
176
|
+
dispose?(): void;
|
|
175
177
|
}
|
|
176
178
|
/** The base interface for a database of events */
|
|
177
179
|
export interface IEventDatabase extends IEventStoreRead {
|
package/dist/models/base.js
CHANGED
|
@@ -106,6 +106,7 @@ export function TimelineModel(filters, includeOldVersion) {
|
|
|
106
106
|
filters = Array.isArray(filters) ? filters : [filters];
|
|
107
107
|
return (store) => {
|
|
108
108
|
const seen = new Map();
|
|
109
|
+
const getTimelineUID = (event) => (includeOldVersion ? event.id : getEventUID(event));
|
|
109
110
|
// get current events
|
|
110
111
|
return defer(() => {
|
|
111
112
|
const r = store.getTimeline(filters);
|
|
@@ -125,27 +126,37 @@ export function TimelineModel(filters, includeOldVersion) {
|
|
|
125
126
|
// build a timeline
|
|
126
127
|
scan((timeline, event) => {
|
|
127
128
|
// filter out removed events from timeline
|
|
128
|
-
if (typeof event === "string")
|
|
129
|
+
if (typeof event === "string") {
|
|
130
|
+
// Forget the removed event so the seen map does not grow unbounded on long-lived
|
|
131
|
+
// feeds. Only forget when the seen entry is the event being removed (not a newer
|
|
132
|
+
// replaceable version that happens to share a UID).
|
|
133
|
+
const removed = timeline.find((e) => e.id === event);
|
|
134
|
+
if (removed) {
|
|
135
|
+
const uid = getTimelineUID(removed);
|
|
136
|
+
if (seen.get(uid)?.id === event)
|
|
137
|
+
seen.delete(uid);
|
|
138
|
+
}
|
|
129
139
|
return timeline.filter((e) => e.id !== event);
|
|
140
|
+
}
|
|
130
141
|
// initial timeline array
|
|
131
142
|
if (Array.isArray(event)) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
seen.set(getEventUID(e), e);
|
|
136
|
-
}
|
|
143
|
+
seen.clear();
|
|
144
|
+
for (const e of event)
|
|
145
|
+
seen.set(getTimelineUID(e), e);
|
|
137
146
|
// Always return a new array instance to ensure UI libraries detect changes
|
|
138
147
|
return [...event];
|
|
139
148
|
}
|
|
140
149
|
// create a new timeline and insert the event into it
|
|
141
150
|
let newTimeline = [...timeline];
|
|
151
|
+
const uid = getTimelineUID(event);
|
|
152
|
+
const existing = seen.get(uid);
|
|
153
|
+
// Ignore duplicate regular events and older replaceable versions.
|
|
154
|
+
if (existing &&
|
|
155
|
+
(!isReplaceable(event.kind) || (!includeOldVersion && event.created_at < existing.created_at))) {
|
|
156
|
+
return [...timeline];
|
|
157
|
+
}
|
|
142
158
|
// remove old replaceable events if enabled
|
|
143
159
|
if (!includeOldVersion && isReplaceable(event.kind)) {
|
|
144
|
-
const uid = getEventUID(event);
|
|
145
|
-
const existing = seen.get(uid);
|
|
146
|
-
// if this is an older replaceable event, return a new array instance
|
|
147
|
-
if (existing && event.created_at < existing.created_at)
|
|
148
|
-
return [...timeline];
|
|
149
160
|
// update latest version
|
|
150
161
|
seen.set(uid, event);
|
|
151
162
|
// remove old event from timeline
|
|
@@ -155,6 +166,9 @@ export function TimelineModel(filters, includeOldVersion) {
|
|
|
155
166
|
newTimeline.splice(index, 1);
|
|
156
167
|
}
|
|
157
168
|
}
|
|
169
|
+
else {
|
|
170
|
+
seen.set(uid, event);
|
|
171
|
+
}
|
|
158
172
|
// add event into timeline
|
|
159
173
|
insertEventIntoDescendingList(newTimeline, event);
|
|
160
174
|
return newTimeline;
|