Files
grimoire/docs/outbox-future-work.md
Claude 501f2746fb docs: add outbox relay selection improvement plans
Research and analysis of outbox implementations from nosotros, noStrudel,
and jumble to create a comprehensive improvement plan for Grimoire.

Priority improvements (docs/outbox-improvements-plan.md):
1. Relay Performance Scoring - track response time, connection time, stability
2. Adaptive Timeouts - use historical data for per-relay timeouts
3. Per-Relay Filter Optimization - send only relevant authors to each relay
4. Custom Scoring Function - combine coverage + performance in selection

Future work saved in docs/outbox-future-work.md:
5. Progressive Relay Selection
6. NIP-66 Relay Discovery
2025-12-24 13:23:21 +00:00

264 lines
7.0 KiB
Markdown

# Outbox Relay Selection: Future Work
These improvements are lower priority and saved for future implementation after the core scoring and optimization work is complete.
---
## 5. Progressive Relay Selection
### Problem
Currently, relay selection waits for all relay list fetches before returning results. Users wait for the full timeout even when cached data is available.
### Proposed Solution
Return results in phases:
1. **Phase 1 (0-10ms)**: Return relays from memory cache immediately
2. **Phase 2 (10-100ms)**: Add relays from Dexie cache
3. **Phase 3 (100-1000ms)**: Add relays from network fetches
```typescript
export async function selectRelaysIncremental(
eventStore: IEventStore,
filter: NostrFilter,
options?: RelaySelectionOptions,
onUpdate?: (partial: RelaySelectionResult) => void
): Promise<RelaySelectionResult> {
const authors = filter.authors || [];
// Phase 1: Return cached relays immediately
const cachedPointers = await Promise.all(
authors.map(async (pubkey) => {
const cached = await relayListCache.getOutboxRelays(pubkey);
return cached ? { pubkey, relays: cached } : null;
})
);
const initialRelays = cachedPointers
.filter((p): p is NonNullable<typeof p> => p !== null)
.flatMap(p => p.relays);
if (initialRelays.length > 0 && onUpdate) {
onUpdate({
relays: initialRelays,
reasoning: [],
isOptimized: true,
});
}
// Phase 2: Fetch missing relay lists
const uncachedAuthors = authors.filter((_, i) => !cachedPointers[i]);
if (uncachedAuthors.length > 0) {
const subscription = eventStore
.query({ kinds: [10002], authors: uncachedAuthors })
.subscribe((event) => {
relayListCache.set(event);
if (onUpdate) {
selectRelaysForFilter(eventStore, filter, options).then(onUpdate);
}
});
await new Promise(resolve =>
setTimeout(resolve, options?.timeout || 1000)
);
subscription.unsubscribe();
}
// Phase 3: Final selection
return selectRelaysForFilter(eventStore, filter, options);
}
```
### Hook Integration
```typescript
export function useOutboxRelaysIncremental(
filter: NostrFilter,
options?: RelaySelectionOptions
) {
const [result, setResult] = useState<RelaySelectionResult>({
relays: options?.fallbackRelays || [],
reasoning: [],
isOptimized: false,
});
useEffect(() => {
selectRelaysIncremental(eventStore, filter, options, setResult);
}, [filter, options]);
return result;
}
```
### Expected Impact
- Show initial results within 10-50ms (cached relays)
- Progressive enhancement as more relay lists arrive
- Better perceived performance
### Effort: Medium
### Priority: Lower (current streaming approach already shows results as they arrive)
---
## 6. NIP-66 Relay Discovery
### Problem
Grimoire uses a fixed set of fallback/aggregator relays. New relays are never discovered automatically.
### NIP-66 Overview
NIP-66 defines relay discovery via monitor relays that publish relay metadata:
- Kind 30166: Relay metadata (NIPs supported, network, country)
- Monitor relays: `wss://relay.nostr.watch`, `wss://monitorlizard.nostr1.com`
### Proposed Implementation
```typescript
// src/services/relay-discovery.ts
class RelayDiscoveryService {
private discoveryRelays = [
"wss://relay.nostr.watch/",
"wss://monitorlizard.nostr1.com/",
];
private relayCache = new Map<string, RelayMetadata>();
private cacheExpiry = 60 * 60 * 1000; // 1 hour
/**
* Discover relays by supported NIPs
*/
async getRelaysByNIPs(nips: number[]): Promise<string[]> {
await this.ensureCacheLoaded();
return Array.from(this.relayCache.entries())
.filter(([_, meta]) =>
nips.every(nip => meta.supportedNips.includes(nip))
)
.map(([url]) => url);
}
/**
* Discover relays by country
*/
async getRelaysByCountry(countryCode: string): Promise<string[]> {
await this.ensureCacheLoaded();
return Array.from(this.relayCache.entries())
.filter(([_, meta]) => meta.countryCode === countryCode)
.map(([url]) => url);
}
/**
* Get online relays (recently seen active)
*/
async getOnlineRelays(): Promise<string[]> {
await this.ensureCacheLoaded();
const now = Date.now();
const recentThreshold = 5 * 60 * 1000; // 5 minutes
return Array.from(this.relayCache.entries())
.filter(([_, meta]) => now - meta.lastSeen < recentThreshold)
.map(([url]) => url);
}
/**
* Fetch relay metadata from monitor relays
*/
private async fetchRelayMetadata(): Promise<void> {
const filter = { kinds: [30166], limit: 500 };
for (const monitorRelay of this.discoveryRelays) {
try {
const events = await pool.querySync([monitorRelay], filter);
for (const event of events) {
const url = getTagValue(event, "d");
if (!url) continue;
const metadata: RelayMetadata = {
url: normalizeRelayURL(url),
supportedNips: parseNipTags(event),
network: getTagValue(event, "n") || "clearnet",
countryCode: getTagValue(event, "l"),
lastSeen: event.created_at * 1000,
};
this.relayCache.set(metadata.url, metadata);
}
} catch (error) {
console.warn(`[RelayDiscovery] Failed to fetch from ${monitorRelay}:`, error);
}
}
}
}
interface RelayMetadata {
url: string;
supportedNips: number[];
network: "clearnet" | "tor" | "i2p";
countryCode?: string;
lastSeen: number;
}
```
### Use Cases
1. **Dynamic fallbacks**: Instead of hardcoded aggregators, discover relays that support NIP-50 (search)
2. **Geographic optimization**: Prefer relays in user's region for lower latency
3. **Feature detection**: Find relays supporting specific NIPs for advanced queries
### Integration with Relay Selection
```typescript
// In relay-selection.ts
async function selectRelaysForFilter(...) {
// If all users have no relay lists, try NIP-66 discovery
if (fallbackCount === allPointers.length) {
const discoveredRelays = await relayDiscovery.getOnlineRelays();
if (discoveredRelays.length > 0) {
return {
relays: discoveredRelays.slice(0, 10),
reasoning: discoveredRelays.slice(0, 10).map(relay => ({
relay,
writers: [],
readers: [],
isFallback: true,
isDiscovered: true, // New field
})),
isOptimized: false,
};
}
}
}
```
### Expected Impact
- Better fallback relay selection
- Automatic discovery of new relays
- Geographic optimization potential
### Effort: High
### Priority: Low (current fallback aggregators work well)
---
## When to Implement
### Progressive Relay Selection (#5)
Implement when:
- Users report slow initial load times
- Cache hit rates are low
- There's demand for faster perceived performance
### NIP-66 Relay Discovery (#6)
Implement when:
- Fallback aggregators become unreliable
- Users want geographic relay preferences
- There's a need for automatic relay discovery
---
*Created: 2024-12-24*
*Status: Backlog*