Add VisibilityService for managing app visibility state and connection recovery
- Introduce a new VisibilityService to handle application visibility state, including online/offline status and connection management. - Update DI container to register the new VisibilityService and integrate it into the base module. - Modify BaseService to include visibilityService as a dependency, ensuring proper initialization and error handling. - Enhance RelayHub to register with VisibilityService for improved connection management during visibility changes. - Refactor related components to utilize the new service, streamlining visibility handling across the application.
This commit is contained in:
parent
099c16abc9
commit
3e9c9bbdef
5 changed files with 588 additions and 60 deletions
|
|
@ -10,8 +10,9 @@ import { auth } from './auth/auth-service'
|
|||
// Import PWA services
|
||||
import { pwaService } from './pwa/pwa-service'
|
||||
|
||||
// Import payment service
|
||||
// Import core services
|
||||
import { paymentService } from '@/core/services/PaymentService'
|
||||
import { visibilityService } from '@/core/services/VisibilityService'
|
||||
|
||||
/**
|
||||
* Base Module Plugin
|
||||
|
|
@ -34,6 +35,9 @@ export const baseModule: ModulePlugin = {
|
|||
// Register payment service
|
||||
container.provide(SERVICE_TOKENS.PAYMENT_SERVICE, paymentService)
|
||||
|
||||
// Register visibility service
|
||||
container.provide(SERVICE_TOKENS.VISIBILITY_SERVICE, visibilityService)
|
||||
|
||||
// Register PWA service
|
||||
container.provide('pwaService', pwaService)
|
||||
|
||||
|
|
@ -48,6 +52,10 @@ export const baseModule: ModulePlugin = {
|
|||
waitForDependencies: true, // PaymentService depends on AuthService
|
||||
maxRetries: 3
|
||||
})
|
||||
await visibilityService.initialize({
|
||||
waitForDependencies: false, // VisibilityService has no dependencies
|
||||
maxRetries: 1
|
||||
})
|
||||
|
||||
console.log('✅ Base module installed successfully')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class RelayHub extends BaseService {
|
|||
protected readonly metadata = {
|
||||
name: 'RelayHub',
|
||||
version: '1.0.0',
|
||||
dependencies: [] // RelayHub has no dependencies on other services
|
||||
dependencies: ['VisibilityService'] // Depends on visibility service for connection management
|
||||
}
|
||||
|
||||
// EventEmitter functionality
|
||||
|
|
@ -74,10 +74,11 @@ export class RelayHub extends BaseService {
|
|||
private pool: SimplePool
|
||||
private relayConfigs: Map<string, RelayConfig> = new Map()
|
||||
private connectedRelays: Map<string, Relay> = new Map()
|
||||
private subscriptions: Map<string, any> = new Map()
|
||||
private subscriptions: Map<string, SubscriptionConfig> = new Map()
|
||||
private activeSubscriptions: Map<string, any> = new Map() // Actual subscription objects
|
||||
private reconnectInterval?: number
|
||||
private healthCheckInterval?: number
|
||||
private mobileVisibilityHandler?: () => void
|
||||
private visibilityUnsubscribe?: () => void
|
||||
|
||||
// Connection state - we need both a reactive ref for components and internal state for business logic
|
||||
public isConnected = ref(false)
|
||||
|
|
@ -90,7 +91,6 @@ export class RelayHub extends BaseService {
|
|||
constructor() {
|
||||
super()
|
||||
this.pool = new SimplePool()
|
||||
this.setupMobileVisibilityHandling()
|
||||
}
|
||||
|
||||
// Delegate to internal EventEmitter
|
||||
|
|
@ -198,6 +198,10 @@ export class RelayHub extends BaseService {
|
|||
this.debug('Starting connection...')
|
||||
await this.connect()
|
||||
this.startHealthCheck()
|
||||
|
||||
// Register with visibility service for connection management
|
||||
this.registerWithVisibilityService()
|
||||
|
||||
this.debug('Initialization complete')
|
||||
}
|
||||
|
||||
|
|
@ -289,8 +293,13 @@ export class RelayHub extends BaseService {
|
|||
this.healthCheckInterval = undefined
|
||||
}
|
||||
|
||||
// Close all subscriptions
|
||||
this.subscriptions.forEach(sub => sub.close())
|
||||
// Close all active subscriptions
|
||||
for (const [, sub] of this.activeSubscriptions) {
|
||||
if (sub && typeof sub.close === 'function') {
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
this.activeSubscriptions.clear()
|
||||
this.subscriptions.clear()
|
||||
|
||||
// Close all relay connections
|
||||
|
|
@ -302,6 +311,154 @@ export class RelayHub extends BaseService {
|
|||
this.emit('disconnected')
|
||||
}
|
||||
|
||||
/**
|
||||
* Register with visibility service for connection management
|
||||
*/
|
||||
private registerWithVisibilityService(): void {
|
||||
if (!this.visibilityService) {
|
||||
this.debug('VisibilityService not available')
|
||||
return
|
||||
}
|
||||
|
||||
this.visibilityUnsubscribe = this.visibilityService.registerService(
|
||||
'RelayHub',
|
||||
async () => this.handleResume(),
|
||||
async () => this.handlePause()
|
||||
)
|
||||
|
||||
this.debug('Registered with VisibilityService')
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app resume (visibility restored)
|
||||
*/
|
||||
private async handleResume(): Promise<void> {
|
||||
this.debug('Handling resume from visibility change')
|
||||
|
||||
// Check connection health
|
||||
const disconnectedRelays = this.checkDisconnectedRelays()
|
||||
|
||||
if (disconnectedRelays.length > 0) {
|
||||
this.debug(`Found ${disconnectedRelays.length} disconnected relays, reconnecting...`)
|
||||
await this.reconnectToRelays(disconnectedRelays)
|
||||
}
|
||||
|
||||
// Restore all subscriptions
|
||||
await this.restoreSubscriptions()
|
||||
|
||||
// Resume health check
|
||||
this.startHealthCheck()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app pause (visibility lost)
|
||||
*/
|
||||
private async handlePause(): Promise<void> {
|
||||
this.debug('Handling pause from visibility change')
|
||||
|
||||
// Stop health check while paused
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval)
|
||||
this.healthCheckInterval = undefined
|
||||
}
|
||||
|
||||
// Don't disconnect immediately - just pause health checks
|
||||
// Connections will be verified when we resume
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which relays have disconnected
|
||||
*/
|
||||
private checkDisconnectedRelays(): string[] {
|
||||
const disconnected: string[] = []
|
||||
|
||||
for (const [url] of this.relayConfigs) {
|
||||
if (!this.connectedRelays.has(url)) {
|
||||
disconnected.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return disconnected
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to specific relays
|
||||
*/
|
||||
private async reconnectToRelays(relayUrls: string[]): Promise<void> {
|
||||
const promises = relayUrls.map(async url => {
|
||||
try {
|
||||
const relay = await this.pool.ensureRelay(url)
|
||||
this.connectedRelays.set(url, relay)
|
||||
this.debug(`Reconnected to relay: ${url}`)
|
||||
return { url, success: true }
|
||||
} catch (error) {
|
||||
this.debug(`Failed to reconnect to relay ${url}:`, error)
|
||||
return { url, success: false }
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.allSettled(promises)
|
||||
const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length
|
||||
|
||||
if (successful > 0) {
|
||||
this._isConnected = true
|
||||
this.isConnected.value = true
|
||||
this.emit('reconnected', successful)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all subscriptions after reconnection
|
||||
*/
|
||||
private async restoreSubscriptions(): Promise<void> {
|
||||
if (this.subscriptions.size === 0) {
|
||||
this.debug('No subscriptions to restore')
|
||||
return
|
||||
}
|
||||
|
||||
this.debug(`Restoring ${this.subscriptions.size} subscriptions`)
|
||||
|
||||
// Close old subscription objects
|
||||
for (const [, sub] of this.activeSubscriptions) {
|
||||
if (sub && typeof sub.close === 'function') {
|
||||
sub.close()
|
||||
}
|
||||
}
|
||||
this.activeSubscriptions.clear()
|
||||
|
||||
// Recreate subscriptions
|
||||
for (const [id, config] of this.subscriptions) {
|
||||
try {
|
||||
const targetRelays = config.relays || Array.from(this.connectedRelays.keys())
|
||||
const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
|
||||
|
||||
if (availableRelays.length === 0) {
|
||||
this.debug(`No available relays for subscription ${id}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Recreate the subscription
|
||||
const subscription = this.pool.subscribeMany(availableRelays, config.filters, {
|
||||
onevent: (event: Event) => {
|
||||
config.onEvent?.(event)
|
||||
this.emit('event', { subscriptionId: id, event, relay: 'unknown' })
|
||||
},
|
||||
oneose: () => {
|
||||
config.onEose?.()
|
||||
this.emit('eose', { subscriptionId: id })
|
||||
}
|
||||
})
|
||||
|
||||
this.activeSubscriptions.set(id, subscription)
|
||||
this.debug(`Restored subscription: ${id}`)
|
||||
} catch (error) {
|
||||
this.debug(`Failed to restore subscription ${id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('subscriptionsRestored', this.subscriptions.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to events from relays
|
||||
*/
|
||||
|
|
@ -336,8 +493,9 @@ export class RelayHub extends BaseService {
|
|||
}
|
||||
})
|
||||
|
||||
// Store subscription for cleanup
|
||||
this.subscriptions.set(config.id, subscription)
|
||||
// Store both config and active subscription
|
||||
this.subscriptions.set(config.id, config)
|
||||
this.activeSubscriptions.set(config.id, subscription)
|
||||
|
||||
// Emit subscription created event
|
||||
this.emit('subscriptionCreated', { id: config.id, count: this.subscriptions.size })
|
||||
|
|
@ -352,15 +510,18 @@ export class RelayHub extends BaseService {
|
|||
* 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 })
|
||||
// Close the active subscription
|
||||
const activeSubscription = this.activeSubscriptions.get(subscriptionId)
|
||||
if (activeSubscription && typeof activeSubscription.close === 'function') {
|
||||
activeSubscription.close()
|
||||
}
|
||||
|
||||
// Remove from both maps
|
||||
this.subscriptions.delete(subscriptionId)
|
||||
this.activeSubscriptions.delete(subscriptionId)
|
||||
|
||||
// Emit subscription removed event
|
||||
this.emit('subscriptionRemoved', { id: subscriptionId, count: this.subscriptions.size })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -509,41 +670,6 @@ export class RelayHub extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.isConnected.value = false
|
||||
this.emit('networkOffline')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
|
|
@ -556,14 +682,10 @@ export class RelayHub extends BaseService {
|
|||
* Cleanup when service is disposed (called by BaseService)
|
||||
*/
|
||||
protected async onDispose(): Promise<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', () => {})
|
||||
// Unregister from visibility service
|
||||
if (this.visibilityUnsubscribe) {
|
||||
this.visibilityUnsubscribe()
|
||||
this.visibilityUnsubscribe = undefined
|
||||
}
|
||||
|
||||
// Disconnect and cleanup
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue