applesauce-core 0.0.0-next-20250918142212 → 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 (163) hide show
  1. package/dist/helpers/mailboxes.d.ts +2 -6
  2. package/dist/helpers/mailboxes.js +26 -20
  3. package/dist/helpers/pointers.js +2 -1
  4. package/dist/helpers/relay-selection.d.ts +3 -9
  5. package/dist/helpers/relay-selection.js +63 -104
  6. package/dist/helpers/url.js +3 -3
  7. package/dist/models/outbox.d.ts +1 -1
  8. package/dist/models/outbox.js +4 -15
  9. package/dist/observable/relay-selection.d.ts +0 -2
  10. package/dist/observable/relay-selection.js +13 -55
  11. package/package.json +3 -1
  12. package/dist/__tests__/exports.test.d.ts +0 -1
  13. package/dist/__tests__/exports.test.js +0 -27
  14. package/dist/__tests__/fixtures.d.ts +0 -17
  15. package/dist/__tests__/fixtures.js +0 -28
  16. package/dist/event-store/__tests__/event-store.test.d.ts +0 -1
  17. package/dist/event-store/__tests__/event-store.test.js +0 -386
  18. package/dist/event-store/common.d.ts +0 -1
  19. package/dist/event-store/common.js +0 -2
  20. package/dist/event-store/database.d.ts +0 -67
  21. package/dist/event-store/database.js +0 -316
  22. package/dist/event-store/event-database.d.ts +0 -74
  23. package/dist/event-store/event-database.js +0 -339
  24. package/dist/event-store/event-set.d.ts +0 -75
  25. package/dist/event-store/event-set.js +0 -341
  26. package/dist/event-store/event-store.test.d.ts +0 -1
  27. package/dist/event-store/event-store.test.js +0 -74
  28. package/dist/helpers/__tests__/app-handlers.test.d.ts +0 -1
  29. package/dist/helpers/__tests__/app-handlers.test.js +0 -184
  30. package/dist/helpers/__tests__/blossom.test.d.ts +0 -1
  31. package/dist/helpers/__tests__/blossom.test.js +0 -13
  32. package/dist/helpers/__tests__/bookmarks.test.d.ts +0 -1
  33. package/dist/helpers/__tests__/bookmarks.test.js +0 -88
  34. package/dist/helpers/__tests__/comment.test.d.ts +0 -1
  35. package/dist/helpers/__tests__/comment.test.js +0 -249
  36. package/dist/helpers/__tests__/contacts.test.d.ts +0 -1
  37. package/dist/helpers/__tests__/contacts.test.js +0 -34
  38. package/dist/helpers/__tests__/emoji.test.d.ts +0 -1
  39. package/dist/helpers/__tests__/emoji.test.js +0 -110
  40. package/dist/helpers/__tests__/encrypted-content-cache.test.d.ts +0 -1
  41. package/dist/helpers/__tests__/encrypted-content-cache.test.js +0 -65
  42. package/dist/helpers/__tests__/encryption.test.d.ts +0 -1
  43. package/dist/helpers/__tests__/encryption.test.js +0 -21
  44. package/dist/helpers/__tests__/event.test.d.ts +0 -1
  45. package/dist/helpers/__tests__/event.test.js +0 -36
  46. package/dist/helpers/__tests__/events.test.d.ts +0 -1
  47. package/dist/helpers/__tests__/events.test.js +0 -32
  48. package/dist/helpers/__tests__/exports.test.d.ts +0 -1
  49. package/dist/helpers/__tests__/exports.test.js +0 -293
  50. package/dist/helpers/__tests__/file-metadata.test.d.ts +0 -1
  51. package/dist/helpers/__tests__/file-metadata.test.js +0 -103
  52. package/dist/helpers/__tests__/groups.test.d.ts +0 -1
  53. package/dist/helpers/__tests__/groups.test.js +0 -61
  54. package/dist/helpers/__tests__/hidden-tags.test.d.ts +0 -1
  55. package/dist/helpers/__tests__/hidden-tags.test.js +0 -29
  56. package/dist/helpers/__tests__/mailboxes.test.d.ts +0 -1
  57. package/dist/helpers/__tests__/mailboxes.test.js +0 -81
  58. package/dist/helpers/__tests__/messages.test.d.ts +0 -1
  59. package/dist/helpers/__tests__/messages.test.js +0 -91
  60. package/dist/helpers/__tests__/mutes.test.d.ts +0 -1
  61. package/dist/helpers/__tests__/mutes.test.js +0 -55
  62. package/dist/helpers/__tests__/nip-19.test.d.ts +0 -1
  63. package/dist/helpers/__tests__/nip-19.test.js +0 -42
  64. package/dist/helpers/__tests__/pointers.test.d.ts +0 -1
  65. package/dist/helpers/__tests__/pointers.test.js +0 -118
  66. package/dist/helpers/__tests__/profile.test.d.ts +0 -1
  67. package/dist/helpers/__tests__/profile.test.js +0 -72
  68. package/dist/helpers/__tests__/reactions.test.d.ts +0 -1
  69. package/dist/helpers/__tests__/reactions.test.js +0 -88
  70. package/dist/helpers/__tests__/relays.test.d.ts +0 -1
  71. package/dist/helpers/__tests__/relays.test.js +0 -21
  72. package/dist/helpers/__tests__/tags.test.d.ts +0 -1
  73. package/dist/helpers/__tests__/tags.test.js +0 -24
  74. package/dist/helpers/__tests__/threading.test.d.ts +0 -1
  75. package/dist/helpers/__tests__/threading.test.js +0 -41
  76. package/dist/helpers/direct-messages.d.ts +0 -4
  77. package/dist/helpers/direct-messages.js +0 -5
  78. package/dist/helpers/file-metadata.test.d.ts +0 -1
  79. package/dist/helpers/file-metadata.test.js +0 -103
  80. package/dist/helpers/hidden-tags.test.d.ts +0 -1
  81. package/dist/helpers/hidden-tags.test.js +0 -29
  82. package/dist/helpers/legacy-direct-messages.d.ts +0 -8
  83. package/dist/helpers/legacy-direct-messages.js +0 -17
  84. package/dist/helpers/mailboxes.test.d.ts +0 -1
  85. package/dist/helpers/mailboxes.test.js +0 -81
  86. package/dist/helpers/nip-19.d.ts +0 -4
  87. package/dist/helpers/nip-19.js +0 -27
  88. package/dist/helpers/tags.test.d.ts +0 -1
  89. package/dist/helpers/tags.test.js +0 -16
  90. package/dist/helpers/threading.test.d.ts +0 -1
  91. package/dist/helpers/threading.test.js +0 -41
  92. package/dist/helpers/wrapped-direct-messages.d.ts +0 -6
  93. package/dist/helpers/wrapped-direct-messages.js +0 -11
  94. package/dist/models/__tests__/comments.test.d.ts +0 -1
  95. package/dist/models/__tests__/comments.test.js +0 -36
  96. package/dist/models/__tests__/exports.test.d.ts +0 -1
  97. package/dist/models/__tests__/exports.test.js +0 -53
  98. package/dist/observable/__tests__/claim-events.test.d.ts +0 -1
  99. package/dist/observable/__tests__/claim-events.test.js +0 -23
  100. package/dist/observable/__tests__/claim-latest.test.d.ts +0 -1
  101. package/dist/observable/__tests__/claim-latest.test.js +0 -37
  102. package/dist/observable/__tests__/exports.test.d.ts +0 -1
  103. package/dist/observable/__tests__/exports.test.js +0 -21
  104. package/dist/observable/__tests__/map-events-to-store.test.d.ts +0 -1
  105. package/dist/observable/__tests__/map-events-to-store.test.js +0 -38
  106. package/dist/observable/__tests__/simple-timeout.test.d.ts +0 -1
  107. package/dist/observable/__tests__/simple-timeout.test.js +0 -34
  108. package/dist/observable/__tests__/watch-event-updates.test.d.ts +0 -1
  109. package/dist/observable/__tests__/watch-event-updates.test.js +0 -55
  110. package/dist/observable/getValue.d.ts +0 -2
  111. package/dist/observable/getValue.js +0 -13
  112. package/dist/observable/map-events-timeline.d.ts +0 -7
  113. package/dist/observable/map-events-timeline.js +0 -9
  114. package/dist/observable/share-behavior.d.ts +0 -2
  115. package/dist/observable/share-behavior.js +0 -7
  116. package/dist/observable/share-latest-value.d.ts +0 -6
  117. package/dist/observable/share-latest-value.js +0 -24
  118. package/dist/observable/stateful.d.ts +0 -10
  119. package/dist/observable/stateful.js +0 -60
  120. package/dist/observable/throttle.d.ts +0 -3
  121. package/dist/observable/throttle.js +0 -23
  122. package/dist/promise/__tests__/exports.test.d.ts +0 -1
  123. package/dist/promise/__tests__/exports.test.js +0 -11
  124. package/dist/queries/blossom.d.ts +0 -2
  125. package/dist/queries/blossom.js +0 -10
  126. package/dist/queries/bookmarks.d.ts +0 -8
  127. package/dist/queries/bookmarks.js +0 -23
  128. package/dist/queries/channels.d.ts +0 -11
  129. package/dist/queries/channels.js +0 -73
  130. package/dist/queries/comments.d.ts +0 -4
  131. package/dist/queries/comments.js +0 -14
  132. package/dist/queries/contacts.d.ts +0 -3
  133. package/dist/queries/contacts.js +0 -12
  134. package/dist/queries/index.d.ts +0 -13
  135. package/dist/queries/index.js +0 -13
  136. package/dist/queries/mailboxes.d.ts +0 -6
  137. package/dist/queries/mailboxes.js +0 -13
  138. package/dist/queries/mutes.d.ts +0 -8
  139. package/dist/queries/mutes.js +0 -23
  140. package/dist/queries/pins.d.ts +0 -3
  141. package/dist/queries/pins.js +0 -12
  142. package/dist/queries/profile.d.ts +0 -4
  143. package/dist/queries/profile.js +0 -12
  144. package/dist/queries/reactions.d.ts +0 -4
  145. package/dist/queries/reactions.js +0 -19
  146. package/dist/queries/simple.d.ts +0 -16
  147. package/dist/queries/simple.js +0 -38
  148. package/dist/queries/thread.d.ts +0 -25
  149. package/dist/queries/thread.js +0 -92
  150. package/dist/queries/user-status.d.ts +0 -11
  151. package/dist/queries/user-status.js +0 -39
  152. package/dist/queries/zaps.d.ts +0 -5
  153. package/dist/queries/zaps.js +0 -21
  154. package/dist/query-store/__tests__/query-store.test.d.ts +0 -1
  155. package/dist/query-store/__tests__/query-store.test.js +0 -63
  156. package/dist/query-store/index.d.ts +0 -1
  157. package/dist/query-store/index.js +0 -1
  158. package/dist/query-store/query-store.d.ts +0 -53
  159. package/dist/query-store/query-store.js +0 -97
  160. package/dist/query-store/query-store.test.d.ts +0 -1
  161. package/dist/query-store/query-store.test.js +0 -33
  162. package/dist/utils/lru.d.ts +0 -32
  163. package/dist/utils/lru.js +0 -148
