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
|
|
@ -45,6 +45,7 @@ export abstract class BaseService {
|
|||
protected relayHub: any = null
|
||||
protected authService: any = null
|
||||
protected nostrClientHub: any = null
|
||||
protected visibilityService: any = null
|
||||
|
||||
// Service state
|
||||
public readonly isInitialized: Ref<boolean> = ref(false)
|
||||
|
|
@ -134,6 +135,7 @@ export abstract class BaseService {
|
|||
this.relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB)
|
||||
this.authService = tryInjectService(SERVICE_TOKENS.AUTH_SERVICE)
|
||||
this.nostrClientHub = tryInjectService(SERVICE_TOKENS.NOSTR_CLIENT_HUB)
|
||||
this.visibilityService = tryInjectService(SERVICE_TOKENS.VISIBILITY_SERVICE)
|
||||
|
||||
// Check if all required dependencies are available
|
||||
const missingDeps = this.getMissingDependencies()
|
||||
|
|
@ -181,6 +183,9 @@ export abstract class BaseService {
|
|||
if (deps.includes('NostrClientHub') && !this.nostrClientHub) {
|
||||
missing.push('NostrClientHub')
|
||||
}
|
||||
if (deps.includes('VisibilityService') && !this.visibilityService) {
|
||||
missing.push('VisibilityService')
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ export const SERVICE_TOKENS = {
|
|||
// Payment services
|
||||
PAYMENT_SERVICE: Symbol('paymentService'),
|
||||
|
||||
// Visibility services
|
||||
VISIBILITY_SERVICE: Symbol('visibilityService'),
|
||||
|
||||
// Market services
|
||||
MARKET_STORE: Symbol('marketStore'),
|
||||
PAYMENT_MONITOR: Symbol('paymentMonitor'),
|
||||
|
|
|
|||
390
src/core/services/VisibilityService.ts
Normal file
390
src/core/services/VisibilityService.ts
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { BaseService } from '@/core/base/BaseService'
|
||||
|
||||
export interface VisibilityState {
|
||||
isVisible: boolean
|
||||
isOnline: boolean
|
||||
lastHiddenAt: number | null
|
||||
lastVisibleAt: number | null
|
||||
hiddenDuration: number | null
|
||||
}
|
||||
|
||||
export interface VisibilityOptions {
|
||||
reconnectThreshold?: number // Time in ms before reconnection is needed (default: 30s)
|
||||
debounceDelay?: number // Debounce rapid visibility changes (default: 100ms)
|
||||
checkInterval?: number // Interval to check connection health (default: 5s when visible)
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized Visibility Service
|
||||
* Manages app visibility state and coordinates connection recovery
|
||||
*/
|
||||
export class VisibilityService extends BaseService {
|
||||
// Service metadata
|
||||
protected readonly metadata = {
|
||||
name: 'VisibilityService',
|
||||
version: '1.0.0',
|
||||
dependencies: []
|
||||
}
|
||||
|
||||
// Configuration
|
||||
private readonly reconnectThreshold = 30000 // 30 seconds
|
||||
private readonly debounceDelay = 100 // 100ms
|
||||
private readonly checkInterval = 5000 // 5 seconds
|
||||
|
||||
// State
|
||||
private _isVisible = ref(true)
|
||||
private _isOnline = ref(true)
|
||||
private _lastHiddenAt = ref<number | null>(null)
|
||||
private _lastVisibleAt = ref<number | null>(null)
|
||||
private _isPaused = ref(false)
|
||||
|
||||
// Listeners
|
||||
private visibilityHandler?: () => void
|
||||
private onlineHandler?: () => void
|
||||
private offlineHandler?: () => void
|
||||
private focusHandler?: () => void
|
||||
private blurHandler?: () => void
|
||||
private debounceTimer?: number
|
||||
private healthCheckInterval?: number
|
||||
|
||||
// Subscribed services that need reconnection
|
||||
private subscribedServices = new Set<{
|
||||
name: string
|
||||
onResume: () => Promise<void>
|
||||
onPause: () => Promise<void>
|
||||
}>()
|
||||
|
||||
// Public reactive state
|
||||
public readonly isVisible = computed(() => this._isVisible.value)
|
||||
public readonly isOnline = computed(() => this._isOnline.value)
|
||||
public readonly isPaused = computed(() => this._isPaused.value)
|
||||
public readonly lastHiddenAt = computed(() => this._lastHiddenAt.value)
|
||||
public readonly lastVisibleAt = computed(() => this._lastVisibleAt.value)
|
||||
|
||||
public readonly hiddenDuration = computed(() => {
|
||||
if (!this._lastHiddenAt.value) return null
|
||||
if (this._isVisible.value) {
|
||||
return this._lastVisibleAt.value
|
||||
? this._lastVisibleAt.value - this._lastHiddenAt.value
|
||||
: null
|
||||
}
|
||||
return Date.now() - this._lastHiddenAt.value
|
||||
})
|
||||
|
||||
public readonly needsReconnection = computed(() => {
|
||||
const duration = this.hiddenDuration.value
|
||||
return duration !== null && duration > this.reconnectThreshold
|
||||
})
|
||||
|
||||
/**
|
||||
* Service-specific initialization (called by BaseService)
|
||||
*/
|
||||
protected async onInitialize(): Promise<void> {
|
||||
this.setupVisibilityHandlers()
|
||||
this.setupNetworkHandlers()
|
||||
this.setupFocusHandlers()
|
||||
this.startHealthCheck()
|
||||
|
||||
// Set initial state
|
||||
this._isVisible.value = !document?.hidden
|
||||
this._isOnline.value = navigator?.onLine ?? true
|
||||
|
||||
this.debug('VisibilityService initialized', {
|
||||
isVisible: this._isVisible.value,
|
||||
isOnline: this._isOnline.value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a service for visibility-based lifecycle management
|
||||
*/
|
||||
registerService(
|
||||
name: string,
|
||||
onResume: () => Promise<void>,
|
||||
onPause: () => Promise<void>
|
||||
): () => void {
|
||||
const service = { name, onResume, onPause }
|
||||
this.subscribedServices.add(service)
|
||||
|
||||
this.debug(`Service registered: ${name}`)
|
||||
|
||||
// Return unregister function
|
||||
return () => {
|
||||
this.subscribedServices.delete(service)
|
||||
this.debug(`Service unregistered: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup document visibility handlers
|
||||
*/
|
||||
private setupVisibilityHandlers(): void {
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
this.visibilityHandler = () => {
|
||||
// Debounce rapid changes
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer)
|
||||
}
|
||||
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
const wasVisible = this._isVisible.value
|
||||
const isNowVisible = !document.hidden
|
||||
|
||||
if (wasVisible === isNowVisible) return
|
||||
|
||||
this._isVisible.value = isNowVisible
|
||||
|
||||
if (isNowVisible) {
|
||||
this.handleAppVisible()
|
||||
} else {
|
||||
this.handleAppHidden()
|
||||
}
|
||||
}, this.debounceDelay) as unknown as number
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', this.visibilityHandler)
|
||||
|
||||
// Also listen for page show/hide events (iOS Safari)
|
||||
window.addEventListener('pageshow', () => this.handleAppVisible())
|
||||
window.addEventListener('pagehide', () => this.handleAppHidden())
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup network status handlers
|
||||
*/
|
||||
private setupNetworkHandlers(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
this.onlineHandler = () => {
|
||||
this._isOnline.value = true
|
||||
this.debug('Network online')
|
||||
this.handleNetworkReconnect()
|
||||
}
|
||||
|
||||
this.offlineHandler = () => {
|
||||
this._isOnline.value = false
|
||||
this.debug('Network offline')
|
||||
this.handleNetworkDisconnect()
|
||||
}
|
||||
|
||||
window.addEventListener('online', this.onlineHandler)
|
||||
window.addEventListener('offline', this.offlineHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup window focus handlers (desktop)
|
||||
*/
|
||||
private setupFocusHandlers(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
// Window focus/blur for desktop browsers
|
||||
this.focusHandler = () => {
|
||||
if (!this._isVisible.value) {
|
||||
this._isVisible.value = true
|
||||
this.handleAppVisible()
|
||||
}
|
||||
}
|
||||
|
||||
this.blurHandler = () => {
|
||||
// Don't treat blur as hidden on desktop unless document is actually hidden
|
||||
if (document.hidden && this._isVisible.value) {
|
||||
this._isVisible.value = false
|
||||
this.handleAppHidden()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
window.addEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app becoming visible
|
||||
*/
|
||||
private async handleAppVisible(): Promise<void> {
|
||||
const now = Date.now()
|
||||
this._lastVisibleAt.value = now
|
||||
this._isPaused.value = false
|
||||
|
||||
const hiddenDuration = this._lastHiddenAt.value
|
||||
? now - this._lastHiddenAt.value
|
||||
: 0
|
||||
|
||||
this.debug('App became visible', {
|
||||
hiddenDuration,
|
||||
needsReconnection: hiddenDuration > this.reconnectThreshold
|
||||
})
|
||||
|
||||
// Resume services if we were hidden long enough
|
||||
if (hiddenDuration > this.reconnectThreshold) {
|
||||
await this.resumeAllServices()
|
||||
}
|
||||
|
||||
// Restart health check
|
||||
this.startHealthCheck()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app becoming hidden
|
||||
*/
|
||||
private async handleAppHidden(): Promise<void> {
|
||||
this._lastHiddenAt.value = Date.now()
|
||||
this._isPaused.value = true
|
||||
|
||||
this.debug('App became hidden')
|
||||
|
||||
// Stop health check while hidden
|
||||
this.stopHealthCheck()
|
||||
|
||||
// Pause services after a delay (in case of quick tab switches)
|
||||
setTimeout(() => {
|
||||
if (this._isPaused.value) {
|
||||
this.pauseAllServices()
|
||||
}
|
||||
}, 5000) // 5 second delay before pausing
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle network reconnection
|
||||
*/
|
||||
private async handleNetworkReconnect(): Promise<void> {
|
||||
if (!this._isVisible.value) return
|
||||
|
||||
this.debug('Network reconnected, resuming services')
|
||||
await this.resumeAllServices()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle network disconnection
|
||||
*/
|
||||
private async handleNetworkDisconnect(): Promise<void> {
|
||||
this.debug('Network disconnected, pausing services')
|
||||
await this.pauseAllServices()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume all registered services
|
||||
*/
|
||||
private async resumeAllServices(): Promise<void> {
|
||||
this.debug(`Resuming ${this.subscribedServices.size} services`)
|
||||
|
||||
const promises = Array.from(this.subscribedServices).map(async service => {
|
||||
try {
|
||||
await service.onResume()
|
||||
this.debug(`Resumed service: ${service.name}`)
|
||||
} catch (error) {
|
||||
this.handleError(error, `resumeService:${service.name}`)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.allSettled(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause all registered services
|
||||
*/
|
||||
private async pauseAllServices(): Promise<void> {
|
||||
this.debug(`Pausing ${this.subscribedServices.size} services`)
|
||||
|
||||
const promises = Array.from(this.subscribedServices).map(async service => {
|
||||
try {
|
||||
await service.onPause()
|
||||
this.debug(`Paused service: ${service.name}`)
|
||||
} catch (error) {
|
||||
this.handleError(error, `pauseService:${service.name}`)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.allSettled(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start health check interval
|
||||
*/
|
||||
private startHealthCheck(): void {
|
||||
this.stopHealthCheck()
|
||||
|
||||
if (!this._isVisible.value) return
|
||||
|
||||
this.healthCheckInterval = setInterval(() => {
|
||||
if (this._isVisible.value && this._isOnline.value) {
|
||||
// Emit health check event for services to respond to
|
||||
this.debug('Health check tick')
|
||||
// Services can listen to this to verify their connections
|
||||
}
|
||||
}, this.checkInterval) as unknown as number
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop health check interval
|
||||
*/
|
||||
private stopHealthCheck(): void {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval)
|
||||
this.healthCheckInterval = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force check all connections
|
||||
*/
|
||||
async forceConnectionCheck(): Promise<void> {
|
||||
this.debug('Forcing connection check')
|
||||
await this.resumeAllServices()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current visibility state
|
||||
*/
|
||||
getState(): VisibilityState {
|
||||
return {
|
||||
isVisible: this._isVisible.value,
|
||||
isOnline: this._isOnline.value,
|
||||
lastHiddenAt: this._lastHiddenAt.value,
|
||||
lastVisibleAt: this._lastVisibleAt.value,
|
||||
hiddenDuration: this.hiddenDuration.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when service is disposed (called by BaseService)
|
||||
*/
|
||||
protected async onDispose(): Promise<void> {
|
||||
// Remove all event listeners
|
||||
if (typeof document !== 'undefined' && this.visibilityHandler) {
|
||||
document.removeEventListener('visibilitychange', this.visibilityHandler)
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
if (this.onlineHandler) {
|
||||
window.removeEventListener('online', this.onlineHandler)
|
||||
}
|
||||
if (this.offlineHandler) {
|
||||
window.removeEventListener('offline', this.offlineHandler)
|
||||
}
|
||||
if (this.focusHandler) {
|
||||
window.removeEventListener('focus', this.focusHandler)
|
||||
}
|
||||
if (this.blurHandler) {
|
||||
window.removeEventListener('blur', this.blurHandler)
|
||||
}
|
||||
window.removeEventListener('pageshow', () => this.handleAppVisible())
|
||||
window.removeEventListener('pagehide', () => this.handleAppHidden())
|
||||
}
|
||||
|
||||
// Clear timers
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer)
|
||||
}
|
||||
this.stopHealthCheck()
|
||||
|
||||
// Clear subscribed services
|
||||
this.subscribedServices.clear()
|
||||
|
||||
this.debug('VisibilityService disposed')
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const visibilityService = new VisibilityService()
|
||||
|
|
@ -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