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.
Files changed (50) hide show
  1. package/README.md +39 -0
  2. package/dist/event-store/database.d.ts +57 -13
  3. package/dist/event-store/database.js +52 -15
  4. package/dist/event-store/event-store.d.ts +4 -1
  5. package/dist/event-store/event-store.js +122 -31
  6. package/dist/event-store/index.d.ts +2 -1
  7. package/dist/event-store/index.js +2 -1
  8. package/dist/helpers/channel.d.ts +15 -0
  9. package/dist/helpers/channel.js +27 -0
  10. package/dist/helpers/event.d.ts +12 -0
  11. package/dist/helpers/event.js +13 -8
  12. package/dist/helpers/filter.d.ts +5 -0
  13. package/dist/helpers/filter.js +5 -2
  14. package/dist/helpers/index.d.ts +1 -1
  15. package/dist/helpers/index.js +1 -1
  16. package/dist/helpers/json.d.ts +1 -0
  17. package/dist/helpers/json.js +8 -0
  18. package/dist/helpers/mailboxes.d.ts +14 -0
  19. package/dist/helpers/mailboxes.js +14 -7
  20. package/dist/helpers/mailboxes.test.d.ts +1 -0
  21. package/dist/helpers/mailboxes.test.js +80 -0
  22. package/dist/helpers/mute.d.ts +21 -0
  23. package/dist/helpers/mute.js +52 -0
  24. package/dist/helpers/profile.d.ts +6 -0
  25. package/dist/helpers/profile.js +4 -4
  26. package/dist/helpers/relays.d.ts +6 -0
  27. package/dist/helpers/relays.js +7 -6
  28. package/dist/promise/index.d.ts +1 -1
  29. package/dist/promise/index.js +1 -1
  30. package/dist/query-store/index.d.ts +35 -17
  31. package/dist/query-store/index.js +40 -58
  32. package/dist/query-store/queries/channel.d.ts +11 -0
  33. package/dist/query-store/queries/channel.js +72 -0
  34. package/dist/query-store/queries/index.d.ts +6 -0
  35. package/dist/query-store/queries/index.js +6 -0
  36. package/dist/query-store/queries/mailboxes.d.ts +5 -0
  37. package/dist/query-store/queries/mailboxes.js +11 -0
  38. package/dist/query-store/queries/mute.d.ts +7 -0
  39. package/dist/query-store/queries/mute.js +16 -0
  40. package/dist/query-store/queries/profile.d.ts +3 -0
  41. package/dist/query-store/queries/profile.js +10 -0
  42. package/dist/query-store/queries/reactions.d.ts +4 -0
  43. package/dist/query-store/queries/reactions.js +19 -0
  44. package/dist/query-store/queries/simple.d.ts +5 -0
  45. package/dist/query-store/queries/simple.js +20 -0
  46. package/dist/utils/lru.d.ts +32 -0
  47. package/dist/utils/lru.js +148 -0
  48. package/package.json +19 -3
  49. package/dist/helpers/symbols.d.ts +0 -17
  50. 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 "tiny-lru";
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
- constructor(max?: number);
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
- private getKindIndex;
17
- private getAuthorsIndex;
18
- private getTagIndex;
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(eventOrId: string | NostrEvent): boolean;
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
- prune(): number | undefined;
76
+ /** Remove the oldest events that are not claimed */
77
+ prune(limit?: number): number;
34
78
  }
@@ -1,12 +1,14 @@
1
- import { LRU } from "tiny-lru";
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 { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
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
- constructor(max) {
18
- this.max = max;
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(eventOrId) {
80
- let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
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
- prune() {
228
- if (!this.max)
229
- return;
261
+ /** Remove the oldest events that are not claimed */
262
+ prune(limit = 1000) {
230
263
  let removed = 0;
231
- while (this.events.size > this.max) {
232
- const event = this.events.first;
233
- if (event) {
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
- single(uid: string): Observable<import("nostr-tools").Event>;
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
- single(uid) {
50
- return new Observable((control) => {
51
- const event = this.events.getEvent(uid);
52
- if (event)
53
- control.next(event);
54
- this.singles.set(control, uid);
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
- this.singles.delete(control);
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((control) => {
63
- const events = this.events.getForFilters(filters);
64
- for (const event of events)
65
- control.next(event);
66
- this.streams.set(control, filters);
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
- this.streams.delete(control);
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
- let events = [];
75
- return this.stream(filters).map((event) => {
76
- insertEventIntoDescendingList(events, event);
77
- return events;
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 './event-store.js';
1
+ export * from "./event-store.js";
2
+ export * from "./database.js";
@@ -1 +1,2 @@
1
- export * from './event-store.js';
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
+ }
@@ -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;
@@ -1,34 +1,39 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { INDEXABLE_TAGS } from "../event-store/common.js";
3
- import { EventIndexableTags, EventUID } from "./symbols.js";
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[EventUID]) {
14
+ if (!event[EventUIDSymbol]) {
10
15
  if (isReplaceable(event.kind)) {
11
16
  const d = event.tags.find((t) => t[0] === "d")?.[1];
12
- event[EventUID] = getReplaceableUID(event.kind, event.pubkey, d);
17
+ event[EventUIDSymbol] = getReplaceableUID(event.kind, event.pubkey, d);
13
18
  }
14
19
  else {
15
- event[EventUID] = event.id;
20
+ event[EventUIDSymbol] = event.id;
16
21
  }
17
22
  }
18
- return event[EventUID];
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[EventIndexableTags]) {
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[EventIndexableTags] = tags;
36
+ event[EventIndexableTagsSymbol] = tags;
32
37
  }
33
- return event[EventIndexableTags];
38
+ return event[EventIndexableTagsSymbol];
34
39
  }
@@ -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;
@@ -1,6 +1,8 @@
1
1
  import { getIndexableTags } from "./event.js";
2
- // Copied from https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
3
- // And modified to use getIndexableTags
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)) {
@@ -2,4 +2,4 @@ export * from "./profile.js";
2
2
  export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
- export * as symbols from "./symbols.js";
5
+ export * from "./mailboxes.js";
@@ -2,4 +2,4 @@ export * from "./profile.js";
2
2
  export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
- export * as symbols from "./symbols.js";
5
+ export * from "./mailboxes.js";
@@ -0,0 +1 @@
1
+ export declare function safeParse<T extends unknown = any>(str: string): T | undefined;
@@ -0,0 +1,8 @@
1
+ export function safeParse(str) {
2
+ try {
3
+ return JSON.parse(str);
4
+ }
5
+ catch (error) {
6
+ return undefined;
7
+ }
8
+ }