Migrate InvoiceService to dependency injection pattern
- Add INVOICE_SERVICE token to DI container - Register InvoiceService in base module with proper lifecycle - Update market store to use dependency injection instead of singleton - Remove exported singleton from InvoiceService class - Add comprehensive migration documentation with examples - Maintain type safety with proper TypeScript interfaces This migration eliminates the legacy singleton pattern and improves: - Testability through service injection - Modular architecture with clear boundaries - Single source of truth for service instances - Consistent dependency injection patterns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6cb10a31db
commit
7a32085ee1
5 changed files with 383 additions and 6 deletions
369
docs/04-migrations/dependency-injection-migration.md
Normal file
369
docs/04-migrations/dependency-injection-migration.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -134,6 +134,9 @@ export const SERVICE_TOKENS = {
|
||||||
|
|
||||||
// Events services
|
// Events services
|
||||||
EVENTS_SERVICE: Symbol('eventsService'),
|
EVENTS_SERVICE: Symbol('eventsService'),
|
||||||
|
|
||||||
|
// Invoice services
|
||||||
|
INVOICE_SERVICE: Symbol('invoiceService'),
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// Type-safe injection helpers
|
// Type-safe injection helpers
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export interface PaymentStatus {
|
||||||
payment_hash: string
|
payment_hash: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvoiceService {
|
export class InvoiceService {
|
||||||
private baseUrl: string
|
private baseUrl: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -186,6 +186,3 @@ class InvoiceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const invoiceService = new InvoiceService()
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ import { paymentService } from '@/core/services/PaymentService'
|
||||||
import { visibilityService } from '@/core/services/VisibilityService'
|
import { visibilityService } from '@/core/services/VisibilityService'
|
||||||
import { storageService } from '@/core/services/StorageService'
|
import { storageService } from '@/core/services/StorageService'
|
||||||
import { toastService } from '@/core/services/ToastService'
|
import { toastService } from '@/core/services/ToastService'
|
||||||
|
import { InvoiceService } from '@/core/services/invoiceService'
|
||||||
|
|
||||||
|
// Create service instances
|
||||||
|
const invoiceService = new InvoiceService()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base Module Plugin
|
* Base Module Plugin
|
||||||
|
|
@ -44,6 +48,9 @@ export const baseModule: ModulePlugin = {
|
||||||
// Register toast service
|
// Register toast service
|
||||||
container.provide(SERVICE_TOKENS.TOAST_SERVICE, toastService)
|
container.provide(SERVICE_TOKENS.TOAST_SERVICE, toastService)
|
||||||
|
|
||||||
|
// Register invoice service
|
||||||
|
container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService)
|
||||||
|
|
||||||
// Register PWA service
|
// Register PWA service
|
||||||
container.provide('pwaService', pwaService)
|
container.provide('pwaService', pwaService)
|
||||||
|
|
||||||
|
|
@ -92,6 +99,7 @@ export const baseModule: ModulePlugin = {
|
||||||
visibilityService,
|
visibilityService,
|
||||||
storageService,
|
storageService,
|
||||||
toastService,
|
toastService,
|
||||||
|
invoiceService,
|
||||||
pwaService
|
pwaService
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed, readonly, watch } from 'vue'
|
import { ref, computed, readonly, watch } from 'vue'
|
||||||
import { invoiceService } from '@/core/services/invoiceService'
|
|
||||||
import { paymentMonitor } from '../services/paymentMonitor'
|
import { paymentMonitor } from '../services/paymentMonitor'
|
||||||
import { nostrmarketService } from '../services/nostrmarketService'
|
import { nostrmarketService } from '../services/nostrmarketService'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
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 {
|
import type {
|
||||||
|
|
@ -18,6 +17,7 @@ import type {
|
||||||
export const useMarketStore = defineStore('market', () => {
|
export const useMarketStore = defineStore('market', () => {
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const storageService = injectService(SERVICE_TOKENS.STORAGE_SERVICE) as any
|
const storageService = injectService(SERVICE_TOKENS.STORAGE_SERVICE) as any
|
||||||
|
const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
|
||||||
// Core market state
|
// Core market state
|
||||||
const markets = ref<Market[]>([])
|
const markets = ref<Market[]>([])
|
||||||
const stalls = ref<Stall[]>([])
|
const stalls = ref<Stall[]>([])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue