Enhance VisibilityService integration for WebSocket and chat services

- Add detailed examples for WebSocket connection recovery and chat message synchronization in the VisibilityService documentation.
- Refactor ChatService to register with VisibilityService, enabling automatic handling of app visibility changes and missed message synchronization.
- Implement connection recovery logic in NostrclientHub and ChatService to ensure seamless user experience during app backgrounding.
- Update base module to ensure proper initialization of services with VisibilityService dependencies, enhancing overall connection management.
This commit is contained in:
padreug 2025-09-05 15:57:02 +02:00
parent d03a1fcd2c
commit ef7333e68e
5 changed files with 756 additions and 121 deletions

View file

@ -423,49 +423,194 @@ export class RelayHub extends BaseService {
}
```
### Chat Service Integration
### Chat Service Integration - WebSocket Connection Recovery
**Real-World Scenario**: User receives a WhatsApp notification, switches to WhatsApp for 2 minutes, then returns to the Nostr chat app. The WebSocket connection was suspended by the mobile browser.
```typescript
export class ChatService extends BaseService {
private messageQueue: Message[] = []
private connectionRetryCount = 0
protected readonly metadata = {
name: 'ChatService',
version: '1.0.0',
dependencies: ['RelayHub', 'AuthService', 'VisibilityService']
}
private subscriptionUnsubscriber?: () => void
private visibilityUnsubscribe?: () => void
private async handleResume(): Promise<void> {
// Reset retry count on successful resume
this.connectionRetryCount = 0
protected async onInitialize(): Promise<void> {
// Set up chat subscription and register with visibility service
await this.initializeMessageHandling()
this.registerWithVisibilityService()
// Check if we missed any messages while away
this.debug('Chat service fully initialized!')
}
private registerWithVisibilityService(): void {
if (!this.visibilityService) return
this.visibilityUnsubscribe = this.visibilityService.registerService(
this.metadata.name,
async () => this.handleAppResume(),
async () => this.handleAppPause()
)
}
/**
* STEP 1: App becomes visible again
* VisibilityService detects visibility change and calls this method
*/
private async handleAppResume(): Promise<void> {
this.debug('App resumed - checking chat connections')
// Check if our chat subscription is still active
if (!this.subscriptionUnsubscriber) {
this.debug('Chat subscription lost, re-establishing...')
this.setupMessageSubscription() // Recreate subscription
}
// Sync any messages missed while app was hidden
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()
}
/**
* STEP 2: Sync missed messages from the time we were away
*/
private async syncMissedMessages(): Promise<void> {
const lastSeenTimestamp = this.getLastSeenTimestamp()
const missedMessages = await this.fetchMessagesSince(lastSeenTimestamp)
try {
const peers = Array.from(this.peers.value.values())
const syncPromises = peers.map(peer => this.loadRecentMessagesForPeer(peer.pubkey))
await Promise.allSettled(syncPromises)
this.debug('Missed messages sync completed')
} catch (error) {
console.warn('Failed to sync missed messages:', error)
}
}
/**
* STEP 3: Load recent messages for each chat peer
*/
private async loadRecentMessagesForPeer(peerPubkey: string): Promise<void> {
const userPubkey = this.authService?.user?.value?.pubkey
if (!userPubkey || !this.relayHub) return
try {
// Get messages from the last hour (while we were away)
const oneHourAgo = Math.floor(Date.now() / 1000) - 3600
const events = await this.relayHub.queryEvents([
{
kinds: [4], // Encrypted DMs
authors: [peerPubkey],
'#p': [userPubkey],
since: oneHourAgo,
limit: 10
},
{
kinds: [4], // Encrypted DMs from us to them
authors: [userPubkey],
'#p': [peerPubkey],
since: oneHourAgo,
limit: 10
}
])
// Process any new messages we missed
for (const event of events) {
await this.processIncomingMessage(event)
}
} catch (error) {
this.debug(`Failed to load recent messages for peer ${peerPubkey.slice(0, 8)}:`, error)
}
}
/**
* STEP 4: Process messages (decrypt, filter market messages, add to chat)
*/
private async processIncomingMessage(event: any): Promise<void> {
try {
const userPubkey = this.authService?.user?.value?.pubkey
const userPrivkey = this.authService?.user?.value?.prvkey
if (!userPubkey || !userPrivkey) return
const senderPubkey = event.pubkey
const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content)
// Check if this is a market order (type 1 or 2)
let isMarketMessage = false
try {
const parsedContent = JSON.parse(decryptedContent)
if (parsedContent.type === 1 || parsedContent.type === 2) {
isMarketMessage = true
if (this.marketMessageHandler) {
await this.marketMessageHandler(event)
}
}
} catch (e) {
// Not JSON, treat as regular chat message
}
// Add to chat if it's not a market message
if (!isMarketMessage) {
const message: ChatMessage = {
id: event.id,
content: decryptedContent,
created_at: event.created_at,
sent: false,
pubkey: senderPubkey
}
this.addPeer(senderPubkey) // Ensure peer exists
this.addMessage(senderPubkey, message) // Add to chat history
console.log('💬 Recovered missed message from:', senderPubkey.slice(0, 8))
}
} catch (error) {
console.error('Failed to process recovered message:', error)
}
}
/**
* Battery-conscious pause behavior
*/
private async handleAppPause(): Promise<void> {
this.debug('App paused - chat subscription maintained for quick resume')
for (const message of missedMessages) {
this.processMessage(message)
// Don't immediately unsubscribe - RelayHub will handle connection management
// This allows for quick resume without full subscription recreation overhead
}
/**
* Cleanup when service is disposed
*/
protected async onDispose(): Promise<void> {
if (this.visibilityUnsubscribe) {
this.visibilityUnsubscribe()
}
if (this.subscriptionUnsubscriber) {
this.subscriptionUnsubscriber()
}
}
}
```
**What happens in this example:**
1. **🔍 Detection**: VisibilityService detects app became visible after 2+ minutes
2. **🔌 Connection Check**: ChatService checks if its subscription is still active
3. **📥 Message Recovery**: Queries for missed messages from all chat peers in the last hour
4. **🔓 Decryption**: Decrypts and processes each missed message
5. **📱 UI Update**: New messages appear in chat history as if they were never missed
6. **⚡ Real-time Resume**: Chat subscription is fully restored for new incoming messages
**The user experience**: Seamless. Messages that arrived while the app was backgrounded appear instantly when the app regains focus.
### Custom Service Example
```typescript
@ -569,6 +714,74 @@ private async handleAppResume(): Promise<void> {
}
```
#### WebSocket Connection Issues
```typescript
// Problem: WebSocket connections not recovering after app backgrounding
// Common on mobile browsers (iOS Safari, Chrome mobile)
// ❌ Incorrect - not integrating with VisibilityService
export class MyRealtimeService extends BaseService {
private ws: WebSocket | null = null
protected async onInitialize(): Promise<void> {
this.ws = new WebSocket('wss://example.com')
// Missing: visibility service registration
}
}
// ✅ Correct - proper WebSocket recovery integration
export class MyRealtimeService extends BaseService {
protected readonly metadata = {
name: 'MyRealtimeService',
dependencies: ['VisibilityService']
}
private ws: WebSocket | null = null
private visibilityUnsubscribe?: () => void
protected async onInitialize(): Promise<void> {
await this.connect()
this.registerWithVisibilityService() // ✅ Essential for mobile
}
private registerWithVisibilityService(): void {
if (!this.visibilityService) return
this.visibilityUnsubscribe = this.visibilityService.registerService(
this.metadata.name,
async () => this.handleAppResume(),
async () => this.handleAppPause()
)
}
private async handleAppResume(): Promise<void> {
// Check WebSocket connection health
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.debug('WebSocket disconnected, reconnecting...')
await this.connect()
}
// Restore any lost subscriptions
await this.restoreSubscriptions()
}
private async handleAppPause(): Promise<void> {
// Don't immediately close WebSocket
// Mobile browsers may suspend it anyway
this.debug('App paused - WebSocket will be checked on resume')
}
}
```
**Mobile WebSocket Behavior:**
- **iOS Safari**: Suspends WebSocket connections after ~30 seconds in background
- **Chrome Mobile**: May suspend connections when memory is needed
- **Desktop**: Generally maintains connections but may timeout after extended periods
- **PWA Standalone**: Better connection persistence but still subject to system limitations
**Solution**: Always integrate WebSocket services with VisibilityService for automatic recovery.
#### Memory Leaks
```typescript