@@ -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 */
@@ -2,18 +2,12 @@ import { ProfilePointer } from "nostr-tools/nip19";
2
2
  export type SelectOptimalRelaysOptions = {
3
3
  /** Maximum number of connections (relays) to select */
4
4
  maxConnections: number;
5
- /** Maximum coverage percentage a single relay can have (0-100 default 50) */
6
- maxRelayCoverage?: number;
7
- /** Maximum number of relays per user (default 8) */
5
+ /** Cap the number of relays a user can have */
8
6
  maxRelaysPerUser?: number;
9
- /** Minimum number of relays per user (default 2) */
10
- minRelaysPerUser?: number;
11
7
  };
12
8
  /** Selects the optimal relays for a list of ProfilePointers */
13
- export declare function selectOptimalRelays(users: ProfilePointer[], { maxConnections, maxRelayCoverage, maxRelaysPerUser, minRelaysPerUser }: SelectOptimalRelaysOptions): ProfilePointer[];
14
- /** Sorts each ProfilePointer's relays by popularity */
15
- export declare function sortRelaysByPopularity(users: ProfilePointer[]): ProfilePointer[];
9
+ export declare function selectOptimalRelays(users: ProfilePointer[], { maxConnections, maxRelaysPerUser }: SelectOptimalRelaysOptions): ProfilePointer[];
16
10
  /** A map of pubkeys by relay */
17
- export type OutboxMap = Record<string, string[]>;
11
+ export type OutboxMap = Record<string, ProfilePointer[]>;
18
12
  /** RxJS operator that aggregates contacts with outboxes into a relay -> pubkeys map */
19
13
  export declare function groupPubkeysByRelay(pointers: ProfilePointer[]): OutboxMap;
@@ -1,111 +1,72 @@
1
- import { logger } from "../logger.js";
2
- const log = logger.extend("relay-selection");
3
1
  /** Selects the optimal relays for a list of ProfilePointers */
4
- export function selectOptimalRelays(users, { maxConnections, maxRelayCoverage = 50, maxRelaysPerUser, minRelaysPerUser }) {
5
- if (!users.length)
6
- return [];
7
- // Initialize result array and tracking structures
8
- const result = [];
9
- const selectedRelays = new Set();
10
- const relayUserCounts = new Map();
11
- const totalUsers = users.length;
12
- const maxUsersPerRelay = Math.ceil((totalUsers * maxRelayCoverage) / 100);
13
- // Process each user to select optimal relays
14
- for (const user of users) {
15
- const userRelays = [];
16
- const availableRelays = user.relays || [];
17
- // If user has no relays, add them with empty relays
18
- if (availableRelays.length === 0) {
19
- result.push({ ...user, relays: [] });
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)
20
8
  continue;
21
- }
22
- // Try to select relays for this user, respecting priority order
23
- let attempts = 0;
24
- const maxAttempts = availableRelays.length * 2; // Prevent infinite loops
25
- while (userRelays.length < (maxRelaysPerUser || availableRelays.length) &&
26
- selectedRelays.size < maxConnections &&
27
- attempts < maxAttempts) {
28
- attempts++;
29
- let foundRelay = false;
30
- // Try each relay in priority order (first = highest priority)
31
- for (const relay of availableRelays) {
32
- // Skip if we already selected this relay for this user
33
- if (userRelays.includes(relay))
34
- continue;
35
- // Check if this relay would exceed coverage limit
36
- const currentRelayUsers = relayUserCounts.get(relay) || 0;
37
- if (currentRelayUsers >= maxUsersPerRelay)
38
- continue;
39
- // Select this relay
40
- userRelays.push(relay);
41
- selectedRelays.add(relay);
42
- relayUserCounts.set(relay, currentRelayUsers + 1);
43
- foundRelay = true;
44
- // Stop if we've reached maxRelaysPerUser for this user
45
- if (maxRelaysPerUser && userRelays.length >= maxRelaysPerUser)
46
- break;
47
- // Stop if we've reached maxConnections globally
48
- if (selectedRelays.size >= maxConnections)
49
- break;
50
- }
51
- // If we couldn't find any more suitable relays, break
52
- if (!foundRelay)
53
- break;
54
- }
55
- // Ensure minimum relays per user if specified
56
- if (minRelaysPerUser && userRelays.length < minRelaysPerUser) {
57
- // Try to add more relays even if they exceed coverage limits
58
- let minAttempts = 0;
59
- const maxMinAttempts = availableRelays.length;
60
- while (userRelays.length < minRelaysPerUser && minAttempts < maxMinAttempts) {
61
- minAttempts++;
62
- for (const relay of availableRelays) {
63
- if (userRelays.includes(relay))
64
- continue;
65
- if (selectedRelays.size >= maxConnections)
66
- break;
67
- userRelays.push(relay);
68
- selectedRelays.add(relay);
69
- relayUserCounts.set(relay, (relayUserCounts.get(relay) || 0) + 1);
70
- if (userRelays.length >= minRelaysPerUser)
71
- break;
72
- }
73
- if (selectedRelays.size >= maxConnections)
74
- break;
75
- }
76
- }
77
- // Add user with selected relays (maintaining original relay order)
78
- const finalRelays = availableRelays.filter((relay) => userRelays.includes(relay));
79
- result.push({ ...user, relays: finalRelays });
9
+ for (const relay of user.relays)
10
+ popular.set(relay, (popular.get(relay) || 0) + 1);
80
11
  }
81
- log(`Selected ${selectedRelays.size} relays for ${result.length} users`);
82
- log(`Relay distribution:`, Array.from(relayUserCounts.entries()));
83
- return result;
84
- }
85
- /** Sorts each ProfilePointer's relays by popularity */
86
- export function sortRelaysByPopularity(users) {
87
- const relayUsageCount = new Map();
88
- // Count the times the relays are used
89
- for (const user of users) {
12
+ // sort users relays by popularity
13
+ for (const user of usersWithRelays) {
90
14
  if (!user.relays)
91
15
  continue;
92
- for (const relay of user.relays) {
93
- relayUsageCount.set(relay, (relayUsageCount.get(relay) || 0) + 1);
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
+ }
94
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
+ });
95
59
  }
96
- return users.map((user) => {
97
- if (!user.relays)
98
- return user;
99
- // Sort the user's relays by popularity
100
- return {
101
- ...user,
102
- relays: user.relays.sort((a, b) => {
103
- const countA = relayUsageCount.get(a) || 0;
104
- const countB = relayUsageCount.get(b) || 0;
105
- return countB - countA;
106
- }),
107
- };
108
- });
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
+ }));
109
70
  }
110
71
  /** RxJS operator that aggregates contacts with outboxes into a relay -> pubkeys map */
111
72
  export function groupPubkeysByRelay(pointers) {
@@ -116,9 +77,7 @@ export function groupPubkeysByRelay(pointers) {
116
77
  for (const relay of pointer.relays) {
117
78
  if (!outbox[relay])
118
79
  outbox[relay] = [];
119
- if (!outbox[relay].includes(pointer.pubkey)) {
120
- outbox[relay].push(pointer.pubkey);
121
- }
80
+ outbox[relay].push(pointer);
122
81
  }
123
82
  }
124
83
  return outbox;
@@ -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
- import { ignoreBlacklistedRelays } from "../observable/relay-selection.js";
4
3
  import { SelectOptimalRelaysOptions } from "../helpers/relay-selection.js";
4
+ import { ignoreBlacklistedRelays } from "../observable/relay-selection.js";
5
5
  export type OutboxModelOptions = SelectOptimalRelaysOptions & {
6
6
  type?: "inbox" | "outbox";
7
7
  blacklist?: Parameters<typeof ignoreBlacklistedRelays>[0];
@@ -1,7 +1,7 @@
1
- import { ignoreBlacklistedRelays, includeLegacyAppRelays, includeMailboxes } from "../observable/relay-selection.js";
2
- import { selectOptimalRelays, sortRelaysByPopularity } from "../helpers/relay-selection.js";
3
- import { identity, map } from "rxjs";
4
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
5
  /** A model that returns the users contacts with the relays to connect to */
6
6
  export function OutboxModel(user, opts) {
7
7
  return (store) => store.contacts(user).pipe(
@@ -9,21 +9,10 @@ export function OutboxModel(user, opts) {
9
9
  opts?.blacklist ? ignoreBlacklistedRelays(opts.blacklist) : identity,
10
10
  /** Include mailboxes */
11
11
  includeMailboxes(store, opts.type),
12
- /** Include legacy app relays */
13
- includeLegacyAppRelays(store, opts.type),
14
- /** Sort the relays by popularity */
15
- map(sortRelaysByPopularity),
16
12
  /** Select the optimal relays */
17
13
  map((users) => selectOptimalRelays(users, opts)));
18
14
  }
19
15
  OutboxModel.getKey = (user, opts) => {
20
16
  const p = typeof user === "string" ? user : user.pubkey;
21
- return hash_sum([
22
- p,
23
- opts.type,
24
- opts.maxConnections,
25
- opts.maxRelayCoverage,
26
- opts.maxRelaysPerUser,
27
- opts.minRelaysPerUser,
28
- ]);
17
+ return hash_sum([p, opts.type, opts.maxConnections, opts.maxRelaysPerUser]);
29
18
  };
@@ -3,7 +3,5 @@ import { type MonoTypeOperatorFunction, type Observable, type OperatorFunction }
3
3
  import { IEventSubscriptions } from "../event-store/interface.js";
4
4
  /** RxJS operator that fetches outboxes for profile pointers from the event store */
5
5
  export declare function includeMailboxes(store: IEventSubscriptions, type?: "inbox" | "outbox"): OperatorFunction<ProfilePointer[], ProfilePointer[]>;
6
- /** An operator that reads and adds the legacy relays from the kind 3 event */
7
- export declare function includeLegacyAppRelays(store: IEventSubscriptions, type?: "inbox" | "outbox"): OperatorFunction<ProfilePointer[], ProfilePointer[]>;
8
6
  /** Removes blacklisted relays from the user's relays */
9
7
  export declare function ignoreBlacklistedRelays(blacklist: string[] | Observable<string[]>): MonoTypeOperatorFunction<ProfilePointer[]>;
@@ -1,76 +1,34 @@
1
- import { combineLatest, combineLatestWith, defaultIfEmpty, EMPTY, map, of, pipe, switchMap, timeout, } from "rxjs";
2
- import { getRelaysFromContactsEvent } from "../helpers/contacts.js";
1
+ import { combineLatest, combineLatestWith, isObservable, map, of, pipe, switchMap, } from "rxjs";
3
2
  import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
4
3
  import { addRelayHintsToPointer } from "../helpers/pointers.js";
5
- import { defined } from "./defined.js";
6
- import { logger } from "../logger.js";
7
- const log = logger.extend("relay-selection");
8
4
  /** RxJS operator that fetches outboxes for profile pointers from the event store */
9
5
  export function includeMailboxes(store, type = "outbox") {
10
6
  // Get the outboxes for all contacts
11
- return switchMap((contacts) => combineLatest(contacts.map((contact) =>
12
- // Get the outboxes for the contact
7
+ return switchMap((contacts) => combineLatest(contacts.map((user) =>
8
+ // Subscribe to the outboxes for the user
13
9
  store
14
10
  .replaceable({
15
11
  kind: 10002,
16
- pubkey: contact.pubkey,
17
- relays: contact.relays,
12
+ pubkey: user.pubkey,
18
13
  })
19
14
  .pipe(
20
- // Wait for the event to be defined
21
- defined(),
22
- // Merge the outboxes into the pointer
15
+ // Add the relays to the user
23
16
  map((event) => {
17
+ if (!event)
18
+ return user;
19
+ // Get the relays from the event
24
20
  const relays = type === "outbox" ? getOutboxes(event) : getInboxes(event);
25
21
  if (!relays)
26
- return contact;
27
- return addRelayHintsToPointer(contact, relays);
28
- }),
29
- // Timeout the request if it takes too long
30
- timeout({ first: 5_000, with: () => EMPTY }),
31
- // If no event is found, return the contact
32
- defaultIfEmpty(contact)))));
33
- }
34
- /** An operator that reads and adds the legacy relays from the kind 3 event */
35
- export function includeLegacyAppRelays(store, type = "outbox") {
36
- return switchMap((users) => {
37
- // Get the relays for all contacts
38
- return combineLatest(users.map((contact) => {
39
- // If the contact already has relays don't add any
40
- if (contact.relays && contact.relays.length > 0)
41
- return of(contact);
42
- // Get the relays for the contact
43
- return store
44
- .replaceable({
45
- kind: 1003,
46
- pubkey: contact.pubkey,
47
- relays: contact.relays,
48
- })
49
- .pipe(defined(),
50
- // Merge the relays into the pointer
51
- map((event) => {
52
- let relays = getRelaysFromContactsEvent(event);
53
- if (!relays)
54
- return contact;
55
- // Get the write relays
56
- const urls = Array.from(relays.entries())
57
- .filter(([_, t]) => t === type || t === "all")
58
- .map(([relay]) => relay);
59
- log(`Found ${urls.length} legacy ${type} relays for ${contact.pubkey}`);
60
- return addRelayHintsToPointer(contact, urls);
61
- }),
62
- // Timeout the request if it takes too long
63
- timeout({ first: 5_000, with: () => EMPTY }),
64
- // If no event is found, return the contact
65
- defaultIfEmpty(contact));
66
- }));
67
- });
22
+ return user;
23
+ // Add the relays to the user
24
+ return addRelayHintsToPointer(user, relays);
25
+ })))));
68
26
  }
