applesauce-core 0.1.0 → 0.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/README.md +1 -0
- package/dist/event-store/database.d.ts +57 -13
- package/dist/event-store/database.js +52 -15
- package/dist/event-store/event-store.d.ts +2 -1
- package/dist/event-store/event-store.js +116 -29
- package/dist/event-store/index.d.ts +2 -1
- package/dist/event-store/index.js +2 -1
- package/dist/helpers/event.d.ts +4 -0
- package/dist/helpers/event.js +4 -0
- package/dist/helpers/filter.d.ts +5 -0
- package/dist/helpers/filter.js +5 -2
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/mailboxes.d.ts +6 -0
- package/dist/helpers/mailboxes.js +6 -0
- package/dist/query-store/index.d.ts +8 -8
- package/dist/query-store/index.js +4 -4
- package/dist/utils/lru.d.ts +32 -0
- package/dist/utils/lru.js +148 -0
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# applesauce-core
|
|
@@ -1,34 +1,78 @@
|
|
|
1
|
+
/// <reference types="debug" />
|
|
2
|
+
/// <reference types="zen-observable" />
|
|
1
3
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
-
import { LRU } from "
|
|
4
|
+
import { LRU } from "../utils/lru.js";
|
|
5
|
+
/**
|
|
6
|
+
* An in-memory database for nostr events
|
|
7
|
+
*/
|
|
3
8
|
export declare class Database {
|
|
4
9
|
log: import("debug").Debugger;
|
|
5
|
-
/** Max number of events to hold */
|
|
6
|
-
max?: number;
|
|
7
10
|
/** Indexes */
|
|
8
|
-
kinds: Map<number, Set<import("nostr-tools").Event>>;
|
|
9
|
-
authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
10
|
-
tags: LRU<Set<import("nostr-tools").Event>>;
|
|
11
|
-
created_at: NostrEvent[];
|
|
11
|
+
protected kinds: Map<number, Set<import("nostr-tools").Event>>;
|
|
12
|
+
protected authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
13
|
+
protected tags: LRU<Set<import("nostr-tools").Event>>;
|
|
14
|
+
protected created_at: NostrEvent[];
|
|
12
15
|
/** LRU cache of last events touched */
|
|
13
16
|
events: LRU<import("nostr-tools").Event>;
|
|
14
|
-
|
|
17
|
+
private insertedSignal;
|
|
18
|
+
private deletedSignal;
|
|
19
|
+
/** A stream of events inserted into the database */
|
|
20
|
+
inserted: {
|
|
21
|
+
subscribe(observer: ZenObservable.Observer<import("nostr-tools").Event>): ZenObservable.Subscription;
|
|
22
|
+
subscribe(onNext: (value: import("nostr-tools").Event) => void, onError?: ((error: any) => void) | undefined, onComplete?: (() => void) | undefined): ZenObservable.Subscription;
|
|
23
|
+
forEach(callback: (value: import("nostr-tools").Event) => void): Promise<void>;
|
|
24
|
+
map<R>(callback: (value: import("nostr-tools").Event) => R): import("zen-observable")<R>;
|
|
25
|
+
filter<S extends import("nostr-tools").Event>(callback: (value: import("nostr-tools").Event) => value is S): import("zen-observable")<S>;
|
|
26
|
+
filter(callback: (value: import("nostr-tools").Event) => boolean): import("zen-observable")<import("nostr-tools").Event>;
|
|
27
|
+
reduce(callback: (previousValue: import("nostr-tools").Event, currentValue: import("nostr-tools").Event) => import("nostr-tools").Event, initialValue?: import("nostr-tools").Event | undefined): import("zen-observable")<import("nostr-tools").Event>;
|
|
28
|
+
reduce<R_1>(callback: (previousValue: R_1, currentValue: import("nostr-tools").Event) => R_1, initialValue?: R_1 | undefined): import("zen-observable")<R_1>;
|
|
29
|
+
flatMap<R_2>(callback: (value: import("nostr-tools").Event) => ZenObservable.ObservableLike<R_2>): import("zen-observable")<R_2>;
|
|
30
|
+
concat<R_3>(...observable: import("zen-observable")<R_3>[]): import("zen-observable")<R_3>;
|
|
31
|
+
};
|
|
32
|
+
/** A stream of events removed of the database */
|
|
33
|
+
deleted: {
|
|
34
|
+
subscribe(observer: ZenObservable.Observer<import("nostr-tools").Event>): ZenObservable.Subscription;
|
|
35
|
+
subscribe(onNext: (value: import("nostr-tools").Event) => void, onError?: ((error: any) => void) | undefined, onComplete?: (() => void) | undefined): ZenObservable.Subscription;
|
|
36
|
+
forEach(callback: (value: import("nostr-tools").Event) => void): Promise<void>;
|
|
37
|
+
map<R>(callback: (value: import("nostr-tools").Event) => R): import("zen-observable")<R>;
|
|
38
|
+
filter<S extends import("nostr-tools").Event>(callback: (value: import("nostr-tools").Event) => value is S): import("zen-observable")<S>;
|
|
39
|
+
filter(callback: (value: import("nostr-tools").Event) => boolean): import("zen-observable")<import("nostr-tools").Event>;
|
|
40
|
+
reduce(callback: (previousValue: import("nostr-tools").Event, currentValue: import("nostr-tools").Event) => import("nostr-tools").Event, initialValue?: import("nostr-tools").Event | undefined): import("zen-observable")<import("nostr-tools").Event>;
|
|
41
|
+
reduce<R_1>(callback: (previousValue: R_1, currentValue: import("nostr-tools").Event) => R_1, initialValue?: R_1 | undefined): import("zen-observable")<R_1>;
|
|
42
|
+
flatMap<R_2>(callback: (value: import("nostr-tools").Event) => ZenObservable.ObservableLike<R_2>): import("zen-observable")<R_2>;
|
|
43
|
+
concat<R_3>(...observable: import("zen-observable")<R_3>[]): import("zen-observable")<R_3>;
|
|
44
|
+
};
|
|
45
|
+
protected claims: WeakMap<import("nostr-tools").Event, any>;
|
|
15
46
|
/** Index helper methods */
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
|
|
48
|
+
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
|
|
49
|
+
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
50
|
+
/** Moves an event to the top of the LRU cache */
|
|
19
51
|
touch(event: NostrEvent): void;
|
|
20
52
|
hasEvent(uid: string): import("nostr-tools").Event | undefined;
|
|
21
53
|
getEvent(uid: string): import("nostr-tools").Event | undefined;
|
|
54
|
+
/** Checks if the database contains a replaceable event without touching it */
|
|
22
55
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
56
|
+
/** Gets a replaceable event and touches it */
|
|
23
57
|
getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
|
|
24
58
|
addEvent(event: NostrEvent): import("nostr-tools").Event;
|
|
25
|
-
deleteEvent(
|
|
59
|
+
deleteEvent(eventOrUID: string | NostrEvent): boolean;
|
|
60
|
+
/** Sets the claim on the event and touches it */
|
|
61
|
+
claimEvent(event: NostrEvent, claim: any): void;
|
|
62
|
+
/** Checks if an event is claimed by anything */
|
|
63
|
+
isClaimed(event: NostrEvent): boolean;
|
|
64
|
+
/** Removes a claim from an event */
|
|
65
|
+
removeClaim(event: NostrEvent, claim: any): void;
|
|
66
|
+
/** Removes all claims on an event */
|
|
67
|
+
clearClaim(event: NostrEvent): void;
|
|
26
68
|
iterateAuthors(authors: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
27
69
|
iterateTag(tag: string, values: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
28
70
|
iterateKinds(kinds: Iterable<number>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
29
71
|
iterateTime(since: number | undefined, until: number | undefined): Generator<never, Set<import("nostr-tools").Event>, unknown>;
|
|
30
72
|
iterateIds(ids: Iterable<string>): Generator<import("nostr-tools").Event, void, unknown>;
|
|
73
|
+
/** Returns all events that match the filter */
|
|
31
74
|
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
32
75
|
getForFilters(filters: Filter[]): Set<import("nostr-tools").Event>;
|
|
33
|
-
|
|
76
|
+
/** Remove the oldest events that are not claimed */
|
|
77
|
+
prune(limit?: number): number;
|
|
34
78
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
|
+
import PushStream from "zen-push";
|
|
2
3
|
import { getEventUID, getIndexableTags, getReplaceableUID } from "../helpers/event.js";
|
|
3
4
|
import { INDEXABLE_TAGS } from "./common.js";
|
|
4
5
|
import { logger } from "../logger.js";
|
|
5
|
-
import {
|
|
6
|
+
import { LRU } from "../utils/lru.js";
|
|
7
|
+
/**
|
|
8
|
+
* An in-memory database for nostr events
|
|
9
|
+
*/
|
|
6
10
|
export class Database {
|
|
7
11
|
log = logger.extend("Database");
|
|
8
|
-
/** Max number of events to hold */
|
|
9
|
-
max;
|
|
10
12
|
/** Indexes */
|
|
11
13
|
kinds = new Map();
|
|
12
14
|
authors = new Map();
|
|
@@ -14,9 +16,13 @@ export class Database {
|
|
|
14
16
|
created_at = [];
|
|
15
17
|
/** LRU cache of last events touched */
|
|
16
18
|
events = new LRU();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
insertedSignal = new PushStream();
|
|
20
|
+
deletedSignal = new PushStream();
|
|
21
|
+
/** A stream of events inserted into the database */
|
|
22
|
+
inserted = this.insertedSignal.observable;
|
|
23
|
+
/** A stream of events removed of the database */
|
|
24
|
+
deleted = this.deletedSignal.observable;
|
|
25
|
+
claims = new WeakMap();
|
|
20
26
|
/** Index helper methods */
|
|
21
27
|
getKindIndex(kind) {
|
|
22
28
|
if (!this.kinds.has(kind))
|
|
@@ -45,6 +51,7 @@ export class Database {
|
|
|
45
51
|
}
|
|
46
52
|
return this.tags.get(tagAndValue);
|
|
47
53
|
}
|
|
54
|
+
/** Moves an event to the top of the LRU cache */
|
|
48
55
|
touch(event) {
|
|
49
56
|
this.events.set(getEventUID(event), event);
|
|
50
57
|
}
|
|
@@ -54,9 +61,11 @@ export class Database {
|
|
|
54
61
|
getEvent(uid) {
|
|
55
62
|
return this.events.get(uid);
|
|
56
63
|
}
|
|
64
|
+
/** Checks if the database contains a replaceable event without touching it */
|
|
57
65
|
hasReplaceable(kind, pubkey, d) {
|
|
58
66
|
return this.events.has(getReplaceableUID(kind, pubkey, d));
|
|
59
67
|
}
|
|
68
|
+
/** Gets a replaceable event and touches it */
|
|
60
69
|
getReplaceable(kind, pubkey, d) {
|
|
61
70
|
return this.events.get(getReplaceableUID(kind, pubkey, d));
|
|
62
71
|
}
|
|
@@ -74,10 +83,11 @@ export class Database {
|
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
insertEventIntoDescendingList(this.created_at, event);
|
|
86
|
+
this.insertedSignal.next(event);
|
|
77
87
|
return event;
|
|
78
88
|
}
|
|
79
|
-
deleteEvent(
|
|
80
|
-
let event = typeof
|
|
89
|
+
deleteEvent(eventOrUID) {
|
|
90
|
+
let event = typeof eventOrUID === "string" ? this.events.get(eventOrUID) : eventOrUID;
|
|
81
91
|
if (!event)
|
|
82
92
|
throw new Error("Missing event");
|
|
83
93
|
const uid = getEventUID(event);
|
|
@@ -95,8 +105,31 @@ export class Database {
|
|
|
95
105
|
const i = this.created_at.indexOf(event);
|
|
96
106
|
this.created_at.splice(i, 1);
|
|
97
107
|
this.events.delete(uid);
|
|
108
|
+
this.deletedSignal.next(event);
|
|
98
109
|
return true;
|
|
99
110
|
}
|
|
111
|
+
/** Sets the claim on the event and touches it */
|
|
112
|
+
claimEvent(event, claim) {
|
|
113
|
+
if (!this.claims.has(event)) {
|
|
114
|
+
this.claims.set(event, claim);
|
|
115
|
+
}
|
|
116
|
+
// always touch event
|
|
117
|
+
this.touch(event);
|
|
118
|
+
}
|
|
119
|
+
/** Checks if an event is claimed by anything */
|
|
120
|
+
isClaimed(event) {
|
|
121
|
+
return this.claims.has(event);
|
|
122
|
+
}
|
|
123
|
+
/** Removes a claim from an event */
|
|
124
|
+
removeClaim(event, claim) {
|
|
125
|
+
const current = this.claims.get(event);
|
|
126
|
+
if (current === claim)
|
|
127
|
+
this.claims.delete(event);
|
|
128
|
+
}
|
|
129
|
+
/** Removes all claims on an event */
|
|
130
|
+
clearClaim(event) {
|
|
131
|
+
this.claims.delete(event);
|
|
132
|
+
}
|
|
100
133
|
*iterateAuthors(authors) {
|
|
101
134
|
for (const author of authors) {
|
|
102
135
|
const events = this.authors.get(author);
|
|
@@ -157,6 +190,7 @@ export class Database {
|
|
|
157
190
|
yield this.events.get(id);
|
|
158
191
|
}
|
|
159
192
|
}
|
|
193
|
+
/** Returns all events that match the filter */
|
|
160
194
|
getEventsForFilter(filter) {
|
|
161
195
|
// search is not supported, return an empty set
|
|
162
196
|
if (filter.search)
|
|
@@ -224,16 +258,19 @@ export class Database {
|
|
|
224
258
|
}
|
|
225
259
|
return events;
|
|
226
260
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return;
|
|
261
|
+
/** Remove the oldest events that are not claimed */
|
|
262
|
+
prune(limit = 1000) {
|
|
230
263
|
let removed = 0;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
264
|
+
let cursor = this.events.first;
|
|
265
|
+
while (cursor) {
|
|
266
|
+
const event = cursor.value;
|
|
267
|
+
if (!this.isClaimed(event)) {
|
|
234
268
|
this.deleteEvent(event);
|
|
235
269
|
removed++;
|
|
270
|
+
if (removed >= limit)
|
|
271
|
+
break;
|
|
236
272
|
}
|
|
273
|
+
cursor = cursor.next;
|
|
237
274
|
}
|
|
238
275
|
return removed;
|
|
239
276
|
}
|
|
@@ -5,6 +5,7 @@ export declare class EventStore {
|
|
|
5
5
|
events: Database;
|
|
6
6
|
private singles;
|
|
7
7
|
private streams;
|
|
8
|
+
private timelines;
|
|
8
9
|
constructor();
|
|
9
10
|
add(event: NostrEvent, fromRelay?: string): import("nostr-tools").Event;
|
|
10
11
|
getAll(filters: Filter[]): Set<import("nostr-tools").Event>;
|
|
@@ -13,7 +14,7 @@ export declare class EventStore {
|
|
|
13
14
|
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
14
15
|
getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
|
|
15
16
|
/** Creates an observable that updates a single event */
|
|
16
|
-
single(uid: string): Observable<import("nostr-tools").Event>;
|
|
17
|
+
single(uid: string): Observable<import("nostr-tools").Event | undefined>;
|
|
17
18
|
/** Creates an observable that streams all events that match the filter */
|
|
18
19
|
stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
|
|
19
20
|
/** Creates an observable that updates with an array of sorted events */
|
|
@@ -8,24 +8,12 @@ export class EventStore {
|
|
|
8
8
|
events;
|
|
9
9
|
singles = new Map();
|
|
10
10
|
streams = new Map();
|
|
11
|
+
timelines = new Map();
|
|
11
12
|
constructor() {
|
|
12
13
|
this.events = new Database();
|
|
13
14
|
}
|
|
14
15
|
add(event, fromRelay) {
|
|
15
16
|
const inserted = this.events.addEvent(event);
|
|
16
|
-
if (inserted === event) {
|
|
17
|
-
// forward to single event requests
|
|
18
|
-
const eventUID = getEventUID(event);
|
|
19
|
-
for (const [control, uid] of this.singles) {
|
|
20
|
-
if (eventUID === uid)
|
|
21
|
-
control.next(event);
|
|
22
|
-
}
|
|
23
|
-
// forward to streams
|
|
24
|
-
for (const [control, filters] of this.streams) {
|
|
25
|
-
if (matchFilters(filters, event))
|
|
26
|
-
control.next(event);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
17
|
if (fromRelay)
|
|
30
18
|
addSeenRelay(inserted, fromRelay);
|
|
31
19
|
return inserted;
|
|
@@ -47,34 +35,133 @@ export class EventStore {
|
|
|
47
35
|
}
|
|
48
36
|
/** Creates an observable that updates a single event */
|
|
49
37
|
single(uid) {
|
|
50
|
-
return new Observable((
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
return new Observable((observer) => {
|
|
39
|
+
let current = this.events.getEvent(uid);
|
|
40
|
+
if (current) {
|
|
41
|
+
observer.next(current);
|
|
42
|
+
this.events.claimEvent(current, observer);
|
|
43
|
+
}
|
|
44
|
+
// subscribe to future events
|
|
45
|
+
const inserted = this.events.inserted.subscribe((event) => {
|
|
46
|
+
if (getEventUID(event) === uid && (!current || event.created_at > current.created_at)) {
|
|
47
|
+
// remove old claim
|
|
48
|
+
if (current)
|
|
49
|
+
this.events.removeClaim(current, observer);
|
|
50
|
+
current = event;
|
|
51
|
+
observer.next(event);
|
|
52
|
+
// claim new event
|
|
53
|
+
this.events.claimEvent(current, observer);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// subscribe to deleted events
|
|
57
|
+
const deleted = this.events.deleted.subscribe((event) => {
|
|
58
|
+
if (getEventUID(event) === uid && current) {
|
|
59
|
+
this.events.removeClaim(current, observer);
|
|
60
|
+
current = undefined;
|
|
61
|
+
observer.next(undefined);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.singles.set(observer, uid);
|
|
55
65
|
return () => {
|
|
56
|
-
|
|
66
|
+
inserted.unsubscribe();
|
|
67
|
+
deleted.unsubscribe();
|
|
68
|
+
this.singles.delete(observer);
|
|
69
|
+
if (current)
|
|
70
|
+
this.events.removeClaim(current, observer);
|
|
57
71
|
};
|
|
58
72
|
});
|
|
59
73
|
}
|
|
60
74
|
/** Creates an observable that streams all events that match the filter */
|
|
61
75
|
stream(filters) {
|
|
62
|
-
return new Observable((
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
return new Observable((observer) => {
|
|
77
|
+
let claimed = new Set();
|
|
78
|
+
let events = this.events.getForFilters(filters);
|
|
79
|
+
for (const event of events) {
|
|
80
|
+
observer.next(event);
|
|
81
|
+
this.events.claimEvent(event, observer);
|
|
82
|
+
claimed.add(event);
|
|
83
|
+
}
|
|
84
|
+
// subscribe to future events
|
|
85
|
+
const sub = this.events.inserted.subscribe((event) => {
|
|
86
|
+
if (matchFilters(filters, event)) {
|
|
87
|
+
observer.next(event);
|
|
88
|
+
this.events.claimEvent(event, observer);
|
|
89
|
+
claimed.add(event);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
this.streams.set(observer, filters);
|
|
67
93
|
return () => {
|
|
68
|
-
|
|
94
|
+
sub.unsubscribe();
|
|
95
|
+
this.streams.delete(observer);
|
|
96
|
+
// remove all claims
|
|
97
|
+
for (const event of claimed)
|
|
98
|
+
this.events.removeClaim(event, observer);
|
|
99
|
+
claimed.clear();
|
|
69
100
|
};
|
|
70
101
|
});
|
|
71
102
|
}
|
|
72
103
|
/** Creates an observable that updates with an array of sorted events */
|
|
73
104
|
timeline(filters) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
return new Observable((observer) => {
|
|
106
|
+
const seen = new Map();
|
|
107
|
+
const timeline = [];
|
|
108
|
+
// build initial timeline
|
|
109
|
+
const events = this.events.getForFilters(filters);
|
|
110
|
+
for (const event of events) {
|
|
111
|
+
insertEventIntoDescendingList(timeline, event);
|
|
112
|
+
this.events.claimEvent(event, observer);
|
|
113
|
+
seen.set(getEventUID(event), event);
|
|
114
|
+
}
|
|
115
|
+
observer.next([...timeline]);
|
|
116
|
+
// subscribe to future events
|
|
117
|
+
const inserted = this.events.inserted.subscribe((event) => {
|
|
118
|
+
if (matchFilters(filters, event)) {
|
|
119
|
+
const uid = getEventUID(event);
|
|
120
|
+
let current = seen.get(uid);
|
|
121
|
+
if (current) {
|
|
122
|
+
if (event.created_at > current.created_at) {
|
|
123
|
+
// replace event
|
|
124
|
+
timeline.splice(timeline.indexOf(current), 1, event);
|
|
125
|
+
observer.next([...timeline]);
|
|
126
|
+
// update the claim
|
|
127
|
+
seen.set(uid, event);
|
|
128
|
+
this.events.removeClaim(current, observer);
|
|
129
|
+
this.events.claimEvent(event, observer);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
insertEventIntoDescendingList(timeline, event);
|
|
134
|
+
observer.next([...timeline]);
|
|
135
|
+
// claim new event
|
|
136
|
+
this.events.claimEvent(event, observer);
|
|
137
|
+
seen.set(getEventUID(event), event);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// subscribe to removed events
|
|
142
|
+
const deleted = this.events.deleted.subscribe((event) => {
|
|
143
|
+
const uid = getEventUID(event);
|
|
144
|
+
let current = seen.get(uid);
|
|
145
|
+
if (current) {
|
|
146
|
+
// remove the event
|
|
147
|
+
timeline.splice(timeline.indexOf(current), 1);
|
|
148
|
+
observer.next([...timeline]);
|
|
149
|
+
// remove the claim
|
|
150
|
+
seen.delete(uid);
|
|
151
|
+
this.events.removeClaim(current, observer);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
this.timelines.set(observer, filters);
|
|
155
|
+
return () => {
|
|
156
|
+
this.timelines.delete(observer);
|
|
157
|
+
inserted.unsubscribe();
|
|
158
|
+
deleted.unsubscribe();
|
|
159
|
+
// remove all claims
|
|
160
|
+
for (const [_, event] of seen) {
|
|
161
|
+
this.events.removeClaim(event, observer);
|
|
162
|
+
}
|
|
163
|
+
seen.clear();
|
|
164
|
+
};
|
|
78
165
|
});
|
|
79
166
|
}
|
|
80
167
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./event-store.js";
|
|
2
|
+
export * from "./database.js";
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "./event-store.js";
|
|
2
|
+
export * from "./database.js";
|
package/dist/helpers/event.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
/**
|
|
3
|
+
* Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
|
|
4
|
+
* or parameterized replaceable ( 30000 <= n < 40000 )
|
|
5
|
+
*/
|
|
2
6
|
export declare function isReplaceable(kind: number): boolean;
|
|
3
7
|
/** returns the events Unique ID */
|
|
4
8
|
export declare function getEventUID(event: NostrEvent): string;
|
package/dist/helpers/event.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { kinds } from "nostr-tools";
|
|
2
2
|
import { INDEXABLE_TAGS } from "../event-store/common.js";
|
|
3
3
|
import { EventIndexableTags, EventUID } from "./symbols.js";
|
|
4
|
+
/**
|
|
5
|
+
* Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
|
|
6
|
+
* or parameterized replaceable ( 30000 <= n < 40000 )
|
|
7
|
+
*/
|
|
4
8
|
export function isReplaceable(kind) {
|
|
5
9
|
return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind);
|
|
6
10
|
}
|
package/dist/helpers/filter.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
/**
|
|
3
|
+
* Copied from nostr-tools and modified to use getIndexableTags
|
|
4
|
+
* @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
|
|
5
|
+
*/
|
|
2
6
|
export declare function matchFilter(filter: Filter, event: NostrEvent): boolean;
|
|
7
|
+
/** Copied from nostr-tools */
|
|
3
8
|
export declare function matchFilters(filters: Filter[], event: NostrEvent): boolean;
|
package/dist/helpers/filter.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getIndexableTags } from "./event.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Copied from nostr-tools and modified to use getIndexableTags
|
|
4
|
+
* @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
|
|
5
|
+
*/
|
|
4
6
|
export function matchFilter(filter, event) {
|
|
5
7
|
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
|
|
6
8
|
return false;
|
|
@@ -28,6 +30,7 @@ export function matchFilter(filter, event) {
|
|
|
28
30
|
return false;
|
|
29
31
|
return true;
|
|
30
32
|
}
|
|
33
|
+
/** Copied from nostr-tools */
|
|
31
34
|
export function matchFilters(filters, event) {
|
|
32
35
|
for (let i = 0; i < filters.length; i++) {
|
|
33
36
|
if (matchFilter(filters[i], event)) {
|
package/dist/helpers/index.d.ts
CHANGED
package/dist/helpers/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
/**
|
|
3
|
+
* Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxes} symbol
|
|
4
|
+
*/
|
|
2
5
|
export declare function getInboxes(event: NostrEvent): Set<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxes} symbol
|
|
8
|
+
*/
|
|
3
9
|
export declare function getOutboxes(event: NostrEvent): Set<string>;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { safeRelayUrl } from "./relays.js";
|
|
2
2
|
import { MailboxesInboxes, MailboxesOutboxes } from "./symbols.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxes} symbol
|
|
5
|
+
*/
|
|
3
6
|
export function getInboxes(event) {
|
|
4
7
|
if (!event[MailboxesInboxes]) {
|
|
5
8
|
const inboxes = new Set();
|
|
@@ -14,6 +17,9 @@ export function getInboxes(event) {
|
|
|
14
17
|
}
|
|
15
18
|
return event[MailboxesInboxes];
|
|
16
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxes} symbol
|
|
22
|
+
*/
|
|
17
23
|
export function getOutboxes(event) {
|
|
18
24
|
if (!event[MailboxesOutboxes]) {
|
|
19
25
|
const outboxes = new Set();
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import Observable from "zen-observable";
|
|
2
|
-
import { LRU } from "tiny-lru";
|
|
3
2
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
4
3
|
import { EventStore } from "../event-store/event-store.js";
|
|
5
4
|
import { ProfileContent } from "../helpers/profile.js";
|
|
5
|
+
import { LRU } from "../utils/lru.js";
|
|
6
6
|
export declare class QueryStore {
|
|
7
7
|
store: EventStore;
|
|
8
8
|
constructor(store: EventStore);
|
|
9
|
-
singleEvents: LRU<Observable<import("nostr-tools").Event>>;
|
|
10
|
-
getEvent(id: string): Observable<import("nostr-tools").Event>;
|
|
11
|
-
getReplaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event>;
|
|
9
|
+
singleEvents: LRU<Observable<import("nostr-tools").Event | undefined>>;
|
|
10
|
+
getEvent(id: string): Observable<import("nostr-tools").Event | undefined>;
|
|
11
|
+
getReplaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>;
|
|
12
12
|
timelines: LRU<Observable<import("nostr-tools").Event[]>>;
|
|
13
13
|
getTimeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>;
|
|
14
|
-
profiles: LRU<Observable<ProfileContent>>;
|
|
15
|
-
getProfile(pubkey: string): Observable<ProfileContent>;
|
|
14
|
+
profiles: LRU<Observable<ProfileContent | undefined>>;
|
|
15
|
+
getProfile(pubkey: string): Observable<ProfileContent | undefined>;
|
|
16
16
|
reactions: LRU<Observable<import("nostr-tools").Event[]>>;
|
|
17
17
|
getReactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
|
|
18
18
|
mailboxes: LRU<Observable<{
|
|
19
19
|
inboxes: Set<string>;
|
|
20
20
|
outboxes: Set<string>;
|
|
21
|
-
}>>;
|
|
21
|
+
} | undefined>>;
|
|
22
22
|
getMailboxes(pubkey: string): Observable<{
|
|
23
23
|
inboxes: Set<string>;
|
|
24
24
|
outboxes: Set<string>;
|
|
25
|
-
}>;
|
|
25
|
+
} | undefined>;
|
|
26
26
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { LRU } from "tiny-lru";
|
|
2
1
|
import { kinds } from "nostr-tools";
|
|
3
2
|
import stringify from "json-stringify-deterministic";
|
|
4
3
|
import { stateful } from "../observable/stateful.js";
|
|
5
4
|
import { getProfileContent } from "../helpers/profile.js";
|
|
6
5
|
import { getEventUID, getReplaceableUID, isReplaceable } from "../helpers/event.js";
|
|
7
6
|
import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
|
|
7
|
+
import { LRU } from "../utils/lru.js";
|
|
8
8
|
export class QueryStore {
|
|
9
9
|
store;
|
|
10
10
|
constructor(store) {
|
|
@@ -33,7 +33,7 @@ export class QueryStore {
|
|
|
33
33
|
profiles = new LRU();
|
|
34
34
|
getProfile(pubkey) {
|
|
35
35
|
if (!this.profiles.has(pubkey)) {
|
|
36
|
-
const observable = stateful(this.getReplaceable(kinds.Metadata, pubkey).map((event) => getProfileContent(event)));
|
|
36
|
+
const observable = stateful(this.getReplaceable(kinds.Metadata, pubkey).map((event) => event && getProfileContent(event)));
|
|
37
37
|
this.profiles.set(pubkey, observable);
|
|
38
38
|
}
|
|
39
39
|
return this.profiles.get(pubkey);
|
|
@@ -60,10 +60,10 @@ export class QueryStore {
|
|
|
60
60
|
mailboxes = new LRU();
|
|
61
61
|
getMailboxes(pubkey) {
|
|
62
62
|
if (!this.mailboxes.has(pubkey)) {
|
|
63
|
-
const observable = stateful(this.getReplaceable(kinds.RelayList, pubkey).map((event) =>
|
|
63
|
+
const observable = stateful(this.getReplaceable(kinds.RelayList, pubkey).map((event) => event && {
|
|
64
64
|
inboxes: getInboxes(event),
|
|
65
65
|
outboxes: getOutboxes(event),
|
|
66
|
-
}))
|
|
66
|
+
}));
|
|
67
67
|
this.mailboxes.set(pubkey, observable);
|
|
68
68
|
}
|
|
69
69
|
return this.mailboxes.get(pubkey);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type Item<T> = {
|
|
2
|
+
key: string;
|
|
3
|
+
prev: Item<T> | null;
|
|
4
|
+
value: T;
|
|
5
|
+
next: Item<T> | null;
|
|
6
|
+
expiry: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Copied from tiny-lru and modified to support typescript
|
|
10
|
+
* @see https://github.com/avoidwork/tiny-lru/blob/master/src/lru.js
|
|
11
|
+
*/
|
|
12
|
+
export declare class LRU<T extends unknown> {
|
|
13
|
+
first: Item<T> | null;
|
|
14
|
+
items: Record<string, Item<T>>;
|
|
15
|
+
last: Item<T> | null;
|
|
16
|
+
max: number;
|
|
17
|
+
resetTtl: boolean;
|
|
18
|
+
size: number;
|
|
19
|
+
ttl: number;
|
|
20
|
+
constructor(max?: number, ttl?: number, resetTtl?: boolean);
|
|
21
|
+
clear(): this;
|
|
22
|
+
delete(key: string): this;
|
|
23
|
+
entries(keys?: string[]): (string | T | undefined)[][];
|
|
24
|
+
evict(bypass?: boolean): this;
|
|
25
|
+
expiresAt(key: string): number | undefined;
|
|
26
|
+
get(key: string): T | undefined;
|
|
27
|
+
has(key: string): boolean;
|
|
28
|
+
keys(): string[];
|
|
29
|
+
set(key: string, value: T, bypass?: boolean, resetTtl?: boolean): this;
|
|
30
|
+
values(keys?: string[]): NonNullable<T>[];
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copied from tiny-lru and modified to support typescript
|
|
3
|
+
* @see https://github.com/avoidwork/tiny-lru/blob/master/src/lru.js
|
|
4
|
+
*/
|
|
5
|
+
export class LRU {
|
|
6
|
+
first = null;
|
|
7
|
+
items = Object.create(null);
|
|
8
|
+
last = null;
|
|
9
|
+
max;
|
|
10
|
+
resetTtl;
|
|
11
|
+
size;
|
|
12
|
+
ttl;
|
|
13
|
+
constructor(max = 0, ttl = 0, resetTtl = false) {
|
|
14
|
+
this.first = null;
|
|
15
|
+
this.items = Object.create(null);
|
|
16
|
+
this.last = null;
|
|
17
|
+
this.max = max;
|
|
18
|
+
this.resetTtl = resetTtl;
|
|
19
|
+
this.size = 0;
|
|
20
|
+
this.ttl = ttl;
|
|
21
|
+
}
|
|
22
|
+
clear() {
|
|
23
|
+
this.first = null;
|
|
24
|
+
this.items = Object.create(null);
|
|
25
|
+
this.last = null;
|
|
26
|
+
this.size = 0;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
delete(key) {
|
|
30
|
+
if (this.has(key)) {
|
|
31
|
+
const item = this.items[key];
|
|
32
|
+
delete this.items[key];
|
|
33
|
+
this.size--;
|
|
34
|
+
if (item.prev !== null) {
|
|
35
|
+
item.prev.next = item.next;
|
|
36
|
+
}
|
|
37
|
+
if (item.next !== null) {
|
|
38
|
+
item.next.prev = item.prev;
|
|
39
|
+
}
|
|
40
|
+
if (this.first === item) {
|
|
41
|
+
this.first = item.next;
|
|
42
|
+
}
|
|
43
|
+
if (this.last === item) {
|
|
44
|
+
this.last = item.prev;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
entries(keys = this.keys()) {
|
|
50
|
+
return keys.map((key) => [key, this.get(key)]);
|
|
51
|
+
}
|
|
52
|
+
evict(bypass = false) {
|
|
53
|
+
if (bypass || this.size > 0) {
|
|
54
|
+
const item = this.first;
|
|
55
|
+
delete this.items[item.key];
|
|
56
|
+
if (--this.size === 0) {
|
|
57
|
+
this.first = null;
|
|
58
|
+
this.last = null;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.first = item.next;
|
|
62
|
+
this.first.prev = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
expiresAt(key) {
|
|
68
|
+
let result;
|
|
69
|
+
if (this.has(key)) {
|
|
70
|
+
result = this.items[key].expiry;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
get(key) {
|
|
75
|
+
let result;
|
|
76
|
+
if (this.has(key)) {
|
|
77
|
+
const item = this.items[key];
|
|
78
|
+
if (this.ttl > 0 && item.expiry <= Date.now()) {
|
|
79
|
+
this.delete(key);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
result = item.value;
|
|
83
|
+
this.set(key, result, true);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
has(key) {
|
|
89
|
+
return key in this.items;
|
|
90
|
+
}
|
|
91
|
+
keys() {
|
|
92
|
+
const result = [];
|
|
93
|
+
let x = this.first;
|
|
94
|
+
while (x !== null) {
|
|
95
|
+
result.push(x.key);
|
|
96
|
+
x = x.next;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
set(key, value, bypass = false, resetTtl = this.resetTtl) {
|
|
101
|
+
let item;
|
|
102
|
+
if (bypass || this.has(key)) {
|
|
103
|
+
item = this.items[key];
|
|
104
|
+
item.value = value;
|
|
105
|
+
if (bypass === false && resetTtl) {
|
|
106
|
+
item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;
|
|
107
|
+
}
|
|
108
|
+
if (this.last !== item) {
|
|
109
|
+
const last = this.last, next = item.next, prev = item.prev;
|
|
110
|
+
if (this.first === item) {
|
|
111
|
+
this.first = item.next;
|
|
112
|
+
}
|
|
113
|
+
item.next = null;
|
|
114
|
+
item.prev = this.last;
|
|
115
|
+
last.next = item;
|
|
116
|
+
if (prev !== null) {
|
|
117
|
+
prev.next = next;
|
|
118
|
+
}
|
|
119
|
+
if (next !== null) {
|
|
120
|
+
next.prev = prev;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (this.max > 0 && this.size === this.max) {
|
|
126
|
+
this.evict(true);
|
|
127
|
+
}
|
|
128
|
+
item = this.items[key] = {
|
|
129
|
+
expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,
|
|
130
|
+
key: key,
|
|
131
|
+
prev: this.last,
|
|
132
|
+
next: null,
|
|
133
|
+
value,
|
|
134
|
+
};
|
|
135
|
+
if (++this.size === 1) {
|
|
136
|
+
this.first = item;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.last.next = item;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.last = item;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
values(keys = this.keys()) {
|
|
146
|
+
return keys.map((key) => this.get(key));
|
|
147
|
+
}
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,11 +36,12 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@types/zen-push": "^0.1.4",
|
|
39
40
|
"debug": "^4.3.7",
|
|
40
41
|
"json-stringify-deterministic": "^1.0.12",
|
|
41
42
|
"nanoid": "^5.0.7",
|
|
42
43
|
"nostr-tools": "^2.7.2",
|
|
43
|
-
"
|
|
44
|
+
"zen-push": "^0.3.1"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/debug": "^4.1.12",
|