Implement modular architecture with core services and Nostr integration
- Introduce a modular application structure with a new app configuration file to manage module settings and features. - Implement a dependency injection container for service management across modules. - Create a plugin manager to handle module registration, installation, and lifecycle management. - Develop a global event bus for inter-module communication, enhancing loose coupling between components. - Add core modules including base functionalities, Nostr feed, and PWA services, with support for dynamic loading and configuration. - Establish a Nostr client hub for managing WebSocket connections and event handling. - Enhance user experience with a responsive Nostr feed component, integrating admin announcements and community posts. - Refactor existing components to align with the new modular architecture, improving maintainability and scalability.
This commit is contained in:
parent
2d8215a35e
commit
519a9003d4
16 changed files with 2520 additions and 14 deletions
137
src/core/di-container.ts
Normal file
137
src/core/di-container.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import type { DIContainer, ServiceToken } from './types'
|
||||
|
||||
interface ServiceRegistration {
|
||||
service: any
|
||||
scope: 'singleton' | 'transient'
|
||||
instance?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency Injection Container
|
||||
* Manages service registration and injection across modules
|
||||
*/
|
||||
export class DIContainerImpl implements DIContainer {
|
||||
private services = new Map<ServiceToken, ServiceRegistration>()
|
||||
|
||||
/**
|
||||
* Register a service in the container
|
||||
*/
|
||||
provide<T>(token: ServiceToken, service: T, scope: 'singleton' | 'transient' = 'singleton'): void {
|
||||
this.services.set(token, {
|
||||
service,
|
||||
scope,
|
||||
instance: scope === 'singleton' ? service : undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a service from the container
|
||||
*/
|
||||
inject<T>(token: ServiceToken): T | undefined {
|
||||
const registration = this.services.get(token)
|
||||
|
||||
if (!registration) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (registration.scope === 'singleton') {
|
||||
return registration.instance as T
|
||||
}
|
||||
|
||||
// For transient services, create new instance
|
||||
// Note: This assumes the service is a constructor function
|
||||
if (typeof registration.service === 'function') {
|
||||
try {
|
||||
return new registration.service() as T
|
||||
} catch (error) {
|
||||
console.error(`Error creating transient service for token ${String(token)}:`, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return registration.service as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a service from the container
|
||||
*/
|
||||
remove(token: ServiceToken): boolean {
|
||||
return this.services.delete(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all services
|
||||
*/
|
||||
clear(): void {
|
||||
this.services.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered service tokens
|
||||
*/
|
||||
getRegisteredTokens(): ServiceToken[] {
|
||||
return Array.from(this.services.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a service is registered
|
||||
*/
|
||||
has(token: ServiceToken): boolean {
|
||||
return this.services.has(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service registration info
|
||||
*/
|
||||
getServiceInfo(token: ServiceToken): { scope: string; hasInstance: boolean } | undefined {
|
||||
const registration = this.services.get(token)
|
||||
if (!registration) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
scope: registration.scope,
|
||||
hasInstance: registration.instance !== undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global DI container instance
|
||||
export const container = new DIContainerImpl()
|
||||
|
||||
// Service token constants
|
||||
export const SERVICE_TOKENS = {
|
||||
// Core services
|
||||
EVENT_BUS: Symbol('eventBus'),
|
||||
ROUTER: Symbol('router'),
|
||||
|
||||
// Nostr services
|
||||
RELAY_HUB: Symbol('relayHub'),
|
||||
NOSTR_CLIENT_HUB: Symbol('nostrClientHub'),
|
||||
|
||||
// Auth services
|
||||
AUTH_SERVICE: Symbol('authService'),
|
||||
|
||||
// Market services
|
||||
MARKET_STORE: Symbol('marketStore'),
|
||||
PAYMENT_MONITOR: Symbol('paymentMonitor'),
|
||||
|
||||
// Chat services
|
||||
CHAT_SERVICE: Symbol('chatService'),
|
||||
|
||||
// Events services
|
||||
EVENTS_SERVICE: Symbol('eventsService'),
|
||||
} as const
|
||||
|
||||
// Type-safe injection helpers
|
||||
export function injectService<T>(token: ServiceToken): T {
|
||||
const service = container.inject<T>(token)
|
||||
if (!service) {
|
||||
throw new Error(`Service not found for token: ${String(token)}`)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
export function tryInjectService<T>(token: ServiceToken): T | undefined {
|
||||
return container.inject<T>(token)
|
||||
}
|
||||
124
src/core/event-bus.ts
Normal file
124
src/core/event-bus.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import type { ModuleEvent, ModuleEventHandler } from './types'
|
||||
|
||||
/**
|
||||
* Global event bus for inter-module communication
|
||||
* Provides loose coupling between modules via event-driven architecture
|
||||
*/
|
||||
export class ModuleEventBus {
|
||||
private listeners = new Map<string, Set<ModuleEventHandler>>()
|
||||
private eventHistory: ModuleEvent[] = []
|
||||
private maxHistorySize = 1000
|
||||
|
||||
/**
|
||||
* Emit an event to all registered listeners
|
||||
*/
|
||||
emit(type: string, data: any, source = 'unknown'): void {
|
||||
const event: ModuleEvent = {
|
||||
type,
|
||||
source,
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
// Store in history
|
||||
this.eventHistory.push(event)
|
||||
if (this.eventHistory.length > this.maxHistorySize) {
|
||||
this.eventHistory.shift()
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
const listeners = this.listeners.get(type)
|
||||
if (listeners) {
|
||||
listeners.forEach(handler => {
|
||||
try {
|
||||
handler(event)
|
||||
} catch (error) {
|
||||
console.error(`Error in event handler for ${type}:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Also notify wildcard listeners
|
||||
const wildcardListeners = this.listeners.get('*')
|
||||
if (wildcardListeners) {
|
||||
wildcardListeners.forEach(handler => {
|
||||
try {
|
||||
handler(event)
|
||||
} catch (error) {
|
||||
console.error(`Error in wildcard event handler:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event listener
|
||||
*/
|
||||
on(type: string, handler: ModuleEventHandler): () => void {
|
||||
if (!this.listeners.has(type)) {
|
||||
this.listeners.set(type, new Set())
|
||||
}
|
||||
|
||||
this.listeners.get(type)!.add(handler)
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => this.off(type, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener
|
||||
*/
|
||||
off(type: string, handler: ModuleEventHandler): void {
|
||||
const listeners = this.listeners.get(type)
|
||||
if (listeners) {
|
||||
listeners.delete(handler)
|
||||
if (listeners.size === 0) {
|
||||
this.listeners.delete(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all listeners for a given event type
|
||||
*/
|
||||
removeAllListeners(type: string): void {
|
||||
this.listeners.delete(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all listeners
|
||||
*/
|
||||
clear(): void {
|
||||
this.listeners.clear()
|
||||
this.eventHistory = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent events (for debugging/monitoring)
|
||||
*/
|
||||
getEventHistory(count = 50): ModuleEvent[] {
|
||||
return this.eventHistory.slice(-count)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered event types
|
||||
*/
|
||||
getEventTypes(): string[] {
|
||||
return Array.from(this.listeners.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get listener count for an event type
|
||||
*/
|
||||
getListenerCount(type: string): number {
|
||||
return this.listeners.get(type)?.size || 0
|
||||
}
|
||||
}
|
||||
|
||||
// Global event bus instance
|
||||
export const eventBus = new ModuleEventBus()
|
||||
|
||||
// Convenience functions
|
||||
export const emit = eventBus.emit.bind(eventBus)
|
||||
export const on = eventBus.on.bind(eventBus)
|
||||
export const off = eventBus.off.bind(eventBus)
|
||||
329
src/core/plugin-manager.ts
Normal file
329
src/core/plugin-manager.ts
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
import type { App } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
import type { ModulePlugin, ModuleConfig, ModuleRegistration } from './types'
|
||||
import { eventBus } from './event-bus'
|
||||
import { container } from './di-container'
|
||||
|
||||
/**
|
||||
* Plugin Manager
|
||||
* Handles module registration, dependency resolution, and lifecycle management
|
||||
*/
|
||||
export class PluginManager {
|
||||
private modules = new Map<string, ModuleRegistration>()
|
||||
private app: App | null = null
|
||||
private router: Router | null = null
|
||||
private installOrder: string[] = []
|
||||
|
||||
/**
|
||||
* Initialize the plugin manager
|
||||
*/
|
||||
init(app: App, router: Router): void {
|
||||
this.app = app
|
||||
this.router = router
|
||||
|
||||
// Register core services
|
||||
container.provide('app', app)
|
||||
container.provide('router', router)
|
||||
container.provide('eventBus', eventBus)
|
||||
|
||||
console.log('🔧 Plugin Manager initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module plugin
|
||||
*/
|
||||
async register(plugin: ModulePlugin, config: ModuleConfig): Promise<void> {
|
||||
if (this.modules.has(plugin.name)) {
|
||||
throw new Error(`Module ${plugin.name} is already registered`)
|
||||
}
|
||||
|
||||
// Validate dependencies
|
||||
const missingDeps = this.validateDependencies(plugin)
|
||||
if (missingDeps.length > 0) {
|
||||
throw new Error(`Module ${plugin.name} has missing dependencies: ${missingDeps.join(', ')}`)
|
||||
}
|
||||
|
||||
// Register the module
|
||||
const registration: ModuleRegistration = {
|
||||
plugin,
|
||||
config,
|
||||
installed: false
|
||||
}
|
||||
|
||||
this.modules.set(plugin.name, registration)
|
||||
console.log(`📦 Registered module: ${plugin.name} v${plugin.version}`)
|
||||
|
||||
// Auto-install if enabled and not lazy
|
||||
if (config.enabled && !config.lazy) {
|
||||
await this.install(plugin.name)
|
||||
}
|
||||
|
||||
eventBus.emit('module:registered', { name: plugin.name, config }, 'plugin-manager')
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a module
|
||||
*/
|
||||
async install(moduleName: string): Promise<void> {
|
||||
const registration = this.modules.get(moduleName)
|
||||
if (!registration) {
|
||||
throw new Error(`Module ${moduleName} is not registered`)
|
||||
}
|
||||
|
||||
if (registration.installed) {
|
||||
console.warn(`Module ${moduleName} is already installed`)
|
||||
return
|
||||
}
|
||||
|
||||
const { plugin, config } = registration
|
||||
|
||||
// Install dependencies first
|
||||
if (plugin.dependencies) {
|
||||
for (const dep of plugin.dependencies) {
|
||||
const depRegistration = this.modules.get(dep)
|
||||
if (!depRegistration) {
|
||||
throw new Error(`Dependency ${dep} is not registered`)
|
||||
}
|
||||
if (!depRegistration.installed) {
|
||||
await this.install(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Install the module
|
||||
if (!this.app) {
|
||||
throw new Error('Plugin manager not initialized')
|
||||
}
|
||||
|
||||
await plugin.install(this.app, config.config)
|
||||
|
||||
// Register routes if provided
|
||||
if (plugin.routes && this.router) {
|
||||
for (const route of plugin.routes) {
|
||||
this.router.addRoute(route)
|
||||
}
|
||||
}
|
||||
|
||||
// Register services in DI container
|
||||
if (plugin.services) {
|
||||
for (const [name, service] of Object.entries(plugin.services)) {
|
||||
container.provide(Symbol(name), service)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as installed
|
||||
registration.installed = true
|
||||
registration.installTime = Date.now()
|
||||
this.installOrder.push(moduleName)
|
||||
|
||||
console.log(`✅ Installed module: ${moduleName}`)
|
||||
eventBus.emit('module:installed', { name: moduleName, plugin, config }, 'plugin-manager')
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to install module ${moduleName}:`, error)
|
||||
eventBus.emit('module:install-failed', { name: moduleName, error }, 'plugin-manager')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall a module
|
||||
*/
|
||||
async uninstall(moduleName: string): Promise<void> {
|
||||
const registration = this.modules.get(moduleName)
|
||||
if (!registration) {
|
||||
throw new Error(`Module ${moduleName} is not registered`)
|
||||
}
|
||||
|
||||
if (!registration.installed) {
|
||||
console.warn(`Module ${moduleName} is not installed`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for dependents
|
||||
const dependents = this.findDependents(moduleName)
|
||||
if (dependents.length > 0) {
|
||||
throw new Error(`Cannot uninstall ${moduleName}: required by ${dependents.join(', ')}`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Call module's uninstall hook
|
||||
if (registration.plugin.uninstall) {
|
||||
await registration.plugin.uninstall()
|
||||
}
|
||||
|
||||
// Remove routes
|
||||
if (registration.plugin.routes && this.router) {
|
||||
// Note: Vue Router doesn't have removeRoute, so we'd need to track and rebuild
|
||||
// For now, we'll just log this limitation
|
||||
console.warn(`Routes from ${moduleName} cannot be removed (Vue Router limitation)`)
|
||||
}
|
||||
|
||||
// Remove services from DI container
|
||||
if (registration.plugin.services) {
|
||||
for (const name of Object.keys(registration.plugin.services)) {
|
||||
container.remove(Symbol(name))
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as uninstalled
|
||||
registration.installed = false
|
||||
registration.installTime = undefined
|
||||
|
||||
const orderIndex = this.installOrder.indexOf(moduleName)
|
||||
if (orderIndex !== -1) {
|
||||
this.installOrder.splice(orderIndex, 1)
|
||||
}
|
||||
|
||||
console.log(`🗑️ Uninstalled module: ${moduleName}`)
|
||||
eventBus.emit('module:uninstalled', { name: moduleName }, 'plugin-manager')
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to uninstall module ${moduleName}:`, error)
|
||||
eventBus.emit('module:uninstall-failed', { name: moduleName, error }, 'plugin-manager')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module registration info
|
||||
*/
|
||||
getModule(name: string): ModuleRegistration | undefined {
|
||||
return this.modules.get(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered modules
|
||||
*/
|
||||
getModules(): Map<string, ModuleRegistration> {
|
||||
return new Map(this.modules)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installed modules in installation order
|
||||
*/
|
||||
getInstalledModules(): string[] {
|
||||
return [...this.installOrder]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is installed
|
||||
*/
|
||||
isInstalled(name: string): boolean {
|
||||
return this.modules.get(name)?.installed || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module status
|
||||
*/
|
||||
getStatus(): {
|
||||
registered: number
|
||||
installed: number
|
||||
failed: number
|
||||
modules: Array<{ name: string; version: string; installed: boolean; dependencies?: string[] }>
|
||||
} {
|
||||
const modules = Array.from(this.modules.values())
|
||||
|
||||
return {
|
||||
registered: modules.length,
|
||||
installed: modules.filter(m => m.installed).length,
|
||||
failed: modules.filter(m => !m.installed && m.config.enabled).length,
|
||||
modules: modules.map(({ plugin, installed }) => ({
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
installed,
|
||||
dependencies: plugin.dependencies
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate module dependencies
|
||||
*/
|
||||
private validateDependencies(plugin: ModulePlugin): string[] {
|
||||
if (!plugin.dependencies) {
|
||||
return []
|
||||
}
|
||||
|
||||
return plugin.dependencies.filter(dep => !this.modules.has(dep))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find modules that depend on the given module
|
||||
*/
|
||||
private findDependents(moduleName: string): string[] {
|
||||
const dependents: string[] = []
|
||||
|
||||
for (const [name, registration] of this.modules) {
|
||||
if (registration.installed &&
|
||||
registration.plugin.dependencies?.includes(moduleName)) {
|
||||
dependents.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
return dependents
|
||||
}
|
||||
|
||||
/**
|
||||
* Install all enabled modules
|
||||
*/
|
||||
async installAll(): Promise<void> {
|
||||
const enabledModules = Array.from(this.modules.entries())
|
||||
.filter(([_, reg]) => reg.config.enabled && !reg.config.lazy)
|
||||
.map(([name, _]) => name)
|
||||
|
||||
// Sort by dependencies to ensure correct installation order
|
||||
const sortedModules = this.topologicalSort(enabledModules)
|
||||
|
||||
for (const moduleName of sortedModules) {
|
||||
if (!this.isInstalled(moduleName)) {
|
||||
await this.install(moduleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Topological sort for dependency resolution
|
||||
*/
|
||||
private topologicalSort(moduleNames: string[]): string[] {
|
||||
const visited = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
const result: string[] = []
|
||||
|
||||
const visit = (name: string) => {
|
||||
if (visiting.has(name)) {
|
||||
throw new Error(`Circular dependency detected involving module: ${name}`)
|
||||
}
|
||||
if (visited.has(name)) {
|
||||
return
|
||||
}
|
||||
|
||||
visiting.add(name)
|
||||
|
||||
const registration = this.modules.get(name)
|
||||
if (registration?.plugin.dependencies) {
|
||||
for (const dep of registration.plugin.dependencies) {
|
||||
if (moduleNames.includes(dep)) {
|
||||
visit(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visiting.delete(name)
|
||||
visited.add(name)
|
||||
result.push(name)
|
||||
}
|
||||
|
||||
for (const name of moduleNames) {
|
||||
if (!visited.has(name)) {
|
||||
visit(name)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Global plugin manager instance
|
||||
export const pluginManager = new PluginManager()
|
||||
93
src/core/types.ts
Normal file
93
src/core/types.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import type { App, Component } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
// Base module plugin interface
|
||||
export interface ModulePlugin {
|
||||
/** Unique module name */
|
||||
name: string
|
||||
|
||||
/** Module version */
|
||||
version: string
|
||||
|
||||
/** Required dependencies (other module names) */
|
||||
dependencies?: string[]
|
||||
|
||||
/** Module configuration */
|
||||
config?: Record<string, any>
|
||||
|
||||
/** Install the module */
|
||||
install(app: App, options?: any): Promise<void> | void
|
||||
|
||||
/** Uninstall the module (cleanup) */
|
||||
uninstall?(): Promise<void> | void
|
||||
|
||||
/** Routes provided by this module */
|
||||
routes?: RouteRecordRaw[]
|
||||
|
||||
/** Components provided by this module */
|
||||
components?: Record<string, Component>
|
||||
|
||||
/** Services provided by this module */
|
||||
services?: Record<string, any>
|
||||
|
||||
/** Composables provided by this module */
|
||||
composables?: Record<string, any>
|
||||
}
|
||||
|
||||
// Module configuration for app setup
|
||||
export interface ModuleConfig {
|
||||
/** Module name */
|
||||
name: string
|
||||
|
||||
/** Whether module is enabled */
|
||||
enabled: boolean
|
||||
|
||||
/** Module-specific configuration */
|
||||
config?: Record<string, any>
|
||||
|
||||
/** Load module dynamically */
|
||||
lazy?: boolean
|
||||
}
|
||||
|
||||
// App configuration
|
||||
export interface AppConfig {
|
||||
/** Configured modules */
|
||||
modules: Record<string, ModuleConfig>
|
||||
|
||||
/** Global app features */
|
||||
features: {
|
||||
pwa: boolean
|
||||
pushNotifications: boolean
|
||||
electronApp: boolean
|
||||
developmentMode: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Module registration info
|
||||
export interface ModuleRegistration {
|
||||
plugin: ModulePlugin
|
||||
config: ModuleConfig
|
||||
installed: boolean
|
||||
installTime?: number
|
||||
}
|
||||
|
||||
// Event system for inter-module communication
|
||||
export interface ModuleEvent {
|
||||
type: string
|
||||
source: string
|
||||
data: any
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type ModuleEventHandler = (event: ModuleEvent) => void
|
||||
|
||||
// Service injection tokens
|
||||
export type ServiceToken = string | symbol
|
||||
|
||||
// Dependency injection container interface
|
||||
export interface DIContainer {
|
||||
provide<T>(token: ServiceToken, service: T, scope?: 'singleton' | 'transient'): void
|
||||
inject<T>(token: ServiceToken): T | undefined
|
||||
remove(token: ServiceToken): boolean
|
||||
clear(): void
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue