applesauce-core 0.2.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 (42) hide show
  1. package/README.md +38 -0
  2. package/dist/event-store/event-store.d.ts +3 -1
  3. package/dist/event-store/event-store.js +6 -2
  4. package/dist/helpers/channel.d.ts +15 -0
  5. package/dist/helpers/channel.js +27 -0
  6. package/dist/helpers/event.d.ts +8 -0
  7. package/dist/helpers/event.js +9 -8
  8. package/dist/helpers/index.d.ts +0 -1
  9. package/dist/helpers/index.js +0 -1
  10. package/dist/helpers/json.d.ts +1 -0
  11. package/dist/helpers/json.js +8 -0
  12. package/dist/helpers/mailboxes.d.ts +10 -2
  13. package/dist/helpers/mailboxes.js +10 -9
  14. package/dist/helpers/mailboxes.test.d.ts +1 -0
  15. package/dist/helpers/mailboxes.test.js +80 -0
  16. package/dist/helpers/mute.d.ts +21 -0
  17. package/dist/helpers/mute.js +52 -0
  18. package/dist/helpers/profile.d.ts +6 -0
  19. package/dist/helpers/profile.js +4 -4
  20. package/dist/helpers/relays.d.ts +6 -0
  21. package/dist/helpers/relays.js +7 -6
  22. package/dist/promise/index.d.ts +1 -1
  23. package/dist/promise/index.js +1 -1
  24. package/dist/query-store/index.d.ts +33 -15
  25. package/dist/query-store/index.js +39 -57
  26. package/dist/query-store/queries/channel.d.ts +11 -0
  27. package/dist/query-store/queries/channel.js +72 -0
  28. package/dist/query-store/queries/index.d.ts +6 -0
  29. package/dist/query-store/queries/index.js +6 -0
  30. package/dist/query-store/queries/mailboxes.d.ts +5 -0
  31. package/dist/query-store/queries/mailboxes.js +11 -0
  32. package/dist/query-store/queries/mute.d.ts +7 -0
  33. package/dist/query-store/queries/mute.js +16 -0
  34. package/dist/query-store/queries/profile.d.ts +3 -0
  35. package/dist/query-store/queries/profile.js +10 -0
  36. package/dist/query-store/queries/reactions.d.ts +4 -0
  37. package/dist/query-store/queries/reactions.js +19 -0
  38. package/dist/query-store/queries/simple.d.ts +5 -0
  39. package/dist/query-store/queries/simple.js +20 -0
  40. package/package.json +17 -2
  41. package/dist/helpers/symbols.d.ts +0 -17
  42. package/dist/helpers/symbols.js +0 -10
package/README.md CHANGED
@@ -1 +1,39 @@
1
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
+ ```
@@ -14,7 +14,9 @@ export declare class EventStore {
14
14
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
15
15
  getReplaceable(kind: number, pubkey: string, d?: string): import("nostr-tools").Event | undefined;
16
16
  /** Creates an observable that updates a single event */
17
- single(uid: string): Observable<import("nostr-tools").Event | undefined>;
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>;
18
20
  /** Creates an observable that streams all events that match the filter */
19
21
  stream(filters: Filter[]): Observable<import("nostr-tools").Event>;
20
22
  /** Creates an observable that updates with an array of sorted events */
@@ -1,7 +1,7 @@
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 {
@@ -34,7 +34,7 @@ export class EventStore {
34
34
  return this.events.getReplaceable(kind, pubkey, d);
35
35
  }
36
36
  /** Creates an observable that updates a single event */
37
- single(uid) {
37
+ event(uid) {
38
38
  return new Observable((observer) => {
39
39
  let current = this.events.getEvent(uid);
40
40
  if (current) {
@@ -71,6 +71,10 @@ export class EventStore {
71
71
  };
72
72
  });
73
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
+ }
74
78
  /** Creates an observable that streams all events that match the filter */
75
79
  stream(filters) {
76
80
  return new Observable((observer) => {
@@ -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,12 @@
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
+ }
2
10
  /**
3
11
  * Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
4
12
  * or parameterized replaceable ( 30000 <= n < 40000 )
@@ -1,6 +1,7 @@
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");
4
5
  /**
5
6
  * Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
6
7
  * or parameterized replaceable ( 30000 <= n < 40000 )
@@ -10,29 +11,29 @@ export function isReplaceable(kind) {
10
11
  }
11
12
  /** returns the events Unique ID */
12
13
  export function getEventUID(event) {
13
- if (!event[EventUID]) {
14
+ if (!event[EventUIDSymbol]) {
14
15
  if (isReplaceable(event.kind)) {
15
16
  const d = event.tags.find((t) => t[0] === "d")?.[1];
16
- event[EventUID] = getReplaceableUID(event.kind, event.pubkey, d);
17
+ event[EventUIDSymbol] = getReplaceableUID(event.kind, event.pubkey, d);
17
18
  }
18
19
  else {
19
- event[EventUID] = event.id;
20
+ event[EventUIDSymbol] = event.id;
20
21
  }
21
22
  }
22
- return event[EventUID];
23
+ return event[EventUIDSymbol];
23
24
  }
24
25
  export function getReplaceableUID(kind, pubkey, d) {
25
26
  return d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`;
