applesauce-core 0.0.0-next-20250916134023 → 0.0.0-next-20250918142212

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.
@@ -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";
@@ -0,0 +1,19 @@
1
+ import { ProfilePointer } from "nostr-tools/nip19";
2
+ export type SelectOptimalRelaysOptions = {
3
+ /** Maximum number of connections (relays) to select */
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) */
8
+ maxRelaysPerUser?: number;
9
+ /** Minimum number of relays per user (default 2) */
10
+ minRelaysPerUser?: number;
11
+ };
12
+ /** 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[];
16
+ /** A map of pubkeys by relay */
17
+ export type OutboxMap = Record<string, string[]>;
18
+ /** RxJS operator that aggregates contacts with outboxes into a relay -> pubkeys map */
19
+ export declare function groupPubkeysByRelay(pointers: ProfilePointer[]): OutboxMap;
@@ -0,0 +1,125 @@
1
+ import { logger } from "../logger.js";
2
+ const log = logger.extend("relay-selection");
3
+ /** 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: [] });
20
+ 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 });
80
+ }
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) {
90
+ if (!user.relays)
91
+ continue;
92
+ for (const relay of user.relays) {
93
+ relayUsageCount.set(relay, (relayUsageCount.get(relay) || 0) + 1);
94
+ }
95
+ }
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
+ });
109
+ }
110
+ /** RxJS operator that aggregates contacts with outboxes into a relay -> pubkeys map */
111
+ export function groupPubkeysByRelay(pointers) {
112
+ const outbox = {};
113
+ for (const pointer of pointers) {
114
+ if (!pointer.relays)
115
+ continue;
116
+ for (const relay of pointer.relays) {
117
+ if (!outbox[relay])
118
+ outbox[relay] = [];
119
+ if (!outbox[relay].includes(pointer.pubkey)) {
120
+ outbox[relay].push(pointer.pubkey);
121
+ }
122
+ }
123
+ }
124
+ return outbox;
125
+ }
@@ -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 { ignoreBlacklistedRelays } from "../observable/relay-selection.js";
4
+ import { SelectOptimalRelaysOptions } from "../helpers/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,29 @@
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
+ import hash_sum from "hash-sum";
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
+ /** Include legacy app relays */
13
+ includeLegacyAppRelays(store, opts.type),
14
+ /** Sort the relays by popularity */
15
+ map(sortRelaysByPopularity),
16
+ /** Select the optimal relays */
17
+ map((users) => selectOptimalRelays(users, opts)));
18
+ }
19
+ OutboxModel.getKey = (user, opts) => {
20
+ 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
+ ]);
29
+ };
@@ -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,9 @@
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
+ /** 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
+ /** Removes blacklisted relays from the user's relays */
9
+ export declare function ignoreBlacklistedRelays(blacklist: string[] | Observable<string[]>): MonoTypeOperatorFunction<ProfilePointer[]>;
@@ -0,0 +1,80 @@
1
+ import { combineLatest, combineLatestWith, defaultIfEmpty, EMPTY, map, of, pipe, switchMap, timeout, } from "rxjs";
2
+ import { getRelaysFromContactsEvent } from "../helpers/contacts.js";
3
+ import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
4
+ 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
+ /** RxJS operator that fetches outboxes for profile pointers from the event store */
9
+ export function includeMailboxes(store, type = "outbox") {
10
+ // Get the outboxes for all contacts
11
+ return switchMap((contacts) => combineLatest(contacts.map((contact) =>
12
+ // Get the outboxes for the contact
13
+ store
14
+ .replaceable({
15
+ kind: 10002,
16
+ pubkey: contact.pubkey,
17
+ relays: contact.relays,
18
+ })
19
+ .pipe(
20
+ // Wait for the event to be defined
21
+ defined(),
22
+ // Merge the outboxes into the pointer
23
+ map((event) => {
24
+ const relays = type === "outbox" ? getOutboxes(event) : getInboxes(event);
25
+ 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
+ });
68
+ }
69
+ /** Removes blacklisted relays from the user's relays */
70
+ export function ignoreBlacklistedRelays(blacklist) {
71
+ return pipe(
72
+ // Combine with the observable so it re-emits when the blacklist changes
73
+ combineLatestWith(Array.isArray(blacklist) ? of(blacklist) : blacklist),
74
+ // Filter the relays for the user
75
+ map(([users, blacklist]) => users.map((user) => {
76
+ if (!user.relays)
77
+ return user;
78
+ return { ...user, relays: user.relays.filter((relay) => !blacklist.includes(relay)) };
79
+ })));
80
+ }
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-20250918142212",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",