# VisibilityService Documentation ## Overview The `VisibilityService` is a centralized service that monitors app visibility state and coordinates connection recovery across all modules. It's designed to optimize battery life on mobile devices while ensuring reliable reconnections when the app becomes visible again. ## Table of Contents - [Core Concepts](#core-concepts) - [Architecture](#architecture) - [API Reference](#api-reference) - [Integration Guide](#integration-guide) - [Best Practices](#best-practices) - [Mobile Optimization](#mobile-optimization) - [Troubleshooting](#troubleshooting) --- ## Core Concepts ### Visibility States The service tracks multiple visibility-related states: ```typescript interface VisibilityState { isVisible: boolean // Document is visible and focused isOnline: boolean // Network connectivity status lastHiddenAt: number // Timestamp when app was hidden lastVisibleAt: number // Timestamp when app became visible hiddenDuration: number // How long the app was hidden (ms) } ``` ### Reconnection Strategy The service uses intelligent thresholds to determine when reconnection is needed: - **Reconnection Threshold**: 30 seconds (configurable) - **Debounce Delay**: 100ms for rapid visibility changes - **Health Check Interval**: 5 seconds when visible - **Pause Delay**: 5 seconds before pausing services ### Service Registration Services register callbacks for pause/resume operations: ```typescript const unregister = visibilityService.registerService( 'ServiceName', async () => handleResume(), // Called when app becomes visible async () => handlePause() // Called when app becomes hidden ) ``` --- ## Architecture ### Core Components ```mermaid graph TB VS[VisibilityService] --> |monitors| DOM[Document Events] VS --> |monitors| WIN[Window Events] VS --> |monitors| NET[Network Events] VS --> |manages| RH[RelayHub] VS --> |manages| CS[ChatService] VS --> |manages| OTHER[Other Services] RH --> |reconnects| RELAYS[Nostr Relays] RH --> |restores| SUBS[Subscriptions] ``` ### Event Flow 1. **App becomes hidden** → Stop health checks → Schedule pause (5s delay) 2. **App becomes visible** → Calculate hidden duration → Resume services if needed 3. **Network offline** → Immediately pause all services 4. **Network online** → Resume all services if app is visible --- ## API Reference ### VisibilityService Class #### Properties ```typescript // Reactive state (read-only) readonly isVisible: ComputedRef readonly isOnline: ComputedRef readonly isPaused: ComputedRef readonly lastHiddenAt: ComputedRef readonly lastVisibleAt: ComputedRef readonly hiddenDuration: ComputedRef readonly needsReconnection: ComputedRef ``` #### Methods ```typescript // Service registration registerService( name: string, onResume: () => Promise, onPause: () => Promise ): () => void // Manual control forceConnectionCheck(): Promise getState(): VisibilityState ``` ### BaseService Integration All services extending `BaseService` automatically have access to `visibilityService`: ```typescript export class MyService extends BaseService { protected readonly metadata = { name: 'MyService', version: '1.0.0', dependencies: ['VisibilityService'] // Optional: declare dependency } protected async onInitialize(): Promise { // Register for visibility management this.registerWithVisibilityService() } private registerWithVisibilityService(): void { if (!this.visibilityService) return this.visibilityService.registerService( 'MyService', async () => this.handleResume(), async () => this.handlePause() ) } } ``` --- ## Integration Guide ### Step 1: Service Registration Register your service during initialization: ```typescript protected async onInitialize(): Promise { // Your service initialization code await this.setupConnections() // Register with visibility service this.visibilityUnsubscribe = this.visibilityService?.registerService( this.metadata.name, async () => this.handleAppResume(), async () => this.handleAppPause() ) } ``` ### Step 2: Implement Resume Handler Handle app resuming (visibility restored): ```typescript private async handleAppResume(): Promise { this.debug('App resumed, checking connections') // 1. Check connection health const needsReconnection = await this.checkConnectionHealth() // 2. Reconnect if necessary if (needsReconnection) { await this.reconnect() } // 3. Restore any lost subscriptions await this.restoreSubscriptions() // 4. Resume normal operations this.startBackgroundTasks() } ``` ### Step 3: Implement Pause Handler Handle app pausing (visibility lost): ```typescript private async handleAppPause(): Promise { this.debug('App paused, reducing activity') // 1. Stop non-essential background tasks this.stopBackgroundTasks() // 2. Reduce connection activity (don't disconnect immediately) this.reduceConnectionActivity() // 3. Save any pending state await this.saveCurrentState() } ``` ### Step 4: Cleanup Unregister when service is disposed: ```typescript protected async onDispose(): Promise { // Unregister from visibility service if (this.visibilityUnsubscribe) { this.visibilityUnsubscribe() this.visibilityUnsubscribe = undefined } // Other cleanup... } ``` --- ## Best Practices ### Do's ✅ ```typescript // ✅ Register during service initialization protected async onInitialize(): Promise { this.registerWithVisibilityService() } // ✅ Check connection health before resuming private async handleAppResume(): Promise { if (await this.needsReconnection()) { await this.reconnect() } } // ✅ Graceful pause - don't immediately disconnect private async handleAppPause(): Promise { this.stopHealthChecks() // Stop periodic tasks // Keep connections alive for quick resume } // ✅ Handle network events separately private async handleNetworkChange(isOnline: boolean): Promise { if (isOnline) { await this.forceReconnection() } else { this.pauseNetworkOperations() } } // ✅ Store subscription configurations for restoration private subscriptionConfigs = new Map() ``` ### Don'ts ❌ ```typescript // ❌ Don't immediately disconnect on pause private async handleAppPause(): Promise { this.disconnect() // Too aggressive for quick tab switches } // ❌ Don't ignore hidden duration private async handleAppResume(): Promise { await this.reconnect() // Should check if reconnection is needed } // ❌ Don't handle visibility changes without debouncing document.addEventListener('visibilitychange', () => { this.handleVisibilityChange() // Can fire rapidly }) // ❌ Don't forget to clean up registrations protected async onDispose(): Promise { // Missing: this.visibilityUnsubscribe?.() } ``` ### Performance Optimizations ```typescript class OptimizedService extends BaseService { private connectionHealthCache = new Map() private async checkConnectionHealth(): Promise { const now = Date.now() const cached = this.connectionHealthCache.get('main') // Use cached result if recent (within 5 seconds) if (cached && (now - cached.lastChecked) < 5000) { return cached.isHealthy } // Perform actual health check const isHealthy = await this.performHealthCheck() this.connectionHealthCache.set('main', { isHealthy, lastChecked: now }) return isHealthy } } ``` --- ## Mobile Optimization ### Battery Life Considerations The service optimizes for mobile battery life: ```typescript // Configurable thresholds for different platforms const MOBILE_CONFIG = { reconnectThreshold: 30000, // 30s before reconnection needed debounceDelay: 100, // 100ms debounce for rapid changes healthCheckInterval: 5000, // 5s health checks when visible pauseDelay: 5000 // 5s delay before pausing } const DESKTOP_CONFIG = { reconnectThreshold: 60000, // 60s (desktop tabs stay connected longer) debounceDelay: 50, // 50ms (faster response) healthCheckInterval: 3000, // 3s (more frequent checks) pauseDelay: 10000 // 10s (longer delay before pausing) } ``` ### Browser-Specific Handling ```typescript // iOS Safari specific events window.addEventListener('pageshow', () => this.handleAppVisible()) window.addEventListener('pagehide', () => this.handleAppHidden()) // Standard visibility API (all modern browsers) document.addEventListener('visibilitychange', this.visibilityHandler) // Desktop focus handling window.addEventListener('focus', this.focusHandler) window.addEventListener('blur', this.blurHandler) // Network status window.addEventListener('online', this.onlineHandler) window.addEventListener('offline', this.offlineHandler) ``` ### PWA/Standalone App Handling ```typescript // Detect if running as standalone PWA const isStandalone = window.matchMedia('(display-mode: standalone)').matches // Adjust behavior for standalone apps const config = isStandalone ? { ...MOBILE_CONFIG, reconnectThreshold: 15000, // Shorter threshold for PWAs healthCheckInterval: 2000 // More frequent checks for better UX } : MOBILE_CONFIG ``` --- ## Real-World Examples ### RelayHub Integration ```typescript export class RelayHub extends BaseService { private subscriptions = new Map() private activeSubscriptions = new Map() protected async onInitialize(): Promise { // Initialize connections await this.connect() this.startHealthCheck() // Register with visibility service this.registerWithVisibilityService() } private async handleResume(): Promise { this.debug('Handling resume from visibility change') // Check which relays disconnected const disconnectedRelays = this.checkDisconnectedRelays() if (disconnectedRelays.length > 0) { this.debug(`Found ${disconnectedRelays.length} disconnected relays`) await this.reconnectToRelays(disconnectedRelays) } // Restore all subscriptions await this.restoreSubscriptions() // Resume health check this.startHealthCheck() } private async handlePause(): Promise { this.debug('Handling pause from visibility change') // Stop health check while paused (saves battery) if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval) this.healthCheckInterval = undefined } // Don't disconnect immediately - connections will be verified on resume } } ``` ### Chat Service Integration ```typescript export class ChatService extends BaseService { private messageQueue: Message[] = [] private connectionRetryCount = 0 private async handleResume(): Promise { // Reset retry count on successful resume this.connectionRetryCount = 0 // Check if we missed any messages while away await this.syncMissedMessages() // Process any queued messages await this.processMessageQueue() // Resume real-time message monitoring this.startMessageMonitoring() } private async handlePause(): Promise { // Queue outgoing messages instead of sending immediately this.enableMessageQueueing() // Stop real-time monitoring this.stopMessageMonitoring() // Save current conversation state await this.saveConversationState() } private async syncMissedMessages(): Promise { const lastSeenTimestamp = this.getLastSeenTimestamp() const missedMessages = await this.fetchMessagesSince(lastSeenTimestamp) for (const message of missedMessages) { this.processMessage(message) } } } ``` ### Custom Service Example ```typescript export class DataSyncService extends BaseService { private syncQueue: SyncOperation[] = [] private lastSyncTimestamp: number = 0 protected readonly metadata = { name: 'DataSyncService', version: '1.0.0', dependencies: ['VisibilityService', 'RelayHub'] } protected async onInitialize(): Promise { this.registerWithVisibilityService() this.startPeriodicSync() } private registerWithVisibilityService(): void { if (!this.visibilityService) { this.debug('VisibilityService not available') return } this.visibilityUnsubscribe = this.visibilityService.registerService( 'DataSyncService', async () => this.handleAppResume(), async () => this.handleAppPause() ) } private async handleAppResume(): Promise { const hiddenDuration = Date.now() - this.lastSyncTimestamp // If we were hidden for more than 5 minutes, do a full sync if (hiddenDuration > 300000) { await this.performFullSync() } else { // Otherwise just sync changes since we paused await this.performIncrementalSync() } // Process any queued sync operations await this.processSyncQueue() // Resume periodic sync this.startPeriodicSync() } private async handleAppPause(): Promise { // Stop periodic sync to save battery this.stopPeriodicSync() // Queue any pending operations instead of executing immediately this.enableOperationQueueing() // Save current sync state this.lastSyncTimestamp = Date.now() await this.saveSyncState() } } ``` --- ## Troubleshooting ### Common Issues #### Services Not Resuming ```typescript // Problem: Service not registered properly // Solution: Check registration in onInitialize() protected async onInitialize(): Promise { // ❌ Incorrect - missing registration await this.setupService() // ✅ Correct - register with visibility service await this.setupService() this.registerWithVisibilityService() } ``` #### Excessive Reconnections ```typescript // Problem: Not checking hidden duration // Solution: Implement proper threshold checking private async handleAppResume(): Promise { // ❌ Incorrect - always reconnects await this.reconnect() // ✅ Correct - check if reconnection is needed const state = this.visibilityService.getState() if (state.hiddenDuration && state.hiddenDuration > 30000) { await this.reconnect() } } ``` #### Memory Leaks ```typescript // Problem: Not cleaning up registrations // Solution: Proper disposal in onDispose() protected async onDispose(): Promise { // ✅ Always clean up registrations if (this.visibilityUnsubscribe) { this.visibilityUnsubscribe() this.visibilityUnsubscribe = undefined } // Clean up other resources this.clearTimers() this.closeConnections() } ``` ### Debugging Enable debug logging for visibility-related issues: ```typescript // In your service constructor or initialization constructor() { super() // Enable visibility service debugging if (this.visibilityService) { this.visibilityService.on('debug', (message: string, data?: any) => { console.log(`[VisibilityService] ${message}`, data) }) } } ``` Check visibility state in browser console: ```javascript // Get current visibility state console.log(visibilityService.getState()) // Force connection check await visibilityService.forceConnectionCheck() // Check service registrations console.log(visibilityService.subscribedServices.size) ``` --- ## Configuration ### Environment-Based Configuration ```typescript // src/config/visibility.ts export const getVisibilityConfig = () => { const isMobile = /Mobile|Android|iPhone|iPad/.test(navigator.userAgent) const isPWA = window.matchMedia('(display-mode: standalone)').matches if (isPWA) { return { reconnectThreshold: 15000, // 15s for PWAs debounceDelay: 50, healthCheckInterval: 2000, pauseDelay: 3000 } } else if (isMobile) { return { reconnectThreshold: 30000, // 30s for mobile web debounceDelay: 100, healthCheckInterval: 5000, pauseDelay: 5000 } } else { return { reconnectThreshold: 60000, // 60s for desktop debounceDelay: 50, healthCheckInterval: 3000, pauseDelay: 10000 } } } ``` ### Module-Specific Configuration ```typescript // Each module can provide custom visibility config export interface ModuleVisibilityConfig { enableVisibilityManagement?: boolean customThresholds?: { reconnectThreshold?: number pauseDelay?: number } criticalService?: boolean // Never pause critical services } export const chatModule: ModulePlugin = { name: 'chat', visibilityConfig: { enableVisibilityManagement: true, customThresholds: { reconnectThreshold: 10000, // Chat needs faster reconnection pauseDelay: 2000 }, criticalService: false } } ``` --- ## Summary The VisibilityService provides a powerful, centralized way to manage app visibility and connection states across all modules. By following the integration patterns and best practices outlined in this documentation, your services will automatically benefit from: - **Optimized battery life** on mobile devices - **Reliable connection recovery** after app visibility changes - **Intelligent reconnection logic** based on hidden duration - **Seamless subscription restoration** for real-time features - **Cross-platform compatibility** for web, mobile, and PWA The modular architecture ensures that adding visibility management to any service is straightforward while maintaining the flexibility to customize behavior per service needs. For questions or issues, check the troubleshooting section or review the real-world examples for implementation guidance.