26
27
  }
27
28
  export function getIndexableTags(event) {
28
- if (!event[EventIndexableTags]) {
29
+ if (!event[EventIndexableTagsSymbol]) {
29
30
  const tags = new Set();
30
31
  for (const tag of event.tags) {
31
32
  if (tag[0] && INDEXABLE_TAGS.has(tag[0]) && tag[1]) {
32
33
  tags.add(tag[0] + ":" + tag[1]);
33
34
  }
34
35
  }
35
- event[EventIndexableTags] = tags;
36
+ event[EventIndexableTagsSymbol] = tags;
36
37
  }
37
- return event[EventIndexableTags];
38
+ return event[EventIndexableTagsSymbol];
38
39
  }
@@ -3,4 +3,3 @@ export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
5
  export * from "./mailboxes.js";
6
- export * as symbols from "./symbols.js";
@@ -3,4 +3,3 @@ export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
5
  export * from "./mailboxes.js";
6
- export * as symbols from "./symbols.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
+ }
@@ -1,9 +1,17 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
+ export declare const MailboxesInboxesSymbol: unique symbol;
3
+ export declare const MailboxesOutboxesSymbol: unique symbol;
4
+ declare module "nostr-tools" {
5
+ interface Event {
6
+ [MailboxesInboxesSymbol]?: Set<string>;
7
+ [MailboxesOutboxesSymbol]?: Set<string>;
8
+ }
9
+ }
2
10
  /**
3
- * Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxes} symbol
11
+ * Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxesSymbol} symbol
4
12
  */
5
13
  export declare function getInboxes(event: NostrEvent): Set<string>;
6
14
  /**
7
- * Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxes} symbol
15
+ * Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxesSymbol} symbol
8
16
  */
9
17
  export declare function getOutboxes(event: NostrEvent): Set<string>;
@@ -1,10 +1,11 @@
1
1
  import { safeRelayUrl } from "./relays.js";
2
- import { MailboxesInboxes, MailboxesOutboxes } from "./symbols.js";
2
+ export const MailboxesInboxesSymbol = Symbol.for("mailboxes-inboxes");
3
+ export const MailboxesOutboxesSymbol = Symbol.for("mailboxes-outboxes");
3
4
  /**
4
- * Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxes} symbol
5
+ * Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxesSymbol} symbol
5
6
  */