69
27
  /** Removes blacklisted relays from the user's relays */
70
28
  export function ignoreBlacklistedRelays(blacklist) {
71
29
  return pipe(
72
30
  // Combine with the observable so it re-emits when the blacklist changes
73
- combineLatestWith(Array.isArray(blacklist) ? of(blacklist) : blacklist),
31
+ combineLatestWith(isObservable(blacklist) ? blacklist : of(blacklist)),
74
32
  // Filter the relays for the user
75
33
  map(([users, blacklist]) => users.map((user) => {
76
34
  if (!user.relays)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20250918142212",
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
- }
@@ -1,28 +0,0 @@
1
- import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04, nip44 } from "nostr-tools";
2
- import { unixNow } from "../helpers/time.js";
3
- export class FakeUser {
4
- key = generateSecretKey();
5
- pubkey = getPublicKey(this.key);
6
- nip04 = {
7
- encrypt: (pubkey, plaintext) => nip04.encrypt(this.key, pubkey, plaintext),
8
- decrypt: (pubkey, ciphertext) => nip04.decrypt(this.key, pubkey, ciphertext),
9
- };
10
- nip44 = {
11
- encrypt: (pubkey, plaintext) => nip44.encrypt(plaintext, nip44.getConversationKey(this.key, pubkey)),
12
- decrypt: (pubkey, ciphertext) => nip44.decrypt(ciphertext, nip44.getConversationKey(this.key, pubkey)),
13
- };
14
- event(data) {
15
- return finalizeEvent({
16
- kind: data?.kind ?? kinds.ShortTextNote,
17
- content: data?.content || "",
18
- created_at: data?.created_at ?? unixNow(),
19
- tags: data?.tags || [],
20
- }, this.key);
21
- }
22
- note(content = "Hello World", extra) {
23
- return this.event({ kind: kinds.ShortTextNote, content, ...extra });
24
- }
25
- profile(profile, extra) {
26
- return this.event({ kind: kinds.Metadata, content: JSON.stringify({ ...profile }), ...extra });
27
- }
28
- }
@@ -1 +0,0 @@
1
- export {};