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 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 "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,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((control) => {
51
- const event = this.events.getEvent(uid);
52
- if (event)
53
- control.next(event);
54
- this.singles.set(control, 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
  }
60
74
  /** Creates an observable that streams all events that match the filter */
61
75
  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);
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
- this.streams.delete(control);
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
- let events = [];
75
- return this.stream(filters).map((event) => {
76
- insertEventIntoDescendingList(events, event);
77
- return events;
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 './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";
@@ -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;
@@ -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
  }
@@ -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,5 @@ export * from "./profile.js";
2
2
  export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
+ export * from "./mailboxes.js";
5
6
  export * as symbols from "./symbols.js";
@@ -2,4 +2,5 @@ export * from "./profile.js";
2
2
  export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
+ export * from "./mailboxes.js";
5
6
  export * as symbols from "./symbols.js";
@@ -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.1.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
- "tiny-lru": "^11.2.11"
44
+ "zen-push": "^0.3.1"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/debug": "^4.1.12",