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.
This commit is contained in:
parent
c7fcd51990
commit
8d4c389f71
4 changed files with 398 additions and 66 deletions
311
src/core/base/BaseService.ts
Normal file
311
src/core/base/BaseService.ts
Normal file
|
|
@ -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<void> {
|
||||
* // 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<boolean> = ref(false)
|
||||
public readonly isInitializing: Ref<boolean> = ref(false)
|
||||
public readonly initError: Ref<Error | null> = 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void>
|
||||
|
||||
/**
|
||||
* Cleanup method for service disposal
|
||||
*/
|
||||
public async dispose(): Promise<void> {
|
||||
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<void> {
|
||||
// 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<TDeps extends Record<string, any> = {}> extends BaseService {
|
||||
protected dependencies!: TDeps
|
||||
|
||||
/**
|
||||
* Get typed dependency
|
||||
*/
|
||||
protected getDependency<K extends keyof TDeps>(key: K): TDeps[K] {
|
||||
return this.dependencies[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject typed dependencies
|
||||
*/
|
||||
protected async injectTypedDependencies(tokens: Record<keyof TDeps, ServiceToken>): Promise<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import type { DIContainer, ServiceToken } from './types'
|
||||
|
||||
export type { ServiceToken } from './types'
|
||||
|
||||
interface ServiceRegistration {
|
||||
service: any
|
||||
scope: 'singleton' | 'transient'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue