applesauce-core 0.12.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +28 -10
  2. package/dist/__tests__/exports.test.d.ts +1 -0
  3. package/dist/__tests__/exports.test.js +17 -0
  4. package/dist/event-store/__tests__/event-store.test.js +52 -1
  5. package/dist/event-store/database.js +3 -3
  6. package/dist/event-store/event-store.js +15 -8
  7. package/dist/event-store/interface.d.ts +11 -7
  8. package/dist/helpers/__tests__/bookmarks.test.d.ts +1 -0
  9. package/dist/helpers/__tests__/bookmarks.test.js +88 -0
  10. package/dist/helpers/__tests__/comment.test.js +14 -0
  11. package/dist/helpers/__tests__/contacts.test.d.ts +1 -0
  12. package/dist/helpers/__tests__/contacts.test.js +34 -0
  13. package/dist/helpers/__tests__/events.test.d.ts +1 -0
  14. package/dist/helpers/__tests__/events.test.js +32 -0
  15. package/dist/helpers/__tests__/exports.test.d.ts +1 -0
  16. package/dist/helpers/__tests__/exports.test.js +220 -0
  17. package/dist/helpers/__tests__/mutes.test.d.ts +1 -0
  18. package/dist/helpers/__tests__/mutes.test.js +55 -0
  19. package/dist/helpers/blossom.d.ts +2 -0
  20. package/dist/helpers/blossom.js +18 -0
  21. package/dist/helpers/bookmarks.d.ts +6 -1
  22. package/dist/helpers/bookmarks.js +52 -7
  23. package/dist/helpers/comment.d.ts +7 -3
  24. package/dist/helpers/comment.js +6 -1
  25. package/dist/helpers/contacts.d.ts +11 -0
  26. package/dist/helpers/contacts.js +34 -0
  27. package/dist/helpers/event.d.ts +8 -3
  28. package/dist/helpers/event.js +21 -13
  29. package/dist/helpers/lists.d.ts +40 -12
  30. package/dist/helpers/lists.js +62 -23
  31. package/dist/helpers/mutes.d.ts +8 -0
  32. package/dist/helpers/mutes.js +66 -5
  33. package/dist/helpers/nip-19.d.ts +14 -0
  34. package/dist/helpers/nip-19.js +29 -0
  35. package/dist/helpers/pointers.js +6 -6
  36. package/dist/helpers/profile.js +1 -1
  37. package/dist/observable/__tests__/exports.test.d.ts +1 -0
  38. package/dist/observable/__tests__/exports.test.js +18 -0
  39. package/dist/observable/__tests__/listen-latest-updates.test.d.ts +1 -0
  40. package/dist/observable/__tests__/listen-latest-updates.test.js +55 -0
  41. package/dist/observable/defined.d.ts +3 -0
  42. package/dist/observable/defined.js +5 -0
  43. package/dist/observable/get-observable-value.d.ts +4 -1
  44. package/dist/observable/get-observable-value.js +4 -1
  45. package/dist/observable/index.d.ts +5 -1
  46. package/dist/observable/index.js +6 -1
  47. package/dist/observable/listen-latest-updates.d.ts +5 -0
  48. package/dist/observable/listen-latest-updates.js +12 -0
  49. package/dist/promise/__tests__/exports.test.d.ts +1 -0
  50. package/dist/promise/__tests__/exports.test.js +11 -0
  51. package/dist/queries/__tests__/exports.test.d.ts +1 -0
  52. package/dist/queries/__tests__/exports.test.js +41 -0
  53. package/dist/queries/blossom.js +1 -6
  54. package/dist/queries/bookmarks.d.ts +5 -5
  55. package/dist/queries/bookmarks.js +18 -17
  56. package/dist/queries/channels.js +41 -53
  57. package/dist/queries/comments.js +6 -9
  58. package/dist/queries/contacts.d.ts +6 -1
  59. package/dist/queries/contacts.js +21 -9
  60. package/dist/queries/index.d.ts +1 -0
  61. package/dist/queries/index.js +1 -0
  62. package/dist/queries/mailboxes.js +4 -7
  63. package/dist/queries/mutes.d.ts +6 -6
  64. package/dist/queries/mutes.js +20 -19
  65. package/dist/queries/pins.d.ts +1 -0
  66. package/dist/queries/pins.js +4 -6
  67. package/dist/queries/profile.js +1 -6
  68. package/dist/queries/reactions.js +11 -14
  69. package/dist/queries/relays.d.ts +27 -0
  70. package/dist/queries/relays.js +44 -0
  71. package/dist/queries/simple.js +5 -22
  72. package/dist/queries/thread.js +45 -51
  73. package/dist/queries/user-status.js +23 -29
  74. package/dist/queries/zaps.js +10 -13
  75. package/dist/query-store/__tests__/exports.test.d.ts +1 -0
  76. package/dist/query-store/__tests__/exports.test.js +12 -0
  77. package/dist/query-store/query-store.d.ts +7 -6
  78. package/dist/query-store/query-store.js +13 -8
  79. package/package.json +3 -3
  80. package/dist/observable/share-latest-value.d.ts +0 -6
  81. package/dist/observable/share-latest-value.js +0 -24
