applesauce-core 0.0.0-next-20250916134023 → 0.0.0-next-20250919114711

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 (181) hide show
  1. package/dist/event-store/async-event-store.d.ts +1 -1
  2. package/dist/event-store/async-event-store.js +8 -8
  3. package/dist/event-store/event-memory.d.ts +2 -2
  4. package/dist/event-store/event-memory.js +2 -2
  5. package/dist/event-store/event-store.d.ts +1 -1
  6. package/dist/event-store/event-store.js +7 -7
  7. package/dist/event-store/interface.d.ts +1 -1
  8. package/dist/event-store/model-mixin.d.ts +1 -1
  9. package/dist/helpers/index.d.ts +1 -0
  10. package/dist/helpers/index.js +1 -0
  11. package/dist/helpers/mailboxes.d.ts +2 -6
  12. package/dist/helpers/mailboxes.js +26 -20
  13. package/dist/helpers/pointers.js +2 -1
  14. package/dist/helpers/relay-selection.d.ts +13 -0
  15. package/dist/helpers/relay-selection.js +84 -0
  16. package/dist/helpers/url.js +3 -3
  17. package/dist/models/contacts.d.ts +1 -1
  18. package/dist/models/contacts.js +1 -1
  19. package/dist/models/index.d.ts +1 -0
  20. package/dist/models/index.js +1 -0
  21. package/dist/models/outbox.d.ts +13 -0
  22. package/dist/models/outbox.js +18 -0
  23. package/dist/observable/index.d.ts +3 -2
  24. package/dist/observable/index.js +4 -3
  25. package/dist/observable/map-events-to-store.d.ts +3 -3
  26. package/dist/observable/map-events-to-store.js +12 -3
  27. package/dist/observable/relay-selection.d.ts +7 -0
  28. package/dist/observable/relay-selection.js +38 -0
  29. package/package.json +3 -1
  30. package/dist/__tests__/exports.test.d.ts +0 -1
  31. package/dist/__tests__/exports.test.js +0 -27
  32. package/dist/__tests__/fixtures.d.ts +0 -17
  33. package/dist/__tests__/fixtures.js +0 -28
  34. package/dist/event-store/__tests__/event-store.test.d.ts +0 -1
  35. package/dist/event-store/__tests__/event-store.test.js +0 -386
  36. package/dist/event-store/common.d.ts +0 -1
  37. package/dist/event-store/common.js +0 -2
  38. package/dist/event-store/database.d.ts +0 -67
  39. package/dist/event-store/database.js +0 -316
  40. package/dist/event-store/event-database.d.ts +0 -74
  41. package/dist/event-store/event-database.js +0 -339
  42. package/dist/event-store/event-set.d.ts +0 -75
  43. package/dist/event-store/event-set.js +0 -341
  44. package/dist/event-store/event-store.test.d.ts +0 -1
  45. package/dist/event-store/event-store.test.js +0 -74
  46. package/dist/helpers/__tests__/app-handlers.test.d.ts +0 -1
  47. package/dist/helpers/__tests__/app-handlers.test.js +0 -184
  48. package/dist/helpers/__tests__/blossom.test.d.ts +0 -1
  49. package/dist/helpers/__tests__/blossom.test.js +0 -13
  50. package/dist/helpers/__tests__/bookmarks.test.d.ts +0 -1
  51. package/dist/helpers/__tests__/bookmarks.test.js +0 -88
  52. package/dist/helpers/__tests__/comment.test.d.ts +0 -1
  53. package/dist/helpers/__tests__/comment.test.js +0 -249
  54. package/dist/helpers/__tests__/contacts.test.d.ts +0 -1
  55. package/dist/helpers/__tests__/contacts.test.js +0 -34
  56. package/dist/helpers/__tests__/emoji.test.d.ts +0 -1
  57. package/dist/helpers/__tests__/emoji.test.js +0 -110
  58. package/dist/helpers/__tests__/encrypted-content-cache.test.d.ts +0 -1
  59. package/dist/helpers/__tests__/encrypted-content-cache.test.js +0 -65
  60. package/dist/helpers/__tests__/encryption.test.d.ts +0 -1
  61. package/dist/helpers/__tests__/encryption.test.js +0 -21
  62. package/dist/helpers/__tests__/event.test.d.ts +0 -1
  63. package/dist/helpers/__tests__/event.test.js +0 -36
  64. package/dist/helpers/__tests__/events.test.d.ts +0 -1
  65. package/dist/helpers/__tests__/events.test.js +0 -32
  66. package/dist/helpers/__tests__/exports.test.d.ts +0 -1
  67. package/dist/helpers/__tests__/exports.test.js +0 -293
  68. package/dist/helpers/__tests__/file-metadata.test.d.ts +0 -1
  69. package/dist/helpers/__tests__/file-metadata.test.js +0 -103
  70. package/dist/helpers/__tests__/groups.test.d.ts +0 -1
  71. package/dist/helpers/__tests__/groups.test.js +0 -61
  72. package/dist/helpers/__tests__/hidden-tags.test.d.ts +0 -1
  73. package/dist/helpers/__tests__/hidden-tags.test.js +0 -29
  74. package/dist/helpers/__tests__/mailboxes.test.d.ts +0 -1
  75. package/dist/helpers/__tests__/mailboxes.test.js +0 -81
  76. package/dist/helpers/__tests__/messages.test.d.ts +0 -1
  77. package/dist/helpers/__tests__/messages.test.js +0 -91
  78. package/dist/helpers/__tests__/mutes.test.d.ts +0 -1
  79. package/dist/helpers/__tests__/mutes.test.js +0 -55
  80. package/dist/helpers/__tests__/nip-19.test.d.ts +0 -1
  81. package/dist/helpers/__tests__/nip-19.test.js +0 -42
  82. package/dist/helpers/__tests__/pointers.test.d.ts +0 -1
  83. package/dist/helpers/__tests__/pointers.test.js +0 -118
  84. package/dist/helpers/__tests__/profile.test.d.ts +0 -1
  85. package/dist/helpers/__tests__/profile.test.js +0 -72
  86. package/dist/helpers/__tests__/reactions.test.d.ts +0 -1
  87. package/dist/helpers/__tests__/reactions.test.js +0 -88
  88. package/dist/helpers/__tests__/relays.test.d.ts +0 -1
  89. package/dist/helpers/__tests__/relays.test.js +0 -21
  90. package/dist/helpers/__tests__/tags.test.d.ts +0 -1
  91. package/dist/helpers/__tests__/tags.test.js +0 -24
  92. package/dist/helpers/__tests__/threading.test.d.ts +0 -1
  93. package/dist/helpers/__tests__/threading.test.js +0 -41
  94. package/dist/helpers/direct-messages.d.ts +0 -4
  95. package/dist/helpers/direct-messages.js +0 -5
  96. package/dist/helpers/file-metadata.test.d.ts +0 -1
  97. package/dist/helpers/file-metadata.test.js +0 -103
  98. package/dist/helpers/hidden-tags.test.d.ts +0 -1
  99. package/dist/helpers/hidden-tags.test.js +0 -29
  100. package/dist/helpers/legacy-direct-messages.d.ts +0 -8
  101. package/dist/helpers/legacy-direct-messages.js +0 -17
  102. package/dist/helpers/mailboxes.test.d.ts +0 -1
  103. package/dist/helpers/mailboxes.test.js +0 -81
  104. package/dist/helpers/nip-19.d.ts +0 -4
  105. package/dist/helpers/nip-19.js +0 -27
  106. package/dist/helpers/tags.test.d.ts +0 -1
  107. package/dist/helpers/tags.test.js +0 -16
  108. package/dist/helpers/threading.test.d.ts +0 -1
  109. package/dist/helpers/threading.test.js +0 -41
  110. package/dist/helpers/wrapped-direct-messages.d.ts +0 -6
  111. package/dist/helpers/wrapped-direct-messages.js +0 -11
  112. package/dist/models/__tests__/comments.test.d.ts +0 -1
  113. package/dist/models/__tests__/comments.test.js +0 -36
  114. package/dist/models/__tests__/exports.test.d.ts +0 -1
  115. package/dist/models/__tests__/exports.test.js +0 -53
  116. package/dist/observable/__tests__/claim-events.test.d.ts +0 -1
  117. package/dist/observable/__tests__/claim-events.test.js +0 -23
  118. package/dist/observable/__tests__/claim-latest.test.d.ts +0 -1
  119. package/dist/observable/__tests__/claim-latest.test.js +0 -37
  120. package/dist/observable/__tests__/exports.test.d.ts +0 -1
  121. package/dist/observable/__tests__/exports.test.js +0 -21
  122. package/dist/observable/__tests__/map-events-to-store.test.d.ts +0 -1
  123. package/dist/observable/__tests__/map-events-to-store.test.js +0 -38
  124. package/dist/observable/__tests__/simple-timeout.test.d.ts +0 -1
  125. package/dist/observable/__tests__/simple-timeout.test.js +0 -34
  126. package/dist/observable/__tests__/watch-event-updates.test.d.ts +0 -1
  127. package/dist/observable/__tests__/watch-event-updates.test.js +0 -55
  128. package/dist/observable/getValue.d.ts +0 -2
  129. package/dist/observable/getValue.js +0 -13
  130. package/dist/observable/map-events-timeline.d.ts +0 -7
  131. package/dist/observable/map-events-timeline.js +0 -9
  132. package/dist/observable/share-behavior.d.ts +0 -2
  133. package/dist/observable/share-behavior.js +0 -7
  134. package/dist/observable/share-latest-value.d.ts +0 -6
  135. package/dist/observable/share-latest-value.js +0 -24
  136. package/dist/observable/stateful.d.ts +0 -10
  137. package/dist/observable/stateful.js +0 -60
  138. package/dist/observable/throttle.d.ts +0 -3
  139. package/dist/observable/throttle.js +0 -23
  140. package/dist/promise/__tests__/exports.test.d.ts +0 -1
  141. package/dist/promise/__tests__/exports.test.js +0 -11
  142. package/dist/queries/blossom.d.ts +0 -2
  143. package/dist/queries/blossom.js +0 -10
  144. package/dist/queries/bookmarks.d.ts +0 -8
  145. package/dist/queries/bookmarks.js +0 -23
  146. package/dist/queries/channels.d.ts +0 -11
  147. package/dist/queries/channels.js +0 -73
  148. package/dist/queries/comments.d.ts +0 -4
  149. package/dist/queries/comments.js +0 -14
  150. package/dist/queries/contacts.d.ts +0 -3
  151. package/dist/queries/contacts.js +0 -12
  152. package/dist/queries/index.d.ts +0 -13
  153. package/dist/queries/index.js +0 -13
  154. package/dist/queries/mailboxes.d.ts +0 -6
  155. package/dist/queries/mailboxes.js +0 -13
  156. package/dist/queries/mutes.d.ts +0 -8
  157. package/dist/queries/mutes.js +0 -23
  158. package/dist/queries/pins.d.ts +0 -3
  159. package/dist/queries/pins.js +0 -12
  160. package/dist/queries/profile.d.ts +0 -4
  161. package/dist/queries/profile.js +0 -12
  162. package/dist/queries/reactions.d.ts +0 -4
  163. package/dist/queries/reactions.js +0 -19
  164. package/dist/queries/simple.d.ts +0 -16
  165. package/dist/queries/simple.js +0 -38
  166. package/dist/queries/thread.d.ts +0 -25
  167. package/dist/queries/thread.js +0 -92
  168. package/dist/queries/user-status.d.ts +0 -11
  169. package/dist/queries/user-status.js +0 -39
  170. package/dist/queries/zaps.d.ts +0 -5
  171. package/dist/queries/zaps.js +0 -21
  172. package/dist/query-store/__tests__/query-store.test.d.ts +0 -1
  173. package/dist/query-store/__tests__/query-store.test.js +0 -63
  174. package/dist/query-store/index.d.ts +0 -1
  175. package/dist/query-store/index.js +0 -1
  176. package/dist/query-store/query-store.d.ts +0 -53
  177. package/dist/query-store/query-store.js +0 -97
  178. package/dist/query-store/query-store.test.d.ts +0 -1
  179. package/dist/query-store/query-store.test.js +0 -33
  180. package/dist/utils/lru.d.ts +0 -32
  181. package/dist/utils/lru.js +0 -148
@@ -17,7 +17,7 @@ declare const AsyncEventStore_base: {
17
17
  addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
18
18
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
19
19
  profile(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
20
- contacts(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("nostr-tools/nip19").ProfilePointer[] | undefined>;
20
+ contacts(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("nostr-tools/nip19").ProfilePointer[]>;
21
21
  mutes(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
22
22
  mailboxes(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<{
23
23
  inboxes: string[];
@@ -214,7 +214,7 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
214
214
  // remove all old version of the replaceable event
215
215
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
216
216
  const existing = await this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
217
- if (existing) {
217
+ if (existing && existing.length > 0) {
218
218
  const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
219
219
  for (const old of older)
220
220
  await this.remove(old);
@@ -231,16 +231,16 @@ export class AsyncEventStore extends EventStoreModelMixin(class {
231
231
  }
232
232
  /** Removes an event from the store and updates subscriptions */
233
233
  async remove(event) {
234
- // Get the current instance from the database
235
- const e = await this.database.getEvent(typeof event === "string" ? event : event.id);
236
- if (!e)
237
- return false;
234
+ let instance = this.memory?.getEvent(typeof event === "string" ? event : event.id);
238
235
  // Remove from memory if available
239
236
  if (this.memory)
240
- this.memory.remove(typeof event === "string" ? event : event.id);
237
+ this.memory.remove(event);
238
+ // Remove the event from the database
241
239
  const removed = await this.database.remove(event);
242
- if (removed && e)
243
- this.remove$.next(e);
240
+ // If the event was removed, notify the subscriptions
241
+ if (removed && instance) {
242
+ this.remove$.next(instance);
243
+ }
244
244
  return removed;
245
245
  }
246
246
  /** Add an event to the store and notifies all subscribes it has updated */
@@ -66,9 +66,9 @@ export declare class EventMemory implements IEventMemory {
66
66
  /** Iterates over all events by id */
67
67
  iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
68
68
  /** Returns all events that match the filter */
69
- getEventsForFilter(filter: Filter): Set<NostrEvent>;
69
+ protected getEventsForFilter(filter: Filter): Set<NostrEvent>;
70
70
  /** Returns all events that match the filters */
71
- getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
71
+ protected getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
72
72
  /** Resets the event set */
73
73
  reset(): void;
74
74
  }
@@ -90,7 +90,7 @@ export class EventMemory {
90
90
  remove(eventOrId) {
91
91
  let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
92
92
  if (!event)
93
- throw new Error("Missing event");
93
+ return false;
94
94
  const id = event.id;
95
95
  // only remove events that are known
96
96
  if (!this.events.has(id))
@@ -327,7 +327,7 @@ export class EventMemory {
327
327
  /** Returns all events that match the filters */
328
328
  getEventsForFilters(filters) {
329
329
  if (filters.length === 0)
330
- throw new Error("No Filters");
330
+ return new Set();
331
331
  let events = new Set();
332
332
  for (const filter of filters) {
333
333
  const filtered = this.getEventsForFilter(filter);
@@ -17,7 +17,7 @@ declare const EventStore_base: {
17
17
  addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
18
18
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
19
19
  profile(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
20
- contacts(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("nostr-tools/nip19").ProfilePointer[] | undefined>;
20
+ contacts(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("nostr-tools/nip19").ProfilePointer[]>;
21
21
  mutes(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
22
22
  mailboxes(user: string | import("nostr-tools/nip19").ProfilePointer): Observable<{
23
23
  inboxes: string[];
@@ -218,7 +218,7 @@ export class EventStore extends EventStoreModelMixin(class {
218
218
  // remove all old version of the replaceable event
219
219
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
220
220
  const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
221
- if (existing) {
221
+ if (existing && existing.length > 0) {
222
222
  const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
223
223
  for (const old of older)
224
224
  this.remove(old);
@@ -235,16 +235,16 @@ export class EventStore extends EventStoreModelMixin(class {
235
235
  }
236
236
  /** Removes an event from the store and updates subscriptions */
237
237
  remove(event) {
238
- // Get the current instance from the database
239
- const e = this.database.getEvent(typeof event === "string" ? event : event.id);
240
- if (!e)
241
- return false;
238
+ let instance = this.memory?.getEvent(typeof event === "string" ? event : event.id);
242
239
  // Remove from memory if available
243
240
  if (this.memory)
244
241
  this.memory.remove(event);
242
+ // Remove the event from the database
245
243
  const removed = this.database.remove(event);
246
- if (removed && e)
247
- this.remove$.next(e);
244
+ // If the event was removed, notify the subscriptions
245
+ if (removed && instance) {
246
+ this.remove$.next(instance);
247
+ }
248
248
  return removed;
249
249
  }
250
250
  /** Add an event to the store and notifies all subscribes it has updated */
@@ -112,7 +112,7 @@ export interface IEventHelpfulSubscriptions {
112
112
  /** Subscribe to a users profile */
113
113
  profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
114
114
  /** Subscribe to a users contacts */
115
- contacts(user: string | ProfilePointer): Observable<ProfilePointer[] | undefined>;
115
+ contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
116
116
  /** Subscribe to a users mutes */
117
117
  mutes(user: string | ProfilePointer): Observable<Mutes | undefined>;
118
118
  /** Subscribe to a users NIP-65 mailboxes */
@@ -31,7 +31,7 @@ export declare function EventStoreModelMixin<T extends new (...args: any[]) => a
31
31
  /** Subscribe to a users profile */
32
32
  profile(user: string | ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
33
33
  /** Subscribe to a users contacts */
34
- contacts(user: string | ProfilePointer): Observable<ProfilePointer[] | undefined>;
34
+ contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
35
35
  /** Subscribe to a users mutes */
36
36
  mutes(user: string | ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
37
37
  /** Subscribe to a users NIP-65 mailboxes */
@@ -43,6 +43,7 @@ export * from "./pointers.js";
43
43
  export * from "./poll.js";
44
44
  export * from "./profile.js";
45
45
  export * from "./reactions.js";
46
+ export * from "./relay-selection.js";
46
47
  export * from "./relays.js";
47
48
  export * from "./reports.js";
48
49
  export * from "./share.js";
@@ -43,6 +43,7 @@ export * from "./pointers.js";
43
43
  export * from "./poll.js";
44
44
  export * from "./profile.js";
45
45
  export * from "./reactions.js";
46
+ export * from "./relay-selection.js";
46
47
  export * from "./relays.js";
47
48
  export * from "./reports.js";
48
49
  export * from "./share.js";
@@ -1,11 +1,7 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
2
  export declare const MailboxesInboxesSymbol: unique symbol;
3
3
  export declare const MailboxesOutboxesSymbol: unique symbol;
4
- /**
5
- * Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxesSymbol} symbol
6
- */
4
+ /** Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxesSymbol} symbol */
7
5
  export declare function getInboxes(event: NostrEvent): string[];
8
- /**
9
- * Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxesSymbol} symbol
10
- */
6
+ /** Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxesSymbol} symbol */
11
7
  export declare function getOutboxes(event: NostrEvent): string[];
@@ -1,41 +1,47 @@
1
1
  import { getOrComputeCachedValue } from "./cache.js";
2
2
  import { isSafeRelayURL } from "./relays.js";
3
+ import { isRTag } from "./tags.js";
3
4
  import { normalizeURL } from "./url.js";
4
5
  export const MailboxesInboxesSymbol = Symbol.for("mailboxes-inboxes");
5
6
  export const MailboxesOutboxesSymbol = Symbol.for("mailboxes-outboxes");
6
- /**
7
- * Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxesSymbol} symbol
8
- */
7
+ /** Parses a 10002 event and stores the inboxes in the event using the {@link MailboxesInboxesSymbol} symbol */
9
8
  export function getInboxes(event) {
10
9
  return getOrComputeCachedValue(event, MailboxesInboxesSymbol, () => {
11
10
  const inboxes = [];
12
11
  for (const tag of event.tags) {
13
- const [name, url, mode] = tag;
14
- if (name === "r" &&
15
- url &&
16
- isSafeRelayURL(url) &&
17
- !inboxes.includes(url) &&
18
- (mode === "read" || mode === undefined)) {
19
- inboxes.push(normalizeURL(url));
12
+ if (!isRTag(tag))
13
+ continue;
14
+ try {
15
+ const [, url, mode] = tag;
16
+ if (url && isSafeRelayURL(url) && !inboxes.includes(url) && (mode === "read" || mode === undefined)) {
17
+ inboxes.push(normalizeURL(url));
18
+ }
19
+ }
20
+ catch {
21
+ // Ignore invalid url tags
20
22
  }
21
23
  }
22
24
  return inboxes;
23
25
  });
24
26
  }
25
- /**
26
- * Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxesSymbol} symbol
27
- */
27
+ /** Parses a 10002 event and stores the outboxes in the event using the {@link MailboxesOutboxesSymbol} symbol */
28
28
  export function getOutboxes(event) {
29
29
  return getOrComputeCachedValue(event, MailboxesOutboxesSymbol, () => {
30
30
  const outboxes = [];
31
31
  for (const tag of event.tags) {
32
- const [name, url, mode] = tag;
33
- if (name === "r" &&
34
- url &&
35
- isSafeRelayURL(url) &&
36
- !outboxes.includes(url) &&
37
- (mode === "write" || mode === undefined)) {
38
- outboxes.push(normalizeURL(url));
32
+ if (!isRTag(tag))
33
+ continue;
34
+ try {
35
+ const [name, url, mode] = tag;
36
+ if (name === "r" &&
37
+ isSafeRelayURL(url) &&
38
+ !outboxes.includes(url) &&
39
+ (mode === "write" || mode === undefined)) {
40
+ outboxes.push(normalizeURL(url));
41
+ }
42
+ }
43
+ catch {
44
+ // Ignore invalid url tags
39
45
  }
40
46
  }
41
47
  return outboxes;
@@ -5,6 +5,7 @@ import { isAddressableKind } from "nostr-tools/kinds";
5
5
  import { isSafeRelayURL, mergeRelaySets } from "./relays.js";
6
6
  import { isHexKey } from "./string.js";
7
7
  import { hexToBytes } from "@noble/hashes/utils";
8
+ import { normalizeURL } from "./url.js";
8
9
  export function parseCoordinate(a, requireD = false, silent = true) {
9
10
  const parts = a.split(":");
10
11
  const kind = parts[0] ? parseInt(parts[0]) : undefined;
@@ -117,7 +118,7 @@ export function getProfilePointerFromPTag(tag) {
117
118
  throw new Error("Invalid pubkey");
118
119
  const pointer = { pubkey: tag[1] };
119
120
  if (tag[2] && isSafeRelayURL(tag[2]))
120
- pointer.relays = [tag[2]];
121
+ pointer.relays = [normalizeURL(tag[2])];
121
122
  return pointer;
122
123
  }
123
124
  /** Checks if a pointer is an AddressPointer */
@@ -0,0 +1,13 @@
1
+ import { ProfilePointer } from "nostr-tools/nip19";
2
+ export type SelectOptimalRelaysOptions = {
3
+ /** Maximum number of connections (relays) to select */
4
+ maxConnections: number;
5
+ /** Cap the number of relays a user can have */
6
+ maxRelaysPerUser?: number;
7
+ };
8
+ /** Selects the optimal relays for a list of ProfilePointers */
9
+ export declare function selectOptimalRelays(users: ProfilePointer[], { maxConnections, maxRelaysPerUser }: SelectOptimalRelaysOptions): ProfilePointer[];
10
+ /** A map of pubkeys by relay */
11
+ export type OutboxMap = Record<string, ProfilePointer[]>;
12
+ /** RxJS operator that aggregates contacts with outboxes into a relay -> pubkeys map */
13
+ export declare function groupPubkeysByRelay(pointers: ProfilePointer[]): OutboxMap;
@@ -0,0 +1,84 @@
1
+ /** Selects the optimal relays for a list of ProfilePointers */
2
+ export function selectOptimalRelays(users, { maxConnections, maxRelaysPerUser }) {
3
+ const usersWithRelays = users.filter((user) => user.relays && user.relays.length > 0);
4
+ // create map of popular relays
5
+ const popular = new Map();
6
+ for (const user of usersWithRelays) {
7
+ if (!user.relays)
8
+ continue;
9
+ for (const relay of user.relays)
10
+ popular.set(relay, (popular.get(relay) || 0) + 1);
11
+ }
12
+ // sort users relays by popularity
13
+ for (const user of usersWithRelays) {
14
+ if (!user.relays)
15
+ continue;
16
+ user.relays = Array.from(user.relays).sort((a, b) => popular.get(b) - popular.get(a));
17
+ }
18
+ // Create a pool of users to calculate relay coverage from
19
+ let selectionPool = Array.from(usersWithRelays);
20
+ // Create map of times a users relay has been selected
21
+ const selectionCount = new Map();
22
+ let selection = new Set();
23
+ while (selectionPool.length > 0 && selection.size < maxConnections) {
24
+ // Create map of number of pool users per relay
25
+ const relayUserCount = new Map();
26
+ for (const user of selectionPool) {
27
+ if (!user.relays)
28
+ continue;
29
+ for (const relay of user.relays) {
30
+ // Skip relays that are already selected
31
+ if (selection.has(relay))
32
+ continue;
33
+ // Increment relay user count
34
+ relayUserCount.set(relay, (relayUserCount.get(relay) || 0) + 1);
35
+ }
36
+ }
37
+ // Sort relays by coverage
38
+ const byCoverage = Array.from(relayUserCount.entries()).sort((a, b) => b[1] - a[1]);
39
+ // No more relays to select, exit loop
40
+ if (byCoverage.length === 0)
41
+ break;
42
+ // Pick the most popular relay
43
+ const relay = byCoverage[0][0];
44
+ // Add relay to selection
45
+ selection.add(relay);
46
+ // Increment user relay count and remove users over the limit
47
+ selectionPool = selectionPool.filter((user) => {
48
+ // Ignore users that don't have the relay
49
+ if (!user.relays || !user.relays.includes(relay))
50
+ return true;
51
+ // Increment user relay count
52
+ let count = selectionCount.get(relay) || 0;
53
+ selectionCount.set(relay, count++);
54
+ // Remove user if they their relay has been selected more than minRelaysPerUser times
55
+ if (count >= 1)
56
+ return false;
57
+ return true;
58
+ });
59
+ }
60
+ // Take the original users and only include relays that where selected
61
+ return users.map((user) => ({
62
+ ...user,
63
+ relays: maxRelaysPerUser
64
+ ? user.relays
65
+ ?.filter((relay) => selection.has(relay))
66
+ .sort((a, b) => (popular.get(a) ?? 0) - (popular.get(b) ?? 0))
67
+ .slice(0, maxRelaysPerUser)
68
+ : user.relays?.filter((relay) => selection.has(relay)),
69
+ }));
70
+ }
71
+ /** RxJS operator that aggregates contacts with outboxes into a relay -> pubkeys map */
72
+ export function groupPubkeysByRelay(pointers) {
73
+ const outbox = {};
74
+ for (const pointer of pointers) {
75
+ if (!pointer.relays)
76
+ continue;
77
+ for (const relay of pointer.relays) {
78
+ if (!outbox[relay])
79
+ outbox[relay] = [];
80
+ outbox[relay].push(pointer);
81
+ }
82
+ }
83
+ return outbox;
84
+ }
@@ -78,13 +78,13 @@ export function ensureHttpURL(url) {
78
78
  */
79
79
  export function normalizeURL(url) {
80
80
  let p = new URL(url);
81
- // remove any double slashes
81
+ // Remove any double slashes
82
82
  p.pathname = p.pathname.replace(/\/+/g, "/");
83
- // drop the port if its not needed
83
+ // Remove the port if its not needed
84
84
  if ((p.port === "80" && (p.protocol === "ws:" || p.protocol === "http:")) ||
85
85
  (p.port === "443" && (p.protocol === "wss:" || p.protocol === "https:")))
86
86
  p.port = "";
87
- // return a string if a string was passed in
87
+ // Return a string if a string was passed in
88
88
  // @ts-expect-error
89
89
  return typeof url === "string" ? p.toString() : p;
90
90
  }
@@ -1,7 +1,7 @@
1
1
  import { ProfilePointer } from "nostr-tools/nip19";
2
2
  import { Model } from "../event-store/interface.js";
3
3
  /** A model that returns all contacts for a user */
4
- export declare function ContactsModel(user: string | ProfilePointer): Model<ProfilePointer[] | undefined>;
4
+ export declare function ContactsModel(user: string | ProfilePointer): Model<ProfilePointer[]>;
5
5
  /** A model that returns all public contacts for a user */
6
6
  export declare function PublicContactsModel(pubkey: string): Model<ProfilePointer[] | undefined>;
7
7
  /** A model that returns all hidden contacts for a user */
@@ -10,7 +10,7 @@ export function ContactsModel(user) {
10
10
  // listen for event updates (hidden tags unlocked)
11
11
  watchEventUpdates(events),
12
12
  // Get all contacts
13
- map((e) => (e ? getContacts(e) : undefined)));
13
+ map((e) => (e ? getContacts(e) : [])));
14
14
  }
15
15
  /** A model that returns all public contacts for a user */
16
16
  export function PublicContactsModel(pubkey) {
@@ -17,3 +17,4 @@ export * from "./relays.js";
17
17
  export * from "./thread.js";
18
18
  export * from "./wrapped-messages.js";
19
19
  export * from "./zaps.js";
20
+ export * from "./outbox.js";
@@ -17,3 +17,4 @@ export * from "./relays.js";
17
17
  export * from "./thread.js";
18
18
  export * from "./wrapped-messages.js";
19
19
  export * from "./zaps.js";
20
+ export * from "./outbox.js";
@@ -0,0 +1,13 @@
1
+ import { ProfilePointer } from "nostr-tools/nip19";
2
+ import { Model } from "../event-store/interface.js";
3
+ import { SelectOptimalRelaysOptions } from "../helpers/relay-selection.js";
4
+ import { ignoreBlacklistedRelays } from "../observable/relay-selection.js";
5
+ export type OutboxModelOptions = SelectOptimalRelaysOptions & {
6
+ type?: "inbox" | "outbox";
7
+ blacklist?: Parameters<typeof ignoreBlacklistedRelays>[0];
8
+ };
9
+ /** A model that returns the users contacts with the relays to connect to */
10
+ export declare function OutboxModel(user: string | ProfilePointer, opts: OutboxModelOptions): Model<ProfilePointer[]>;
11
+ export declare namespace OutboxModel {
12
+ var getKey: (user: string | ProfilePointer, opts: OutboxModelOptions) => string;
13
+ }
@@ -0,0 +1,18 @@
1
+ import hash_sum from "hash-sum";
2
+ import { identity, map } from "rxjs";
3
+ import { selectOptimalRelays } from "../helpers/relay-selection.js";
4
+ import { ignoreBlacklistedRelays, includeMailboxes } from "../observable/relay-selection.js";
5
+ /** A model that returns the users contacts with the relays to connect to */
6
+ export function OutboxModel(user, opts) {
7
+ return (store) => store.contacts(user).pipe(
8
+ /** Ignore blacklisted relays */
9
+ opts?.blacklist ? ignoreBlacklistedRelays(opts.blacklist) : identity,
10
+ /** Include mailboxes */
11
+ includeMailboxes(store, opts.type),
12
+ /** Select the optimal relays */
13
+ map((users) => selectOptimalRelays(users, opts)));
14
+ }
15
+ OutboxModel.getKey = (user, opts) => {
16
+ const p = typeof user === "string" ? user : user.pubkey;
17
+ return hash_sum([p, opts.type, opts.maxConnections, opts.maxRelaysPerUser]);
18
+ };
@@ -1,4 +1,5 @@
1
- import { firstValueFrom, lastValueFrom } from "rxjs";
1
+ export { firstValueFrom, lastValueFrom, combineLatest, merge } from "rxjs";
2
+ export { Observable, Subject, BehaviorSubject, ReplaySubject } from "rxjs";
2
3
  export * from "./defined.js";
3
4
  export * from "./get-observable-value.js";
4
5
  export * from "./map-events-to-timeline.js";
@@ -6,4 +7,4 @@ export * from "./map-events-to-store.js";
6
7
  export * from "./simple-timeout.js";
7
8
  export * from "./watch-event-updates.js";
8
9
  export * from "./with-immediate-value.js";
9
- export { firstValueFrom, lastValueFrom };
10
+ export * from "./relay-selection.js";
@@ -1,4 +1,6 @@
1
- import { firstValueFrom, lastValueFrom } from "rxjs";
1
+ // Re-export some useful rxjs functions
2
+ export { firstValueFrom, lastValueFrom, combineLatest, merge } from "rxjs";
3
+ export { Observable, Subject, BehaviorSubject, ReplaySubject } from "rxjs";
2
4
  export * from "./defined.js";
3
5
  export * from "./get-observable-value.js";
4
6
  export * from "./map-events-to-timeline.js";
@@ -6,5 +8,4 @@ export * from "./map-events-to-store.js";
6
8
  export * from "./simple-timeout.js";
7
9
  export * from "./watch-event-updates.js";
8
10
  export * from "./with-immediate-value.js";
9
- // Re-export some useful rxjs functions
10
- export { firstValueFrom, lastValueFrom };
11
+ export * from "./relay-selection.js";
@@ -1,7 +1,7 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
2
  import { MonoTypeOperatorFunction } from "rxjs";
3
- import { IEventStoreActions } from "../event-store/interface.js";
3
+ import { IAsyncEventStoreActions, IEventStoreActions } from "../event-store/interface.js";
4
4
  /** Saves all events to an event store and filters out invalid events */
5
- export declare function mapEventsToStore(store: IEventStoreActions, removeDuplicates?: boolean): MonoTypeOperatorFunction<NostrEvent>;
5
+ export declare function mapEventsToStore(store: IEventStoreActions | IAsyncEventStoreActions, removeDuplicates?: boolean): MonoTypeOperatorFunction<NostrEvent>;
6
6
  /** Alias for {@link mapEventsToStore} */
7
- export declare const filterDuplicateEvents: (store: IEventStoreActions) => MonoTypeOperatorFunction<import("nostr-tools").Event>;
7
+ export declare const filterDuplicateEvents: (store: IEventStoreActions | IAsyncEventStoreActions) => MonoTypeOperatorFunction<import("nostr-tools").Event>;
@@ -1,10 +1,19 @@
1
- import { distinct, filter, identity, map } from "rxjs";
1
+ import { catchError, distinct, filter, from, identity, mergeMap, of } from "rxjs";
2
2
  /** Saves all events to an event store and filters out invalid events */
3
3
  export function mapEventsToStore(store, removeDuplicates = true) {
4
4
  return (source) => source.pipe(
5
5
  // Map all events to the store
6
- // NOTE: map is used here because we want to return the single instance of the event so that distinct() can be used later
7
- map((event) => store.add(event)),
6
+ // NOTE: mergeMap is used here because we want to return the single instance of the event so that distinct() can be used later
7
+ mergeMap((event) => {
8
+ const r = store.add(event);
9
+ // Unwrap the promise from the async store
10
+ if (r instanceof Promise)
11
+ return from(r);
12
+ else
13
+ return of(r);
14
+ }),
15
+ // Ignore errors when inserting events into the store
16
+ catchError(() => of(null)),
8
17
  // Ignore invalid events
9
18
  filter((e) => e !== null),
10
19
  // Remove duplicates if requested
@@ -0,0 +1,7 @@
1
+ import { ProfilePointer } from "nostr-tools/nip19";
2
+ import { type MonoTypeOperatorFunction, type Observable, type OperatorFunction } from "rxjs";
3
+ import { IEventSubscriptions } from "../event-store/interface.js";
4
+ /** RxJS operator that fetches outboxes for profile pointers from the event store */
5
+ export declare function includeMailboxes(store: IEventSubscriptions, type?: "inbox" | "outbox"): OperatorFunction<ProfilePointer[], ProfilePointer[]>;
6
+ /** Removes blacklisted relays from the user's relays */
7
+ export declare function ignoreBlacklistedRelays(blacklist: string[] | Observable<string[]>): MonoTypeOperatorFunction<ProfilePointer[]>;
@@ -0,0 +1,38 @@
1
+ import { combineLatest, combineLatestWith, isObservable, map, of, pipe, switchMap, } from "rxjs";
2
+ import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
3
+ import { addRelayHintsToPointer } from "../helpers/pointers.js";
4
+ /** RxJS operator that fetches outboxes for profile pointers from the event store */
5
+ export function includeMailboxes(store, type = "outbox") {
6
+ // Get the outboxes for all contacts
7
+ return switchMap((contacts) => combineLatest(contacts.map((user) =>
8
+ // Subscribe to the outboxes for the user
9
+ store
10
+ .replaceable({
11
+ kind: 10002,
12
+ pubkey: user.pubkey,
13
+ })
14
+ .pipe(
15
+ // Add the relays to the user
16
+ map((event) => {
17
+ if (!event)
18
+ return user;
19
+ // Get the relays from the event
20
+ const relays = type === "outbox" ? getOutboxes(event) : getInboxes(event);
21
+ if (!relays)
22
+ return user;
23
+ // Add the relays to the user
24
+ return addRelayHintsToPointer(user, relays);
25
+ })))));
26
+ }
27
+ /** Removes blacklisted relays from the user's relays */
28
+ export function ignoreBlacklistedRelays(blacklist) {
29
+ return pipe(
30
+ // Combine with the observable so it re-emits when the blacklist changes
31
+ combineLatestWith(isObservable(blacklist) ? blacklist : of(blacklist)),
32
+ // Filter the relays for the user
33
+ map(([users, blacklist]) => users.map((user) => {
34
+ if (!user.relays)
35
+ return user;
36
+ return { ...user, relays: user.relays.filter((relay) => !blacklist.includes(relay)) };
37
+ })));
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20250916134023",
3
+ "version": "0.0.0-next-20250919114711",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -71,6 +71,7 @@
71
71
  "@hirez_io/observer-spy": "^2.2.0",
72
72
  "@types/debug": "^4.1.12",
73
73
  "@types/hash-sum": "^1.0.2",
74
+ "rimraf": "^6.0.1",
74
75
  "typescript": "^5.8.3",
75
76
  "vitest": "^3.2.4"
76
77
  },
@@ -79,6 +80,7 @@
79
80
  "url": "lightning:nostrudel@geyser.fund"
80
81
  },
81
82
  "scripts": {
83
+ "prebuild": "rimraf dist",
82
84
  "build": "tsc",
83
85
  "watch:build": "tsc --watch > /dev/null",
84
86
  "test": "vitest run --passWithNoTests",
@@ -1 +0,0 @@
1
- export {};
@@ -1,27 +0,0 @@
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
- "EventSet",
8
- "EventStore",
9
- "EventStoreSymbol",
10
- "Helpers",
11
- "Models",
12
- "TimeoutError",
13
- "defined",
14
- "firstValueFrom",
15
- "getObservableValue",
16
- "lastValueFrom",
17
- "logger",
18
- "mapEventsToStore",
19
- "mapEventsToTimeline",
20
- "simpleTimeout",
21
- "watchEventUpdates",
22
- "watchEventsUpdates",
23
- "withImmediateValueOrDefault",
24
- ]
25
- `);
26
- });
27
- });
@@ -1,17 +0,0 @@
1
- import type { NostrEvent } from "nostr-tools";
2
- import { EncryptedContentSigner } from "../helpers/encrypted-content.js";
3
- export declare class FakeUser implements EncryptedContentSigner {
4
- key: Uint8Array<ArrayBufferLike>;
5
- pubkey: string;
6
- nip04: {
7
- encrypt: (pubkey: string, plaintext: string) => string;
8
- decrypt: (pubkey: string, ciphertext: string) => string;
9
- };
10
- nip44: {
11
- encrypt: (pubkey: string, plaintext: string) => string;
12
- decrypt: (pubkey: string, ciphertext: string) => string;
13
- };
14
- event(data?: Partial<NostrEvent>): NostrEvent;
15
- note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
16
- profile(profile: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
17
- }