applesauce-core 0.11.0 → 0.12.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 (41) hide show
  1. package/dist/event-store/__tests__/event-store.test.js +14 -1
  2. package/dist/event-store/database.d.ts +1 -1
  3. package/dist/event-store/database.js +1 -1
  4. package/dist/event-store/event-store.d.ts +11 -5
  5. package/dist/event-store/event-store.js +25 -9
  6. package/dist/event-store/index.d.ts +1 -0
  7. package/dist/event-store/index.js +1 -0
  8. package/dist/event-store/interface.d.ts +27 -0
  9. package/dist/helpers/__tests__/nip-19.test.d.ts +1 -0
  10. package/dist/helpers/__tests__/nip-19.test.js +42 -0
  11. package/dist/helpers/cache.d.ts +3 -4
  12. package/dist/helpers/cache.js +1 -1
  13. package/dist/helpers/direct-messages.d.ts +4 -0
  14. package/dist/helpers/direct-messages.js +5 -0
  15. package/dist/helpers/event.d.ts +10 -2
  16. package/dist/helpers/event.js +5 -0
  17. package/dist/helpers/gift-wraps.d.ts +12 -0
  18. package/dist/helpers/gift-wraps.js +49 -0
  19. package/dist/helpers/hidden-content.d.ts +48 -0
  20. package/dist/helpers/hidden-content.js +88 -0
  21. package/dist/helpers/hidden-tags.d.ts +10 -28
  22. package/dist/helpers/hidden-tags.js +24 -59
  23. package/dist/helpers/index.d.ts +3 -0
  24. package/dist/helpers/index.js +3 -0
  25. package/dist/helpers/mutes.d.ts +1 -0
  26. package/dist/helpers/mutes.js +2 -1
  27. package/dist/helpers/nip-19.d.ts +4 -0
  28. package/dist/helpers/nip-19.js +27 -0
  29. package/dist/observable/claim-latest.d.ts +3 -2
  30. package/dist/observable/claim-latest.js +2 -1
  31. package/dist/observable/with-immediate-value.d.ts +3 -0
  32. package/dist/observable/with-immediate-value.js +19 -0
  33. package/dist/queries/mutes.js +1 -1
  34. package/dist/queries/simple.d.ts +1 -1
  35. package/dist/queries/simple.js +3 -3
  36. package/dist/query-store/__tests__/query-store.test.d.ts +1 -0
  37. package/dist/query-store/{query-store.test.js → __tests__/query-store.test.js} +33 -3
  38. package/dist/query-store/query-store.d.ts +6 -4
  39. package/dist/query-store/query-store.js +12 -3
  40. package/package.json +5 -4
  41. /package/dist/{query-store/query-store.test.d.ts → event-store/interface.js} +0 -0
@@ -69,7 +69,7 @@ describe("verifyEvent", () => {
69
69
  expect(verifyEvent).toHaveBeenCalledTimes(1);
70
70
  });
71
71
  });
72
- describe("deleted", () => {
72
+ describe("removed", () => {
73
73
  it("should complete when event is removed", () => {
74
74
  eventStore.add(profile);
75
75
  const spy = subscribeSpyTo(eventStore.removed(profile.id));
@@ -223,6 +223,19 @@ describe("timeline", () => {
223
223
  eventStore.add(user.profile({ name: "old-name" }, { created_at: profile.created_at - 1000 }));
224
224
  expect(spy.getValues()).toEqual([[profile]]);
225
225
  });
226
+ it("should return new array for every value", () => {
227
+ const first = user.note("first note");
228
+ const second = user.note("second note");
229
+ const third = user.note("third note");
230
+ eventStore.add(first);
231
+ const spy = subscribeSpyTo(eventStore.timeline({ kinds: [0] }));
232
+ eventStore.add(second);
233
+ eventStore.add(third);
234
+ const hasDuplicates = (arr) => {
235
+ return new Set(arr).size !== arr.length;
236
+ };
237
+ expect(hasDuplicates(spy.getValues())).toBe(false);
238
+ });
226
239
  });
227
240
  describe("replaceableSet", () => {
228
241
  it("should not emit if there are not events", () => {
@@ -60,7 +60,7 @@ export declare class Database {
60
60
  iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
61
61
  /** Returns all events that match the filter */
62
62
  getEventsForFilter(filter: Filter): Set<NostrEvent>;
63
- getForFilters(filters: Filter[]): Set<NostrEvent>;
63
+ getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
64
64
  /** Remove the oldest events that are not claimed */
65
65
  prune(limit?: number): number;
66
66
  }
@@ -287,7 +287,7 @@ export class Database {
287
287
  }
288
288
  return events;
289
289
  }
290
- getForFilters(filters) {
290
+ getEventsForFilters(filters) {
291
291
  if (filters.length === 0)
292
292
  throw new Error("No Filters");
293
293
  let events = new Set();
@@ -1,12 +1,16 @@
1
1
  import { Filter, NostrEvent } from "nostr-tools";
2
2
  import { Observable } from "rxjs";
3
3
  import { Database } from "./database.js";
4
- export declare class EventStore {
4
+ import { IEventStore } from "./interface.js";
5
+ export declare const EventStoreSymbol: unique symbol;
6
+ export declare class EventStore implements IEventStore {
5
7
  database: Database;
6
8
  /** Enable this to keep old versions of replaceable events */
7
9
  keepOldVersions: boolean;
8
10
  /** A method used to verify new events before added them */
9
11
  verifyEvent?: (event: NostrEvent) => boolean;
12
+ /** A stream of events that have been updated */
13
+ updates: Observable<NostrEvent>;
10
14
  constructor();
11
15
  protected deletedIds: Set<string>;
12
16
  protected deletedCoords: Map<string, number>;
@@ -26,16 +30,18 @@ export declare class EventStore {
26
30
  /** Add an event to the store and notifies all subscribes it has updated */
27
31
  update(event: NostrEvent): NostrEvent;
28
32
  /** Get all events matching a filter */
29
- getAll(filters: Filter[]): Set<NostrEvent>;
33
+ getAll(filters: Filter | Filter[]): Set<NostrEvent>;
30
34
  /** Check if the store has an event */
31
- hasEvent(uid: string): boolean;
32
- getEvent(uid: string): NostrEvent | undefined;
35
+ hasEvent(id: string): boolean;
36
+ getEvent(id: string): NostrEvent | undefined;
33
37
  /** Check if the store has a replaceable event */
34
38
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
35
39
  /** Gets the latest version of a replaceable event */
36
40
  getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent | undefined;
37
41
  /** Returns all versions of a replaceable event */
38
42
  getReplaceableHistory(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
43
+ /** Returns a timeline of events that match filters */
44
+ getTimeline(filters: Filter | Filter[]): NostrEvent[];
39
45
  /**
40
46
  * Creates an observable that streams all events that match the filter and remains open
41
47
  * @param filters
@@ -45,7 +51,7 @@ export declare class EventStore {
45
51
  /** Returns an observable that completes when an event is removed */
46
52
  removed(id: string): Observable<never>;
47
53
  /** Creates an observable that emits when event is updated */
48
- updated(id: string): Observable<NostrEvent>;
54
+ updated(event: string | NostrEvent): Observable<NostrEvent>;
49
55
  /** Creates an observable that subscribes to a single event */
50
56
  event(id: string): Observable<NostrEvent | undefined>;
51
57
  /** Creates an observable that subscribes to multiple events */
@@ -9,6 +9,7 @@ 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
+ export const EventStoreSymbol = Symbol.for("event-store");
12
13
  function sortDesc(a, b) {
13
14
  return b.created_at - a.created_at;
14
15
  }
@@ -18,6 +19,8 @@ export class EventStore {
18
19
  keepOldVersions = false;
19
20
  /** A method used to verify new events before added them */
20
21
  verifyEvent;
22
+ /** A stream of events that have been updated */
23
+ updates;
21
24
  constructor() {
22
25
  this.database = new Database();
23
26
  this.database.onBeforeInsert = (event) => {
@@ -25,6 +28,15 @@ export class EventStore {
25
28
  if (this.verifyEvent && this.verifyEvent(event) === false)
26
29
  throw new Error("Invalid event");
27
30
  };
31
+ // when events are added to the database, add the symbol
32
+ this.database.inserted.subscribe((event) => {
33
+ Reflect.set(event, EventStoreSymbol, this);
34
+ });
35
+ // when events are removed from the database, remove the symbol
36
+ this.database.removed.subscribe((event) => {
37
+ Reflect.deleteProperty(event, EventStoreSymbol);
38
+ });
39
+ this.updates = this.database.updated;
28
40
  }
29
41
  // delete state
30
42
  deletedIds = new Set();
@@ -117,14 +129,14 @@ export class EventStore {
117
129
  }
118
130
  /** Get all events matching a filter */
119
131
  getAll(filters) {
120
- return this.database.getForFilters(filters);
132
+ return this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
121
133
  }
122
134
  /** Check if the store has an event */
123
- hasEvent(uid) {
124
- return this.database.hasEvent(uid);
135
+ hasEvent(id) {
136
+ return this.database.hasEvent(id);
125
137
  }
126
- getEvent(uid) {
127
- return this.database.getEvent(uid);
138
+ getEvent(id) {
139
+ return this.database.getEvent(id);
128
140
  }
129
141
  /** Check if the store has a replaceable event */
130
142
  hasReplaceable(kind, pubkey, d) {
@@ -138,6 +150,10 @@ export class EventStore {
138
150
  getReplaceableHistory(kind, pubkey, d) {
139
151
  return this.database.getReplaceable(kind, pubkey, d);
140
152
  }
153
+ /** Returns a timeline of events that match filters */
154
+ getTimeline(filters) {
155
+ return Array.from(this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters])).sort(sortDesc);
156
+ }
141
157
  /**
142
158
  * Creates an observable that streams all events that match the filter and remains open
143
159
  * @param filters
@@ -165,8 +181,8 @@ export class EventStore {
165
181
  mergeMap(() => EMPTY));
166
182
  }
167
183
  /** Creates an observable that emits when event is updated */
168
- updated(id) {
169
- return this.database.updated.pipe(filter((e) => e.id === id));
184
+ updated(event) {
185
+ return this.database.updated.pipe(filter((e) => e.id === event || e === event));
170
186
  }
171
187
  /** Creates an observable that subscribes to a single event */
172
188
  event(id) {
@@ -183,7 +199,7 @@ export class EventStore {
183
199
  // emit undefined when deleted
184
200
  this.removed(id).pipe(endWith(undefined))).pipe(
185
201
  // claim all events
186
- claimEvents(this.database));
202
+ claimLatest(this.database));
187
203
  }
188
204
  /** Creates an observable that subscribes to multiple events */
189
205
  events(ids) {
@@ -278,7 +294,7 @@ export class EventStore {
278
294
  filters = Array.isArray(filters) ? filters : [filters];
279
295
  const seen = new Map();
280
296
  // get current events
281
- return defer(() => of(Array.from(this.database.getForFilters(filters)).sort(sortDesc))).pipe(
297
+ return defer(() => of(Array.from(this.database.getEventsForFilters(filters)).sort(sortDesc))).pipe(
282
298
  // claim existing events
283
299
  claimEvents(this.database),
284
300
  // subscribe to newer events
@@ -1,2 +1,3 @@
1
1
  export * from "./event-store.js";
2
2
  export * from "./database.js";
3
+ export * from "./interface.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./event-store.js";
2
2
  export * from "./database.js";
3
+ export * from "./interface.js";
@@ -0,0 +1,27 @@
1
+ import { Filter, NostrEvent } from "nostr-tools";
2
+ import { Observable } from "rxjs";
3
+ export interface IEventStore {
4
+ updates: Observable<NostrEvent>;
5
+ add(event: NostrEvent, fromRelay?: string): NostrEvent;
6
+ remove(event: string | NostrEvent): boolean;
7
+ update(event: NostrEvent): NostrEvent;
8
+ hasEvent(id: string): boolean;
9
+ hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
10
+ getEvent(id: string): NostrEvent | undefined;
11
+ getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
12
+ getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
13
+ getAll(filters: Filter | Filter[]): Set<NostrEvent>;
14
+ getTimeline(filters: Filter | Filter[]): NostrEvent[];
15
+ filters(filters: Filter | Filter[]): Observable<NostrEvent>;
16
+ updated(id: string | NostrEvent): Observable<NostrEvent>;
17
+ removed(id: string): Observable<never>;
18
+ event(id: string): Observable<NostrEvent | undefined>;
19
+ events(ids: string[]): Observable<Record<string, NostrEvent>>;
20
+ replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
21
+ replaceableSet(pointers: {
22
+ kind: number;
23
+ pubkey: string;
24
+ identifier?: string;
25
+ }[]): Observable<Record<string, NostrEvent>>;
26
+ timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { bytesToHex } from "@noble/hashes/utils";
3
+ import { normalizeToPubkey, normalizeToSecretKey } from "../nip-19.js";
4
+ describe("normalizeToPubkey", () => {
5
+ it("should get pubkey from npub", () => {
6
+ expect(normalizeToPubkey("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
7
+ });
8
+ it("should get pubkey from nprofile", () => {
9
+ expect(normalizeToPubkey("nprofile1qyw8wumn8ghj7umpw3jkcmrfw3jju6r6wfjrzdpe9e3k7mf0qyf8wumn8ghj7mn0wd68yat99e3k7mf0qqszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fg5g7lja")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
10
+ });
11
+ it("should return hex pubkey", () => {
12
+ expect(normalizeToPubkey("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
13
+ });
14
+ it("should throw on invalid hex pubkey", () => {
15
+ expect(() => {
16
+ normalizeToPubkey("5028372");
17
+ }).toThrow();
18
+ });
19
+ it("should throw on invalid string", () => {
20
+ expect(() => {
21
+ normalizeToPubkey("testing");
22
+ }).toThrow();
23
+ });
24
+ });
25
+ describe("normalizeToSecretKey", () => {
26
+ it("should get secret key from nsec", () => {
27
+ expect(bytesToHex(normalizeToSecretKey("nsec1xe7znq745x5n68566l32ru72aajz3pk2cys9lnf3tuexvkw0dldsj8v2lm"))).toEqual("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb");
28
+ });
29
+ it("should get secret key from raw hex", () => {
30
+ expect(bytesToHex(normalizeToSecretKey("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb"))).toEqual("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb");
31
+ });
32
+ it("should throw on invalid hex key", () => {
33
+ expect(() => {
34
+ normalizeToSecretKey("209573290");
35
+ }).toThrow();
36
+ });
37
+ it("should throw on npub", () => {
38
+ expect(() => {
39
+ normalizeToSecretKey("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr");
40
+ }).toThrow();
41
+ });
42
+ });
@@ -1,5 +1,4 @@
1
- import { EventTemplate, NostrEvent } from "nostr-tools";
2
- export declare function getCachedValue<T extends unknown>(event: NostrEvent | EventTemplate, symbol: symbol): T | undefined;
3
- export declare function setCachedValue<T extends unknown>(event: NostrEvent | EventTemplate, symbol: symbol, value: T): void;
1
+ export declare function getCachedValue<T extends unknown>(event: any, symbol: symbol): T | undefined;
2
+ export declare function setCachedValue<T extends unknown>(event: any, symbol: symbol, value: T): void;
4
3
  /** Internal method used to cache computed values on events */
5
- export declare function getOrComputeCachedValue<T extends unknown>(event: NostrEvent | EventTemplate, symbol: symbol, compute: (event: NostrEvent | EventTemplate) => T): T;
4
+ export declare function getOrComputeCachedValue<T extends unknown>(event: any, symbol: symbol, compute: () => T): T;
@@ -10,7 +10,7 @@ export function getOrComputeCachedValue(event, symbol, compute) {
10
10
  return Reflect.get(event, symbol);
11
11
  }
12
12
  else {
13
- const value = compute(event);
13
+ const value = compute();
14
14
  Reflect.set(event, symbol, value);
15
15
  return value;
16
16
  }
@@ -0,0 +1,4 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { HiddenContentSigner } from "./hidden-content.js";
3
+ /** Returns the decrypted content of a direct message */
4
+ export declare function decryptDirectMessage(message: NostrEvent, signer: HiddenContentSigner): Promise<string>;
@@ -0,0 +1,5 @@
1
+ import { getHiddenContent, unlockHiddenContent } from "./hidden-content.js";
2
+ /** Returns the decrypted content of a direct message */
3
+ export async function decryptDirectMessage(message, signer) {
4
+ return getHiddenContent(message) || (await unlockHiddenContent(message, signer));
5
+ }
@@ -1,4 +1,5 @@
1
- import { EventTemplate, NostrEvent, VerifiedEvent } from "nostr-tools";
1
+ import { NostrEvent, VerifiedEvent } from "nostr-tools";
2
+ import { IEventStore } from "../event-store/interface.js";
2
3
  export declare const EventUIDSymbol: unique symbol;
3
4
  export declare const EventIndexableTagsSymbol: unique symbol;
4
5
  export declare const FromCacheSymbol: unique symbol;
@@ -34,13 +35,20 @@ export declare function getIndexableTags(event: NostrEvent): Set<string>;
34
35
  * Returns the second index ( tag[1] ) of the first tag that matches the name
35
36
  * If the event has any hidden tags they will be searched first
36
37
  */
37
- export declare function getTagValue(event: NostrEvent | EventTemplate, name: string): string | undefined;
38
+ export declare function getTagValue<T extends {
39
+ kind: number;
40
+ tags: string[][];
41
+ content: string;
42
+ pubkey: string;
43
+ }>(event: T, name: string): string | undefined;
38
44
  /** Sets events verified flag without checking anything */
39
45
  export declare function fakeVerifyEvent(event: NostrEvent): event is VerifiedEvent;
40
46
  /** Marks an event as being from a cache */
41
47
  export declare function markFromCache(event: NostrEvent): void;
42
48
  /** Returns if an event was from a cache */
43
49
  export declare function isFromCache(event: NostrEvent): boolean;
50
+ /** Returns the EventStore of an event if its been added to one */
51
+ export declare function getParentEventStore<T extends object>(event: T): IEventStore | undefined;
44
52
  /**
45
53
  * Returns the replaceable identifier for a replaceable event
46
54
  * @throws
@@ -3,6 +3,7 @@ import { INDEXABLE_TAGS } from "../event-store/common.js";
3
3
  import { getHiddenTags } from "./hidden-tags.js";
4
4
  import { getOrComputeCachedValue } from "./cache.js";
5
5
  import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
6
+ import { EventStoreSymbol } from "../event-store/event-store.js";
6
7
  export const EventUIDSymbol = Symbol.for("event-uid");
7
8
  export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
8
9
  export const FromCacheSymbol = Symbol.for("from-cache");
@@ -90,6 +91,10 @@ export function markFromCache(event) {
90
91
  export function isFromCache(event) {
91
92
  return !!event[FromCacheSymbol];
92
93
  }
94
+ /** Returns the EventStore of an event if its been added to one */
95
+ export function getParentEventStore(event) {
96
+ return Reflect.get(event, EventStoreSymbol);
97
+ }
93
98
  /**
94
99
  * Returns the replaceable identifier for a replaceable event
95
100
  * @throws
@@ -0,0 +1,12 @@
1
+ import { NostrEvent, UnsignedEvent } from "nostr-tools";
2
+ import { HiddenContentSigner } from "./hidden-content.js";
3
+ export declare const GiftWrapSealSymbol: unique symbol;
4
+ export declare const GiftWrapEventSymbol: unique symbol;
5
+ /** Returns the unsigned seal event in a gift-wrap event */
6
+ export declare function getGiftWrapSeal(gift: NostrEvent): NostrEvent | undefined;
7
+ /** Returns the unsigned event in the gift-wrap seal */
8
+ export declare function getGiftWrapEvent(gift: NostrEvent): UnsignedEvent | undefined;
9
+ /** Returns if a gift-wrap event or gift-wrap seal is locked */
10
+ export declare function isGiftWrapLocked(gift: NostrEvent): boolean;
11
+ /** Unlocks and returns the unsigned seal event in a gift-wrap */
12
+ export declare function unlockGiftWrap(gift: NostrEvent, signer: HiddenContentSigner): Promise<UnsignedEvent>;
@@ -0,0 +1,49 @@
1
+ import { verifyEvent } from "nostr-tools";
2
+ import { getHiddenContent, isHiddenContentLocked, unlockHiddenContent } from "./hidden-content.js";
3
+ import { getOrComputeCachedValue } from "./cache.js";
4
+ export const GiftWrapSealSymbol = Symbol.for("gift-wrap-seal");
5
+ export const GiftWrapEventSymbol = Symbol.for("gift-wrap-event");
6
+ /** Returns the unsigned seal event in a gift-wrap event */
7
+ export function getGiftWrapSeal(gift) {
8
+ if (isHiddenContentLocked(gift))
9
+ return undefined;
10
+ return getOrComputeCachedValue(gift, GiftWrapSealSymbol, () => {
11
+ const plaintext = getHiddenContent(gift);
12
+ if (!plaintext)
13
+ throw new Error("Gift-wrap is locked");
14
+ const seal = JSON.parse(plaintext);
15
+ // verify the seal is valid
16
+ verifyEvent(seal);
17
+ return seal;
18
+ });
19
+ }
20
+ /** Returns the unsigned event in the gift-wrap seal */
21
+ export function getGiftWrapEvent(gift) {
22
+ if (isHiddenContentLocked(gift))
23
+ return undefined;
24
+ return getOrComputeCachedValue(gift, GiftWrapEventSymbol, () => {
25
+ const seal = getGiftWrapSeal(gift);
26
+ if (!seal)
27
+ throw new Error("Gift is locked");
28
+ const plaintext = getHiddenContent(seal);
29
+ if (!plaintext)
30
+ throw new Error("Gift-wrap seal is locked");
31
+ const event = JSON.parse(plaintext);
32
+ if (event.pubkey !== seal.pubkey)
33
+ throw new Error("Seal author does not match content");
34
+ return event;
35
+ });
36
+ }
37
+ /** Returns if a gift-wrap event or gift-wrap seal is locked */
38
+ export function isGiftWrapLocked(gift) {
39
+ return isHiddenContentLocked(gift) || isHiddenContentLocked(getGiftWrapSeal(gift));
40
+ }
41
+ /** Unlocks and returns the unsigned seal event in a gift-wrap */
42
+ export async function unlockGiftWrap(gift, signer) {
43
+ if (isHiddenContentLocked(gift))
44
+ await unlockHiddenContent(gift, signer);
45
+ const seal = getGiftWrapSeal(gift);
46
+ if (isHiddenContentLocked(seal))
47
+ await unlockHiddenContent(seal, signer);
48
+ return getGiftWrapEvent(gift);
49
+ }
@@ -0,0 +1,48 @@
1
+ export declare const HiddenContentSymbol: unique symbol;
2
+ export type HiddenContentSigner = {
3
+ nip04?: {
4
+ encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
5
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
6
+ };
7
+ nip44?: {
8
+ encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
9
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
10
+ };
11
+ };
12
+ /** Various event kinds that can have encrypted tags in their content and which encryption method they use */
13
+ export declare const EventContentEncryptionMethod: Record<number, "nip04" | "nip44">;
14
+ /** Sets the encryption method that is used for the contents of a specific event kind */
15
+ export declare function setEventContentEncryptionMethod(kind: number, method: "nip04" | "nip44"): void;
16
+ /** Checks if an event can have hidden content */
17
+ export declare function canHaveHiddenContent(kind: number): boolean;
18
+ /** Checks if an event has hidden content */
19
+ export declare function hasHiddenContent<T extends {
20
+ kind: number;
21
+ content: string;
22
+ }>(event: T): boolean;
23
+ /** Returns the hidden tags for an event if they are unlocked */
24
+ export declare function getHiddenContent<T extends object>(event: T): string | undefined;
25
+ /** Checks if the hidden tags are locked */
26
+ export declare function isHiddenContentLocked<T extends object>(event: T): boolean;
27
+ /** Returns either nip04 or nip44 encryption methods depending on event kind */
28
+ export declare function getHiddenContentEncryptionMethods(kind: number, signer: HiddenContentSigner): {
29
+ encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
30
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
31
+ } | {
32
+ encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
33
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
34
+ };
35
+ export type HiddenContentEvent = {
36
+ kind: number;
37
+ pubkey: string;
38
+ content: string;
39
+ };
40
+ /**
41
+ * Unlocks the encrypted content in an event
42
+ * @param event The event with content to decrypt
43
+ * @param signer A signer to use to decrypt the tags
44
+ * @throws
45
+ */
46
+ export declare function unlockHiddenContent<T extends HiddenContentEvent>(event: T, signer: HiddenContentSigner): Promise<string>;
47
+ /** Removes the unencrypted hidden content on an event */
48
+ export declare function lockHiddenContent<T extends object>(event: T): void;
@@ -0,0 +1,88 @@
1
+ import * as kinds from "nostr-tools/kinds";
2
+ import { GROUPS_LIST_KIND } from "./groups.js";
3
+ import { getParentEventStore, isEvent } from "./event.js";
4
+ export const HiddenContentSymbol = Symbol.for("hidden-content");
5
+ /** Various event kinds that can have encrypted tags in their content and which encryption method they use */
6
+ export const EventContentEncryptionMethod = {
7
+ // NIP-60 wallet
8
+ 17375: "nip44",
9
+ 375: "nip44",
10
+ 7375: "nip44",
11
+ 7376: "nip44",
12
+ // DMs
13
+ [kinds.EncryptedDirectMessage]: "nip04",
14
+ // Gift wraps
15
+ [kinds.GiftWrap]: "nip44",
16
+ // NIP-51 lists
17
+ [kinds.BookmarkList]: "nip04",
18
+ [kinds.InterestsList]: "nip04",
19
+ [kinds.Mutelist]: "nip04",
20
+ [kinds.CommunitiesList]: "nip04",
21
+ [kinds.PublicChatsList]: "nip04",
22
+ [kinds.SearchRelaysList]: "nip04",
23
+ [GROUPS_LIST_KIND]: "nip04",
24
+ // NIP-51 sets
25
+ [kinds.Bookmarksets]: "nip04",
26
+ [kinds.Relaysets]: "nip04",
27
+ [kinds.Followsets]: "nip04",
28
+ [kinds.Curationsets]: "nip04",
29
+ [kinds.Interestsets]: "nip04",
30
+ };
31
+ /** Sets the encryption method that is used for the contents of a specific event kind */
32
+ export function setEventContentEncryptionMethod(kind, method) {
33
+ EventContentEncryptionMethod[kind] = method;
34
+ }
35
+ /** Checks if an event can have hidden content */
36
+ export function canHaveHiddenContent(kind) {
37
+ return EventContentEncryptionMethod[kind] !== undefined;
38
+ }
39
+ /** Checks if an event has hidden content */
40
+ export function hasHiddenContent(event) {
41
+ return canHaveHiddenContent(event.kind) && event.content.length > 0;
42
+ }
43
+ /** Returns the hidden tags for an event if they are unlocked */
44
+ export function getHiddenContent(event) {
45
+ return Reflect.get(event, HiddenContentSymbol);
46
+ }
47
+ /** Checks if the hidden tags are locked */
48
+ export function isHiddenContentLocked(event) {
49
+ return Reflect.has(event, HiddenContentSymbol) === false;
50
+ }
51
+ /** Returns either nip04 or nip44 encryption methods depending on event kind */
52
+ export function getHiddenContentEncryptionMethods(kind, signer) {
53
+ const method = EventContentEncryptionMethod[kind];
54
+ const encryption = signer[method];
55
+ if (!encryption)
56
+ throw new Error(`Signer does not support ${method} encryption`);
57
+ return encryption;
58
+ }
59
+ /**
60
+ * Unlocks the encrypted content in an event
61
+ * @param event The event with content to decrypt
62
+ * @param signer A signer to use to decrypt the tags
63
+ * @throws
64
+ */
65
+ export async function unlockHiddenContent(event, signer) {
66
+ if (!canHaveHiddenContent(event.kind))
67
+ throw new Error("Event kind does not support hidden content");
68
+ const encryption = getHiddenContentEncryptionMethods(event.kind, signer);
69
+ const plaintext = await encryption.decrypt(event.pubkey, event.content);
70
+ Reflect.set(event, HiddenContentSymbol, plaintext);
71
+ // if the event has been added to an event store, notify it
72
+ if (isEvent(event)) {
73
+ const eventStore = getParentEventStore(event);
74
+ if (eventStore)
75
+ eventStore.update(event);
76
+ }
77
+ return plaintext;
78
+ }
79
+ /** Removes the unencrypted hidden content on an event */
80
+ export function lockHiddenContent(event) {
81
+ Reflect.deleteProperty(event, HiddenContentSymbol);
82
+ // if the event has been added to an event store, notify it
83
+ if (isEvent(event)) {
84
+ const eventStore = getParentEventStore(event);
85
+ if (eventStore)
86
+ eventStore.update(event);
87
+ }
88
+ }
@@ -1,28 +1,18 @@
1
- import { EventTemplate, NostrEvent } from "nostr-tools";
2
- import { EventStore } from "../event-store/event-store.js";
3
- export type HiddenTagsSigner = {
4
- nip04?: {
5
- encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
6
- decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
7
- };
8
- nip44?: {
9
- encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
10
- decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
11
- };
12
- };
1
+ import { HiddenContentEvent, HiddenContentSigner } from "./hidden-content.js";
13
2
  export declare const HiddenTagsSymbol: unique symbol;
14
- /** Various event kinds that can have encrypted tags in their content and which encryption method they use */
15
- export declare const EventEncryptionMethod: Record<number, "nip04" | "nip44">;
16
3
  /** Checks if an event can have hidden tags */
17
4
  export declare function canHaveHiddenTags(kind: number): boolean;
18
5
  /** Checks if an event has hidden tags */
19
- export declare function hasHiddenTags(event: NostrEvent | EventTemplate): boolean;
6
+ export declare function hasHiddenTags<T extends {
7
+ content: string;
8
+ kind: number;
9
+ }>(event: T): boolean;
20
10
  /** Returns the hidden tags for an event if they are unlocked */
21
- export declare function getHiddenTags(event: NostrEvent | EventTemplate): string[][] | undefined;
11
+ export declare function getHiddenTags<T extends object>(event: T): string[][] | undefined;
22
12
  /** Checks if the hidden tags are locked */
23
- export declare function isHiddenTagsLocked(event: NostrEvent): boolean;
13
+ export declare function isHiddenTagsLocked<T extends object>(event: T): boolean;
24
14
  /** Returns either nip04 or nip44 encryption method depending on list kind */
25
- export declare function getListEncryptionMethods(kind: number, signer: HiddenTagsSigner): {
15
+ export declare function getHiddenTagsEncryptionMethods(kind: number, signer: HiddenContentSigner): {
26
16
  encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
27
17
  decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
28
18
  } | {
@@ -36,13 +26,5 @@ export declare function getListEncryptionMethods(kind: number, signer: HiddenTag
36
26
  * @param store An optional EventStore to notify about the update
37
27
  * @throws
38
28
  */
39
- export declare function unlockHiddenTags<T extends {
40
- kind: number;
41
- pubkey: string;
42
- content: string;
43
- }>(event: T, signer: HiddenTagsSigner, store?: EventStore): Promise<T>;
44
- /**
45
- * Override the hidden tags in an event
46
- * @throws
47
- */
48
- export declare function overrideHiddenTags(event: NostrEvent, hidden: string[][], signer: HiddenTagsSigner): Promise<EventTemplate>;
29
+ export declare function unlockHiddenTags<T extends HiddenContentEvent>(event: T, signer: HiddenContentSigner): Promise<string[][]>;
30
+ export declare function lockHiddenTags<T extends object>(event: T): void;
@@ -1,30 +1,9 @@
1
- import { kinds } from "nostr-tools";
2
- import { GROUPS_LIST_KIND } from "./groups.js";
3
- import { unixNow } from "./time.js";
4
- import { isEvent } from "./event.js";
1
+ import { canHaveHiddenContent, getHiddenContent, getHiddenContentEncryptionMethods, isHiddenContentLocked, lockHiddenContent, unlockHiddenContent, } from "./hidden-content.js";
2
+ import { getOrComputeCachedValue } from "./cache.js";
5
3
  export const HiddenTagsSymbol = Symbol.for("hidden-tags");
6
- /** Various event kinds that can have encrypted tags in their content and which encryption method they use */
7
- export const EventEncryptionMethod = {
8
- // NIP-60 wallet
9
- 37375: "nip44",
10
- // NIP-51 lists
11
- [kinds.BookmarkList]: "nip04",
12
- [kinds.InterestsList]: "nip04",
13
- [kinds.Mutelist]: "nip04",
14
- [kinds.CommunitiesList]: "nip04",
15
- [kinds.PublicChatsList]: "nip04",
16
- [kinds.SearchRelaysList]: "nip04",
17
- [GROUPS_LIST_KIND]: "nip04",
18
- // NIP-51 sets
19
- [kinds.Bookmarksets]: "nip04",
20
- [kinds.Relaysets]: "nip04",
21
- [kinds.Followsets]: "nip04",
22
- [kinds.Curationsets]: "nip04",
23
- [kinds.Interestsets]: "nip04",
24
- };
25
4
  /** Checks if an event can have hidden tags */
26
5
  export function canHaveHiddenTags(kind) {
27
- return EventEncryptionMethod[kind] !== undefined;
6
+ return canHaveHiddenContent(kind);
28
7
  }
29
8
  /** Checks if an event has hidden tags */
30
9
  export function hasHiddenTags(event) {
@@ -32,19 +11,24 @@ export function hasHiddenTags(event) {
32
11
  }
33
12
  /** Returns the hidden tags for an event if they are unlocked */
34
13
  export function getHiddenTags(event) {
35
- return Reflect.get(event, HiddenTagsSymbol);
14
+ if (isHiddenTagsLocked(event))
15
+ return undefined;
16
+ return getOrComputeCachedValue(event, HiddenTagsSymbol, () => {
17
+ const plaintext = getHiddenContent(event);
18
+ const parsed = JSON.parse(plaintext);
19
+ if (!Array.isArray(parsed))
20
+ throw new Error("Content is not an array of tags");
21
+ // Convert array to tags array string[][]
22
+ return parsed.filter((t) => Array.isArray(t)).map((t) => t.map((v) => String(v)));
23
+ });
36
24
  }
37
25
  /** Checks if the hidden tags are locked */
38
26
  export function isHiddenTagsLocked(event) {
39
- return hasHiddenTags(event) && getHiddenTags(event) === undefined;
27
+ return isHiddenContentLocked(event);
40
28
  }
41
29
  /** Returns either nip04 or nip44 encryption method depending on list kind */
42
- export function getListEncryptionMethods(kind, signer) {
43
- const method = EventEncryptionMethod[kind];
44
- const encryption = signer[method];
45
- if (!encryption)
46
- throw new Error(`Signer does not support ${method} encryption`);
47
- return encryption;
30
+ export function getHiddenTagsEncryptionMethods(kind, signer) {
31
+ return getHiddenContentEncryptionMethods(kind, signer);
48
32
  }
49
33
  /**
50
34
  * Decrypts the private list
@@ -53,34 +37,15 @@ export function getListEncryptionMethods(kind, signer) {
53
37
  * @param store An optional EventStore to notify about the update
54
38
  * @throws
55
39
  */
56
- export async function unlockHiddenTags(event, signer, store) {
40
+ export async function unlockHiddenTags(event, signer) {
57
41
  if (!canHaveHiddenTags(event.kind))
58
42
  throw new Error("Event kind does not support hidden tags");
59
- const encryption = getListEncryptionMethods(event.kind, signer);
60
- const plaintext = await encryption.decrypt(event.pubkey, event.content);
61
- const parsed = JSON.parse(plaintext);
62
- if (!Array.isArray(parsed))
63
- throw new Error("Content is not an array of tags");
64
- // Convert array to tags array string[][]
65
- const tags = parsed.filter((t) => Array.isArray(t)).map((t) => t.map((v) => String(v)));
66
- Reflect.set(event, HiddenTagsSymbol, tags);
67
- if (store && isEvent(event))
68
- store.update(event);
69
- return event;
43
+ // unlock hidden content is needed
44
+ if (isHiddenContentLocked(event))
45
+ await unlockHiddenContent(event, signer);
46
+ return getHiddenTags(event);
70
47
  }
71
- /**
72
- * Override the hidden tags in an event
73
- * @throws
74
- */
75
- export async function overrideHiddenTags(event, hidden, signer) {
76
- if (!canHaveHiddenTags(event.kind))
77
- throw new Error("Event kind does not support hidden tags");
78
- const encryption = getListEncryptionMethods(event.kind, signer);
79
- const ciphertext = await encryption.encrypt(event.pubkey, JSON.stringify(hidden));
80
- return {
81
- kind: event.kind,
82
- content: ciphertext,
83
- created_at: unixNow(),
84
- tags: event.tags,
85
- };
48
+ export function lockHiddenTags(event) {
49
+ Reflect.deleteProperty(event, HiddenTagsSymbol);
50
+ lockHiddenContent(event);
86
51
  }
@@ -7,14 +7,17 @@ export * from "./comment.js";
7
7
  export * from "./contacts.js";
8
8
  export * from "./content.js";
9
9
  export * from "./delete.js";
10
+ export * from "./direct-messages.js";
10
11
  export * from "./dns-identity.js";
11
12
  export * from "./emoji.js";
12
13
  export * from "./event.js";
13
14
  export * from "./external-id.js";
14
15
  export * from "./file-metadata.js";
15
16
  export * from "./filter.js";
17
+ export * from "./gift-wraps.js";
16
18
  export * from "./groups.js";
17
19
  export * from "./hashtag.js";
20
+ export * from "./hidden-content.js";
18
21
  export * from "./hidden-tags.js";
19
22
  export * from "./json.js";
20
23
  export * from "./lists.js";
@@ -7,14 +7,17 @@ export * from "./comment.js";
7
7
  export * from "./contacts.js";
8
8
  export * from "./content.js";
9
9
  export * from "./delete.js";
10
+ export * from "./direct-messages.js";
10
11
  export * from "./dns-identity.js";
11
12
  export * from "./emoji.js";
12
13
  export * from "./event.js";
13
14
  export * from "./external-id.js";
14
15
  export * from "./file-metadata.js";
15
16
  export * from "./filter.js";
17
+ export * from "./gift-wraps.js";
16
18
  export * from "./groups.js";
17
19
  export * from "./hashtag.js";
20
+ export * from "./hidden-content.js";
18
21
  export * from "./hidden-tags.js";
19
22
  export * from "./json.js";
20
23
  export * from "./lists.js";
@@ -7,6 +7,7 @@ export type Mutes = {
7
7
  hashtags: Set<string>;
8
8
  words: Set<string>;
9
9
  };
10
+ /** Parses mute tags */
10
11
  export declare function parseMutedTags(tags: string[][]): Mutes;
11
12
  /** Returns muted things */
12
13
  export declare function getMutedThings(mute: NostrEvent): Mutes;
@@ -3,6 +3,7 @@ import { getOrComputeCachedValue } from "./cache.js";
3
3
  import { getHiddenTags } from "./hidden-tags.js";
4
4
  export const MutePublicSymbol = Symbol.for("mute-public");
5
5
  export const MuteHiddenSymbol = Symbol.for("mute-hidden");
6
+ /** Parses mute tags */
6
7
  export function parseMutedTags(tags) {
7
8
  const pubkeys = new Set(tags.filter(isPTag).map((t) => t[1]));
8
9
  const threads = new Set(tags.filter(isETag).map((t) => t[1]));
@@ -12,7 +13,7 @@ export function parseMutedTags(tags) {
12
13
  }
13
14
  /** Returns muted things */
14
15
  export function getMutedThings(mute) {
15
- return getOrComputeCachedValue(mute, MutePublicSymbol, (e) => parseMutedTags(e.tags));
16
+ return getOrComputeCachedValue(mute, MutePublicSymbol, () => parseMutedTags(mute.tags));
16
17
  }
17
18
  /** Returns the hidden muted content if the event is unlocked */
18
19
  export function getHiddenMutedThings(mute) {
@@ -0,0 +1,4 @@
1
+ /** Gets the hex pubkey from any nip-19 encoded string */
2
+ export declare function normalizeToPubkey(str: string): string;
3
+ /** Converts hex to nsec strings into Uint8 secret keys */
4
+ export declare function normalizeToSecretKey(str: string): Uint8Array;
@@ -0,0 +1,27 @@
1
+ import { nip19 } from "nostr-tools";
2
+ import { hexToBytes } from "@noble/hashes/utils";
3
+ import { isHexKey } from "./string.js";
4
+ import { getPubkeyFromDecodeResult } from "./pointers.js";
5
+ /** Gets the hex pubkey from any nip-19 encoded string */
6
+ export function normalizeToPubkey(str) {
7
+ if (isHexKey(str))
8
+ return str;
9
+ else {
10
+ const decode = nip19.decode(str);
11
+ const pubkey = getPubkeyFromDecodeResult(decode);
12
+ if (!pubkey)
13
+ throw new Error(`Cant find pubkey in ${decode.type}`);
14
+ return pubkey;
15
+ }
16
+ }
17
+ /** Converts hex to nsec strings into Uint8 secret keys */
18
+ export function normalizeToSecretKey(str) {
19
+ if (isHexKey(str))
20
+ return hexToBytes(str);
21
+ else {
22
+ const decode = nip19.decode(str);
23
+ if (decode.type !== "nsec")
24
+ throw new Error(`Cant get secret key from ${decode.type}`);
25
+ return decode.data;
26
+ }
27
+ }
@@ -1,4 +1,5 @@
1
1
  import { MonoTypeOperatorFunction } from "rxjs";
2
2
  import { NostrEvent } from "nostr-tools";
3
- import { Database } from "../event-store/database.js";
4
- export declare function claimLatest(database: Database): MonoTypeOperatorFunction<NostrEvent | undefined>;
3
+ import { type Database } from "../event-store/database.js";
4
+ /** An operator that claims the latest event with the database */
5
+ export declare function claimLatest<T extends NostrEvent | undefined>(database: Database): MonoTypeOperatorFunction<T>;
@@ -1,4 +1,5 @@
1
1
  import { finalize, tap } from "rxjs";
2
+ /** An operator that claims the latest event with the database */
2
3
  export function claimLatest(database) {
3
4
  return (source) => {
4
5
  let latest = undefined;
@@ -12,7 +13,7 @@ export function claimLatest(database) {
12
13
  // update state
13
14
  latest = event;
14
15
  }), finalize(() => {
15
- // late claim
16
+ // remove latest claim
16
17
  if (latest)
17
18
  database.removeClaim(latest, source);
18
19
  }));
@@ -0,0 +1,3 @@
1
+ import { OperatorFunction } from "rxjs";
2
+ /** if a synchronous value is not emitted, default is emitted */
3
+ export declare function withImmediateValueOrDefault<Value, Default extends unknown = unknown>(defaultValue: Default): OperatorFunction<Value, Value | Default>;
@@ -0,0 +1,19 @@
1
+ import { Observable } from "rxjs";
2
+ /** if a synchronous value is not emitted, default is emitted */
3
+ export function withImmediateValueOrDefault(defaultValue) {
4
+ return (source) => new Observable((observer) => {
5
+ let seen = false;
6
+ const sub = source.subscribe({
7
+ next: (v) => {
8
+ seen = true;
9
+ observer.next(v);
10
+ },
11
+ error: (err) => observer.error(err),
12
+ complete: () => observer.complete(),
13
+ });
14
+ // if a value is not emitted sync, emit default
15
+ if (!seen)
16
+ observer.next(defaultValue);
17
+ return sub;
18
+ });
19
+ }
@@ -5,7 +5,7 @@ import { isHiddenTagsLocked } from "../helpers/hidden-tags.js";
5
5
  export function UserMuteQuery(pubkey) {
6
6
  return {
7
7
  key: pubkey,
8
- run: (store) => store.replaceable(kinds.Mutelist, pubkey).pipe(map((event) => event && getMutedThings(event))),
8
+ run: (event) => event.replaceable(kinds.Mutelist, pubkey).pipe(map((event) => event && getMutedThings(event))),
9
9
  };
10
10
  }
11
11
  export function UserHiddenMuteQuery(pubkey) {
@@ -7,7 +7,7 @@ export declare function MultipleEventsQuery(ids: string[]): Query<Record<string,
7
7
  /** Creates a Query returning the latest version of a replaceable event */
8
8
  export declare function ReplaceableQuery(kind: number, pubkey: string, d?: string): Query<NostrEvent | undefined>;
9
9
  /** Creates a Query that returns an array of sorted events matching the filters */
10
- export declare function TimelineQuery(filters: Filter | Filter[], keepOldVersions?: boolean): Query<NostrEvent[]>;
10
+ export declare function TimelineQuery(filters: Filter | Filter[], includeOldVersion?: boolean): Query<NostrEvent[]>;
11
11
  /** Creates a Query that returns a directory of events by their UID */
12
12
  export declare function ReplaceableSetQuery(pointers: {
13
13
  kind: number;
@@ -22,11 +22,11 @@ export function ReplaceableQuery(kind, pubkey, d) {
22
22
  };
23
23
  }
24
24
  /** Creates a Query that returns an array of sorted events matching the filters */
25
- export function TimelineQuery(filters, keepOldVersions) {
25
+ export function TimelineQuery(filters, includeOldVersion) {
26
26
  filters = Array.isArray(filters) ? filters : [filters];
27
27
  return {
28
- key: hash_sum(filters) + (keepOldVersions ? "-history" : ""),
29
- run: (events) => events.timeline(filters, keepOldVersions),
28
+ key: hash_sum(filters) + (includeOldVersion ? "-history" : ""),
29
+ run: (events) => events.timeline(filters, includeOldVersion),
30
30
  };
31
31
  }
32
32
  /** Creates a Query that returns a directory of events by their UID */
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,9 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
- import { EventStore } from "../event-store/event-store.js";
3
- import { QueryStore } from "./query-store.js";
4
- import { ProfileQuery } from "../queries/profile.js";
2
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
+ import { EventStore } from "../../event-store/event-store.js";
4
+ import { QueryStore } from "../query-store.js";
5
+ import { ProfileQuery } from "../../queries/profile.js";
6
+ import { SingleEventQuery } from "../../queries/simple.js";
5
7
  let eventStore;
6
8
  let queryStore;
7
9
  const event = {
@@ -17,6 +19,34 @@ beforeEach(() => {
17
19
  eventStore = new EventStore();
18
20
  queryStore = new QueryStore(eventStore);
19
21
  });
22
+ describe("createQuery", () => {
23
+ it("should emit synchronous value if it exists", () => {
24
+ let value = undefined;
25
+ eventStore.add(event);
26
+ queryStore.createQuery(ProfileQuery, event.pubkey).subscribe((v) => (value = v));
27
+ expect(value).not.toBe(undefined);
28
+ });
29
+ it("should not emit undefined if value exists", () => {
30
+ eventStore.add(event);
31
+ const spy = subscribeSpyTo(queryStore.createQuery(SingleEventQuery, event.id));
32
+ expect(spy.getValues()).toEqual([event]);
33
+ });
34
+ it("should emit synchronous undefined if value does not exists", () => {
35
+ let value = 0;
36
+ queryStore.createQuery(ProfileQuery, event.pubkey).subscribe((v) => {
37
+ value = v;
38
+ });
39
+ expect(value).not.toBe(0);
40
+ expect(value).toBe(undefined);
41
+ });
42
+ it("should share latest value", () => {
43
+ eventStore.add(event);
44
+ const spy = subscribeSpyTo(queryStore.createQuery(SingleEventQuery, event.id));
45
+ const spy2 = subscribeSpyTo(queryStore.createQuery(SingleEventQuery, event.id));
46
+ expect(spy.getValues()).toEqual([event]);
47
+ expect(spy2.getValues()).toEqual([event]);
48
+ });
49
+ });
20
50
  describe("executeQuery", () => {
21
51
  it("should resolve when value is already present", async () => {
22
52
  eventStore.add(event);
@@ -1,19 +1,19 @@
1
1
  import { Observable } from "rxjs";
2
2
  import { Filter, NostrEvent } from "nostr-tools";
3
3
  import type { AddressPointer, EventPointer } from "nostr-tools/nip19";
4
- import { EventStore } from "../event-store/event-store.js";
4
+ import { IEventStore } from "../event-store/interface.js";
5
5
  import * as Queries from "../queries/index.js";
6
6
  export type Query<T extends unknown> = {
7
7
  /** A unique key for this query. this is used to detect duplicate queries */
8
8
  key: string;
9
9
  /** The meat of the query, this should return an Observables that subscribes to the eventStore in some way */
10
- run: (events: EventStore, store: QueryStore) => Observable<T>;
10
+ run: (events: IEventStore, store: QueryStore) => Observable<T>;
11
11
  };
12
12
  export type QueryConstructor<T extends unknown, Args extends Array<any>> = (...args: Args) => Query<T>;
13
13
  export declare class QueryStore {
14
14
  static Queries: typeof Queries;
15
- store: EventStore;
16
- constructor(store: EventStore);
15
+ store: IEventStore;
16
+ constructor(store: IEventStore);
17
17
  /** A directory of all active queries */
18
18
  queries: Map<QueryConstructor<any, any[]>, Map<string, Observable<any>>>;
19
19
  /** How long a query should be kept "warm" while nothing is subscribed to it */
@@ -45,6 +45,8 @@ export declare class QueryStore {
45
45
  inboxes: string[];
46
46
  outboxes: string[];
47
47
  } | undefined>;
48
+ /** Creates a query for a users blossom servers */
49
+ blossomServers(pubkey: string): Observable<URL[] | undefined>;
48
50
  /** Creates a ThreadQuery */
49
51
  thread(root: string | EventPointer | AddressPointer): Observable<Queries.Thread | undefined>;
50
52
  }
@@ -1,7 +1,8 @@
1
- import { filter, finalize, ReplaySubject, share, startWith, timer } from "rxjs";
1
+ import { filter, finalize, ReplaySubject, share, timer } from "rxjs";
2
2
  import hash_sum from "hash-sum";
3
3
  import * as Queries from "../queries/index.js";
4
4
  import { getObservableValue } from "../observable/get-observable-value.js";
5
+ import { withImmediateValueOrDefault } from "../observable/with-immediate-value.js";
5
6
  export class QueryStore {
6
7
  static Queries = Queries;
7
8
  store;
@@ -32,11 +33,15 @@ export class QueryStore {
32
33
  .run(this.store, this)
33
34
  .pipe(
34
35
  // always emit undefined so the observable is sync
35
- startWith(undefined),
36
+ withImmediateValueOrDefault(undefined),
36
37
  // remove the observable when its subscribed
37
38
  finalize(cleanup),
38
39
  // only create a single observable for all components
39
- share({ connector: () => new ReplaySubject(1), resetOnComplete: () => timer(this.queryKeepWarmTimeout) }));
40
+ share({
41
+ connector: () => new ReplaySubject(1),
42
+ resetOnComplete: () => timer(this.queryKeepWarmTimeout),
43
+ resetOnRefCountZero: () => timer(this.queryKeepWarmTimeout),
44
+ }));
40
45
  // set debug fields
41
46
  Reflect.set(observable, "queryArgs", args);
42
47
  observables.set(key, observable);
@@ -80,6 +85,10 @@ export class QueryStore {
80
85
  mailboxes(pubkey) {
81
86
  return this.createQuery(Queries.MailboxesQuery, pubkey);
82
87
  }
88
+ /** Creates a query for a users blossom servers */
89
+ blossomServers(pubkey) {
90
+ return this.createQuery(Queries.UserBlossomServersQuery, pubkey);
91
+ }
83
92
  /** Creates a ThreadQuery */
84
93
  thread(root) {
85
94
  return this.createQuery(Queries.ThreadQuery, root);
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "keywords": [
9
- "nostr"
9
+ "nostr",
10
+ "applesauce"
10
11
  ],
11
12
  "author": "hzrd149",
12
13
  "license": "MIT",
13
14
  "files": [
14
- "dist",
15
- "applesauce"
15
+ "dist"
16
16
  ],
17
17
  "exports": {
18
18
  ".": {
@@ -62,6 +62,7 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
+ "@noble/hashes": "^1.7.1",
65
66
  "@scure/base": "^1.2.4",
66
67
  "debug": "^4.4.0",
67
68
  "fast-deep-equal": "^3.1.3",