package/README.md CHANGED
@@ -1,37 +1,55 @@
1
1
  # applesauce-core
2
2
 
3
- AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/typedoc/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/typedoc/modules/Queries)
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.
4
4
 
5
- # Example
5
+ ## Key Components
6
+
7
+ - **Helpers**: Core utility methods for parsing and extracting data from nostr events
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
11
+
12
+ ## Documentation
13
+
14
+ For detailed documentation and guides, visit:
15
+
16
+ - [Getting Started](https://hzrd149.github.io/applesauce/introduction/getting-started)
17
+ - [API Reference](https://hzrd149.github.io/applesauce/typedoc/)
18
+
19
+ ## Example
6
20
 
7
21
  ```js
8
22
  import { EventStore, QueryStore } from "applesauce-core";
9
23
  import { Relay } from "nostr-tools/relay";
10
24
 
11
- // The EventStore handles all the events
25
+ // Create a single EventStore instance for your app
12
26
  const eventStore = new EventStore();
13
27
 
14
- // The QueryStore handles queries and makes sure not to run multiple of the same query
28
+ // Create a QueryStore to manage subscriptions efficiently
15
29
  const queryStore = new QueryStore(eventStore);
16
30
 
17
- // Use nostr-tools or anything else to talk to relays
31
+ // Use any nostr library for relay connections (nostr-tools, ndk, nostrify, etc...)
18
32
  const relay = await Relay.connect("wss://relay.example.com");
19
33
 
20
- const sub = relay.subscribe([{ authors: ["266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5"] }], {
34
+ // Subscribe to events and add them to the store
35
+ const sub = relay.subscribe([{ authors: ["3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"] }], {
21
36
  onevent(event) {
22
37
  eventStore.add(event);
23
38
  },
24
39
  });
25
40
 
26
- // This will return an Observable<ProfileContent | undefined> of the parsed metadata
27
- const profile = queryStore.profile("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
41
+ // Subscribe to profile changes using ProfileQuery
42
+ const profile = queryStore.createQuery(
43
+ ProfileQuery,
44
+ "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
45
+ );
28
46
 
29
47
  profile.subscribe((parsed) => {
30
48
  if (parsed) console.log(parsed);
31
49
  });
32
50
 
33
- // This will return an Observable<NostrEvent[]> of all kind 1 events sorted by created_at
34
- const timeline = queryStore.timeline({ kinds: [1] });
51
+ // Subscribe to a timeline of events
52
+ const timeline = queryStore.createQuery(TimelineQuery, { kinds: [1] });
35
53
 
36
54
  timeline.subscribe((events) => {
37
55
  console.log(events);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as exports from "../index.js";
3
+ describe("exports", () => {
4
+ it("should export the expected functions", () => {
5
+ expect(Object.keys(exports).sort()).toMatchInlineSnapshot(`
6
+ [
7
+ "Database",
8
+ "EventStore",
9
+ "EventStoreSymbol",
10
+ "Helpers",
11
+ "Queries",
12
+ "QueryStore",
13
+ "logger",
14
+ ]
15
+ `);
16
+ });
17
+ });
@@ -31,7 +31,7 @@ describe("add", () => {
31
31
  expect(eventStore.getEvent(profile.id)).toBeDefined();
32
32
  expect([...getSeenRelays(eventStore.getEvent(profile.id))]).toEqual(expect.arrayContaining(["wss://relay.a.com", "wss://relay.b.com"]));
33
33
  });
34
- it("should ignore deleted events", () => {
34
+ it("should ignore old deleted events but not newer ones", () => {
35
35
  const deleteEvent = {
36
36
  id: "delete event id",
37
37
  kind: kinds.EventDeletion,
@@ -45,7 +45,58 @@ describe("add", () => {
45
45
  eventStore.add(deleteEvent);
46
46
  // now event should be ignored
47
47
  eventStore.add(profile);
48
+ const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
49
+ eventStore.add(newProfile);
48
50
  expect(eventStore.getEvent(profile.id)).toBeUndefined();
51
+ expect(eventStore.getEvent(newProfile.id)).toBeDefined();
52
+ });
53
+ it("should remove profile events when delete event is added", () => {
54
+ // Add initial replaceable event
55
+ eventStore.add(profile);
56
+ expect(eventStore.getEvent(profile.id)).toBeDefined();
57
+ const newProfile = user.profile({ name: "new name" }, { created_at: profile.created_at + 1000 });
58
+ eventStore.add(newProfile);
59
+ const deleteEvent = {
60
+ id: "delete event id",
61
+ kind: kinds.EventDeletion,
62
+ created_at: profile.created_at + 100,
63
+ pubkey: user.pubkey,
64
+ tags: [["a", `${profile.kind}:${profile.pubkey}`]],
65
+ sig: "this should be ignored for the test",
66
+ content: "test",
67
+ };
68
+ // Add delete event with coordinate
69
+ eventStore.add(deleteEvent);
70
+ // Profile should be removed since delete event is newer
71
+ expect(eventStore.getEvent(profile.id)).toBeUndefined();
72
+ expect(eventStore.getEvent(newProfile.id)).toBeDefined();
73
+ expect(eventStore.getReplaceable(profile.kind, profile.pubkey)).toBe(newProfile);
74
+ });
75
+ it("should remove addressable replaceable events when delete event is added", () => {
76
+ // Add initial replaceable event
77
+ const event = user.event({ content: "test", kind: 30000, tags: [["d", "test"]] });
78
+ eventStore.add(event);
79
+ expect(eventStore.getEvent(event.id)).toBeDefined();
80
+ const newEvent = user.event({
81
+ ...event,
82
+ created_at: event.created_at + 500,
83
+ });
84
+ eventStore.add(newEvent);
85
+ const deleteEvent = {
86
+ id: "delete event id",
87
+ kind: kinds.EventDeletion,
88
+ created_at: event.created_at + 100,
89
+ pubkey: user.pubkey,
90
+ tags: [["a", `${event.kind}:${event.pubkey}:test`]],
91
+ sig: "this should be ignored for the test",
92
+ content: "test",
93
+ };
94
+ // Add delete event with coordinate
95
+ eventStore.add(deleteEvent);
96
+ // Profile should be removed since delete event is newer
97
+ expect(eventStore.getEvent(event.id)).toBeUndefined();
98
+ expect(eventStore.getEvent(newEvent.id)).toBeDefined();
99
+ expect(eventStore.getReplaceable(event.kind, event.pubkey, "test")).toBe(newEvent);
49
100
  });
50
101
  });
51
102
  describe("inserts", () => {
@@ -1,6 +1,6 @@
1
1
  import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
2
2
  import { Subject } from "rxjs";
3
- import { getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
3
+ import { getEventUID, getIndexableTags, createReplaceableAddress, isReplaceable } from "../helpers/event.js";
4
4
  import { INDEXABLE_TAGS } from "./common.js";
5
5
  import { logger } from "../logger.js";
6
6
  import { LRU } from "../helpers/lru.js";
@@ -73,12 +73,12 @@ export class Database {
73
73
  }
74
74
  /** Checks if the database contains a replaceable event without touching it */
75
75
  hasReplaceable(kind, pubkey, d) {
76
- const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d));
76
+ const events = this.replaceable.get(createReplaceableAddress(kind, pubkey, d));
77
77
  return !!events && events.length > 0;
78
78
  }
79
79
  /** Gets an array of replaceable events */
80
80
  getReplaceable(kind, pubkey, d) {
81
- return this.replaceable.get(getReplaceableUID(kind, pubkey, d));
81
+ return this.replaceable.get(createReplaceableAddress(kind, pubkey, d));
82
82
  }
83
83
  /** Inserts an event into the database and notifies all subscriptions */
84
84
  addEvent(event) {
@@ -1,14 +1,15 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { insertEventIntoDescendingList } from "nostr-tools/utils";
3
- import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
3
+ import { isAddressableKind } from "nostr-tools/kinds";
4
4
  import { defer, distinctUntilChanged, EMPTY, endWith, filter, finalize, from, map, merge, mergeMap, mergeWith, of, repeat, scan, take, takeUntil, tap, } from "rxjs";
5
5
  import { Database } from "./database.js";
6
- import { FromCacheSymbol, getEventUID, getReplaceableIdentifier, getReplaceableUID, getTagValue, isReplaceable, } from "../helpers/event.js";
6
+ import { FromCacheSymbol, getEventUID, getReplaceableIdentifier, createReplaceableAddress, getTagValue, isReplaceable, } from "../helpers/event.js";
7
7
  import { matchFilters } from "../helpers/filter.js";
8
8
  import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
9
9
  import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
10
10
  import { claimEvents } from "../observable/claim-events.js";
11
11
  import { claimLatest } from "../observable/claim-latest.js";
12
+ import { parseCoordinate } from "../helpers/pointers.js";
12
13
  export const EventStoreSymbol = Symbol.for("event-store");
13
14
  function sortDesc(a, b) {
14
15
  return b.created_at - a.created_at;
@@ -53,7 +54,7 @@ export class EventStore {
53
54
  else {
54
55
  if (this.deletedIds.has(event.id))
55
56
  return true;
56
- if (isParameterizedReplaceableKind(event.kind)) {
57
+ if (isAddressableKind(event.kind)) {
57
58
  const deleted = this.deletedCoords.get(getEventUID(event));
58
59
  if (deleted)
59
60
  return deleted > event.created_at;
@@ -74,10 +75,16 @@ export class EventStore {
74
75
  const coords = getDeleteCoordinates(deleteEvent);
75
76
  for (const coord of coords) {
76
77
  this.deletedCoords.set(coord, Math.max(this.deletedCoords.get(coord) ?? 0, deleteEvent.created_at));
77
- // remove deleted events in the database
78
- const event = this.database.getEvent(coord);
79
- if (event && event.created_at < deleteEvent.created_at)
80
- this.database.removeEvent(event);
78
+ // Parse the nostr address coordinate
79
+ const parsed = parseCoordinate(coord);
80
+ if (!parsed)
81
+ continue;
82
+ // Remove older versions of replaceable events
83
+ const events = this.database.getReplaceable(parsed.kind, parsed.pubkey, parsed.identifier) ?? [];
84
+ for (const event of events) {
85
+ if (event.created_at < deleteEvent.created_at)
86
+ this.database.removeEvent(event);
87
+ }
81
88
  }
82
89
  }
83
90
  /** Copies important metadata from and identical event to another */
@@ -288,7 +295,7 @@ export class EventStore {
288
295
  }
289
296
  /** Creates an observable that subscribes to the latest version of an array of replaceable events*/
290
297
  replaceableSet(pointers) {
291
- const uids = new Set(pointers.map((p) => getReplaceableUID(p.kind, p.pubkey, p.identifier)));
298
+ const uids = new Set(pointers.map((p) => createReplaceableAddress(p.kind, p.pubkey, p.identifier)));
292
299
  return merge(
293
300
  // start with existing events
294
301
  defer(() => from(pointers.map((p) => this.getReplaceable(p.kind, p.pubkey, p.identifier)))),
@@ -1,12 +1,6 @@
1
1
  import { Filter, NostrEvent } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
- export interface IEventStore {
4
- inserts: Observable<NostrEvent>;
5
- updates: Observable<NostrEvent>;
6
- removes: Observable<NostrEvent>;
7
- add(event: NostrEvent, fromRelay?: string): NostrEvent;
8
- remove(event: string | NostrEvent): boolean;
9
- update(event: NostrEvent): NostrEvent;
3
+ export interface ISyncEventStore {
10
4
  hasEvent(id: string): boolean;
11
5
  hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
12
6
  getEvent(id: string): NostrEvent | undefined;
@@ -14,6 +8,11 @@ export interface IEventStore {
14
8
  getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
15
9
  getAll(filters: Filter | Filter[]): Set<NostrEvent>;
16
10
  getTimeline(filters: Filter | Filter[]): NostrEvent[];
11
+ }
12
+ export interface IStreamEventStore {
13
+ inserts: Observable<NostrEvent>;
14
+ updates: Observable<NostrEvent>;
15
+ removes: Observable<NostrEvent>;
17
16
  filters(filters: Filter | Filter[]): Observable<NostrEvent>;
18
17
  updated(id: string | NostrEvent): Observable<NostrEvent>;
19
18
  removed(id: string): Observable<never>;
@@ -27,3 +26,8 @@ export interface IEventStore {
27
26
  }[]): Observable<Record<string, NostrEvent>>;
28
27
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
29
28
  }
29
+ export interface IEventStore extends ISyncEventStore, IStreamEventStore {
30
+ add(event: NostrEvent, fromRelay?: string): NostrEvent;
31
+ remove(event: string | NostrEvent): boolean;
32
+ update(event: NostrEvent): NostrEvent;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { kinds } from "nostr-tools";
3
+ import { mergeBookmarks } from "../bookmarks.js";
4
+ describe("mergeBookmarks", () => {
5
+ it("should merge bookmarks and handle duplicates", () => {
6
+ // Create test data with some duplicates
7
+ const eventPointer1 = {
8
+ id: "event1",
9
+ relays: ["wss://relay1.com/", "wss://relay2.com/"],
10
+ author: "author1",
11
+ };
12
+ const eventPointer2 = {
13
+ id: "event1", // Same ID as eventPointer1
14
+ relays: ["wss://relay2.com/", "wss://relay3.com/"],
15
+ author: "author1",
16
+ };
17
+ const eventPointer3 = {
18
+ id: "event2",
19
+ relays: ["wss://relay1.com/"],
20
+ author: "author2",
21
+ };
22
+ const addressPointer1 = {
23
+ kind: kinds.LongFormArticle,
24
+ pubkey: "pubkey1",
25
+ identifier: "article1",
26
+ relays: ["wss://relay1.com/", "wss://relay2.com/"],
27
+ };
28
+ const addressPointer2 = {
29
+ kind: kinds.LongFormArticle,
30
+ pubkey: "pubkey1",
31
+ identifier: "article1", // Same as addressPointer1
32
+ relays: ["wss://relay3.com/"],
33
+ };
34
+ const bookmark1 = {
35
+ notes: [eventPointer1],
36
+ articles: [addressPointer1],
37
+ hashtags: ["tag1", "tag2"],
38
+ urls: ["https://example1.com/"],
39
+ };
40
+ const bookmark2 = {
41
+ notes: [eventPointer2, eventPointer3],
42
+ articles: [addressPointer2],
43
+ hashtags: ["tag2", "tag3"],
44
+ urls: ["https://example1.com/", "https://example2.com/"],
45
+ };
46
+ const result = mergeBookmarks(bookmark1, bookmark2);
47
+ // Check that duplicates are properly merged
48
+ expect(result.notes).toHaveLength(2); // event1 should be merged, plus event2
49
+ expect(result.articles).toHaveLength(1); // article1 should be merged
50
+ expect(result.hashtags).toHaveLength(3); // unique tags
51
+ expect(result.urls).toHaveLength(2); // unique urls
52
+ // Check that relays are merged for duplicate event
53
+ const mergedEvent = result.notes.find((note) => note.id === "event1");
54
+ expect(mergedEvent?.relays).toHaveLength(3);
55
+ expect(mergedEvent?.relays).toContain("wss://relay1.com/");
56
+ expect(mergedEvent?.relays).toContain("wss://relay2.com/");
57
+ expect(mergedEvent?.relays).toContain("wss://relay3.com/");
58
+ // Check that relays are merged for duplicate article
59
+ const mergedArticle = result.articles[0];
60
+ expect(mergedArticle.relays).toHaveLength(3);
61
+ expect(mergedArticle.relays).toContain("wss://relay1.com/");
62
+ expect(mergedArticle.relays).toContain("wss://relay2.com/");
63
+ expect(mergedArticle.relays).toContain("wss://relay3.com/");
64
+ // Check that hashtags are unique
65
+ expect(result.hashtags).toContain("tag1");
66
+ expect(result.hashtags).toContain("tag2");
67
+ expect(result.hashtags).toContain("tag3");
68
+ // Check that urls are unique
69
+ expect(result.urls).toContain("https://example1.com/");
70
+ expect(result.urls).toContain("https://example2.com/");
71
+ });
72
+ it("should handle undefined bookmarks", () => {
73
+ const bookmark = {
74
+ notes: [{ id: "event1", relays: ["wss://relay1.com/"] }],
75
+ articles: [],
76
+ hashtags: ["tag1"],
77
+ urls: ["https://example.com/"],
78
+ };
79
+ const result = mergeBookmarks(bookmark, undefined);
80
+ expect(result).toEqual(bookmark);
81
+ expect(mergeBookmarks(undefined, undefined)).toEqual({
82
+ notes: [],
83
+ articles: [],
84
+ hashtags: [],
85
+ urls: [],
86
+ });
87
+ });
88
+ });
@@ -27,6 +27,7 @@ describe("getCommentEventPointer", () => {
27
27
  ["e", "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f"],
28
28
  ];
29
29
  expect(getCommentEventPointer(tags, true)).toEqual({
30
+ type: "event",
30
31
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
31
32
  kind: 1621,
32
33
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -45,6 +46,7 @@ describe("getCommentEventPointer", () => {
45
46
  ["P", "bad-pubkey"],
46
47
  ];
47
48
  expect(getCommentEventPointer(tags, true)).toEqual({
49
+ type: "event",
48
50
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
49
51
  kind: 1621,
50
52
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -62,6 +64,7 @@ describe("getCommentEventPointer", () => {
62
64
  ["K", "1621"],
63
65
  ];
64
66
  expect(getCommentEventPointer(tags, true)).toEqual({
67
+ type: "event",
65
68
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
66
69
  kind: 1621,
67
70
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -75,6 +78,7 @@ describe("getCommentEventPointer", () => {
75
78
  ["P", "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10"],
76
79
  ];
77
80
  expect(getCommentEventPointer(tags, true)).toEqual({
81
+ type: "event",
78
82
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
79
83
  kind: 1621,
80
84
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -107,6 +111,7 @@ describe("getCommentAddressPointer", () => {
107
111
  ["E", "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f"],
108
112
  ["K", "30000"],
109
113
  ], true)).toEqual({
114
+ type: "address",
110
115
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
111
116
  kind: 30000,
112
117
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -118,6 +123,7 @@ describe("getCommentAddressPointer", () => {
118
123
  ["e", "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f"],
119
124
  ["k", "30000"],
120
125
  ])).toEqual({
126
+ type: "address",
121
127
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
122
128
  kind: 30000,
123
129
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -130,6 +136,7 @@ describe("getCommentAddressPointer", () => {
130
136
  ["A", "30000:e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10:list", "wss://relay.io/"],
131
137
  ["K", "30000"],
132
138
  ], true)).toEqual({
139
+ type: "address",
133
140
  kind: 30000,
134
141
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
135
142
  identifier: "list",
@@ -140,6 +147,7 @@ describe("getCommentAddressPointer", () => {
140
147
  ["a", "30000:e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10:list", "wss://relay.io/"],
141
148
  ["k", "30000"],
142
149
  ])).toEqual({
150
+ type: "address",
143
151
  kind: 30000,
144
152
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
145
153
  identifier: "list",
@@ -153,6 +161,7 @@ describe("getCommentAddressPointer", () => {
153
161
  ["E", "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f", "wss://relay.io/"],
154
162
  ["K", "30000"],
155
163
  ], true)).toEqual({
164
+ type: "address",
156
165
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
157
166
  kind: 30000,
158
167
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -165,6 +174,7 @@ describe("getCommentAddressPointer", () => {
165
174
  ["e", "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f", "wss://relay.io/"],
166
175
  ["k", "30000"],
167
176
  ])).toEqual({
177
+ type: "address",
168
178
  id: "86c0b95589b016ffb703bfc080d49e54106e74e2d683295119c3453e494dbe6f",
169
179
  kind: 30000,
170
180
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
@@ -178,6 +188,7 @@ describe("getCommentAddressPointer", () => {
178
188
  ["A", "30010:e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10:list"],
179
189
  ["K", "30000"],
180
190
  ], true)).toEqual({
191
+ type: "address",
181
192
  kind: 30010,
182
193
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
183
194
  identifier: "list",
@@ -187,6 +198,7 @@ describe("getCommentAddressPointer", () => {
187
198
  ["a", "30010:e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10:list"],
188
199
  ["k", "30000"],
189
200
  ])).toEqual({
201
+ type: "address",
190
202
  kind: 30010,
191
203
  pubkey: "e4336cd525df79fa4d3af364fd9600d4b10dce4215aa4c33ed77ea0842344b10",
192
204
  identifier: "list",
@@ -214,6 +226,7 @@ describe("getCommentExternalPointer", () => {
214
226
  ["I", "podcast:item:guid:d98d189b-dc7b-45b1-8720-d4b98690f31f"],
215
227
  ["K", "podcast:item:guid"],
216
228
  ], true)).toEqual({
229
+ type: "external",
217
230
  identifier: "podcast:item:guid:d98d189b-dc7b-45b1-8720-d4b98690f31f",
218
231
  kind: "podcast:item:guid",
219
232
  });
@@ -222,6 +235,7 @@ describe("getCommentExternalPointer", () => {
222
235
  ["i", "podcast:item:guid:d98d189b-dc7b-45b1-8720-d4b98690f31f"],
223
236
  ["k", "podcast:item:guid"],
224
237
  ])).toEqual({
238
+ type: "external",
225
239
  identifier: "podcast:item:guid:d98d189b-dc7b-45b1-8720-d4b98690f31f",
226
240
  kind: "podcast:item:guid",
227
241
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mergeContacts } from "../contacts.js";
3
+ describe("mergeContacts", () => {
4
+ it("should merge contacts and remove duplicates", () => {
5
+ // Create some test profile pointers
6
+ const pointer1 = { pubkey: "pubkey1", relays: ["relay1"] };
7
+ const pointer2 = { pubkey: "pubkey2" }; // No relays
8
+ const pointer3 = { pubkey: "pubkey3", relays: ["relay3"] };
9
+ const pointer4 = { pubkey: "pubkey1" }; // Duplicate pubkey without relays
10
+ // Test merging arrays of pointers
11
+ const result1 = mergeContacts([pointer1, pointer2], [pointer3, pointer4]);
12
+ // Should have 3 unique pubkeys
13
+ expect(result1.length).toBe(3);
14
+ // Check that the duplicate was handled correctly (last one should win)
15
+ const pubkey1Entry = result1.find((p) => p.pubkey === "pubkey1");
16
+ expect(pubkey1Entry).toBeDefined();
17
+ expect(pubkey1Entry?.relays).toBeUndefined();
18
+ // Test with undefined values
19
+ const result2 = mergeContacts([pointer1], undefined, [pointer2, undefined]);
20
+ expect(result2.length).toBe(2);
21
+ // Test with single pointers
22
+ const result3 = mergeContacts(pointer1, pointer2, pointer1);
23
+ expect(result3.length).toBe(2);
24
+ // Test with empty arrays
25
+ const result4 = mergeContacts([], [pointer1], []);
26
+ expect(result4.length).toBe(1);
27
+ // Test with pointers that have and don't have relays
28
+ const pointer5 = { pubkey: "pubkey5", relays: ["relay5"] };
29
+ const pointer6 = { pubkey: "pubkey5" }; // Same pubkey without relays
30
+ const result5 = mergeContacts([pointer5], [pointer6]);
31
+ expect(result5.length).toBe(1);
32
+ expect(result5[0].relays).toBeUndefined();
33
+ });
34
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { describe, expect, it } from "vitest";
3
+ import { FakeUser } from "../../__tests__/fixtures.js";
4
+ import { getReplaceableAddress } from "../event.js";
5
+ const user = new FakeUser();
6
+ describe("getReplaceableAddress", () => {
7
+ it("should throw an error for non-replaceable events", () => {
8
+ const normalEvent = user.note("Hello world");
9
+ expect(() => {
10
+ getReplaceableAddress(normalEvent);
11
+ }).toThrow("Event is not replaceable or addressable");
12
+ });
13
+ it("should return the correct address for replaceable events", () => {
14
+ const replaceableEvent = user.event({
15
+ kind: kinds.Metadata,
16
+ content: JSON.stringify({ name: "Test User" }),
17
+ tags: [],
18
+ });
19
+ const expectedAddress = `${kinds.Metadata}:${user.pubkey}:`;
20
+ expect(getReplaceableAddress(replaceableEvent)).toBe(expectedAddress);
21
+ });
22
+ it("should include the identifier for addressable events", () => {
23
+ const identifier = "test-profile";
24
+ const addressableEvent = user.event({
25
+ kind: 30000, // Parameterized replaceable event
26
+ content: "Test content",
27
+ tags: [["d", identifier]],
28
+ });
29
+ const expectedAddress = `30000:${user.pubkey}:${identifier}`;
30
+ expect(getReplaceableAddress(addressableEvent)).toBe(expectedAddress);
31
+ });
32
+ });
@@ -0,0 +1 @@
1
+ export {};