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:
parent
3e9c9bbdef
commit
d03a1fcd2c
3 changed files with 1139 additions and 0 deletions
429
docs/VisibilityService-Integration.md
Normal file
429
docs/VisibilityService-Integration.md
Normal 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
699
docs/VisibilityService.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue