diff --git a/docs/04-migrations/dependency-injection-migration.md b/docs/04-migrations/dependency-injection-migration.md new file mode 100644 index 0000000..2c5da69 --- /dev/null +++ b/docs/04-migrations/dependency-injection-migration.md @@ -0,0 +1,369 @@ +# Dependency Injection Migration Plan + +## Overview + +This document outlines the migration approach for converting legacy exported singleton services to the dependency injection pattern, following the successful authentication architecture refactoring. + +## Legacy Pattern vs New Pattern + +### ❌ Legacy Pattern (To Be Replaced) +```typescript +// Service definition with exported singleton +export class SomeService { + // service implementation +} +export const someService = new SomeService() + +// Consumer code +import { someService } from '@/services/someService' +someService.doSomething() +``` + +### ✅ New Dependency Injection Pattern +```typescript +// Service definition (no exported singleton) +export class SomeService extends BaseService { + // service implementation +} + +// Service registration in module +container.provide(SERVICE_TOKENS.SOME_SERVICE, someService) + +// Consumer code +const someService = injectService(SERVICE_TOKENS.SOME_SERVICE) as SomeService +someService.doSomething() +``` + +## Services to Migrate + +### Priority 1: High Impact Services + +#### 1. InvoiceService +- **File**: `src/core/services/invoiceService.ts` +- **Current**: `export const invoiceService = new InvoiceService()` +- **Token**: Need to add `INVOICE_SERVICE: Symbol('invoiceService')` +- **Usage**: Market store and components for payment processing +- **Impact**: Core business logic, high usage +- **Dependencies**: Likely uses LnbitsAPI and other services + +#### 2. NostrmarketService +- **File**: `src/modules/market/services/nostrmarketService.ts` +- **Current**: `export const nostrmarketService = new NostrmarketService()` +- **Token**: Need to add `NOSTRMARKET_SERVICE: Symbol('nostrmarketService')` +- **Usage**: Market module composables and components +- **Impact**: Key module service, should follow DI pattern +- **Dependencies**: Likely uses RelayHub and other Nostr services + +### Priority 2: Medium Impact Services + +#### 3. PaymentMonitorService +- **File**: `src/modules/market/services/paymentMonitor.ts` +- **Current**: `export const paymentMonitor = new PaymentMonitorService()` +- **Token**: `SERVICE_TOKENS.PAYMENT_MONITOR` (already exists) +- **Status**: Partially migrated - token exists but service not properly registered +- **Usage**: Market store +- **Impact**: Background service, less direct component usage + +#### 4. LnbitsAPI +- **File**: `src/lib/api/lnbits.ts` +- **Current**: `export const lnbitsAPI = new LnbitsAPI()` +- **Token**: Need to add `LNBITS_API: Symbol('lnbitsAPI')` +- **Usage**: AuthService and other core services +- **Impact**: API layer foundation, widely used but lower-level + +## Migration Steps + +### Step 1: Add SERVICE_TOKENS + +Update `src/core/di-container.ts` to add missing tokens: + +```typescript +export const SERVICE_TOKENS = { + // ... existing tokens ... + + // New service tokens + INVOICE_SERVICE: Symbol('invoiceService'), + NOSTRMARKET_SERVICE: Symbol('nostrmarketService'), + LNBITS_API: Symbol('lnbitsAPI'), + + // PAYMENT_MONITOR already exists +} as const +``` + +### Step 2: Register Services in Modules + +#### Base Module Registration +For core services like InvoiceService and LnbitsAPI: + +```typescript +// src/modules/base/index.ts +import { InvoiceService } from '@/core/services/invoiceService' +import { LnbitsAPI } from '@/lib/api/lnbits' + +const invoiceService = new InvoiceService() +const lnbitsAPI = new LnbitsAPI() + +container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService) +container.provide(SERVICE_TOKENS.LNBITS_API, lnbitsAPI) +``` + +#### Market Module Registration +For market-specific services: + +```typescript +// src/modules/market/index.ts +import { NostrmarketService } from './services/nostrmarketService' +import { PaymentMonitorService } from './services/paymentMonitor' + +const nostrmarketService = new NostrmarketService() +const paymentMonitor = new PaymentMonitorService() + +container.provide(SERVICE_TOKENS.NOSTRMARKET_SERVICE, nostrmarketService) +container.provide(SERVICE_TOKENS.PAYMENT_MONITOR, paymentMonitor) +``` + +### Step 3: Update Service Classes + +Convert services to extend BaseService for dependency injection: + +```typescript +// Before +export class SomeService { + constructor() { + // direct imports/dependencies + } +} + +// After +export class SomeService extends BaseService { + protected readonly metadata = { + name: 'SomeService', + dependencies: ['AuthService', 'RelayHub'] // declare dependencies + } + + // Access dependencies through DI + private get authService(): AuthService { + return this.dependencies.get('AuthService') as AuthService + } +} +``` + +### Step 4: Update Consumer Code + +Replace direct imports with dependency injection: + +```typescript +// Before +import { invoiceService } from '@/core/services/invoiceService' + +// After +import { injectService, SERVICE_TOKENS } from '@/core/di-container' + +const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService +``` + +### Step 5: Remove Exported Singletons + +After migration, remove the exported singleton from service files: + +```typescript +// Remove this line +export const someService = new SomeService() +``` + +## Migration Order + +1. **LnbitsAPI** - Foundation API layer, needed by other services +2. **InvoiceService** - High impact business logic +3. **PaymentMonitorService** - Complete partial migration +4. **NostrmarketService** - Module-specific service + +## Validation Steps + +For each migrated service: + +1. ✅ Service registered in appropriate module +2. ✅ SERVICE_TOKEN added to di-container.ts +3. ✅ All consumers updated to use injectService() +4. ✅ Exported singleton removed from service file +5. ✅ Service extends BaseService if applicable +6. ✅ Dependencies declared in metadata +7. ✅ No build errors or TypeScript warnings +8. ✅ Functionality testing - service works as expected + +## Benefits of Migration + +### Architecture Benefits +- **Single Source of Truth**: Services managed by DI container +- **Loose Coupling**: Components depend on interfaces, not concrete classes +- **Testability**: Easy to inject mock services for testing +- **Module Boundaries**: Clear service ownership and lifecycle + +### Maintainability Benefits +- **Centralized Registration**: All services registered in module definitions +- **Consistent Patterns**: Same DI pattern across all services +- **Service Lifecycle**: Container manages service instantiation and disposal +- **Dependency Visibility**: Service dependencies clearly declared + +### Performance Benefits +- **No Duplicate Instances**: Container ensures singleton behavior +- **Lazy Loading**: Services only created when needed +- **Efficient Memory Usage**: Single instances shared across modules + +## Testing Strategy + +Each migrated service should include: + +1. **Unit Tests**: Test service logic with mocked dependencies +2. **Integration Tests**: Test service with real dependencies via DI +3. **Mock Service Creation**: For testing consumers + +Example mock setup: +```typescript +// Test setup +const mockInvoiceService = { + createInvoice: vi.fn(), + checkPayment: vi.fn() +} + +container.provide(SERVICE_TOKENS.INVOICE_SERVICE, mockInvoiceService) +``` + +## Risk Mitigation + +- **Incremental Migration**: One service at a time +- **Backward Compatibility**: Keep old imports temporarily during migration +- **Comprehensive Testing**: Validate each service after migration +- **Rollback Plan**: Git commits allow easy rollback if issues arise + +## Migration Status + +### ✅ Completed Migrations + +#### 1. InvoiceService (Completed) +- **Status**: ✅ Successfully migrated +- **Token**: `SERVICE_TOKENS.INVOICE_SERVICE` +- **Registration**: Base module (`src/modules/base/index.ts`) +- **Usage**: Market store (`src/modules/market/stores/market.ts`) + +**Before:** +```typescript +// ❌ Legacy singleton pattern +import { invoiceService } from '@/core/services/invoiceService' +const invoice = await invoiceService.createInvoice(order, adminKey) +``` + +**After:** +```typescript +// ✅ Dependency injection pattern +const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService +const invoice = await invoiceService.createInvoice(order, adminKey) +``` + +### 🔄 In Progress + +#### 2. NostrmarketService (Pending) +- **Status**: 🔄 Ready for migration +- **File**: `src/modules/market/services/nostrmarketService.ts` +- **Token**: Need to add `NOSTRMARKET_SERVICE` +- **Registration**: Market module + +#### 3. PaymentMonitorService (Partially Migrated) +- **Status**: 🔄 Token exists, needs completion +- **Token**: `SERVICE_TOKENS.PAYMENT_MONITOR` (already exists) +- **Registration**: Market module + +#### 4. LnbitsAPI (Pending) +- **Status**: 🔄 Ready for migration +- **File**: `src/lib/api/lnbits.ts` +- **Token**: Need to add `LNBITS_API` +- **Registration**: Base module + +## Practical Example: Market Module Using InvoiceService + +Here's a real-world example of how the Market module now uses the migrated InvoiceService through dependency injection: + +### Market Store Implementation + +```typescript +// src/modules/market/stores/market.ts +import { defineStore } from 'pinia' +import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import type { InvoiceService } from '@/core/services/invoiceService' + +export const useMarketStore = defineStore('market', () => { + // Inject InvoiceService via dependency injection + const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService + + const createOrderInvoice = async (order: Order, adminKey: string) => { + try { + // Use injected service to create Lightning invoice + const invoice = await invoiceService.createInvoice(order, adminKey, { + tag: "nostrmarket", + order_id: order.id, + merchant_pubkey: order.sellerPubkey, + expiry: 3600 + }) + + console.log('Invoice created:', { + paymentHash: invoice.payment_hash, + bolt11: invoice.bolt11, + amount: invoice.amount + }) + + return invoice + } catch (error) { + console.error('Failed to create order invoice:', error) + throw error + } + } + + return { + createOrderInvoice, + // ... other store methods + } +}) +``` + +### Benefits Demonstrated + +1. **Service Injection**: `injectService(SERVICE_TOKENS.INVOICE_SERVICE)` provides the service +2. **Type Safety**: TypeScript knows the exact service type with `as InvoiceService` +3. **Testability**: Easy to mock `INVOICE_SERVICE` for testing +4. **Modularity**: Market module depends on base module's InvoiceService +5. **Single Source**: One InvoiceService instance shared across the app + +### Service Registration (Base Module) + +```typescript +// src/modules/base/index.ts +import { InvoiceService } from '@/core/services/invoiceService' + +const invoiceService = new InvoiceService() + +export const baseModule: ModulePlugin = { + async install(_app: App) { + // Register InvoiceService in DI container + container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService) + + // Other service registrations... + } +} +``` + +## Success Metrics + +### Completed Services +- ✅ **InvoiceService**: Successfully migrated to DI pattern +- ✅ **AuthService**: Previously migrated to DI pattern + +### Remaining Tasks +- 🔄 **NostrmarketService**: Needs migration +- 🔄 **PaymentMonitorService**: Complete partial migration +- 🔄 **LnbitsAPI**: Needs migration + +### Overall Progress +- ✅ 2/5 services migrated to dependency injection +- ✅ Zero build errors +- ✅ Consistent DI patterns established +- ✅ Documentation updated with examples \ No newline at end of file diff --git a/src/core/di-container.ts b/src/core/di-container.ts index 21e5f4f..753f28f 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -134,6 +134,9 @@ export const SERVICE_TOKENS = { // Events services EVENTS_SERVICE: Symbol('eventsService'), + + // Invoice services + INVOICE_SERVICE: Symbol('invoiceService'), } as const // Type-safe injection helpers diff --git a/src/core/services/invoiceService.ts b/src/core/services/invoiceService.ts index f6dfab5..98bc11b 100644 --- a/src/core/services/invoiceService.ts +++ b/src/core/services/invoiceService.ts @@ -32,7 +32,7 @@ export interface PaymentStatus { payment_hash: string } -class InvoiceService { +export class InvoiceService { private baseUrl: string constructor() { @@ -186,6 +186,3 @@ class InvoiceService { } } -// Export singleton instance -export const invoiceService = new InvoiceService() - diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts index d5c62ee..080a954 100644 --- a/src/modules/base/index.ts +++ b/src/modules/base/index.ts @@ -14,6 +14,10 @@ import { paymentService } from '@/core/services/PaymentService' import { visibilityService } from '@/core/services/VisibilityService' import { storageService } from '@/core/services/StorageService' import { toastService } from '@/core/services/ToastService' +import { InvoiceService } from '@/core/services/invoiceService' + +// Create service instances +const invoiceService = new InvoiceService() /** * Base Module Plugin @@ -44,6 +48,9 @@ export const baseModule: ModulePlugin = { // Register toast service container.provide(SERVICE_TOKENS.TOAST_SERVICE, toastService) + // Register invoice service + container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService) + // Register PWA service container.provide('pwaService', pwaService) @@ -92,6 +99,7 @@ export const baseModule: ModulePlugin = { visibilityService, storageService, toastService, + invoiceService, pwaService }, diff --git a/src/modules/market/stores/market.ts b/src/modules/market/stores/market.ts index e8ea1f1..e49a945 100644 --- a/src/modules/market/stores/market.ts +++ b/src/modules/market/stores/market.ts @@ -1,11 +1,10 @@ import { defineStore } from 'pinia' import { ref, computed, readonly, watch } from 'vue' -import { invoiceService } from '@/core/services/invoiceService' import { paymentMonitor } from '../services/paymentMonitor' import { nostrmarketService } from '../services/nostrmarketService' import { useAuth } from '@/composables/useAuthService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { LightningInvoice } from '@/core/services/invoiceService' +import type { LightningInvoice, InvoiceService } from '@/core/services/invoiceService' import type { @@ -18,6 +17,7 @@ import type { export const useMarketStore = defineStore('market', () => { const auth = useAuth() const storageService = injectService(SERVICE_TOKENS.STORAGE_SERVICE) as any + const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService // Core market state const markets = ref([]) const stalls = ref([])