6
7
  export function getInboxes(event) {
7
- if (!event[MailboxesInboxes]) {
8
+ if (!event[MailboxesInboxesSymbol]) {
8
9
  const inboxes = new Set();
9
10
  for (const tag of event.tags) {
10
11
  if (tag[0] === "r" && tag[1] && (tag[2] === "read" || tag[2] === undefined)) {
@@ -13,15 +14,15 @@ export function getInboxes(event) {
13
14
  inboxes.add(url);
14
15
  }
15
16
  }
16
- event[MailboxesInboxes] = inboxes;
17
+ event[MailboxesInboxesSymbol] = inboxes;
17
18
  }
18
- return event[MailboxesInboxes];
19
+ return event[MailboxesInboxesSymbol];
19
20
  }
20
21
  /**
21
- * Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxes} symbol
22
+ * Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxesSymbol} symbol
22
23
  */
23
24
  export function getOutboxes(event) {
24
- if (!event[MailboxesOutboxes]) {
25
+ if (!event[MailboxesOutboxesSymbol]) {
25
26
  const outboxes = new Set();
26
27
  for (const tag of event.tags) {
27
28
  if (tag[0] === "r" && tag[1] && (tag[2] === "write" || tag[2] === undefined)) {
@@ -30,7 +31,7 @@ export function getOutboxes(event) {
30
31
  outboxes.add(url);
31
32
  }
32
33
  }
33
- event[MailboxesOutboxes] = outboxes;
34
+ event[MailboxesOutboxesSymbol] = outboxes;
34
35
  }
35
- return event[MailboxesOutboxes];
36
+ return event[MailboxesOutboxesSymbol];
36
37
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { getInboxes, getOutboxes } from "./mailboxes.js";
2
+ const emptyEvent = {
3
+ kind: 10002,
4
+ content: "",
5
+ tags: [],
6
+ created_at: 0,
7
+ sig: "",
8
+ id: "",
9
+ pubkey: "",
10
+ };
11
+ describe("Mailboxes", () => {
12
+ describe("getInboxes", () => {
13
+ test("should transform urls", () => {
14
+ expect(Array.from(getInboxes({
15
+ ...emptyEvent,
16
+ tags: [["r", "wss://inbox.com"]],
17
+ }))).toIncludeAllMembers(["wss://inbox.com/"]);
18
+ });
19
+ test("should remove bad urls", () => {
20
+ expect(Array.from(getInboxes({
21
+ ...emptyEvent,
22
+ tags: [["r", "bad://inbox.com"]],
23
+ }))).toBeArrayOfSize(0);
24
+ expect(Array.from(getInboxes({
25
+ ...emptyEvent,
26
+ tags: [["r", "something that is not a url"]],
27
+ }))).toBeArrayOfSize(0);
28
+ expect(Array.from(getInboxes({
29
+ ...emptyEvent,
30
+ tags: [["r", "wss://inbox.com,wss://inbox.org"]],
31
+ }))).toBeArrayOfSize(0);
32
+ });
33
+ test("without marker", () => {
34
+ expect(Array.from(getInboxes({
35
+ ...emptyEvent,
36
+ tags: [["r", "wss://inbox.com/"]],
37
+ }))).toIncludeAllMembers(["wss://inbox.com/"]);
38
+ });
39
+ test("with marker", () => {
40
+ expect(Array.from(getInboxes({
41
+ ...emptyEvent,
42
+ tags: [["r", "wss://inbox.com/", "read"]],
43
+ }))).toIncludeAllMembers(["wss://inbox.com/"]);
44
+ });
45
+ });
46
+ describe("getOutboxes", () => {
47
+ test("should transform urls", () => {
48
+ expect(Array.from(getOutboxes({
49
+ ...emptyEvent,
50
+ tags: [["r", "wss://outbox.com"]],
51
+ }))).toIncludeAllMembers(["wss://outbox.com/"]);
52
+ });
53
+ test("should remove bad urls", () => {
54
+ expect(Array.from(getOutboxes({
55
+ ...emptyEvent,
56
+ tags: [["r", "bad://inbox.com"]],
57
+ }))).toBeArrayOfSize(0);
58
+ expect(Array.from(getOutboxes({
59
+ ...emptyEvent,
60
+ tags: [["r", "something that is not a url"]],
61
+ }))).toBeArrayOfSize(0);
62
+ expect(Array.from(getOutboxes({
63
+ ...emptyEvent,
64
+ tags: [["r", "wss://outbox.com,wss://inbox.org"]],
65
+ }))).toBeArrayOfSize(0);
66
+ });
67
+ test("without marker", () => {
68
+ expect(Array.from(getOutboxes({
69
+ ...emptyEvent,
70
+ tags: [["r", "wss://outbox.com/"]],
71
+ }))).toIncludeAllMembers(["wss://outbox.com/"]);
72
+ });
73
+ test("with marker", () => {
74
+ expect(Array.from(getOutboxes({
75
+ ...emptyEvent,
76
+ tags: [["r", "wss://outbox.com/", "write"]],
77
+ }))).toIncludeAllMembers(["wss://outbox.com/"]);
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,21 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ export declare const MutePubkeysSymbol: unique symbol;
3
+ export declare const MuteThreadsSymbol: unique symbol;
4
+ export declare const MuteHashtagsSymbol: unique symbol;
5
+ export declare const MuteWordsSymbol: unique symbol;
6
+ declare module "nostr-tools" {
7
+ interface Event {
8
+ [MutePubkeysSymbol]?: Set<string>;
9
+ [MuteThreadsSymbol]?: Set<string>;
10
+ [MuteHashtagsSymbol]?: Set<string>;
11
+ [MuteWordsSymbol]?: Set<string>;
12
+ }
13
+ }
14
+ /** Returns a set of muted pubkeys */
15
+ export declare function getMutedPubkeys(mute: NostrEvent): Set<string>;
16
+ /** Returns a set of muted threads */
17
+ export declare function getMutedThreads(mute: NostrEvent): Set<string>;
18
+ /** Returns a set of muted words ( lowercase ) */
19
+ export declare function getMutedWords(mute: NostrEvent): Set<string>;
20
+ /** Returns a set of muted hashtags ( lowercase ) */
21
+ export declare function getMutedHashtags(mute: NostrEvent): Set<string>;
@@ -0,0 +1,52 @@
1
+ export const MutePubkeysSymbol = Symbol.for("mute-pubkeys");
2
+ export const MuteThreadsSymbol = Symbol.for("mute-threads");
3
+ export const MuteHashtagsSymbol = Symbol.for("mute-hashtags");
4
+ export const MuteWordsSymbol = Symbol.for("mute-words");
5
+ /** Returns a set of muted pubkeys */
6
+ export function getMutedPubkeys(mute) {
7
+ let pubkeys = mute[MutePubkeysSymbol];
8
+ if (!pubkeys) {
9
+ pubkeys = mute[MutePubkeysSymbol] = new Set();
10
+ for (const tag of mute.tags) {
11
+ if (tag[0] === "p" && tag[1])
12
+ pubkeys.add(tag[1]);
13
+ }
14
+ }
15
+ return pubkeys;
16
+ }
17
+ /** Returns a set of muted threads */
18
+ export function getMutedThreads(mute) {
19
+ let threads = mute[MuteThreadsSymbol];
20
+ if (!threads) {
21
+ threads = mute[MuteThreadsSymbol] = new Set();
22
+ for (const tag of mute.tags) {
23
+ if (tag[0] === "e" && tag[1])
24
+ threads.add(tag[1]);
25
+ }
26
+ }
27
+ return threads;
28
+ }
29
+ /** Returns a set of muted words ( lowercase ) */
30
+ export function getMutedWords(mute) {
31
+ let words = mute[MuteWordsSymbol];
32
+ if (!words) {
33
+ words = mute[MuteWordsSymbol] = new Set();
34
+ for (const tag of mute.tags) {
35
+ if (tag[0] === "word" && tag[1])
36
+ words.add(tag[1].toLocaleLowerCase());
37
+ }
38
+ }
39
+ return words;
40
+ }
41
+ /** Returns a set of muted hashtags ( lowercase ) */
42
+ export function getMutedHashtags(mute) {
43
+ let hashtags = mute[MuteHashtagsSymbol];
44
+ if (!hashtags) {
45
+ hashtags = mute[MuteHashtagsSymbol] = new Set();
46
+ for (const tag of mute.tags) {
47
+ if (tag[0] === "t" && tag[1])
48
+ hashtags.add(tag[1].toLocaleLowerCase());
49
+ }
50
+ }
51
+ return hashtags;
52
+ }
@@ -1,4 +1,10 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
+ export declare const ProfileContentSymbol: unique symbol;
3
+ declare module "nostr-tools" {
4
+ interface Event {
5
+ [ProfileContentSymbol]?: ProfileContent | Error;
6
+ }
7
+ }
2
8
  export type ProfileContent = {
3
9
  name?: string;
4
10
  display_name?: string;
@@ -1,17 +1,17 @@
1
- import { ProfileContent } from "./symbols.js";
1
+ export const ProfileContentSymbol = Symbol.for("profile-content");
2
2
  export function getProfileContent(event, quite = false) {
3
- let cached = event[ProfileContent];
3
+ let cached = event[ProfileContentSymbol];
4
4
  if (!cached) {
5
5
  try {
6
6
  const profile = JSON.parse(event.content);
7
7
  // ensure nip05 is a string
8
8
  if (profile.nip05 && typeof profile.nip05 !== "string")
9
9
  profile.nip05 = String(profile.nip05);
10
- cached = event[ProfileContent] = profile;
10
+ cached = event[ProfileContentSymbol] = profile;
11
11
  }
12
12
  catch (e) {
13
13
  if (e instanceof Error)
14
- cached = event[ProfileContent] = e;
14
+ cached = event[ProfileContentSymbol] = e;
15
15
  }
16
16
  }
17
17
  if (cached === undefined) {
@@ -1,4 +1,10 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
+ export declare const SeenRelaysSymbol: unique symbol;
3
+ declare module "nostr-tools" {
4
+ interface Event {
5
+ [SeenRelaysSymbol]?: Set<string>;
6
+ }
7
+ }
2
8
  export declare function addSeenRelay(event: NostrEvent, relay: string): Set<string>;
3
9
  export declare function getSeenRelays(event: NostrEvent): Set<string> | undefined;
4
10
  export declare function validateRelayURL(relay: string | URL): URL;
@@ -1,12 +1,13 @@
1
- import { FromRelays } from "./symbols.js";
1
+ export const SeenRelaysSymbol = Symbol.for("seen-relays");
2
+ // Seen relays
2
3
  export function addSeenRelay(event, relay) {
3
- if (!event[FromRelays])
4
- event[FromRelays] = new Set();
5
- event[FromRelays].add(relay);
6
- return event[FromRelays];
4
+ if (!event[SeenRelaysSymbol])
5
+ event[SeenRelaysSymbol] = new Set();
6
+ event[SeenRelaysSymbol].add(relay);
7
+ return event[SeenRelaysSymbol];
7
8
  }
8
9
  export function getSeenRelays(event) {
9
- return event[FromRelays];
10
+ return event[SeenRelaysSymbol];
10
11
  }
11
12
  // Relay URLs
12
13
  export function validateRelayURL(relay) {
@@ -1 +1 @@
1
- export * from './deferred.js';
1
+ export * from "./deferred.js";
@@ -1 +1 @@
1
- export * from './deferred.js';
1
+ export * from "./deferred.js";
@@ -1,26 +1,44 @@
1
1
  import Observable from "zen-observable";
2
2
  import { Filter, NostrEvent } from "nostr-tools";
3
3
  import { EventStore } from "../event-store/event-store.js";
4
- import { ProfileContent } from "../helpers/profile.js";
5
4
  import { LRU } from "../utils/lru.js";
5
+ import * as Queries from "./queries/index.js";
6
+ export type Query<T extends unknown> = {
7
+ key: string;
8
+ run: (events: EventStore, store: QueryStore) => Observable<T>;
9
+ };
10
+ export type QueryConstructor<T extends unknown, Args extends Array<any>> = (...args: Args) => Query<T>;
6
11
  export declare class QueryStore {
12
+ static Queries: typeof Queries;
7
13
  store: EventStore;
8
14
  constructor(store: EventStore);
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
- timelines: LRU<Observable<import("nostr-tools").Event[]>>;
13
- getTimeline(filters: Filter[]): Observable<import("nostr-tools").Event[]>;
14
- profiles: LRU<Observable<ProfileContent | undefined>>;
15
- getProfile(pubkey: string): Observable<ProfileContent | undefined>;
16
- reactions: LRU<Observable<import("nostr-tools").Event[]>>;
17
- getReactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
18
- mailboxes: LRU<Observable<{
19
- inboxes: Set<string>;
20
- outboxes: Set<string>;
21
- } | undefined>>;
22
- getMailboxes(pubkey: string): Observable<{
15
+ queries: LRU<Observable<any>>;
16
+ /** Creates a cached query */
17
+ runQuery<T extends unknown, Args extends Array<any>>(queryConstructor: (...args: Args) => {
18
+ key: string;
19
+ run: (events: EventStore, store: QueryStore) => Observable<T>;
20
+ }): (...args: Args) => Observable<T>;
21
+ /** Returns a single event */
22
+ event(id: string): Observable<import("nostr-tools").Event | undefined>;
23
+ /** Returns the latest version of a replaceable event */
24
+ replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>;
25
+ /** Returns an array of events that match the filter */
26
+ timeline(filters: Filter | Filter[]): Observable<import("nostr-tools").Event[]>;
27
+ /** Returns the parsed profile (0) for a pubkey */
28
+ profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
29
+ /** Returns all reactions for an event (supports replaceable events) */
30
+ reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
31
+ /** Returns the parsed relay list (10002) for the pubkey */
32
+ mailboxes(pubkey: string): Observable<{
23
33
  inboxes: Set<string>;
24
34
  outboxes: Set<string>;
25
35
  } | undefined>;
36
+ /** Returns the parsed mute list for the pubkey */
37
+ mute(pubkey: string): Observable<{
38
+ words: Set<string>;
39
+ pubkeys: Set<string>;
40
+ threads: Set<string>;
41
+ hashtags: Set<string>;
42
+ } | undefined>;
26
43
  }
44
+ export { Queries };
@@ -1,71 +1,53 @@
1
- import { kinds } from "nostr-tools";
2
- import stringify from "json-stringify-deterministic";
3
1
  import { stateful } from "../observable/stateful.js";
4
- import { getProfileContent } from "../helpers/profile.js";
5
- import { getEventUID, getReplaceableUID, isReplaceable } from "../helpers/event.js";
6
- import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
7
2
  import { LRU } from "../utils/lru.js";
3
+ import * as Queries from "./queries/index.js";
8
4
  export class QueryStore {
5
+ static Queries = Queries;
9
6
  store;
10
7
  constructor(store) {
11
8
  this.store = store;
12
9
  }
13
- singleEvents = new LRU();
14
- getEvent(id) {
15
- if (!this.singleEvents.has(id)) {
16
- const observable = stateful(this.store.single(id));
17
- this.singleEvents.set(id, observable);
18
- }
19
- return this.singleEvents.get(id);
10
+ queries = new LRU();
11
+ /** Creates a cached query */
12
+ runQuery(queryConstructor) {
13
+ return (...args) => {
14
+ const query = queryConstructor(...args);
15
+ const key = `${queryConstructor.name}|${query.key}`;
16
+ if (!this.queries.has(key)) {
17
+ const observable = stateful(query.run(this.store, this));
18
+ this.queries.set(key, observable);
19
+ return observable;
20
+ }
21
+ return this.queries.get(key);
22
+ };
20
23
  }
21
- getReplaceable(kind, pubkey, d) {
22
- return this.getEvent(getReplaceableUID(kind, pubkey, d));
24
+ /** Returns a single event */
25
+ event(id) {
26
+ return this.runQuery(Queries.SingleEventQuery)(id);
23
27
  }
24
- timelines = new LRU();
25
- getTimeline(filters) {
26
- const key = stringify(filters);
27
- if (!this.singleEvents.has(key)) {
28
- const observable = stateful(this.store.timeline(filters));
29
- this.timelines.set(key, observable);
30
- }
31
- return this.timelines.get(key);
28
+ /** Returns the latest version of a replaceable event */
29
+ replaceable(kind, pubkey, d) {
30
+ return this.runQuery(Queries.ReplaceableQuery)(kind, pubkey, d);
32
31
  }
33
- profiles = new LRU();
34
- getProfile(pubkey) {
35
- if (!this.profiles.has(pubkey)) {
36
- const observable = stateful(this.getReplaceable(kinds.Metadata, pubkey).map((event) => event && getProfileContent(event)));
37
- this.profiles.set(pubkey, observable);
38
- }
39
- return this.profiles.get(pubkey);
32
+ /** Returns an array of events that match the filter */
33
+ timeline(filters) {
34
+ return this.runQuery(Queries.TimelineQuery)(filters);
40
35
  }
41
- reactions = new LRU();
42
- getReactions(event) {
43
- const uid = getEventUID(event);
44
- if (!this.reactions.has(uid)) {
45
- const observable = this.getTimeline(isReplaceable(event.kind)
46
- ? [
47
- { kinds: [kinds.Reaction], "#e": [event.id] },
48
- { kinds: [kinds.Reaction], "#a": [uid] },
49
- ]
50
- : [
51
- {
52
- kinds: [kinds.Reaction],
53
- "#e": [event.id],
54
- },
55
- ]);
56
- this.reactions.set(uid, observable);
57
- }
58
- return this.reactions.get(uid);
36
+ /** Returns the parsed profile (0) for a pubkey */
37
+ profile(pubkey) {
38
+ return this.runQuery(Queries.ProfileQuery)(pubkey);
59
39
  }
60
- mailboxes = new LRU();
61
- getMailboxes(pubkey) {
62
- if (!this.mailboxes.has(pubkey)) {
63
- const observable = stateful(this.getReplaceable(kinds.RelayList, pubkey).map((event) => event && {
64
- inboxes: getInboxes(event),
65
- outboxes: getOutboxes(event),
66
- }));
67
- this.mailboxes.set(pubkey, observable);
68
- }
69
- return this.mailboxes.get(pubkey);
40
+ /** Returns all reactions for an event (supports replaceable events) */
41
+ reactions(event) {
42
+ return this.runQuery(Queries.ReactionsQuery)(event);
43
+ }
44
+ /** Returns the parsed relay list (10002) for the pubkey */
45
+ mailboxes(pubkey) {
46
+ return this.runQuery(Queries.MailboxesQuery)(pubkey);
47
+ }
48
+ /** Returns the parsed mute list for the pubkey */
49
+ mute(pubkey) {
50
+ return this.runQuery(Queries.UserMuteQuery)(pubkey);
70
51
  }
71
52
  }
53
+ export { Queries };
@@ -0,0 +1,11 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Query } from "../index.js";
3
+ import { ChannelMetadataContent } from "../../helpers/channel.js";
4
+ /** Creates a query that returns the latest parsed metadata */
5
+ export declare function ChannelMetadataQuery(channel: NostrEvent): Query<ChannelMetadataContent | undefined>;
6
+ /** Creates a query that returns a map of hidden messages Map<id, reason> */
7
+ export declare function ChannelHiddenQuery(channel: NostrEvent, authors?: string[]): Query<Map<string, string>>;
8
+ /** Creates a query that returns a map of muted users Map<pubkey, reason> */
9
+ export declare function ChannelMutedQuery(channel: NostrEvent, authors?: string[]): Query<Map<string, string>>;
10
+ /** Creates a query that returns all messages in a channel */
11
+ export declare function ChannelMessagesQuery(channel: NostrEvent): Query<NostrEvent[]>;
@@ -0,0 +1,72 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { getChannelMetadataContent } from "../../helpers/channel.js";
3
+ import { safeParse } from "../../helpers/json.js";
4
+ /** Creates a query that returns the latest parsed metadata */
5
+ export function ChannelMetadataQuery(channel) {
6
+ return {
7
+ key: channel.id,
8
+ run: (events) => {
9
+ const filters = [
10
+ { ids: [channel.id] },
11
+ { kinds: [kinds.ChannelMetadata], "#e": [channel.id], authors: [channel.pubkey] },
12
+ ];
13
+ let latest = channel;
14
+ return events.stream(filters).map((event) => {
15
+ try {
16
+ if (event.pubkey === latest.pubkey && event.created_at > latest.created_at) {
17
+ latest = event;
18
+ }
19
+ return getChannelMetadataContent(latest);
20
+ }
21
+ catch (error) {
22
+ return undefined;
23
+ }
24
+ });
25
+ },
26
+ };
27
+ }
28
+ /** Creates a query that returns a map of hidden messages Map<id, reason> */
29
+ export function ChannelHiddenQuery(channel, authors = []) {
30
+ return {
31
+ key: channel.id,
32
+ run: (events) => {
33
+ const hidden = new Map();
34
+ return events
35
+ .stream([{ kinds: [kinds.ChannelHideMessage], "#e": [channel.id], authors: [channel.pubkey, ...authors] }])
36
+ .map((event) => {
37
+ const reason = safeParse(event.content)?.reason;
38
+ for (const tag of event.tags) {
39
+ if (tag[0] === "e" && tag[1])
40
+ hidden.set(tag[1], reason ?? "");
41
+ }
42
+ return hidden;
43
+ });
44
+ },
45
+ };
46
+ }
47
+ /** Creates a query that returns a map of muted users Map<pubkey, reason> */
48
+ export function ChannelMutedQuery(channel, authors = []) {
49
+ return {
50
+ key: channel.id + authors.join(","),
51
+ run: (events) => {
52
+ const muted = new Map();
53
+ return events
54
+ .stream([{ kinds: [kinds.ChannelMuteUser], "#e": [channel.id], authors: [channel.pubkey, ...authors] }])
55
+ .map((event) => {
56
+ const reason = safeParse(event.content)?.reason;
57
+ for (const tag of event.tags) {
58
+ if (tag[0] === "p" && tag[1])
59
+ muted.set(tag[1], reason ?? "");
60
+ }
61
+ return muted;
62
+ });
63
+ },
64
+ };
65
+ }
66
+ /** Creates a query that returns all messages in a channel */
67
+ export function ChannelMessagesQuery(channel) {
68
+ return {
69
+ key: channel.id,
70
+ run: (events) => events.timeline([{ kinds: [kinds.ChannelMessage], "#e": [channel.id] }]),
71
+ };
72
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./simple.js";
2
+ export * from "./profile.js";
3
+ export * from "./mailboxes.js";
4
+ export * from "./reactions.js";
5
+ export * from "./channel.js";
6
+ export * from "./mute.js";
@@ -0,0 +1,6 @@
1
+ export * from "./simple.js";
2
+ export * from "./profile.js";
3
+ export * from "./mailboxes.js";
4
+ export * from "./reactions.js";
5
+ export * from "./channel.js";
6
+ export * from "./mute.js";
@@ -0,0 +1,5 @@
1
+ import { Query } from "../index.js";
2
+ export declare function MailboxesQuery(pubkey: string): Query<{
3
+ inboxes: Set<string>;
4
+ outboxes: Set<string>;
5
+ } | undefined>;
@@ -0,0 +1,11 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { getInboxes, getOutboxes } from "../../helpers/mailboxes.js";
3
+ export function MailboxesQuery(pubkey) {
4
+ return {
5
+ key: pubkey,
6
+ run: (events) => events.replaceable(kinds.RelayList, pubkey).map((event) => event && {
7
+ inboxes: getInboxes(event),
8
+ outboxes: getOutboxes(event),
9
+ }),
10
+ };
11
+ }
@@ -0,0 +1,7 @@
1
+ import { Query } from "../index.js";
2
+ export declare function UserMuteQuery(pubkey: string): Query<{
3
+ words: Set<string>;
4
+ pubkeys: Set<string>;
5
+ threads: Set<string>;
6
+ hashtags: Set<string>;
7
+ } | undefined>;
@@ -0,0 +1,16 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { getMutedHashtags, getMutedPubkeys, getMutedThreads, getMutedWords } from "../../helpers/mute.js";
3
+ export function UserMuteQuery(pubkey) {
4
+ return {
5
+ key: pubkey,
6
+ run: (store) => store.replaceable(kinds.Mutelist, pubkey).map((event) => {
7
+ if (!event)
8
+ return;
9
+ const pubkeys = getMutedPubkeys(event);
10
+ const threads = getMutedThreads(event);
11
+ const hashtags = getMutedHashtags(event);
12
+ const words = getMutedWords(event);
13
+ return { pubkeys, threads, hashtags, words };
14
+ }),
15
+ };
16
+ }
@@ -0,0 +1,3 @@
1
+ import { ProfileContent } from "../../helpers/profile.js";
2
+ import { Query } from "../index.js";
3
+ export declare function ProfileQuery(pubkey: string): Query<ProfileContent | undefined>;
@@ -0,0 +1,10 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { getProfileContent } from "../../helpers/profile.js";
3
+ export function ProfileQuery(pubkey) {
4
+ return {
5
+ key: pubkey,
6
+ run: (events) => {
7
+ return events.replaceable(kinds.Metadata, pubkey).map((event) => event && getProfileContent(event));
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,4 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Query } from "../index.js";
3
+ /** Creates a query that returns all reactions to an event (supports replaceable events) */
4
+ export declare function ReactionsQuery(event: NostrEvent): Query<NostrEvent[]>;
@@ -0,0 +1,19 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { getEventUID, isReplaceable } from "../../helpers/event.js";
3
+ /** Creates a query that returns all reactions to an event (supports replaceable events) */
4
+ export function ReactionsQuery(event) {
5
+ return {
6
+ key: getEventUID(event),
7
+ run: (events) => events.timeline(isReplaceable(event.kind)
8
+ ? [
9
+ { kinds: [kinds.Reaction], "#e": [event.id] },
10
+ { kinds: [kinds.Reaction], "#a": [getEventUID(event)] },
11
+ ]
12
+ : [
13
+ {
14
+ kinds: [kinds.Reaction],
15
+ "#e": [event.id],
16
+ },
17
+ ]),
18
+ };
19
+ }
@@ -0,0 +1,5 @@
1
+ import { Filter, NostrEvent } from "nostr-tools";
2
+ import { Query } from "../index.js";
3
+ export declare function SingleEventQuery(uid: string): Query<NostrEvent | undefined>;
4
+ export declare function ReplaceableQuery(kind: number, pubkey: string, d?: string): Query<NostrEvent | undefined>;
5
+ export declare function TimelineQuery(filters: Filter | Filter[]): Query<NostrEvent[]>;
@@ -0,0 +1,20 @@
1
+ import stringify from "json-stringify-deterministic";
2
+ import { getReplaceableUID } from "../../helpers/event.js";
3
+ export function SingleEventQuery(uid) {
4
+ return {
5
+ key: uid,
6
+ run: (events) => events.event(uid),
7
+ };
8
+ }
9
+ export function ReplaceableQuery(kind, pubkey, d) {
10
+ return {
11
+ key: getReplaceableUID(kind, pubkey, d),
12
+ run: (events) => events.replaceable(kind, pubkey, d),
13
+ };
14
+ }
15
+ export function TimelineQuery(filters) {
16
+ return {
17
+ key: stringify(filters),
18
+ run: (events) => events.timeline(Array.isArray(filters) ? filters : [filters]),
19
+ };
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,11 +44,26 @@
44
44
  "zen-push": "^0.3.1"
45
45
  },
46
46
  "devDependencies": {
47
+ "@jest/globals": "^29.7.0",
47
48
  "@types/debug": "^4.1.12",
49
+ "@types/jest": "^29.5.13",
48
50
  "@types/zen-observable": "^0.8.7",
51
+ "jest": "^29.7.0",
52
+ "jest-extended": "^4.0.2",
49
53
  "zen-observable": "^0.10.0"
50
54
  },
55
+ "jest": {
56
+ "roots": [
57
+ "dist"
58
+ ],
59
+ "setupFilesAfterEnv": [
60
+ "jest-extended/all"
61
+ ]
62
+ },
51
63
  "scripts": {
52
- "build": "tsc"
64
+ "build": "tsc",
65
+ "watch:build": "tsc --watch > /dev/null",
66
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
67
+ "watch:test": "(trap 'kill 0' SIGINT; pnpm run build -w > /dev/null & pnpm run test --watch)"
53
68
  }
54
69
  }
@@ -1,17 +0,0 @@
1
- import { ProfileContent as TProfileContent } from "./profile.js";
2
- export declare const EventUID: unique symbol;
3
- export declare const EventIndexableTags: unique symbol;
4
- export declare const MailboxesInboxes: unique symbol;
5
- export declare const MailboxesOutboxes: unique symbol;
6
- export declare const ProfileContent: unique symbol;
7
- export declare const FromRelays: unique symbol;
8
- declare module "nostr-tools" {
9
- interface Event {
10
- [EventUID]?: string;
11
- [MailboxesInboxes]?: Set<string>;
12
- [MailboxesOutboxes]?: Set<string>;
13
- [ProfileContent]?: TProfileContent | Error;
14
- [EventIndexableTags]?: Set<string>;
15
- [FromRelays]?: Set<string>;
16
- }
17
- }
@@ -1,10 +0,0 @@
1
- // event
2
- export const EventUID = Symbol.for("event-uid");
3
- export const EventIndexableTags = Symbol.for("indexable-tags");
4
- // mailboxes
5
- export const MailboxesInboxes = Symbol.for("mailboxes-inboxes");
6
- export const MailboxesOutboxes = Symbol.for("mailboxes-outboxes");
7
- // profile
8
- export const ProfileContent = Symbol.for("profile-content");
9
- // event relays
10
- export const FromRelays = Symbol.for("from-relays");