applesauce-core 0.0.0-next-20250526151506 → 0.0.0-next-20250606170247

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 (134) hide show
  1. package/README.md +7 -13
  2. package/dist/__tests__/exports.test.js +3 -4
  3. package/dist/__tests__/fixtures.d.ts +10 -1
  4. package/dist/__tests__/fixtures.js +9 -1
  5. package/dist/event-store/__tests__/event-store.test.js +43 -17
  6. package/dist/event-store/event-set.d.ts +82 -0
  7. package/dist/event-store/event-set.js +347 -0
  8. package/dist/event-store/event-store.d.ts +55 -20
  9. package/dist/event-store/event-store.js +147 -200
  10. package/dist/event-store/index.d.ts +1 -1
  11. package/dist/event-store/index.js +1 -1
  12. package/dist/event-store/interface.d.ts +71 -13
  13. package/dist/helpers/__tests__/encrypted-content-cache.test.d.ts +1 -0
  14. package/dist/helpers/__tests__/encrypted-content-cache.test.js +65 -0
  15. package/dist/helpers/__tests__/encryption.test.d.ts +1 -0
  16. package/dist/helpers/__tests__/encryption.test.js +21 -0
  17. package/dist/helpers/__tests__/exports.test.js +52 -5
  18. package/dist/helpers/__tests__/pointers.test.d.ts +1 -0
  19. package/dist/helpers/__tests__/pointers.test.js +118 -0
  20. package/dist/helpers/article.d.ts +9 -0
  21. package/dist/helpers/article.js +21 -0
  22. package/dist/helpers/bookmarks.js +1 -2
  23. package/dist/helpers/encrypted-content-cache.d.ts +15 -0
  24. package/dist/helpers/encrypted-content-cache.js +125 -0
  25. package/dist/helpers/encrypted-content.d.ts +48 -0
  26. package/dist/helpers/encrypted-content.js +65 -0
  27. package/dist/helpers/encryption.d.ts +5 -0
  28. package/dist/helpers/encryption.js +10 -0
  29. package/dist/helpers/event.d.ts +4 -1
  30. package/dist/helpers/event.js +13 -3
  31. package/dist/helpers/expiration.d.ts +6 -0
  32. package/dist/helpers/expiration.js +16 -0
  33. package/dist/helpers/filter.d.ts +1 -3
  34. package/dist/helpers/filter.js +1 -3
  35. package/dist/helpers/gift-wraps.d.ts +17 -5
  36. package/dist/helpers/gift-wraps.js +65 -27
  37. package/dist/helpers/groups.js +1 -1
  38. package/dist/helpers/hidden-content.d.ts +27 -32
  39. package/dist/helpers/hidden-content.js +35 -65
  40. package/dist/helpers/hidden-tags.d.ts +23 -4
  41. package/dist/helpers/hidden-tags.js +39 -4
  42. package/dist/helpers/index.d.ts +7 -1
  43. package/dist/helpers/index.js +7 -1
  44. package/dist/helpers/legacy-direct-messages.d.ts +19 -0
  45. package/dist/helpers/legacy-direct-messages.js +35 -0
  46. package/dist/helpers/legacy-messages.d.ts +21 -0
  47. package/dist/helpers/legacy-messages.js +39 -0
  48. package/dist/helpers/lists.d.ts +1 -1
  49. package/dist/helpers/lists.js +2 -2
  50. package/dist/helpers/messages.d.ts +4 -2
  51. package/dist/helpers/mutes.js +1 -1
  52. package/dist/helpers/pointers.d.ts +19 -0
  53. package/dist/helpers/pointers.js +55 -1
  54. package/dist/helpers/user-status.js +2 -1
  55. package/dist/helpers/wrapped-direct-messages.d.ts +10 -0
  56. package/dist/helpers/wrapped-direct-messages.js +19 -0
  57. package/dist/helpers/wrapped-messages.d.ts +23 -0
  58. package/dist/helpers/wrapped-messages.js +38 -0
  59. package/dist/helpers/zap.d.ts +8 -5
  60. package/dist/helpers/zap.js +11 -6
  61. package/dist/index.d.ts +1 -2
  62. package/dist/index.js +1 -2
  63. package/dist/models/__tests__/comments.test.d.ts +1 -0
  64. package/dist/models/__tests__/comments.test.js +36 -0
  65. package/dist/models/__tests__/exports.test.d.ts +1 -0
  66. package/dist/models/__tests__/exports.test.js +53 -0
  67. package/dist/models/blossom.d.ts +3 -0
  68. package/dist/models/blossom.js +8 -0
  69. package/dist/models/bookmarks.d.ts +8 -0
  70. package/dist/models/bookmarks.js +24 -0
  71. package/dist/models/channels.d.ts +11 -0
  72. package/dist/models/channels.js +61 -0
  73. package/dist/models/comments.d.ts +4 -0
  74. package/dist/models/comments.js +11 -0
  75. package/dist/models/common.d.ts +16 -0
  76. package/dist/models/common.js +176 -0
  77. package/dist/models/contacts.d.ts +8 -0
  78. package/dist/models/contacts.js +24 -0
  79. package/dist/models/encrypted-content.d.ts +4 -0
  80. package/dist/models/encrypted-content.js +11 -0
  81. package/dist/models/gift-wrap.d.ts +7 -0
  82. package/dist/models/gift-wrap.js +20 -0
  83. package/dist/models/index.d.ts +18 -0
  84. package/dist/models/index.js +18 -0
  85. package/dist/models/legacy-direct-messages.d.ts +5 -0
  86. package/dist/models/legacy-direct-messages.js +16 -0
  87. package/dist/models/legacy-messages.d.ts +8 -0
  88. package/dist/models/legacy-messages.js +29 -0
  89. package/dist/models/mailboxes.d.ts +6 -0
  90. package/dist/models/mailboxes.js +10 -0
  91. package/dist/models/mutes.d.ts +8 -0
  92. package/dist/models/mutes.js +24 -0
  93. package/dist/models/pins.d.ts +4 -0
  94. package/dist/models/pins.js +10 -0
  95. package/dist/models/profile.d.ts +4 -0
  96. package/dist/models/profile.js +14 -0
  97. package/dist/models/reactions.d.ts +4 -0
  98. package/dist/models/reactions.js +16 -0
  99. package/dist/models/relays.d.ts +27 -0
  100. package/dist/models/relays.js +44 -0
  101. package/dist/models/simple.d.ts +16 -0
  102. package/dist/models/simple.js +21 -0
  103. package/dist/models/thread.d.ts +26 -0
  104. package/dist/models/thread.js +87 -0
  105. package/dist/models/user-status.d.ts +11 -0
  106. package/dist/models/user-status.js +33 -0
  107. package/dist/models/wrapped-direct-messages.d.ts +14 -0
  108. package/dist/models/wrapped-direct-messages.js +30 -0
  109. package/dist/models/wrapped-messages.d.ts +25 -0
  110. package/dist/models/wrapped-messages.js +61 -0
  111. package/dist/models/zaps.d.ts +9 -0
  112. package/dist/models/zaps.js +26 -0
  113. package/dist/observable/__tests__/claim-events.test.js +4 -4
  114. package/dist/observable/__tests__/claim-latest.test.js +5 -5
  115. package/dist/observable/__tests__/exports.test.js +1 -1
  116. package/dist/observable/claim-events.d.ts +3 -3
  117. package/dist/observable/claim-events.js +4 -4
  118. package/dist/observable/claim-latest.d.ts +3 -3
  119. package/dist/observable/claim-latest.js +4 -4
  120. package/dist/observable/simple-timeout.d.ts +1 -0
  121. package/dist/observable/simple-timeout.js +1 -0
  122. package/dist/observable/watch-event-updates.d.ts +5 -5
  123. package/dist/observable/watch-event-updates.js +16 -5
  124. package/dist/queries/__tests__/exports.test.js +3 -0
  125. package/dist/queries/gift-wrap.d.ts +4 -0
  126. package/dist/queries/gift-wrap.js +12 -0
  127. package/dist/queries/index.d.ts +2 -1
  128. package/dist/queries/index.js +2 -1
  129. package/dist/queries/zaps.d.ts +4 -0
  130. package/dist/queries/zaps.js +8 -0
  131. package/dist/query-store/__tests__/query-store.test.js +2 -2
  132. package/dist/query-store/query-store.d.ts +13 -16
  133. package/dist/query-store/query-store.js +3 -3
  134. package/package.json +9 -14
package/README.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # applesauce-core
2
2
 
3
- AppleSauce is a collection of utilities for building reactive nostr applications. The core package provides an in-memory event database and reactive queries to help you build nostr UIs with less code.
3
+ AppleSauce is a collection of utilities for building reactive nostr applications. The core package provides an in-memory event database and reactive models to help you build nostr UIs with less code.
4
4
 
5
5
  ## Key Components
6
6
 
7
7
  - **Helpers**: Core utility methods for parsing and extracting data from nostr events
8
8
  - **EventStore**: In-memory database for storing and subscribing to nostr events
9
- - **QueryStore**: Manages queries and ensures efficient subscription handling
10
- - **Queries**: Complex subscriptions for common nostr data patterns
9
+ - **Models**: Complex subscriptions for common nostr data patterns
11
10
 
12
11
  ## Documentation
13
12
 
@@ -19,15 +18,13 @@ For detailed documentation and guides, visit:
19
18
  ## Example
20
19
 
21
20
  ```js
22
- import { EventStore, QueryStore } from "applesauce-core";
21
+ import { EventStore } from "applesauce-core";
22
+ import { ProfileModel, TimelineModel } from "applesauce-core/models";
23
23
  import { Relay } from "nostr-tools/relay";
24
24
 
25
25
  // Create a single EventStore instance for your app
26
26
  const eventStore = new EventStore();
27
27
 
28
- // Create a QueryStore to manage subscriptions efficiently
29
- const queryStore = new QueryStore(eventStore);
30
-
31
28
  // Use any nostr library for relay connections (nostr-tools, ndk, nostrify, etc...)
32
29
  const relay = await Relay.connect("wss://relay.example.com");
33
30
 
@@ -38,18 +35,15 @@ const sub = relay.subscribe([{ authors: ["3bf0c63fcb93463407af97a5e5ee64fa883d10
38
35
  },
39
36
  });
40
37
 
41
- // Subscribe to profile changes using ProfileQuery
42
- const profile = queryStore.createQuery(
43
- ProfileQuery,
44
- "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
45
- );
38
+ // Subscribe to profile changes using ProfileModel
39
+ const profile = eventStore.model(ProfileModel, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");
46
40
 
47
41
  profile.subscribe((parsed) => {
48
42
  if (parsed) console.log(parsed);
49
43
  });
50
44
 
51
45
  // Subscribe to a timeline of events
52
- const timeline = queryStore.createQuery(TimelineQuery, { kinds: [1] });
46
+ const timeline = eventStore.model(TimelineModel, { kinds: [1] });
53
47
 
54
48
  timeline.subscribe((events) => {
55
49
  console.log(events);
@@ -4,23 +4,22 @@ describe("exports", () => {
4
4
  it("should export the expected functions", () => {
5
5
  expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
6
  [
7
- "Database",
7
+ "EventSet",
8
8
  "EventStore",
9
9
  "EventStoreSymbol",
10
10
  "Helpers",
11
- "Queries",
12
- "QueryStore",
11
+ "Models",
13
12
  "TimeoutError",
14
13
  "defined",
15
14
  "firstValueFrom",
16
15
  "getObservableValue",
17
16
  "lastValueFrom",
18
- "listenLatestUpdates",
19
17
  "logger",
20
18
  "mapEventsToStore",
21
19
  "mapEventsToTimeline",
22
20
  "simpleTimeout",
23
21
  "watchEventUpdates",
22
+ "watchEventsUpdates",
24
23
  "withImmediateValueOrDefault",
25
24
  ]
26
25
  `);
