From 8d4c389f71156b86c2638fc33fe2a0060c3da211 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 5 Sep 2025 06:19:08 +0200 Subject: [PATCH] Add BaseService and refactor ChatService for improved dependency management - Introduce BaseService as a foundational class for services, providing standardized dependency injection and initialization logic. - Refactor ChatService to extend BaseService, enhancing its initialization process and dependency handling. - Implement service metadata and structured initialization in ChatService, allowing for better tracking and error handling during service setup. - Update chat module to initialize ChatService with dependency management, ensuring readiness before use. --- src/core/base/BaseService.ts | 311 ++++++++++++++++++++++ src/core/di-container.ts | 2 + src/modules/chat/index.ts | 9 + src/modules/chat/services/chat-service.ts | 142 +++++----- 4 files changed, 398 insertions(+), 66 deletions(-) create mode 100644 src/core/base/BaseService.ts diff --git a/src/core/base/BaseService.ts b/src/core/base/BaseService.ts new file mode 100644 index 0000000..419db33 --- /dev/null +++ b/src/core/base/BaseService.ts @@ -0,0 +1,311 @@ +import { tryInjectService, SERVICE_TOKENS, type ServiceToken } from '@/core/di-container' +import { eventBus } from '@/core/event-bus' +import type { Ref } from 'vue' +import { ref } from 'vue' + +/** + * Service metadata for tracking and debugging + */ +export interface ServiceMetadata { + name: string + version?: string + dependencies?: string[] +} + +/** + * Service initialization options + */ +export interface ServiceInitOptions { + waitForDependencies?: boolean + maxRetries?: number + retryDelay?: number +} + +/** + * Base class for all services in the modular architecture + * Provides standardized dependency injection, initialization, and error handling + * + * @example + * ```typescript + * export class MyService extends BaseService { + * protected metadata = { + * name: 'MyService', + * dependencies: ['RelayHub', 'AuthService'] + * } + * + * protected async onInitialize(): Promise { + * // Service-specific initialization + * await this.setupSubscriptions() + * } + * } + * ``` + */ +export abstract class BaseService { + // Core dependencies with proper typing + protected relayHub: any = null + protected authService: any = null + protected nostrClientHub: any = null + + // Service state + public readonly isInitialized: Ref = ref(false) + public readonly isInitializing: Ref = ref(false) + public readonly initError: Ref = ref(null) + + // Service metadata + protected abstract readonly metadata: ServiceMetadata + + constructor() { + // Dependencies will be injected after construction + } + + /** + * Initialize the service with dependency injection and error handling + */ + public async initialize(options: ServiceInitOptions = {}): Promise { + const { + waitForDependencies = true, + maxRetries = 3, + retryDelay = 1000 + } = options + + if (this.isInitialized.value) { + console.log(`✅ ${this.metadata.name} already initialized`) + return + } + + if (this.isInitializing.value) { + console.log(`⏳ ${this.metadata.name} is already initializing...`) + return + } + + this.isInitializing.value = true + this.initError.value = null + + try { + console.log(`🚀 Initializing ${this.metadata.name}...`) + + // Inject dependencies + await this.injectDependencies(waitForDependencies, maxRetries, retryDelay) + + // Call service-specific initialization + await this.onInitialize() + + this.isInitialized.value = true + console.log(`✅ ${this.metadata.name} initialized successfully`) + + // Emit initialization event + eventBus.emit(`service:initialized`, { + service: this.metadata.name, + timestamp: Date.now() + }, this.metadata.name) + + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)) + this.initError.value = err + + console.error(`❌ Failed to initialize ${this.metadata.name}:`, err) + + // Emit error event + eventBus.emit(`service:error`, { + service: this.metadata.name, + error: err.message, + timestamp: Date.now() + }, this.metadata.name) + + throw err + } finally { + this.isInitializing.value = false + } + } + + /** + * Inject required dependencies with retry logic + */ + protected async injectDependencies( + waitForDependencies: boolean, + maxRetries: number, + retryDelay: number + ): Promise { + let retries = 0 + + while (retries < maxRetries) { + try { + // Try to inject core dependencies + this.relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB) + this.authService = tryInjectService(SERVICE_TOKENS.AUTH_SERVICE) + this.nostrClientHub = tryInjectService(SERVICE_TOKENS.NOSTR_CLIENT_HUB) + + // Check if all required dependencies are available + const missingDeps = this.getMissingDependencies() + + if (missingDeps.length === 0) { + console.log(`✅ All dependencies injected for ${this.metadata.name}`) + return + } + + if (!waitForDependencies) { + console.warn(`⚠️ ${this.metadata.name} starting with missing dependencies:`, missingDeps) + return + } + + if (retries < maxRetries - 1) { + console.log(`⏳ Waiting for dependencies (${missingDeps.join(', ')})... Retry ${retries + 1}/${maxRetries}`) + await new Promise(resolve => setTimeout(resolve, retryDelay)) + retries++ + } else { + throw new Error(`Missing required dependencies: ${missingDeps.join(', ')}`) + } + + } catch (error) { + if (retries >= maxRetries - 1) { + throw error + } + retries++ + } + } + } + + /** + * Get list of missing dependencies + */ + protected getMissingDependencies(): string[] { + const missing: string[] = [] + const deps = this.metadata.dependencies || [] + + if (deps.includes('RelayHub') && !this.relayHub) { + missing.push('RelayHub') + } + if (deps.includes('AuthService') && !this.authService) { + missing.push('AuthService') + } + if (deps.includes('NostrClientHub') && !this.nostrClientHub) { + missing.push('NostrClientHub') + } + + return missing + } + + /** + * Wait for specific dependencies to be available + */ + protected async waitForDependencies(timeout = 10000): Promise { + const startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const missingDeps = this.getMissingDependencies() + + if (missingDeps.length === 0) { + return + } + + await new Promise(resolve => setTimeout(resolve, 100)) + } + + throw new Error(`Timeout waiting for dependencies: ${this.getMissingDependencies().join(', ')}`) + } + + /** + * Check if user is authenticated (helper method) + */ + protected requireAuth(): void { + if (!this.authService?.isAuthenticated?.value) { + throw new Error('Authentication required') + } + } + + /** + * Get current user pubkey (helper method) + */ + protected getUserPubkey(): string | undefined { + return this.authService?.user?.value?.pubkey + } + + /** + * Handle errors consistently across all services + */ + protected handleError(error: unknown, context: string): Error { + const err = error instanceof Error ? error : new Error(String(error)) + + console.error(`[${this.metadata.name}] Error in ${context}:`, err) + + // Emit error event for monitoring + eventBus.emit('service:error', { + service: this.metadata.name, + context, + error: err.message, + timestamp: Date.now() + }, this.metadata.name) + + return err + } + + /** + * Log debug information (only in development) + */ + protected debug(...args: any[]): void { + if (import.meta.env.DEV) { + console.log(`[${this.metadata.name}]`, ...args) + } + } + + /** + * Abstract method for service-specific initialization + * Must be implemented by derived classes + */ + protected abstract onInitialize(): Promise + + /** + * Cleanup method for service disposal + */ + public async dispose(): Promise { + try { + await this.onDispose() + + this.isInitialized.value = false + this.relayHub = null + this.authService = null + this.nostrClientHub = null + + console.log(`♻️ ${this.metadata.name} disposed`) + + } catch (error) { + console.error(`Failed to dispose ${this.metadata.name}:`, error) + } + } + + /** + * Optional cleanup logic for derived classes + */ + protected async onDispose(): Promise { + // Override in derived classes if cleanup is needed + } +} + +/** + * Type-safe service class with proper dependency typing + * Use this when you need full type safety for dependencies + */ +export abstract class TypedBaseService = {}> extends BaseService { + protected dependencies!: TDeps + + /** + * Get typed dependency + */ + protected getDependency(key: K): TDeps[K] { + return this.dependencies[key] + } + + /** + * Inject typed dependencies + */ + protected async injectTypedDependencies(tokens: Record): Promise { + this.dependencies = {} as TDeps + + for (const [key, token] of Object.entries(tokens)) { + const service = tryInjectService(token as ServiceToken) + if (service) { + (this.dependencies as any)[key] = service + } + } + } +} \ No newline at end of file diff --git a/src/core/di-container.ts b/src/core/di-container.ts index c28d2d1..ff5a625 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -1,5 +1,7 @@ import type { DIContainer, ServiceToken } from './types' +export type { ServiceToken } from './types' + interface ServiceRegistration { service: any scope: 'singleton' | 'transient' diff --git a/src/modules/chat/index.ts b/src/modules/chat/index.ts index 6c475c5..0cbd173 100644 --- a/src/modules/chat/index.ts +++ b/src/modules/chat/index.ts @@ -35,6 +35,15 @@ export const chatModule: ModulePlugin = { const chatService = new ChatService(config) container.provide(CHAT_SERVICE_TOKEN, chatService) + // Initialize the service (will handle dependency injection) + await chatService.initialize({ + waitForDependencies: true, + maxRetries: 3 + }).catch(error => { + console.warn('💬 Chat service initialization deferred:', error) + // Service will auto-initialize when dependencies are available + }) + // Also make it globally available for other modules (like market) ;(globalThis as any).chatService = chatService diff --git a/src/modules/chat/services/chat-service.ts b/src/modules/chat/services/chat-service.ts index 8f21a92..4457a8d 100644 --- a/src/modules/chat/services/chat-service.ts +++ b/src/modules/chat/services/chat-service.ts @@ -1,6 +1,6 @@ import { ref, computed } from 'vue' import { eventBus } from '@/core/event-bus' -import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import { BaseService } from '@/core/base/BaseService' import { nip04, finalizeEvent, type Event, type EventTemplate } from 'nostr-tools' import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types' import { getAuthToken } from '@/lib/config/lnbits' @@ -9,20 +9,25 @@ import { config } from '@/lib/config' const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages' const PEERS_KEY = 'nostr-chat-peers' -export class ChatService { +export class ChatService extends BaseService { + // Service metadata + protected readonly metadata = { + name: 'ChatService', + version: '1.0.0', + dependencies: ['RelayHub', 'AuthService'] + } + + // Service-specific state private messages = ref>(new Map()) private peers = ref>(new Map()) private config: ChatConfig private subscriptionUnsubscriber?: () => void - private isInitialized = ref(false) private marketMessageHandler?: (event: any) => Promise constructor(config: ChatConfig) { + super() this.config = config this.loadPeersFromStorage() - - // Defer initialization until services are available - this.deferredInitialization() } // Register market message handler for forwarding market-related DMs @@ -30,47 +35,43 @@ export class ChatService { this.marketMessageHandler = handler } - // Defer initialization until services are ready - private deferredInitialization(): void { - // Try initialization immediately - this.tryInitialization() - - // Also listen for auth events to re-initialize when user logs in - eventBus.on('auth:login', () => { - console.log('💬 Auth login detected, initializing chat...') - this.tryInitialization() - }) - } - - // Try to initialize services if they're available - private async tryInitialization(): Promise { - try { - const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any - const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + /** + * Service-specific initialization (called by BaseService) + */ + protected async onInitialize(): Promise { + // Check if we have user pubkey + if (!this.authService?.user?.value?.pubkey) { + this.debug('User not authenticated yet, deferring full initialization') - if (!relayHub || !authService?.user?.value?.pubkey) { - console.log('💬 Services not ready yet, will retry when auth completes...') - return - } - - console.log('💬 Services ready, initializing chat functionality...') - - // Load peers from API - await this.loadPeersFromAPI().catch(error => { - console.warn('Failed to load peers from API:', error) + // Listen for auth events to complete initialization when user logs in + const unsubscribe = eventBus.on('auth:login', async () => { + this.debug('Auth login detected, completing chat initialization...') + unsubscribe() + + // Re-inject dependencies and complete initialization + await this.waitForDependencies() + await this.completeInitialization() }) - // Initialize message handling (subscription + history loading) - await this.initializeMessageHandling() - - // Mark as initialized - this.isInitialized.value = true - console.log('💬 Chat service fully initialized and ready!') - - } catch (error) { - console.error('💬 Failed to initialize chat:', error) - this.isInitialized.value = false + return } + + await this.completeInitialization() + } + + /** + * Complete the initialization once all dependencies are available + */ + private async completeInitialization(): Promise { + // Load peers from API + await this.loadPeersFromAPI().catch(error => { + console.warn('Failed to load peers from API:', error) + }) + + // Initialize message handling (subscription + history loading) + await this.initializeMessageHandling() + + this.debug('Chat service fully initialized and ready!') } // Initialize message handling (subscription + history loading) @@ -95,7 +96,7 @@ export class ChatService { } get isReady() { - return computed(() => this.isInitialized.value) + return this.isInitialized } // Get messages for a specific peer @@ -197,18 +198,17 @@ export class ChatService { // Check if services are available for messaging private checkServicesAvailable(): { relayHub: any; authService: any } | null { - const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any - const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + // Dependencies are already injected by BaseService - if (!relayHub || !authService?.user?.value?.prvkey) { + if (!this.relayHub || !this.authService?.user?.value?.prvkey) { return null } - if (!relayHub.isConnected) { + if (!this.relayHub.isConnected) { return null } - return { relayHub, authService } + return { relayHub: this.relayHub, authService: this.authService } } // Send a message @@ -390,16 +390,15 @@ export class ChatService { // Load message history for known peers private async loadMessageHistory(): Promise { try { - const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any - const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + // Dependencies are already injected by BaseService - if (!relayHub || !authService?.user?.value?.pubkey) { + if (!this.relayHub || !this.authService?.user?.value?.pubkey) { console.warn('Cannot load message history: missing services') return } - const userPubkey = authService.user.value.pubkey - const userPrivkey = authService.user.value.prvkey + const userPubkey = this.authService.user.value.pubkey + const userPrivkey = this.authService.user.value.prvkey const peerPubkeys = Array.from(this.peers.value.keys()) if (peerPubkeys.length === 0) { @@ -411,7 +410,7 @@ export class ChatService { // Query historical messages (kind 4) to/from known peers // We need separate queries for sent vs received messages due to different tagging - const receivedEvents = await relayHub.queryEvents([ + const receivedEvents = await this.relayHub.queryEvents([ { kinds: [4], authors: peerPubkeys, // Messages from peers @@ -420,7 +419,7 @@ export class ChatService { } ]) - const sentEvents = await relayHub.queryEvents([ + const sentEvents = await this.relayHub.queryEvents([ { kinds: [4], authors: [userPubkey], // Messages from us @@ -474,29 +473,28 @@ export class ChatService { // Setup subscription for incoming messages private setupMessageSubscription(): void { try { - const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any - const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + // Dependencies are already injected by BaseService - if (!relayHub || !authService?.user?.value?.pubkey) { + if (!this.relayHub || !this.authService?.user?.value?.pubkey) { console.warn('💬 Cannot setup message subscription: missing services') return } - if (!relayHub.isConnected) { + if (!this.relayHub.isConnected) { console.warn('💬 RelayHub not connected, waiting for connection...') // Listen for connection event - relayHub.on('connected', () => { + this.relayHub.on('connected', () => { console.log('💬 RelayHub connected, setting up message subscription...') this.setupMessageSubscription() }) return } - const userPubkey = authService.user.value.pubkey - const userPrivkey = authService.user.value.prvkey + const userPubkey = this.authService.user.value.pubkey + const userPrivkey = this.authService.user.value.prvkey // Subscribe to encrypted direct messages (kind 4) addressed to this user - this.subscriptionUnsubscriber = relayHub.subscribe({ + this.subscriptionUnsubscriber = this.relayHub.subscribe({ id: 'chat-messages', filters: [ { @@ -573,14 +571,26 @@ export class ChatService { } } - // Cleanup - destroy(): void { + /** + * Cleanup when service is disposed (overrides BaseService) + */ + protected async onDispose(): Promise { // Unsubscribe from message subscription if (this.subscriptionUnsubscriber) { this.subscriptionUnsubscriber() + this.subscriptionUnsubscriber = undefined } this.messages.value.clear() this.peers.value.clear() + + this.debug('Chat service disposed') + } + + /** + * Legacy destroy method for backward compatibility + */ + destroy(): void { + this.dispose() } } \ No newline at end of file