- Change the active file in workspace.json to point to the new modular-design.md. - Revise modular-design.md to enhance clarity and structure, including a new table of contents and updated sections on design philosophy, architecture components, and service abstractions. - Remove outdated content and improve the overall presentation of modular design patterns and best practices. These updates aim to streamline the documentation for better accessibility and understanding of the modular architecture.
12 KiB
12 KiB
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)
// 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
// 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:
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:
// 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:
// 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:
// 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:
// 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:
// Remove this line
export const someService = new SomeService()
Migration Order
- LnbitsAPI - Foundation API layer, needed by other services
- InvoiceService - High impact business logic
- PaymentMonitorService - Complete partial migration
- NostrmarketService - Module-specific service
Validation Steps
For each migrated service:
- ✅ Service registered in appropriate module
- ✅ SERVICE_TOKEN added to di-container.ts
- ✅ All consumers updated to use injectService()
- ✅ Exported singleton removed from service file
- ✅ Service extends BaseService if applicable
- ✅ Dependencies declared in metadata
- ✅ No build errors or TypeScript warnings
- ✅ 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:
- Unit Tests: Test service logic with mocked dependencies
- Integration Tests: Test service with real dependencies via DI
- Mock Service Creation: For testing consumers
Example mock setup:
// 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:
// ❌ Legacy singleton pattern
import { invoiceService } from '@/core/services/invoiceService'
const invoice = await invoiceService.createInvoice(order, adminKey)
After:
// ✅ Dependency injection pattern
const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
const invoice = await invoiceService.createInvoice(order, adminKey)
2. NostrmarketService (Completed)
- Status: ✅ Successfully migrated
- Token:
SERVICE_TOKENS.NOSTRMARKET_SERVICE - Registration: Market module (
src/modules/market/index.ts) - Usage: Market module composables and stores
Before:
// ❌ Legacy singleton pattern
import { nostrmarketService } from '@/modules/market/services/nostrmarketService'
await nostrmarketService.publishOrder(orderData)
After:
// ✅ Dependency injection pattern
const nostrmarketService = injectService(SERVICE_TOKENS.NOSTRMARKET_SERVICE) as NostrmarketService
await nostrmarketService.publishOrder(orderData)
3. PaymentMonitorService (Completed)
- Status: ✅ Successfully migrated
- Token:
SERVICE_TOKENS.PAYMENT_MONITOR - Registration: Market module (
src/modules/market/index.ts) - Usage: Market store for payment status monitoring
4. LnbitsAPI (Completed)
- Status: ✅ Successfully migrated
- Token:
SERVICE_TOKENS.LNBITS_API - Registration: Base module (
src/modules/base/index.ts) - Usage: AuthService and events API integration
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
// 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
- Service Injection:
injectService(SERVICE_TOKENS.INVOICE_SERVICE)provides the service - Type Safety: TypeScript knows the exact service type with
as InvoiceService - Testability: Easy to mock
INVOICE_SERVICEfor testing - Modularity: Market module depends on base module's InvoiceService
- Single Source: One InvoiceService instance shared across the app
Service Registration (Base Module)
// 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