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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue