applesauce-core 0.3.0 → 0.4.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 (35) hide show
  1. package/dist/helpers/event.d.ts +9 -1
  2. package/dist/helpers/event.js +20 -8
  3. package/dist/helpers/index.d.ts +2 -0
  4. package/dist/helpers/index.js +2 -0
  5. package/dist/helpers/pointers.d.ts +22 -0
  6. package/dist/helpers/pointers.js +127 -0
  7. package/dist/helpers/profile.d.ts +1 -0
  8. package/dist/helpers/threading.d.ts +55 -0
  9. package/dist/helpers/threading.js +61 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/observable/stateful.d.ts +1 -1
  13. package/dist/observable/stateful.js +1 -1
  14. package/dist/observable/throttle.d.ts +1 -0
  15. package/dist/observable/throttle.js +1 -0
  16. package/dist/{query-store/queries → queries}/channel.d.ts +2 -2
  17. package/dist/{query-store/queries → queries}/channel.js +2 -2
  18. package/dist/{query-store/queries → queries}/index.d.ts +1 -0
  19. package/dist/{query-store/queries → queries}/index.js +1 -0
  20. package/dist/{query-store/queries → queries}/mailboxes.d.ts +1 -1
  21. package/dist/{query-store/queries → queries}/mailboxes.js +1 -1
  22. package/dist/{query-store/queries → queries}/mute.d.ts +1 -1
  23. package/dist/{query-store/queries → queries}/mute.js +1 -1
  24. package/dist/queries/profile.d.ts +3 -0
  25. package/dist/{query-store/queries → queries}/profile.js +1 -1
  26. package/dist/{query-store/queries → queries}/reactions.d.ts +1 -1
  27. package/dist/{query-store/queries → queries}/reactions.js +1 -1
  28. package/dist/{query-store/queries → queries}/simple.d.ts +1 -1
  29. package/dist/{query-store/queries → queries}/simple.js +1 -1
  30. package/dist/queries/thread.d.ts +23 -0
  31. package/dist/queries/thread.js +65 -0
  32. package/dist/query-store/index.d.ts +3 -1
  33. package/dist/query-store/index.js +4 -1
  34. package/package.json +5 -1
  35. package/dist/query-store/queries/profile.d.ts +0 -3
@@ -12,7 +12,15 @@ declare module "nostr-tools" {
12
12
  * or parameterized replaceable ( 30000 <= n < 40000 )
13
13
  */
14
14
  export declare function isReplaceable(kind: number): boolean;
15
- /** returns the events Unique ID */
15
+ /**
16
+ * Returns the events Unique ID
17
+ * For normal or ephemeral events this is ( event.id )
18
+ * For replaceable events this is ( event.kind + ":" + event.pubkey )
19
+ * For parametrized replaceable events this is ( event.kind + ":" + event.pubkey + ":" + event.tags.d.1 )
20
+ */
16
21
  export declare function getEventUID(event: NostrEvent): string;
17
22
  export declare function getReplaceableUID(kind: number, pubkey: string, d?: string): string;
23
+ /** Returns a Set of tag names and values that are indexable */
18
24
  export declare function getIndexableTags(event: NostrEvent): Set<string>;
25
+ /** Returns the second index ( tag[1] ) of the first tag that matches the name */
26
+ export declare function getTagValue(event: NostrEvent, name: string): string | undefined;
@@ -9,31 +9,43 @@ export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
9
9
  export function isReplaceable(kind) {
10
10
  return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind);
11
11
  }
12
- /** returns the events Unique ID */
12
+ /**
13
+ * Returns the events Unique ID
14
+ * For normal or ephemeral events this is ( event.id )
15
+ * For replaceable events this is ( event.kind + ":" + event.pubkey )
16
+ * For parametrized replaceable events this is ( event.kind + ":" + event.pubkey + ":" + event.tags.d.1 )
17
+ */
13
18
  export function getEventUID(event) {
14
- if (!event[EventUIDSymbol]) {
19
+ let id = event[EventUIDSymbol];
20
+ if (!id) {
15
21
  if (isReplaceable(event.kind)) {
16
22
  const d = event.tags.find((t) => t[0] === "d")?.[1];
17
- event[EventUIDSymbol] = getReplaceableUID(event.kind, event.pubkey, d);
23
+ id = getReplaceableUID(event.kind, event.pubkey, d);
18
24
  }
19
25
  else {
20
- event[EventUIDSymbol] = event.id;
26
+ id = event.id;
21
27
  }
22
28
  }
23
- return event[EventUIDSymbol];
29
+ return id;
24
30
  }
25
31
  export function getReplaceableUID(kind, pubkey, d) {
26
32
  return d ? `${kind}:${pubkey}:${d}` : `${kind}:${pubkey}`;
27
33
  }
34
+ /** Returns a Set of tag names and values that are indexable */
28
35
  export function getIndexableTags(event) {
29
- if (!event[EventIndexableTagsSymbol]) {
36
+ let indexable = event[EventIndexableTagsSymbol];
37
+ if (!indexable) {
30
38
  const tags = new Set();
31
39
  for (const tag of event.tags) {
32
40
  if (tag[0] && INDEXABLE_TAGS.has(tag[0]) && tag[1]) {
33
41
  tags.add(tag[0] + ":" + tag[1]);
34
42
  }
35
43
  }
36
- event[EventIndexableTagsSymbol] = tags;
44
+ indexable = event[EventIndexableTagsSymbol] = tags;
37
45
  }
38
- return event[EventIndexableTagsSymbol];
46
+ return indexable;
47
+ }
48
+ /** Returns the second index ( tag[1] ) of the first tag that matches the name */
49
+ export function getTagValue(event, name) {
50
+ return event.tags.find((t) => t[0] === name)?.[1];
39
51
  }
@@ -3,3 +3,5 @@ export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
5
  export * from "./mailboxes.js";
6
+ export * from "./threading.js";
7
+ export * from "./pointers.js";
@@ -3,3 +3,5 @@ export * from "./relays.js";
3
3
  export * from "./event.js";
4
4
  export * from "./filter.js";
5
5
  export * from "./mailboxes.js";
6
+ export * from "./threading.js";
7
+ export * from "./pointers.js";
@@ -0,0 +1,22 @@
1
+ import { AddressPointer, DecodeResult, EventPointer, ProfilePointer } from "nostr-tools/nip19";
2
+ export type AddressPointerWithoutD = Omit<AddressPointer, "identifier"> & {
3
+ identifier?: string;
4
+ };
5
+ export declare function parseCoordinate(a: string): AddressPointerWithoutD | null;
6
+ export declare function parseCoordinate(a: string, requireD: false): AddressPointerWithoutD | null;
7
+ export declare function parseCoordinate(a: string, requireD: true): AddressPointer | null;
8
+ export declare function parseCoordinate(a: string, requireD: false, silent: false): AddressPointerWithoutD;
9
+ export declare function parseCoordinate(a: string, requireD: true, silent: false): AddressPointer;
10
+ export declare function parseCoordinate(a: string, requireD: true, silent: true): AddressPointer | null;
11
+ export declare function parseCoordinate(a: string, requireD: false, silent: true): AddressPointerWithoutD | null;
12
+ export declare function getPubkeyFromDecodeResult(result?: DecodeResult): string | undefined;
13
+ export declare function encodeDecodeResult(result: DecodeResult): "" | `nsec1${string}` | `npub1${string}` | `note1${string}` | `nprofile1${string}` | `nevent1${string}` | `naddr1${string}` | `nrelay1${string}`;
14
+ export declare function getEventPointerFromTag(tag: string[]): EventPointer;
15
+ export declare function getAddressPointerFromTag(tag: string[]): AddressPointer;
16
+ export declare function getProfilePointerFromTag(tag: string[]): ProfilePointer;
17
+ export declare function getPointerFromTag(tag: string[]): DecodeResult | null;
18
+ export declare function isAddressPointer(pointer: DecodeResult["data"]): pointer is AddressPointer;
19
+ export declare function isEventPointer(pointer: DecodeResult["data"]): pointer is EventPointer;
20
+ export declare function getCoordinateFromAddressPointer(pointer: AddressPointer): string;
21
+ export declare function getATagFromAddressPointer(pointer: AddressPointer): ["a", ...string[]];
22
+ export declare function getETagFromEventPointer(pointer: EventPointer): ["e", ...string[]];
@@ -0,0 +1,127 @@
1
+ import { naddrEncode, neventEncode, noteEncode, nprofileEncode, npubEncode, nrelayEncode, nsecEncode, } from "nostr-tools/nip19";
2
+ import { getPublicKey } from "nostr-tools";
3
+ import { safeRelayUrls } from "./relays.js";
4
+ export function parseCoordinate(a, requireD = false, silent = true) {
5
+ const parts = a.split(":");
6
+ const kind = parts[0] && parseInt(parts[0]);
7
+ const pubkey = parts[1];
8
+ const d = parts[2];
9
+ if (!kind) {
10
+ if (silent)
11
+ return null;
12
+ else
13
+ throw new Error("Missing kind");
14
+ }
15
+ if (!pubkey) {
16
+ if (silent)
17
+ return null;
18
+ else
19
+ throw new Error("Missing pubkey");
20
+ }
21
+ if (requireD && d === undefined) {
22
+ if (silent)
23
+ return null;
24
+ else
25
+ throw new Error("Missing identifier");
26
+ }
27
+ return {
28
+ kind,
29
+ pubkey,
30
+ identifier: d,
31
+ };
32
+ }
33
+ export function getPubkeyFromDecodeResult(result) {
34
+ if (!result)
35
+ return;
36
+ switch (result.type) {
37
+ case "naddr":
38
+ case "nprofile":
39
+ return result.data.pubkey;
40
+ case "npub":
41
+ return result.data;
42
+ case "nsec":
43
+ return getPublicKey(result.data);
44
+ default:
45
+ return undefined;
46
+ }
47
+ }
48
+ export function encodeDecodeResult(result) {
49
+ switch (result.type) {
50
+ case "naddr":
51
+ return naddrEncode(result.data);
52
+ case "nprofile":
53
+ return nprofileEncode(result.data);
54
+ case "nevent":
55
+ return neventEncode(result.data);
56
+ case "nrelay":
57
+ return nrelayEncode(result.data);
58
+ case "nsec":
59
+ return nsecEncode(result.data);
60
+ case "npub":
61
+ return npubEncode(result.data);
62
+ case "note":
63
+ return noteEncode(result.data);
64
+ }
65
+ return "";
66
+ }
67
+ export function getEventPointerFromTag(tag) {
68
+ if (!tag[1])
69
+ throw new Error("Missing event id in tag");
70
+ let pointer = { id: tag[1] };
71
+ if (tag[2])
72
+ pointer.relays = safeRelayUrls([tag[2]]);
73
+ return pointer;
74
+ }
75
+ export function getAddressPointerFromTag(tag) {
76
+ if (!tag[1])
77
+ throw new Error("Missing coordinate in tag");
78
+ const pointer = parseCoordinate(tag[1], true, false);
79
+ if (tag[2])
80
+ pointer.relays = safeRelayUrls([tag[2]]);
81
+ return pointer;
82
+ }
83
+ export function getProfilePointerFromTag(tag) {
84
+ if (!tag[1])
85
+ throw new Error("Missing pubkey in tag");
86
+ const pointer = { pubkey: tag[1] };
87
+ if (tag[2])
88
+ pointer.relays = safeRelayUrls([tag[2]]);
89
+ return pointer;
90
+ }
91
+ export function getPointerFromTag(tag) {
92
+ try {
93
+ switch (tag[0]) {
94
+ case "e":
95
+ return { type: "nevent", data: getEventPointerFromTag(tag) };
96
+ case "a":
97
+ return {
98
+ type: "naddr",
99
+ data: getAddressPointerFromTag(tag),
100
+ };
101
+ case "p":
102
+ return { type: "nprofile", data: getProfilePointerFromTag(tag) };
103
+ }
104
+ }
105
+ catch (error) { }
106
+ return null;
107
+ }
108
+ export function isAddressPointer(pointer) {
109
+ return (typeof pointer !== "string" &&
110
+ Object.hasOwn(pointer, "identifier") &&
111
+ Object.hasOwn(pointer, "pubkey") &&
112
+ Object.hasOwn(pointer, "kind"));
113
+ }
114
+ export function isEventPointer(pointer) {
115
+ return typeof pointer !== "string" && Object.hasOwn(pointer, "id");
116
+ }
117
+ export function getCoordinateFromAddressPointer(pointer) {
118
+ return `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`;
119
+ }
120
+ export function getATagFromAddressPointer(pointer) {
121
+ const relay = pointer.relays?.[0];
122
+ const coordinate = getCoordinateFromAddressPointer(pointer);
123
+ return relay ? ["a", coordinate, relay] : ["a", coordinate];
124
+ }
125
+ export function getETagFromEventPointer(pointer) {
126
+ return pointer.relays?.length ? ["e", pointer.id, pointer.relays[0]] : ["e", pointer.id];
127
+ }
@@ -19,6 +19,7 @@ export type ProfileContent = {
19
19
  lud06?: string;
20
20
  nip05?: string;
21
21
  };
22
+ /** Returns the parsed profile content for a kind 0 event */
22
23
  export declare function getProfileContent(event: NostrEvent): ProfileContent;
23
24
  export declare function getProfileContent(event: NostrEvent, quite: false): ProfileContent;
24
25
  export declare function getProfileContent(event: NostrEvent, quite: true): ProfileContent | Error;
@@ -0,0 +1,55 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { AddressPointer, EventPointer } from "nostr-tools/nip19";
3
+ export type ThreadReferences = {
4
+ root?: {
5
+ e: EventPointer;
6
+ a: undefined;
7
+ } | {
8
+ e: undefined;
9
+ a: AddressPointer;
10
+ } | {
11
+ e: EventPointer;
12
+ a: AddressPointer;
13
+ };
14
+ reply?: {
15
+ e: EventPointer;
16
+ a: undefined;
17
+ } | {
18
+ e: undefined;
19
+ a: AddressPointer;
20
+ } | {
21
+ e: EventPointer;
22
+ a: AddressPointer;
23
+ };
24
+ };
25
+ export declare const Nip10ThreadRefsSymbol: unique symbol;
26
+ declare module "nostr-tools" {
27
+ interface Event {
28
+ [Nip10ThreadRefsSymbol]?: ThreadReferences;
29
+ }
30
+ }
31
+ /** Parses NIP-10 tags and handles legacy behavior */
32
+ export declare function interpretThreadTags(event: NostrEvent): {
33
+ root?: {
34
+ e: string[];
35
+ a: undefined;
36
+ } | {
37
+ e: undefined;
38
+ a: string[];
39
+ } | {
40
+ e: string[];
41
+ a: string[];
42
+ } | undefined;
43
+ reply?: {
44
+ e: string[];
45
+ a: undefined;
46
+ } | {
47
+ e: undefined;
48
+ a: string[];
49
+ } | {
50
+ e: string[];
51
+ a: string[];
52
+ } | undefined;
53
+ };
54
+ /** Returns the parsed NIP-10 tags for an event */
55
+ export declare function getNip10References(event: NostrEvent): ThreadReferences;
@@ -0,0 +1,61 @@
1
+ import { getAddressPointerFromTag, getEventPointerFromTag } from "./pointers.js";
2
+ export const Nip10ThreadRefsSymbol = Symbol.for("nip10-thread-refs");
3
+ /** Parses NIP-10 tags and handles legacy behavior */
4
+ export function interpretThreadTags(event) {
5
+ const eTags = event.tags.filter((t) => t[0] === "e" && t[1]);
6
+ const aTags = event.tags.filter((t) => t[0] === "a" && t[1]);
7
+ // find the root and reply tags.
8
+ let rootETag = eTags.find((t) => t[3] === "root");
9
+ let replyETag = eTags.find((t) => t[3] === "reply");
10
+ let rootATag = aTags.find((t) => t[3] === "root");
11
+ let replyATag = aTags.find((t) => t[3] === "reply");
12
+ if (!rootETag || !replyETag) {
13
+ // a direct reply does not need a "reply" reference
14
+ // https://github.com/nostr-protocol/nips/blob/master/10.md
15
+ // this is not necessarily to spec. but if there is only one id (root or reply) then assign it to both
16
+ // this handles the cases where a client only set a "reply" tag and no root
17
+ rootETag = replyETag = rootETag || replyETag;
18
+ }
19
+ if (!rootATag || !replyATag) {
20
+ rootATag = replyATag = rootATag || replyATag;
21
+ }
22
+ if (!rootETag && !replyETag) {
23
+ // legacy behavior
24
+ // https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
25
+ const legacyETags = eTags.filter((t) => {
26
+ // ignore it if there is a marker
27
+ if (t[3])
28
+ return false;
29
+ return true;
30
+ });
31
+ if (legacyETags.length >= 1) {
32
+ // first tag is the root
33
+ rootETag = legacyETags[0];
34
+ // last tag is reply
35
+ replyETag = legacyETags[legacyETags.length - 1] ?? rootETag;
36
+ }
37
+ }
38
+ return {
39
+ root: rootETag || rootATag ? { e: rootETag, a: rootATag } : undefined,
40
+ reply: replyETag || replyATag ? { e: replyETag, a: replyATag } : undefined,
41
+ };
42
+ }
43
+ /** Returns the parsed NIP-10 tags for an event */
44
+ export function getNip10References(event) {
45
+ let refs = event[Nip10ThreadRefsSymbol];
46
+ if (!refs) {
47
+ const e = event;
48
+ const tags = interpretThreadTags(e);
49
+ refs = event[Nip10ThreadRefsSymbol] = {
50
+ root: tags.root && {
51
+ e: tags.root.e && getEventPointerFromTag(tags.root.e),
52
+ a: tags.root.a && getAddressPointerFromTag(tags.root.a),
53
+ },
54
+ reply: tags.reply && {
55
+ e: tags.reply.e && getEventPointerFromTag(tags.reply.e),
56
+ a: tags.reply.a && getAddressPointerFromTag(tags.reply.a),
57
+ },
58
+ };
59
+ }
60
+ return refs;
61
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./event-store/index.js";
2
2
  export * from "./query-store/index.js";
3
3
  export * as helpers from "./helpers/index.js";
4
+ export * as Queries from "./queries/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./event-store/index.js";
2
2
  export * from "./query-store/index.js";
3
3
  export * as helpers from "./helpers/index.js";
4
+ export * as Queries from "./queries/index.js";
@@ -5,6 +5,6 @@ export type StatefulObservable<T> = Observable<T> & {
5
5
  error?: Error;
6
6
  complete?: boolean;
7
7
  };
8
- /** Wraps an observable and makes it stateful */
8
+ /** Wraps an {@link Observable} and makes it stateful */
9
9
  export declare function stateful<T extends unknown>(observable: Observable<T>, cleanup?: boolean): StatefulObservable<T>;
10
10
  export declare function isStateful<T extends unknown>(observable: Observable<T> | StatefulObservable<T>): observable is StatefulObservable<T>;
@@ -1,5 +1,5 @@
1
1
  import Observable from "zen-observable";
2
- /** Wraps an observable and makes it stateful */
2
+ /** Wraps an {@link Observable} and makes it stateful */
3
3
  export function stateful(observable, cleanup = false) {
4
4
  let subscription = undefined;
5
5
  let observers = [];
@@ -1,2 +1,3 @@
1
1
  import Observable from "zen-observable";
2
+ /** Throttles an {@link Observable} */
2
3
  export declare function throttle<T>(source: Observable<T>, interval: number): Observable<T>;
@@ -1,4 +1,5 @@
1
1
  import Observable from "zen-observable";
2
+ /** Throttles an {@link Observable} */
2
3
  export function throttle(source, interval) {
3
4
  return new Observable((observer) => {
4
5
  let lastEmissionTime = 0;
@@ -1,6 +1,6 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
- import { Query } from "../index.js";
3
- import { ChannelMetadataContent } from "../../helpers/channel.js";
2
+ import { Query } from "../query-store/index.js";
3
+ import { ChannelMetadataContent } from "../helpers/channel.js";
4
4
  /** Creates a query that returns the latest parsed metadata */
5
5
  export declare function ChannelMetadataQuery(channel: NostrEvent): Query<ChannelMetadataContent | undefined>;
6
6
  /** Creates a query that returns a map of hidden messages Map<id, reason> */
@@ -1,6 +1,6 @@
1
1
  import { kinds } from "nostr-tools";
2
- import { getChannelMetadataContent } from "../../helpers/channel.js";
3
- import { safeParse } from "../../helpers/json.js";
2
+ import { getChannelMetadataContent } from "../helpers/channel.js";
3
+ import { safeParse } from "../helpers/json.js";
4
4
  /** Creates a query that returns the latest parsed metadata */
5
5
  export function ChannelMetadataQuery(channel) {
6
6
  return {
@@ -4,3 +4,4 @@ export * from "./mailboxes.js";
4
4
  export * from "./reactions.js";
5
5
  export * from "./channel.js";
6
6
  export * from "./mute.js";
7
+ export * from "./thread.js";
@@ -4,3 +4,4 @@ export * from "./mailboxes.js";
4
4
  export * from "./reactions.js";
5
5
  export * from "./channel.js";
6
6
  export * from "./mute.js";
7
+ export * from "./thread.js";
@@ -1,4 +1,4 @@
1
- import { Query } from "../index.js";
1
+ import { Query } from "../query-store/index.js";
2
2
  export declare function MailboxesQuery(pubkey: string): Query<{
3
3
  inboxes: Set<string>;
4
4
  outboxes: Set<string>;
@@ -1,5 +1,5 @@
1
1
  import { kinds } from "nostr-tools";
2
- import { getInboxes, getOutboxes } from "../../helpers/mailboxes.js";
2
+ import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
3
3
  export function MailboxesQuery(pubkey) {
4
4
  return {
5
5
  key: pubkey,
@@ -1,4 +1,4 @@
1
- import { Query } from "../index.js";
1
+ import { Query } from "../query-store/index.js";
2
2
  export declare function UserMuteQuery(pubkey: string): Query<{
3
3
  words: Set<string>;
4
4
  pubkeys: Set<string>;
@@ -1,5 +1,5 @@
1
1
  import { kinds } from "nostr-tools";
2
- import { getMutedHashtags, getMutedPubkeys, getMutedThreads, getMutedWords } from "../../helpers/mute.js";
2
+ import { getMutedHashtags, getMutedPubkeys, getMutedThreads, getMutedWords } from "../helpers/mute.js";
3
3
  export function UserMuteQuery(pubkey) {
4
4
  return {
5
5
  key: pubkey,
@@ -0,0 +1,3 @@
1
+ import { ProfileContent } from "../helpers/profile.js";
2
+ import { Query } from "../query-store/index.js";
3
+ export declare function ProfileQuery(pubkey: string): Query<ProfileContent | undefined>;
@@ -1,5 +1,5 @@
1
1
  import { kinds } from "nostr-tools";
2
- import { getProfileContent } from "../../helpers/profile.js";
2
+ import { getProfileContent } from "../helpers/profile.js";
3
3
  export function ProfileQuery(pubkey) {
4
4
  return {
5
5
  key: pubkey,
@@ -1,4 +1,4 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
- import { Query } from "../index.js";
2
+ import { Query } from "../query-store/index.js";
3
3
  /** Creates a query that returns all reactions to an event (supports replaceable events) */
4
4
  export declare function ReactionsQuery(event: NostrEvent): Query<NostrEvent[]>;
@@ -1,5 +1,5 @@
1
1
  import { kinds } from "nostr-tools";
2
- import { getEventUID, isReplaceable } from "../../helpers/event.js";
2
+ import { getEventUID, isReplaceable } from "../helpers/event.js";
3
3
  /** Creates a query that returns all reactions to an event (supports replaceable events) */
4
4
  export function ReactionsQuery(event) {
5
5
  return {
@@ -1,5 +1,5 @@
1
1
  import { Filter, NostrEvent } from "nostr-tools";
2
- import { Query } from "../index.js";
2
+ import { Query } from "../query-store/index.js";
3
3
  export declare function SingleEventQuery(uid: string): Query<NostrEvent | undefined>;
4
4
  export declare function ReplaceableQuery(kind: number, pubkey: string, d?: string): Query<NostrEvent | undefined>;
5
5
  export declare function TimelineQuery(filters: Filter | Filter[]): Query<NostrEvent[]>;
@@ -1,5 +1,5 @@
1
1
  import stringify from "json-stringify-deterministic";
2
- import { getReplaceableUID } from "../../helpers/event.js";
2
+ import { getReplaceableUID } from "../helpers/event.js";
3
3
  export function SingleEventQuery(uid) {
4
4
  return {
5
5
  key: uid,
@@ -0,0 +1,23 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { AddressPointer, EventPointer } from "nostr-tools/nip19";
3
+ import { Query } from "../query-store/index.js";
4
+ import { ThreadReferences } from "../helpers/threading.js";
5
+ export type Thread = {
6
+ root?: ThreadItem;
7
+ all: Map<string, ThreadItem>;
8
+ };
9
+ export type ThreadItem = {
10
+ /** underlying nostr event */
11
+ event: NostrEvent;
12
+ refs: ThreadReferences;
13
+ /** the thread root, according to this event */
14
+ root?: ThreadItem;
15
+ /** the parent event this is replying to */
16
+ parent?: ThreadItem;
17
+ /** direct child replies */
18
+ replies: Set<ThreadItem>;
19
+ };
20
+ export type ThreadQueryOptions = {
21
+ kinds?: number[];
22
+ };
23
+ export declare function ThreadQuery(root: string | AddressPointer | EventPointer, opts?: ThreadQueryOptions): Query<Thread>;
@@ -0,0 +1,65 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { getNip10References } from "../helpers/threading.js";
3
+ import { getCoordinateFromAddressPointer, isAddressPointer } from "../helpers/pointers.js";
4
+ import { getEventUID } from "../helpers/event.js";
5
+ const defaultOptions = {
6
+ kinds: [kinds.ShortTextNote],
7
+ };
8
+ export function ThreadQuery(root, opts) {
9
+ const parentReferences = new Map();
10
+ const items = new Map();
11
+ const { kinds } = { ...defaultOptions, ...opts };
12
+ let rootUID = "";
13
+ const rootFilter = {};
14
+ const replyFilter = { kinds };
15
+ if (isAddressPointer(root)) {
16
+ rootUID = getCoordinateFromAddressPointer(root);
17
+ rootFilter.kinds = [root.kind];
18
+ rootFilter.authors = [root.pubkey];
19
+ rootFilter["#d"] = [root.identifier];
20
+ replyFilter["#a"] = [rootUID];
21
+ }
22
+ else if (typeof root === "string") {
23
+ rootUID = root;
24
+ rootFilter.ids = [root];
25
+ replyFilter["#e"] = [root];
26
+ }
27
+ else {
28
+ rootUID = root.id;
29
+ rootFilter.ids = [root.id];
30
+ replyFilter["#e"] = [root.id];
31
+ }
32
+ return {
33
+ key: `${rootUID}-${kinds.join(",")}`,
34
+ run: (events) => events.stream([rootFilter, replyFilter]).map((event) => {
35
+ if (!items.has(getEventUID(event))) {
36
+ const refs = getNip10References(event);
37
+ const replies = parentReferences.get(getEventUID(event)) || new Set();
38
+ const item = { event, refs, replies };
39
+ for (const child of replies) {
40
+ child.parent = item;
41
+ }
42
+ // add item to parent
43
+ if (refs.reply?.e || refs.reply?.a) {
44
+ let uid = refs.reply.e ? refs.reply.e.id : getCoordinateFromAddressPointer(refs.reply.a);
45
+ item.parent = items.get(uid);
46
+ if (item.parent) {
47
+ item.parent.replies.add(item);
48
+ }
49
+ else {
50
+ // parent isn't created yet, store ref for later
51
+ let set = parentReferences.get(uid);
52
+ if (!set) {
53
+ set = new Set();
54
+ parentReferences.set(uid, set);
55
+ }
56
+ set.add(item);
57
+ }
58
+ }
59
+ // add item to map
60
+ items.set(getEventUID(event), item);
61
+ }
62
+ return { root: items.get(rootUID), all: items };
63
+ }),
64
+ };
65
+ }
@@ -2,7 +2,8 @@ import Observable from "zen-observable";
2
2
  import { Filter, NostrEvent } from "nostr-tools";
3
3
  import { EventStore } from "../event-store/event-store.js";
4
4
  import { LRU } from "../utils/lru.js";
5
- import * as Queries from "./queries/index.js";
5
+ import * as Queries from "../queries/index.js";
6
+ import { AddressPointer, EventPointer } from "nostr-tools/nip19";
6
7
  export type Query<T extends unknown> = {
7
8
  key: string;
8
9
  run: (events: EventStore, store: QueryStore) => Observable<T>;
@@ -40,5 +41,6 @@ export declare class QueryStore {
40
41
  threads: Set<string>;
41
42
  hashtags: Set<string>;
42
43
  } | undefined>;
44
+ thread(root: string | EventPointer | AddressPointer): Observable<Queries.Thread>;
43
45
  }
44
46
  export { Queries };
@@ -1,6 +1,6 @@
1
1
  import { stateful } from "../observable/stateful.js";
2
2
  import { LRU } from "../utils/lru.js";
3
- import * as Queries from "./queries/index.js";
3
+ import * as Queries from "../queries/index.js";
4
4
  export class QueryStore {
5
5
  static Queries = Queries;
6
6
  store;
@@ -49,5 +49,8 @@ export class QueryStore {
49
49
  mute(pubkey) {
50
50
  return this.runQuery(Queries.UserMuteQuery)(pubkey);
51
51
  }
52
+ thread(root) {
53
+ return this.runQuery(Queries.ThreadQuery)(root);
54
+ }
52
55
  }
53
56
  export { Queries };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,10 @@
33
33
  "./event-store": {
34
34
  "import": "./dist/event-store/index.js",
35
35
  "types": "./dist/event-store/index.d.ts"
36
+ },
37
+ "./queries": {
38
+ "import": "./dist/queries/index.js",
39
+ "types": "./dist/queries/index.d.ts"
36
40
  }
37
41
  },
38
42
  "dependencies": {
@@ -1,3 +0,0 @@
1
- import { ProfileContent } from "../../helpers/profile.js";
2
- import { Query } from "../index.js";
3
- export declare function ProfileQuery(pubkey: string): Query<ProfileContent | undefined>;