applesauce-core 0.0.0-next-20250916120818 → 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.
- package/dist/event-store/async-event-store.js +8 -8
- package/dist/event-store/event-memory.d.ts +2 -2
- package/dist/event-store/event-memory.js +2 -2
- package/dist/event-store/event-store.js +7 -7
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/relay-selection.d.ts +19 -0
- package/dist/helpers/relay-selection.js +125 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/outbox.d.ts +13 -0
- package/dist/models/outbox.js +29 -0
- package/dist/observable/index.d.ts +3 -2
- package/dist/observable/index.js +4 -3
- package/dist/observable/map-events-to-store.d.ts +3 -3
- package/dist/observable/map-events-to-store.js +12 -3
- package/dist/observable/relay-selection.d.ts +9 -0
- package/dist/observable/relay-selection.js +80 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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(
|
|
237
|
+
this.memory.remove(event);
|
|
238
|
+
// Remove the event from the database
|
|
241
239
|
const removed = await this.database.remove(event);
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
+
return new Set();
|
|
331
331
|
let events = new Set();
|
|
332
332
|
for (const filter of filters) {
|
|
333
333
|
const filtered = this.getEventsForFilter(filter);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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 */
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/helpers/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/models/index.d.ts
CHANGED
package/dist/models/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
10
|
+
export * from "./relay-selection.js";
|
package/dist/observable/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
7
|
-
|
|
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
|
+
}
|