applesauce-core 0.1.0 → 0.3.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 +39 -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 +4 -1
- package/dist/event-store/event-store.js +122 -31
- package/dist/event-store/index.d.ts +2 -1
- package/dist/event-store/index.js +2 -1
- package/dist/helpers/channel.d.ts +15 -0
- package/dist/helpers/channel.js +27 -0
- package/dist/helpers/event.d.ts +12 -0
- package/dist/helpers/event.js +13 -8
- package/dist/helpers/filter.d.ts +5 -0
- package/dist/helpers/filter.js +5 -2
- package/dist/helpers/index.d.ts +1 -1
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/json.d.ts +1 -0
- package/dist/helpers/json.js +8 -0
- package/dist/helpers/mailboxes.d.ts +14 -0
- package/dist/helpers/mailboxes.js +14 -7
- package/dist/helpers/mailboxes.test.d.ts +1 -0
- package/dist/helpers/mailboxes.test.js +80 -0
- package/dist/helpers/mute.d.ts +21 -0
- package/dist/helpers/mute.js +52 -0
- package/dist/helpers/profile.d.ts +6 -0
- package/dist/helpers/profile.js +4 -4
- package/dist/helpers/relays.d.ts +6 -0
- package/dist/helpers/relays.js +7 -6
- package/dist/promise/index.d.ts +1 -1
- package/dist/promise/index.js +1 -1
- package/dist/query-store/index.d.ts +35 -17
- package/dist/query-store/index.js +40 -58
- package/dist/query-store/queries/channel.d.ts +11 -0
- package/dist/query-store/queries/channel.js +72 -0
- package/dist/query-store/queries/index.d.ts +6 -0
- package/dist/query-store/queries/index.js +6 -0
- package/dist/query-store/queries/mailboxes.d.ts +5 -0
- package/dist/query-store/queries/mailboxes.js +11 -0
- package/dist/query-store/queries/mute.d.ts +7 -0
- package/dist/query-store/queries/mute.js +16 -0
- package/dist/query-store/queries/profile.d.ts +3 -0
- package/dist/query-store/queries/profile.js +10 -0
- package/dist/query-store/queries/reactions.d.ts +4 -0
- package/dist/query-store/queries/reactions.js +19 -0
- package/dist/query-store/queries/simple.d.ts +5 -0
- package/dist/query-store/queries/simple.js +20 -0
- package/dist/utils/lru.d.ts +32 -0
- package/dist/utils/lru.js +148 -0
- package/package.json +19 -3
- package/dist/helpers/symbols.d.ts +0 -17
- package/dist/helpers/symbols.js +0 -10
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# applesauce-core
|
|
2
|
+
|
|
3
|
+
AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/modules/Queries)
|
|
4
|
+
|
|
5
|
+
# Example
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { EventStore, QueryStore } from "applesauce-core";
|
|
9
|
+
import { Relay } from "nostr-tools/relay";
|
|
10
|
+
|
|
11
|
+
// The EventStore handles all the events
|
|
12
|
+
const eventStore = new EventStore();
|
|
13
|
+
|
|
14
|
+
// The QueryStore handles queries and makes sure not to run multiple of the same query
|
|
15
|
+
const queryStore = new QueryStore(eventStore);
|
|
16
|
+
|
|
17
|
+
// Use nostr-tools or anything else to talk to relays
|
|
18
|
+
const relay = await Relay.connect("wss://relay.example.com");
|
|
19
|
+
|
|
20
|
+
const sub = relay.subscribe([{ authors: ["266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5"] }], {
|
|
21
|
+
onevent(event) {
|
|
22
|
+
eventStore.add(event);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// This will return an Observable<ProfileContent | undefined> of the parsed metadata
|
|
27
|
+
const profile = queryStore.profile("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
28
|
+
|
|
29
|
+
profile.subscribe((parsed) => {
|
|
30
|
+
if (parsed) console.log(parsed);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// This will return an Observable<NostrEvent[]> of all kind 1 events sorted by created_at
|
|
34
|
+
const timeline = queryStore.timeline({ kinds: [1] });
|
|
35
|
+
|
|
36
|
+
timeline.subscribe((events) => {
|
|
37
|
+
console.log(events);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
@@ -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,9 @@ 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
|
-
|
|
17
|
+
event(uid: string): Observable<import("nostr-tools").Event | undefined>;
|
|
18
|
+
/** Creates an observable that updates a single replaceable event */
|
|
19
|
+
replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>;
|
|
17
20
|
/** Creates an observable that streams all events that match the filter */
|
|
18
21
|
stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
|
|
19
22
|
/** Creates an observable that updates with an array of sorted events */
|
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import { insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import Observable from "zen-observable";
|
|
3
3
|
import { Database } from "./database.js";
|
|
4
|
-
import { getEventUID } from "../helpers/event.js";
|
|
4
|
+
import { getEventUID, getReplaceableUID } from "../helpers/event.js";
|
|
5
5
|
import { matchFilters } from "../helpers/filter.js";
|
|
6
6
|
import { addSeenRelay } from "../helpers/relays.js";
|
|
7
7
|
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;
|
|
@@ -46,35 +34,138 @@ export class EventStore {
|
|
|
46
34
|
return this.events.getReplaceable(kind, pubkey, d);
|
|
47
35
|
}
|
|
48
36
|
/** Creates an observable that updates a single event */
|
|
49
|
-
|
|
50
|
-
return new Observable((
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
37
|
+
event(uid) {
|
|
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
|
}
|
|
74
|
+
/** Creates an observable that updates a single replaceable event */
|
|
75
|
+
replaceable(kind, pubkey, d) {
|
|
76
|
+
return this.event(getReplaceableUID(kind, pubkey, d));
|
|
77
|
+
}
|
|
60
78
|
/** Creates an observable that streams all events that match the filter */
|
|
61
79
|
stream(filters) {
|
|
62
|
-
return new Observable((
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
return new Observable((observer) => {
|
|
81
|
+
let claimed = new Set();
|
|
82
|
+
let events = this.events.getForFilters(filters);
|
|
83
|
+
for (const event of events) {
|
|
84
|
+
observer.next(event);
|
|
85
|
+
this.events.claimEvent(event, observer);
|
|
86
|
+
claimed.add(event);
|
|
87
|
+
}
|
|
88
|
+
// subscribe to future events
|
|
89
|
+
const sub = this.events.inserted.subscribe((event) => {
|
|
90
|
+
if (matchFilters(filters, event)) {
|
|
91
|
+
observer.next(event);
|
|
92
|
+
this.events.claimEvent(event, observer);
|
|
93
|
+
claimed.add(event);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
this.streams.set(observer, filters);
|
|
67
97
|
return () => {
|
|
68
|
-
|
|
98
|
+
sub.unsubscribe();
|
|
99
|
+
this.streams.delete(observer);
|
|
100
|
+
// remove all claims
|
|
101
|
+
for (const event of claimed)
|
|
102
|
+
this.events.removeClaim(event, observer);
|
|
103
|
+
claimed.clear();
|
|
69
104
|
};
|
|
70
105
|
});
|
|
71
106
|
}
|
|
72
107
|
/** Creates an observable that updates with an array of sorted events */
|
|
73
108
|
timeline(filters) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
return new Observable((observer) => {
|
|
110
|
+
const seen = new Map();
|
|
111
|
+
const timeline = [];
|
|
112
|
+
// build initial timeline
|
|
113
|
+
const events = this.events.getForFilters(filters);
|
|
114
|
+
for (const event of events) {
|
|
115
|
+
insertEventIntoDescendingList(timeline, event);
|
|
116
|
+
this.events.claimEvent(event, observer);
|
|
117
|
+
seen.set(getEventUID(event), event);
|
|
118
|
+
}
|
|
119
|
+
observer.next([...timeline]);
|
|
120
|
+
// subscribe to future events
|
|
121
|
+
const inserted = this.events.inserted.subscribe((event) => {
|
|
122
|
+
if (matchFilters(filters, event)) {
|
|
123
|
+
const uid = getEventUID(event);
|
|
124
|
+
let current = seen.get(uid);
|
|
125
|
+
if (current) {
|
|
126
|
+
if (event.created_at > current.created_at) {
|
|
127
|
+
// replace event
|
|
128
|
+
timeline.splice(timeline.indexOf(current), 1, event);
|
|
129
|
+
observer.next([...timeline]);
|
|
130
|
+
// update the claim
|
|
131
|
+
seen.set(uid, event);
|
|
132
|
+
this.events.removeClaim(current, observer);
|
|
133
|
+
this.events.claimEvent(event, observer);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
insertEventIntoDescendingList(timeline, event);
|
|
138
|
+
observer.next([...timeline]);
|
|
139
|
+
// claim new event
|
|
140
|
+
this.events.claimEvent(event, observer);
|
|
141
|
+
seen.set(getEventUID(event), event);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// subscribe to removed events
|
|
146
|
+
const deleted = this.events.deleted.subscribe((event) => {
|
|
147
|
+
const uid = getEventUID(event);
|
|
148
|
+
let current = seen.get(uid);
|
|
149
|
+
if (current) {
|
|
150
|
+
// remove the event
|
|
151
|
+
timeline.splice(timeline.indexOf(current), 1);
|
|
152
|
+
observer.next([...timeline]);
|
|
153
|
+
// remove the claim
|
|
154
|
+
seen.delete(uid);
|
|
155
|
+
this.events.removeClaim(current, observer);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.timelines.set(observer, filters);
|
|
159
|
+
return () => {
|
|
160
|
+
this.timelines.delete(observer);
|
|
161
|
+
inserted.unsubscribe();
|
|
162
|
+
deleted.unsubscribe();
|
|
163
|
+
// remove all claims
|
|
164
|
+
for (const [_, event] of seen) {
|
|
165
|
+
this.events.removeClaim(event, observer);
|
|
166
|
+
}
|
|
167
|
+
seen.clear();
|
|
168
|
+
};
|
|
78
169
|
});
|
|
79
170
|
}
|
|
80
171
|
}
|
|
@@ -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";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { nip19, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { ChannelMetadata } from "nostr-tools/nip28";
|
|
3
|
+
export declare const ChannelMetadataSymbol: unique symbol;
|
|
4
|
+
declare module "nostr-tools" {
|
|
5
|
+
interface Event {
|
|
6
|
+
[ChannelMetadataSymbol]?: ChannelMetadataContent;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export type ChannelMetadataContent = ChannelMetadata & {
|
|
10
|
+
relays?: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Gets the parsed metadata on a channel creation or channel metadata event */
|
|
13
|
+
export declare function getChannelMetadataContent(channel: NostrEvent): ChannelMetadataContent;
|
|
14
|
+
/** gets the EventPointer for a channel message or metadata event */
|
|
15
|
+
export declare function getChannelPointer(event: NostrEvent): nip19.EventPointer | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const ChannelMetadataSymbol = Symbol.for("channel-metadata");
|
|
2
|
+
function parseChannelMetadataContent(channel) {
|
|
3
|
+
const metadata = JSON.parse(channel.content);
|
|
4
|
+
if (metadata.name === undefined)
|
|
5
|
+
throw new Error("Missing name");
|
|
6
|
+
if (metadata.about === undefined)
|
|
7
|
+
throw new Error("Missing about");
|
|
8
|
+
if (metadata.picture === undefined)
|
|
9
|
+
throw new Error("Missing picture");
|
|
10
|
+
if (metadata.relays && !Array.isArray(metadata.relays))
|
|
11
|
+
throw new Error("Invalid relays");
|
|
12
|
+
return metadata;
|
|
13
|
+
}
|
|
14
|
+
/** Gets the parsed metadata on a channel creation or channel metadata event */
|
|
15
|
+
export function getChannelMetadataContent(channel) {
|
|
16
|
+
let metadata = channel[ChannelMetadataSymbol];
|
|
17
|
+
if (!metadata)
|
|
18
|
+
metadata = channel[ChannelMetadataSymbol] = parseChannelMetadataContent(channel);
|
|
19
|
+
return metadata;
|
|
20
|
+
}
|
|
21
|
+
/** gets the EventPointer for a channel message or metadata event */
|
|
22
|
+
export function getChannelPointer(event) {
|
|
23
|
+
const tag = event.tags.find((t) => t[0] === "e" && t[1]);
|
|
24
|
+
if (!tag)
|
|
25
|
+
return undefined;
|
|
26
|
+
return tag[2] ? { id: tag[1], relays: [tag[2]] } : { id: tag[1] };
|
|
27
|
+
}
|
package/dist/helpers/event.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
export declare const EventUIDSymbol: unique symbol;
|
|
3
|
+
export declare const EventIndexableTagsSymbol: unique symbol;
|
|
4
|
+
declare module "nostr-tools" {
|
|
5
|
+
interface Event {
|
|
6
|
+
[EventUIDSymbol]?: string;
|
|
7
|
+
[EventIndexableTagsSymbol]?: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
|
|
12
|
+
* or parameterized replaceable ( 30000 <= n < 40000 )
|
|
13
|
+
*/
|
|
2
14
|
export declare function isReplaceable(kind: number): boolean;
|
|
3
15
|
/** returns the events Unique ID */
|
|
4
16
|
export declare function getEventUID(event: NostrEvent): string;
|
package/dist/helpers/event.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import { kinds } from "nostr-tools";
|
|
2
2
|
import { INDEXABLE_TAGS } from "../event-store/common.js";
|
|
3
|
-
|
|
3
|
+
export const EventUIDSymbol = Symbol.for("event-uid");
|
|
4
|
+
export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
|
|
5
|
+
/**
|
|
6
|
+
* Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
|
|
7
|
+
* or parameterized replaceable ( 30000 <= n < 40000 )
|
|
8
|
+
*/
|
|
4
9
|
export function isReplaceable(kind) {
|
|
5
10
|
return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind);
|
|
6
11
|
}
|
|
7
12
|
/** returns the events Unique ID */
|
|
8
13
|
export function getEventUID(event) {
|
|
9
|
-
if (!event[
|
|
14
|
+
if (!event[EventUIDSymbol]) {
|
|
10
15
|
if (isReplaceable(event.kind)) {
|
|
11
16
|
const d = event.tags.find((t) => t[0] === "d")?.[1];
|
|
12
|
-
event[
|
|
17
|
+
event[EventUIDSymbol] = getReplaceableUID(event.kind, event.pubkey, d);
|
|
13
18
|
}
|
|
14
19
|
else {
|
|
15
|
-
event[
|
|
20
|
+
event[EventUIDSymbol] = event.id;
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
|
-
return event[
|
|
23
|
+
return event[EventUIDSymbol];
|
|
19
24
|
}
|
|
20
25
|
export function getReplaceableUID(kind, pubkey, d) {
|
|
21
26
|
return d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`;
|
|
22
27
|
}
|
|
23
28
|
export function getIndexableTags(event) {
|
|
24
|
-
if (!event[
|
|
29
|
+
if (!event[EventIndexableTagsSymbol]) {
|
|
25
30
|
const tags = new Set();
|
|
26
31
|
for (const tag of event.tags) {
|
|
27
32
|
if (tag[0] && INDEXABLE_TAGS.has(tag[0]) && tag[1]) {
|
|
28
33
|
tags.add(tag[0] + ":" + tag[1]);
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
|
-
event[
|
|
36
|
+
event[EventIndexableTagsSymbol] = tags;
|
|
32
37
|
}
|
|
33
|
-
return event[
|
|
38
|
+
return event[EventIndexableTagsSymbol];
|
|
34
39
|
}
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function safeParse<T extends unknown = any>(str: string): T | undefined;
|