- Reorganize all markdown documentation into structured docs/ folder - Create 7 main documentation categories (00-overview through 06-deployment) - Add comprehensive index files for each category with cross-linking - Implement Obsidian-compatible [[link]] syntax throughout - Move legacy/deprecated documentation to archive folder - Establish documentation standards and maintenance guidelines - Provide complete coverage of modular architecture, services, and deployment - Enable better navigation and discoverability for developers and contributors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
VisibilityService Integration Guide for Module Developers
Quick Start
1. Basic Service Integration
// 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.)
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
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
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
// 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()protected async onInitialize(): Promise<void> { await this.setupService() this.registerWithVisibilityService() // ✅ } -
Check hidden duration before expensive operations
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
protected async onDispose(): Promise<void> { if (this.visibilityUnsubscribe) { this.visibilityUnsubscribe() // ✅ } } -
Use graceful pause strategies
private async handleAppPause(): Promise<void> { this.stopHeartbeat() // ✅ Stop periodic tasks // Keep connections alive for quick resume }
❌ Don'ts
-
Don't immediately disconnect on pause
private async handleAppPause(): Promise<void> { this.disconnectAll() // ❌ Too aggressive } -
Don't ignore the service availability check
private registerWithVisibilityService(): void { // ❌ Missing availability check this.visibilityService.registerService(/*...*/) // ✅ Correct if (!this.visibilityService) return this.visibilityService.registerService(/*...*/) } -
Don't forget dependencies in metadata
protected readonly metadata = { name: 'MyService', dependencies: [] // ❌ Should include 'VisibilityService' }
Common Patterns
Connection Health Checking
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
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
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
// 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
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.
export class ChatService extends BaseService {
private async handleAppResume(): Promise<void> {
// 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<void> {
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.
export class RelayHub extends BaseService {
private async handleResume(): Promise<void> {
// 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<void> {
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.
export class WebSocketService extends BaseService {
private ws: WebSocket | null = null
private subscriptions = new Map<string, any>()
private async handleAppResume(): Promise<void> {
// 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<void> {
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<void> {
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
// In browser console or service initialization
localStorage.setItem('debug', 'VisibilityService,RelayHub,ChatService')
// Or programmatically in your service
protected async onInitialize(): Promise<void> {
if (import.meta.env.DEV) {
this.visibilityService.on('debug', (message, data) => {
console.log(`[VisibilityService] ${message}`, data)
})
}
}
Check Connection Status
// 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
export class MyService extends BaseService {
protected async onInitialize(): Promise<void> {
// 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.