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.
- package/README.md +28 -10
- package/dist/__tests__/exports.test.d.ts +1 -0
- package/dist/__tests__/exports.test.js +17 -0
- package/dist/event-store/__tests__/event-store.test.js +52 -1
- package/dist/event-store/database.js +3 -3
- package/dist/event-store/event-store.js +15 -8
- package/dist/event-store/interface.d.ts +11 -7
- package/dist/helpers/__tests__/bookmarks.test.d.ts +1 -0
- package/dist/helpers/__tests__/bookmarks.test.js +88 -0
- package/dist/helpers/__tests__/comment.test.js +14 -0
- package/dist/helpers/__tests__/contacts.test.d.ts +1 -0
- package/dist/helpers/__tests__/contacts.test.js +34 -0
- package/dist/helpers/__tests__/events.test.d.ts +1 -0
- package/dist/helpers/__tests__/events.test.js +32 -0
- package/dist/helpers/__tests__/exports.test.d.ts +1 -0
- package/dist/helpers/__tests__/exports.test.js +220 -0
- package/dist/helpers/__tests__/mutes.test.d.ts +1 -0
- package/dist/helpers/__tests__/mutes.test.js +55 -0
- package/dist/helpers/blossom.d.ts +2 -0
- package/dist/helpers/blossom.js +18 -0
- package/dist/helpers/bookmarks.d.ts +6 -1
- package/dist/helpers/bookmarks.js +52 -7
- package/dist/helpers/comment.d.ts +7 -3
- package/dist/helpers/comment.js +6 -1
- package/dist/helpers/contacts.d.ts +11 -0
- package/dist/helpers/contacts.js +34 -0
- package/dist/helpers/event.d.ts +8 -3
- package/dist/helpers/event.js +21 -13
- package/dist/helpers/lists.d.ts +40 -12
- package/dist/helpers/lists.js +62 -23
- package/dist/helpers/mutes.d.ts +8 -0
- package/dist/helpers/mutes.js +66 -5
- package/dist/helpers/nip-19.d.ts +14 -0
- package/dist/helpers/nip-19.js +29 -0
- package/dist/helpers/pointers.js +6 -6
- package/dist/helpers/profile.js +1 -1
- package/dist/observable/__tests__/exports.test.d.ts +1 -0
- package/dist/observable/__tests__/exports.test.js +18 -0
- package/dist/observable/__tests__/listen-latest-updates.test.d.ts +1 -0
- package/dist/observable/__tests__/listen-latest-updates.test.js +55 -0
- package/dist/observable/defined.d.ts +3 -0
- package/dist/observable/defined.js +5 -0
- package/dist/observable/get-observable-value.d.ts +4 -1
- package/dist/observable/get-observable-value.js +4 -1
- package/dist/observable/index.d.ts +5 -1
- package/dist/observable/index.js +6 -1
- package/dist/observable/listen-latest-updates.d.ts +5 -0
- package/dist/observable/listen-latest-updates.js +12 -0
- package/dist/promise/__tests__/exports.test.d.ts +1 -0
- package/dist/promise/__tests__/exports.test.js +11 -0
- package/dist/queries/__tests__/exports.test.d.ts +1 -0
- package/dist/queries/__tests__/exports.test.js +41 -0
- package/dist/queries/blossom.js +1 -6
- package/dist/queries/bookmarks.d.ts +5 -5
- package/dist/queries/bookmarks.js +18 -17
- package/dist/queries/channels.js +41 -53
- package/dist/queries/comments.js +6 -9
- package/dist/queries/contacts.d.ts +6 -1
- package/dist/queries/contacts.js +21 -9
- package/dist/queries/index.d.ts +1 -0
- package/dist/queries/index.js +1 -0
- package/dist/queries/mailboxes.js +4 -7
- package/dist/queries/mutes.d.ts +6 -6
- package/dist/queries/mutes.js +20 -19
- package/dist/queries/pins.d.ts +1 -0
- package/dist/queries/pins.js +4 -6
- package/dist/queries/profile.js +1 -6
- package/dist/queries/reactions.js +11 -14
- package/dist/queries/relays.d.ts +27 -0
- package/dist/queries/relays.js +44 -0
- package/dist/queries/simple.js +5 -22
- package/dist/queries/thread.js +45 -51
- package/dist/queries/user-status.js +23 -29
- package/dist/queries/zaps.js +10 -13
- package/dist/query-store/__tests__/exports.test.d.ts +1 -0
- package/dist/query-store/__tests__/exports.test.js +12 -0
- package/dist/query-store/query-store.d.ts +7 -6
- package/dist/query-store/query-store.js +13 -8
- package/package.json +3 -3
- package/dist/observable/share-latest-value.d.ts +0 -6
- 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
|
|
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
|
-
|
|
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
|
-
//
|
|
25
|
+
// Create a single EventStore instance for your app
|
|
12
26
|
const eventStore = new EventStore();
|
|
13
27
|
|
|
14
|
-
//
|
|
28
|
+
// Create a QueryStore to manage subscriptions efficiently
|
|
15
29
|
const queryStore = new QueryStore(eventStore);
|
|
16
30
|
|
|
17
|
-
// Use nostr
|
|
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
|
-
|
|
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
|
-
//
|
|
27
|
-
const profile = queryStore.
|
|
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
|
-
//
|
|
34
|
-
const timeline = queryStore.
|
|
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,
|
|
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(
|
|
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(
|
|
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 {
|
|
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,
|
|
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 (
|
|
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
|
-
//
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
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) =>
|
|
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
|
|
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 {};
|