@@ -1,7 +1,16 @@
1
1
  import type { NostrEvent } from "nostr-tools";
2
- export declare class FakeUser {
2
+ import { EncryptedContentSigner } from "../helpers/encrypted-content.js";
3
+ export declare class FakeUser implements EncryptedContentSigner {
3
4
  key: Uint8Array<ArrayBufferLike>;
4
5
  pubkey: string;
6
+ nip04: {
7
+ encrypt: (pubkey: string, plaintext: string) => string;
8
+ decrypt: (pubkey: string, ciphertext: string) => string;
9
+ };
10
+ nip44: {
11
+ encrypt: (pubkey: string, plaintext: string) => string;
12
+ decrypt: (pubkey: string, ciphertext: string) => string;
13
+ };
5
14
  event(data?: Partial<NostrEvent>): NostrEvent;
6
15
  note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
7
16
  profile(profile: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
@@ -1,8 +1,16 @@
1
- import { finalizeEvent, generateSecretKey, getPublicKey, kinds } from "nostr-tools";
1
+ import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04, nip44 } from "nostr-tools";
2
2
  import { unixNow } from "../helpers/time.js";
3
3
  export class FakeUser {
4
4
  key = generateSecretKey();
5
5
  pubkey = getPublicKey(this.key);
6
+ nip04 = {
7
+ encrypt: (pubkey, plaintext) => nip04.encrypt(this.key, pubkey, plaintext),
8
+ decrypt: (pubkey, ciphertext) => nip04.decrypt(this.key, pubkey, ciphertext),
9
+ };
10
+ nip44 = {
11
+ encrypt: (pubkey, plaintext) => nip44.encrypt(plaintext, nip44.getConversationKey(this.key, pubkey)),
12
+ decrypt: (pubkey, ciphertext) => nip44.decrypt(ciphertext, nip44.getConversationKey(this.key, pubkey)),
13
+ };
6
14
  event(data) {
7
15
  return finalizeEvent({
8
16
  kind: data?.kind ?? kinds.ShortTextNote,
@@ -1,10 +1,12 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { kinds } from "nostr-tools";
3
1
  import { subscribeSpyTo } from "@hirez_io/observer-spy";
4
- import { EventStore } from "../event-store.js";
5
- import { addSeenRelay, getSeenRelays } from "../../helpers/relays.js";
6
- import { getEventUID } from "../../helpers/event.js";
2
+ import { kinds } from "nostr-tools";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
4
  import { FakeUser } from "../../__tests__/fixtures.js";
5
+ import { getEventUID } from "../../helpers/event.js";
6
+ import { addSeenRelay, getSeenRelays } from "../../helpers/relays.js";
7
+ import { EventModel } from "../../models/common.js";
8
+ import { ProfileModel } from "../../models/profile.js";
9
+ import { EventStore } from "../event-store.js";
8
10
  let eventStore;
9
11
  beforeEach(() => {
10
12
  eventStore = new EventStore();
@@ -107,14 +109,14 @@ describe("add", () => {
107
109
  });
108
110
  describe("inserts", () => {
109
111
  it("should emit newer replaceable events", () => {
110
- const spy = subscribeSpyTo(eventStore.inserts);
112
+ const spy = subscribeSpyTo(eventStore.insert$);
111
113
  eventStore.add(profile);
112
114
  const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 100 });
113
115
  eventStore.add(newer);
114
116
  expect(spy.getValues()).toEqual([profile, newer]);
115
117
  });
116
118
  it("should not emit when older replaceable event is added", () => {
117
- const spy = subscribeSpyTo(eventStore.inserts);
119
+ const spy = subscribeSpyTo(eventStore.insert$);
118
120
  eventStore.add(profile);
119
121
  eventStore.add(user.profile({ name: "new name" }, { created_at: profile.created_at - 1000 }));
120
122
  expect(spy.getValues()).toEqual([profile]);
@@ -122,7 +124,7 @@ describe("inserts", () => {
122
124
  });
123
125
  describe("removes", () => {
124
126
  it("should emit older replaceable events when the newest replaceable event is added", () => {
125
- const spy = subscribeSpyTo(eventStore.removes);
127
+ const spy = subscribeSpyTo(eventStore.remove$);
126
128
  eventStore.add(profile);
127
129
  const newer = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
128
130
  eventStore.add(newer);
@@ -159,6 +161,34 @@ describe("removed", () => {
159
161
  expect(spy.receivedComplete()).toBe(true);
160
162
  });
161
163
  });
164
+ describe("model", () => {
165
+ it("should emit synchronous value if it exists", () => {
166
+ let value = undefined;
167
+ eventStore.add(profile);
168
+ eventStore.model(ProfileModel, user.pubkey).subscribe((v) => (value = v));
169
+ expect(value).not.toBe(undefined);
170
+ });
171
+ it("should not emit undefined if value exists", () => {
172
+ eventStore.add(profile);
173
+ const spy = subscribeSpyTo(eventStore.model(EventModel, profile.id));
174
+ expect(spy.getValues()).toEqual([profile]);
175
+ });
176
+ it("should emit synchronous undefined if value does not exists", () => {
177
+ let value = 0;
178
+ eventStore.model(ProfileModel, user.pubkey).subscribe((v) => {
179
+ value = v;
180
+ });
181
+ expect(value).not.toBe(0);
182
+ expect(value).toBe(undefined);
183
+ });
184
+ it("should share latest value", () => {
185
+ eventStore.add(profile);
186
+ const spy = subscribeSpyTo(eventStore.model(EventModel, profile.id));
187
+ const spy2 = subscribeSpyTo(eventStore.model(EventModel, profile.id));
188
+ expect(spy.getValues()).toEqual([profile]);
189
+ expect(spy2.getValues()).toEqual([profile]);
190
+ });
191
+ });
162
192
  describe("event", () => {
163
193
  it("should emit existing event", () => {
164
194
  eventStore.add(profile);
@@ -167,9 +197,9 @@ describe("event", () => {
167
197
  });
168
198
  it("should emit then event when its added", () => {
169
199
  const spy = subscribeSpyTo(eventStore.event(profile.id));
170
- expect(spy.getValues()).toEqual([]);
200
+ expect(spy.getValues()).toEqual([undefined]);
171
201
  eventStore.add(profile);
172
- expect(spy.getValues()).toEqual([profile]);
202
+ expect(spy.getValues()).toEqual([undefined, profile]);
173
203
  });
174
204
  it("should emit undefined when event is removed", () => {
175
205
  eventStore.add(profile);
@@ -191,9 +221,9 @@ describe("event", () => {
191
221
  eventStore.remove(profile);
192
222
  expect(spy.receivedComplete()).toBe(false);
193
223
  });
194
- it("should not emit any values if there are no events", () => {
224
+ it("should emit undefined if event is not found", () => {
195
225
  const spy = subscribeSpyTo(eventStore.event(profile.id));
196
- expect(spy.receivedNext()).toBe(false);
226
+ expect(spy.getValues()).toEqual([undefined]);
197
227
  });
198
228
  });
199
229
  describe("events", () => {
@@ -222,10 +252,6 @@ describe("events", () => {
222
252
  });
223
253
  });
224
254
  describe("replaceable", () => {
225
- it("should not emit till there is an event", () => {
226
- const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
227
- expect(spy.receivedNext()).toBe(false);
228
- });
229
255
  it("should emit existing events", () => {
230
256
  eventStore.add(profile);
231
257
  const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
@@ -269,8 +295,8 @@ describe("replaceable", () => {
269
295
  expect(spy.getValues()).toEqual([profile]);
270
296
  });
271
297
  it("should emit newer events", () => {
272
- const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
273
298
  eventStore.add(profile);
299
+ const spy = subscribeSpyTo(eventStore.replaceable(0, user.pubkey));
274
300
  const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 500 });
275
301
  eventStore.add(newProfile);
276
302
  expect(spy.getValues()).toEqual([profile, newProfile]);
@@ -0,0 +1,82 @@
1
+ import { Filter, NostrEvent } from "nostr-tools";
2
+ import { Subject } from "rxjs";
3
+ import { LRU } from "../helpers/lru.js";
4
+ import { IEventSet } from "./interface.js";
5
+ /**
6
+ * A set of nostr events that can be queried and subscribed to
7
+ * NOTE: does not handle replaceable events or any deletion logic
8
+ */
9
+ export declare class EventSet implements IEventSet {
10
+ protected log: import("debug").Debugger;
11
+ /** Indexes */
12
+ protected kinds: Map<number, Set<import("nostr-tools").Event>>;
13
+ protected authors: Map<string, Set<import("nostr-tools").Event>>;
14
+ protected tags: LRU<Set<import("nostr-tools").Event>>;
15
+ protected created_at: NostrEvent[];
16
+ /** LRU cache of last events touched */
17
+ events: LRU<import("nostr-tools").Event>;
18
+ /** A sorted array of replaceable events by uid */
19
+ protected replaceable: Map<string, import("nostr-tools").Event[]>;
20
+ /** A stream of events inserted into the database */
21
+ insert$: Subject<import("nostr-tools").Event>;
22
+ /** A stream of events that have been updated */
23
+ update$: Subject<import("nostr-tools").Event>;
24
+ /** A stream of events removed from the database */
25
+ remove$: Subject<import("nostr-tools").Event>;
26
+ /** A method thats called before a new event is inserted */
27
+ onBeforeInsert?: (event: NostrEvent) => boolean;
28
+ /** The number of events in the event set */
29
+ get size(): number;
30
+ /** Moves an event to the top of the LRU cache */
31
+ touch(event: NostrEvent): void;
32
+ /** Checks if the database contains an event without touching it */
33
+ hasEvent(id: string): boolean;
34
+ /** Gets a single event based on id */
35
+ getEvent(id: string): NostrEvent | undefined;
36
+ /** Checks if the event set has a replaceable event */
37
+ hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
38
+ /** Gets the latest replaceable event */
39
+ getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
40
+ /** Gets the history of a replaceable event */
41
+ getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
42
+ /** Gets all events that match the filters */
43
+ getByFilters(filters: Filter | Filter[]): Set<NostrEvent>;
44
+ /** Gets a timeline of events that match the filters */
45
+ getTimeline(filters: Filter | Filter[]): NostrEvent[];
46
+ /** Inserts an event into the database and notifies all subscriptions */
47
+ add(event: NostrEvent): NostrEvent | null;
48
+ /** Inserts and event into the database and notifies all subscriptions that the event has updated */
49
+ update(event: NostrEvent): boolean;
50
+ /** Removes an event from the database and notifies all subscriptions */
51
+ remove(eventOrId: string | NostrEvent): boolean;
52
+ /** A weak map of events that are claimed by other things */
53
+ protected claims: WeakMap<import("nostr-tools").Event, any>;
54
+ /** Sets the claim on the event and touches it */
55
+ claim(event: NostrEvent, claim: any): void;
56
+ /** Checks if an event is claimed by anything */
57
+ isClaimed(event: NostrEvent): boolean;
58
+ /** Removes a claim from an event */
59
+ removeClaim(event: NostrEvent, claim: any): void;
60
+ /** Removes all claims on an event */
61
+ clearClaim(event: NostrEvent): void;
62
+ /** Index helper methods */
63
+ protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
64
+ protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
65
+ protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
66
+ /** Iterates over all events by author */
67
+ iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
68
+ /** Iterates over all events by indexable tag and value */
69
+ iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
70
+ /** Iterates over all events by kind */
71
+ iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
72
+ /** Iterates over all events by time */
73
+ iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
74
+ /** Iterates over all events by id */
75
+ iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
76
+ /** Returns all events that match the filter */
77
+ getEventsForFilter(filter: Filter): Set<NostrEvent>;
78
+ /** Returns all events that match the filters */
79
+ getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
80
+ /** Remove the oldest events that are not claimed */
81
+ prune(limit?: number): number;
82
+ }
@@ -0,0 +1,347 @@
1
+ import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
2
+ import { Subject } from "rxjs";
3
+ import { createReplaceableAddress, getEventUID, getIndexableTags, isReplaceable } from "../helpers/event.js";
4
+ import { LRU } from "../helpers/lru.js";
5
+ import { logger } from "../logger.js";
6
+ import { INDEXABLE_TAGS } from "./common.js";
7
+ /**
8
+ * A set of nostr events that can be queried and subscribed to
9
+ * NOTE: does not handle replaceable events or any deletion logic
10
+ */
11
+ export class EventSet {
12
+ log = logger.extend("EventSet");
13
+ /** Indexes */
14
+ kinds = new Map();
15
+ authors = new Map();
16
+ tags = new LRU();
17
+ created_at = [];
18
+ /** LRU cache of last events touched */
19
+ events = new LRU();
20
+ /** A sorted array of replaceable events by uid */
21
+ replaceable = new Map();
22
+ /** A stream of events inserted into the database */
23
+ insert$ = new Subject();
24
+ /** A stream of events that have been updated */
25
+ update$ = new Subject();
26
+ /** A stream of events removed from the database */
27
+ remove$ = new Subject();
28
+ /** A method thats called before a new event is inserted */
29
+ onBeforeInsert;
30
+ /** The number of events in the event set */
31
+ get size() {
32
+ return this.events.size;
33
+ }
34
+ /** Moves an event to the top of the LRU cache */
35
+ touch(event) {
36
+ this.events.set(event.id, event);
37
+ }
38
+ /** Checks if the database contains an event without touching it */
39
+ hasEvent(id) {
40
+ return this.events.has(id);
41
+ }
42
+ /** Gets a single event based on id */
43
+ getEvent(id) {
44
+ return this.events.get(id);
45
+ }
46
+ /** Checks if the event set has a replaceable event */
47
+ hasReplaceable(kind, pubkey, identifier) {
48
+ const events = this.replaceable.get(createReplaceableAddress(kind, pubkey, identifier));
49
+ return !!events && events.length > 0;
50
+ }
51
+ /** Gets the latest replaceable event */
52
+ getReplaceable(kind, pubkey, identifier) {
53
+ const address = createReplaceableAddress(kind, pubkey, identifier);
54
+ const events = this.replaceable.get(address);
55
+ return events?.[0];
56
+ }
57
+ /** Gets the history of a replaceable event */
58
+ getReplaceableHistory(kind, pubkey, identifier) {
59
+ const address = createReplaceableAddress(kind, pubkey, identifier);
60
+ return this.replaceable.get(address);
61
+ }
62
+ /** Gets all events that match the filters */
63
+ getByFilters(filters) {
64
+ return this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
65
+ }
66
+ /** Gets a timeline of events that match the filters */
67
+ getTimeline(filters) {
68
+ const timeline = [];
69
+ const events = this.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
70
+ for (const event of events)
71
+ insertEventIntoDescendingList(timeline, event);
72
+ return timeline;
73
+ }
74
+ /** Inserts an event into the database and notifies all subscriptions */
75
+ add(event) {
76
+ const id = event.id;
77
+ const current = this.events.get(id);
78
+ if (current)
79
+ return current;
80
+ // Ignore events if before insert returns false
81
+ if (this.onBeforeInsert?.(event) === false)
82
+ return null;
83
+ this.events.set(id, event);
84
+ this.getKindIndex(event.kind).add(event);
85
+ this.getAuthorsIndex(event.pubkey).add(event);
86
+ // Add the event to the tag indexes if they exist
87
+ for (const tag of getIndexableTags(event)) {
88
+ if (this.tags.has(tag))
89
+ this.getTagIndex(tag).add(event);
90
+ }
91
+ // Insert into time index
92
+ insertEventIntoDescendingList(this.created_at, event);
93
+ // Insert into replaceable index
94
+ if (isReplaceable(event.kind)) {
95
+ const uid = getEventUID(event);
96
+ let array = this.replaceable.get(uid);
97
+ if (!this.replaceable.has(uid)) {
98
+ // add an empty array if there is no array
99
+ array = [];
100
+ this.replaceable.set(uid, array);
101
+ }
102
+ // insert the event into the sorted array
103
+ insertEventIntoDescendingList(array, event);
104
+ }
105
+ // Notify subscribers that the event was inserted
106
+ this.insert$.next(event);
107
+ return event;
108
+ }
109
+ /** Inserts and event into the database and notifies all subscriptions that the event has updated */
110
+ update(event) {
111
+ const inserted = this.add(event);
112
+ if (inserted)
113
+ this.update$.next(inserted);
114
+ return inserted !== null;
115
+ }
116
+ /** Removes an event from the database and notifies all subscriptions */
117
+ remove(eventOrId) {
118
+ let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
119
+ if (!event)
120
+ throw new Error("Missing event");
121
+ const id = event.id;
122
+ // only remove events that are known
123
+ if (!this.events.has(id))
124
+ return false;
125
+ this.getAuthorsIndex(event.pubkey).delete(event);
126
+ this.getKindIndex(event.kind).delete(event);
127
+ for (const tag of getIndexableTags(event)) {
128
+ if (this.tags.has(tag)) {
129
+ this.getTagIndex(tag).delete(event);
130
+ }
131
+ }
132
+ // remove from created_at index
133
+ const i = this.created_at.indexOf(event);
134
+ this.created_at.splice(i, 1);
135
+ this.events.delete(id);
136
+ // remove from replaceable index
137
+ if (isReplaceable(event.kind)) {
138
+ const uid = getEventUID(event);
139
+ const array = this.replaceable.get(uid);
140
+ if (array && array.includes(event)) {
141
+ const idx = array.indexOf(event);
142
+ array.splice(idx, 1);
143
+ }
144
+ }
145
+ // remove any claims this event has
146
+ this.claims.delete(event);
147
+ // notify subscribers this event was removed
148
+ this.remove$.next(event);
149
+ return true;
150
+ }
151
+ /** A weak map of events that are claimed by other things */
152
+ claims = new WeakMap();
153
+ /** Sets the claim on the event and touches it */
154
+ claim(event, claim) {
155
+ if (!this.claims.has(event)) {
156
+ this.claims.set(event, claim);
157
+ }
158
+ // always touch event
159
+ this.touch(event);
160
+ }
161
+ /** Checks if an event is claimed by anything */
162
+ isClaimed(event) {
163
+ return this.claims.has(event);
164
+ }
165
+ /** Removes a claim from an event */
166
+ removeClaim(event, claim) {
167
+ const current = this.claims.get(event);
168
+ if (current === claim)
169
+ this.claims.delete(event);
170
+ }
171
+ /** Removes all claims on an event */
172
+ clearClaim(event) {
173
+ this.claims.delete(event);
174
+ }
175
+ /** Index helper methods */
176
+ getKindIndex(kind) {
177
+ if (!this.kinds.has(kind))
178
+ this.kinds.set(kind, new Set());
179
+ return this.kinds.get(kind);
180
+ }
181
+ getAuthorsIndex(author) {
182
+ if (!this.authors.has(author))
183
+ this.authors.set(author, new Set());
184
+ return this.authors.get(author);
185
+ }
186
+ getTagIndex(tagAndValue) {
187
+ if (!this.tags.has(tagAndValue)) {
188
+ // build new tag index from existing events
189
+ const events = new Set();
190
+ const ts = Date.now();
191
+ for (const event of this.events.values()) {
192
+ if (getIndexableTags(event).has(tagAndValue)) {
193
+ events.add(event);
194
+ }
195
+ }
196
+ const took = Date.now() - ts;
197
+ if (took > 100)
198
+ this.log(`Built index ${tagAndValue} took ${took}ms`);
199
+ this.tags.set(tagAndValue, events);
200
+ }
201
+ return this.tags.get(tagAndValue);
202
+ }
203
+ /** Iterates over all events by author */
204
+ *iterateAuthors(authors) {
205
+ for (const author of authors) {
206
+ const events = this.authors.get(author);
207
+ if (events) {
208
+ for (const event of events)
209
+ yield event;
210
+ }
211
+ }
212
+ }
213
+ /** Iterates over all events by indexable tag and value */
214
+ *iterateTag(tag, values) {
215
+ for (const value of values) {
216
+ const events = this.getTagIndex(tag + ":" + value);
217
+ if (events) {
218
+ for (const event of events)
219
+ yield event;
220
+ }
221
+ }
222
+ }
223
+ /** Iterates over all events by kind */
224
+ *iterateKinds(kinds) {
225
+ for (const kind of kinds) {
226
+ const events = this.kinds.get(kind);
227
+ if (events) {
228
+ for (const event of events)
229
+ yield event;
230
+ }
231
+ }
232
+ }
233
+ /** Iterates over all events by time */
234
+ *iterateTime(since, until) {
235
+ let untilIndex = 0;
236
+ let sinceIndex = this.created_at.length - 1;
237
+ let start = until
238
+ ? binarySearch(this.created_at, (mid) => {
239
+ return mid.created_at - until;
240
+ })
241
+ : undefined;
242
+ if (start)
243
+ untilIndex = start[0];
244
+ const end = since
245
+ ? binarySearch(this.created_at, (mid) => {
246
+ return mid.created_at - since;
247
+ })
248
+ : undefined;
249
+ if (end)
250
+ sinceIndex = end[0];
251
+ for (let i = untilIndex; i < sinceIndex; i++) {
252
+ yield this.created_at[i];
253
+ }
254
+ }
255
+ /** Iterates over all events by id */
256
+ *iterateIds(ids) {
257
+ for (const id of ids) {
258
+ if (this.events.has(id))
259
+ yield this.events.get(id);
260
+ }
261
+ }
262
+ /** Returns all events that match the filter */
263
+ getEventsForFilter(filter) {
264
+ // search is not supported, return an empty set
265
+ if (filter.search)
266
+ return new Set();
267
+ let first = true;
268
+ let events = new Set();
269
+ const and = (iterable) => {
270
+ const set = iterable instanceof Set ? iterable : new Set(iterable);
271
+ if (first) {
272
+ events = set;
273
+ first = false;
274
+ }
275
+ else {
276
+ for (const event of events) {
277
+ if (!set.has(event))
278
+ events.delete(event);
279
+ }
280
+ }
281
+ return events;
282
+ };
283
+ if (filter.ids)
284
+ and(this.iterateIds(filter.ids));
285
+ let time = null;
286
+ // query for time first if since is set
287
+ if (filter.since !== undefined) {
288
+ time = Array.from(this.iterateTime(filter.since, filter.until));
289
+ and(time);
290
+ }
291
+ for (const t of INDEXABLE_TAGS) {
292
+ const key = `#${t}`;
293
+ const values = filter[key];
294
+ if (values?.length)
295
+ and(this.iterateTag(t, values));
296
+ }
297
+ if (filter.authors)
298
+ and(this.iterateAuthors(filter.authors));
299
+ if (filter.kinds)
300
+ and(this.iterateKinds(filter.kinds));
301
+ // query for time last if only until is set
302
+ if (filter.since === undefined && filter.until !== undefined) {
303
+ time = Array.from(this.iterateTime(filter.since, filter.until));
304
+ and(time);
305
+ }
306
+ // if the filter queried on time and has a limit. truncate the events now
307
+ if (filter.limit && time) {
308
+ const limited = new Set();
309
+ for (const event of time) {
310
+ if (limited.size >= filter.limit)
311
+ break;
312
+ if (events.has(event))
313
+ limited.add(event);
314
+ }
315
+ return limited;
316
+ }
317
+ return events;
318
+ }
319
+ /** Returns all events that match the filters */
320
+ getEventsForFilters(filters) {
321
+ if (filters.length === 0)
322
+ throw new Error("No Filters");
323
+ let events = new Set();
324
+ for (const filter of filters) {
325
+ const filtered = this.getEventsForFilter(filter);
326
+ for (const event of filtered)
327
+ events.add(event);
328
+ }
329
+ return events;
330
+ }
331
+ /** Remove the oldest events that are not claimed */
332
+ prune(limit = 1000) {
333
+ let removed = 0;
334
+ let cursor = this.events.first;
335
+ while (cursor) {
336
+ const event = cursor.value;
337
+ if (!this.isClaimed(event)) {
338
+ this.remove(event);
339
+ removed++;
340
+ if (removed >= limit)
341
+ break;
342
+ }
343
+ cursor = cursor.next;
344
+ }
345
+ return removed;
346
+ }
347
+ }