feat: Integrate Relay Hub for centralized Nostr connection management

- Introduce a new composable, useRelayHub, to manage all Nostr WebSocket connections, enhancing connection stability and performance.
- Update existing components and composables to utilize the Relay Hub for connecting, publishing events, and subscribing to updates, streamlining the overall architecture.
- Add a RelayHubStatus component to display connection status and health metrics, improving user feedback on the connection state.
- Implement a RelayHubDemo page to showcase the functionality of the Relay Hub, including connection tests and subscription management.
- Ensure proper error handling and logging throughout the integration process to facilitate debugging and user experience.
This commit is contained in:
padreug 2025-08-10 11:48:33 +02:00
parent df7e461c91
commit 7d7bee8e77
14 changed files with 1982 additions and 955 deletions

View file

@ -1,6 +1,6 @@
import { ref, readonly } from 'vue'
import type { NostrNote } from '@/lib/nostr/client'
import { useNostr } from '@/composables/useNostr'
import { useRelayHub } from '@/composables/useRelayHub'
import { useNostrStore } from '@/stores/nostr'
import { config as globalConfig } from '@/lib/config'
import { notificationManager } from '@/lib/notifications/manager'
@ -13,7 +13,7 @@ export interface NostrFeedConfig {
}
export function useNostrFeed(config: NostrFeedConfig = {}) {
const { getClient } = useNostr(config.relays ? { relays: config.relays } : undefined)
const relayHub = useRelayHub()
const nostrStore = useNostrStore()
// State
@ -71,17 +71,16 @@ export function useNostrFeed(config: NostrFeedConfig = {}) {
error.value = null
// Connect to Nostr
const client = getClient()
await client.connect()
isConnected.value = client.isConnected
// Connect to Nostr using the centralized relay hub
await relayHub.connect()
isConnected.value = relayHub.isConnected.value
if (!isConnected.value) {
throw new Error('Failed to connect to Nostr relays')
}
// Configure fetch options based on feed type
const fetchOptions: Parameters<typeof client.fetchNotes>[0] = {
const fetchOptions: any = {
limit: config.limit || 50,
includeReplies: config.includeReplies || false
}
@ -96,8 +95,14 @@ export function useNostrFeed(config: NostrFeedConfig = {}) {
}
}
// Fetch new notes
const newNotes = await client.fetchNotes(fetchOptions)
// Fetch new notes using the relay hub
const newNotes = await relayHub.queryEvents([
{
kinds: [1], // TEXT_NOTE
limit: fetchOptions.limit,
authors: fetchOptions.authors
}
])
// Client-side filtering for 'general' feed (exclude admin posts)
let filteredNotes = newNotes
@ -147,35 +152,37 @@ export function useNostrFeed(config: NostrFeedConfig = {}) {
const subscribeToFeedUpdates = () => {
try {
const client = getClient()
// Subscribe to real-time notes
unsubscribe = client.subscribeToNotes((newNote) => {
// Only process notes newer than last seen
if (newNote.created_at > lastSeenTimestamp) {
// Check if note should be included based on feed type
const shouldInclude = shouldIncludeNote(newNote)
if (shouldInclude) {
// Add to beginning of notes array
notes.value.unshift(newNote)
// Limit the array size to prevent memory issues
if (notes.value.length > 100) {
notes.value = notes.value.slice(0, 100)
}
// Subscribe to real-time notes using the relay hub
unsubscribe = relayHub.subscribe({
id: `feed-${config.feedType || 'all'}`,
filters: [{ kinds: [1] }], // TEXT_NOTE
onEvent: (event: any) => {
// Only process notes newer than last seen
if (event.created_at > lastSeenTimestamp) {
// Check if note should be included based on feed type
const shouldInclude = shouldIncludeNote(event)
if (shouldInclude) {
// Add to beginning of notes array
notes.value.unshift(event)
// Limit the array size to prevent memory issues
if (notes.value.length > 100) {
notes.value = notes.value.slice(0, 100)
}
// Save to localStorage
const storageKey = `nostr-feed-${config.feedType || 'all'}`
localStorage.setItem(storageKey, JSON.stringify(notes.value))
// Save to localStorage
const storageKey = `nostr-feed-${config.feedType || 'all'}`
localStorage.setItem(storageKey, JSON.stringify(notes.value))
}
// Send notification if appropriate (only for admin announcements when not in announcements feed)
if (config.feedType !== 'announcements' && adminPubkeys.includes(event.pubkey)) {
notificationManager.notifyForNote(event, nostrStore.account?.pubkey)
}
// Update last seen timestamp
lastSeenTimestamp = Math.max(lastSeenTimestamp, event.created_at)
}
// Send notification if appropriate (only for admin announcements when not in announcements feed)
if (config.feedType !== 'announcements' && adminPubkeys.includes(newNote.pubkey)) {
notificationManager.notifyForNote(newNote, nostrStore.account?.pubkey)
}
// Update last seen timestamp
lastSeenTimestamp = Math.max(lastSeenTimestamp, newNote.created_at)
}
})
} catch (error) {
@ -197,9 +204,8 @@ export function useNostrFeed(config: NostrFeedConfig = {}) {
const connectToFeed = async () => {
try {
console.log('Connecting to Nostr feed...')
const client = getClient()
await client.connect()
isConnected.value = client.isConnected
await relayHub.connect()
isConnected.value = relayHub.isConnected.value
console.log('Connected to Nostr feed')
} catch (err) {
console.error('Error connecting to feed:', err)