Enhance ChatService initialization and authentication handling

- Introduce periodic authentication checks to ensure full initialization of the ChatService.
- Implement a flag to prevent redundant initialization.
- Update message handling and subscription methods to accommodate both injected and global authentication services.
- Improve error handling and logging for message subscription setup and processing.
- Clear authentication check intervals during service disposal to prevent memory leaks.

This commit improves the reliability and responsiveness of the chat service in handling user authentication and message subscriptions.
This commit is contained in:
padreug 2025-09-06 23:26:35 +02:00
parent 034f3ce80f
commit 4db7645a8f

View file

@ -22,6 +22,8 @@ export class ChatService extends BaseService {
private subscriptionUnsubscriber?: () => void private subscriptionUnsubscriber?: () => void
private marketMessageHandler?: (event: any) => Promise<void> private marketMessageHandler?: (event: any) => Promise<void>
private visibilityUnsubscribe?: () => void private visibilityUnsubscribe?: () => void
private isFullyInitialized = false
private authCheckInterval?: ReturnType<typeof setInterval>
constructor(config: ChatConfig) { constructor(config: ChatConfig) {
super() super()
@ -38,20 +40,58 @@ export class ChatService extends BaseService {
* Service-specific initialization (called by BaseService) * Service-specific initialization (called by BaseService)
*/ */
protected async onInitialize(): Promise<void> { protected async onInitialize(): Promise<void> {
// Check if we have user pubkey this.debug('Chat service onInitialize called')
if (!this.authService?.user?.value?.pubkey) {
this.debug('User not authenticated yet, deferring full initialization') // Check both injected auth service AND global auth composable
const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.pubkey
const hasGlobalAuth = auth.currentUser.value?.pubkey
this.debug('Auth detection:', {
hasAuthService: !!hasAuthService,
hasGlobalAuth: !!hasGlobalAuth,
authServicePubkey: hasAuthService ? hasAuthService.substring(0, 10) + '...' : null,
globalAuthPubkey: hasGlobalAuth ? hasGlobalAuth.substring(0, 10) + '...' : null
})
if (!hasAuthService && !hasGlobalAuth) {
this.debug('User not authenticated yet, deferring full initialization with periodic check')
// Listen for auth events to complete initialization when user logs in // Listen for auth events to complete initialization when user logs in
const unsubscribe = eventBus.on('auth:login', async () => { const unsubscribe = eventBus.on('auth:login', async () => {
this.debug('Auth login detected, completing chat initialization...') this.debug('Auth login detected, completing chat initialization...')
unsubscribe() unsubscribe()
if (this.authCheckInterval) {
clearInterval(this.authCheckInterval)
this.authCheckInterval = undefined
}
// Re-inject dependencies and complete initialization // Re-inject dependencies and complete initialization
await this.waitForDependencies() await this.waitForDependencies()
await this.completeInitialization() await this.completeInitialization()
}) })
// Also check periodically in case we missed the auth event
this.authCheckInterval = setInterval(async () => {
const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.pubkey
const hasGlobalAuth = auth.currentUser.value?.pubkey
if (hasAuthService || hasGlobalAuth) {
this.debug('Auth detected via periodic check, completing initialization')
if (this.authCheckInterval) {
clearInterval(this.authCheckInterval)
this.authCheckInterval = undefined
}
unsubscribe()
await this.waitForDependencies()
await this.completeInitialization()
}
}, 2000) // Check every 2 seconds
return return
} }
@ -62,6 +102,13 @@ export class ChatService extends BaseService {
* Complete the initialization once all dependencies are available * Complete the initialization once all dependencies are available
*/ */
private async completeInitialization(): Promise<void> { private async completeInitialization(): Promise<void> {
if (this.isFullyInitialized) {
this.debug('Chat service already fully initialized, skipping')
return
}
this.debug('Completing chat service initialization...')
// Load peers from storage first // Load peers from storage first
this.loadPeersFromStorage() this.loadPeersFromStorage()
@ -76,13 +123,14 @@ export class ChatService extends BaseService {
// Register with visibility service // Register with visibility service
this.registerWithVisibilityService() this.registerWithVisibilityService()
this.isFullyInitialized = true
this.debug('Chat service fully initialized and ready!') this.debug('Chat service fully initialized and ready!')
} }
// Initialize message handling (subscription + history loading) // Initialize message handling (subscription + history loading)
async initializeMessageHandling(): Promise<void> { async initializeMessageHandling(): Promise<void> {
// Set up real-time subscription // Set up real-time subscription
this.setupMessageSubscription() await this.setupMessageSubscription()
// Load message history for known peers // Load message history for known peers
await this.loadMessageHistory() await this.loadMessageHistory()
@ -198,14 +246,29 @@ export class ChatService extends BaseService {
// Refresh peers from API // Refresh peers from API
async refreshPeers(): Promise<void> { async refreshPeers(): Promise<void> {
// Check if we should trigger full initialization
const { auth } = await import('@/composables/useAuth')
const hasAuth = this.authService?.user?.value?.pubkey || auth.currentUser.value?.pubkey
if (!this.isFullyInitialized && hasAuth) {
console.log('💬 Refresh peers triggered full initialization')
await this.completeInitialization()
}
return this.loadPeersFromAPI() return this.loadPeersFromAPI()
} }
// Check if services are available for messaging // Check if services are available for messaging
private checkServicesAvailable(): { relayHub: any; authService: any } | null { private async checkServicesAvailable(): Promise<{ relayHub: any; authService: any; userPubkey: string; userPrivkey: string } | null> {
// Dependencies are already injected by BaseService // Check both injected auth service AND global auth composable
const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.prvkey
const hasGlobalAuth = auth.currentUser.value?.prvkey
if (!this.relayHub || !this.authService?.user?.value?.prvkey) { const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
return null return null
} }
@ -213,22 +276,24 @@ export class ChatService extends BaseService {
return null return null
} }
return { relayHub: this.relayHub, authService: this.authService } return {
relayHub: this.relayHub,
authService: this.authService || auth,
userPubkey: userPubkey!,
userPrivkey: userPrivkey!
}
} }
// Send a message // Send a message
async sendMessage(peerPubkey: string, content: string): Promise<void> { async sendMessage(peerPubkey: string, content: string): Promise<void> {
try { try {
const services = this.checkServicesAvailable() const services = await this.checkServicesAvailable()
if (!services) { if (!services) {
throw new Error('Chat services not ready. Please wait for connection to establish.') throw new Error('Chat services not ready. Please wait for connection to establish.')
} }
const { relayHub, authService } = services const { relayHub, userPrivkey, userPubkey } = services
const userPrivkey = authService.user.value.prvkey
const userPubkey = authService.user.value.pubkey
// Encrypt the message using NIP-04 // Encrypt the message using NIP-04
const encryptedContent = await nip04.encrypt(userPrivkey, peerPubkey, content) const encryptedContent = await nip04.encrypt(userPrivkey, peerPubkey, content)
@ -378,15 +443,23 @@ export class ChatService extends BaseService {
// Load message history for known peers // Load message history for known peers
private async loadMessageHistory(): Promise<void> { private async loadMessageHistory(): Promise<void> {
try { try {
// Dependencies are already injected by BaseService // Check both injected auth service AND global auth composable
const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.pubkey
const hasGlobalAuth = auth.currentUser.value?.pubkey
if (!this.relayHub || !this.authService?.user?.value?.pubkey) { const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
console.warn('Cannot load message history: missing services') console.warn('Cannot load message history: missing services')
return return
} }
const userPubkey = this.authService.user.value.pubkey if (!userPubkey || !userPrivkey) {
const userPrivkey = this.authService.user.value.prvkey console.warn('Cannot load message history: missing user keys')
return
}
const peerPubkeys = Array.from(this.peers.value.keys()) const peerPubkeys = Array.from(this.peers.value.keys())
if (peerPubkeys.length === 0) { if (peerPubkeys.length === 0) {
@ -459,12 +532,26 @@ export class ChatService extends BaseService {
} }
// Setup subscription for incoming messages // Setup subscription for incoming messages
private setupMessageSubscription(): void { private async setupMessageSubscription(): Promise<void> {
try { try {
// Dependencies are already injected by BaseService // Check both injected auth service AND global auth composable
const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.pubkey
const hasGlobalAuth = auth.currentUser.value?.pubkey
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
if (!this.relayHub || !this.authService?.user?.value?.pubkey) { this.debug('Setup message subscription auth check:', {
hasAuthService: !!hasAuthService,
hasGlobalAuth: !!hasGlobalAuth,
hasRelayHub: !!this.relayHub,
relayHubConnected: this.relayHub?.isConnected,
userPubkey: userPubkey ? userPubkey.substring(0, 10) + '...' : null
})
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
console.warn('💬 Cannot setup message subscription: missing services') console.warn('💬 Cannot setup message subscription: missing services')
// Retry after 2 seconds
setTimeout(() => this.setupMessageSubscription(), 2000)
return return
} }
@ -475,10 +562,16 @@ export class ChatService extends BaseService {
console.log('💬 RelayHub connected, setting up message subscription...') console.log('💬 RelayHub connected, setting up message subscription...')
this.setupMessageSubscription() this.setupMessageSubscription()
}) })
// Also retry after timeout in case event is missed
setTimeout(() => this.setupMessageSubscription(), 5000)
return return
} }
const userPubkey = this.authService.user.value.pubkey if (!userPubkey) {
console.warn('💬 No user pubkey available for subscription')
setTimeout(() => this.setupMessageSubscription(), 2000)
return
}
// Subscribe to encrypted direct messages (kind 4) addressed to this user // Subscribe to encrypted direct messages (kind 4) addressed to this user
this.subscriptionUnsubscriber = this.relayHub.subscribe({ this.subscriptionUnsubscriber = this.relayHub.subscribe({
@ -498,14 +591,16 @@ export class ChatService extends BaseService {
await this.processIncomingMessage(event) await this.processIncomingMessage(event)
}, },
onEose: () => { onEose: () => {
console.log('Chat message subscription EOSE received') console.log('💬 Chat message subscription EOSE received')
} }
}) })
console.log('Chat message subscription set up successfully') console.log('💬 Chat message subscription set up successfully for pubkey:', userPubkey.substring(0, 10) + '...')
} catch (error) { } catch (error) {
console.error('Failed to setup message subscription:', error) console.error('💬 Failed to setup message subscription:', error)
// Retry after delay
setTimeout(() => this.setupMessageSubscription(), 3000)
} }
} }
@ -575,8 +670,13 @@ export class ChatService extends BaseService {
*/ */
private async processIncomingMessage(event: any): Promise<void> { private async processIncomingMessage(event: any): Promise<void> {
try { try {
const userPubkey = this.authService?.user?.value?.pubkey // Check both injected auth service AND global auth composable
const userPrivkey = this.authService?.user?.value?.prvkey const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.pubkey
const hasGlobalAuth = auth.currentUser.value?.pubkey
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey
if (!userPubkey || !userPrivkey) { if (!userPubkey || !userPrivkey) {
console.warn('Cannot process message: user not authenticated') console.warn('Cannot process message: user not authenticated')
@ -637,7 +737,13 @@ export class ChatService extends BaseService {
* Load recent messages for a specific peer * Load recent messages for a specific peer
*/ */
private async loadRecentMessagesForPeer(peerPubkey: string): Promise<void> { private async loadRecentMessagesForPeer(peerPubkey: string): Promise<void> {
const userPubkey = this.authService?.user?.value?.pubkey // Check both injected auth service AND global auth composable
const { auth } = await import('@/composables/useAuth')
const hasAuthService = this.authService?.user?.value?.pubkey
const hasGlobalAuth = auth.currentUser.value?.pubkey
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
if (!userPubkey || !this.relayHub) return if (!userPubkey || !this.relayHub) return
try { try {
@ -675,6 +781,12 @@ export class ChatService extends BaseService {
* Cleanup when service is disposed (overrides BaseService) * Cleanup when service is disposed (overrides BaseService)
*/ */
protected async onDispose(): Promise<void> { protected async onDispose(): Promise<void> {
// Clear auth check interval
if (this.authCheckInterval) {
clearInterval(this.authCheckInterval)
this.authCheckInterval = undefined
}
// Unregister from visibility service // Unregister from visibility service
if (this.visibilityUnsubscribe) { if (this.visibilityUnsubscribe) {
this.visibilityUnsubscribe() this.visibilityUnsubscribe()
@ -689,6 +801,7 @@ export class ChatService extends BaseService {
this.messages.value.clear() this.messages.value.clear()
this.peers.value.clear() this.peers.value.clear()
this.isFullyInitialized = false
this.debug('Chat service disposed') this.debug('Chat service disposed')
} }