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:
parent
df7e461c91
commit
7d7bee8e77
14 changed files with 1982 additions and 955 deletions
271
src/pages/RelayHubDemo.vue
Normal file
271
src/pages/RelayHubDemo.vue
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-4">Nostr Relay Hub Demo</h1>
|
||||
<p class="text-gray-600">
|
||||
This page demonstrates the centralized relay hub that manages all Nostr WebSocket connections.
|
||||
The hub ensures connections stay active, especially important for mobile devices.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Relay Hub Status -->
|
||||
<div class="mb-8">
|
||||
<RelayHubStatus />
|
||||
</div>
|
||||
|
||||
<!-- Connection Test -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Connection Test</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="testConnection"
|
||||
:disabled="isTesting"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{{ isTesting ? 'Testing...' : 'Test Connection' }}
|
||||
</button>
|
||||
<span v-if="testResult" class="text-sm" :class="testResult.success ? 'text-green-600' : 'text-red-600'">
|
||||
{{ testResult.message }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult && testResult.success" class="text-sm text-gray-600">
|
||||
<p>Successfully connected to {{ testResult.connectedCount }} relays</p>
|
||||
<p>Connection health: {{ testResult.health }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscription Test -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Subscription Test</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="testSubscription"
|
||||
:disabled="!isConnected || isSubscribing"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{{ isSubscribing ? 'Subscribing...' : 'Test Subscription' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="activeTestSubscription"
|
||||
@click="unsubscribeTest"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
Unsubscribe
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="subscriptionEvents.length > 0" class="text-sm">
|
||||
<p class="font-medium mb-2">Received Events ({{ subscriptionEvents.length }}):</p>
|
||||
<div class="space-y-2 max-h-40 overflow-y-auto">
|
||||
<div
|
||||
v-for="event in subscriptionEvents.slice(-5)"
|
||||
:key="event.id"
|
||||
class="p-2 bg-gray-50 rounded text-xs"
|
||||
>
|
||||
<div class="font-mono text-gray-700">{{ event.id.substring(0, 8) }}...</div>
|
||||
<div class="text-gray-600">Kind: {{ event.kind }}</div>
|
||||
<div class="text-gray-600">Author: {{ event.pubkey.substring(0, 8) }}...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Visibility Test -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Mobile Visibility Test</h2>
|
||||
<p class="text-gray-600 mb-4">
|
||||
Test how the relay hub handles mobile app lifecycle events (visibility changes, network changes).
|
||||
</p>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-4 h-4 rounded-full" :class="pageVisible ? 'bg-green-500' : 'bg-red-500'"></span>
|
||||
<span>Page Visible: {{ pageVisible ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-4 h-4 rounded-full" :class="networkOnline ? 'bg-green-500' : 'bg-red-500'"></span>
|
||||
<span>Network Online: {{ networkOnline ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-xs text-gray-500">
|
||||
<p>• Try switching tabs or minimizing the browser window</p>
|
||||
<p>• Disconnect your network to test offline handling</p>
|
||||
<p>• The relay hub will maintain connections and reconnect automatically</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Info -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Configuration</h2>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Configured Relays:</span>
|
||||
<span>{{ config.nostr.relays.length }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Market Relays:</span>
|
||||
<span>{{ config.market.supportedRelays.length }}</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="font-medium mb-2">Relay URLs:</p>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="relay in config.nostr.relays"
|
||||
:key="relay"
|
||||
class="font-mono text-xs bg-gray-50 p-2 rounded"
|
||||
>
|
||||
{{ relay }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import RelayHubStatus from '@/components/RelayHubStatus.vue'
|
||||
import { useRelayHub } from '@/composables/useRelayHub'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
connectionHealth,
|
||||
connectedRelayCount,
|
||||
initialize,
|
||||
connect,
|
||||
subscribe
|
||||
} = useRelayHub()
|
||||
|
||||
// Test state
|
||||
const isTesting = ref(false)
|
||||
const testResult = ref<{
|
||||
success: boolean
|
||||
message: string
|
||||
connectedCount?: number
|
||||
health?: number
|
||||
} | null>(null)
|
||||
|
||||
const isSubscribing = ref(false)
|
||||
const activeTestSubscription = ref<(() => void) | null>(null)
|
||||
const subscriptionEvents = ref<any[]>([])
|
||||
|
||||
// Mobile visibility state
|
||||
const pageVisible = ref(!document.hidden)
|
||||
const networkOnline = ref(navigator.onLine)
|
||||
|
||||
// Test connection
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
isTesting.value = true
|
||||
testResult.value = null
|
||||
|
||||
if (!isConnected.value) {
|
||||
await connect()
|
||||
}
|
||||
|
||||
testResult.value = {
|
||||
success: true,
|
||||
message: 'Connection test successful!',
|
||||
connectedCount: connectedRelayCount.value,
|
||||
health: connectionHealth.value
|
||||
}
|
||||
} catch (error) {
|
||||
testResult.value = {
|
||||
success: false,
|
||||
message: `Connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
}
|
||||
} finally {
|
||||
isTesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Test subscription
|
||||
const testSubscription = async () => {
|
||||
try {
|
||||
isSubscribing.value = true
|
||||
subscriptionEvents.value = []
|
||||
|
||||
// Subscribe to some test events (kind 1 text notes)
|
||||
const unsubscribe = subscribe({
|
||||
id: 'test-subscription',
|
||||
filters: [{ kinds: [1], limit: 10 }],
|
||||
onEvent: (event: any) => {
|
||||
subscriptionEvents.value.push(event)
|
||||
console.log('Received test event:', event)
|
||||
},
|
||||
onEose: () => {
|
||||
console.log('Test subscription EOSE')
|
||||
}
|
||||
})
|
||||
|
||||
activeTestSubscription.value = unsubscribe
|
||||
|
||||
} catch (error) {
|
||||
console.error('Subscription test failed:', error)
|
||||
} finally {
|
||||
isSubscribing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe from test
|
||||
const unsubscribeTest = () => {
|
||||
if (activeTestSubscription.value) {
|
||||
activeTestSubscription.value()
|
||||
activeTestSubscription.value = null
|
||||
subscriptionEvents.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// Setup mobile visibility handlers
|
||||
const handleVisibilityChange = () => {
|
||||
pageVisible.value = !document.hidden
|
||||
}
|
||||
|
||||
const handleOnline = () => {
|
||||
networkOnline.value = true
|
||||
}
|
||||
|
||||
const handleOffline = () => {
|
||||
networkOnline.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Initialize relay hub if not already done
|
||||
if (!isConnected.value && connectionStatus.value === 'disconnected') {
|
||||
initialize().catch(console.error)
|
||||
}
|
||||
|
||||
// Setup visibility and network listeners
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Cleanup listeners
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
|
||||
// Cleanup test subscription
|
||||
if (activeTestSubscription.value) {
|
||||
activeTestSubscription.value()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue