Add VisibilityService documentation and integration guide

- Create comprehensive documentation for the new VisibilityService, detailing its purpose, core concepts, and architecture.
- Include an integration guide for module developers, outlining best practices for registering services and handling app visibility changes.
- Add example code snippets for implementing visibility management in various service types, ensuring clarity and ease of use.
- Introduce a troubleshooting section to address common issues and provide debugging tips for developers.
- Enhance the VisibilityService integration guide with real-world examples to illustrate practical usage scenarios.
This commit is contained in:
padreug 2025-09-05 15:44:22 +02:00
parent 3e9c9bbdef
commit d03a1fcd2c
3 changed files with 1139 additions and 0 deletions

View file

@ -80,6 +80,7 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
- `SERVICE_TOKENS.RELAY_HUB` - Centralized Nostr relay management
- `SERVICE_TOKENS.NOSTR_CLIENT_HUB` - Nostr client services
- `SERVICE_TOKENS.AUTH_SERVICE` - Authentication services
- `SERVICE_TOKENS.VISIBILITY_SERVICE` - App visibility and connection management
**Core Stack:**
- Vue 3 with Composition API (`<script setup>` style)
@ -159,6 +160,16 @@ export const myModule: ModulePlugin = {
- Base module provides core infrastructure services
- Modules can register their own services in the DI container
**⚠️ CRITICAL - WebSocket Connection Management:**
- **ALWAYS integrate with VisibilityService for any module that uses WebSocket connections**
- All services extending `BaseService` have automatic access to `this.visibilityService`
- Register visibility callbacks during service initialization: `this.visibilityService.registerService(name, onResume, onPause)`
- Implement proper connection recovery in `onResume()` handler (check health, reconnect if needed, restore subscriptions)
- Implement battery-conscious pausing in `onPause()` handler (stop heartbeats, queue operations)
- **Mobile browsers suspend WebSocket connections when app loses visibility** - visibility management is essential for reliable real-time features
- See `docs/VisibilityService.md` and `docs/VisibilityService-Integration.md` for comprehensive integration guides
- Future modules will likely ALL depend on WebSocket connections - plan for visibility management from the start
### **Centralized Infrastructure**
**Nostr Relay Management:**

View file

@ -0,0 +1,429 @@
# 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<void> {
// 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<void> {
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<void> {
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<void> {
// 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<boolean> {
// Check if your service connections are healthy
return false
}
private async reconnectService(): Promise<void> {
// Reconnect your service
}
private resumeBackgroundTasks(): void {
// Resume periodic tasks, polling, etc.
}
private pauseBackgroundTasks(): void {
// Pause periodic tasks to save battery
}
private async prepareForPause(): Promise<void> {
// 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<string, Connection>()
private subscriptions = new Map<string, Subscription>()
private async handleAppResume(): Promise<void> {
// 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<void> {
// 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<string[]> {
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<void> {
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<void> {
// 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<void> {
// Resume background processing
this.startProcessing()
// Process any queued tasks
await this.processQueuedTasks()
}
private async handleAppPause(): Promise<void> {
// 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<void> {
await this.setupService()
this.registerWithVisibilityService() // ✅
}
```
- **Check hidden duration before expensive operations**
```typescript
private async handleAppResume(): Promise<void> {
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<void> {
if (this.visibilityUnsubscribe) {
this.visibilityUnsubscribe() // ✅
}
}
```
- **Use graceful pause strategies**
```typescript
private async handleAppPause(): Promise<void> {
this.stopHeartbeat() // ✅ Stop periodic tasks
// Keep connections alive for quick resume
}
```
### ❌ Don'ts
- **Don't immediately disconnect on pause**
```typescript
private async handleAppPause(): Promise<void> {
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<boolean> {
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<void> {
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<void> {
if (this.queueingEnabled) {
this.operationQueue.push(operation)
} else {
await operation.execute()
}
}
private async processQueue(): Promise<void> {
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()
})
```
---
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.

699
docs/VisibilityService.md Normal file
View file

@ -0,0 +1,699 @@
# 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<boolean>
readonly isOnline: ComputedRef<boolean>
readonly isPaused: ComputedRef<boolean>
readonly lastHiddenAt: ComputedRef<number | null>
readonly lastVisibleAt: ComputedRef<number | null>
readonly hiddenDuration: ComputedRef<number | null>
readonly needsReconnection: ComputedRef<boolean>
```
#### Methods
```typescript
// Service registration
registerService(
name: string,
onResume: () => Promise<void>,
onPause: () => Promise<void>
): () => void
// Manual control
forceConnectionCheck(): Promise<void>
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<void> {
// 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<void> {
// 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<void> {
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<void> {
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<void> {
// 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<void> {
this.registerWithVisibilityService()
}
// ✅ Check connection health before resuming
private async handleAppResume(): Promise<void> {
if (await this.needsReconnection()) {
await this.reconnect()
}
}
// ✅ Graceful pause - don't immediately disconnect
private async handleAppPause(): Promise<void> {
this.stopHealthChecks() // Stop periodic tasks
// Keep connections alive for quick resume
}
// ✅ Handle network events separately
private async handleNetworkChange(isOnline: boolean): Promise<void> {
if (isOnline) {
await this.forceReconnection()
} else {
this.pauseNetworkOperations()
}
}
// ✅ Store subscription configurations for restoration
private subscriptionConfigs = new Map<string, SubscriptionConfig>()
```
### Don'ts ❌
```typescript
// ❌ Don't immediately disconnect on pause
private async handleAppPause(): Promise<void> {
this.disconnect() // Too aggressive for quick tab switches
}
// ❌ Don't ignore hidden duration
private async handleAppResume(): Promise<void> {
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<void> {
// Missing: this.visibilityUnsubscribe?.()
}
```
### Performance Optimizations
```typescript
class OptimizedService extends BaseService {
private connectionHealthCache = new Map<string, {
isHealthy: boolean,
lastChecked: number
}>()
private async checkConnectionHealth(): Promise<boolean> {
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<string, SubscriptionConfig>()
private activeSubscriptions = new Map<string, any>()
protected async onInitialize(): Promise<void> {
// Initialize connections
await this.connect()
this.startHealthCheck()
// Register with visibility service
this.registerWithVisibilityService()
}
private async handleResume(): Promise<void> {
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<void> {
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<void> {
// 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<void> {
// 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<void> {
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<void> {
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<void> {
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<void> {
// 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<void> {
// ❌ 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<void> {
// ❌ 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<void> {
// ✅ 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.