# VisibilityService Integration Guide for Module Developers ## Quick Start ### 1. Basic Service Integration ```typescript // src/modules/your-module/services/your-service.ts import { BaseService } from '@/core/base/BaseService' export class YourService extends BaseService { protected readonly metadata = { name: 'YourService', version: '1.0.0', dependencies: ['VisibilityService'] // Optional but recommended } private visibilityUnsubscribe?: () => void protected async onInitialize(): Promise { // Your initialization code await this.setupService() // Register with visibility service this.registerWithVisibilityService() } private registerWithVisibilityService(): void { if (!this.visibilityService) { this.debug('VisibilityService not available') return } this.visibilityUnsubscribe = this.visibilityService.registerService( this.metadata.name, async () => this.handleAppResume(), async () => this.handleAppPause() ) this.debug('Registered with VisibilityService') } private async handleAppResume(): Promise { this.debug('App resumed - checking connections') // 1. Check if reconnection is needed if (await this.needsReconnection()) { await this.reconnectService() } // 2. Resume normal operations this.resumeBackgroundTasks() } private async handleAppPause(): Promise { this.debug('App paused - reducing activity') // 1. Stop non-essential tasks this.pauseBackgroundTasks() // 2. Prepare for potential disconnection await this.prepareForPause() } protected async onDispose(): Promise { // Always clean up registration if (this.visibilityUnsubscribe) { this.visibilityUnsubscribe() } this.debug('Service disposed') } // Implement these methods based on your service needs private async needsReconnection(): Promise { // Check if your service connections are healthy return false } private async reconnectService(): Promise { // Reconnect your service } private resumeBackgroundTasks(): void { // Resume periodic tasks, polling, etc. } private pauseBackgroundTasks(): void { // Pause periodic tasks to save battery } private async prepareForPause(): Promise { // Save state, queue operations, etc. } } ``` ## Integration Patterns by Service Type ### Real-Time Connection Services (WebSocket, Nostr, etc.) ```typescript export class RealtimeService extends BaseService { private connections = new Map() private subscriptions = new Map() private async handleAppResume(): Promise { // 1. Check connection health const brokenConnections = await this.checkConnectionHealth() // 2. Reconnect failed connections for (const connectionId of brokenConnections) { await this.reconnectConnection(connectionId) } // 3. Restore subscriptions await this.restoreSubscriptions() // 4. Resume heartbeat/keepalive this.startHeartbeat() } private async handleAppPause(): Promise { // 1. Stop heartbeat to save battery this.stopHeartbeat() // 2. Don't disconnect immediately (for quick resume) // Connections will be checked when app resumes } private async checkConnectionHealth(): Promise { const broken: string[] = [] for (const [id, connection] of this.connections) { if (!connection.isConnected()) { broken.push(id) } } return broken } } ``` ### Data Sync Services ```typescript export class DataSyncService extends BaseService { private syncQueue: Operation[] = [] private lastSyncTime: number = 0 private async handleAppResume(): Promise { const hiddenTime = Date.now() - this.lastSyncTime // If hidden for > 5 minutes, do full sync if (hiddenTime > 300000) { await this.performFullSync() } else { await this.performIncrementalSync() } // Process queued operations await this.processQueue() // Resume periodic sync this.startPeriodicSync() } private async handleAppPause(): Promise { // Stop periodic sync this.stopPeriodicSync() // Save timestamp this.lastSyncTime = Date.now() // Enable operation queueing this.enableQueueMode() } } ``` ### Background Processing Services ```typescript export class BackgroundService extends BaseService { private processingInterval?: number private taskQueue: Task[] = [] private async handleAppResume(): Promise { // Resume background processing this.startProcessing() // Process any queued tasks await this.processQueuedTasks() } private async handleAppPause(): Promise { // Stop background processing to save CPU/battery if (this.processingInterval) { clearInterval(this.processingInterval) this.processingInterval = undefined } // Queue new tasks instead of processing immediately this.enableTaskQueueing() } } ``` ## Module Registration Pattern ### Module Index File ```typescript // src/modules/your-module/index.ts import type { App } from 'vue' import type { ModulePlugin } from '@/core/types' import { YourService } from './services/your-service' export const yourModule: ModulePlugin = { name: 'your-module', version: '1.0.0', dependencies: ['base'], // base module provides VisibilityService async install(app: App, options?: any) { console.log('🔧 Installing your module...') // Create and initialize service const yourService = new YourService() // Initialize service (this will register with VisibilityService) await yourService.initialize({ waitForDependencies: true, // Wait for VisibilityService maxRetries: 3 }) // Register service in DI container container.provide(YOUR_SERVICE_TOKEN, yourService) console.log('✅ Your module installed successfully') }, async uninstall() { console.log('🗑️ Uninstalling your module...') // Services will auto-dispose and unregister from VisibilityService } } ``` ## Best Practices Checklist ### ✅ Do's - **Always register during `onInitialize()`** ```typescript protected async onInitialize(): Promise { await this.setupService() this.registerWithVisibilityService() // ✅ } ``` - **Check hidden duration before expensive operations** ```typescript private async handleAppResume(): Promise { const state = this.visibilityService.getState() if (state.hiddenDuration && state.hiddenDuration > 30000) { await this.performFullReconnect() // ✅ Only if needed } } ``` - **Always clean up registrations** ```typescript protected async onDispose(): Promise { if (this.visibilityUnsubscribe) { this.visibilityUnsubscribe() // ✅ } } ``` - **Use graceful pause strategies** ```typescript private async handleAppPause(): Promise { this.stopHeartbeat() // ✅ Stop periodic tasks // Keep connections alive for quick resume } ``` ### ❌ Don'ts - **Don't immediately disconnect on pause** ```typescript private async handleAppPause(): Promise { this.disconnectAll() // ❌ Too aggressive } ``` - **Don't ignore the service availability check** ```typescript private registerWithVisibilityService(): void { // ❌ Missing availability check this.visibilityService.registerService(/*...*/) // ✅ Correct if (!this.visibilityService) return this.visibilityService.registerService(/*...*/) } ``` - **Don't forget dependencies in metadata** ```typescript protected readonly metadata = { name: 'MyService', dependencies: [] // ❌ Should include 'VisibilityService' } ``` ## Common Patterns ### Connection Health Checking ```typescript private async checkConnectionHealth(): Promise { try { // Perform a lightweight health check await this.ping() return true } catch (error) { this.debug('Connection health check failed:', error) return false } } ``` ### Subscription Restoration ```typescript private async restoreSubscriptions(): Promise { const subscriptionsToRestore = Array.from(this.subscriptionConfigs.values()) for (const config of subscriptionsToRestore) { try { await this.recreateSubscription(config) } catch (error) { this.debug(`Failed to restore subscription ${config.id}:`, error) } } } ``` ### Operation Queueing ```typescript private operationQueue: Operation[] = [] private queueingEnabled = false private async executeOrQueue(operation: Operation): Promise { if (this.queueingEnabled) { this.operationQueue.push(operation) } else { await operation.execute() } } private async processQueue(): Promise { const operations = this.operationQueue.splice(0) // Clear queue for (const operation of operations) { try { await operation.execute() } catch (error) { this.debug('Queued operation failed:', error) } } } ``` ## Testing Integration ### Mock VisibilityService for Tests ```typescript // tests/setup/mockVisibilityService.ts export const createMockVisibilityService = () => ({ isVisible: { value: true }, isOnline: { value: true }, registerService: vi.fn(() => vi.fn()), // Returns unregister function getState: vi.fn(() => ({ isVisible: true, isOnline: true, hiddenDuration: 0 })) }) // In your test describe('YourService', () => { it('should register with VisibilityService', async () => { const mockVisibility = createMockVisibilityService() const service = new YourService() service.visibilityService = mockVisibility await service.initialize() expect(mockVisibility.registerService).toHaveBeenCalledWith( 'YourService', expect.any(Function), expect.any(Function) ) }) }) ``` ### Test Visibility Events ```typescript it('should handle app resume correctly', async () => { const service = new YourService() const reconnectSpy = vi.spyOn(service, 'reconnect') // Simulate app resume after long pause await service.handleAppResume() expect(reconnectSpy).toHaveBeenCalled() }) ``` ## WebSocket Connection Recovery Examples ### Real-World Chat Message Recovery **Scenario**: User gets a phone call, returns to app 5 minutes later. Chat messages arrived while away. ```typescript export class ChatService extends BaseService { private async handleAppResume(): Promise { // Step 1: Check if subscription still exists if (!this.subscriptionUnsubscriber) { this.debug('Chat subscription lost during backgrounding - recreating') this.setupMessageSubscription() } // Step 2: Sync missed messages from all chat peers await this.syncMissedMessages() } private async syncMissedMessages(): Promise { const peers = Array.from(this.peers.value.values()) for (const peer of peers) { try { // Get messages from last hour for this peer const recentEvents = await this.relayHub.queryEvents([ { kinds: [4], // Encrypted DMs authors: [peer.pubkey], '#p': [this.getUserPubkey()], since: Math.floor(Date.now() / 1000) - 3600, limit: 20 } ]) // Process each recovered message for (const event of recentEvents) { await this.processIncomingMessage(event) } this.debug(`Recovered ${recentEvents.length} messages from ${peer.pubkey.slice(0, 8)}`) } catch (error) { console.warn(`Failed to recover messages from peer ${peer.pubkey.slice(0, 8)}:`, error) } } } } ``` ### Nostr Relay Connection Recovery **Scenario**: Nostr relays disconnected due to mobile browser suspension. Subscriptions need restoration. ```typescript export class RelayHub extends BaseService { private async handleResume(): Promise { // Step 1: Check which relays are still connected const disconnectedRelays = this.checkDisconnectedRelays() if (disconnectedRelays.length > 0) { this.debug(`Found ${disconnectedRelays.length} disconnected relays`) await this.reconnectToRelays(disconnectedRelays) } // Step 2: Restore all subscriptions on recovered relays await this.restoreSubscriptions() this.emit('connectionRecovered', { reconnectedRelays: disconnectedRelays.length, restoredSubscriptions: this.subscriptions.size }) } private async restoreSubscriptions(): Promise { if (this.subscriptions.size === 0) return this.debug(`Restoring ${this.subscriptions.size} subscriptions`) for (const [id, config] of this.subscriptions) { try { // Recreate subscription on available relays const subscription = this.pool.subscribeMany( this.getAvailableRelays(), config.filters, { onevent: (event) => this.emit('event', { subscriptionId: id, event }), 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) } } } } ``` ### WebSocket Service Recovery **Scenario**: Custom WebSocket service (like nostrclient-hub) needs reconnection after suspension. ```typescript export class WebSocketService extends BaseService { private ws: WebSocket | null = null private subscriptions = new Map() private async handleAppResume(): Promise { // Step 1: Check WebSocket connection state if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.debug('WebSocket connection lost, reconnecting...') await this.reconnect() } // Step 2: Resubscribe to all active subscriptions if (this.ws?.readyState === WebSocket.OPEN) { await this.resubscribeAll() } } private async reconnect(): Promise { if (this.ws) { this.ws.close() } return new Promise((resolve, reject) => { this.ws = new WebSocket(this.config.url) this.ws.onopen = () => { this.debug('WebSocket reconnected successfully') resolve() } this.ws.onerror = (error) => { this.debug('WebSocket reconnection failed:', error) reject(error) } this.ws.onmessage = (message) => { this.handleMessage(JSON.parse(message.data)) } }) } private async resubscribeAll(): Promise { for (const [id, config] of this.subscriptions) { const subscribeMessage = JSON.stringify(['REQ', id, ...config.filters]) this.ws?.send(subscribeMessage) this.debug(`Resubscribed to: ${id}`) } } } ``` ## Debugging Connection Issues ### Enable Debug Logging ```typescript // In browser console or service initialization localStorage.setItem('debug', 'VisibilityService,RelayHub,ChatService') // Or programmatically in your service protected async onInitialize(): Promise { if (import.meta.env.DEV) { this.visibilityService.on('debug', (message, data) => { console.log(`[VisibilityService] ${message}`, data) }) } } ``` ### Check Connection Status ```javascript // In browser console // Check visibility state console.log('Visibility state:', visibilityService.getState()) // Check relay connections console.log('Relay status:', relayHub.getConnectionStatus()) // Check active subscriptions console.log('Active subscriptions:', relayHub.subscriptionDetails) // Force connection check await visibilityService.forceConnectionCheck() ``` ### Monitor Recovery Events ```typescript export class MyService extends BaseService { protected async onInitialize(): Promise { // Listen for visibility events this.visibilityService.on('visibilityChanged', (isVisible) => { console.log('App visibility changed:', isVisible) }) // Listen for reconnection events this.relayHub.on('connectionRecovered', (data) => { console.log('Connections recovered:', data) }) } } ``` --- This integration guide provides everything a module developer needs to add visibility management to their services. The patterns are battle-tested and optimize for both user experience and device battery life. **Key takeaway**: Mobile browsers WILL suspend WebSocket connections when apps lose focus. Integrating with VisibilityService ensures your real-time features work reliably across all platforms and usage patterns.