refactor: consolidate interaction relay selection into relay-selection service

Move selectRelaysForInteraction() into the existing relay-selection.ts
service to avoid fragmentation. The service already has the infrastructure
for relay list caching, health filtering, and fallback logic.

- Add selectRelaysForInteraction() to src/services/relay-selection.ts
- Update BaseEventRenderer to import from consolidated location
- Remove separate src/lib/interaction-relay-selection.ts file
This commit is contained in:
Claude
2026-01-21 18:57:08 +00:00
parent 372e034bfc
commit e4390c73a5
3 changed files with 68 additions and 130 deletions

View File

@@ -42,7 +42,7 @@ import { getSemanticAuthor } from "@/lib/semantic-author";
import { EventFactory } from "applesauce-core/event-factory";
import { ReactionBlueprint } from "applesauce-common/blueprints";
import { publishEventToRelays } from "@/services/hub";
import { getInteractionRelays } from "@/lib/interaction-relay-selection";
import { selectRelaysForInteraction } from "@/services/relay-selection";
import type { EmojiTag } from "@/lib/emoji-helpers";
/**
@@ -552,7 +552,7 @@ export function BaseEventContainer({
const signed = await factory.sign(draft);
// Select relays per NIP-65: author's outbox + target's inbox
const relays = await getInteractionRelays(pubkey, event.pubkey);
const relays = await selectRelaysForInteraction(pubkey, event.pubkey);
await publishEventToRelays(signed, relays);
} catch (err) {
console.error("[BaseEventContainer] Failed to send reaction:", err);

View File

@@ -1,128 +0,0 @@
/**
* Interaction Relay Selection Utilities
*
* Provides optimal relay selection for interactions (reactions, replies, etc.)
* following the NIP-65 outbox model.
*
* For interactions, we need to publish to:
* 1. Author's outbox (write) relays - where we publish our events
* 2. Target's inbox (read) relays - so the target sees the interaction
*
* See: https://github.com/nostr-protocol/nips/blob/master/65.md
*/
import { relayListCache } from "@/services/relay-list-cache";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
/** Maximum number of relays to publish to */
const MAX_INTERACTION_RELAYS = 10;
/** Minimum relays per party for redundancy */
const MIN_RELAYS_PER_PARTY = 3;
export interface InteractionRelaySelectionParams {
/** Pubkey of the interaction author (person reacting/replying) */
authorPubkey: string;
/** Pubkey of the target (person being reacted to/replied to) */
targetPubkey: string;
}
export interface InteractionRelaySelectionResult {
/** Selected relays for publishing the interaction */
relays: string[];
/** Debug info about relay sources */
sources: {
authorOutbox: string[];
targetInbox: string[];
fallback: string[];
};
}
/**
* Select optimal relays for publishing an interaction event (reaction, reply, etc.)
*
* Strategy per NIP-65:
* - Author's outbox relays: where we publish our content
* - Target's inbox relays: where the target reads mentions/interactions
* - Fallback aggregators if neither has preferences
* - Deduplicate and limit to MAX_INTERACTION_RELAYS
*/
export async function selectInteractionRelays(
params: InteractionRelaySelectionParams,
): Promise<InteractionRelaySelectionResult> {
const { authorPubkey, targetPubkey } = params;
const sources = {
authorOutbox: [] as string[],
targetInbox: [] as string[],
fallback: [] as string[],
};
// Fetch relays in parallel
const [authorOutbox, targetInbox] = await Promise.all([
relayListCache.getOutboxRelays(authorPubkey),
relayListCache.getInboxRelays(targetPubkey),
]);
if (authorOutbox && authorOutbox.length > 0) {
sources.authorOutbox = authorOutbox;
}
if (targetInbox && targetInbox.length > 0) {
sources.targetInbox = targetInbox;
}
// Build relay list with priority ordering
const relaySet = new Set<string>();
// Priority 1: Author's outbox relays (where we publish)
for (const relay of sources.authorOutbox.slice(0, MIN_RELAYS_PER_PARTY)) {
relaySet.add(relay);
}
// Priority 2: Target's inbox relays (so they see it)
for (const relay of sources.targetInbox.slice(0, MIN_RELAYS_PER_PARTY)) {
relaySet.add(relay);
}
// Add remaining author outbox relays
for (const relay of sources.authorOutbox.slice(MIN_RELAYS_PER_PARTY)) {
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
relaySet.add(relay);
}
// Add remaining target inbox relays
for (const relay of sources.targetInbox.slice(MIN_RELAYS_PER_PARTY)) {
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
relaySet.add(relay);
}
// Fallback to aggregator relays if we don't have enough
if (relaySet.size === 0) {
sources.fallback = [...AGGREGATOR_RELAYS];
for (const relay of AGGREGATOR_RELAYS) {
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
relaySet.add(relay);
}
}
return {
relays: Array.from(relaySet),
sources,
};
}
/**
* Get a simple list of relays for publishing an interaction
* Convenience wrapper that just returns the relay URLs
*/
export async function getInteractionRelays(
authorPubkey: string,
targetPubkey: string,
): Promise<string[]> {
const result = await selectInteractionRelays({
authorPubkey,
targetPubkey,
});
return result.relays;
}

View File

@@ -549,3 +549,69 @@ export async function selectRelaysForFilter(
isOptimized: true,
};
}
/** Maximum number of relays for interactions */
const MAX_INTERACTION_RELAYS = 10;
/** Minimum relays per party for redundancy */
const MIN_RELAYS_PER_PARTY = 3;
/**
* Selects optimal relays for publishing an interaction event (reaction, reply, etc.)
*
* Strategy per NIP-65:
* - Author's outbox relays: where we publish our content
* - Target's inbox relays: where the target reads mentions/interactions
* - Fallback aggregators if neither has preferences
*
* @param authorPubkey - Pubkey of the interaction author (person reacting/replying)
* @param targetPubkey - Pubkey of the target (person being reacted to/replied to)
* @returns Promise resolving to array of relay URLs
*/
export async function selectRelaysForInteraction(
authorPubkey: string,
targetPubkey: string,
): Promise<string[]> {
// Fetch relays in parallel
const [authorOutbox, targetInbox] = await Promise.all([
relayListCache.getOutboxRelays(authorPubkey),
relayListCache.getInboxRelays(targetPubkey),
]);
// Build relay list with priority ordering
const relaySet = new Set<string>();
// Priority 1: Author's outbox relays (where we publish)
const outboxRelays = authorOutbox || [];
for (const relay of outboxRelays.slice(0, MIN_RELAYS_PER_PARTY)) {
relaySet.add(relay);
}
// Priority 2: Target's inbox relays (so they see it)
const inboxRelays = targetInbox || [];
for (const relay of inboxRelays.slice(0, MIN_RELAYS_PER_PARTY)) {
relaySet.add(relay);
}
// Add remaining author outbox relays
for (const relay of outboxRelays.slice(MIN_RELAYS_PER_PARTY)) {
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
relaySet.add(relay);
}
// Add remaining target inbox relays
for (const relay of inboxRelays.slice(MIN_RELAYS_PER_PARTY)) {
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
relaySet.add(relay);
}
// Fallback to aggregator relays if we don't have any
if (relaySet.size === 0) {
for (const relay of AGGREGATOR_RELAYS) {
if (relaySet.size >= MAX_INTERACTION_RELAYS) break;
relaySet.add(relay);
}
}
return Array.from(relaySet);
}