Complete legacy relay infrastructure cleanup and documentation updates
- Remove duplicate legacy relay files from src/lib/nostr/ directory - Add deprecation notices to outdated architecture documentation - Update file path references in RELAY_HUB_ARCHITECTURE.md - Clean up all remaining references to removed NostrclientHub service - Finalize consolidation to single RelayHub service architecture This completes the relay infrastructure cleanup effort, removing all redundant code and updating documentation to reflect current architecture. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3570f5110d
commit
3abdd2d7d9
6 changed files with 18 additions and 882 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
# Web App Architecture Analysis & Modularity Assessment
|
# Web App Architecture Analysis & Modularity Assessment
|
||||||
|
|
||||||
|
> **⚠️ OUTDATED DOCUMENT**
|
||||||
|
> **Updated Version**: `/docs/modular-architecture-analysis.md`
|
||||||
|
> **Date**: This document from September 4, 2025 is outdated - significant architectural changes implemented September 6, 2025
|
||||||
|
|
||||||
**Date:** September 4, 2025
|
**Date:** September 4, 2025
|
||||||
**Project:** Ario Web App (Vue 3 + Nostr + LNbits)
|
**Project:** Ario Web App (Vue 3 + Nostr + LNbits)
|
||||||
**Objective:** Evaluate current architecture for modular plugin-based development
|
**Objective:** Evaluate current architecture for modular plugin-based development
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
# Web App Architecture Analysis & Modularity Assessment
|
# Web App Architecture Analysis & Modularity Assessment
|
||||||
|
|
||||||
|
> **⚠️ OUTDATED DOCUMENT**
|
||||||
|
> **Updated Version**: `/docs/modular-architecture-analysis.md`
|
||||||
|
> **Status**: Print version of outdated September 4, 2025 analysis
|
||||||
|
|
||||||
**Date:** September 4, 2025
|
**Date:** September 4, 2025
|
||||||
**Project:** Ario Web App (Vue 3 + Nostr + LNbits)
|
**Project:** Ario Web App (Vue 3 + Nostr + LNbits)
|
||||||
**Objective:** Evaluate current architecture for modular plugin-based development
|
**Objective:** Evaluate current architecture for modular plugin-based development
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
# Ario Web App Architecture
|
# Ario Web App Architecture
|
||||||
|
|
||||||
|
> **⚠️ DEPRECATED DOCUMENT**
|
||||||
|
> **Date**: September 6, 2025
|
||||||
|
> **Status**: This document describes the legacy singleton-based architecture which has been replaced by a **modular service-based architecture** with dependency injection.
|
||||||
|
>
|
||||||
|
> **Current Architecture**: See `CLAUDE.md` and `docs/modular-architecture-analysis.md` for up-to-date information.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The Ario web app uses a **singleton-based architecture** to manage shared state and resources across components. This document explains how the core singletons work and how different components (chat, market, feed) plug into the system.
|
~~The Ario web app uses a **singleton-based architecture** to manage shared state and resources across components. This document explains how the core singletons work and how different components (chat, market, feed) plug into the system.~~
|
||||||
|
|
||||||
|
**Updated**: The Ario web app now uses a modular plugin architecture with dependency injection through the BaseService pattern and SERVICE_TOKENS.
|
||||||
|
|
||||||
## Core Singleton Architecture
|
## Core Singleton Architecture
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ The `RelayHub` class is the foundation of the relay management system, built on
|
||||||
- **Mobile Optimization**: Handles mobile app visibility changes
|
- **Mobile Optimization**: Handles mobile app visibility changes
|
||||||
- **Event Emission**: Provides event-driven architecture for status updates
|
- **Event Emission**: Provides event-driven architecture for status updates
|
||||||
|
|
||||||
**Location:** `src/lib/nostr/relayHub.ts`
|
**Location:** `src/modules/base/nostr/relay-hub.ts` *(moved from legacy lib/nostr location)*
|
||||||
|
|
||||||
### 2. Relay Hub Composable (`useRelayHub.ts`)
|
### 2. Relay Hub Composable (`useRelayHub.ts`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,359 +0,0 @@
|
||||||
import type { Filter, Event } from 'nostr-tools'
|
|
||||||
|
|
||||||
// Simple EventEmitter for browser compatibility
|
|
||||||
class EventEmitter {
|
|
||||||
private events: { [key: string]: Function[] } = {}
|
|
||||||
|
|
||||||
on(event: string, listener: Function) {
|
|
||||||
if (!this.events[event]) {
|
|
||||||
this.events[event] = []
|
|
||||||
}
|
|
||||||
this.events[event].push(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(event: string, ...args: any[]) {
|
|
||||||
if (this.events[event]) {
|
|
||||||
this.events[event].forEach(listener => listener(...args))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAllListeners(event?: string) {
|
|
||||||
if (event) {
|
|
||||||
delete this.events[event]
|
|
||||||
} else {
|
|
||||||
this.events = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NostrclientConfig {
|
|
||||||
url: string
|
|
||||||
privateKey?: string // For private WebSocket endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionConfig {
|
|
||||||
id: string
|
|
||||||
filters: Filter[]
|
|
||||||
onEvent?: (event: Event) => void
|
|
||||||
onEose?: () => void
|
|
||||||
onClose?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RelayStatus {
|
|
||||||
url: string
|
|
||||||
connected: boolean
|
|
||||||
lastSeen: number
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NostrclientHub extends EventEmitter {
|
|
||||||
private ws: WebSocket | null = null
|
|
||||||
private config: NostrclientConfig
|
|
||||||
private subscriptions: Map<string, SubscriptionConfig> = new Map()
|
|
||||||
private reconnectInterval?: number
|
|
||||||
private reconnectAttempts = 0
|
|
||||||
private readonly maxReconnectAttempts = 5
|
|
||||||
private readonly reconnectDelay = 5000
|
|
||||||
|
|
||||||
// Connection state
|
|
||||||
private _isConnected = false
|
|
||||||
private _isConnecting = false
|
|
||||||
|
|
||||||
constructor(config: NostrclientConfig) {
|
|
||||||
super()
|
|
||||||
this.config = config
|
|
||||||
}
|
|
||||||
|
|
||||||
get isConnected(): boolean {
|
|
||||||
return this._isConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
get isConnecting(): boolean {
|
|
||||||
return this._isConnecting
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalSubscriptionCount(): number {
|
|
||||||
return this.subscriptions.size
|
|
||||||
}
|
|
||||||
|
|
||||||
get subscriptionDetails(): Array<{ id: string; filters: Filter[] }> {
|
|
||||||
return Array.from(this.subscriptions.values()).map(sub => ({
|
|
||||||
id: sub.id,
|
|
||||||
filters: sub.filters
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize and connect to nostrclient WebSocket
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
console.log('🔧 NostrclientHub: Initializing connection to', this.config.url)
|
|
||||||
await this.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to the nostrclient WebSocket
|
|
||||||
*/
|
|
||||||
async connect(): Promise<void> {
|
|
||||||
if (this._isConnecting || this._isConnected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isConnecting = true
|
|
||||||
this.reconnectAttempts++
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('🔧 NostrclientHub: Connecting to nostrclient WebSocket')
|
|
||||||
|
|
||||||
// Determine WebSocket endpoint
|
|
||||||
const wsUrl = this.config.privateKey
|
|
||||||
? `${this.config.url}/${this.config.privateKey}` // Private endpoint
|
|
||||||
: `${this.config.url}/relay` // Public endpoint
|
|
||||||
|
|
||||||
this.ws = new WebSocket(wsUrl)
|
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
console.log('🔧 NostrclientHub: WebSocket connected')
|
|
||||||
this._isConnected = true
|
|
||||||
this._isConnecting = false
|
|
||||||
this.reconnectAttempts = 0
|
|
||||||
this.emit('connected')
|
|
||||||
|
|
||||||
// Resubscribe to existing subscriptions
|
|
||||||
this.resubscribeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
|
||||||
this.handleMessage(event.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onclose = (event) => {
|
|
||||||
console.log('🔧 NostrclientHub: WebSocket closed:', event.code, event.reason)
|
|
||||||
this._isConnected = false
|
|
||||||
this._isConnecting = false
|
|
||||||
this.emit('disconnected', event)
|
|
||||||
|
|
||||||
// Schedule reconnection
|
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
||||||
this.scheduleReconnect()
|
|
||||||
} else {
|
|
||||||
this.emit('maxReconnectionAttemptsReached')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onerror = (error) => {
|
|
||||||
console.error('🔧 NostrclientHub: WebSocket error:', error)
|
|
||||||
this.emit('error', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this._isConnecting = false
|
|
||||||
console.error('🔧 NostrclientHub: Connection failed:', error)
|
|
||||||
this.emit('connectionError', error)
|
|
||||||
|
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
||||||
this.scheduleReconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect from the WebSocket
|
|
||||||
*/
|
|
||||||
disconnect(): void {
|
|
||||||
if (this.reconnectInterval) {
|
|
||||||
clearTimeout(this.reconnectInterval)
|
|
||||||
this.reconnectInterval = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ws) {
|
|
||||||
this.ws.close()
|
|
||||||
this.ws = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isConnected = false
|
|
||||||
this._isConnecting = false
|
|
||||||
this.subscriptions.clear()
|
|
||||||
this.emit('disconnected')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to events
|
|
||||||
*/
|
|
||||||
subscribe(config: SubscriptionConfig): () => void {
|
|
||||||
if (!this._isConnected) {
|
|
||||||
throw new Error('Not connected to nostrclient')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store subscription
|
|
||||||
this.subscriptions.set(config.id, config)
|
|
||||||
|
|
||||||
// Send REQ message
|
|
||||||
const reqMessage = JSON.stringify([
|
|
||||||
'REQ',
|
|
||||||
config.id,
|
|
||||||
...config.filters
|
|
||||||
])
|
|
||||||
|
|
||||||
this.ws?.send(reqMessage)
|
|
||||||
console.log('🔧 NostrclientHub: Subscribed to', config.id)
|
|
||||||
|
|
||||||
// Return unsubscribe function
|
|
||||||
return () => {
|
|
||||||
this.unsubscribe(config.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from events
|
|
||||||
*/
|
|
||||||
unsubscribe(subscriptionId: string): void {
|
|
||||||
if (!this._isConnected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send CLOSE message
|
|
||||||
const closeMessage = JSON.stringify(['CLOSE', subscriptionId])
|
|
||||||
this.ws?.send(closeMessage)
|
|
||||||
|
|
||||||
// Remove from subscriptions
|
|
||||||
this.subscriptions.delete(subscriptionId)
|
|
||||||
console.log('🔧 NostrclientHub: Unsubscribed from', subscriptionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish an event
|
|
||||||
*/
|
|
||||||
async publishEvent(event: Event): Promise<void> {
|
|
||||||
if (!this._isConnected) {
|
|
||||||
throw new Error('Not connected to nostrclient')
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventMessage = JSON.stringify(['EVENT', event])
|
|
||||||
this.ws?.send(eventMessage)
|
|
||||||
|
|
||||||
console.log('🔧 NostrclientHub: Published event', event.id)
|
|
||||||
this.emit('eventPublished', { eventId: event.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query events (one-time fetch)
|
|
||||||
*/
|
|
||||||
async queryEvents(filters: Filter[]): Promise<Event[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this._isConnected) {
|
|
||||||
reject(new Error('Not connected to nostrclient'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryId = `query-${Date.now()}`
|
|
||||||
const events: Event[] = []
|
|
||||||
let eoseReceived = false
|
|
||||||
|
|
||||||
// Create temporary subscription for query
|
|
||||||
const tempSubscription = this.subscribe({
|
|
||||||
id: queryId,
|
|
||||||
filters,
|
|
||||||
onEvent: (event) => {
|
|
||||||
events.push(event)
|
|
||||||
},
|
|
||||||
onEose: () => {
|
|
||||||
eoseReceived = true
|
|
||||||
this.unsubscribe(queryId)
|
|
||||||
resolve(events)
|
|
||||||
},
|
|
||||||
onClose: () => {
|
|
||||||
if (!eoseReceived) {
|
|
||||||
reject(new Error('Query subscription closed unexpectedly'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Timeout after 30 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!eoseReceived) {
|
|
||||||
tempSubscription()
|
|
||||||
reject(new Error('Query timeout'))
|
|
||||||
}
|
|
||||||
}, 30000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle incoming WebSocket messages
|
|
||||||
*/
|
|
||||||
private handleMessage(data: string): void {
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(data)
|
|
||||||
|
|
||||||
if (Array.isArray(message) && message.length >= 2) {
|
|
||||||
const [type, subscriptionId, ...rest] = message
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'EVENT':
|
|
||||||
const event = rest[0] as Event
|
|
||||||
const subscription = this.subscriptions.get(subscriptionId)
|
|
||||||
if (subscription?.onEvent) {
|
|
||||||
subscription.onEvent(event)
|
|
||||||
}
|
|
||||||
this.emit('event', { subscriptionId, event })
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'EOSE':
|
|
||||||
const eoseSubscription = this.subscriptions.get(subscriptionId)
|
|
||||||
if (eoseSubscription?.onEose) {
|
|
||||||
eoseSubscription.onEose()
|
|
||||||
}
|
|
||||||
this.emit('eose', { subscriptionId })
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'NOTICE':
|
|
||||||
console.log('🔧 NostrclientHub: Notice from relay:', rest[0])
|
|
||||||
this.emit('notice', { message: rest[0] })
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('🔧 NostrclientHub: Unknown message type:', type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('🔧 NostrclientHub: Failed to parse message:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resubscribe to all existing subscriptions after reconnection
|
|
||||||
*/
|
|
||||||
private resubscribeAll(): void {
|
|
||||||
for (const [id, config] of this.subscriptions) {
|
|
||||||
const reqMessage = JSON.stringify([
|
|
||||||
'REQ',
|
|
||||||
id,
|
|
||||||
...config.filters
|
|
||||||
])
|
|
||||||
this.ws?.send(reqMessage)
|
|
||||||
}
|
|
||||||
console.log('🔧 NostrclientHub: Resubscribed to', this.subscriptions.size, 'subscriptions')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule automatic reconnection
|
|
||||||
*/
|
|
||||||
private scheduleReconnect(): void {
|
|
||||||
if (this.reconnectInterval) {
|
|
||||||
clearTimeout(this.reconnectInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
|
||||||
console.log(`🔧 NostrclientHub: Scheduling reconnection in ${delay}ms`)
|
|
||||||
|
|
||||||
this.reconnectInterval = setTimeout(async () => {
|
|
||||||
await this.connect()
|
|
||||||
}, delay) as unknown as number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const nostrclientHub = new NostrclientHub({
|
|
||||||
url: import.meta.env.VITE_NOSTRCLIENT_URL || 'wss://localhost:5000/nostrclient/api/v1'
|
|
||||||
})
|
|
||||||
|
|
@ -1,521 +0,0 @@
|
||||||
import { SimplePool, type Filter, type Event, type Relay } from 'nostr-tools'
|
|
||||||
|
|
||||||
// Simple EventEmitter implementation for browser compatibility
|
|
||||||
class EventEmitter {
|
|
||||||
private events: { [key: string]: Function[] } = {}
|
|
||||||
|
|
||||||
on(event: string, listener: Function): void {
|
|
||||||
if (!this.events[event]) {
|
|
||||||
this.events[event] = []
|
|
||||||
}
|
|
||||||
this.events[event].push(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
off(event: string, listener: Function): void {
|
|
||||||
if (!this.events[event]) return
|
|
||||||
const index = this.events[event].indexOf(listener)
|
|
||||||
if (index > -1) {
|
|
||||||
this.events[event].splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(event: string, ...args: any[]): void {
|
|
||||||
if (!this.events[event]) return
|
|
||||||
this.events[event].forEach(listener => listener(...args))
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAllListeners(event?: string): void {
|
|
||||||
if (event) {
|
|
||||||
delete this.events[event]
|
|
||||||
} else {
|
|
||||||
this.events = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RelayConfig {
|
|
||||||
url: string
|
|
||||||
read: boolean
|
|
||||||
write: boolean
|
|
||||||
priority?: number // Lower number = higher priority
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionConfig {
|
|
||||||
id: string
|
|
||||||
filters: Filter[]
|
|
||||||
relays?: string[] // If not specified, uses all connected relays
|
|
||||||
onEvent?: (event: Event) => void
|
|
||||||
onEose?: () => void
|
|
||||||
onClose?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RelayStatus {
|
|
||||||
url: string
|
|
||||||
connected: boolean
|
|
||||||
lastSeen: number
|
|
||||||
error?: string
|
|
||||||
latency?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RelayHub extends EventEmitter {
|
|
||||||
private pool: SimplePool
|
|
||||||
private relayConfigs: Map<string, RelayConfig> = new Map()
|
|
||||||
private connectedRelays: Map<string, Relay> = new Map()
|
|
||||||
private subscriptions: Map<string, any> = new Map()
|
|
||||||
public isInitialized = false
|
|
||||||
private reconnectInterval?: number
|
|
||||||
private healthCheckInterval?: number
|
|
||||||
private mobileVisibilityHandler?: () => void
|
|
||||||
|
|
||||||
// Connection state
|
|
||||||
private _isConnected = false
|
|
||||||
private _connectionAttempts = 0
|
|
||||||
private readonly maxReconnectAttempts = 5
|
|
||||||
private readonly reconnectDelay = 5000 // 5 seconds
|
|
||||||
private readonly healthCheckIntervalMs = 30000 // 30 seconds
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.pool = new SimplePool()
|
|
||||||
this.setupMobileVisibilityHandling()
|
|
||||||
}
|
|
||||||
|
|
||||||
get isConnected(): boolean {
|
|
||||||
return this._isConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
get connectedRelayCount(): number {
|
|
||||||
// Return the actual size of connectedRelays map
|
|
||||||
return this.connectedRelays.size
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalRelayCount(): number {
|
|
||||||
return this.relayConfigs.size
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalSubscriptionCount(): number {
|
|
||||||
return this.subscriptions.size
|
|
||||||
}
|
|
||||||
|
|
||||||
get subscriptionDetails(): Array<{ id: string; filters: any[]; relays?: string[] }> {
|
|
||||||
return Array.from(this.subscriptions.entries()).map(([id, subscription]) => {
|
|
||||||
// Try to extract subscription details if available
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
filters: subscription.filters || [],
|
|
||||||
relays: subscription.relays || []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
get relayStatuses(): RelayStatus[] {
|
|
||||||
return Array.from(this.relayConfigs.values()).map(config => {
|
|
||||||
const relay = this.connectedRelays.get(config.url)
|
|
||||||
return {
|
|
||||||
url: config.url,
|
|
||||||
connected: !!relay,
|
|
||||||
lastSeen: relay ? Date.now() : 0,
|
|
||||||
error: relay ? undefined : 'Not connected',
|
|
||||||
latency: relay ? 0 : undefined // TODO: Implement actual latency measurement
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the relay hub with relay configurations
|
|
||||||
*/
|
|
||||||
async initialize(relayUrls: string[]): Promise<void> {
|
|
||||||
if (this.isInitialized) {
|
|
||||||
console.warn('RelayHub already initialized')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔧 RelayHub: Initializing with URLs:', relayUrls)
|
|
||||||
|
|
||||||
// Convert URLs to relay configs
|
|
||||||
this.relayConfigs.clear()
|
|
||||||
relayUrls.forEach((url, index) => {
|
|
||||||
this.relayConfigs.set(url, {
|
|
||||||
url,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
priority: index
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('🔧 RelayHub: Relay configs created:', Array.from(this.relayConfigs.values()))
|
|
||||||
|
|
||||||
// Start connection management
|
|
||||||
console.log('🔧 RelayHub: Starting connection...')
|
|
||||||
await this.connect()
|
|
||||||
this.startHealthCheck()
|
|
||||||
this.isInitialized = true
|
|
||||||
console.log('🔧 RelayHub: Initialization complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to all configured relays
|
|
||||||
*/
|
|
||||||
async connect(): Promise<void> {
|
|
||||||
if (this.relayConfigs.size === 0) {
|
|
||||||
throw new Error('No relay configurations found. Call initialize() first.')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔧 RelayHub: Connecting to', this.relayConfigs.size, 'relays')
|
|
||||||
|
|
||||||
try {
|
|
||||||
this._connectionAttempts++
|
|
||||||
console.log('🔧 RelayHub: Connection attempt', this._connectionAttempts)
|
|
||||||
|
|
||||||
// Connect to relays in priority order
|
|
||||||
const sortedRelays = Array.from(this.relayConfigs.values())
|
|
||||||
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
|
||||||
|
|
||||||
console.log('🔧 RelayHub: Attempting connections to:', sortedRelays.map(r => r.url))
|
|
||||||
|
|
||||||
const connectionPromises = sortedRelays.map(async (config) => {
|
|
||||||
try {
|
|
||||||
console.log('🔧 RelayHub: Connecting to relay:', config.url)
|
|
||||||
const relay = await this.pool.ensureRelay(config.url)
|
|
||||||
this.connectedRelays.set(config.url, relay)
|
|
||||||
console.log('🔧 RelayHub: Successfully connected to:', config.url)
|
|
||||||
|
|
||||||
return { url: config.url, success: true }
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`🔧 RelayHub: Failed to connect to relay ${config.url}:`, error)
|
|
||||||
return { url: config.url, success: false, error }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(connectionPromises)
|
|
||||||
const successfulConnections = results.filter(
|
|
||||||
result => result.status === 'fulfilled' && result.value.success
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('🔧 RelayHub: Connection results:', {
|
|
||||||
total: results.length,
|
|
||||||
successful: successfulConnections.length,
|
|
||||||
failed: results.length - successfulConnections.length
|
|
||||||
})
|
|
||||||
|
|
||||||
if (successfulConnections.length > 0) {
|
|
||||||
this._isConnected = true
|
|
||||||
this._connectionAttempts = 0
|
|
||||||
console.log('🔧 RelayHub: Connection successful, connected to', successfulConnections.length, 'relays')
|
|
||||||
this.emit('connected', successfulConnections.length)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error('🔧 RelayHub: Failed to connect to any relay')
|
|
||||||
throw new Error('Failed to connect to any relay')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this._isConnected = false
|
|
||||||
console.error('🔧 RelayHub: Connection failed with error:', error)
|
|
||||||
this.emit('connectionError', error)
|
|
||||||
|
|
||||||
// Schedule reconnection if we haven't exceeded max attempts
|
|
||||||
if (this._connectionAttempts < this.maxReconnectAttempts) {
|
|
||||||
console.log('🔧 RelayHub: Scheduling reconnection attempt', this._connectionAttempts + 1)
|
|
||||||
this.scheduleReconnect()
|
|
||||||
} else {
|
|
||||||
this.emit('maxReconnectionAttemptsReached')
|
|
||||||
console.error('🔧 RelayHub: Max reconnection attempts reached')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect from all relays
|
|
||||||
*/
|
|
||||||
disconnect(): void {
|
|
||||||
|
|
||||||
|
|
||||||
// Clear intervals
|
|
||||||
if (this.reconnectInterval) {
|
|
||||||
clearTimeout(this.reconnectInterval)
|
|
||||||
this.reconnectInterval = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.healthCheckInterval) {
|
|
||||||
clearInterval(this.healthCheckInterval)
|
|
||||||
this.healthCheckInterval = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all subscriptions
|
|
||||||
this.subscriptions.forEach(sub => sub.close())
|
|
||||||
this.subscriptions.clear()
|
|
||||||
|
|
||||||
// Close all relay connections
|
|
||||||
this.pool.close(Array.from(this.relayConfigs.keys()))
|
|
||||||
this.connectedRelays.clear()
|
|
||||||
|
|
||||||
this._isConnected = false
|
|
||||||
this.emit('disconnected')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to events from relays
|
|
||||||
*/
|
|
||||||
subscribe(config: SubscriptionConfig): () => void {
|
|
||||||
if (!this.isInitialized) {
|
|
||||||
throw new Error('RelayHub not initialized. Call initialize() first.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._isConnected) {
|
|
||||||
throw new Error('Not connected to any relays')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine which relays to use
|
|
||||||
const targetRelays = config.relays || Array.from(this.connectedRelays.keys())
|
|
||||||
const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
|
|
||||||
|
|
||||||
if (availableRelays.length === 0) {
|
|
||||||
throw new Error('No available relays for subscription')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create subscription using the pool
|
|
||||||
const subscription = this.pool.subscribeMany(availableRelays, config.filters, {
|
|
||||||
onevent: (event: Event) => {
|
|
||||||
config.onEvent?.(event)
|
|
||||||
this.emit('event', { subscriptionId: config.id, event, relay: 'unknown' })
|
|
||||||
},
|
|
||||||
oneose: () => {
|
|
||||||
config.onEose?.()
|
|
||||||
this.emit('eose', { subscriptionId: config.id })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store subscription for cleanup
|
|
||||||
this.subscriptions.set(config.id, subscription)
|
|
||||||
|
|
||||||
// Emit subscription created event
|
|
||||||
this.emit('subscriptionCreated', { id: config.id, count: this.subscriptions.size })
|
|
||||||
|
|
||||||
// Return unsubscribe function
|
|
||||||
return () => {
|
|
||||||
this.unsubscribe(config.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from a specific subscription
|
|
||||||
*/
|
|
||||||
unsubscribe(subscriptionId: string): void {
|
|
||||||
const subscription = this.subscriptions.get(subscriptionId)
|
|
||||||
if (subscription) {
|
|
||||||
subscription.close()
|
|
||||||
this.subscriptions.delete(subscriptionId)
|
|
||||||
|
|
||||||
|
|
||||||
// Emit subscription removed event
|
|
||||||
this.emit('subscriptionRemoved', { id: subscriptionId, count: this.subscriptions.size })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish an event to all connected relays
|
|
||||||
*/
|
|
||||||
async publishEvent(event: Event): Promise<{ success: number; total: number }> {
|
|
||||||
if (!this._isConnected) {
|
|
||||||
throw new Error('Not connected to any relays')
|
|
||||||
}
|
|
||||||
|
|
||||||
const relayUrls = Array.from(this.connectedRelays.keys())
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
relayUrls.map(relay => this.pool.publish([relay], event))
|
|
||||||
)
|
|
||||||
|
|
||||||
const successful = results.filter(result => result.status === 'fulfilled').length
|
|
||||||
const total = results.length
|
|
||||||
|
|
||||||
|
|
||||||
this.emit('eventPublished', { eventId: event.id, success: successful, total })
|
|
||||||
|
|
||||||
return { success: successful, total }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query events from relays (one-time fetch)
|
|
||||||
*/
|
|
||||||
async queryEvents(filters: Filter[], relays?: string[]): Promise<Event[]> {
|
|
||||||
if (!this._isConnected) {
|
|
||||||
throw new Error('Not connected to any relays')
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetRelays = relays || Array.from(this.connectedRelays.keys())
|
|
||||||
const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
|
|
||||||
|
|
||||||
if (availableRelays.length === 0) {
|
|
||||||
throw new Error('No available relays for query')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Query each filter separately and combine results
|
|
||||||
const allEvents: Event[] = []
|
|
||||||
for (const filter of filters) {
|
|
||||||
const events = await this.pool.querySync(availableRelays, filter)
|
|
||||||
allEvents.push(...events)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allEvents
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Query failed:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a specific relay instance
|
|
||||||
*/
|
|
||||||
getRelay(url: string): Relay | undefined {
|
|
||||||
return this.connectedRelays.get(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a specific relay is connected
|
|
||||||
*/
|
|
||||||
isRelayConnected(url: string): boolean {
|
|
||||||
return this.connectedRelays.has(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force reconnection to all relays
|
|
||||||
*/
|
|
||||||
async reconnect(): Promise<void> {
|
|
||||||
|
|
||||||
this.disconnect()
|
|
||||||
await this.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule automatic reconnection
|
|
||||||
*/
|
|
||||||
private scheduleReconnect(): void {
|
|
||||||
if (this.reconnectInterval) {
|
|
||||||
clearTimeout(this.reconnectInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reconnectInterval = setTimeout(async () => {
|
|
||||||
|
|
||||||
await this.connect()
|
|
||||||
}, this.reconnectDelay) as unknown as number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start health check monitoring
|
|
||||||
*/
|
|
||||||
private startHealthCheck(): void {
|
|
||||||
if (this.healthCheckInterval) {
|
|
||||||
clearInterval(this.healthCheckInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.healthCheckInterval = setInterval(() => {
|
|
||||||
this.performHealthCheck()
|
|
||||||
}, this.healthCheckIntervalMs) as unknown as number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform health check on all relays
|
|
||||||
*/
|
|
||||||
private async performHealthCheck(): Promise<void> {
|
|
||||||
if (!this._isConnected) return
|
|
||||||
|
|
||||||
|
|
||||||
const disconnectedRelays: string[] = []
|
|
||||||
|
|
||||||
// Check each relay connection
|
|
||||||
for (const [url] of this.connectedRelays) {
|
|
||||||
try {
|
|
||||||
// Try to send a ping or check connection status
|
|
||||||
// For now, we'll just check if the relay is still in our connected relays map
|
|
||||||
if (!this.connectedRelays.has(url)) {
|
|
||||||
disconnectedRelays.push(url)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Health check failed for relay ${url}:`, error)
|
|
||||||
disconnectedRelays.push(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove disconnected relays
|
|
||||||
disconnectedRelays.forEach(url => {
|
|
||||||
this.connectedRelays.delete(url)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update connection status
|
|
||||||
if (this.connectedRelays.size === 0) {
|
|
||||||
this._isConnected = false
|
|
||||||
this.emit('allRelaysDisconnected')
|
|
||||||
console.warn('All relays disconnected, attempting reconnection...')
|
|
||||||
await this.connect()
|
|
||||||
} else if (this.connectedRelays.size < this.relayConfigs.size) {
|
|
||||||
this.emit('partialDisconnection', {
|
|
||||||
connected: this.connectedRelays.size,
|
|
||||||
total: this.relayConfigs.size
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup mobile visibility handling for better WebSocket management
|
|
||||||
*/
|
|
||||||
private setupMobileVisibilityHandling(): void {
|
|
||||||
// Handle page visibility changes (mobile app backgrounding)
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
this.mobileVisibilityHandler = () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
|
|
||||||
// Keep connections alive but reduce activity
|
|
||||||
} else {
|
|
||||||
console.log('Page visible, resuming normal WebSocket activity')
|
|
||||||
// Resume normal activity and check connections
|
|
||||||
this.performHealthCheck()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', this.mobileVisibilityHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle online/offline events
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
|
|
||||||
this.performHealthCheck()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
console.log('Network offline, marking as disconnected...')
|
|
||||||
this._isConnected = false
|
|
||||||
this.emit('networkOffline')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup resources
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
|
|
||||||
|
|
||||||
// Remove event listeners
|
|
||||||
if (this.mobileVisibilityHandler && typeof document !== 'undefined') {
|
|
||||||
document.removeEventListener('visibilitychange', this.mobileVisibilityHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.removeEventListener('online', () => {})
|
|
||||||
window.removeEventListener('offline', () => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect and cleanup
|
|
||||||
this.disconnect()
|
|
||||||
this.removeAllListeners()
|
|
||||||
this.isInitialized = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const relayHub = new RelayHub()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue